写点什么

基于 STM32+ESP8266+ 华为云 IoT 设计的健康管理系统

作者:DS小龙哥
  • 2022 年 2 月 27 日
  • 本文字数:10232 字

    阅读完需:约 34 分钟

一、前言

近几年随着科技的进步和智能化浪潮的到来,智能穿戴设备也在飞速火爆发展,各种健康智能手环,智能手表、智能跑鞋、智能眼镜纷纷上市,并出现了很多针对个人家庭的健康管理设备。比如: 智能血压计、智能心率检测、脂肪秤、智能体重秤等等,都带上了智能、健康各种标签。


可穿戴设备,即直接穿在身上,或是整合到用户的衣服或配件的一种便携式设备。可穿戴设备不仅仅是一种硬件设备,更是通过软件支持以及数据交互、云端交互来实现强大的功能,可穿戴设备将会对生活、感知带来很大的转变。


这篇文章就利用 STM32 加上各种外设传感器配合华为云 IOT 物联网平台设计一个健康管理设备,通过 ESP8266+MQTT 协议将数据传输导致华为云物联网平台,并通过华为云的应用侧完成应用层软件开发;设计本项目的目的就是,上手体验华为云物联网平台,并探究一下智能设备的实现原理。


当前设计的监控管理设备支持的功能有:(1)人体温度测量(2)运动监测、计步功能(3)睡眠监测(4)心率测量


STM32 采集这些传感器数据之后,进行处理,在本地 OLED 显示屏上完成显示;再通过 ESP8266 将数据传递到华为云物联网平台,关联数据可视化大屏完成数据展示。


下面是示波器测量的心率显示



设备运行效果:



二、硬件介绍

2.1 主控芯片

主控芯片采用 STM32F103C8T6,它一款基于 ARM Cortex-M 内核 STM32 系列的 32 位的微控制器,程序存储器容量是 64KB,RAM 空间是 20K,工作电压 2V~3.6V,运行速度 72MHZ。


2.2 体温测量

人体温度测量,采用非接触式红外测温芯片 GY-MCU90615,工作电压 3-5v 功耗小,体积小。其工作原理, 是通过单片机读取红外温度度数据,串口(TTL 电平)通信方式输出。串口的波特率有 9600bps 与 115200bps 有连续输出与询问输出两种方式,可适应不同的工作环境,与所有的单片机及电脑连接。


2.3 心率测量

心率测量,采用 PulseSensor 传感器,这是一款用于脉搏心率测量的光电反射式模拟传感器,通过模拟输出口可将采集到的模拟信号传输给 STM32 单片机用来转换为数字信号,再通过单片机简单计算后就可以得到心率数值。


2.4 计步、睡眠监测功能

计步模块,睡眠监测,运动监测功能采用 MUP6050 陀螺仪实现,这是一款高性能三轴加速度+三轴陀螺仪的六轴传感器,该模块采用 InvenSense 公司的 MPU6050 芯片作为核心, 该芯片内部整合了 3 轴陀螺仪和 3 轴加速度传感器,并可利用自带的数字运动处理器硬件加速引擎,通过主 IIC 接口,向应用端输出姿态解算后的数据。有了 DMP,可以使用 InvenSense 公司提供的运动处理资料库,非常方便的实现姿态解算,降低了运动处理运算对操作系统的负荷,同时大大降低了开发难度。MPU6050 模块具有:体积小、自带 DMP、 自带温度传感器、 支持 IIC 从机地址设置和中断、兼容 3.3V/5V 系统、使用方便等特点。



(5)本地数据显示用的 OLED 显示屏采用 0.96 寸的 SPI 接口显示屏,分辨率为 128*64,主要是在本地显示采集的数据,时间等信息。



(6)上网的模块采用 ESP8266,ESP8266 是物联网领域常见无线网卡芯片,支持 AT 指令,支持串口协议控制,只需要几个简单的 AT 指令就可以完成网络连接,数据传输。当前项目里,就是通过 ESP8266 将采集的数据传递到华为云 IOT 平台,实现数据展示。


