Funpack3-3 X-NUCLEO-IKS4A1——交互式传感器数据可视化
该项目使用了PyQt5框架,X-NUCLEO-IKS4A1开发板,实现了交互式传感器数据可视化的设计,它的主要功能为:板卡传感器数据的读取;板卡选择和切换显示的,上位机可视化传感器数据。
标签
Funpack活动
上位机
pyqt5
数据可视化
X-NUCLEO-IKS4A1
TetraPak
更新2024-07-08
37

项目背景

功能介绍

任务描述

  • 使用板卡上的触摸按键,实现点按和左右滑动,实现传感器选择和切换
  • 将数据发送到上位机
  • 上位机完成传感器数据的功能选择和可视化


硬件介绍

运动和环境传感器拓展板

image.png

X-NUCLEO-IKS4A1 集成板配备了多种MEMS传感器,包括LSM6DSO16IS和LSM6DSV16X的3轴加速度和3轴角速度传感器、LIS2MDL三轴磁力计、LIS2DUXS12三轴加速计、LPS22DF压力传感器、STTS22H温度传感器和SHT40AD1B湿度温度传感器,能够在紧凑的空间内提供加速度、磁力、压力、温度等多维度测量。此外,板卡还配备了Qvar触摸/滑动电极,支持开发高度互动和响应式的应用。该板卡通过IIC协议与外界通讯,使得集成和应用开发更为便捷。

设计思路

主要困难

此小节主要分析功能需求,实现该项目会面临的困难。

从任务描述中可以看出,实现该任务有三个核心功能:

  1. 获取多种传感器数据并通过串口发送至上位机,此部分功能示例软件已经提供。
  2. 驱动触摸按键,获取用户触摸行为,将选择展示传感器信息传递给上位机。
  3. 上位机中传感器数据可视化的UI设计与显示逻辑。

解决思路

此小节针对面临的困难,提出解决方案(拟使用的传感器及通信协议)。

整体思路如图所示。

核心功能一

捕获用户点按和左右滑动的操作

主要使用传感器:MEMS 3D加速度计+ 3D陀螺仪LSM6DSV16X)、其他运动传感器和环境传感器。

配置LSM6DSV16X的Qvar功能。打开X-NUCLEO-IKS4A1的文档,可以看到Qvar接口与传感器的连接方式

image.png

可以看到,可以通过 LSM6DSV16X使用装备的 Qvar 滑动电极。

板载配置为:使用跳线帽连接J4和J5的3-4端口

  • J4: 3-4 (HUB1_SDx = QVAR1)
  • J5: 3-4 (HUB1_Scx = QVAR2)

单片机iic,usart等外设初始化。传感器初始化,传感器采样时间为1s,并通过串口发送到上位机。

每隔0.1s获取qvar值,完成用户动作检测,当用户触发上滑,下滑,双击动作时,发送控制信号至上位机。

根据ST提供的传感器数据手册的描述,通过设置 CTRL7(16h)寄存器中的 AH_QVAR_EN 位为 1,可以激活 Qvar 通道;可以通过STATUS_REG (1Eh)的AH_QVARDA获取数据是否准备好;Qvar 数据以 16 位二进制补码形式提供,在 AH_QVAR_OUT_L(3Ah)和 AH_QVAR_OUT_H(3Bh)寄存器中以固定 240 Hz 的速率输出。

  1. 硬件连接 使用跳线帽连接J4和J5的3-4端口
  1. 配置传感器

阅读6.8 Qvar functionality章节,首先配置传感器功能,设置传感器内部寄存器的值。

通过CTRL1 (10h)设置加速度速率为120Hz,以达到加速度计为高性能的要求。

设置CTRL7(16h)寄存器中的 AH_QVAR_EN 位为 1。

  1. 获取Qvar数据

读取AH_QVAR_OUT_L(3Ah)和 AH_QVAR_OUT_H(3Bh)这两个寄存器,来获取Qvar 数据。

  1. 用户行为捕获

