
1. 先想清楚这套系统到底要监测什么测给谁看事情得从一次“数据打架”说起。我家楼下那条街每天晚上八点到十点总有烧烤摊出没手机天气App里显示的AQI永远是“良”但站在路口明显能闻到一股呛人的油烟味。我拿手持检测仪测了一下PM2.5瞬时值直接飙到120μg/m³跟官方数据差了快三倍。这就是我决定自己动手搭一套城市空气质量监测系统的直接原因——官方监测站覆盖的是城市尺度而真正影响我们日常呼吸的是街区尺度甚至楼栋尺度的空气质量。这里要先说清楚一个容易被忽略的概念城市空气质量监测系统Urban Air Quality Monitoring System并不等于把国控监测站那套设备搬回家。国控站用的β射线法、微量振荡天平法单台设备几十万起步那是环保部门的事情。我们自建系统核心价值在于“补盲”——填补官方站点之间的监测空白拿到高时间分辨率、高空间密度的本地数据用来回答几个实际问题小区附近的工地扬尘到底有多严重儿童放学路线的颗粒物浓度高峰期在几点家里开窗通风的最佳时段是什么时候这套系统适合谁做我认为有三类人最能从中受益一是像我一样住在城市里、对居住环境敏感的人二是做环境科学、物联网课程设计的在校学生三是有智慧社区、园区管理需求的工程技术人员。整个项目的硬件成本可以控制在300到800元区间全部使用开源工具链不需要购买任何商业云服务。最终交付的成果包括一个能独立采集PM2.5、PM10、温湿度、TVOC数据的监测节点一条稳定上报数据到服务器的传输链路以及一套可实时查看趋势、支持阈值告警的可视化看板。动手之前先做需求拆解。我把整个系统的功能清单列成了下面这张表后面所有硬件选型和代码编写都围绕这张表展开功能模块具体指标优先级颗粒物监测PM2.5、PM10量程0~500μg/m³精度±10%必选温湿度补偿温度-20~60℃湿度0~100%RH必选气态污染物TVOC/CO₂用于辅助判断污染来源可选数据上报每60秒采集一次每60秒上报一次必选本地显示OLED屏幕实时显示当前读数推荐数据存储云端保留至少30天历史数据必选告警通知超过阈值时推送提醒推荐供电方式市电为主预留太阳能接口推荐这张表确定之后后面的工作就不再是“想到哪做到哪”而是按图索骥。架构上整个系统分四层感知层负责信号采集传输层负责数据上送平台层负责存储和计算应用层负责人机交互。我采用的方案是ESP32作为主控通过串口读取激光颗粒物传感器数据通过I2C读取温湿度和气体传感器数据然后走WiFi用MQTT协议上报到本地服务器服务器端用Node-RED做数据接入InfluxDB负责存储Grafana做可视化看板。这套选型不是拍脑袋决定的后面每一层我都会详细解释为什么这么选。2. 传感器选型是成败关键别被参数表骗了空气质量监测系统的数据可信度百分之八十取决于传感器选型。这个环节如果选错后面不管软件写得多精妙出来的数据都是垃圾。我在这个项目上前后试过六七种传感器组合最后沉淀下来的经验是宁可多花几十块钱选靠谱的传感器也不要为了省钱买那种十几块钱的模块因为后期校准和清洗的时间成本远远超过差价。2.1 颗粒物传感器PMS5003、PMS7003与SPS30的横向对比颗粒物浓度是整个系统最核心的指标也是用户最关心的数据。市面上常见的粉尘传感器分两类红外散射式和激光散射式。红外式的典型代表是夏普GP2Y1010AU0F价格二十多块但精度极差对0.3μm以下的细颗粒几乎无感出数也很不稳定我建议直接跳过。激光散射式是当前性价比最高的方案原理是让空气流过激光照射区域颗粒物散射的光被光电二极管接收通过脉冲宽度和幅值反推颗粒物粒径和浓度。我用过的三款主流激光传感器对比如下型号通信方式测量范围典型精度价格区间特点攀藤PMS5003UART0.3~500μg/m³±10%60~90元出货量最大资料多经典方案攀藤PMS7003UART0.3~500μg/m³±10%70~100元体积更小功耗更低适合便携Sensirion SPS30UART/I2C0.3~1000μg/m³±10%150~200元瑞士品牌带自校准功能我的建议是优先选PMS5003或PMS7003不是因为它最好而是因为它经过全球数百万台空气净化器的量产验证。最关键的逻辑是这类传感器的数据可信度高度依赖出厂校准攀藤的产线校准流程成熟批次一致性相对有保障。SPS30的长期稳定性确实更好但价格翻倍而且官方资料相对封闭。我最终选了PMS5003主要原因有三一是UART串口协议简单数据手册公开解析帧结构很清晰二是风扇内置自带采样气流不用额外设计气泵三是可以在淘宝轻松买到带转接板的版本方便插拔替换。2.2 温湿度传感器SHT30优于DHT22温湿度数据对空气质量监测系统来说不是主角但它是必不可少的辅助数据。颗粒物传感器的测量结果受温度和湿度影响显著湿度过高时水蒸气会在颗粒物表面凝结导致激光散射信号增强读数虚高。此外温度数据还可以用于后续的传感器补偿算法。DHT22是Arduino生态里最常见的温湿度传感器价格便宜但有两个致命问题一是采样率极低最快2秒才能读取一次完整数据二是时序敏感GPIO读取对延时要求非常苛刻在RTOS环境下容易出问题。我实际测试发现用ESP32读取DHT22时如果系统有中断频繁触发偶尔会出现数据全零或CRC校验失败的异常。我最终换成了Sensirion SHT30I2C接口温湿度精度分别为±0.3℃和±2%RH自带CRC校验读取速度远超DHT22。价格也就十几块钱完全在合理范围内。这里插一句题外话SHT30有两种I2C地址0x44和0x45如果你后续想在一根I2C总线上挂两个传感器可以利用这个特性不用额外做地址切换。2.3 气体传感器CCS811的边界与坑如果预算允许建议加装一个TVOC传感器。TVOC是“总挥发性有机物”的缩写包括甲醛、苯系物、烷类等室内常见污染物。室外环境下TVOC数据可以用来辅助判断附近是否有化工厂、加油站或餐饮油烟排放。我选型时对比了CCS811和BME680。CCS811是AMS公司的产品I2C接口直接输出TVOC和CO₂当量值使用门槛低。但它的坑也不少一是需要至少48小时的“老化”时间前48小时的数据漂移很大二是它内部自带一个微型加热器如果散热不好读数会持续偏高三是它的CO₂数据实际上是“等效CO₂”并非真实CO₂浓度只能用于趋势参考。BME680则更加复杂它输出的是原始电阻值需要自己换算成空气质量指数开发门槛高一些。从“开箱即用”的角度我建议新手先用CCS811但要做好心理准备——它的绝对值不能太当真看趋势比看具体数值更有价值。实际的模块接线上CCS811的I2C地址是0x5ASHT30是0x44两者挂在同一总线上不会冲突。2.4 传感器一致性为什么同一批传感器读数能差30%这是整个项目里最容易被低估的问题。我一开始做了两个监测节点用的是同一家店买的两个PMS5003放在同一位置同时开机PM2.5读数差别最高的时候能到30%。一开始我以为是传感器坏了后来查资料才知道这涉及到两个因素一是激光传感器的光学腔体在生产过程中存在装配公差导致同样浓度的颗粒物产生不同的散射信号强度二是传感器内部的风扇转速存在个体差异导致采样气流流量不同。怎么应对这个问题两个思路一是做“多点平均”如果你计划部署多个节点不要拿单个节点的绝对值跟官方数据对比而是用整个网络的平均值去对比这样个体偏差会互相抵消一部分二是做“基准校准”把传感器拿到已知浓度的环境里与标准仪器对照生成修正系数。具体的校准方法我在后面第6章详细展开这里先记住一个结论不要迷信传感器出厂数据一定要在自己的环境里重新校准。3. 硬件主板选型与电路连接ESP32为什么是首选确定传感器之后主控选型就顺理成章了。我用的是ESP32-WROOM-32模组开发板选择的是ESP32 DevKitC兼容版三十块钱左右功能完全够用。选择ESP32而不是Arduino Uno或ESP8266是出于一套完整的评估逻辑。3.1 ESP32、ESP8266、Arduino Uno三选一这三个平台我都实际用过各自的定位差异很明显对比维度Arduino UnoESP8266ESP32主频16MHz160MHz240MHz双核WiFi无需外接模块内置内置蓝牙无无双模蓝牙ADC精度10位10位12位数字IO14个17个25个价格25元15元30元可扩展性低中高Arduino Uno的问题显而易见没有内置WiFi需要外接ESP8266模块走串口AT指令不仅占用了大量IO资源还增加了通信出错的概率。而且它只有2KB SRAM处理稍复杂的JSON数据包就捉襟见肘。ESP8266支持WiFi价格更便宜但只有一个核心、一个ADC通道而且是10位分辨率。对于传感器数据采集来说如果以后想挂模拟输出的传感器10位ADC意味着最大只能分辨到约4.9mV的电压变化精度不够用。ESP32的240MHz双核架构有一个实实在在的好处我可以把WiFi协议栈放在Core 1上跑把传感器采集和数据处理放在Core 0上跑互不干扰。实际项目中WiFi的射频活动会产生大量中断如果和传感器读取跑在同一核上偶尔会导致UART数据帧丢失。双核设计从根本上解决了这个问题。3.2 电路连接与电源设计整个系统的接线比想象中简单因为大部分传感器都是数字接口不需要复杂的模拟前端。按照PMS5003的数据手册它需要5V供电串口是3.3V TTL电平输出脚可以直连ESP32的串口RX。但要注意ESP32的TX是3.3V电平而PMS5003的RX脚如果在某些版本上没有内置电平转换建议串联一个1kΩ电阻做保护防止烧坏传感器。接线顺序如下PMS5003的VCC接5V电源正极GND接公共地TXD接ESP32的GPIO16UART2 RXRXD接GPIO17UART2 TX如果不需要向传感器发送命令可以不接。SHT30的VCC接3.3VGND接公共地SDA接GPIO21SCL接GPIO22这两根线分别接10kΩ上拉电阻到3.3V。CCS811的VCC接3.3VGND接公共地SDA和SCL与SHT30并联在同一条I2C总线上WAK引脚不接。OLED显示屏SSD1306128x64同样挂在I2C总线上地址通常为0x3C。如果使用继电器或LED做本地告警GPIO25接一个NPN三极管来驱动。电源是整个项目里最容易出问题的环节没有之一。ESP32在WiFi发射瞬间的峰值电流可以达到300mA以上而PMS5003内部风扇启动时也有约100mA的冲击电流如果电源余量不足会出现WiFi频繁断开、传感器读取超时等疑难杂症。我推荐的电源方案是5V/2A的USB适配器先接入一个大容量电解电容470μF/16V做储能缓冲再接入AMS1117-3.3V稳压芯片给ESP32和I2C传感器供电。这里有一个重要的细节模拟地和数字地要单点连接防止数字信号的高频噪声干扰模拟信号虽然本项目没有模拟传感器但这个习惯很重要。3.3 户外防护外壳通风比防水更考验设计很多人的系统在室内跑得好好的一放到窗外就开始数据漂移最核心的原因不是传感器坏了而是外壳设计不合理。空气质量监测设备需要空气流动到传感器内部参与采样所以防护外壳既不能让雨水和虫子进去又不能把气路完全封死。我的设计方案是“双层结构”最外层是带百叶窗的防雨罩可以用ABS塑料盒自行改造四周开45度向下倾斜的百叶窗条槽宽5mm既能保证空气流通又能阻断垂直落下的雨水。内层是传感器舱室PMS5003自带风扇会主动吸气出风口朝下这样颗粒物不会在舱内沉积。实测在大雨天这个结构能保持内部干燥。另外一个被忽略的细节是传感器进风口的防虫网。PMS5003的风扇吸力不大但足以吸进小飞虫。我的第一版外壳在夏天运行两周后传感器内部发现了一只干掉的蚂蚁直接导致读数偏高。后来在进风口加了一层孔径为0.5mm的尼龙网问题彻底解决。要知道这种细节不会出现在任何数据手册里只能靠实际部署经验补上。3.4 功耗预算与太阳能供电的取舍如果监测点附近没有插座太阳能供电是唯一选择。但这里必须算清一笔账别被广告宣传里的“太阳能板电池”组合忽悠了。以我的节点为例ESP32在正常工作状态下的平均功耗约为80mA含WiFiPMS5003风扇约100mAOLED屏幕约20mA总平均电流约200mA按12V电池计算相当于2.4W功耗。如果使用ESP32的深度睡眠模式每10分钟唤醒采集一次再上报平均功耗可以降到50mA以下但这样会牺牲数据连续性。太阳能方案推荐搭配20W单晶硅太阳能板 12V/12Ah铅酸电池 PWM充电控制器。这个组合可以保证在连续阴雨天3天内不断电但晴天时发的电有大量剩余属于超配方案。如果只是做短期一周以内的临时监测完全可以采用18650电池组方案4节18650锂电池串联成14.8V加上TP4056充电模块成本低很多但需要手动充电。我最后的选择是直接使用5V/2A的USB适配器供电理由很简单我在阳台上找到了一个空调插座。能用市电就不要折腾太阳能把精力留给数据质量——这是我在这个项目中一个非常实际的经验总结。4. 固件开发从串口协议解析到MQTT稳定上报主控选好了传感器接好了接下来是软件部分。这部分是整个系统的大脑和神经我按照“读取-处理-传输”三层结构来组织代码。开发环境用PlatformIO比Arduino IDE更适合管理依赖和做工程化配置。4.1 PMS5003串口协议解析别用字符串函数要按帧解析PMS5003的数据手册定义了一个固定帧结构每帧32个字节以0x42 0x4D开头紧接着是帧长度、校验码、PM1.0、PM2.5、PM10数据。新手最容易犯的错误是把串口数据当字符串读然后用strlen或strstr处理这在二进制协议上完全行不通。正确的做法是写一个状态机解析器。我在代码里用环形缓冲区接收串口数据然后在主循环里逐个字节匹配帧头。核心逻辑如下// 状态机解析PMS5003帧 enum ParserState { WAIT_FOR_START, WAIT_FOR_SECOND, READ_FRAME }; bool parsePMSFrame(uint8_t byte, PMSData *data) { static ParserState state WAIT_FOR_START; static uint8_t buf[32]; static uint8_t idx 0; static uint8_t checksum 0; switch (state) { case WAIT_FOR_START: if (byte 0x42) state WAIT_FOR_SECOND; break; case WAIT_FOR_SECOND: state (byte 0x4D) ? READ_FRAME : WAIT_FOR_START; if (state READ_FRAME) { buf[0] 0x42; buf[1] 0x4D; idx 2; checksum 0x42 0x4D; } break; case READ_FRAME: buf[idx] byte; if (idx 30) checksum byte; // 校验码是除最后两位之外的所有字节求和 idx; if (idx 32) { state WAIT_FOR_START; // 校验帧尾的16位校验码 uint16_t sum (buf[30] 8) | buf[31]; if (sum (checksum 0xFFFF)) { // 数据从buf[4]开始PM1.0, PM2.5, PM10 >// EWMA滤波器 float filteredPm25 0; float alpha 0.3; void applyFilter(float newValue) { filteredPm25 alpha * newValue (1.0f - alpha) * filteredPm25; if (filteredPm25 0) filteredPm25 0; }中值滤波我用来处理偶发的“毛刺”数据。PMS5003偶尔会输出一个离谱的值比如瞬间跳到500以上然后马上回落这通常是传感器光学腔体内有尘埃颗粒或者风扇转速波动造成的。中值滤波的做法是维护一个长度为5的滑动窗口每次取窗口内的中位数作为输出值。实测证明这个方案能有效消除99%以上的异常尖峰而不会像低通滤波那样滞后响应真实浓度变化。异常值的另一个来源是传感器自检。PMS5003在上电后前10秒内输出的数据是不稳定的因为激光二极管需要时间达到工作温度风扇也需要时间建立稳定气流。我的代码里做了一个简单策略开机后20秒内的数据只用于建立滤波初值不参与上报。4.3 MQTT上报断线重连和QoS选择数据链路我选择MQTT协议这也是物联网领域最成熟的轻量级消息协议。它的发布/订阅模型非常适合多节点数据汇聚的场景一个Broker可以接收多个监测节点的数据下游的应用服务通过订阅Topic获得数据流。Broker我用的是本地树莓派上部署的Mosquitto。每个监测节点使用唯一的Client IDTopic格式为sensor/{node_id}/dataPayload使用JSON格式方便下游解析{ node_id: balcony_node_01, timestamp: 1700000000, pm25: 35.2, pm10: 52.1, temperature: 24.5, humidity: 60.2, tvoc: 180, rssi: -55 }MQTT的连接质量决定了整个系统的可用性。ESP32的WiFi连接在长时间运行后可能因为路由器DHCP租约过期、信道拥挤等原因断开如果没有断线重连逻辑节点就会静默死亡。我的方案是主循环中每30秒检查一次MQTT连接状态断开时先尝试重新连接WiFi再重新连接MQTT Broker。同时设置KeepAlive为60秒让Broker能及时检测到“僵尸连接”并清理资源。关于QoS级别我选择QoS 0。原因是空气质量数据是周期上报的偶尔丢一帧可以接受而QoS 1需要Broker确认如果网络质量不好重传队列会越积越多反而拖垮节点。数据平台上再做丢帧补全的逻辑比在传输层做强保证要划算得多。4.4 本地显示与交互OLED屏幕的取舍OLED屏幕主要用于本地调试和数据可视化。SSD1306这种128x64分辨率的屏幕虽然小但足够实时显示PM2.5、PM10、温度和湿度四个关键指标。我用的是U8g2库支持中文显示。设计屏幕界面时有个原则一屏只放最关键的数据不要试图把所有信息都堆上去。我的主界面分四行分别是PM2.5、PM10、温度/湿度、WiFi信号强度。每5秒切换一次翻页显示TVOC数据和系统运行时间。U8g2库有一个比较隐蔽的性能问题它默认的刷新模式是全帧缓冲占用1KB的RAM而且I2C接口在128x64分辨率下刷新率只有约15帧/秒。如果你在界面上加入了大量动态变化元素屏幕会出现明显的闪烁。解决办法是使用u8g2.setDisplayRotation()和局部更新功能只刷新变化区域。这个优化虽然效果明显但代码复杂度会稍微上升如果对屏幕刷新没有强迫症可以忽略。4.5 OTA远程升级这步不做每次改代码都要爬阳台户外部署的节点如果每次改代码都要拆下来重刷固件维护成本会高到让你放弃升级。所以从第一版固件开始我就集成了OTAOver-The-Air远程升级功能。ESP32的Arduino生态自带ArduinoOTA库使用非常简单#include ArduinoOTA.h void setupOTA() { ArduinoOTA.setHostname(air-quality-node-01); ArduinoOTA.setPassword(your-ota-password); ArduinoOTA.begin(); } void loopOTA() { ArduinoOTA.handle(); }OTA功能让我的远程维护效率提升了至少十倍。现在如果我要调整滤波系数或者上报频率只需要重新编译固件在PlatformIO里选择OTA上传一分钟就能完成。不过使用OTA有一个硬性前提新固件不能破坏WiFi连接逻辑否则上传成功后节点失联那就真成了“搬起石头砸自己的脚”。5. 数据平台与可视化让数据从“数字”变成“信息”传感器采集到的原始数据只是零散的数值要让它们发挥价值必须经过存储、处理和可视化。这一章我讲平台选型、数据库设计、看板搭建和告警配置。前端展示我用Grafana这是目前最成熟的开源可视化平台。5.1 数据接入层选型Node-RED足够别过度设计数据接入层的职责是接收MQTT消息经过解析后写入数据库。这里有三条路线一是用Node-RED的MQTT节点直接接入优点是零代码图形化拖拽二是用Python写一个常驻脚本订阅MQTT并写库优点是灵活可控三是用Telegraf这类专门的数据采集器优点是与InfluxDB无缝集成。三条路线我都试过最终选用了Node-RED。原因很简单Node-RED的MQTT节点自带QoS设置和自动重连机制不需要自己处理消息确认和异常重试而Python脚本方案虽然灵活但需要额外处理进程守护、日志轮转这些运维问题。Telegraf虽然性能好但配置格式是TOML调试不如Node-RED直观。Node-RED的流程设计也非常直观一个mqtt in节点监听sensor/#主题输出到function节点做JSON解析和字段重组再输出到influxdb out节点写库。整个流程节点加起来不超过6个部署后在浏览器里能看到每个节点的实时消息量排查问题时一眼就能定位到是数据没进来还是写库失败。5.2 时序数据库选型与数据表设计空气质量数据本质上是时间序列数据天然适合用时序数据库存储。InfluxDB是这个领域的标杆而且1.x版本使用简单不需要理解Flux查询语言。我用的InfluxDB 1.8数据库名叫air_quality每个监测节点使用独立的measurement。数据模型设计上有一个重要的概念需要区分tag和field的区别。tag是索引字段用于过滤和分组比如节点ID和地理位置field是实际数值比如PM2.5浓度和温度。tag应该只存低基数数据不能把每次测量都不同的数值如PM2.5塞进tag否则会导致索引膨胀。我的measurement设计如下MeasurementTagFieldpm25node_id, locationvaluepm10node_id, locationvaluetemperaturenode_id, locationvaluehumiditynode_id, locationvaluetvocnode_id, locationvalue存储策略上我设置了两个retention policy原始数据保留30天每5分钟采样的降采样数据保留1年。这样可以平衡存储空间和查询性能。InfluxDB的连续查询功能可以自动做降采样CREATE CONTINUOUS QUERY cq_5m_pm25 ON air_quality BEGIN SELECT mean(value) AS value INTO air_quality.1year.pm25_5m FROM air_quality.30days.pm25 GROUP BY time(5m), node_id, location END这个CQ会在后台每分钟执行一次把30天窗口内的原始数据聚合为5分钟均线数据写入1年窗口。查询历史趋势时走降采样表查询实时明细时走原始表两者互不干扰。5.3 Grafana看板设计不能只有折线图要有信息层级Grafana的默认面板布局很容易做得花里胡哨但真正好用的看板应该有清晰的信息层级。我把看板分成三层顶层是当前状态概览中间是趋势分析底层是原始数据查询。顶层我用Stat面板显示当前PM2.5、PM10、温度和湿度四个关键指标同时配置了颜色阈值比如PM2.5低于35μg/m³显示绿色35到75显示黄色超过75显示红色。这一步非常重要因为人眼对颜色的感知远比对具体数字的感知更快速。管理员打开看板的第一眼就应该知道当前空气状况是好是坏。中间层用Time series面板展示过去24小时的PM2.5和PM10趋势曲线。有两个可视化细节值得注意一是把Y轴的单位设置为μg/m³并启用对数刻度因为PM2.5和PM10的浓度可能跨越两个数量级线性刻度会让低浓度期的曲线变化完全不可见二是叠加显示国控站点的AQI等级背景色带这样能直观看到自建节点与官方数据的偏差趋势。底层我用Table面板展示最近100条原始记录方便排查问题时查看具体数值。这张表只对管理员可见不参与日常监控。5.4 告警规则阈值触发后还要对齐时间和位置告警是整个系统里最能体现“自动化价值”的功能。Grafana内置了Alerting模块可以基于面板查询条件配置阈值告警支持通过Webhook推送到企业微信、钉钉或者邮件。我配置了三条告警规则PM2.5超过75μg/m³国标二级限值持续15分钟以上触发“污染事件”告警。节点离线超过5分钟触发“设备离线”告警。温度超过50℃或低于-10℃触发“环境异常”告警用于判断设备是否暴晒或冻结。配置告警时有一个坑Grafana的告警条件是基于最后一个数据点如果节点数据上报间隔是60秒告警查询的时间范围必须大于上报周期否则会出现“漏报”。我的规则里专门设置了for: 15m表示连续15分钟满足触发条件才发出告警这样可以过滤掉瞬时尖峰带来的误报。告警通知渠道我选择了企业微信群机器人因为它支持Markdown格式可以在消息中带上当前数值、触发时间、节点位置和Grafana看板链接业务相关人员点开就能看到完整上下文。实际运营中告警阈值要根据季节动态调整冬季北方采暖期间的PM2.5基线明显高于夏季固定阈值会导致告警疲劳这个我在后面“可持续运营”部分会展开聊。6. 部署到真实城市环境校准、防护、长期维护系统能在实验室跑通不等于能在城市真实环境中稳定运行。这一章我讲部署过程中最容易被忽略、但影响最大的三个环节校准、防护和长期维护。这些都是我在阳台和小区公共区域实际部署中积累出来的经验。6.1 数据校准跟官方站点做“邻域对比”不要幻想绝对准确自建监测系统的数据无论如何都不可能达到国控站点的精度水平但这不代表校准没有意义。校准的目标不是让传感器读数等于官方数据而是让传感器读数与官方数据保持可预测的线性关系这样你才能判断官方AQI上涨时你的节点是否也能同步上涨。最实用的校准方法是“邻域对比法”。在距离你家最近的国控站点3~5公里范围内连续运行你的设备一周同时每天记录官方公布的PM2.5小时均值然后做线性回归分析。理想的拟合结果是官方值 a × 设备值 b其中a应该在0.8到1.2之间b的绝对值不应超过20R²不应低于0.7。如果R²低于0.7说明你的传感器读数与官方数据之间没有稳定的相关性这时候需要先检查设备的位置是否遮挡严重、传感器进风口是否被污染。我自己的校准结果是PMS5003在PM2.5低于50μg/m³的区间与国控站相关系数R²为0.82斜率a为0.95偏移b为3.2。这个精度对于民用级别的环境监测已经完全够用。校准完成后把修正系数写入固件配置// 校准系数官方值 0.95 * 设备值 3.2 #define CALIB_SLOPE 0.95f #define CALIB_OFFSET 3.2f float calibratedPm25 filteredPm25 * CALIB_SLOPE CALIB_OFFSET;需要特别说明的是校准系数会随季节和地域变化。湿度高的季节颗粒物吸湿增长会导致低浓度段读数偏高北方沙尘季大颗粒物占比高PMS5003对PM10的测量误差会明显增大。所以建议每个季度重新校准一次雨天多的季节可以缩短到两个月。6.2 部署位置选点比你想的更讲究城市环境下监测节点的安装位置直接决定了数据的代表性。我一开始把节点挂在阳台栏杆上结果数据显示午餐和晚餐时间段PM2.5明显升高后来发现是楼下餐饮店的排烟管道正好在节点的下风向。这个例子说明部署位置的选择本身就是数据可信度的组成部分。位置选点遵循几个原则离地面高度3~15米避免地面扬尘影响也避免楼顶高空层污染浓度偏低。距离污染源道路、排烟口、施工工地至少10米以上除非你专门监测该污染源的影响。避免安装在空调外机旁边热气流和风扇扰动会影响颗粒物的自然扩散。选择通风良好的位置避免建筑角落的死角区那里空气滞留会导致颗粒物沉积。如果条件允许可以安装两个节点做对照一个靠近主要污染源方向一个远离污染源方向这样不仅能监测平均浓度还能判断污染来向。我后来增加了第二个节点放在小区内部花园与大门口节点形成对照能明显看出交通高峰期两个位置的浓度差异对小区物业评估停车区扬尘治理效果很有帮助。6.3 传感器需要呼吸进风口清灰周期和预防性维护空气质量监测设备常年暴露在灰尘环境中传感器光学腔体一定会被污染这是物理规律无法避免。养护的核心目标是让传感器在污染到“读数失真”之前得到清理。我的维护周期表维护项目周期操作内容进风口滤网检查每月取下尼龙网用软毛刷清理灰尘堵塞严重时直接更换传感器外壳除尘每季度打开外壳用压缩空气吹扫内部灰尘风扇清理每半年拆开PMS5003用小毛刷清理风扇叶片灰尘光学腔体清洁每年或读数异常时用无水乙醇棉签轻擦激光窗口注意不要触碰激光二极管重新校准每季度重复官方站点邻域对比更新修正系数风扇清理这一步很多人会忽略但实际上风扇转速下降对采样流量的影响非常直接流量不足会导致传感器吸入的颗粒物数量减少读数偏低。我踩过这个坑我的节点在运行到第5个月时PM2.5读数系统性偏低约20%当时还以为是传感器老化后来拆开发现风扇叶片上积了一层灰清理之后读数值立刻恢复正常。从那以后我养成了每半年强制拆机清灰的习惯。6.4 数据质量的日常监控不只看平均值要看数据变异系数系统上线后运营者会面对海量数据。如果只盯着PM2.5平均值你会很难发现问题。我推荐关注两个指标一是数据上传率。正常节点24小时应上报1440条数据如果60秒一条如果当天上传率低于95%说明设备可能有间歇性断线问题。我通过Grafana的count聚合就能看到每个节点的上报率。二是读数的变异系数。变异系数是标准差除以平均值反映了数据的离散程度。正常环境下的PM2.5小时变异系数通常在0.2到0.8之间。如果某个节点的变异系数长期低于0.1数据波动极小这通常意味着传感器已经“死”了读数固定在某个值附近比如传感器被堵住了或者I2C通信异常如果变异系数大于2说明数据抖动严重可能是传感器进入自检模式或者电源供电不稳。我经历了多次故障后发现变异系数这个指标比单纯的阈值告警更早发现问题。有一次节点报上来的数据保持24小时几乎无波动我通过变异系数发现异常过去检查发现是微控制器死机后串口数据被中断接收端读到的全是上次的残留数据。重启之后恢复正常。6.5 可持续运营从“做了一个项目”到“运营一套网络”最后一个心得是关于项目的长期运营。很多DIY项目在开发阶段热火朝天一旦跑通了就失去新鲜感不再维护。但空气质量监测系统恰恰是一个“越跑越有价值”的项目因为它的核心价值来自于长期积累的数据趋势。你收集一年以上的数据后才能回答“今年冬天的雾霾比去年严重还是减轻”这类需要长周期视角的问题。我的建议从一开始就想清楚系统要运行多久并在设计上做好运维准备。OTA升级功能让固件迭代变得轻松远程重启通过GPIO控制的继电器实现这样即使设备死机也能通过局域网内的重启指令恢复。我还把系统的监控面板做成了家庭电视的屏保自动播放让家人也参与进来有人发现数据异常会主动告诉我这比我一个人盯着Grafana看板高效得多。在我部署这套系统的第二周隔壁邻居看到我挂在阳台上的白色盒子好奇地问我在做什么。我当时指着手机上的App告诉他“楼下烧烤摊今晚几点开张我现在能提前半小时猜到。”他半信半疑地看了我一眼。后来有一次烧烤摊刚支起来不过十分钟我手机上的PM2.5曲线就开始抬头等开烤的时候已经突破了100μg/m³。邻居看到数据后默默在业主群里发了一条消息。这种感觉大概就是技术人做环境监测最直接的成就感——不是跑通了一个Demo而是真正让看不见的东西变得可见了。