三、创建 IOT 产品、上云测试

3.1 创建产品

官网地址: https://www.huaweicloud.com/s/JeeJqeiBlOe9kSU


选择 IOTDA 进入,选择免费试用。




在产品页面,选择右上角创建产品。



根据提示,填入对应参数。



创建好之后,查看产品详情,进入属性配置页面。



选择自定义模型。



添加服务。



接下来就添加属性,属性就是传感器上传的数据类型,需要展示的数据;根据自己传感器的数量、类型自己设置即可。



添加心率传感器数据属性。



添加体温传感器数据属性。



添加计步功能的数据属性。



创建成功:


3.2 注册设备

打开设备页面,点击右上角注册设备按钮,根据提示和产品的信息填入;创建完保存得到的信息。



点击确定之后,创建成功效果如下;目前设备还未激活,需要设备登录一次服务器即可激活;接下来就是如何登录了。


3.3 设备上云测试

完成产品、设备创建之后,接下来采用 MQTT 客户端模拟设备,测试是否可以正常上华为云。


连接协议使用 MQTT 协议,MQTT 协议登录服务器,就像 QQ 登录一样,需要输入账号、密码等一些信息;下面先利用华为云的小工具完成这些数据的创建。


华为云提供的 MQTT 账户信息生成在线小工具: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/


前面两行填入的数据,在创建设备成功时提示下载的文件里有,照着填写即可。



我的设备生成的数据如下:


ClientId   61df9a6bc7fb24029b0c160d_1126626497_0_0_2022011303Username   61df9a6bc7fb24029b0c160d_1126626497Password   20618c172eb24418e0910804889c7d2074a5847e9e7205a41a8bf5adeec399f9
复制代码


华为云 IOT 平台的 MQTT 服务器地址信息如下:


端口: 1883域名: a161a58a78.iot-mqtts.cn-north-4.myhuaweicloud.comIP地址: 121.36.42.100
复制代码


华为云 IOT 平台 MQTT 协议订阅主题的格式:


格式: $oc/devices/{device_id}/sys/messages/down//订阅主题: 平台下发消息给设备$oc/devices/61df9a6bc7fb24029b0c160d_1126626497/sys/messages/down
复制代码


华为云 IOT 平台 MQTT 协议上报主题的格式:


格式: $oc/devices/{device_id}/sys/properties/report//设备上报主题请求$oc/devices/61df9a6bc7fb24029b0c160d_1126626497/sys/properties/report

//上报的数据格式如下{"services": [{"service_id": "healthy","properties":{"HeartRate":127}},{"service_id": "healthy","properties":{"motion":2000}},{"service_id": "healthy","properties":{"temperature":36.2}}]}
复制代码


打开 MQTT 客户端,填入对应数据,连接华为云物联网平台:


如需使用和我一样的同款软件,打开百度搜索MQTT客户端_v2.4(协议3.1.1).exe 即可找到下载地址。



登录成功后,查看华为云页面,可以看到设备已经在线,并且上传的数据已经展示出来。


四、应用侧软件开发

4.1 功能介绍

为了更方便的展示设备数据,与设备完成交互,还需要开发一个配套的上位机,官方提供了应用侧开发的 API 接口、SDK 接口,为了方便通用一点,我这里采用了 API 接口完成数据交互,上位机软件采用 QT 开发。


帮助文档地址: https://support.huaweicloud.com/usermanual-iothub/iot_01_0045.html


4.2 查询设备属性接口

设备属性就是设备上传的传感器状态数据信息,应用侧提供了 API 接口,可以主动向设备端下发请求指令;设备端收到指令之后需要按照约定的数据格式上报数据;所以,要实现应用层与设备端的数据交互,需要应用层与设备端配合才能完成。


下面分别介绍应用测和设备测的实现流程。


(1)应用层下发的指令


帮助文档地址: https://support.huaweicloud.com/api-iothub/iot_06_v5_0034.html