根据Qvar数据的变化特征,来判断用户是否在触摸板上完成手指的移动。采样Qvar的数据波形,识别用户是否开始点按和左右滑动。采样率也会导致识别行为的可靠性。向左滑动选择拟展示上一个传感器,向右滑动拟展示下一个传感器,双击后才切换显示所选择传感器的数据。


核心功能二

qt的ui设计,采用左右布局,左边为X-NUCLEO-IKS4A1板载传感器的名称,右边展示所选传感器的数据。左边布局可供用户鼠标点击,也可根据用户在开发套件中的行为更换样式。具体地,当用户在静电传感器上滑动时,按钮的样式会随着发生改变。电脑使用的COM口为COM9。


核心功能三

使用串口方式,开发板与上位机完成通信。主要传输传感器数据和用户行为。串口数据格式根据传感器种类不同分为二类,分别是命令类和数据类。

命令类为用户的上滑,下滑,双击等操作,主要用来控制上位机UI显示不同的传感器显示页面。

命令类格式为CMD:xx,其中,xx可能为up、down、double_click。

各种传感器的数据通过字符串发送,数据类格式如下:

Motion sensor instance 1:ACC_X: 15, ACC_Y: -20, ACC_Z: 1008
Motion sensor instance 1:GYR_X: 0, GYR_Y: 140, GYR_Z: 350
Motion sensor instance 2:ACC_X: -25, ACC_Y: -18, ACC_Z: 1010
Motion sensor instance 3:ACC_X: -21, ACC_Y: -25, ACC_Z: 1024
Motion sensor instance 3:GYR_X: 2590, GYR_Y: -3990, GYR_Z: -1960
Environmental sensor instance 0:Temp: +27.27 degC
Environmental sensor instance 1:Temp: +27.22 degC
Environmental sensor instance 1:Press: 1010.59 hPa
Environmental sensor instance 2:Hum: 43.87 %
Environmental sensor instance 2:Temp: +27.07 degC

运动传感器的字符串形如Motion sensor instance 0:MAG_X: -393, MAG_Y: -417, MAG_Z: -46

有效信息为传感器id,传感器类型(磁力计、加速度计、陀螺仪),三轴物理量数值

环境传感器的字符串形如Environmental sensor instance 0:Temp: +27.28 degC

有效信息为传感器id,传感器类型(温度、湿度、气压),环境物理量数值

上位机接收并通过正则表达式解析串口数据,控制UI页面的变化。

遇到的问题:停止串口接收时,上位机闪退,需要在串口数据接收前判断串口是否关闭。


软件流程图

此小节主要描述解决方案的软件实现,提出实现项目功能的技术路线。

上位机程序流程图

开发板程序流程图

功能展示

核心代码片段及说明

MCU核心代码

初始化外设

int main(void)
{
//...
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_I2C1_Init();
MX_MEMS_Init();
}

初始化传感器

void MX_IKS4A1_DataLogTerminal_Init(void)
{
//...
IKS4A1_MOTION_SENSOR_Init(IKS4A1_LSM6DSV16X_0, MOTION_ACCELERO | MOTION_GYRO); // 1
  IKS4A1_MOTION_SENSOR_Init(IKS4A1_LSM6DSO16IS_0, MOTION_ACCELERO | MOTION_GYRO); // 3
  IKS4A1_MOTION_SENSOR_Init(IKS4A1_LIS2DUXS12_0, MOTION_ACCELERO); // 2
  IKS4A1_MOTION_SENSOR_Init(IKS4A1_LIS2MDL_0, MOTION_MAGNETO); // 0
IKS4A1_ENV_SENSOR_Init(IKS4A1_SHT40AD1B_0, ENV_TEMPERATURE | ENV_HUMIDITY); // 2
  IKS4A1_ENV_SENSOR_Init(IKS4A1_LPS22DF_0, ENV_TEMPERATURE | ENV_PRESSURE); // 1
  IKS4A1_ENV_SENSOR_Init(IKS4A1_STTS22H_0, ENV_TEMPERATURE); // 0
//...
}


初始化Qvar传感器功能

