
前阵子把家里的智能环境监测系统重新搭了一遍从传感器选型、主控板焊接到数据上云、大屏展示前后折腾了小半年总算稳定跑了快半年。之前用成品传感器网关总觉得数据不准、协议封闭想接自己的告警规则都要看厂商脸色。索性从硬件到云端全部自己设计一遍这篇就把整个设计过程、材料清单、关键代码片段、调试心得一起整理出来供想自建一套智能环境监测系统的朋友参考。这个系统能测温度、湿度、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和蓝牙双核240MHzADC、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是经典入门款尤其是DHT22AM2302精度±0.5℃、±2%RH看起来还行。可它的单总线协议对时序要求非常严格稍微中断一下就会读出错误数据需要写很多滤波逻辑。我后来换成了SHT30I2C接口精度更高而且数据线只有两根不容易被干扰。SHT30价格也才几块钱完全没必要抱着DHT不放。要注意的是SHT30有不同地址版本默认地址0x44I2C上拉电阻一般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精度±50ppm5%读数。这个传感器支持PWM输出和UART输出我用UART读取。关键在于MH-Z19B自带自动校准功能默认打开但这套校准会在传感器认为环境空气干净时把读数归到400ppm左右。如果传感器一直暴露高浓度环境自动校准会产生严重误校准所以我会在长期使用时关掉自动校准改为定期手动校准。**光照强度BH1750是性价比最高的选择。**它也是I2C接口直接输出单位是lux的数值不需要自己换算。BH1750内部有分光响应处理比较接近人眼感知亮度。要注意它的量程可选默认是1~65535lux精度±20%。在强光下可能饱和可以调整测量精度寄存器降低分辨率来扩展量程。**还有一个可选项噪声传感器。**我用的是简单驻极体麦克风模块MAX4466输出模拟量。它本身不是标准分贝计需要自己对输出幅值做RMS均方根计算再映射到dBA。如果对噪声精度要求高就要上数字式声级计探头价格贵很多。我的系统里噪声是“参考级”所以模拟方案就够用了。表格对比一下各个传感器的接口和供电传感器测量参数通信接口供电电压典型精度关键坑点SHT30温度/湿度I2C3.3V±0.3℃/±2%RH需要上拉电阻PMS5003PM1.0/2.5/10UART5V颗粒物计数误差±10%电平转换、风扇寿命MH-Z19BCO2UART/PWM5V±50ppm5%读数自动校准陷阱BH1750光照强度I2C3.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降压到5V5V电源轨直接供给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引脚是GPIO21SDA和GPIO22SCL。这两个引脚在开发板上通常有上拉电阻但外接多设备时建议再补两个4.7kΩ上拉电阻到3.3V提高抗干扰能力。走线要短电源线和数据线分开避免电源噪声耦合到信号线上。PMS5003和MH-Z19B都走UART但ESP32有3个UART控制器可以分别使用。我使用UART2TX2接GPIO17RX2接GPIO16PMS5003的TX接ESP32的RX2MH-Z19B的TX也接RX2不行两个设备不能共用一个RX。正确做法是PMS5003用UART2MH-Z19B用UART1GPIO9和GPIO10。不过ESP32的GPIO10在开发板上可能用于Flash需要确认默认配置。更简单的方案给MH-Z19B单独用软件Serial或者用一块ESP32-C3做协处理器。我在原系统里把MH-Z19B放在UART1PMS5003放在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); // RX16, TX17 CO2Serial.begin(9600, SERIAL_8N1, 9, 10); // RX9, TX10 } 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/dataMQTT 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_keytag_value field_keyvalue 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一直为0TX/RX接反或波特率错检查串口接线确认PMS5003默认9600波特率CO2读数异常偏高传感器在校准关闭自动校准在通风良好处手动校准WiFi频繁断连信道拥挤、供电不足换到5GHz频段如果支持或换2A电源MQTT收不到消息主题不匹配、QoS配置检查Payload格式用MQTT客户端订阅测试Grafana无数据时间戳单位错误确认UNIX时间戳单位InfluxDB默认纳秒光照始终是0I2C地址冲突用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”。校准后一旦数据曲线出现台阶跳变你就能判断是环境变化还是校准偏差这对后期数据分析帮助极大。智能环境监测系统做起来不难难的是把每个环节的数据可信度管住希望这份踩坑记录能让你少走几段冤枉路。