接口的在线调试地址: https://apiexplorer.developer.huaweicloud.com/apiexplorer/debug?product=IoTDA&api=ListProperties


如果请求参数和返回值不清楚,写代码前,先使用在线调试接口体验一下,验证数据交互是否 OK。



请求参数里比较总要的两个必填参数,是设备 ID 和服务 ID,这两个参数在第 3 章节就介绍过如何获取了,在产品页面创建自定义属性时可以看到服务 ID。



请求接口总结:


请求方法 GETURI地址  /v5/iot/{project_id}/devices/{device_id}/properties传输协议 HTTPS
拼接好的地址: https://iotda.cn-north-4.myhuaweicloud.com/v5/iot/0e5957be8a00f53c2fa7c0045e4d8fbf/devices/61df9a6bc7fb24029b0c160d_1126626497/properties?service_id=1126626497
其中的project_id和device_id需要根据自己的设备信息修改。

请求头:{ "User-Agent": "API Explorer", "X-Auth-Token": "******", 这个是鉴权用的token "Content-Type": "application/json"}

响应体(设备上传的数据){ "response": { "services": [ { "service_id": "healthy", "properties": { "HeartRate": 127 } }, { "service_id": "healthy", "properties": { "motion": 2000 } }, { "service_id": "healthy", "properties": { "temperature": 36.2 } } ] }}
复制代码


请求头里需要填X-Subject-Token参数,这个参数只要是访问任何华为云都需要填,获取具体的流程可以看这里。https://bbs.huaweicloud.com/blogs/317759 翻到第 3 小节。


(2)设备上传数据


应用层向设备端请求查询设备属性时,设备端会收到如下的消息:


$oc/devices/61df9a6bc7fb24029b0c160d_1126626497/sys/properties/get/request_id=336bcb57-0e0a-44d0-90f7-31386cb54a3c{"service_id":"1126626497"}
复制代码


这个消息里有一个主要参数request_id请求 ID,设备端需要解析出这个参数,给应用层响应数据时,需要带上这个 ID。


这个请求属性详细帮助文档看这里: https://support.huaweicloud.com/api-iothub/iot_06_v5_3011.html



设备响应的数据格式:


主题格式: $oc/devices/{device_id}/sys/properties/get/response/request_id={request_id}
示 例:$oc/devices/61df9a6bc7fb24029b0c160d_1126626497/sys/properties/get/response/request_id=336bcb57-0e0a-44d0-90f7-31386cb54a3c
响应的数据格式:{"services": [{"service_id": "healthy","properties":{"HeartRate":127}},{"service_id": "healthy","properties":{"motion":2000}},{"service_id": "healthy","properties":{"temperature":36.2}}]}
复制代码


响应的数据格式可以看这里的介绍: https://support.huaweicloud.com/api-iothub/iot_06_v5_3010.html


4.3 在线 API 调试结合设备模拟

下面使用 MQTT 客户端与在线 API 接口联合模拟一下接口效果:


(1)先打开调试页面: https://apiexplorer.developer.huaweicloud.com/apiexplorer/debug?product=IoTDA&api=ListProperties


然后填好设备 DI 和服务 ID:



(2)、打开 MQTT 客户端,登录华为云物联网平台(也就是模拟设备上线):



(3)、打开在线 API 调试页面,点击调试: 点击后可以看到页面上已经在等待客户端的响应了。



(4)、MQTT 客户端响应详细


按照前面说的响应格式,拼接好接口,数据。然后发布主题。



(5)、应用层收到客户端响应,调试成功


调试成功后,响应体里收到的就是设备端上传的设备属性数据。


4.4 应用层核心代码