/* Enable Qvar functionality */
  lsm6dsv16x_ah_qvar_mode_t mode;
  mode.ah_qvar_en = 1;

  if (lsm6dsv16x_ah_qvar_mode_set(&(pObj->Ctx), mode) != LSM6DSV16X_OK)
  {
    return LSM6DSV16X_ERROR;
  }


获取Qvar值

LSM6DSV16X_Object_t* pObj = LSM6DSV16X_GetQvar();
lsm6dsv16x_all_sources_get(&(pObj->Ctx), &all_sources);
if ( all_sources.drdy_ah_qvar ) {
  lsm6dsv16x_ah_qvar_raw_get(&(pObj->Ctx), &qvar_data);
  sprintf((char*)tx_buffer,"QVAR [mV]:%6.2f\r\n", lsm6dsv16x_from_lsb_to_mv(qvar_data));
  printf((char*)tx_buffer);
}


用户点按和滑动行为捕获

每隔50ms判断Qvar值的变化,从而识别用户触摸状态

void MX_IKS4A1_DataLogTerminal_Process(void)
{

// 定义变量
static float_t t1 = 0;
  static float_t t2 = 0;
  static uint8_t t = 0;
  static uint8_t t_click = 0;
  static uint8_t t_sensor = 0;
  static uint8_t flag_push = 0;
  static uint8_t flag_click = 0;
  static uint8_t flag_double_click = 0;

t1 = t2; // 获得上一个时刻Qvar的值
//...
t2 = lsm6dsv16x_from_lsb_to_mv(qvar_data);
// 状态判断
if (t2 < -50 && t1 > 50)
  {
    printf("CMD:up\r\n"); // 左滑
    flag_push = 0;
   
  }
  else if (t2 > 50 && t1 < -50)
  {
    printf("CMD:down\r\n"); // 右滑
    flag_push = 0;
  }
  else if ((-10 < t2 && t2 < 10) && (t1 < -50 || t1 > 50))
  {
    // printf("release\r\n");
    t2 = 0; t1 = 0;
    if (flag_click && flag_push && t_click < 10)
    {
      flag_double_click = 1;
      printf("CMD:double_click\r\n"); // 双击
      t = 0;
      flag_click = 0;
    }
    else if(flag_push && t < 5) // 一秒内按下并抬起
    {
      flag_click = 1;
      t_click = 0;
      // printf("click\r\n");
    }
    flag_push = 0;
  }
  else if ((-10 < t1 && t1 < 10) && (t2 < -50 || t2 > 50))
  {
    // printf("push\r\n");
    flag_push = 1;
    t = 0;
  }
 
  // 1s发送一次传感器数据
  if (t_sensor == 20){
    t_sensor = 0;
    //...
  }

  HAL_Delay( 50 );
  t_sensor++;
  t++;
  t_click ++; // 从第一次点击后开始计时
  if (t > 30){ // 长时间不点击,重置变量
    t = 0;
    t_click = 0;
    flag_click = 0;
    flag_double_click = 0;
    flag_push = 0;
  }
}


上位机核心代码(Python)

更新所选择传感器的按钮背景为黄色

# ...
def update_selected_button_styles(self, button_index):
self.selected_button_index = button_index
# 清除所有按钮的高亮样式
default_style = "QPushButton { border: 1px solid rgb(124, 124, 124); background-color: none; border-radius:2px;}"
for button in self.display_pushButtons:
button.setStyleSheet(default_style)

# 设置当前选中的按钮的高亮样式
active_style = "QPushButton { border: 1px solid rgb(124, 124, 124); background-color: yellow; border-radius:2px;}"
self.display_pushButtons[self.selected_button_index].setStyleSheet(active_style)
self.current_button_index = self.selected_button_index
self.stackedWidget.setCurrentIndex(button_index)

更新待选择传感器的按钮边框为虚线

