学向勤中得,萤窗万卷书。这篇文章主要讲述#yyds干货盘点# 基于STM32+ESP8266+华为云IoT设计的健康管理系统并完成应用侧开发相关的知识,希望能为你提供帮助。
一、前言近几年随着科技的进步和智能化浪潮的到来,智能穿戴设备也在飞速火爆发展,各种健康智能手环,智能手表、智能跑鞋、智能眼镜纷纷上市,并出现了很多针对个人家庭的健康管理设备。比如:智能血压计、智能心率检测、脂肪秤、智能体重秤等等,都带上了智能、健康各种标签。
可穿戴设备,即直接穿在身上,或是整合到用户的衣服或配件的一种便携式设备。可穿戴设备不仅仅是一种硬件设备,更是通过软件支持以及数据交互、云端交互来实现强大的功能,可穿戴设备将会对生活、感知带来很大的转变。
这篇文章就利用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/
前面两行填入的数据,在创建设备成功时提示下载的文件里有,照着填写即可。
文章图片
我的设备生成的数据如下:
ClientId61df9a6bc7fb24029b0c160d_1126626497_0_0_2022011303
Username61df9a6bc7fb24029b0c160d_1126626497
Password20618c172eb24418e0910804889c7d2074a5847e9e7205a41a8bf5adeec399f9
华为云IOT平台的MQTT服务器地址信息如下:
端口: 1883
域名: a161a58a78.iot-mqtts.cn-north-4.myhuaweicloud.com
IP地址: 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。
文章图片
请求接口总结:
请求方法 GET
URI地址/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);
文章图片
五、设备底层开发【#yyds干货盘点# 基于STM32+ESP8266+华为云IoT设计的健康管理系统并完成应用侧开发】下面列出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;
//缓存清0void 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 = https://www.songbingjia.com/android/(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>
8step_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;
推荐阅读
- 开发之痛(稳定的测试环境,怎么就那么难 | 研发效能提升36计)
- #yyds干货盘点#Android C++系列(JNI引用管理)
- #yyds干货盘点# python scrapy 管道学习,并拿在行练手爬虫项目
- #yyds干货盘点# 如何保障你的 Kubernetes 集群资源不会被打爆(18)
- Python训练营Python每日一练----第29天:有理数的循环节
- NoSQL之Redis配置与优化(后半段)
- # yyds干货盘点 # Python实现(与其穷举构造字典,不如直接用《百家姓》返回其index)
- linux配置autofsNFS共享(linux系统之间的文件共享)
- Zabbix经验分享-缺包常见问题处理