前阵子把家里的智能环境监测系统重新搭了一遍,从传感器选型、主控板焊接到数据上云、大屏展示,前后折腾了小半年,总算稳定跑了快半年。之前用成品传感器网关,总觉得数据不准、协议封闭,想接自己的告警规则都要看厂商脸色。索性从硬件到云端全部自己设计一遍,这篇就把整个设计过程、材料清单、关键代码片段、调试心得一起整理出来,供想自建一套智能环境监测系统的朋友参考。这个系统能测温度、湿度、PM2.5、CO2、光照强度,数据每隔30秒上报一次,在手机和电脑上都能看实时曲线,阈值超了还会微信推送告警。适合室内空气质量监控、小型仓库环境监测、阳台种植棚管理这类场景,动手之前先说清楚:这不是一个“插电即用”的攻略,而是一份讲原理、讲取舍的实战记录。
1. 方案选型:为什么不是Arduino,也不是树莓派
1.1 先想清楚系统要解决什么问题
智能环境监测系统听起来是个大词,落到实际就三件事:采集、传输、展示。采集靠传感器,传输靠无线协议,展示靠服务端平台。但真正设计的时候,最容易被忽略的是“数据要拿来干什么”。如果只是看个温度湿度,几个传感器加一块OLED屏幕就能完事;如果要远程监控、历史回溯、异常告警,那就必须考虑数据如何上云、如何存储、如何以时间序列的方式组织。
我在设计时的核心目标很明确:多参数连续采集,数据至少保留30天,支持远程查看和告警。这套逻辑决定了三个基本决策:主控要有WiFi能力,传感器必须数字化输出(减少模拟采样误差),数据传输走轻量级消息协议而不是HTTP轮询。这些问题想清楚之后,再去看硬件选型就不会被各种参数带偏。
实际规划时,我把系统分成了四层:感知层(传感器)、传输层(主控+通信模块)、数据层(消息服务器+时序数据库)、展示层(可视化面板)。每一层都有一个核心约束,比如感知层的功耗不能太大,传输层的断线重连必须可靠,数据层要能扛住长时间写入,展示层要能快速绘图。有了这个框架,后面所有细节都可以对号入座。
1.2 主控选择:ESP32是均衡选择
一开始我也纠结过用Arduino Uno、树莓派Zero还是ESP32。把需求摆出来对比之后就清楚了:Arduino Uno没有自带WiFi,要外挂ESP8266模块,拧巴不说,稳定性也一般;树莓派性能强,但价格高、启动慢、需要SD卡,而且普通场景用不到Linux那套,功耗还高。ESP32自带WiFi和蓝牙,双核240MHz,ADC、I2C、UART、SPI这些接口齐全,价格只要十几块钱,开发直接用Arduino环境,生态成熟。
我最终选的ESP32-WROOM-32模组,配一个ESP32-DevKitC开发板方便调试。实际量产节点可以换成裸模组自己画板,但对环境监测这种低复杂度产品,开发板完全够用。有人担心ESP32的ADC线性度不好,这个确实是个坑,后面我会单独讲。选ESP32还有一个隐藏原因:它的RTC和深度睡眠模式很省电,如果走电池供电,可以做到让系统大部分时间休眠、定时唤醒采集,对野外部署很有帮助。
1.3 通信协议:MQTT比HTTP更适合传感器上报
很多做小项目的人习惯用HTTP POST把数据扔到服务器,我也这么干过,后来发现问题很多:传感器数据一秒钟可能好几条,HTTP每次都要建立连接、带header,服务器开销大;网络抖动时HTTP请求失败率高,没有消息重发的机制;而且HTTP是“客户端主动请求服务端”,没法做服务端向多端推送。
换成MQTT之后一切顺理成章。MQTT基于发布/订阅模型,传感器作为客户端把数据发布到主题,订阅该主题的服务端和手机App都能实时收到消息。它最小的控制报文只有2个字节,头部开销远小于HTTP。它还支持QoS级别:0是至多一次,1是至少一次,2是恰好一次。环境监测数据我用的QoS 1,保证不丢数据,但也不会因为重试机制卡死网络。
这里给个直接对比结论:同样的数据量,HTTP POST每个包带约200字节额外开销,MQTT只要几十字节;在弱网环境下,MQTT的断线重连和消息保留机制比你自己实现一套HTTP重试要稳得多。后面放代码的时候我会展示实际payload结构,你就知道MQTT这东西有多轻了。
2. 传感器选型与数据采集的硬核细节
2.1 六类传感器怎么选,参数怎么看
智能环境监测系统的核心价值全在传感器上。我分别踩过温湿度、PM2.5、CO2、光照这几条线,把每类的选型逻辑和注意事项列出来。
**温湿度:DHT系列虽然便宜,但推荐用数字I2C传感器。**DHT11和DHT22是经典入门款,尤其是DHT22(AM2302),精度±0.5℃、±2%RH,看起来还行。可它的单总线协议对时序要求非常严格,稍微中断一下就会读出错误数据,需要写很多滤波逻辑。我后来换成了SHT30,I2C接口,精度更高,而且数据线只有两根,不容易被干扰。SHT30价格也才几块钱,完全没必要抱着DHT不放。要注意的是SHT30有不同地址版本,默认地址0x44,I2C上拉电阻一般4.7kΩ到10kΩ,别省略。
**PM2.5:攀藤PMS5003是我比较推荐的。**它用激光散射原理,内部有风扇,把空气抽过传感器腔体,用光电器件测散射光强,换算成颗粒物浓度。它能同时输出PM1.0、PM2.5、PM10三个数值,通过UART串口输出,波特率9600。这个传感器有个特点:数据帧是7个字节一组的协议,需要解析帧头0x42、0x4D,然后校验正确性。我建议用UART2连接,避免和调试串口冲突。PMS5003需要5V供电,而ESP32的GPIO是3.3V电平,好在这个传感器本身就是5V逻辑输出,接入ESP32的串口引脚时要注意两者电平不一致,实际测试中3.3V也可以识别,但稳妥起见可以用分压或电平转换模块。
**CO2:红外NDIR方案是首选。**早期很多人用化学传感器测CO2,但寿命短、漂移大、受温湿度影响严重。我用的MH-Z19B,它内部有一个红外光源和探测器,利用CO2分子对特定波长红外光的吸收特性来测浓度,量程0~5000ppm,精度±50ppm+5%读数。这个传感器支持PWM输出和UART输出,我用UART读取。关键在于MH-Z19B自带自动校准功能,默认打开,但这套校准会在传感器认为环境空气干净时把读数归到400ppm左右。如果传感器一直暴露高浓度环境,自动校准会产生严重误校准,所以我会在长期使用时关掉自动校准,改为定期手动校准。
**光照强度:BH1750是性价比最高的选择。**它也是I2C接口,直接输出单位是lux的数值,不需要自己换算。BH1750内部有分光响应处理,比较接近人眼感知亮度。要注意它的量程可选,默认是1~65535lux,精度±20%。在强光下可能饱和,可以调整测量精度寄存器降低分辨率来扩展量程。
**还有一个可选项:噪声传感器。**我用的是简单驻极体麦克风模块MAX4466,输出模拟量。它本身不是标准分贝计,需要自己对输出幅值做RMS均方根计算,再映射到dB(A)。如果对噪声精度要求高,就要上数字式声级计探头,价格贵很多。我的系统里噪声是“参考级”,所以模拟方案就够用了。
表格对比一下各个传感器的接口和供电:
| 传感器 | 测量参数 | 通信接口 | 供电电压 | 典型精度 | 关键坑点 |
|---|---|---|---|---|---|
| SHT30 | 温度/湿度 | I2C | 3.3V | ±0.3℃/±2%RH | 需要上拉电阻 |
| PMS5003 | PM1.0/2.5/10 | UART | 5V | 颗粒物计数误差±10% | 电平转换、风扇寿命 |
| MH-Z19B | CO2 | UART/PWM | 5V | ±50ppm+5%读数 | 自动校准陷阱 |
| BH1750 | 光照强度 | I2C | 3.3V | ±20% | 强光饱和 |
| MAX4466 | 噪声 | 模拟量 | 3.3V | 参考级 | 需要信号调理 |
2.2 采样周期、均值滤波和异常值剔除
传感器拿到原始数据只是第一步,真正进入系统的数据必须经过处理。我采用了“30秒采集一次、一分钟上报一次”的策略:采集频率太低看到的是瞬时值,波动很大;太高会增加功耗和网络流量。每个采集周期内,对每个参数连续读取5次,去掉最大最小值,取剩余3次的平均值,相当于一个滑动中值滤波。这个方法简单有效,能干掉绝大多数尖峰噪声。
异常值剔除也要做,否则系统会被一个偶发错误值带偏。我在代码里设置了阈值区间:温度-40~80℃,湿度0~100%,PM2.5 0~1000μg/m³,CO2 400~5000ppm。凡是超出物理可能区间的数据直接丢弃,连续超过5个丢弃数据才算一次传感器故障。这个逻辑用起立Flag的布尔变量去跟踪,比单纯阈值判断更抗干扰。
滤波处理放在主控端,而不是云端,这是有讲究的。主控端先做一次降噪,云端只负责存储和展示。如果每个上报周期都传原始序列,数据库会有大量冗余点。按一分钟一条数据算,一个月就是43200条左右,一个6参数系统就是25.9万条记录,时序数据库虽然能扛,但没必要浪费空间。
3. 核心电路与主控设计:接线和供电的坑
3.1 电源设计:5V和3.3V站队不能乱
整个系统的供电方案我吃过几次亏。ESP32开发板通常带有USB转串口芯片和AMS1117稳压器,可以输入5V输出3.3V。但传感器里有5V的PMS5003和MH-Z19B,也有3.3V的SHT30和BH1750,混在一起供电必须想清楚。
我的做法是:外接5V/2A直流电源(或者用12V适配器加DC-DC降压到5V),5V电源轨直接供给PMS5003和MH-Z19B,同时进入ESP32开发板Vin引脚,开发板会把5V稳压到3.3V供给ESP32和I2C传感器。注意不要用ESP32的3.3V引脚去给5V传感器供电,那样压降和电流都不够。反过来,I2C传感器(SHT30、BH1750)接在3.3V轨上,它们的数据引脚也是3.3V,刚好匹配。
这里要重点提醒:PMS5003启动瞬间电流能到100mA以上,MH-Z19B虽然标称平均电流60mA,但红外光源发射时峰值可能到100多mA。如果用一个劣质USB电源,可能电压跌落,导致WiFi重启。所以建议电源余量至少50%。实测整机平均电流在250mA左右,WiFi传输时峰值能到350mA,用5V/2A电源很稳。如果是电池供电,一块2600mAh的18650电池大概只能支撑不到10小时,必须开深度睡眠模式,把采集周期拉长到10分钟,才能做到数天级续航。
3.2 I2C总线和串口的接线细节
SHT30和BH1750都在同一条I2C总线上,ESP32默认的I2C引脚是GPIO21(SDA)和GPIO22(SCL)。这两个引脚在开发板上通常有上拉电阻,但外接多设备时建议再补两个4.7kΩ上拉电阻到3.3V,提高抗干扰能力。走线要短,电源线和数据线分开,避免电源噪声耦合到信号线上。
PMS5003和MH-Z19B都走UART,但ESP32有3个UART控制器,可以分别使用。我使用UART2:TX2接GPIO17,RX2接GPIO16,PMS5003的TX接ESP32的RX2,MH-Z19B的TX也接RX2?不行,两个设备不能共用一个RX。正确做法是PMS5003用UART2,MH-Z19B用UART1(GPIO9和GPIO10)。不过ESP32的GPIO10在开发板上可能用于Flash,需要确认默认配置。更简单的方案:给MH-Z19B单独用软件Serial或者用一块ESP32-C3做协处理器。我在原系统里把MH-Z19B放在UART1,PMS5003放在UART2,调试串口UART0,三个串口互不干扰。
接线还有一个细节:传感器的地线必须和ESP32地线共地。如果传感器单独一个电源而没有共地,串口电平参考点不一致,数据必然乱码。我试过一次接反TX/RX,结果读出来全是0xFF,排查了半天才发现是TM(发送和接收)搞反了。拿万用表测一下各引脚电压基本能定位问题。
3.3 防静电和线缆屏蔽
环境监测系统的传感器通常会拉线到设备外部,PMS5003甚至需要暴露在空气中才能采样。这时候静电放电(ESD)就可能打坏芯片。我有一次冬天装传感器,手碰到PMS5003的金属外壳,第二天PM数据就消失了。后来给传感器外壳贴了接地铜箔,并在信号线上加了一个100nF电容到地,问题才解决。如果你部署的位置靠近空调外机或电机,还要考虑电磁干扰,对PMS5003的电源输入并联电解电容和陶瓷电容,效果立竿见影。
4. 数据采集与通信实现:代码级别的关键点
4.1 传感器读取和滤波的Arduino实现
用Arduino框架写ESP32很方便,下面是我用的核心采集代码,先看结构,再解释。
#include <Wire.h> #include <SHT3x.h> #include <BH1750.h> #include <SoftwareSerial.h> // 传感器对象 SHT3x sht; BH1750 lightMeter; HardwareSerial PMSerial(2); // UART2 for PMS5003 HardwareSerial CO2Serial(1); // UART1 for MH-Z19B float temperature, humidity, lightIntensity; float pm25, co2; void setup() { Wire.begin(); sht.begin(); lightMeter.begin(); PMSerial.begin(9600, SERIAL_8N1, 16, 17); // RX=16, TX=17 CO2Serial.begin(9600, SERIAL_8N1, 9, 10); // RX=9, TX=10 } void loop() { // 每30秒采集一轮 static unsigned long lastSample = 0; if (millis() - lastSample >= 30000) { readSensors(); uploadData(); lastSample = millis(); } } void readSensors() { sht.read(); temperature = sht.getTemperature(); humidity = sht.getHumidity(); lightIntensity = lightMeter.readLightLevel(); readPMS5003(&pm25); readMHZ19B(&co2); // 异常值过滤... }这里注意几个细节:SHT30的读取需要给一段转换时间,连续读太快会拿到上一帧的数据;BH1750同理,它内部有测量周期,读取后要等15ms左右。PMS5003的数据帧解析必须做帧头匹配和校验,不能只取数值字节。下面是我写的一个简化校验版本:
bool readPMS5003(float* pm25) { uint8_t buf[32]; int idx = 0; unsigned long start = millis(); // 循环读取直到凑够一个完整帧 while (millis() - start < 200) { if (PMSerial.available() > 0) { buf[idx++] = PMSerial.read(); if (idx >= 2 && buf[0] == 0x42 && buf[1] == 0x4D) { if (idx >= 28) { // 校验和检查 uint16_t sum = 0; for (int i = 0; i < 28; i++) sum += buf[i]; uint16_t checksum = (buf[28] << 8) | buf[29]; if (sum == checksum) { *pm25 = (buf[12] << 8) | buf[13]; // PM2.5 浓度单位 μg/m³ return true; } } } else if (buf[0] != 0x42 || buf[1] != 0x4D) { continue; // 跳过非帧头字节 } } } return false; }这个代码不是最优的,但思路清楚:先找0x42 0x4D帧头,再等完整帧长度,最后求和校验。我实际用队列缓冲来处理数据,看起来更复杂,但道理一样。写代码时千万不能“读到了就取数”,很多新手就是栽在校验这里。
4.2 MQTT上报和数据格式
上报格式我选择了JSON,虽然比CSV多一点字节,但扩展性好。一个完整的payload长这样:
{ "device_id": "env-01", "timestamp": 1712044800, "temp": 24.5, "humidity": 52.3, "pm25": 18.6, "co2": 520, "light": 320 }发布主题用env/device-01/data,MQTT broker会把该主题的消息推送给所有订阅者,服务器订阅这个主题后写入数据库,手机App也订阅同一个主题,实时刷新。如果想让新接入的设备拿到当前值,可以设置MQTT保留消息(retain), broker会保存最近一条消息,新订阅者立即收到最后状态。这个特性很好用,但要定期清理,否则会存很多无用消息。
MQTT的客户端库我用PubSubClient,连接配置大概是:
#include <WiFi.h> #include <PubSubClient.h> const char* ssid = "your_wifi"; const char* password = "your_password"; const char* mqtt_server = "your_broker_ip"; WiFiClient espClient; PubSubClient client(espClient); void reconnect() { while (!client.connected()) { if (client.connect("env-01", "username", "password")) { client.subscribe("env/device-01/cmd"); } else { delay(5000); } } }重连间隔设置5秒,避免频繁失败打爆broker。上报频率一分钟一次,正好符合MQTT的最佳实践。如果哪一次网络中断,数据会缓存在ESP32的RTC内存或Flash里,等WiFi恢复后补报。这个“断线补传”逻辑很有必要,否则网络一抖数据就缺一块,后期看趋势就直接跳轴。
4.3 时间同步与定时采集策略
环境监测数据必须带时间戳。ESP32可以通过NTP服务器同步时间,我用的configTime(0, 0, "ntp.aliyun.com"),然后在每次采集时调用getLocalTime()获取UNIX时间戳。这里有个坑:ESP32的本地时间默认是1970年,如果你不同步就直接取,入库的数据全是1970-01-01。我处理方式是先等NTP同步成功(最多等10秒)再开始循环,并且在每次采集时检查RTC时间是否合理(比如年份大于2020才接受)。
采集周期用millis()而非delay(),因为环境监测系统可能同时还要处理按键、显示、网络重连,阻塞式delay会拖死整个主循环。如果用FreeRTOS,还可以把采集任务放在高优先级任务里,通信任务放在低优先级,互不干扰。我这里用简单的时间判断就够了。
5. 服务端和可视化:从消息到屏幕
5.1 消息服务器与时序数据库选型
数据从ESP32通过WiFi发出来,第一步到达MQTT Broker。我试过Mosquitto和EMQX。Mosquitto轻量,部署最简单,单机跑个服务就行;EMQX功能更丰富,有Dashboard、集群、规则引擎,适合以后接更多设备。家用级环境监测用Mosquitto就很稳了。
Broker把消息推送出去之后,需要一个消费者订阅主题,把JSON解析后写入时序数据库。时序数据库选型我对比了InfluxDB和Prometheus。Prometheus更适合监控指标抓取,InfluxDB则对写多读少、时间范围查询、连续聚合更友好,所以我用了InfluxDB 2.x。InfluxDB 2.x自带bucket和token机制,写数据用Line Protocol格式:
measurement,tag_key=tag_value field_key="value" timestamp在Node-RED里订阅MQTT主题再写InfluxDB,可视化用Grafana,这个组合非常经典。Node-RED做消息转换很方便,拖拽节点就能把JSON拆成InfluxDB字段。如果不想用Node-RED,也可以直接用Python小脚本订阅MQTT,再用influxdb-client写入。我后来换成Python脚本,因为更好控制失败重试。
5.2 Grafana面板设计心得
Grafana画时间序列图很顺手,但有几个优化点值得说。第一,时间字段必须用UNIX时间戳,单位毫秒,InfluxDB里默认纳秒,用Grafana查询时要注意单位转换,否则横轴会偏差到1970年。第二,图表变量可以直接用$field下拉切换,方便查看不同传感器。第三,不要把所有参数都堆在一张图上,温度和湿度用双Y轴或者分两个面板,PM2.5单独一个面板,因为量纲差太多。我现在的Dashboard有五个面板:温度、湿度、PM2.5、CO2、光照,外加一个表格显示最近一天每小时的平均值。用Grafana的mean()聚合函数,按小时分组,能看出来整天变化趋势。
告警规则也放在Grafana里设置,比在ESP32里写阈值更灵活。比如温度超过35℃持续5分钟,就触发告警,通过Webhook通知到手机(微信推送、钉钉机器人、Telegram Bot都行)。我没用内置的告警邮件,因为延迟高、邮件容易进垃圾箱。
5.3 数据存储的清理与成本控制
环境监测数据虽然不大,但长期跑还是会膨胀。一个设备一分钟一条数据,一天1440条,一个月43200条。如果用默认保留策略,一年就是52万条。InfluxDB可以设置Bucket的Retention Policy,我只保留90天数据,90天前的自动删除。对家用和中小型项目来说,90天足够看季节趋势,再往前的数据价值不大,但是如果想做长期气候变化研究,可以导出来存CSV归档。
另外,建议数据库里加一个device_id标签(tag),按设备分组,这样将来加第二个监测点不会混淆。同一个主控如果以后挂多个传感器节点,每个节点用不同的主题,在服务端按topic分流到对应Tag。
6. 实际部署与调试实录
6.1 部署位置选择:放哪里才算准
很多系统做完后数据精度差,不是传感器不行,是放的位置不对。我把设备放在客厅窗边,阳光直射时温度读数能比房间真实温度高好几度。后面改成放在遮阳的墙角、距离窗户1米以上,并且不要让空调气流直接吹到传感器。PM2.5传感器更讲究,如果靠墙放,空气流通不畅,采样到的颗粒物浓度会偏低;如果直接贴在进风口,又可能抽不进空气。我最终把PMS5003安装在机箱侧面,开了一个进风口,让风扇能自然吸进环境空气。
CO2传感器对安装方向也有要求。MH-Z19B的进气口需要在侧面留出足够的孔洞,不能紧贴外壳,否则内部气流受限,读数迟钝。光照传感器BH1750需要感光面朝上,但又要避开阳光直射,不然会饱和。这些在实际摆放时都要反复测。
6.2 常见问题排查速查表
调试阶段最消耗时间的往往是硬件和通信问题,我把坑整理成一张速查表,你可以直接对照:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 温度读数跳变 | 电源纹波大 | 给传感器电源并联100μF电解电容和0.1μF陶瓷电容 |
| PM2.5一直为0 | TX/RX接反或波特率错 | 检查串口接线,确认PMS5003默认9600波特率 |
| CO2读数异常偏高 | 传感器在校准 | 关闭自动校准,在通风良好处手动校准 |
| WiFi频繁断连 | 信道拥挤、供电不足 | 换到5GHz频段(如果支持),或换2A电源 |
| MQTT收不到消息 | 主题不匹配、QoS配置 | 检查Payload格式,用MQTT客户端订阅测试 |
| Grafana无数据 | 时间戳单位错误 | 确认UNIX时间戳单位,InfluxDB默认纳秒 |
| 光照始终是0 | I2C地址冲突 | 用I2C扫描工具确认设备地址 |
其中WiFi频繁断连这个我印象最深。ESP32在2.4GHz WiFi下,如果同一信道附近有多个路由器,丢包率会明显上升。后来我把路由器的信道固定到1、6、11里的一个,并且关闭了“自动信道选择”,问题立刻缓解。另外,ESP32天线周围不要有金属遮挡,我用塑料外壳,天线竖着放,信号很稳定。
6.3 传感器校准技巧与长期稳定性
传感器不是装上就万事大吉,每类设备的校准手段不一样。温度湿度传感器最简单,把设备放在室内阴凉处和温度计对比,差多少补偿多少。PM2.5传感器要对比标准空气质量站数据,或者和一台权威仪器放在一起做24小时对照。CO2传感器麻烦一点,需要拿到室外空气(400ppm左右)来校准,在通风的室外开机稳定30分钟,然后在串口发校准命令0x A4 0x04 0xFF 0x 00 0x 01 0x 00 0x01之类,具体参考传感器手册。
最容易被忽视的是自动校准时机的坑。MH-Z19B如果长期放在卧室,CO2浓度经常超过1000ppm,自动校准逻辑偶尔会误以为环境干净而把基准拉高,导致读数偏低。我后来在代码里禁用了自动校准,在每年春季开窗通风时手动校准一次。长期使用PMS5003还要留意风扇积灰,风扇转速降低后采样效率下降,虽然传感器内部有自校准,但建议每半年清理一次进风口滤网。
关于长期稳定性,我最后建议把整个系统做成模块化,每个传感器独立置于底座,方便单独拔下来校准或更换。我最初把所有传感器焊死在一块PCB上,结果有一个传感器坏掉,只能整块返工。改成插接件之后维护成本立刻降下来了。这个经验虽然不起眼,但对智能环境监测系统这种需要长期在线运行的东西,省下的时间绝对值得。
最后再分享一个小技巧:使用Grafana的注释(annotation)功能,把每次手动校准的时间点打上标记,比如“CO2校准2024-03-22 14:30”。校准后一旦数据曲线出现台阶跳变,你就能判断是环境变化还是校准偏差,这对后期数据分析帮助极大。智能环境监测系统做起来不难,难的是把每个环节的数据可信度管住,希望这份踩坑记录能让你少走几段冤枉路。