# ...
def update_current_button_styles(self):
default_style = "QPushButton { border: 1px solid rgb(124, 124, 124); background-color: none; border-radius:2px;}"
if self.same_flag:
active_style = "QPushButton { border: 1px solid rgb(124, 124, 124); background-color: yellow; border-radius:2px;}"
self.display_pushButtons[self.selected_button_index].setStyleSheet(active_style)
self.same_flag = 0
for i, button in enumerate(self.display_pushButtons):
if i == self.current_button_index:
if self.current_button_index == self.selected_button_index:
active_style = "QPushButton {border: 1px dashed #000000; background-color: yellow;}"
self.same_flag = 1
else:
active_style = "QPushButton {border: 1px dashed #000000; background-color: none;}"

button.setStyleSheet(active_style)
elif i != self.selected_button_index:

button.setStyleSheet(default_style)

开启串口接收并更新传感器数值

电脑使用的COM口为COM9,根据实际情况进行修改

		def start_serial(self):
self.serial_manager = SerialManager(com="COM9")
self.serial_manager.command_received.connect(self.update_cmd_display)
self.serial_manager.env_data_received.connect(self.update_env_display)
self.serial_manager.motion_data_received.connect(self.update_motion_display)
self.serial_manager.start()
def update_env_display(self, sensor_id, sensor_type, data_value):
if sensor_id == 0: # STTS22H button 7
if sensor_type == "Temp":
self.textEdit7_1.setText(str(data_value))
elif sensor_id == 1: # LPS22DF button 6
if sensor_type == "Temp":
self.textEdit6_1.setText(str(data_value))
elif sensor_type == "Press":
self.textEdit6_2.setText(str(data_value))
# ...

使用正则表达式解析关键信息

def run(self):
pattern_motion = r"Motion sensor instance (\d+):([A-Z]+)_X: (-?\d+), ([A-Z]+)_Y: (-?\d+), ([A-Z]+)_Z: (-?\d+)"
pattern_env = r"Environmental sensor instance (\d+):([A-Za-z]+): ([\+\-\d\.]+) ([a-zA-Z%]+)"
self.ser = serial.Serial(self.COM, baudrate=115200, timeout=1, parity=serial.PARITY_NONE, rtscts=0)
self.sio = io.TextIOWrapper(io.BufferedRWPair(self.ser, self.ser))
self.running = True
while self.running and self.sio:
line = self.sio.readline().strip() # Remove leading/trailing whitespace
if line.startswith("CMD:"):
data = line[4:]
self.command_received.emit(data)
else:
# match_env = re.search(pattern_env, line)
match_env = re.findall(pattern_env, line)
if match_env:
sensor_id = match_env[0][0]
sensor_type = match_env[0][1]
data_value = match_env[0][2]
self.env_data_received.emit(int(sensor_id), sensor_type, float(data_value))
else:
match_motion = re.findall(pattern_motion, line)
if match_motion:
sensor_id = match_motion[0][0]
sensor_type = match_motion[0][1]
values = [int(match_motion[0][2]), int(match_motion[0][4]), int(match_motion[0][6])]
self.motion_data_received.emit(int(sensor_id), sensor_type, values)

实现效果

上位机的显示布局效果如下图所示。



用户有两种途径切换不同传感器的数据展示页面,一是直接在上位机中点击相应按钮,而是通过左右滑动和双击静电传感器来选择。当用户左右滑动时,待选定的按钮样式发生变化,按钮的外边框为虚线,当用户双击时,按钮背景变黄,并在右边页面显示相关传感器数据。


UI中显示了以下关键信息

  1. 运动和环境传感器名称
  2. 各种物理量的测量数据
  3. 开启和关闭串口的按钮
  4. 按钮的样式可代表当前选择的传感器和待选定的传感器。

可实现开启或关闭串口功能,具体测试效果可观看演示视频。

总结

本项目依托X-NUCLEO-IKS4A1和NUCLEO-G0B1平台,完成交互式传感器数据可视化功能。在此过程中阅读传感器数据手册和平台驱动代码,分别编写MCU端的驱动代码和上位机的UI逻辑代码,完成串口通信和数据解析,最终顺利完成任务。

附件下载
keil_Funpack3_3.zip
MCU代码
Funpack3-3上位机.zip
PyQt5上位机代码
团队介绍
电子爱好者
团队成员
TetraPak
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号