基于ESP32与MQTT的AirBox空气质量监测系统:从传感器到实时曲线的完整实现
简介这是一份基于物联网的AirBox空气质量监测与展示系统源码包面向物联网初学者、嵌入式开发者以及关注环境监测的创客解决低成本环境数据采集与可视化展示需求。项目采用经济型传感器与主控板可检测PM1.0、PM2.5、PM10、温度、湿度等多类参数支持WiFi或BLE传输可选配OLED显示屏并通过HTTP API将数据接入Kibana等平台展示。这份代码能帮助读者打通从传感器驱动、数据上报到网页监控看板的完整流程同时为后续二次开发提供清晰范例。资源共17个文件压缩包约273KB其中包含Arduino固件、Node.js后端服务、前端监控页面、JSON配置、Shell部署脚本、Markdown说明与License文件目录按硬件端、服务端、网页端和部署脚本划分结构清晰便于按模块阅读和扩展。已有34人学习下载适合希望掌握AirBox项目原理并基于现有代码搭建或扩展自己空气质量监测系统的开发者对于需要快速搭建环境监测原型的团队或实验课程也同样适用。 很多人以为 AirBox 这类空气质量监测系统最难的部分是传感器选型实际上拿到源码之后你会发现硬件部分两小时就能焊完真正卡人的是数据链路传感器数据怎么采上来、怎么稳定上报、怎么在前端画成曲线这中间每一环都有看不见的坑。这套基于物联网的 AirBox 空气质量监测与展示系统源码目标很明确用一块带 WiFi 的开发板接上颗粒物、温湿度、VOC 等传感器把数据通过 MQTT 传到服务端再由 Web 页面实时展示。适合物联网毕设、智能家居入门、嵌入式开发练手也适合想快速搭一套家庭空气质量监测的同学参考。1. 从看不见的空气到实时曲线AirBox 项目的需求拆解1.1 为什么需要一个“带源码”的空气质量监测系统刚装修完的房子、雾霾天的卧室、长时间密闭的办公室空气里的 PM2.5、甲醛、CO2 这些指标靠鼻子根本闻不出来但长期待在这种环境里身体反应比想象中明显。与其买个几百块的成品检测仪不如自己搭一套——既能实时看到家里空气质量变化又能顺便把物联网整套技术链路摸一遍做毕设还能拿去答辩。AirBox 这个名字本身就很直白Air Box一个装进盒子里的空气质量监测站。市面上这类产品很多但成品基本是封闭系统数据拿不出来更不能联到自己家的智能家居里去。而“带源码”的意义在于从底层传感器驱动到上层前端图表全部可控想改采集频率就改采集频率想接新风系统联动就接新风系统联动。1.2 功能需求拆解三条业务链路我把这个系统的需求拆成三条主线第一条采集链路。这是最底层的工作要解决“空气数据从哪来”的问题。核心指标一般包括 PM2.5、PM10、温湿度、CO2 或 TVOC。传感器把物理量变成电信号主控芯片通过 I2C、UART 或 ADC 接口把信号读成数值。第二条传输链路。解决“数据怎么到远端”的问题。主控连上家里 WiFi通过 MQTT 协议把打包好的 JSON 数据发布到服务器。这一层要重点处理断线重连、消息可靠性和设备标识。第三条展示链路。解决“数据给谁看、怎么看”的问题。后端服务订阅 MQTT 主题把数据写入数据库同时通过 WebSocket 推给浏览器前端用图表组件画出实时曲线和历史趋势。三条链路串起来就是一个完整的物联网项目闭环。很多毕设项目的问题在于只做了第一条和第二条加个串口助手显示数据就算完事没有展示端整个系统就不够完整。这套 AirBox 源码的价值恰恰在于它把三条链路都补齐了。2. 分层架构与硬件选型每个模块为什么这么选2.1 传感器选型测什么、用什么测、差别在哪传感器是整个系统里差异最大的一部分。不同原理、不同价位的传感器精度和稳定性差着一个量级。我按这类项目最常见的组合整理了一张表检测项常见传感器接口方式特点与注意点PM2.5 / PM10攀藤 PMS5003 / PMS7003UART 串口激光散射原理读出的就是数字精度相对靠谱PM2.5低成本方案夏普 GP2Y1010AU0FADC 模拟量便宜但需要自己换算电压到浓度还要做校准CO2攀藤 MH-Z19C / 森尔 S8UART / PWM红外非色散原理适合测 CO2 浓度价格偏高eCO2 / TVOCSensirion SGP30 / SGP40I2C输出等效 CO2 和总挥发性有机物适合评估室内空气综合质量甲醛电化学模块 ZE08-CH2OUART / ADC电化学原理上电需要预热读数才能稳定温湿度SHT30 / SHT31I2C精度高长期稳定性好推荐温湿度入门DHT22 / AM2302单总线便宜够用但读取间隔必须大于 2 秒连续读会出错这套源码里最核心的传感器是 PMS5003 系列。它是激光散射式粉尘传感器内部有一个小风扇把空气抽进去激光打在颗粒物上产生散射光光电探测器把光信号转成电信号最后在传感器内部直接算出 PM2.5 和 PM10 的浓度通过串口以固定帧格式输出。选它而不是 GP2Y1010 的原因很简单数字输出、自带校准、接口简单。GP2Y1010 虽然便宜但输出电压和浓度之间不是线性关系换算公式还会随温湿度漂移新手很难调准。如果是做毕设想显得专业一点推荐“PMS5003 SGP30 SHT30”这个组合颗粒物、TVOC/等效CO2、温湿度全覆盖三样加起来一百多块钱I2C 和 UART 两种接口也都能在源码里体现出来。2.2 主控与通信选型为什么 ESP32 是这类项目的稳妥选择主控方案有三个常见选择Arduino Uno / Mega、ESP8266、ESP32含 ESP32-S3。Arduino Uno 在教学里很常见但它没有 WiFi必须外接 ESP8266 模块或者 ENC28J60 网卡通信链路会很别扭。ESP8266 有 WiFi 但外设资源紧张跑传感器采集加 MQTT 勉强够用可一旦要接 OLED 再加几个传感器内存就比较吃紧。ESP32 是这套源码最合适的主控理由很实际内置 WiFi 和蓝牙不用外挂通信模块双核 240MHz可以一个核跑传感器采集、一个核跑网络任务ADC、I2C、UART、SPI 外设齐全DHT22 接 GPIO、SGP30 走 I2C、PMS5003 走 UART互不冲突。而且 Arduino 生态对 ESP32 支持非常成熟烧录调试的教程一大堆遇到问题好查。如果手头是 ESP32-S3 也没关系大致逻辑完全一致只是部分引脚编号不同。我见过有人为了“用上 S3 的 AI 加速功能”强行选它做毕设实际这个项目根本用不到属于给自己找麻烦。2.3 数据链路设计从传感器到前端展示的完整流向整个系统的数据流向是这样的传感器 - ESP32 主控采集/打包 - MQTT Broker - 后端服务订阅/存储 - WebSocket - 浏览器图表MQTT 在整个链路里扮演的是“消息中枢”角色。主控作为发布者把数据发到某个主题比如airbox/device_01/data后端服务订阅这个主题收到消息后解析 JSON写入数据库再通过 WebSocket 推给前端页面前端收到数据就更新曲线。这样主控、服务端、前端三者之间完全解耦任何一个模块出了问题其他模块不会直接崩掉。展示端有两种常见实现方式。一种是后端只做一个 MQTT 到 WebSocket 的转发浏览器直连 WebSocket 收数据适合 demo 演示另一种是后端把数据落到 MySQL 或 SQLite前端除了实时曲线还能查看历史区间适合毕设答辩时展示“数据持久化”能力。这套源码的后端按第二种方式实现因为只做实时展示的话一刷新页面数据就没了评委老师问一句“历史数据怎么看”就会卡住。3. 源码核心逻辑拆解从传感器帧到网页曲线3.1 PMS5003 串口帧解析最容易写错的地方PMS5003 的输出是固定格式的二进制帧每帧 32 字节帧头是0x42 0x4D接着是 2 字节数据长度通常为 0x001C即 28 字节然后依次是 PM1.0、PM2.5、PM10 的浓度值最后是校验和。最常踩的坑就是串口数据粘包。ESP32 的 UART 寄存器里可能有半帧数据如果不做帧同步就按固定偏移去解析大概率读出乱码。正确做法是先找帧头再按长度字段读完整帧// 伪代码示意串口帧同步 uint8_t b; while (Serial2.available() 0) { b Serial2.read(); if (!frameStarted) { if (b 0x42) frameStarted true; continue; } if (b ! 0x4D) { frameStarted false; continue; } // 已收到帧头 0x42 0x4D uint16_t len read16(); // 数据长度 uint8_t buf[28]; readBytes(buf, len); // 读取数据区 uint16_t pm25 (buf[2] 8) | buf[3]; // 第 2/3 字节是 PM2.5 uint16_t pm10 (buf[4] 8) | buf[5]; // 第 4/5 字节是 PM10 frameStarted false; }这里还有一个容易忽略的细节PMS5003 输出的 PM2.5 分为CF1和ATM两套值CF1是标准颗粒物浓度ATM是大气环境下换算值。室内监测一般用ATM那组更贴合体感但如果要跟气象站数据对比就得用CF1。源码里默认取ATM改代码时不要选错偏移量。3.2 采样、滤波与数据上报传感器裸读数直接上报会有一个问题数值跳变非常厉害。PMS5003 在风扇转动、有人走动、甚至旁边开了一下门的情况下单次读数都可能波动几十微克每立方米。所以源码里不能只做“读一次发一次”必须做采样策略。我建议的实现方式是滑动窗口均值滤波每 2 秒采集一次原始数据缓存最近 10 个读数取平均值作为最终上报值。这样既不会因为单次异常读数据报警误报也不会因为滤波窗口太大导致响应迟钝。SGP30 这类 VOC 传感器本身就有内部算法上电后需要一段“老化校准”时间前 12 小时的读数参考意义不大这点可以在文档里标注清楚。上报的数据包用 JSON 格式打包后端解析起来最省事{ deviceId: airbox_01, timestamp: 1710000000, pm25: 23, pm10: 35, temp: 26.5, humidity: 58.2, tvoc: 120, eco2: 520 }上报间隔建议设为 5 秒到 10 秒。设成 1 秒的话前端图表会因为数据点太密看起来像一团乱麻Broker 和数据库压力也会变大ESP32 频繁发包还会增加 WiFi 功耗和掉线概率。5 秒一包既能看出变化趋势也不会把数据表撑得太快。3.3 MQTT 重连与下行控制通道只做上行上报的系统是不完整的源码里还要有下行通道。比如通过网页远程控制 OLED 屏幕开关、调整采样间隔或者触发设备进入低功耗模式。这套机制本质上是通过订阅一个“控制主题”实现的主控订阅airbox/device_01/command后端或前端向这个主题发布指令主控收到后执行对应动作。很多人在写 MQTT 连接代码时只在setup()里连一次WiFi 一断就再也回不来。正确的做法是把重连逻辑放进loop()里并且做退避重连if (!mqtt.connected()) { // 指数退避连续失败时逐渐拉长重连间隔避免打爆路由器 int delayMs reconnectAttempt 5 ? 2000 : 10000; reconnectAttempt; mqtt.connect(deviceId, user, pwd); if (mqtt.connected()) reconnectAttempt 0; } delay(delayMs);另一个常见问题是设备标识冲突。多个设备如果都用同一个 clientId 连接 Broker后连的会把先连的踢下线。源码里建议用deviceId作为 MQTT clientId比如airbox_01同时放到 Topic 路径里这样每个设备的数据天然隔离前端也能按设备维度筛选。3.4 展示端怎么把数据变成曲线后端收到 MQTT 消息后的处理流程是校验 JSON 格式、解析字段、写入数据库、通过 WebSocket 推送最新一条给前端。数据库表结构不需要太复杂一张表就够字段类型说明idint主键自增device_idvarchar设备标识tsdatetime采集时间pm25floatPM2.5 浓度pm10floatPM10 浓度temp / humidityfloat温湿度tvoc / eco2floatVOC 相关指标这里有个经验写入和推送不要做成同步的。如果每收到一条消息就同步写数据库再推前端数据库一旦慢一点MQTT 消息就会积压。更稳的做法是后端先收消息入内存队列异步线程负责写库另一条通道负责推 WebSocket。毕设规模不需要上消息队列这种重组件一个Queue就能解决。前端展示我用的是 ECharts实时曲线用setOption增量更新series.data保留最近 200 个点超出就 shift 掉。历史查询则通过后端暴露一个 HTTP 接口按时间段查数据库返回给前端重新绘图。这样既满足实时性也满足“回看过去一天数据”的需求。4. 拿到源码后照着做的完整跑通流程4.1 硬件接线与烧录前的自检如果你是第一次拿到这套源码别急着打开 IDE 烧代码。先花十分钟把硬件检查一遍能省掉后面大量排查时间。接线方面PMS5003 需要 5V 供电它的串口 TX 接 ESP32 的 RX2GPIO16RX 接 TX2GPIO17共地必须接好。SGP30 和 SHT30 都是 I2C 设备SDA 接 GPIO21SCL 接 GPIO22两个设备地址不一样可以挂同一条 I2C 总线上。DHT22 接任意 GPIO 即可。上电后先串口监视器看输出。如果 PMS5003 的数值一直是 0大概率不是代码问题而是传感器风扇没转——检查 5V 供电是否足够PMS5003 启动电流接近 100mA有些劣质 USB 线压降太大会导致它带不动。4.2 开发环境配置与依赖安装源码烧录用 Arduino IDE 或 PlatformIO 都行。Arduino IDE 需要先在“开发板管理器”里安装 esp32 板卡包然后在库管理里安装这几项PubSubClientMQTT 客户端Adafruit SGP30 SensorSGP30 驱动Adafruit SSD1306OLED 显示如果要本地显示DHT sensor libraryDHT22 驱动用 PlatformIO 的同学更省事platformio.ini里直接声明库依赖首次编译会自动拉取[env:esp32dev] platform espressif32 board esp32dev framework arduino lib_deps knolleary/PubSubClient^2.8 adafruit/Adafruit SGP30 Library^2.0.0编译时最容易遇到的问题是库版本冲突。比如 Adafruit 系列库对 Arduino 版本有要求ESP32 板卡包 v2.x 和 v3.x 的 API 也有差异。如果你用的是新版板卡包但源码是照着旧版写的可能报class EspClass has no member named getFreeHeap这类错误。遇到版本问题最快的解决办法是看源码 README 里标注的开发环境版本保持一致而不是贸然升级到最新版。4.3 配置联网参数、启动后端服务和打开前端源码里的联网配置集中在config.h文件。需要改四个地方WiFi 名称、WiFi 密码、MQTT Broker 地址、设备 ID。前两个不用多说Broker 地址填服务器的 IP 或域名如果本机运行后端就填局域网 IP。后端如果是 Python 写的就装依赖然后启动pip install -r requirements.txt python server.py如果是 Node.js 版本就是npm install npm start。启动后先看控制台有没有打印Subscribed to airbox/device_01/data这样的日志说明后端已经成功订阅了 MQTT 主题。前端如果是静态页面直接用浏览器打开web/index.html即可如果是 Vue/React 工程需要npm install npm run dev启动开发服务器。打开页面后如果看不到曲线优先按 F12 看 WebSocket 连接是否正常这是最常见的断点。4.4 全链路验收清单我建议按下面这个顺序逐项验收不要一上来就盯着曲线图看串口监视器能看到 PMS5003 的原始读数而且数值会随环境变化对着传感器吹一口气PM2.5 应该明显上升。用 MQTTX 或 Mosquitto 客户端订阅airbox/device_01/data能看到 JSON 消息按预期频率推送。后端日志显示收到消息并成功写入数据库。前端页面实时曲线跟着数据跳动时间戳和当前时间一致。拔掉 ESP32 电源再重新上电一分钟后数据恢复上报前端曲线自动续上不需要重启任何服务。如果第五步通过了说明这套系统已经具备基本的稳定性可以拿去做实际部署了。5. 实测中踩过的坑与排查思路5.1 读数跳变问题从电源到采样策略的排查链路我拿到源码第一次实测时PM2.5 数值在 5 到 80 之间疯狂跳动完全没法看。排查过程是这样的先看串口原始数据输出帧本身是正常的帧头、长度、校验都对说明传感器通信没问题。然后怀疑是采样间隔太短PMS5003 的读数本身就存在逐秒波动于是把采样间隔从 1 秒调到 2 秒、滤波窗口从 3 个点加到 10 个点波形平滑了一些但偶尔还是跳。最后发现问题出在电源上。ESP32 板载的 3.3V 稳压器是从 USB 5V 取的电而 PMS5003 的 5V 如果跟 ESP32 共用一根劣质 USB 线风扇启动瞬间的电流冲击会把 3.3V 拉低导致 ADC 参考电压抖动传感器读数跟着飘。换成带单独供电的 USB 口或者用 5V 2A 适配器供电后问题彻底消失。这个坑的经验是空气质量监测系统的稳定性一半取决于传感器本身一半取决于电源质量。如果要用锂电池供电建议 PM2.5 传感器和主控分开供电至少加一个大电容缓冲。5.2 设备掉线和数据中断另一个高频问题是设备跑几个小时后就不上报了必须断电重启才能恢复。排查方向其实很明确先看是 WiFi 断了还是 MQTT 连接断了。在源码里加日志分别打印 WiFi 状态和 MQTT 状态实测发现 WiFi 一直是连接的丢的是 MQTT 连接。原因是 Broker 默认有一个keepalive超时机制ESP32 如果长时间没有发送心跳包Broker 会主动断开连接。PubSubClient 的loop()函数必须高频调用才能维持心跳如果主循环里跑了大延时比如delay(5000)就会漏掉心跳。解决方案有两个一是把上报逻辑改成非阻塞的millis()定时器保证loop()空转频率足够高二是给mqtt.connect()设置合理的 keepalive 参数比如 30 秒然后loop()每 50ms 调用一次。另外重连成功后要重新订阅主题这个很多人会漏掉导致数据看着断了但设备其实在线。5.3 展示端数据对不上时区与存储精度前端曲线的时间轴比实际时间快了 8 个小时这是时区问题。ESP32 上报时间戳用的是 Unix 时间戳本身没有时区概念但后端 Python 如果用datetime.now()直接存存的是服务器本地时间前端 ECharts 默认按浏览器本地时区解析如果服务器是 UTC 就会差 8 小时。统一的做法是后端收到消息后统一用datetime.utcnow()或带时区信息的 ISO 字符串存储前端按day.js或date-fns做时区转换避免“看起来对不上”的误解。还有一个隐蔽问题MySQL 的float字段存入 26.5查出来可能是 26.499999。这是因为 float 是单精度浮点用 double 或 decimal 字段存储温湿度这类需要展示的小数更合适。别看就这一个小细节答辩演示时被老师截图放大看数据会很尴尬。5.4 部署位置对测量结果的影响最后聊一个传感器之外的问题部署位置。很多同学把 AirBox 放在桌面上旁边就是加湿器或者对着空调出风口测出来的数据根本没参考价值。室内布点有几个原则高度在呼吸带附近大约 1.2 到 1.5 米避开空调、加湿器、空气净化器的出风口不要紧贴墙角四周至少留 20 厘米空间保证空气流通。PMS5003 这类带风扇的传感器对气流非常敏感贴着墙放会把灰尘浓度测高靠近净化器会测出超低值都不真实。如果需要长期部署建议定期清理 PMS5003 的风道和镜片。激光散射传感器的镜片一旦积灰读数会逐渐偏高而且很难在数据处理阶段纠正。我个人的习惯是一个月拆开用气吹吹一次顺手清理进风口滤网这样测量数据长期可信。这套源码跑通之后我建议你把注意力放在数据积累上观察一周的 PM2.5 和 TVOC 曲线你会发现很多有意思的规律做饭时段颗粒物浓度会冲高、开窗通风后 CO2 迅速下降、夜间卧室 CO2 浓度持续爬升。这些真实数据比任何一种功能演示都有说服力也是这套 AirBox 系统最有价值的部分。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →