样本温湿度上传全链路:ESP32/STM32、MQTT与可信数据
2026/9/17 5:54:20 网站建设 项目流程

现场样本的温湿度变化能不能被完整记录下来,关键不在于你手上那块传感器精度标称多少,而在于从采样到上传到平台端、再到平台把这条曲线和具体某个样本对应起来的整条链路是否闭合。我做过几个冷链箱、粮仓和实验室留样的监测项目,最常见的翻车场景是这样的:设备本地存的数据漂漂亮亮,平台端的曲线却断断续续,运维看一眼就说"网络问题",换张卡、换个位置,问题还在。其实温湿度上传这件事,真正难的地方在数据传输的可靠性、时间基准的一致性,以及样本语义的绑定。这篇内容适合正在用 ESP32、STM32 这类方案做环境监测,或者准备把现场数据接进云平台、自建平台的开发者,也适合负责样本管理的业务方了解数据是怎么"可信"起来的。

1. 样本温湿度上传,卡住人的往往不是"采不到数"

很多人第一次做这个功能,目标定得很简单:传感器读出来,连上 WiFi,往平台发一条。跑通了 Demo 就以为完事了。真上现场之后才发现,读数是读到了,但平台端的曲线一会儿缺一段、一会儿又多出一段重复的,样本台账上写着"全程 2~8℃",可曲线里明明有两小时是 11℃。问题不在"采",在采完之后的三道坎。

1.1 三个断层:数据断层、语义断层、可信断层

第一个是数据断层。设备用自己内部的 RTC 打时间戳,平台用服务器时间入库,两边差几分钟甚至几小时都很常见。设备断电重启后时间归零,如果没有 NTP 校时,上报上来的时间戳可能是 1970 年或者 2000 年。这种数据混进时序库,按时间范围查询的时候就会莫名其妙地"消失",因为查询窗口根本覆盖不到它。

第二个是语义断层。平台收到的是{"temp": 5.2, "humi": 61},它并不知道这条数据属于哪个样本、哪个箱子、哪一批货。如果设备是循环复用的,今天贴在这箱试剂上,明天贴到另一箱生鲜上,那平台上如果没有明确的绑定关系,你拿到的数据就没法用来判定任何一箱货物的状态。这个断层是很多项目上线后才暴露的,因为做硬件的人默认"一台设备对一个样本",做业务的人默认"数据里应该带着样本号",谁都没写进协议。

第三个是可信断层。无线链路不是有线,丢包、重连、重复投递都是常态。MQTT 用 QoS1 的时候,同一份数据可能被投递两次以上;HTTP 上报超时后客户端重试,服务端如果没做幂等,就会入两条相同的记录。曲线上一旦出现重复点或空洞,你用来做告警的阈值判断就会误报或者漏报。

1.2 一条完整链路都包含哪些环节

把整条链路摊开,从传感器到平台可以分成五层,每层都有各自的职责和典型坑:

层级典型器件或组件核心动作最常见的坑
感知层SHT30、SHT31、DHT22、DS18B20周期采样、数字滤波自发热、结露、安装位置贴壁
控制层ESP32、STM32F103、STM32L4组帧、本地缓存、低功耗调度断电丢数据、RTC 漂移、看门狗复位
传输层WiFi、4G Cat.1、NB-IoT、LoRa 网关建立连接、上报、重传弱网超时、DNS 失败、证书过期
接入层MQTT Broker、HTTP 网关、云 IoT 平台鉴权、限流、消息路由Topic 权限、连接数上限、消息体超限
应用层时序库、业务库、看板、告警服务入库、聚合、判定、展示重复入库、时区错乱、样本未绑定

这张表建议你在动手前先照着过一遍,把每一层谁负责、用什么实现先定下来。我见过太多项目是先把硬件焊好、代码写完,到对接平台的时候才发现数据模型对不上,回头改协议,硬件那边的缓存结构也得跟着动,代价比前期想清楚大得多。