/*功能: 获取token*/void Widget::GetToken(){    //表示获取token    function_select=3;
QString requestUrl; QNetworkRequest request;
//设置请求地址 QUrl url;
//获取token请求地址 requestUrl = QString("https://iam.%1.myhuaweicloud.com/v3/auth/tokens") .arg(SERVER_ID);
//自己创建的TCP服务器,测试用 //requestUrl="http://10.0.0.6:8080";
//设置数据提交格式 request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8"));
//构造请求 url.setUrl(requestUrl);
request.setUrl(url);
QString text =QString("{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":" "{\"user\":{\"domain\": {" "\"name\":\"%1\"},\"name\": \"%2\",\"password\": \"%3\"}}}," "\"scope\":{\"project\":{\"name\":\"%4\"}}}}") .arg(MAIN_USER) .arg(IAM_USER) .arg(IAM_PASSWORD) .arg(SERVER_ID);
//发送请求 manager->post(request, text.toUtf8());}
//查询设备属性void Widget::Get_device_properties(){ //表示获取token function_select=0;
QString requestUrl; QNetworkRequest request;
//设置请求地址 QUrl url;
//获取token请求地址 requestUrl = QString("https://iotda.%1.myhuaweicloud.com/v5/iot/%2/devices/%3/properties?service_id=%4") .arg(SERVER_ID) .arg(PROJECT_ID) .arg(device_id) .arg(service_id);
//自己创建的TCP服务器,测试用 //requestUrl="http://10.0.0.6:8080";
//设置数据提交格式 request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));
//设置token request.setRawHeader("X-Auth-Token",Token);
//构造请求 url.setUrl(requestUrl);
request.setUrl(url);
//发送请求 manager->get(request);}
复制代码


五、设备底层开发

下面列出 STM32 设备底层端的一些传感器核心处理代码。

5.1 心率采集计算算法

int BPM;                          // 用于保存脉冲速率int Signal;                      // 持有的原始数据int IBI = 600;                   unsigned char Pulse = false;    unsigned char QS = false;        int rate[10];                    unsigned long sampleCounter = 0; unsigned long lastBeatTime = 0;  int P =512;                      int T = 512;                     int thresh = 512;               int amp = 100;                  unsigned char firstBeat = true;  unsigned char secondBeat = false;/*  定时器2中断服务函数 用于周期性采集心率值*/void TIM2_IRQHandler(void){  uint16_t runningTotal=0;  uint8_t i;  uint16_t Num;    if(TIM2->SR&1<<0)  {      //读取到的值右移2位,12位-->10位    Signal = Get_AdcCHx_DATA(1)>>2;           sampleCounter += 2;                              Num = sampleCounter - lastBeatTime; 
//发现脉冲波的波峰和波谷 // find the peak and trough of the pulse wave if(Signal < thresh && Num > (IBI/5)*3) { if (Signal < T) { T = Signal; } }
if(Signal > thresh && Signal > P) { P = Signal; }
//开始寻找心跳 //当脉冲来临的时候,signal的值会上升 if (Num > 250) { if ( (Signal > thresh) && (Pulse == false) && (Num > (IBI/5)*3) ) { Pulse = true; //LED0(0); IBI = sampleCounter - lastBeatTime; lastBeatTime = sampleCounter;
if(secondBeat) { secondBeat = false; for(i=0; i<=9; i++) { rate[i] = IBI; } }
if(firstBeat) { firstBeat = false; secondBeat = true; return; }
for(i=0; i<=8; i++) { rate[i] = rate[i+1]; runningTotal += rate[i]; }
rate[9] = IBI; runningTotal += rate[9]; runningTotal /= 10; BPM = 60000/runningTotal; QS = true; } }
//脉冲开始下降 if (Signal < thresh && Pulse == true) { Pulse = false; amp = P - T; thresh = amp/2 + T; P = thresh; T = thresh; }
//没有检测到脉冲,设置默认值 if (Num > 2500) { thresh = 512; P = 512; T = 512; lastBeatTime = sampleCounter; firstBeat = true; secondBeat = false; } } TIM2->SR&=0x0; //清中断标志}
复制代码

5.2 OLED 关键代码

