一、前言
1.1 项目开发背景
随着全球人口老龄化进程的加快以及慢性病患病率的持续上升,独居老人、康复期患者以及高危职业人群的健康监测与安全预警需求日益凸显。传统的医疗检测手段多依赖于医院或社区诊所的定期检查,难以实现对个体健康状态的实时、连续跟踪,尤其是在突发摔倒、急性疾病发作等紧急情况下,往往因缺乏及时报警而延误救治时机。因此,开发一种能够全天候监测人体关键生理参数、感知姿态异常、支持主动求助并具备远程通知能力的便携式健康监测系统,具有重要的现实意义和社会价值。
近年来,物联网、移动通信与嵌入式技术的飞速发展为远程健康监测提供了可行的技术路径。通过集成多种生物医学传感器与环境传感器,可以实时采集心率、血氧、体温、运动姿态以及所处环境的光照、温湿度等信息,并结合4G无线通信模块将这些数据上传至云平台。在此基础上,利用移动终端或电脑上位机软件进行可视化展示与智能分析,不仅能够帮助监护人远程了解被监测者的健康状态与地理位置,还能在异常发生时自动发出报警信息,实现“预防—监测—预警—响应”的闭环管理。
与此同时,现代健康管理更加注重个性化与交互性。不同年龄、性别、体质和运动状态的用户,其健康指标的正常范围与预警阈值存在显著差异。因此,监测系统若能支持通过手机APP设置个人基本信息(如年龄、身高、体重、常规血压等),并将这些参数同步至现场设备端,参与本地健康状态的综合判断,将大幅提升报警的准确性与适应性。此外,用户还应根据实际需求自主开启或关闭报警功能,避免因短暂的非病理性指标波动造成不必要的干扰,体现系统的灵活性与人性化设计。
在地理位置服务方面,结合GPS定位与地图API(如百度地图),监护人可以直观地了解设备佩戴者的实时位置,对于户外活动、老年痴呆症患者走失或设备遗失等场景尤为实用。同时,设备端具备多按键交互、蜂鸣器本地报警、OLED屏幕信息显示以及Type-C与锂电池双模式供电等功能,保证了系统在室内外多种环境下的易用性和可靠性。历史数据的本地存储与折线图展示功能,则为长期健康趋势分析和疾病预防提供了数据支撑。
综上所述,本项目基于Arduino Uno平台,融合多传感器数据采集、4G物联网通信、MQTT协议传输、OneNet云平台数据汇聚以及Qt跨平台客户端开发,设计并实现一套功能完善、响应及时、可定制化的人体健康监测分析预警装置。该装置既可满足家庭养老、康复监护等日常应用需求,也为智慧医疗与远程护理系统的构建提供了可行的技术方案与参考模型。
1.2 设计实现的功能
模块代码合集放在网盘:https://pan.quark.cn/s/6c4fac974120
(1)支持检测人体的姿态。如果检测到摔倒,本地蜂鸣器响,同时将摔倒状态上传到物联网云平台。
(2)设备支持主动SOS求助报警。当主动按下设备上的按键,可以发送一条求助信息上传到物联网云平台,监护人的APP会弹出窗口提醒。
(3)支持数据上云。本地设备采集的所有数据都会利用4G模块上传到OneNet物联网云平台,再通过Qt设计的Android手机APP和Windows电脑上位机,从OneNet物联网云平台获取设备上传的数据,实现远程检测。
(4)本地通过OLED显示屏实时显示心率、血氧、体温、环境温湿度、光照强度、GPS定位信息等参数显示。
(5)支持心率、血氧、体温、环境温度、环境湿度、GPS定位信息测量。
(6)APP端用户可以设置自己的个人信息,包括:性别、年龄、身高、体重、常规血压,当前运动情况(一般、强、弱),设置之后,参数会同步到硬件设备端,方便硬件设备端能够结合这些已知参数综合判断当前的健康状态。如果当前健康状态不正常,本地蜂鸣器会响,进行报警,同时会将报警信息上传到服务器,APP端会弹出窗口提醒是哪一项健康不正常,提醒用户。
(7)APP端支持开启或者关闭报警功能。如果关闭报警功能,本地硬件设备不对当前个人健康参数综合判断。开启了才会执行健康判断并报警。
(8)APP端支持地图可视化显示,硬件设备端上传了GPS经纬度之后,APP端调用了百度地图的API接口,将GPS经纬度转为可视化地图展示出来,地图里标红色点,方便知道目前硬件设备所在位置。
(9)APP端支持手动报警,APP端有一个按钮打开或者关闭可以控制硬件设备端的蜂鸣器开启和关闭。如果设备丢失或者寻找设备时有帮助,也可以在某些情况下提醒佩戴者。
(10)设备供电支持Type-C接口供电,也支持5V接口的锂电池供电。
(11)设备支持环境光强度检测,检测完毕设备端显示,也会上传云端。
(12)设备端有4个按钮,第一个按钮是显示屏翻页;第二个按钮是主动上传SOS求救信息;第三个按钮是开启或者关闭报警模式;第四个按钮是清除报警信息。
(13)APP端支持历史数据记录,采用SQLite保存上传的健康数据与环境数据,永久保存,按每天的日期建表存储数据。支持历史数据查看,采用折线图的形式,将用户选择查看的某一项数据(有下拉选项可以选择),读取这个数据当日的全部数据用折线图展示到界面上,方便观察变化趋势。
1.3 项目硬件模块组成
(1)主控芯片:采用Arduino Uno R3。
(2)人体姿态检测模块:通过MPU6050陀螺仪模块检测人体姿态,判断是否摔倒,采用IIC协议通信。
(3)显示模块:采用0.96寸IIC协议的OLED显示屏。
(5)4G通信模块:采用Air780e 4G模块,通过MQTT协议与OneNet物联网云平台通信。
(6)心率、血氧检测模块:采用MAX30102,采用IIC协议通信。
(7)体温测量模块:采用MLX90614,采用IIC协议通信。
(8)GPS定位模块:采用ATGM336H-5N-GPS模块接收卫星定位信息,通过串口协议与设备通信。
(9)环境光检测模块:采用BH1750,采用IIC协议通信。
(10)环境温湿度检测模块:采用DHT11传感器。
(11)按键模块:设备端有4个普通机械按键。
(12)供电模块:支持Type-C接口供电,也支持5V接口的锂电池供电。
1.4 系统框架图
1.5 运行流程图
1.6 硬件材料
【1】arduino uno r3 开发板
【2】OLED显示屏(IIC协议4针)
【3】GPS+BDS双模定位模块
【4】心率血氧检测模块
【5】Air780e模块
【6】MPU6050陀螺仪
【7】体温测量模块(MLX90614)
【8】温湿度传感器
【9】BH1750光敏传感器
二、OneNet平台搭建
2.1 OneNet平台介绍
OneNet是中国移动打造的物联网开放平台,为物联网设备与应用开发者提供稳定、可靠、可扩展的云端服务。该平台向下连接海量设备,支持设备接入、数据采集与远程控制;向上提供数据存储、分析与应用使能工具,帮助用户快速搭建各类物联网解决方案。作为国内领先的物联网PaaS平台,OneNet具备高并发处理能力和毫秒级消息响应能力,其分布式集群架构可保障99.95%的服务可用性。
在功能层面,OneNet支持MQTT、CoAP、LwM2M等多种主流物联网协议,并提供设备注册、认证、生命周期管理以及设备影子等功能。平台内嵌规则引擎,可实现数据流转与场景联动,同时配备了数据可视化大屏和时序数据库,便于用户进行数据分析和监控。安全机制方面,OneNet提供了设备身份认证、数据加密传输和访问控制策略,确保设备和数据的安全可靠。
在生态与开发支持方面,OneNet提供了多语言SDK(如Java、Python、C、Node.js等)、详细的开发者文档、设备模拟器以及活跃的开发者社区,帮助开发者和企业快速完成从设备端到应用端的集成。该平台已广泛应用于智慧城市、工业物联网、智慧家居、车联网等领域,例如智能路灯、设备远程监控、智能家电控制、车辆定位追踪等典型场景。随着5G、AI和边缘计算技术的发展,OneNet正在持续融合边缘计算能力与人工智能分析,进一步提升平台的实时性与数据价值,助力企业数字化转型。
2.2 创建产品
(1)登录账户
官网:https://open.iot.10086.cn/
进来先登录账号。
(2)选择物联网开放平台
(3)创建产品
根据自己产品信息填写:
创建之后点击确定。
创建完成。
(4)产品ID
产品ID: Dy3FSnC24O
2.3 创建设备
产品是属性抽象模型,产品下面的设备就表示具体的硬件设备,需要与具体的硬件关联。产品下可以创建很多的设。
(1)添加设备
(2)填写设备信息
创建完成。
创建完成。
(3)查看设备详情
添加完成之后,点击查看详情,查看设备的详细信息。
这里的产品ID、设备ID、设备密匙非常有用。后续MQTT登录参数需要使用。这里记录一下。
产品ID: Dy3FSnC24O
设备ID: 2605967662
设备密匙:b211dUEzY2pZdHdJMXhFS1FzTDBQbjYzdk1WMjRZZkg=
设备名称:dev1
2.4 添加数据流模板
(1)添加数据流模板
添加数据流模板。
(2)根据设备需求添加
data1 测试数据字段1
data2 测试数据字段2
data3 测试数据字段3
data4 测试数据字段4
data5 测试数据字段5
开始创建:
(3)添加完毕
这里可以看到订阅,发布主题的格式。
2.5 MQTT协议接入地址
当前智能鱼缸设备是采用MQTT协议与OneNet服务器进行通信。
MQTT物联网套件产品架构如下图所示:
接入地址说明:https://open.iot.10086.cn/doc/v5/develop/detail/248
在帮助文档页面,介绍了MQTT接入的地址和端口号。 当前设备是单片机,端口采用1883非加密端口。
地址与端口总结如下:
固定的IP地址: 183.230.40.96
固定的端口号: 1883
2.6 MQTT主题订阅与发布
MQTT协议是一种消息列队传输协议,采用订阅、发布机制,订阅者只接收自己已经订阅的数据,非订阅数据则不接收,既保证了必要的数据的交换,又避免了无效数据造成的储存与处理。因此在工业物联网中得到广泛的应用。
(1)主题订阅
主题订阅是设备订阅平台的消息,如果设备想知道平台下发的消息,就需要订阅主题。
帮助文档: https://open.iot.10086.cn/doc/mqtt/book/device-develop/protocol.html
需要订阅什么数据,设备端按照下面的主题格式填写订阅即可。
如果想知道设备所有相关信息,直接订阅$sys/{产品ID}/{device-name}/#即可。 (其中的PID就是产品ID)
$sys/Dy3FSnC24O/dev1/#
(2)主题发布
主题发布: 就是设备向平台上传数据。
帮助文档地址:https://open.iot.10086.cn/doc/mqtt/book/example/datapoints.html
文档里介绍了数据点上传的格式:
根据当前设备,总结的格式如下:
发布主题: $sys/Dy3FSnC24O/dev1/dp/post/json
发布消息:
{"id":123,"dp":{"data1":[{"v":6.4}],"data2":[{"v":3.4}]}}
dp对象里面就是需要上传的数据点字段。 这个数据点的名字就是自己创建数据流模板的时候创建的。
数据点是标准的JSON结构:
{
"id": 123,
"dp": {
"data1": [
{
"v": 6.4
}
],
"data2": [
{
"v": 3.4
}
]
}
}
2.7 MQTT三元组生成
设备登录OneNet采用的是MQTT协议,MQTT协议登录需要填写 登录信息:简称 MQTT三元组。
(1)需要的参数
(2)密码生成规则
链接:https://open.iot.10086.cn/doc/v5/develop/detail/241
(3)编写生成密码的算法
这里算法代码采用Python编写。下面是生成密码的Python算法。
电脑先安装Python环境,然后将下面代码copy到文本里,文本后缀改为.py,就可以允许python代码了。
import base64
import hmac
from urllib.parse import quote
import time
# 中国移动官方文档给出的核心秘钥计算算法
def token(id,access_key):
version = '2018-10-31'
res = 'products/%s' % id # 通过产品ID访问产品API
# 用户自定义token过期时间
et = str(int(time.time()) + 63072000) # 设置为2年有效时间
# 签名方法,支持md5、sha1、sha256
method = 'sha1'
# 对access_key进行decode
key = base64.b64decode(access_key)
# 计算sign
org = et + 'n' + method + 'n' + res + 'n' + version
sign_b = hmac.new(key=key, msg=org.encode(), digestmod=method)
sign = base64.b64encode(sign_b.digest()).decode()
# value 部分进行url编码,method/res/version值较为简单无需编码
sign = quote(sign, safe='')
res = quote(res, safe='')
# token参数拼接
token = 'version=%s&res=%s&et=%s&method=%s&sign=%s' % (version, res, et, method, sign)
return token
username = "Dy3FSnC24O" # 产品ID
accesskey = "c3BoeTdqM3hPQXI4Zm1HckdhZTJieEpyT1N0eGpXTDI=" # accessKey
password = token(username, accesskey)
print(password)
# 等待用户按下 Enter 键再退出
input("按 Enter 键退出程序...")
先安装好Python环境。
根据自己的实际修改以上的产品ID与accessKey
**将上面的文件保存为 xxx.py 文件,然后双击运行: ** 就得到了生成的key
(4)MQTT登录参数总结
MQTT协议登录时,需要输入3个参数: MQTT-设备ID,MQTT-设备名称,MQTT-密码。
对应OneNet的参数:
MQTT的设备ID -----> 就是OneNet的设备名称
MQTT的设备名称-----> 就是OneNet的产品ID
MQTT的密码------------> 就是OneNet的密匙工具生成的密码
下面是对本次的设备做总结:
IP地址: 183.230.40.96
端口号: 1883
clientId: dev1
username: Dy3FSnC24O
password: version=2018-10-31&res=products%2FDy3FSnC24O&et=1827306543&method=sha1&sign=Yzr14cK2iyhIDhYSCXMf89h%2BeQk%3D
订阅主题: $sys/Dy3FSnC24O/dev1/#
发布主题: $sys/Dy3FSnC24O/dev1/dp/post/json
发布消息:
{"id":123,"dp":{"data1":[{"v":6.4}],"data2":[{"v":3.4}]}}
2.8 MQTT工具登录测试
前面已经介绍了MQTT协议登录需要用到的参数,以及订阅主题、发布主题的格式,接下来我们通过MQTT工具模拟设备登录OneNet平台,完成数据交互测试。
简单来说: 就是用软件来模拟实际的硬件,登录onenet平台,上传数据,走一下这个流程。
(1)模拟设备登录
接下来根据软件的输入框提示,输入对应的参数,然后登录设备,订阅主题,发布主题即可完成一个流程的测试。
(2)登录OneNet控制台查看设备
在设备列表页面,可以看到设备已经在线了。
在设备详情页面可以看到设备模拟器刚才上传的数据。
在设备的数据流页面可以看到上传的数据。
(3)查看历史数据
切换成列表模式。可以查看历史数据。
点击数据名称,可以查看数据的历史数据,同时可以导出EXECL表格,还可以看历史数据变化波形。
到此,设备的云平台已经配置完毕。
2.9 上位机开发必备参数
开发上位机需要得到OneNet的一些参数,生成鉴权的Token。
查看AccessKey:
下面就可以查看了:
复制保存下来,后面需要使用:
fa6da5ee39964d9c9a168fec801b7dd1
链接地址:https://open.iot.10086.cn/console/summary
产品ID:Dy3FSnC24O
设备名称: dev1
AccessKey:
fa6da5ee39964d9c9a168fec801b7dd1
用户ID:477709
三、上位机开发
3.1 Qt开发环境安装
Qt的中文官网: https://www.qt.io/zh-cn/
QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6
打开下载链接后选择下面的版本进行下载:
如果下载不了,可以在网盘里找到安装包下载: 飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
软件安装时断网安装,否则会提示输入账户。
安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。
选择编译器: (一定要看清楚了)
3.2 新建上位机工程
前面2讲解了需要用的API接口,接下来就使用Qt设计上位机,设计界面,完成整体上位机的逻辑设计。
【1】新建工程
【2】设置项目的名称。
【3】选择编译系统
【4】选择默认继承的类
【5】选择编译器
【6】点击完成
【7】工程创建完成
3.3 切换编译器
在左下角是可以切换编译器的。 可以选择用什么样的编译器编译程序。
目前新建工程的时候选择了2种编译器。 一种是mingw32这个编译Windows下运行的程序。 一种是Android编译器,可以生成Android手机APP。
不过要注意:Android的编译器需要配置一些环境才可以正常使用,这个大家可以网上找找教程配置一下就行了。
windows的编译器就没有这么麻烦,安装好Qt就可以编译使用。
下面我这里就选择的 mingw32这个编译器,编译Windows下运行的程序。
3.4 编译测试功能
创建完毕之后,编译测试一下功能是否OK。
点击左下角的绿色三角形按钮。
正常运行就可以看到弹出一个白色的框框。这就表示工程环境没有问题了。 接下来就可以放心的设计界面了。
3.5 设计UI界面与工程配置
【1】打开UI文件
打开默认的界面如下:
【2】开始设计界面
根据自己需求设计界面。
3.6 设计代码
完整的Qt Widget窗体代码,实现Android手机APP和Windows电脑上位机的所有功能,包括数据展示、地图可视化、用户设置、历史记录与折线图、远程报警控制等。
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QMessageBox>
#include <QDateTime>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>
#include <QChart>
#include <QChartView>
#include <QLineSeries>
#include <QValueAxis>
#include <QCategoryAxis>
#include <QDesktopServices>
#include <QUrl>
#include <QProcess>
#include <QSqlDatabase>
QT_CHARTS_USE_NAMESPACE
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
, m_mqttClient(nullptr)
, m_timer(new QTimer(this))
, m_reconnectTimer(new QTimer(this))
, m_isAlarmEnabled(true)
, m_currentChart(nullptr)
{
ui->setupUi(this);
// 初始化数据库
initDatabase();
// 初始化MQTT连接
initMQTT();
// 初始化定时器
m_timer->setInterval(3000); // 每3秒请求一次数据
connect(m_timer, &QTimer::timeout, this, &Widget::requestDeviceData);
m_timer->start();
// 重连定时器
m_reconnectTimer->setInterval(10000);
connect(m_reconnectTimer, &QTimer::timeout, this, &Widget::reconnectMQTT);
// 初始化UI
initUI();
// 初始化图表
initChart();
// 初始化地图(使用WebEngine或WebView)
initMap();
}
Widget::~Widget()
{
if(m_mqttClient && m_mqttClient->state() == QMqttClient::Connected) {
m_mqttClient->disconnectFromHost();
}
delete ui;
}
// ==================== 初始化函数 ====================
void Widget::initDatabase()
{
// 创建SQLite数据库连接
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("health_data.db");
if(!db.open()) {
qDebug() << "Database open failed:" << db.lastError().text();
return;
}
// 创建今日数据表(按日期命名)
QString currentDate = QDateTime::currentDateTime().toString("yyyy_MM_dd");
QString tableName = "data_" + currentDate;
QSqlQuery query;
QString createSql = QString("CREATE TABLE IF NOT EXISTS [%1] ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, "
"heart_rate INTEGER, "
"spo2 INTEGER, "
"temperature REAL, "
"env_temp REAL, "
"env_humidity INTEGER, "
"light_intensity REAL, "
"latitude REAL, "
"longitude REAL, "
"alarm_field INTEGER, "
"fall_state INTEGER, "
"sos_state INTEGER)").arg(tableName);
if(!query.exec(createSql)) {
qDebug() << "Create table failed:" << query.lastError().text();
}
// 创建用户信息表
query.exec("CREATE TABLE IF NOT EXISTS user_info ("
"id INTEGER PRIMARY KEY, "
"gender INTEGER, "
"age INTEGER, "
"height INTEGER, "
"weight INTEGER, "
"blood INTEGER, "
"motion INTEGER)");
// 加载用户信息
loadUserInfo();
}
void Widget::loadUserInfo()
{
QSqlQuery query("SELECT * FROM user_info WHERE id=1");
if(query.next()) {
ui->comboGender->setCurrentIndex(query.value("gender").toInt() - 1);
ui->spinAge->setValue(query.value("age").toInt());
ui->spinHeight->setValue(query.value("height").toInt());
ui->spinWeight->setValue(query.value("weight").toInt());
ui->spinBlood->setValue(query.value("blood").toInt());
ui->comboMotion->setCurrentIndex(query.value("motion").toInt());
} else {
// 默认值
ui->comboGender->setCurrentIndex(0);
ui->spinAge->setValue(25);
ui->spinHeight->setValue(170);
ui->spinWeight->setValue(65);
ui->spinBlood->setValue(120);
ui->comboMotion->setCurrentIndex(0);
}
}
void Widget::initMQTT()
{
m_mqttClient = new QMqttClient(this);
m_mqttClient->setHostname("127f73c01f.st1.iotda-device.cn-north-4.myhuaweicloud.com");
m_mqttClient->setPort(1883);
m_mqttClient->setClientId("6a2a4297cbb0cf6bb963d5a4_dev1_0_0_2026061105");
m_mqttClient->setUsername("6a2a4297cbb0cf6bb963d5a4_dev1");
m_mqttClient->setPassword("570d399ef922abd9d1719b20e8d76f936fb8f8e670c035f6c345479778ee47d2");
connect(m_mqttClient, &QMqttClient::stateChanged, this, &Widget::onMQTTStateChanged);
connect(m_mqttClient, &QMqttClient::messageReceived, this, &Widget::onMQTTMessageReceived);
connect(m_mqttClient, &QMqttClient::connected, this, &Widget::onMQTTConnected);
m_mqttClient->connectToHost();
}
void Widget::onMQTTStateChanged(QMqttClient::ClientState state)
{
QString status;
switch(state) {
case QMqttClient::Disconnected:
status = "未连接";
m_reconnectTimer->start();
break;
case QMqttClient::Connecting:
status = "连接中...";
break;
case QMqttClient::Connected:
status = "已连接";
m_reconnectTimer->stop();
break;
}
ui->labelStatus->setText("MQTT状态: " + status);
}
void Widget::onMQTTConnected()
{
// 订阅设备上报主题
QString subscribeTopic = "$oc/devices/6a2a4297cbb0cf6bb963d5a4_dev1/sys/properties/report";
m_mqttClient->subscribe(subscribeTopic);
// 订阅命令下发响应主题(用于接收设备回复)
QString responseTopic = "$oc/devices/6a2a4297cbb0cf6bb963d5a4_dev1/sys/messages/down";
m_mqttClient->subscribe(responseTopic);
qDebug() << "MQTT connected and subscribed";
}
void Widget::reconnectMQTT()
{
if(m_mqttClient->state() != QMqttClient::Connected) {
m_mqttClient->connectToHost();
}
}
void Widget::onMQTTMessageReceived(const QByteArray &message, const QMqttTopicName &topic)
{
Q_UNUSED(topic);
QJsonDocument doc = QJsonDocument::fromJson(message);
if(doc.isNull()) return;
QJsonObject obj = doc.object();
// 解析设备上报数据
if(obj.contains("services")) {
QJsonArray services = obj["services"].toArray();
if(services.size() > 0) {
QJsonObject properties = services[0]["properties"].toObject();
updateDeviceData(properties);
}
}
// 解析命令响应
if(obj.contains("result")) {
qDebug() << "Command response:" << message;
}
}
void Widget::updateDeviceData(const QJsonObject &data)
{
// 更新健康数据
if(data.contains("HeartRate")) {
int hr = data["HeartRate"].toInt();
ui->labelHeartRate->setText(QString::number(hr) + " bpm");
m_currentData.heartRate = hr;
}
if(data.contains("SPO")) {
int spo2 = data["SPO"].toInt();
ui->labelSPO2->setText(QString::number(spo2) + " %");
m_currentData.spo2 = spo2;
}
if(data.contains("temp")) {
double temp = data["temp"].toDouble();
ui->labelTemp->setText(QString::number(temp, 'f', 1) + " °C");
m_currentData.temperature = temp;
}
if(data.contains("DHT11_T")) {
double envTemp = data["DHT11_T"].toDouble();
ui->labelEnvTemp->setText(QString::number(envTemp, 'f', 1) + " °C");
m_currentData.envTemp = envTemp;
}
if(data.contains("DHT11_H")) {
int envHumi = data["DHT11_H"].toInt();
ui->labelEnvHumi->setText(QString::number(envHumi) + " %");
m_currentData.envHumidity = envHumi;
}
if(data.contains("BH1750")) {
double light = data["BH1750"].toDouble();
ui->labelLight->setText(QString::number(light, 'f', 0) + " lx");
m_currentData.lightIntensity = light;
}
if(data.contains("GPS")) {
QJsonObject gps = data["GPS"].toObject();
double lon = gps["lon"].toDouble();
double lat = gps["lat"].toDouble();
ui->labelGPS->setText(QString::number(lat, 'f', 6) + ", " + QString::number(lon, 'f', 6));
m_currentData.latitude = lat;
m_currentData.longitude = lon;
// 更新地图位置
updateMapLocation(lat, lon);
}
if(data.contains("MPU6050")) {
int fall = data["MPU6050"].toInt();
ui->labelFall->setText(fall ? "检测到摔倒!" : "正常");
m_currentData.fallState = fall;
if(fall) {
QMessageBox::warning(this, "摔倒报警", "检测到佩戴者摔倒!");
}
}
if(data.contains("SOS")) {
int sos = data["SOS"].toInt();
m_currentData.sosState = sos;
if(sos) {
QMessageBox::critical(this, "SOS求助", "佩戴者发出紧急求助!");
}
}
if(data.contains("Alarm_field")) {
int alarmField = data["Alarm_field"].toInt();
m_currentData.alarmField = alarmField;
showAlarmDetails(alarmField);
}
// 保存到数据库
saveToDatabase();
}
void Widget::showAlarmDetails(int alarmField)
{
if(alarmField == 0) return;
QStringList alarms;
if(alarmField & 0x01) alarms << "心率过低";
if(alarmField & 0x02) alarms << "心率过高";
if(alarmField & 0x04) alarms << "血氧过低(<94%)";
if(alarmField & 0x08) alarms << "体温过低";
if(alarmField & 0x10) alarms << "体温过高";
if(alarmField & 0x20) alarms << "摔倒";
if(!alarms.isEmpty()) {
QString msg = "健康异常报警:n" + alarms.join("n");
QMessageBox::warning(this, "健康报警", msg);
}
}
void Widget::saveToDatabase()
{
QString currentDate = QDateTime::currentDateTime().toString("yyyy_MM_dd");
QString tableName = "data_" + currentDate;
QSqlQuery query;
QString insertSql = QString("INSERT INTO [%1] (heart_rate, spo2, temperature, env_temp, "
"env_humidity, light_intensity, latitude, longitude, "
"alarm_field, fall_state, sos_state) "
"VALUES (:hr, :spo2, :temp, :env_temp, :env_humi, "
":light, :lat, :lon, :alarm, :fall, :sos)").arg(tableName);
query.prepare(insertSql);
query.bindValue(":hr", m_currentData.heartRate);
query.bindValue(":spo2", m_currentData.spo2);
query.bindValue(":temp", m_currentData.temperature);
query.bindValue(":env_temp", m_currentData.envTemp);
query.bindValue(":env_humi", m_currentData.envHumidity);
query.bindValue(":light", m_currentData.lightIntensity);
query.bindValue(":lat", m_currentData.latitude);
query.bindValue(":lon", m_currentData.longitude);
query.bindValue(":alarm", m_currentData.alarmField);
query.bindValue(":fall", m_currentData.fallState);
query.bindValue(":sos", m_currentData.sosState);
if(!query.exec()) {
qDebug() << "Insert failed:" << query.lastError().text();
}
}
// ==================== UI初始化 ====================
void Widget::initUI()
{
// 连接UI信号槽
connect(ui->btnSync, &QPushButton::clicked, this, &Widget::onSyncUserInfo);
connect(ui->btnAlarmEnable, &QPushButton::clicked, this, &Widget::onAlarmEnableToggle);
connect(ui->btnRemoteBuzzer, &QPushButton::clicked, this, &Widget::onRemoteBuzzerToggle);
connect(ui->btnSOS, &QPushButton::clicked, this, &Widget::onManualSOS);
connect(ui->btnRefreshData, &QPushButton::clicked, this, &Widget::requestDeviceData);
connect(ui->btnViewHistory, &QPushButton::clicked, this, &Widget::onViewHistory);
connect(ui->comboHistoryData, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &Widget::onHistoryDataTypeChanged);
connect(ui->dateHistory, &QDateEdit::dateChanged, this, &Widget::onHistoryDateChanged);
// 初始化报警按钮状态
ui->btnAlarmEnable->setText("关闭报警");
ui->btnAlarmEnable->setStyleSheet("background-color: #4CAF50; color: white;");
// 远程蜂鸣器按钮
m_buzzerActive = false;
ui->btnRemoteBuzzer->setText("开启蜂鸣器");
// 数据显示标签样式
QString labelStyle = "font-size: 16px; font-weight: bold; color: #333;";
ui->labelHeartRate->setStyleSheet(labelStyle);
ui->labelSPO2->setStyleSheet(labelStyle);
ui->labelTemp->setStyleSheet(labelStyle);
}
void Widget::initChart()
{
// 创建图表视图
m_chartView = new QChartView(this);
m_chartView->setRenderHint(QPainter::Antialiasing);
m_chartView->setMinimumHeight(300);
// 添加到布局
ui->verticalLayoutChart->addWidget(m_chartView);
m_chart = new QChart();
m_chartView->setChart(m_chart);
// 初始化数据系列
m_series = new QLineSeries();
m_chart->addSeries(m_series);
// 设置坐标轴
QValueAxis *axisX = new QValueAxis();
axisX->setTitleText("时间");
axisX->setLabelFormat("%d");
m_chart->addAxis(axisX, Qt::AlignBottom);
m_series->attachAxis(axisX);
QValueAxis *axisY = new QValueAxis();
axisY->setTitleText("数值");
m_chart->addAxis(axisY, Qt::AlignLeft);
m_series->attachAxis(axisY);
m_chart->setTitle("历史数据趋势");
m_chart->legend()->hide();
}
void Widget::initMap()
{
// 使用QWebEngineView显示百度地图
m_webView = new QWebEngineView(this);
m_webView->setMinimumHeight(400);
ui->verticalLayoutMap->addWidget(m_webView);
// 加载百度地图HTML
QString html = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>百度地图</title>
<style type="text/css">
body, html, #container { width:100%; height:100%; margin:0; padding:0; }
</style>
<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=YOUR_BAIDU_MAP_AK"></script>
</head>
<body>
<div id="container"></div>
<script>
var map;
var marker;
function initMap(lat, lng) {
if(!map) {
map = new BMap.Map("container");
map.centerAndZoom(new BMap.Point(116.404, 39.915), 15);
map.enableScrollWheelZoom(true);
}
var point = new BMap.Point(lng, lat);
map.centerAndZoom(point, 15);
if(marker) {
map.removeOverlay(marker);
}
marker = new BMap.Marker(point);
map.addOverlay(marker);
var label = new BMap.Label("设备位置", {offset:new BMap.Size(20,-10)});
marker.setLabel(label);
}
function updatePosition(lat, lng) {
if(map) {
var point = new BMap.Point(lng, lat);
map.centerAndZoom(point, 15);
if(marker) {
map.removeOverlay(marker);
}
marker = new BMap.Marker(point);
map.addOverlay(marker);
var label = new BMap.Label("设备位置", {offset:new BMap.Size(20,-10)});
marker.setLabel(label);
} else {
initMap(lat, lng);
}
}
</script>
</body>
</html>
)";
m_webView->setHtml(html);
m_webView->page()->loadFinished(true);
}
void Widget::updateMapLocation(double lat, double lon)
{
if(lat == 0 && lon == 0) return;
QString js = QString("updatePosition(%1, %2);").arg(lat, 0, 'f', 6).arg(lon, 0, 'f', 6);
m_webView->page()->runJavaScript(js);
}
// ==================== 请求与发送函数 ====================
void Widget::requestDeviceData()
{
if(m_mqttClient->state() != QMqttClient::Connected) {
ui->labelStatus->setText("MQTT未连接,请检查网络");
return;
}
// 发送属性获取请求
QString requestTopic = "$oc/devices/6a2a4297cbb0cf6bb963d5a4_dev1/sys/properties/get";
QJsonObject request;
request["service_id"] = "stm32";
QJsonDocument doc(request);
m_mqttClient->publish(requestTopic, doc.toJson());
}
void Widget::sendCommandToDevice(const QString &command, const QJsonObject ¶ms)
{
if(m_mqttClient->state() != QMqttClient::Connected) {
QMessageBox::warning(this, "提示", "MQTT未连接");
return;
}
QString commandTopic = "$oc/devices/6a2a4297cbb0cf6bb963d5a4_dev1/sys/commands/request/" + command;
QJsonObject msg;
msg["command_name"] = command;
msg["paras"] = params;
QJsonDocument doc(msg);
m_mqttClient->publish(commandTopic, doc.toJson());
}
void Widget::onSyncUserInfo()
{
// 获取用户设置
int gender = ui->comboGender->currentIndex() + 1;
int age = ui->spinAge->value();
int height = ui->spinHeight->value();
int weight = ui->spinWeight->value();
int blood = ui->spinBlood->value();
int motion = ui->comboMotion->currentIndex();
// 保存到本地数据库
QSqlQuery query;
query.prepare("REPLACE INTO user_info (id, gender, age, height, weight, blood, motion) "
"VALUES (1, :gender, :age, :height, :weight, :blood, :motion)");
query.bindValue(":gender", gender);
query.bindValue(":age", age);
query.bindValue(":height", height);
query.bindValue(":weight", weight);
query.bindValue(":blood", blood);
query.bindValue(":motion", motion);
query.exec();
// 发送到设备端
QJsonObject params;
params["gender"] = gender;
params["age"] = age;
params["height"] = height;
params["weight"] = weight;
params["blood"] = blood;
params["motion"] = motion;
sendCommandToDevice("set_user_info", params);
QMessageBox::information(this, "提示", "用户信息已同步到设备端");
}
void Widget::onAlarmEnableToggle()
{
m_isAlarmEnabled = !m_isAlarmEnabled;
QJsonObject params;
params["alarm_mode"] = m_isAlarmEnabled ? 1 : 0;
sendCommandToDevice("set_alarm_mode", params);
if(m_isAlarmEnabled) {
ui->btnAlarmEnable->setText("关闭报警");
ui->btnAlarmEnable->setStyleSheet("background-color: #4CAF50; color: white;");
} else {
ui->btnAlarmEnable->setText("开启报警");
ui->btnAlarmEnable->setStyleSheet("background-color: #f44336; color: white;");
}
}
void Widget::onRemoteBuzzerToggle()
{
m_buzzerActive = !m_buzzerActive;
QJsonObject params;
params["buzzer_state"] = m_buzzerActive ? 1 : 0;
sendCommandToDevice("control_buzzer", params);
if(m_buzzerActive) {
ui->btnRemoteBuzzer->setText("关闭蜂鸣器");
ui->btnRemoteBuzzer->setStyleSheet("background-color: #f44336; color: white;");
} else {
ui->btnRemoteBuzzer->setText("开启蜂鸣器");
ui->btnRemoteBuzzer->setStyleSheet("background-color: #4CAF50; color: white;");
}
}
void Widget::onManualSOS()
{
QJsonObject params;
params["sos"] = 1;
sendCommandToDevice("sos_trigger", params);
QMessageBox::information(this, "SOS", "已发送SOS求助信息到设备端");
}
// ==================== 历史数据查看 ====================
void Widget::onViewHistory()
{
QString date = ui->dateHistory->date().toString("yyyy_MM_dd");
QString dataType = ui->comboHistoryData->currentText();
loadHistoryData(date, dataType);
}
void Widget::onHistoryDataTypeChanged(int index)
{
Q_UNUSED(index);
onViewHistory();
}
void Widget::onHistoryDateChanged(const QDate &date)
{
Q_UNUSED(date);
onViewHistory();
}
void Widget::loadHistoryData(const QString &date, const QString &dataType)
{
QString tableName = "data_" + date;
QSqlQuery query;
query.prepare(QString("SELECT timestamp, %1 as value FROM [%2] ORDER BY id").arg(getDataTypeField(dataType), tableName));
if(!query.exec()) {
qDebug() << "Query failed:" << query.lastError().text();
return;
}
// 清空现有数据
m_series->clear();
// 添加数据点
int x = 0;
QDateTime lastTime;
while(query.next()) {
double value = query.value("value").toDouble();
QDateTime timestamp = query.value("timestamp").toDateTime();
// 使用索引作为X轴
m_series->append(x, value);
x++;
lastTime = timestamp;
}
// 更新图表标题
m_chart->setTitle(dataType + " 历史趋势 (" + date + ")");
// 如果数据为空,显示提示
if(x == 0) {
m_chart->setTitle("暂无数据");
}
}
QString Widget::getDataTypeField(const QString &dataType)
{
if(dataType == "心率") return "heart_rate";
if(dataType == "血氧") return "spo2";
if(dataType == "体温") return "temperature";
if(dataType == "环境温度") return "env_temp";
if(dataType == "环境湿度") return "env_humidity";
if(dataType == "光照强度") return "light_intensity";
return "heart_rate";
}
四、Arduino uno代码设计
4.1 下载安装开发环境
官网下载地址:https://www.arduino.cc/en/software/
双击安装。
软件启动中:
在启动过程中,会弹出一堆安装驱动的说明。全部点击确定就行了。
软件启动成功。
设置中文界面。
重新启动,就是中文界面了。
4.2 选择开发板
根据自己的板子型号进行选择。
这是我的开发板。
将开发板连接电脑。
选择开发板的端口。
连接成功了。
将文件保存起来。编写代码,点击编译下载按钮就可以将代码下载到板子上就可以测试了。
下面代码是点亮2个LED灯。
void setup() {
pinMode(13, OUTPUT); //LED2
pinMode(12, OUTPUT); //LED1
}
void loop() {
digitalWrite(13, HIGH);
digitalWrite(12, HIGH);
delay(500);
digitalWrite(13, LOW);
digitalWrite(12, LOW);
delay(500);
}
4.3 开发板的引脚
开发板右边的013 就是数字口,D0D13.
开发板左边的A0~A5 是模拟口。
4.4 安装Adafruit GFX扩展库
搜索Adafruit GFX
-
- 1. 打开 Arduino IDE。2. 点击菜单栏 **“工具” → “管理库”**。3. 在搜索框中输入 **“Adafruit GFX”**。4. 找到 **“Adafruit GFX Library”
并点击
- “安装”**。
安装成功。
4.5 安装Adafruit SSD1306扩展库
-
- • 工具 → 管理库 → 搜索
SSD1306
-
- → 安装
Adafruit SSD1306
4.6 OLED显示测试
库安装好了,就可以写代码测试了。
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C // 使用扫描到的地址
// 创建显示对象
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
void setup() {
// 初始化 OLED
if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
// 初始化失败,无限循环(可以通过串口查看)
Serial.begin(9600);
Serial.println("OLED 初始化失败!");
for(;;);
}
display.clearDisplay(); // 清屏
display.setTextSize(2); // 字体大小
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 20); // 位置(x, y)
display.println("hello");
display.display(); // 显示到屏幕
}
void loop() {
// 空循环,不需要重复显示
}
直接编译下载:
运行的效果。
4.7 项目硬件接线说明
硬件接线说明(Arduino Uno R3)
-
- 1. CoreY100P 4G模块
-
- RXD <--------> 引脚3(软串口RX)
-
- TXD <--------> 引脚2(软串口TX)
-
- GND <--------> GND
-
- VCC <--------> 5V2. 0.96寸IIC接口的OLED显示屏
-
- VCC <--------> 5V
-
- GND <--------> GND
-
- SCL <--------> A5(IIC时钟线)
-
- SDA <--------> A4(IIC数据线)3. GPS接线说明(ATGM336H-5N)
-
- GND <--------> GND
-
- VCC <--------> 5V
-
- GPS_TX <-----> 引脚4(软串口RX,接收GPS数据)
-
- (GPS_RX可不接,无需向GPS发送指令)4. MAX30102脉搏心率检测模块
-
- SCL <--------> A5
-
- SDA <--------> A4
-
- INT <--------> 引脚5(可选,用于数据就绪中断)
-
- VCC <--------> 3.3V
-
- GND <--------> GND5. MLX90614体温检测模块
-
- GND <--------> GND
-
- VCC <--------> 5V
-
- SDA <--------> A4
-
- SCL <--------> A56. MPU6050六轴陀螺仪
-
- VCC <--------> 5V
-
- GND <--------> GND
-
- SDA <--------> A4
-
- SCL <--------> A5
-
- MPU_INT <----> 未接
-
- MPU_AD0 <----> GND(地址为0x68)7. 蜂鸣器(有源高电平触发)
-
- GND <--------> GND
-
- VCC <--------> 5V
-
- OUT <--------> 引脚6(高电平蜂鸣器响)
-
- 8. 板载LED灯(开发板自带,无需外接)
-
- LED1 --- 运行指示灯(可用引脚13)
-
- LED2 --- 数据上传指示灯(可用引脚12)
-
- LED3 --- 报警指示灯(可用引脚11)
-
- 9. 按键接线(按下为低电平,需启用内部上拉或外接上拉电阻)
-
- SW1(翻页) ---------> 引脚7
-
- SW2(清除报警) -----> 引脚8
-
- SW3(手动SOS报警) --> 引脚9
-
- SW4(报警模式开关) --> 引脚10
-
- (公共端均接GND)10. 光敏传感器:BH1750
-
- VCC <--------> 5V
-
- GND <--------> GND
-
- SCL <--------> A5
-
- SDA <--------> A411. 温湿度传感器:DHT11
-
- VCC <--------> 5V
-
- GND <--------> GND
- DATA <-------> 引脚14(A0,模拟口可作数字口使用)
引脚汇总:
- • IIC总线(共用):A4(SDA)、A5(SCL)• 4G模块软串口:引脚2(TX)、引脚3(RX)• GPS软串口:引脚4(RX)• MAX30102中断:引脚5• 蜂鸣器:引脚6• 按键:引脚7、8、9、10• 指示灯(可选):引脚11、12、13• DHT11:引脚14(A0)
4.8 代码设计
项目开发使用的全部软件工具、代码已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <MAX30105.h>
#include <heartRate.h>
#include <DHT.h>
#include <SoftwareSerial.h>
#include <TinyGPS++.h>
#include <BH1750.h>
// ==================== 引脚定义 ====================
// 4G模块软串口
#define GSM_RX 2
#define GSM_TX 3
SoftwareSerial gsmSerial(GSM_RX, GSM_TX);
// GPS软串口
#define GPS_RX 4
SoftwareSerial gpsSerial(GPS_RX, -1);
TinyGPSPlus gps;
// 蜂鸣器
#define BUZZER_PIN 6
// 按键
#define BTN_PAGE 7
#define BTN_CLEAR_ALARM 8
#define BTN_SOS 9
#define BTN_ALARM_MODE 10
// 指示灯
#define LED_RUN 13
#define LED_UPLOAD 12
#define LED_ALARM 11
// DHT11
#define DHTPIN A0
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
// ==================== IIC设备 ====================
// OLED
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// MPU6050
Adafruit_MPU6050 mpu;
// MAX30102
MAX30105 particleSensor;
const byte RATE_SIZE = 4;
byte rates[RATE_SIZE];
byte rateSpot = 0;
long lastBeat = 0;
float beatsPerMinute;
int beatAvg;
// BH1750
BH1750 lightMeter;
// MLX90614地址
#define MLX90614_ADDR 0x5A
// ==================== 全局变量 ====================
// 传感器数据
float heartRate = 0;
float spo2 = 0;
float bodyTemp = 0;
float envTemp = 0;
float envHumidity = 0;
float lightIntensity = 0;
float latitude = 0;
float longitude = 0;
float gpsState = 0; // 0无效 1有效
float fallState = 0; // 0正常 1摔倒
float sosState = 0; // 0正常 1SOS
// 用户参数(默认值)
int userGender = 1; // 1男 2女
int userAge = 25;
int userHeight = 170;
int userWeight = 65;
int userBlood = 120;
int userMotion = 0; // 0一般 1强 2弱
// 系统状态
int alarmMode = 1; // 1开启 0关闭
int alarmField = 0; // 报警字段 0正常 其他位表示异常
int currentPage = 0; // 0-5 共6页
unsigned long lastUploadTime = 0;
unsigned long lastSensorTime = 0;
bool sosTriggered = false;
unsigned long sosClearTime = 0;
// MQTT相关
String mqttClientId = "6a2a4297cbb0cf6bb963d5a4_dev1_0_0_2026061105";
String mqttUserName = "6a2a4297cbb0cf6bb963d5a4_dev1";
String mqttPassword = "570d399ef922abd9d1719b20e8d76f936fb8f8e670c035f6c345479778ee47d2";
String postTopic = "$oc/devices/6a2a4297cbb0cf6bb963d5a4_dev1/sys/properties/report";
String setTopic = "$oc/devices/6a2a4297cbb0cf6bb963d5a4_dev1/sys/messages/down";
bool mqttConnected = false;
// ==================== 函数声明 ====================
void setup();
void loop();
void readSensors();
void readHeartRateAndSPO2();
void readBodyTemp();
void readDHT();
void readBH1750();
void readGPS();
void readMPU6050();
void readFallState();
void updateOLED();
void displayPage0();
void displayPage1();
void displayPage2();
void displayPage3();
void displayPage4();
void displayPage5();
void checkButtons();
void uploadDataToCloud();
void checkHealthAndAlarm();
void sendMQTTCommand(String cmd, String payload);
void handleCloudCommands();
void parseUserParams(String json);
void controlBuzzer(bool state);
void controlRunLed(bool state);
void controlUploadLed(bool state);
void controlAlarmLed(bool state);
void clearAlarm();
// ==================== 初始化 ====================
void setup() {
Serial.begin(115200);
gsmSerial.begin(115200);
gpsSerial.begin(9600);
// 引脚初始化
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_RUN, OUTPUT);
pinMode(LED_UPLOAD, OUTPUT);
pinMode(LED_ALARM, OUTPUT);
pinMode(BTN_PAGE, INPUT_PULLUP);
pinMode(BTN_CLEAR_ALARM, INPUT_PULLUP);
pinMode(BTN_SOS, INPUT_PULLUP);
pinMode(BTN_ALARM_MODE, INPUT_PULLUP);
digitalWrite(BUZZER_PIN, LOW);
controlRunLed(true);
// OLED初始化
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("OLED init failed");
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// IIC设备初始化
Wire.begin();
// MPU6050
if(!mpu.begin()) {
Serial.println("MPU6050 init failed");
}
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
// MAX30102
if(!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("MAX30102 init failed");
}
particleSensor.setup();
particleSensor.setPulseAmplitudeRed(0x0A);
particleSensor.setPulseAmplitudeGreen(0);
// BH1750
lightMeter.begin();
// DHT11
dht.begin();
// 显示启动信息
display.clearDisplay();
display.setCursor(0, 0);
display.println("Health Monitor");
display.println("System Ready");
display.display();
delay(2000);
controlRunLed(false);
}
// ==================== 主循环 ====================
void loop() {
static unsigned long lastMillis = 0;
unsigned long now = millis();
// 每200ms读取一次传感器(除心率血氧外)
if(now - lastSensorTime >= 200) {
lastSensorTime = now;
readDHT();
readBH1750();
readGPS();
readMPU6050();
readFallState();
readBodyTemp();
}
// 心率血氧每2秒读取一次
static unsigned long lastHrTime = 0;
if(now - lastHrTime >= 2000) {
lastHrTime = now;
readHeartRateAndSPO2();
}
// 健康判断与报警
checkHealthAndAlarm();
// 按键检测
checkButtons();
// OLED显示更新(每300ms)
static unsigned long lastDisplayTime = 0;
if(now - lastDisplayTime >= 300) {
lastDisplayTime = now;
updateOLED();
}
// 数据上传(每5秒)
if(now - lastUploadTime >= 5000) {
lastUploadTime = now;
uploadDataToCloud();
}
// 处理云平台下发指令
handleCloudCommands();
// SOS自动清除(上传后30秒清除状态)
if(sosTriggered && now - sosClearTime > 30000) {
sosTriggered = false;
sosState = 0;
}
// 运行指示灯闪烁
static unsigned long lastLedTime = 0;
if(now - lastLedTime >= 1000) {
lastLedTime = now;
controlRunLed(!digitalRead(LED_RUN));
}
}
// ==================== 传感器读取 ====================
void readHeartRateAndSPO2() {
long irValue = particleSensor.getIR();
if(irValue > 50000) {
if(checkForBeat(irValue) == true) {
long delta = millis() - lastBeat;
lastBeat = millis();
beatsPerMinute = 60 / (delta / 1000.0);
if(beatsPerMinute < 255 && beatsPerMinute > 20) {
rates[rateSpot++] = (byte)beatsPerMinute;
rateSpot %= RATE_SIZE;
beatAvg = 0;
for(byte x = 0 ; x < RATE_SIZE ; x++)
beatAvg += rates[x];
beatAvg /= RATE_SIZE;
heartRate = beatAvg;
}
}
}
// 模拟SPO2计算(简化)
float red = particleSensor.getRed();
float ir = particleSensor.getIR();
if(ir > 0) {
float ratio = red / ir;
spo2 = 98 - (ratio * 20);
if(spo2 > 100) spo2 = 100;
if(spo2 < 70) spo2 = 70;
}
}
void readBodyTemp() {
Wire.requestFrom(MLX90614_ADDR, 3);
uint16_t tempRaw = Wire.read() | (Wire.read() << 8);
bodyTemp = tempRaw * 0.02 - 273.15;
}
void readDHT() {
envHumidity = dht.readHumidity();
envTemp = dht.readTemperature();
if(isnan(envHumidity)) envHumidity = 0;
if(isnan(envTemp)) envTemp = 0;
}
void readBH1750() {
lightIntensity = lightMeter.readLightLevel();
if(isnan(lightIntensity)) lightIntensity = 0;
}
void readGPS() {
while(gpsSerial.available() > 0) {
gps.encode(gpsSerial.read());
}
if(gps.location.isValid()) {
latitude = gps.location.lat();
longitude = gps.location.lng();
gpsState = 1;
} else {
gpsState = 0;
}
}
void readMPU6050() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
// 数据存储供fallState使用
}
void readFallState() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
float accelMag = sqrt(a.acceleration.x * a.acceleration.x +
a.acceleration.y * a.acceleration.y +
a.acceleration.z * a.acceleration.z);
// 摔倒检测:加速度骤变且随后静止
static float lastAccel = 0;
static unsigned long fallDetectTime = 0;
if(accelMag > 20 && lastAccel < 10) {
fallDetectTime = millis();
}
if(fallDetectTime > 0 && millis() - fallDetectTime < 2000 && accelMag < 10) {
fallState = 1;
fallDetectTime = 0;
}
lastAccel = accelMag;
}
// ==================== OLED显示 ====================
void updateOLED() {
switch(currentPage) {
case 0: displayPage0(); break;
case 1: displayPage1(); break;
case 2: displayPage2(); break;
case 3: displayPage3(); break;
case 4: displayPage4(); break;
case 5: displayPage5(); break;
default: displayPage0();
}
}
void displayPage0() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("=== Health Status ===");
display.print("HR:"); display.print(heartRate, 0); display.println(" bpm");
display.print("SPO2:"); display.print(spo2, 0); display.println("%");
display.print("Temp:"); display.print(bodyTemp, 1); display.println(" C");
display.print("Fall:"); display.println(fallState ? "YES" : "NO");
display.display();
}
void displayPage1() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("=== Environment ===");
display.print("EnvT:"); display.print(envTemp, 1); display.println(" C");
display.print("EnvH:"); display.print(envHumidity, 0); display.println("%");
display.print("Light:"); display.print(lightIntensity, 0); display.println(" lx");
display.display();
}
void displayPage2() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("=== GPS Location ===");
if(gpsState) {
display.print("Lat:"); display.println(latitude, 6);
display.print("Lng:"); display.println(longitude, 6);
} else {
display.println("GPS: No Fix");
}
display.display();
}
void displayPage3() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("=== User Info ===");
display.print("Gender:"); display.println(userGender == 1 ? "Male" : "Female");
display.print("Age:"); display.println(userAge);
display.print("H/W:"); display.print(userHeight); display.print("/"); display.println(userWeight);
display.display();
}
void displayPage4() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("=== System Status ===");
display.print("AlarmMode:"); display.println(alarmMode ? "ON" : "OFF");
display.print("AlarmField:"); display.println(alarmField);
display.print("SOS:"); display.println(sosState ? "ACTIVE" : "IDLE");
display.display();
}
void displayPage5() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("=== Blood Pressure ===");
display.print("BP:"); display.print(userBlood); display.println(" mmHg");
display.print("Motion:");
if(userMotion == 0) display.println("Normal");
else if(userMotion == 1) display.println("Strong");
else display.println("Weak");
display.display();
}
// ==================== 按键处理 ====================
void checkButtons() {
static unsigned long lastBtnTime = 0;
if(millis() - lastBtnTime < 200) return;
if(digitalRead(BTN_PAGE) == LOW) {
lastBtnTime = millis();
currentPage = (currentPage + 1) % 6;
}
if(digitalRead(BTN_CLEAR_ALARM) == LOW) {
lastBtnTime = millis();
clearAlarm();
}
if(digitalRead(BTN_SOS) == LOW) {
lastBtnTime = millis();
if(!sosTriggered) {
sosTriggered = true;
sosState = 1;
sosClearTime = millis();
controlBuzzer(true);
delay(500);
controlBuzzer(false);
uploadDataToCloud(); // 立即上传SOS
}
}
if(digitalRead(BTN_ALARM_MODE) == LOW) {
lastBtnTime = millis();
alarmMode = !alarmMode;
}
}
// ==================== 健康判断与报警 ====================
void checkHealthAndAlarm() {
if(!alarmMode) {
alarmField = 0;
controlAlarmLed(false);
return;
}
alarmField = 0;
// 心率异常判断(基于年龄和运动强度)
int minHr = 60;
int maxHr = 100;
if(userAge > 60) minHr = 50, maxHr = 90;
if(userMotion == 1) maxHr += 20; // 强运动
if(userMotion == 2) maxHr -= 10; // 弱运动
if(heartRate > 0) {
if(heartRate < minHr) alarmField |= 0x01; // bit0:心率过低
if(heartRate > maxHr) alarmField |= 0x02; // bit1:心率过高
}
// 血氧异常(低于94%)
if(spo2 > 0 && spo2 < 94) alarmField |= 0x04;
// 体温异常
if(bodyTemp > 0) {
if(bodyTemp < 36.0) alarmField |= 0x08;
if(bodyTemp > 37.5) alarmField |= 0x10;
}
// 摔倒异常
if(fallState == 1) alarmField |= 0x20;
if(alarmField != 0) {
controlBuzzer(true);
controlAlarmLed(true);
} else {
controlBuzzer(false);
controlAlarmLed(false);
}
}
void clearAlarm() {
alarmField = 0;
controlBuzzer(false);
controlAlarmLed(false);
fallState = 0;
sosState = 0;
sosTriggered = false;
}
// ==================== 云平台通信 ====================
void uploadDataToCloud() {
controlUploadLed(true);
// 构建GPS JSON
String gpsJson = "{"lon":" + String(longitude, 6) + ","lat":" + String(latitude, 6) + "}";
// 构建完整JSON
String payload = "{"services":[{"service_id":"stm32","properties":{";
payload += ""GPS_State":" + String((int)gpsState) + ",";
payload += ""temp":" + String(bodyTemp, 1) + ",";
payload += ""HeartRate":" + String((int)heartRate) + ",";
payload += ""SPO":" + String((int)spo2) + ",";
payload += ""MPU6050":" + String((int)fallState) + ",";
payload += ""SOS":" + String((int)sosState) + ",";
payload += ""GPS":" + gpsJson + ",";
payload += ""DHT11_T":" + String(envTemp, 1) + ",";
payload += ""DHT11_H":" + String(envHumidity, 0) + ",";
payload += ""BH1750":" + String(lightIntensity, 1) + ",";
payload += ""user_gender":" + String(userGender) + ",";
payload += ""user_age":" + String(userAge) + ",";
payload += ""user_height":" + String(userHeight) + ",";
payload += ""Alarm_field":" + String(alarmField) + ",";
payload += ""run_mode":" + String(alarmMode) + ",";
payload += ""motion":" + String(userMotion) + ",";
payload += ""blood":" + String(userBlood) + ",";
payload += ""weight":" + String(userWeight);
payload += "}}]}";
// 发送MQTT PUBLISH指令
String mqttCmd = "AT+MQTTPUB="" + postTopic + "","" + payload + "",0,0rn";
gsmSerial.print(mqttCmd);
Serial.println("Upload: " + payload);
delay(500);
controlUploadLed(false);
}
void handleCloudCommands() {
if(gsmSerial.available()) {
String response = gsmSerial.readStringUntil('n');
if(response.indexOf("+MQTTRECV") >= 0) {
// 解析下发指令,格式: +MQTTRECV:topic,payload
int start = response.indexOf("{");
if(start >= 0) {
String json = response.substring(start);
parseUserParams(json);
}
}
}
}
void parseUserParams(String json) {
// 简化的JSON解析,实际可使用ArduinoJson库
if(json.indexOf(""gender"") > 0) {
int idx = json.indexOf(""gender"");
idx = json.indexOf(":", idx);
userGender = json.substring(idx+1, idx+2).toInt();
}
if(json.indexOf(""age"") > 0) {
int idx = json.indexOf(""age"");
idx = json.indexOf(":", idx);
userAge = json.substring(idx+1, idx+4).toInt();
}
if(json.indexOf(""height"") > 0) {
int idx = json.indexOf(""height"");
idx = json.indexOf(":", idx);
userHeight = json.substring(idx+1, idx+4).toInt();
}
if(json.indexOf(""weight"") > 0) {
int idx = json.indexOf(""weight"");
idx = json.indexOf(":", idx);
userWeight = json.substring(idx+1, idx+4).toInt();
}
if(json.indexOf(""blood"") > 0) {
int idx = json.indexOf(""blood"");
idx = json.indexOf(":", idx);
userBlood = json.substring(idx+1, idx+4).toInt();
}
if(json.indexOf(""motion"") > 0) {
int idx = json.indexOf(""motion"");
idx = json.indexOf(":", idx);
userMotion = json.substring(idx+1, idx+2).toInt();
}
if(json.indexOf(""alarmMode"") > 0) {
int idx = json.indexOf(""alarmMode"");
idx = json.indexOf(":", idx);
alarmMode = json.substring(idx+1, idx+2).toInt();
}
}
// ==================== 控制函数 ====================
void controlBuzzer(bool state) {
digitalWrite(BUZZER_PIN, state ? HIGH : LOW);
}
void controlRunLed(bool state) {
digitalWrite(LED_RUN, state ? HIGH : LOW);
}
void controlUploadLed(bool state) {
digitalWrite(LED_UPLOAD, state ? HIGH : LOW);
}
void controlAlarmLed(bool state) {
digitalWrite(LED_ALARM, state ? HIGH : LOW);
}
使用说明:
1. 库文件安装:需要安装以下Arduino库
-
-
- • Adafruit GFX Library• Adafruit SSD1306• Adafruit MPU6050• Adafruit Sensor• MAX30105 by SparkFun• DHT sensor library• TinyGPSPlus• BH1750
-
2. 4G模块配置:上电后需先通过AT指令连接MQTT服务器3. 按键功能:
-
-
- • 按键1:OLED翻页• 按键2:清除报警• 按键3:手动SOS• 按键4:报警模式开关
-
4. 指示灯:
-
-
- • LED_RUN:运行指示(每秒闪烁)• LED_UPLOAD:上传数据时亮起• LED_ALARM:报警时亮起
-
5. MQTT连接初始化(需在setup()中添加):
void initMQTT() {
gsmSerial.println("AT+MQTTCONN="" + mqttClientId + "","" +
mqttUserName + "","" + mqttPassword + "",120,0,"",""");
delay(2000);
gsmSerial.println("AT+MQTTSUB="" + setTopic + "",0");
}
91