1.3 先定数据契约,再画电路图

一个特别省事的做法是:在画原理图之前,先把上报报文的 JSON 结构定死,然后反推设备端需要哪些能力。比如下面这个结构,是我目前在用的一个精简版本:

{ "dev": "SN20240517001", "sample": "BATCH-A-20240517-03", "seq": 10247, "ts_dev": 1715948621, "ts_dev_ok": true, "temp": 5.2, "humi": 61.4, "batt": 3.72, "rssi": -71, "flags": 0 }

几个字段值得单独说。seq是设备侧单调递增的序号,用来做去重和丢包检测——平台收到 10245 之后直接跳到 10247,中间那条大概率丢了。ts_dev_ok标记设备时间是否已经完成过 NTP 校时,平台拿到false的时候就用服务端接收时间入库,避免用 1970 年的时间戳污染时序库。flags是一个位图,可以塞传感器故障、开盖、低电量这些状态位,比每个状态单独加一个字段更省流量。

注意:把样本编号放在上报报文里,而不是只靠设备和样本的绑定关系表来查,是因为绑定表需要维护,一旦有人改了绑定而没通知平台,历史数据就乱了。报文里带样本号,入库存一份快照,绑定表只作为辅助修正手段。

2. 采集端选型:ESP32 和 STM32 到底该怎么选

这个选择被问得最多,其实没有绝对答案,取决于你的现场条件。判断标准不是"哪个芯片更强",而是"你的现场有没有稳定供电、有没有 WiFi 覆盖、电池要撑多久、样本箱是不是移动的"。把这四个问题回答清楚,选型基本就定了。

2.1 传感器选型的三个陷阱

先说传感器。SHT30/SHT31 这类数字式温湿度传感器,标称温度精度 ±0.3℃、湿度 ±3%RH,实际用起来最容易被忽略的是自发热结露。自发热指的是传感器内部电路工作产生的热量会让读数比环境高 0.2~0.5℃,尤其是采样频率高、封装散热差的时候。我在一个试剂箱项目里就遇到过,同款传感器,贴在箱壁内侧的比悬在箱子中央的高出 0.6℃,一度导致低温告警误报。

结露的问题更隐蔽。从冷库取出的样本箱放进常温环境,传感器表面会起一层水膜,湿度读数会直接冲到 100%RH 并持续一段时间,温度读数也会因为水膜蒸发吸热而偏低。如果这时候正好触发阈值告警,你就会收到一堆无意义的告警。常规做法是在固件里加一个简易判定:湿度连续多次读到 100%RH 且温度下降速率异常,就把这组数据标记为可疑,或者干脆等待一个稳定窗口再上报。

DHT22 便宜、资料多,适合做验证,但长期漂移比 SHT3x 大,而且它对采样间隔有要求,最短两秒一次,密集采样很容易读到错误值。DS18B20 只测温,精度不错、防水封装好做,做纯温度监测(比如冷链)其实很合适,就是别忘了它需要一条上拉电阻,而且一总线挂多个探头时要注意布线长度。

2.2 ESP32 方案:验证快、联网省事,但要防"睡死"

如果你的现场有 WiFi 覆盖、供电相对稳定(哪怕是 USB 充电宝级别的移动电源),ESP32 是最省心的选择。它把 WiFi 协议栈、TCP/IP、TLS 都做在芯片里,用 Arduino 框架写起来非常快。下面这段是我常用的最小上报逻辑,核心是深睡眠 + 定时唤醒 + 上报后立刻断连,而不是一直保持长连接:

#include <WiFi.h> #include <PubSubClient.h> RTC_DATA_ATTR uint32_t seqCounter = 0; void setup() { float t = readTemp(); float h = readHumi(); WiFi.begin(SSID, PASS); unsigned long t0 = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t0 < 8000) { delay(100); } if (WiFi.status() == WL_CONNECTED) { PubSubClient mqtt(client); mqtt.setServer(BROKER, 1883); if (mqtt.connect(DEV_ID)) { String payload = buildJson(++seqCounter, t, h); mqtt.publish(TOPIC_UP, payload.c_str(), false); mqtt.loop(); delay(200); } WiFi.disconnect(true); } esp_sleep_enable_timer_wakeup(60ULL * 1000000ULL); esp_deep_sleep_start(); }

这里有几个细节是踩过坑才加的。delay(200)是为了让 MQTT 的 PUBLISH 报文真正发出去,因为publish()只是把数据放进发送缓冲,立刻进深睡眠会丢报文。用RTC_DATA_ATTR保存序号是因为深睡眠会清空普通内存变量,不保存的话每次醒来 seq 都从 0 开始,平台端的去重逻辑就失效了。

ESP32 最常见的问题是"睡死"或者连不上路由器。前者一般是唤醒后 WiFi 连接超时没做兜底,程序卡在while循环里直到看门狗复位,或者干脆电池耗干;所以上面的代码里我加了 8 秒超时。后者通常是路由器限制了单设备连接数,或者设备用的静态 IP 和现场网段冲突,工程部署前一定要在目标现场实测一轮,别只在办公室里调通就发货。

2.3 STM32 + 独立通信模组:适合低功耗与工业现场

STM32 本身没有网络能力,需要外挂模组。好处是控制力强、功耗可以做得很低(STM32L4 系列 STOP 模式下电流能到微安级),适合电池要撑几个月的场景,比如集装箱内的独立记录仪。坏处是你得自己处理很多事:模组的 AT 指令时序、TCP 重连、缓存管理、TLS 握手(如果要走 HTTPS)。

我的做法是把整个系统分成三层:采集任务只负责把温湿度写进环形缓冲区,通信任务负责从缓冲区取数据往外发并标记已发送,管理任务负责看门狗喂狗和低功耗调度。三层通过一个简单的状态机交互,不要在中断里做任何网络相关的操作——模组的 AT 交互耗时动不动几百毫秒,放在中断里会打乱所有时序。

工业现场还有一个绕不开的点是 RS485/Modbus。很多现成的温湿度变送器是 Modbus RTU 输出的,STM32 通过 485 芯片去轮询。轮询要注意帧间隔和超时重试,总线上设备多了以后,一次轮询周期可能超过你的采样间隔,这时候要么降低轮询频率,要么把关键探头单独挂在一条总线上。

2.4 采样周期怎么定,缓存要留多大

采样间隔不是拍脑袋定的。我的经验是:先看业务要求的最短异常持续时间,再倒推采样间隔。如果业务上认为"温度超过 8℃ 持续 10 分钟才算异常",那你的采样间隔至少要小于 5 分钟,最好 1~2 分钟,否则你连异常持续了多久都算不准。

缓存容量按最坏情况算。假设 1 分钟一条,报文 120 字节,现场可能断网 3 天,那就是 4320 条、约 520KB。ESP32 有 4MB 以上 Flash,用 LittleFS 存一个小文件完全够;STM32 如果只有 64KB RAM,就得上外部 SPI Flash,用环形队列的方式覆盖最旧的数据。要预留一个水位线告警,缓存用到 80% 的时候往平台发一条状态上报,让运维提前知道这台设备可能马上要丢数据了。

3. 上传链路的三条路:MQTT、HTTP,以及"先落地再补偿"

传输方式的选择直接决定了你能拿到什么质量的数据。这块我不想讲教科书式的对比,直接说结论:移动网络或 WiFi 相对稳定的场景用 MQTT,网络很抖或者要穿透复杂网络限制的场景用 HTTP 批量上报,绝对不稳定的场景用"本地存储 + 事后补偿"

3.1 MQTT 的 Topic 设计和 QoS 选择

Topic 设计上,我偏好把上行和下行分开,并且把设备维度放在前面,方便做主题级权限控制:

# 设备上行 up/env/{deviceId}/telemetry up/env/{deviceId}/status # 平台下行 down/env/{deviceId}/cmd down/env/{deviceId}/config

