
简介这是一套面向物联网与健康管理方向的智能健康监测系统源码包适合软件开发者、物联网爱好者及健康管理类项目学习者参考。代码以 HTML 页面为载体呈现健康监测系统的核心界面涵盖实时数据展示、健康档案与提醒交互等模块可用于二次开发或快速搭建健康监测原型。资源包共 3 个文件以 HTML 页面、运行环境配置和版本管理文件为主压缩包仅 6KB结构精简便于快速查看与部署。目前已有 172 人学习下载适合用于理解健康监测系统的基础数据展示逻辑与前端实现思路。通过该代码包读者可掌握页面结构、样式交互及轻量级项目组织方式为后续扩展物联网数据接入和数据分析功能打下基础。1. 项目概述与系统架构设计1.1 这个项目到底在解决什么问题智能健康监测系统说白了就是一套能实时采集人体关键生理参数心率、血氧、体温进阶一点还有血压、睡眠质量然后把数据传到手机或云端形成趋势曲线、异常告警的系统。说实话这个方向最近两年特别火但网上大部分资料要么停留在单个传感器的Arduino点灯级别要么直接给你一个商用的黑盒方案真正能落到“自己从零写代码、搞定整套闭环”的教程并不多。我做这个项目的初衷很简单家里老人有心血管方面的老毛病每隔一段时间就得跑社区医院测血氧心率来回折腾。我就想能不能自己搭一套设备让老人每天早晚各测一次数据自动同步万一指标异常我能第一时间收到提醒。后来做完了发现这套东西不仅可以家用稍微改改也能用在养老院床位监测、运动恢复监控、甚至宠物健康跟踪这些场景上。适合谁来参考主要有三类人一是想入门物联网健康硬件开发的嵌入式工程师二是需要快速搭建健康监测原型的创业团队三是高校做课程设计或毕业设计的同学。1.2 整体技术方案选型与理由先交代一下我最终敲定的技术栈后面再逐个讲为什么这么选模块选型说明主控芯片ESP32双核、自带WiFi蓝牙性价比极高心率血氧MAX30102光电容积脉搏波方案便宜、资料多体温DS18B20单总线数字温度传感器精度±0.5℃显示0.96寸OLEDSSD1306本地实时显示方便调试和现场查看数据上传MQTT协议 EMQX Broker轻量级消息协议穿透性好云端存储MySQL 自建API数据沉淀方便做趋势分析可视化Web端 ECharts免费、上手快、图表美观主控这块我毫不犹豫选了ESP32而不是STM32原因很现实这个项目最重的需求不是计算而是联网。ESP32自带WiFi和蓝牙焊上就能用省去外接ESP8266模块的麻烦。STM32性能更强但你要额外配网络模块、写协议栈开发和调试成本直接翻倍。对于健康监测这种低压低功耗场景ESP32绰绰有余。传感器选型上MAX30102是绕不开的一个芯片它是美信出的反射式血氧心率传感器原理是红光和红外光交替照射皮肤组织通过检测透射/反射光强的变化来分析血液容积脉冲。市面上有大量拿它做心率手环的方案。我选它还有一个重要原因它带I2C接口和ESP32通信只需要4根线代码用现成库就能跑起来对新手非常友好。通信协议用MQTT而不是HTTP这个决策我做得比较早。健康监测设备经常处于弱网或移动网络下HTTP的请求-响应模式在这种场景下不稳定而且设备端代码写起来很啰嗦——每个数据点都要构造JSON、管理连接、处理超时。MQTT是发布订阅模式设备端只管往Topic里塞数据Broker负责转发哪怕网络抖动重连也很快这在物联网领域已经是事实标准了。2. 硬件搭建与传感器数据采集2.1 材料清单与接线要点硬件部分材料其实很简单核心就这几样ESP32开发板我用的是ESP32-WROOM-32大概20多块钱MAX30102模块约15元DS18B20防水探头约5元0.96寸OLED屏幕约10元10K电阻一个DS18B20上拉用面包板或洞洞板、杜邦线若干接线是第一个坑点。MAX30102的供电要特别注意——它标称是1.8V-3.3V供电虽然很多模块板载了电平转换电路可以直接接3.3V但你不确定手上的模块是否带稳压就必须先查清楚。我买的模块自带3.3V稳压和电平转换所以可以直接接ESP32的3.3V。具体接线表MAX30102引脚ESP32引脚VIN3.3VGNDGNDSDAGPIO21SCLGPIO22DS18B20的数据线需要接一个4.7K电阻到VCC做上拉我用的是10K也正常工作。数据线我接到了GPIO4注意DS18B20的VCC接3.3V别接到5V上虽然它能承受但会带来额外的发热和功耗。OLED用I2C接口SDA接GPIO21、SCL接GPIO22和MAX30102共用一条I2C总线就行地址不同不会冲突。这样接线省了很多GPIO口这也是I2C的优势。2.2 MAX30102传感器数据采集代码MAX30102的驱动有两个流行的库一个是官方SparkFun MAX30105库另一个是Kandit97的MAX30102库。我用的是SparkFun的库改进版读出来的原始数据需要自己做滤波和心率计算。先看核心初始化代码#include Wire.h #include MAX30105.h #include heartRate.h MAX30105 particleSensor; const byte RATE_SIZE 4; byte rates[RATE_SIZE]; byte rateSpot 0; long lastBeat 0; float beatsPerMinute; int beatAvg; void setup() { Wire.begin(21, 22); Serial.begin(115200); if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { Serial.println(MAX30102 not found); while (1); } particleSensor.setup( 0x1F2, // LED pulse amplitude 4, // sample rate 2, // LED pulse width 100 // ADC range ); particleSensor.enableSPO2(); }这里有几个参数必须解释清楚。setup函数的四个参数分别是LED脉冲幅值、采样率、脉冲宽度和ADC量程。LED幅值决定红外LED的亮度直接影响信号强度。我一开始用默认的0x1F发现信号太弱心率波形几乎是一条直线后来改成0x1F2才正常。不要一上来就跟着别人照抄参数你的传感器模块和手指肤色、佩戴松紧都会影响这个值。采样率4表示每秒4次读取对心率检测来说够了如果想要更平滑的波形可以提高到8或16但功耗和CPU占用都会上去。ADC量程100是最小档适合MAX30102这种近距离反射式应用。心率算法的核心是检测脉搏波峰值。常见做法是拿红外通道数据做带通滤波后用阈值检测峰库里的heartRate.h封装了这层逻辑void loop() { long irValue particleSensor.getIR(); if (checkForBeat(irValue) true) { long delta millis() - lastBeat; lastBeat millis(); beatsPerMinute 60 / (delta / 1000.0); if (beatsPerMinute 40 beatsPerMinute 220) { rates[rateSpot] (byte)beatsPerMinute; rateSpot % RATE_SIZE; beatAvg 0; for (byte x 0; x RATE_SIZE; x) { beatAvg rates[x]; } beatAvg / RATE_SIZE; } } }这里有个细节值得说我加了40-220的BPM范围过滤这是人体的合理生理区间防止外部干扰或传感器抖动导致的异常值污染平均值。另外用了4个采样值做滑动平均比单次测量稳定得多但又不至于滞后太严重。这套心率检测方案的最大痛点是不动的时候准一运动就废——因为运动伪迹会让光电容积脉搏波信号完全失真。如果要做连续运动场景得加加速度传感器做运动伪影消除那就不是这个量级的项目了。2.3 DS18B20温度采集实现DS18B20用OneWire协议数据线既是数据又是时钟代码比I2C稍微费解一点。我用的是DallasTemperature库它封装了底层协议用起来很简单#include OneWire.h #include DallasTemperature.h #define ONE_WIRE_BUS 4 OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(oneWire); void setupTemp() { sensors.begin(); sensors.setResolution(12); // 12位精度0.0625℃分辨率 } float readTemperature() { sensors.requestTemperatures(); return sensors.getTempCByIndex(0); }注意setResolution(12)是最大精度但转换时间也最长需要750ms左右。对体温测量这个场景毫无压力反正一秒最多测一两次。要是做工业温控那种高频场景就得降分辨率换速度了。DS18B20有个很坑的地方它支持寄生供电模式数据线兼做电源但这个模式下线一长就各种不稳定。我的经验是干脆用三线制正常供电就多一根线的事情稳定性提升巨大。2.4 OLED本地显示OLED显示我用的是U8g2库驱动SSD1306 128x64屏幕显示当前心率和温度#include U8g2lib.h U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset*/ U8X8_PIN_NONE); void displayData(int hr, float temp, int spo2) { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); char buf[32]; sprintf(buf, HR: %d bpm, hr); u8g2.drawStr(0, 16, buf); sprintf(buf, SPO2: %d%%, spo2); u8g2.drawStr(0, 36, buf); sprintf(buf, TEMP: %.1f C, temp); u8g2.drawStr(0, 56, buf); u8g2.sendBuffer(); }U8g2这个库选择多内存开销大一点但功能全支持中文和图形绘制。要注意它有两种模式F后缀全缓冲模式会占1KB RAM但绘制更简单ESP32的320KB RAM完全无压力。刷新频率不用太快1秒刷一次足够我实测OLED刷新本身也就几十毫秒不会拖垮主循环。3. 数据上传与云端告警逻辑实现3.1 MQTT协议对接与数据格式定义设备端数据采集完成后接下来才是真正体现“系统”价值的部分——把数据送到云端。我这边用MQTT协议Broker用的是自建的EMQX跑在一台2核4G的云服务器上对家庭规模的应用绰绰有余。MQTT连接的关键参数#include WiFi.h #include PubSubClient.h WiFiClient espClient; PubSubClient mqttClient(espClient); const char* mqttServer your.cloud.server; const int mqttPort 1883; const char* mqttUser device_01; const char* mqttPassword your_password; const char* topic_publish health/monitor/device01/data; const char* topic_alert health/monitor/device01/alert;设备端定时我设置每30秒一个周期发布一组JSON数据{ device_id: device01, timestamp: 1717828200, heart_rate: 72, spo2: 97, temperature: 36.5, battery: 88 }这里有个特别容易忽略的点MQTT的QoS等级。我最初发布消息用QoS 0最多一次结果在弱网环境下偶发丢数据云端曲线断成一段段。后来改成QoS 1至少一次数据完整多了。QoS 1会产生重复投递的可能是存在的但我的场景对重复数据容忍度很高后端用(device_id, timestamp)做唯一索引去重即可性价比很高。另外要提醒的是Topic命名一定要带上device_id做好设备和数据的隔离。很多新手图省事把所有设备都发布到同一个Topic后面做多设备接入的时候就会傻眼。3.2 设备端采集与上报主循环设备端完整的主循环逻辑大概是这样的状态机unsigned long lastPublish 0; const unsigned long publishInterval 30000; void loop() { // 保持MQTT连接 if (!mqttClient.connected()) { reconnectMQTT(); } mqttClient.loop(); // 周期性读取传感器并上报 if (millis() - lastPublish publishInterval) { int hr readHeartRate(); int spo2 readSpO2(); float temp readTemperature(); checkAndSendAlert(hr, spo2, temp); // 先判断是否触发告警 String payload buildJsonPayload(hr, spo2, temp); mqttClient.publish(topic_publish, payload.c_str(), true); mqttClient.publish(topic_alert, NORMAL, true); displayData(hr, temp, spo2); lastPublish millis(); } }三个传感器串行读取一个周期大概100ms左右30秒上报一次对健康监测来说既及时又不费流量。如果做实时监测场景可以把间隔缩到5秒但电池续航会明显下降。3.3 异常告警规则与代码实现告警是这类系统的灵魂。我在设计告警规则时没有傻到只在某个指标超限才告警而是做了一个简单的联合判断void checkAndSendAlert(int hr, int spo2, float temp) { bool abnormal false; String reason ; if (hr 50 || hr 130) { abnormal true; reason heart_rate_abnormal;; } if (spo2 90) { abnormal true; reason spo2_low;; } if (temp 35.5 || temp 37.5) { abnormal true; reason temperature_abnormal;; } // 连续两次异常才真正告警防抖 if (abnormal) { alertCount; if (alertCount 2) { sendAlert(reason); } } else { alertCount 0; } }这里用了连续两次异常才告警的防抖逻辑太重要了。如果不做防抖传感器偶发的一个毛刺就会让手机响个不停几天下来用户就会把通知权限给关了那整个系统就废了。我建议不要用超过2次因为健康异常发现晚是大事2次是务实的折中。告警的发送渠道我先接了一个免费的通知渠道——微信/短信实现方式很多我用的是Server酱的开放接口逻辑极其简单void sendAlert(String alertMsg) { HTTPClient http; String url String(https://sctapi.ftqq.com/YOUR_KEY.send?titleHealthAlertdesp) alertMsg; http.begin(url); int code http.GET(); http.end(); }这个方案的好处是不用写App微信上就能收到推送。如果嫌Server酱免费版额度不够也可以接阿里的短信服务但那个要审核签名模板个人用起来繁琐很多。3.4 云端API与数据存储云端的接收端我用了Node.js写了一个轻量MQTT订阅服务或者是用EMQX自带的Webhook把数据直接写入MySQL。选型上看部署复杂度我最后用了EMQX的规则引擎直接把MQTT消息转发到HTTP API后端只负责解析JSON写入数据库。// Node.js express示例 app.post(/api/health-data, (req, res) { const { device_id, timestamp, heart_rate, spo2, temperature } req.body; const sql INSERT INTO health_records (device_id, ts, hr, spo2, temp) VALUES (?, ?, ?, ?, ?); db.query(sql, [device_id, timestamp, heart_rate, spo2, temperature], (err) { if (err) { console.error(err); return res.status(500).send(insert failed); } res.status(200).send(ok); }); });数据库表结构其实不用复杂核心就是记录表加上设备表。记录表建议给(device_id, ts)建联合唯一索引这样MQTT的QoS 1导致的重复发布就可以通过INSERT IGNORE或者ON DUPLICATE KEY UPDATE消掉。4. 数据可视化与多端查看4.1 Web可视化大屏设计数据进了数据库之后终端展示就是最后一块拼图了。我做了两套展示一套是给家里人用的简易版只看今天的趋势和当前数值另一套是调试用的详细版能看到历史折线和原始波形。前端我选择了比较简单直接的方式一个单页HTML ECharts图表库不需要前端工程化那套复杂的Node构建流程。具体到图表实现上注意ECharts在数据量大的时候性能会下降所以折线图展示时我做了聚合$.ajax({ url: /api/health-history?device_iddevice01range7d, success: function(data) { const hrChart echarts.init(document.getElementById(hrChart)); hrChart.setOption({ xAxis: { type: category, data: data.timestamps }, yAxis: { type: value, name: bpm }, series: [{ name: 心率, type: line, data: data.heart_rates, smooth: true, markLine: { data: [{ yAxis: 50 }, { yAxis: 130 }] } }] }); } });后端做聚合的时候7天的数据几千条直接返回也能渲染但几万条就卡了。我直接按小时取平均和最大值最小值形成一日的迷你趋势手机上看起来也清爽。4.2 本地显示与App端互联的选择移动端我当时权衡了三条路线写原生App工作量最大、用Flutter要配环境重、用微信小程序轻量大用的人多。最后选了微信小程序因为家里人用微信最多不需要额外装App。小程序里用WebSocket或者轮询调后端的REST API展示当前数据和历史图表。这里要单独提醒如果只是自用其实不用一开始就把小程序做得太完善。先用Web端把整个数据和告警链路跑通再考虑移动端。我第一版就是只做了Web端后来确认稳定了才在两天内套了一个微信小程序的壳子。5. 常见问题与排查技巧实录5.1 传感器读数异常排查这部分我踩过的坑太多了整理成表格给后来人直接抄现象可能原因排查方法MAX30102读不到数据/全零接线错误、I2C地址冲突I2C扫描工具看设备地址确认0x57心率数值剧烈跳动传感器贴太松、光照干扰换个深色遮挡环境手指压紧再测血氧低于90但感觉正常传感器信号质量差LED振幅不足调大setup函数第一个参数DS18B20读出来是85℃芯片启动时的ROM暂存值多读几次初始化后延迟500ms再读OLED花屏电源不稳、SPI/I2C速率过高降低I2C速率到400kHz以下加104电容其中心率剧烈跳动问题我费了最多时间。最初以为是滤波算法不够后来发现纯粹的硬件因素手指边沿接触比指腹接触信号好很多还有深肤色、小指尖都会让信号质量下降。MAX30102的LED光强对深肤色天然不友好这是物理限制只能靠调大LED幅值来补偿。5.2 通信断连与数据补发机制MQTT连接不稳定是物联网项目的经典话题了。我遇到过WiFi断连、MQTT Broker重启、网络抖动多种情况。设备端最痛的场景是WiFi断连后重新连上发现MQTT已经断了一分钟这期间采集的数据全丢了。解决办法是在设备端加一个环形缓冲区保存最近50条记录。断线期间数据先存本地重连成功后再批量补发#define BUFFER_SIZE 50 struct health_record_t { unsigned long ts; int hr; int spo2; float temp; } recordBuffer[BUFFER_SIZE]; int bufferHead 0; int bufferCount 0; void bufferRecord(int hr, int spo2, float temp) { recordBuffer[bufferHead] { millis(), hr, spo2, temp }; bufferHead (bufferHead 1) % BUFFER_SIZE; if (bufferCount BUFFER_SIZE) bufferCount; } void flushBuffer() { while (bufferCount 0 mqttClient.connected()) { int idx (bufferHead - bufferCount BUFFER_SIZE) % BUFFER_SIZE; String payload buildJsonPayload(recordBuffer[idx].hr, recordBuffer[idx].spo2, recordBuffer[idx].temp); mqttClient.publish(topic_publish, payload.c_str()); bufferCount--; } }这个环形缓冲区逻辑不复杂但价值很高。健康监测数据有一点时间连续性断几十分钟数据曲线就会出现洞有了补发机制曲线完整度大幅提升。5.3 功耗优化与续航策略如果用电池供电功耗是绕不开的。ESP32在WiFi开启时平均功耗80-200mA18650电池如果2000mAh也就10-20小时。这显然不能满足“全天候佩戴”的需求。我的优化思路降低采样频率心率检测从连续模式改成每10秒采集一次每次采集5秒其余时间深度睡眠。这样可以把平均电流压到20mA以下。使用ESP32的modem-sleep模式WiFi保持连接但周期性唤醒。屏幕关闭策略OLED在非交互状态下30秒自动关闭要到看数据时按键唤醒。esp_sleep_enable_timer_wakeup(10 * 1000000LL); // 10秒唤醒 esp_deep_sleep_start(); // 进入深度睡眠但要注意深度睡眠唤醒后WiFi重新连接需要3-5秒这期间的数据采集不了。权衡下来我用了light sleep而非deep sleep保留RAM数据WiFi重连也更快。实时性要求不高的日常使用这个方案实测能让2000mAh电池撑到3天以上。5.4 数据校准与长期稳定性最后再说一个很少人注意但使用中非常重要的问题数据校准。MAX30102这种光学传感器个体差异极大出厂数据不一定准。我的做法是先用一台指夹式血氧仪超市药店都能买到约50元做基准将MAX30102的读数记录并建立线性修正float calibrateSpO2(float raw, float slope 1.0, float intercept 0.0) { return raw * slope intercept; }体温部分DS18B20本身精度是可以的但要注意传感器贴皮肤的位置差异腋下和额头差0.3-0.5度都正常。我做了“测试模式”来确认偏差把系统同一时间的数据和人工用水银温度计测量对比记录差值然后在云端API层面做修正。校准操作建议隔一个月重复一次因为LED老化、手指干湿度都会影响光学传感器读数。这个环节看起来不性感但整个系统靠的就是数据准确性吃饭这一块不做前面的所有工程投入都是白搭。6. 核心代码复用的建议与项目扩展方向整个项目的代码量其实不大加起来不到500行C和几十行前端JS但背后的工程思路是通用的。我最后想分享两个复用建议。第一传感器采集层和设备通信层是完全可以复用的。如果你要换场景无论是做婴儿睡眠监测还是老人跌倒检测跌倒检测要加MPU6050加速度传感器ESP32MQTT云端这条链路不用重新设计改一下传感器部分就行。我后来给另外一位朋友做宠物健康状况跟踪就是直接把MAX30102换成了贴在宠物耳廓的反射式探头。第二云端部分最好做成多设备可扩展的。初始设计时表结构的device_id字段和Topic的device_id命名看起来只是简单的业务字段实际是未来所有扩展的地基。我最初只做了一台设备后来加了第二台整套代码零改动就接入成功了这就是当初设计时留了扩展位的回报。如果你打算用这套思路做自己的项目我强烈建议先跑通最小闭环一块ESP32加一个MAX30102把数据发到一个免费的公共MQTT Broker上用串口或者手机端MQTT调试助手看一眼数据流。这条路走通之后再慢慢往里加温度、加存储、加可视化、加告警。别一上来就想搞大而全否则很可能卡在传感器接线或者环境配置上热情全被磨没了。我做这个项目最大的体会是智能健康监测系统的难点不在单个传感器怎么读而在怎么把采集、传输、存储、展示、告警这一整套链路做得稳定可靠。这里面的每一个环节单拎出来都不算难但串在一起之后的工程化细节才是真正拉开差距的地方。希望这篇分享能帮你少踩几个坑。本文还有配套的精品资源点击获取