//向SSD1106写入一个字节。//dat:要写入的数据/命令//cmd:数据/命令标志 0,表示命令;1,表示数据;void OLED_WR_Byte(u8 dat,u8 cmd){    u8 i;          if(cmd)    OLED_DC_Set();  else     OLED_DC_Clr();        OLED_CS_Clr();  for(i=0;i<8;i++)  {            OLED_SCLK_Clr();    if(dat&0x80)       OLED_SDIN_Set();    else        OLED_SDIN_Clr();    OLED_SCLK_Set();    dat<<=1;     }                 OLED_CS_Set();  OLED_DC_Set();       } 
//设置坐标的位置(x范围: 0~127 , y的范围:0~63)//注意: 8 行为一页,共 64 行即 8 页void OLED_Set_Pos(unsigned char x, unsigned char y) { OLED_WR_Byte(0xb0+y,OLED_CMD); OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD); OLED_WR_Byte((x&0x0f)|0x01,OLED_CMD); }
复制代码

5.3 体温采集换算

u8 Receive_ok;u8 rebuf[20]={0};void RxTempInfo(void){   static uint8_t i=0;  if(USART2->SR&1<<5)     //判断接收标志  {    rebuf[i++]=USART2->DR;//读取串口数据,同时清接收标志    if(rebuf[0]!=0x5a)    //帧头不对      i=0;      if((i==2)&&(rebuf[1]!=0x5a))//帧头不对      i=0;      if(i>3)//i等于4时,已经接收到数据量字节rebuf[3]    {      if(i!=(rebuf[3]+5))//判断是否接收一帧数据完毕        return ;        switch(rebuf[2])   //接收完毕后处理      {        case 0x45:          if(!Receive_ok)//当数据处理完成后才接收新的数据          {             Receive_ok=1;//接收完成标志          }          break;        case 0x15:break;        case 0x35:break;      }      i=0;//缓存清0    }  }}
void GetTempInfo(void){ float TO=0,TA=0; u8 sum=0,i=0; for(sum=0,i=0;i<(rebuf[3]+4);i++) { sum+=rebuf[i]; } if(sum==rebuf[i])//校验和判断 { TO=(float)((rebuf[4]<<8)|rebuf[5])/100; //得到真实温度 TA=(float)((rebuf[6]<<8)|rebuf[7])/100; //得到真实温度 } printf("TO: %f\r\n",TO); printf("TA: %f\r\n",TA);}
复制代码

5.4 运动计步算法

/******************************************************************************** LOCAL VARIABLES*///存放三轴数据  float oriValues[3] = {0};    //用于存放计算阈值的波峰波谷差值  float tempValue[VALUE_NUM] ={0};  int tempCount = 0;  //是否上升的标志位  u8 isDirectionUp = FALSE;  //持续上升次数  int continueUpCount = 0;  //上一点的持续上升的次数,为了记录波峰的上升次数  int continueUpFormerCount = 0;  //上一点的状态,上升还是下降  u8 lastStatus = FALSE;  //波峰值  float peakOfWave = 0;  //波谷值  float valleyOfWave = 0;  //此次波峰的时间  long timeOfThisPeak = 0;  //上次波峰的时间  long timeOfLastPeak = 0;  //当前的时间  long timeOfNow = 0;  //当前传感器的值  float gravityNew = 0;  //上次传感器的值  float gravityOld = 0;  //动态阈值需要动态的数据,这个值用于这些动态数据的阈值  float initialValue = (float) 1.3;  //初始阈值  float ThreadValue = (float) 2.0;//三轴轴值accValue_t accValue;//行走信息:卡路里、里程、步数static sportsInfo_t sportsInfo;//计步缓存static u8 stepTempCount =0;

/******************************************************************************** 函数名:DetectorNewStep* 功能描述: * 步伐更新:如果检测到了波峰,并且符合时间差以及阈值的条件,则判定为1步 * 阀值更新:符合时间差条件,波峰波谷差值大于initialValue,则将该差值纳入阈值的计算中 * 参数说明: 输入:values:经过处理的G-sensor数据timeStamp_p:时间戳* 返回值说明:* 修改记录:sportsInfo_t *onSensorChanged(accValue_t *pAccValue,timeStamp_t *timeStamp_p,personInfo_t * personInfo)*******************************************************************************/sportsInfo_t *DetectorNewStep(float values,timeStamp_t *timeStamp_p,personInfo_t * personInfo) { static u32 time_old; personInfo_t *userInfo = personInfo; static u32 step_per_2_second; //每两秒所走的步数 float step_lenth,walk_speed,walk_distance,Calories;//步长 u32 time_now; timeStamp_t *time_p = timeStamp_p; if (gravityOld == 0) { gravityOld = values; } else { if (DetectorPeak(values, gravityOld))//检测到波峰 { timeOfLastPeak = timeOfThisPeak;//更新上次波峰的时间 //将时间戳转换为以毫秒ms为单位 time_now = timeOfNow = ((time_p->hour*60+time_p->minute)*60+time_p->second)*1000+time_p->twentyMsCount*20; //获取时间 ,并转化为毫秒 //如果检测到了波峰,并且符合时间差以及阈值的条件,则判定为1步 if ( (timeOfNow - timeOfLastPeak >= 250 )//Jahol Fan 修改为300,防止轻微动都也会检测步子 //&& (timeOfNow - timeOfLastPeak <= 2000) &&(peakOfWave - valleyOfWave >= ThreadValue) ) { timeOfThisPeak = timeOfNow; //更新此次波峰时间 stepTempCount++;//Jahol:加1为两步 step_per_2_second ++; //Jahol:这样计算卡路里,不能滤除人为的误操作,导致的结果是:里程和卡路里偏大 if((time_now - time_old) >= 2000 ) //如果时间过了2秒 {
if( 1 == step_per_2_second ) { step_lenth = userInfo->height/5; } else if( 2 == step_per_2_second ) { step_lenth = userInfo->height/4; } else if( 3 == step_per_2_second ) { step_lenth = userInfo->height/3; } else if( 4 == step_per_2_second ) { step_lenth = userInfo->height/2; } else if(5 == step_per_2_second) //Jahol:为了使计步准确,设置上限值为5步,牺牲卡路里准确性 { step_lenth = userInfo->height/1.2f; } else if( 7 == step_per_2_second ) { step_lenth = userInfo->height; } else if(step_per_2_second >= 8) // step_diff>8 { step_lenth = userInfo->height*1.2f; } else { step_lenth = 0; } walk_speed = step_per_2_second*step_lenth/2; //速度 ,单位:米/秒 walk_distance = step_per_2_second*step_lenth; //行走距离,单位:米 Calories = 4.5f*walk_speed*(userInfo->weight/2)/1800; //Jahol:weight是以kg为单位 sportsInfo.calories += Calories; sportsInfo.distance += walk_distance; time_old = time_now; //更新时间 step_per_2_second = 0; } else { //do nothing } /* * 处理无效运动: * 1.连续记录5才开始计步 * 2.例如记录的步用户停住超过3秒,则前面的记录失效,下次从头开始 * 3.连续4记录了步用户还在运动,之前的数据才有效 * */ if ((stepTempCount< 5 )&&(timeOfNow - timeOfLastPeak >= 3000)) { stepTempCount = 0; } else if((stepTempCount>= 5)&&(timeOfNow - timeOfLastPeak <= 3000)) { sportsInfo.stepCount += stepTempCount; stepTempCount = 0; } else { //do nothing } } //Jahol:更新阀值,问题:阀值不会一直变大,不能变小? if (timeOfNow - timeOfLastPeak >= 250 && (peakOfWave - valleyOfWave >= initialValue)) { timeOfThisPeak = timeOfNow; ThreadValue = Peak_Valley_Thread(peakOfWave - valleyOfWave);//更新阀值 } } } gravityOld = values; return &sportsInfo;}
复制代码


项目源码: https://download.csdn.net/download/xiaolong1126626497/81993720

发布于: 刚刚阅读数: 2
用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域

评论

发布
暂无评论
基于STM32+ESP8266+华为云IoT设计的健康管理系统