这样在 Broker 的 ACL 里可以简单写成"设备只能发布up/env/{自己的deviceId}/#,只能订阅down/env/{自己的deviceId}/#",安全边界非常清楚。如果多个设备共用一套账号密码,出问题的时候你连是哪台设备被别人顶替了都不知道。

QoS 的选择是个权衡。QoS0 最快但会丢,只适合高频非关键数据。QoS1 保证送达但可能重复,所以你的平台端一定要用seq做去重,或者在入库前用(deviceId, seq)建唯一索引。QoS2 理论上只送一次,但握手开销大,低功耗设备上不太划算——我一般不用。

还有一个容易忽略的点是保留消息和遗嘱消息。给status主题设 retained,平台新订阅的时候能立刻拿到设备最后状态,不用等下一次心跳;遗嘱消息(LWT)用来在设备异常断连时发布一条离线通知,这样平台能及时发现掉线设备。这两样配起来,运维端的"在线状态"才准。

3.2 HTTP 上报的坑:超时、重试和幂等

HTTP 的好处是通用,任何平台都支持,中间经过的代理、网关也不用特殊配置。坏处是每次请求都有完整的 TCP/TLS 握手开销,低功耗设备上很费电,而且长连接容易被中间设备掐断。

用 HTTP 时必须做三件事。第一是设置合理的超时,连接超时给 5 秒、读超时给 10 秒,别用默认的无限等待。第二是指数退避重试,失败后等 1 秒、2 秒、4 秒再试,最多三次,超过就写回本地缓存等下一轮。第三是幂等设计,请求头里带一个唯一请求 ID(可以就用deviceId + seq),服务端看到重复 ID 直接返回成功但不入库。

批量上报能显著降低开销。把 10~30 条数据打成一个数组发一次,比逐条发省很多电。但批量大小要控制,报文体太大会被网关或平台拒绝,常见的限制是 32KB 到 100KB 不等,保险起见单批控制在 8KB 以内。

3.3 弱网现场的本地缓存与断点续传

如果你的样本箱要经过隧道、地下车库、偏远地区,那"先落地再补偿"是唯一的正解。设备端维护一个环形队列,每条数据分配一个递增 seq,同时记一个"已确认发送到哪个 seq"的游标。网络恢复后,从游标位置开始批量补发,服务端返回最新确认的 seq,游标推进。

# 服务端批量接收的简化逻辑 def ingest(rows): inserted, latest = 0, None for r in rows: key = (r["dev"], r["seq"]) if redis.sadd("seen:" + r["dev"], r["seq"]): db.execute(INSERT_SQL, r) inserted += 1 latest = max(latest or 0, r["seq"]) return {"ok": True, "inserted": inserted, "ack_seq": latest}

这里用 Redis 的集合做去重是个廉价有效的办法,缺点是会占内存,可以用带过期时间的键或者定期清理。如果数据量不大,直接在时序表上建(dev, seq)唯一索引更省事,冲突时忽略即可。

补发的时候要注意限速。几十台设备同时恢复网络,一起往上怼数据,平台侧的写入压力会瞬间拉高。我在设备端加了一个令牌桶,每秒最多发 5 条,或者每批之间 sleep 200 毫秒,效果很明显。

3.4 上报失败怎么定位:一份对照清单

"上传失败:网络请求错误"这种报错信息见过太多了,它几乎什么都没告诉你。下面这张表是我自己总结的定位顺序,按可能性从高到低排:

现象可能原因定位手段
一直连不上 BrokerDNS 解析失败、Broker 端口被拦用 IP 直连测试、换端口试
TLS 握手失败设备时间不对、根证书过期检查 NTP 是否成功、更新证书
连上就断,反复重连客户端 ID 冲突(两台设备同 ID)用唯一 ID,看 Broker 日志踢人记录
单条上报成功,批量失败报文体超过平台限制抓包看报文大小,减小批量
平台侧收到但没入库Topic 权限不对、字段名不匹配看 Broker 订阅日志、校验 JSON schema
数据重复QoS1 重投、HTTP 重试未做幂等按 (dev, seq) 去重
时间戳错乱RTC 未校时、时区处理错误统一用 UTC 秒级时间戳

排查的时候有一条铁律:先在设备端打印原始报文,再在 Broker 或网关侧抓一次,最后看数据库。三个环节对比,问题出在哪一段一目了然。最忌讳的就是直接去改代码,改完发现还是不行,最后连最初的现象都复现不出来。

4. 平台端怎么"看懂"样本:数据模型与告警逻辑

数据传上来了,平台端的活儿才刚开头。要让平台能回答"这批样本现在还好吗",需要三样东西:一张能把设备、样本、批次串起来的关系模型,一套符合业务定义的判定规则,以及能扛住查询压力的存储结构。

4.1 样本、设备、批次的三张表

我的习惯是把业务数据时序数据分开存。业务库(MySQL 或 PostgreSQL)存关系,时序库(TDengine、InfluxDB、TimescaleDB 都行)存温湿度点位。业务库大概长这样:

CREATE TABLE sample ( sample_id VARCHAR(32) PRIMARY KEY, batch_no VARCHAR(32) NOT NULL, name VARCHAR(128), temp_min DECIMAL(4,1), temp_max DECIMAL(4,1), start_at DATETIME, end_at DATETIME ); CREATE TABLE device ( dev_id VARCHAR(32) PRIMARY KEY, model VARCHAR(32), fw_ver VARCHAR(16), last_seen DATETIME ); CREATE TABLE binding ( id BIGINT AUTO_INCREMENT PRIMARY KEY, dev_id VARCHAR(32), sample_id VARCHAR(32), bind_at DATETIME, unbind_at DATETIME, UNIQUE KEY uk_dev_sample (dev_id, sample_id, bind_at) );

关键在binding表用了时间区间,而不是简单的"设备属于样本"一条记录。因为设备会复用到不同的样本上,历史查询的时候必须按上报时间落在这个区间里去找对应样本。如果只存一条当前绑定,那查上周的数据就会全部对到今天这个样本上,这是很典型的错误。

时序表里我建议冗余存一份sample_id。虽然有点反范式,但查询"某个样本的完整温湿度曲线"的时候,一句WHERE sample_id = ?就能搞定,不用去 join 业务库,性能差别非常大。

4.2 阈值告警之外,还要看累计暴露量

简单的高低温阈值告警是最基础的,但它解决不了"样本到底还能不能用"这个问题。温度在允许范围内波动,和温度长时间贴着上限走,对样本的影响是不一样的。所以我在平台上加了两个指标:

一个是累计超限时长。规定温度上限 8℃,任何一次高于 8℃ 的持续片段都累加起来,超过比如 30 分钟就升级告警。计算方式是遍历降采样后的序列,统计连续超出区间的点数和采样间隔的乘积。

另一个是MKT(平均动力学温度),这个概念在药品和食品冷链里用得比较多,简单说就是用阿伦尼乌斯方程折算出来的等效温度,能反映"温度波动对样本的实际影响相当于在某个恒温下放了同样久"。简化后的表达式是:

MKT = (ΔH / R) / ln( ( e^(-ΔH/(R·T1)) + e^(-ΔH/(R·T2)) + ... ) / n )

其中 ΔH 是活化能(常用 83.144 kJ/mol),R 是气体常数 8.314 J/(mol·K),T 是绝对温度。这个公式看着吓人,代码里就是一个循环累加指数项,几十行就能实现。它的好处是能把"虽然没超限,但一直在偏高的位置波动"这种情况量化出来。

4.3 查询统计功能怎么做到又快又不失真

样本监测的查询有两个特点:实时看板要秒级刷新最近的曲线,历史回溯要查几个月前的完整记录,还要出统计报表(最高温、最低温、平均湿度、超限次数)。这两种需求的存储策略完全不同。

实时部分用原始精度存,保留 7 到 30 天,按天分区。历史部分做降采样,1 分钟原始数据聚成 5 分钟或 1 小时的平均/最大/最小,长期保留。降采样必须保留最大最小值,不能只留平均值——平均值会把一个瞬间的高温尖峰抹平,而那个尖峰恰恰是判定的关键。

统计报表不要每次实时算。用定时任务在每天凌晨跑一次预聚合,把结果写进一张统计表,前端查报表直接读这张表,响应时间从几秒降到几十毫秒。这个改动在数据量上来之后几乎是一定要做的,早做早省心。

5. 现场联调和长期运行中,我踩过的几个坑

代码在办公室里跑通,和现场稳定运行三个月,完全是两码事。下面这些是我在真实项目里反复遇到的问题,写出来供你避坑。

5.1 时间同步这件事,比想象中重要

设备的 RTC 一天漂几秒很正常,一个月下来就是几分钟。如果平台用设备时间戳入库,那不同设备之间的曲线就对不齐了。我的做法是双轨制:设备每次联网优先做一次 NTP,失败就用内部 RTC 并在报文里把ts_dev_ok置为 false;平台侧落库时同时存两个时间戳,一个是上报时间(服务端接收时间),一个是设备声称的时间。做判定和告警的时候统一用服务端时间,做设备行为分析的时候再看设备时间。

还有一个细节是时区。设备端和平台端一律用 UTC 秒级时间戳,展示层再转成本地时区。我见过一个项目,设备发的是本地时间,平台当作 UTC 存,看板上所有曲线整体偏了 8 小时,排查了一整天才找到。

5.2 传感器装在哪里,决定了数据准不准

这一条不属于软件问题,但对数据质量的影响比软件大。几个原则:传感器不要贴箱壁、不要靠近电池和电源模块、不要放在出风口正对的位置、尽量放在样本区域的几何中心或者代表性位置。如果是多个探头,要做一次交叉比对,把偏差超过 0.5℃ 的探头找出来做偏移校准。

偏移校准可以写进设备的配置里,平台下发一个temp_offset参数,设备读取后参与计算。这样换探头或者重新标定的时候不用重新烧固件。

5.3 上线后最常出问题的五个地方

按我这几年遇到的频率排一下:

  • 电池和供电:低温环境下锂电池容量骤降,一个标称能撑 30 天的设备可能 10 天就关机了。做低温场景一定要用低温电池,或者直接上外接电源。
  • SIM 卡或网络资费:流量跑超被限速、卡被运营商回收,这类问题在批量部署后非常常见,建议做流量的用量监控和告警。
  • 设备重连风暴:某个区域基站维护或者路由器重启,几十台设备同时重连,把 Broker 的连接数打满。给重连加随机抖动(0~30 秒随机延迟)能有效缓解。
  • 数据库写入压力:设备数上千之后,逐条 insert 会拖垮时序库,一定要用批量写入,批次大小 500~1000 条。
  • 配置变更没生效:平台下发了新的采样间隔,设备因为没订阅到或者没有持久化,重启后又变回旧的。配置参数要存在设备的非易失存储里,并且在每次上报时带上当前配置的版本号,方便核对。

排查顺序我一般是:先看设备是否在线,再看最近一次上报时间,然后抓一条原始报文看内容,最后看数据库有没有入库。绝大多数问题在前两步就能定位,剩下的小部分去看平台日志。

最后分享一个我一直在用的小技巧:给每台设备加一个"心跳包"和一个"自检上报"。心跳包每 30 分钟发一次,只带电量、信号强度和缓存水位,不带业务数据,专门用于判断在线状态;自检上报在设备启动时发一次,带上固件版本、传感器型号、校准偏移量。这两类报文的数据量都很小,但能让你在排查问题的时候省掉大量猜测——你一眼就能知道这台设备是什么版本、电量还剩多少、缓存有没有堆积,不用连现场、不用派人去看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询