1. 项目概述:为什么选择ESP32做“会思考”的空气监测器?
最近工作室的空气质量总让我觉得有点“不对劲”,不是那种明显的异味,而是长时间待着会有点闷,开窗通风又怕外面的PM2.5飘进来。市面上的空气检测仪要么功能单一,要么数据“傻傻的”,只会显示一个数字,告诉你“优”或“差”,至于为什么差、接下来会怎么变化、该开窗还是该开净化器,它一概不知。这让我萌生了自己动手做一个“聪明”一点的空气质量监测器的想法。我的核心需求是:它不仅要能测,还要能“想”,能根据多维度数据给出贴合我生活习惯的智能建议。
在硬件选型上,我几乎没怎么犹豫就锁定了FireBeetle ESP32。原因很直接:第一,它集成了Wi-Fi和蓝牙,天生就是为物联网而生的,数据上传到云端或者手机App查看是刚需;第二,ESP32的双核处理器和相对充裕的内存,让它有足够的“脑力”去运行一些轻量级的机器学习模型,实现本地化的智能推理,这正是“人工智能辅助”的关键;第三,围绕ESP32的生态太成熟了,无论是用Arduino框架快速原型开发,还是用ESP-IDF进行深度定制,都有海量的库和社区支持,各种传感器驱动一应俱全。至于人工智能部分,我打算从简单的开始,比如用线性回归或决策树算法,让设备学会根据温湿度、历史PM2.5数据来预测短期内的空气质量趋势,或者判断当前环境是否适合开窗。
这个项目适合所有对物联网、嵌入式开发和机器学习交叉领域感兴趣的爱好者。无论你是想学习如何将传感器数据接入网络,还是好奇如何在资源受限的单片机上跑AI模型,这个从数据采集到智能决策的完整流程,都会是一次非常棒的实战体验。接下来,我就把从硬件组装、数据采集、云端搭建到智能算法嵌入的整个过程,毫无保留地拆解给你看。
2. 硬件选型与核心电路设计解析
一个可靠的空气质量监测器,硬件是地基。我的设计思路是核心控制+传感器阵列+友好交互。下面这张表格是我最终确定的硬件清单和选型理由:
| 模块名称 | 具体型号/关键参数 | 选型理由与注意事项 |
|---|---|---|
| 主控板 | DFRobot FireBeetle ESP32 (主控核心) | 集成Wi-Fi/蓝牙,低功耗设计,GPIO丰富,兼容Arduino与IDF双开发环境。注意:需确认是ESP32-WROOM模组,确保无线性能。 |
| PM2.5/PM10传感器 | 攀藤PMS5003或PMS7003 | 激光散射原理,精度高,数据稳定,串口输出,有成熟Arduino库。注意:需单独5V供电,串口通信引脚勿接错。 |
| 温湿度传感器 | DHT22 (AM2302) | 性价比高,数字信号输出,精度足以满足环境监测需求(温度±0.5℃,湿度±2%)。注意:单总线协议,读取间隔建议大于2秒。 |
| 甲醛/TVOC传感器 | 炜盛科技ZE08-CH2O或SGP30 | ZE08为电化学原理,针对甲醛;SGP30为金属氧化物,测总VOC和等效CO2。本项目选用SGP30,因其I2C接口更省线,且能反映更广泛的有机污染物。 |
| 二氧化碳传感器 | MH-Z19B | 非分散红外原理,精度高,寿命长,串口输出。注意:需要预热3分钟左右读数才稳定。 |
| 交互与显示 | 0.96寸OLED (SSD1306, I2C) | 低功耗,无需背光,可实时显示关键数据。I2C接口仅需2根线。 |
| 供电与拓展 | 18650锂电池及充放电管理模块 | FireBeetle板载充放电管理,方便移动部署。确保电池容量在2000mAh以上以保证续航。 |
实操心得:传感器供电的坑:PM2.5传感器和CO2传感器通常需要5V供电,而ESP32的引脚和DHT22等是3.3V电平。千万不要直接用ESP32的5V输出(如果有的話)同时给多个传感器供电,瞬间电流可能导致板子重启。我的方案是使用一个小型DC-DC降压模块,直接从锂电池取电,稳定输出5V给这些“电老虎”传感器单独供电,与主控板的3.3V系统完全隔离,稳定性大增。
电路连接是整个项目的物理骨架,连接错误轻则数据不准,重则烧毁传感器。下面是我的接线图详解(以FireBeetle ESP32的引脚为例):
- 电源总线:将18650电池接入FireBeetle的电池接口。从FireBeetle的
VCC引脚(这是经过板载稳压后的3.3V)引出,给DHT22、OLED、SGP30供电。另外,用独立的5V降压模块给PMS5003和MH-Z19B供电。 - I2C总线:这是最简洁的总线。将OLED的SDA、SCL分别接ESP32的
D21(SDA)、D22(SCL)。SGP30同样接入这组I2C引脚。注意:I2C设备都需要上拉电阻,通常OLED和SGP30模块板载已集成,如果通信不稳定,可以在SDA和SCL上各加一个4.7kΩ电阻上拉到3.3V。 - 串口总线:ESP32有多个硬件串口。我定义:
- Serial2(
RX2: D16,TX2: D17) 用于连接PMS5003。PMS5003的TX接ESP32的D16(RX2),RX接D17(TX2)。 - Serial1(
RX1: D9,TX1: D10) 用于连接MH-Z19B。MH-Z19B的TX接ESP32的D9(RX1),RX接D10(TX1)。 - 注意:务必交叉连接,即传感器的TX接MCU的RX。同时,PMS5003的SET引脚可以悬空或接高电平(进入主动上传模式)。
- Serial2(
- 单总线:DHT22的数据线接
D4引脚,同时接一个4.7kΩ上拉电阻到3.3V。
完成连接后,建议先不要组装外壳,上电后用简单的测试程序分别读取每个传感器数据,确保每一个都工作正常后再进行整合。这是避免后期排查问题时“拆机”痛苦的关键一步。
3. 固件开发:数据采集、上传与本地显示
硬件准备就绪后,我们进入“赋予灵魂”的环节——编写运行在ESP32上的固件。我的代码结构分为几个核心模块:传感器驱动初始化、数据定时采集与滤波、本地OLED显示、以及通过Wi-Fi上传数据到云端。这里我选择Arduino IDE进行开发,因为它库管理方便,对新手友好。
3.1 开发环境搭建与库管理
首先,确保你的Arduino IDE已安装ESP32开发板支持。可以在“文件->首选项”的附加开发板管理器网址中添加:https://espressif.github.io/arduino-esp32/package_esp32_index.json。然后在开发板管理器中搜索安装“ESP32”。选择开发板为“DFRobot Firebeetle ESP32”。
接下来,通过库管理器(项目->加载库->管理库)安装以下必需的库:
Adafruit_SGP30:用于SGP30 TVOC/eCO2传感器。DHT sensor library:用于DHT22温湿度传感器。Adafruit_SSD1306和Adafruit_GFX:用于OLED显示。PMSLibrary:一个优秀的攀藤PM2.5传感器库。ArduinoJson:处理JSON数据格式,为上传云端做准备。WiFiManager:强烈推荐!它允许设备在启动时进入AP模式,让你用手机配置Wi-Fi的SSID和密码,无需将密码硬编码在代码里,大大提升了部署灵活性。
3.2 核心数据采集与滤波代码解析
数据采集的稳定性直接决定后续AI模型的质量。我的策略是定时采集+软件滤波。以下是一些关键代码片段和逻辑:
#include <PMS.h> #include <DHT.h> #include <Adafruit_SGP30.h> #include <MHZ19.h> #include <WiFiManager.h> // 定义引脚与对象 #define DHTPIN 4 #define DHTTYPE DHT22 PMS pms(Serial2); DHT dht(DHTPIN, DHTTYPE); Adafruit_SGP30 sgp; MHZ19 mhz19(&Serial1); // 数据结构体,用于存储滤波后的数据 struct SensorData { float temperature; float humidity; uint16_t pm25; uint16_t pm10; uint16_t tvoc; uint16_t eco2; uint16_t co2; } avgData; // 滑动平均滤波数组 const int FILTER_SIZE = 5; float tempBuffer[FILTER_SIZE]; int bufferIndex = 0; float slidingAverage(float newValue, float buffer[]) { buffer[bufferIndex] = newValue; bufferIndex = (bufferIndex + 1) % FILTER_SIZE; float sum = 0; for (int i = 0; i < FILTER_SIZE; i++) { sum += buffer[i]; } return sum / FILTER_SIZE; } void readSensors() { // 读取DHT22 float h = dht.readHumidity(); float t = dht.readTemperature(); if (!isnan(h) && !isnan(t)) { avgData.humidity = slidingAverage(h, humidityBuffer); avgData.temperature = slidingAverage(t, tempBuffer); } // 读取PMS5003 if (pms.read(data)) { avgData.pm25 = slidingAverage(data.PM_AE_UG_2_5, pm25Buffer); avgData.pm10 = slidingAverage(data.PM_AE_UG_10_0, pm10Buffer); } // 读取SGP30,注意它需要温湿度进行补偿 if (sgp.IAQmeasure()) { avgData.tvoc = sgp.TVOC; avgData.eco2 = sgp.eCO2; } // 读取MH-Z19B avgData.co2 = mhz19.getCO2(); }注意事项:传感器读数时机与补偿:SGP30传感器有一个关键特性,它的TVOC和eCO2读数算法需要当前的绝对湿度作为输入进行补偿,否则长期运行会有基线漂移。我们需要在代码中计算绝对湿度并设置给SGP30。公式和代码片段如下:
// 根据温湿度计算绝对湿度(克/立方米) float getAbsoluteHumidity(float temperature, float humidity) { // 计算饱和水汽压 float saturationVaporPressure = 6.1078 * pow(10, (7.5 * temperature) / (temperature + 237.3)); // 计算实际水汽压 float vaporPressure = humidity / 100.0 * saturationVaporPressure; // 计算绝对湿度 return (216.7 * vaporPressure) / (temperature + 273.15); } // 在读取SGP30前调用 sgp.setHumidity(getAbsoluteHumidity(avgData.temperature, avgData.humidity));
3.3 数据上传与云端交互设计
数据上传我选择了MQTT协议,因为它轻量、适合物联网设备,并且与主流的云平台(如阿里云、腾讯云、私有部署的EMQX等)无缝集成。我这里以连接一个公共的MQTT Broker(例如broker.emqx.io)为例进行说明。
#include <PubSubClient.h> #include <ArduinoJson.h> WiFiClient espClient; PubSubClient mqttClient(espClient); const char* mqttServer = "broker.emqx.io"; const int mqttPort = 1883; const char* topic = "firebeetle/air_quality"; void reconnectMQTT() { while (!mqttClient.connected()) { String clientId = "ESP32Client-" + String(random(0xffff), HEX); if (mqttClient.connect(clientId.c_str())) { Serial.println("MQTT connected!"); } else { delay(5000); } } } void publishSensorData() { StaticJsonDocument<256> doc; doc["temp"] = avgData.temperature; doc["humi"] = avgData.humidity; doc["pm25"] = avgData.pm25; doc["pm10"] = avgData.pm10; doc["tvoc"] = avgData.tvoc; doc["eco2"] = avgData.eco2; doc["co2"] = avgData.co2; char jsonBuffer[256]; serializeJson(doc, jsonBuffer); mqttClient.publish(topic, jsonBuffer); }在主循环loop()中,定期(例如每30秒)调用publishSensorData()即可。同时,OLED显示模块我会让它循环显示最重要的几项数据:PM2.5、CO2、温度和湿度,刷新频率可以快一些,比如每2秒切换一项。
4. “人工智能辅助”的核心:本地轻量级AI模型实现
这是本项目区别于普通监测器的精髓所在。所谓“人工智能辅助”,我将其定义为:设备能基于实时和历史数据,进行简单的本地推理,给出超出阈值判断的、更具洞察力的建议。考虑到ESP32的资源(双核240MHz,约520KB SRAM),复杂的深度学习模型不现实,但传统的机器学习算法如决策树、线性回归或简单的神经网络(如TinyML)是完全可行的。
我设计了两个智能功能:
- 空气质量趋势预测:基于过去一段时间(如1小时)的PM2.5、温湿度数据,预测未来15-30分钟的PM2.5变化趋势(上升、下降、平稳)。
- 智能通风建议:综合室内外(假设我们通过云端获取了室外数据)的PM2.5、CO2、温湿度,判断当前是否适合开窗通风。
4.1 模型训练与转换
我们首先在PC上完成模型的训练。以趋势预测为例,我们可以将其简化为一个三分类问题(趋势向上=2,平稳=1,向下=0)。使用Python的scikit-learn库可以快速训练一个决策树模型。
# 示例Python训练代码(在PC上运行) import pandas as pd from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split import joblib # 假设我们有包含历史传感器数据和对应趋势标签的数据集 # 特征:过去N个时间点的PM2.5, 温度, 湿度 # 标签:未来时刻的趋势 (0,1,2) data = pd.read_csv('sensor_history.csv') X = data[['pm25_1', 'temp_1', 'humi_1', 'pm25_2', 'temp_2', ...]] # 特征工程 y = data['trend_label'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) model = DecisionTreeClassifier(max_depth=5) # 控制树深度,防止过拟合和模型过大 model.fit(X_train, y_train) print("Accuracy:", model.score(X_test, y_test)) # 保存模型 joblib.dump(model, 'air_trend_model.pkl')训练好模型后,需要将其转换为能在ESP32上运行的格式。这里推荐使用TensorFlow Lite for Microcontrollers。我们可以先将scikit-learn的模型转换为TensorFlow格式(可能需要使用sklearn-onnx等工具先转ONNX,再转TensorFlow),或者更直接地,因为决策树本身逻辑不复杂,我们可以手动“翻译”它。
4.2 在ESP32上部署与运行推理
对于决策树,我们可以直接将其推理逻辑用C++的if-else语句实现。这虽然“笨”,但极其高效,且不依赖任何额外的库。
假设我们训练出的决策树规则如下(简化示例):
如果 (当前PM2.5 > 35) 且 (过去10分钟PM2.5变化率 > 0.1): 趋势 = 2 (上升) 否则如果 (当前CO2 > 1000) 或 (当前TVOC > 500): 趋势 = 1 (平稳但污染水平高) 否则: 趋势 = 0 (下降或良好)我们在ESP32固件中实现这个推理函数:
int predictAirTrend(float currentPM25, float pm25ChangeRate, float currentCO2, float currentTVOC) { // 规则1:PM2.5本身较高且仍在快速上升 if (currentPM25 > 35.0 && pm25ChangeRate > 0.1) { return 2; // 趋势向上,预警 } // 规则2:虽然PM2.5不高,但室内CO2或有机污染物浓度已超标 else if (currentCO2 > 1000.0 || currentTVOC > 500) { return 1; // 趋势平稳但需注意通风 } // 规则3:各项指标均良好 else { return 0; // 趋势向下或保持良好 } } // 在主循环中调用 int trend = predictAirTrend(avgData.pm25, calculatePM25ChangeRate(), avgData.co2, avgData.tvoc); // 根据trend值,在OLED上显示不同的图标或文字提示,如“↑风险”、“⚠注意”、“↓良好”对于更复杂的模型(如小型的神经网络),则需要集成TensorFlow Lite Micro库。这需要在Arduino IDE中安装相关的库,并将转换后的.tflite模型文件作为数组嵌入到代码中。这个过程稍复杂,但能实现更强大的识别能力,例如识别出“烹饪污染”、“灰尘污染”等模式。
实操心得:本地AI的边界:在ESP32上跑AI,一定要明确边界。它的算力主要用于“推理”,而非“训练”。模型必须足够小(通常小于100KB),输入特征要精心设计(降维、选择关键特征)。我的经验是,对于环境预测这类任务,基于规则的决策树或随机森林模型,经过精炼后,其C++实现版本往往比一个等效的微型神经网络更快、更省内存,且效果足够好。先尝试用规则实现,如果效果不理想再考虑TinyML。
5. 系统集成、功耗优化与外壳设计
当各个模块(传感器采集、数据显示、数据上传、本地AI)的代码都调试通过后,我们需要将它们整合到一个稳定、高效的主循环中,并考虑实际部署的功耗和外观问题。
5.1 固件主循环与状态机设计
一个鲁棒的主循环不能是简单的delay,那会阻塞程序。我采用非阻塞定时和简单状态机的设计,让各个任务有条不紊地执行。
unsigned long previousSensorMillis = 0; const long sensorInterval = 30000; // 传感器读取和上传间隔30秒 unsigned long previousDisplayMillis = 0; const long displayInterval = 2000; // 显示刷新间隔2秒 unsigned long previousAIMillis = 0; const long aiInterval = 60000; // AI推理间隔60秒 void loop() { unsigned long currentMillis = millis(); // 任务1:定时读取传感器并上传 if (currentMillis - previousSensorMillis >= sensorInterval) { previousSensorMillis = currentMillis; readSensors(); // 包含滤波 publishSensorData(); } // 任务2:定时刷新OLED显示 if (currentMillis - previousDisplayMillis >= displayInterval) { previousDisplayMillis = currentMillis; updateDisplay(); // 循环显示PM2.5, CO2, Temp, Humi, AI建议 } // 任务3:定时运行AI推理 if (currentMillis - previousAIMillis >= aiInterval) { previousAIMillis = currentMillis; int trend = predictAirTrend(...); String advice = generateVentilationAdvice(...); // 将建议存储到全局变量,供显示和上传使用 } // 任务4:保持MQTT连接 if (!mqttClient.connected()) { reconnectMQTT(); } mqttClient.loop(); // 维持MQTT心跳,处理传入消息 // 任务5:处理可能的串口数据(如MH-Z19B的自动校准指令) // ... }5.2 低功耗优化策略
如果你想用电池供电并长期部署,功耗是关键。FireBeetle ESP32本身支持深度睡眠,但我们的传感器大多不支持,且需要持续监测。因此,我的优化策略是“局部深度睡眠+外部分时供电”:
- ESP32本身:在
loop()的末尾,如果没有任务,可以调用delay(10)或使用esp_sleep_enable_timer_wakeup()进入轻度睡眠,但我们的任务周期最短2秒,轻度睡眠省电效果有限。更激进的做法是,如果数据上传间隔很长(比如5分钟一次),可以在完成一次“采集-上传-显示”循环后,让ESP32进入深度睡眠,用定时器唤醒。但这需要所有外设都能容忍断电重启,且Wi-Fi重连会耗电。 - 传感器分时供电:对于耗电大的传感器如PMS5003(约100mA)和MH-Z19B(约80mA),可以通过一个MOSFET开关电路,由ESP32的GPIO控制其电源通断。仅在需要读数前几百毫秒打开电源,读数后立即关闭。这能大幅降低平均电流。
- 降低工作频率:将ESP32的CPU频率从240MHz降到80MHz,对传感器读取和简单AI推理影响不大,但能显著降低功耗。
- 关闭不用的功能:如果不用蓝牙,在
setup()里用btStop()关闭蓝牙射频。
实测下来,在每30秒采集上传一次、OLED常亮的情况下,整个系统的平均工作电流约120mA。使用一节2000mAh的18650电池,可以连续工作约16小时。如果采用深度睡眠方案,将采集间隔延长到5分钟,理论续航可达数天甚至一周。
5.3 外壳设计与实际部署
一个好的外壳既能保护电路,也能提升产品感。我使用3D打印来制作外壳。设计要点:
- 风道设计:为PM2.5传感器设计独立的进风风道,避免电路板发热影响读数。进气口可以加一层简单的防尘海绵。
- 屏幕开孔:精确对应OLED屏幕,并考虑一定的漫射板或遮光设计,让显示更清晰。
- 传感器开孔:为DHT22、SGP30等需要接触空气的传感器开孔。
- 电源开关与充电口:预留位置,方便操作。
- 安装方式:设计壁挂孔或底座,可以放在桌面或挂在墙上。
部署时,选择一个能代表室内平均空气状况的位置,避免放在墙角、通风口、空调出风口或阳光直射处。离地面1-1.5米高度为宜。
6. 项目进阶:从监测到智能联动
当这个监测器稳定运行,积累了足够的数据后,它的价值可以进一步放大,实现真正的智能家居联动。
进阶方向一:云端数据可视化与历史分析我们可以将MQTT数据接入更强大的云端平台,如Home Assistant、Node-RED或阿里云物联网平台。在这些平台上,可以轻松地绘制出所有参数的历史曲线图,设置更复杂的自动化规则。例如,在Node-RED中,可以创建一个仪表盘,实时展示所有数据,并设置规则:“如果室内PM2.5连续5分钟高于室外且差值大于20,则自动打开空气净化器(通过智能插座)”。
进阶方向二:多设备组网与数据融合在客厅、卧室、书房各部署一个监测点,利用ESP32的Wi-Fi组成一个分布式监测网络。数据可以汇总到一个中心节点(比如用另一个ESP32做网关,或者直接上报到同一个云平台)。通过对比不同房间的数据,可以更精准地判断污染源位置和空气流动情况。
进阶方向三:模型持续优化与个性化将设备端采集的数据同步到云端后,我们可以在云端用更强大的算力(甚至你的个人电脑)定期重新训练AI模型。你可以根据自己家庭的实际情况,为“舒适”、“闷”、“需要通风”等主观感受打标签,将这些标签与当时的传感器数据结合,训练出更符合你个人体感的个性化预测模型。然后将训练好的新模型参数(比如更新后的决策树规则阈值)通过OTA(空中升级)的方式下发到ESP32设备中,完成模型的迭代更新。这才是“人工智能辅助”的完整闭环:感知-学习-优化-再感知。
这个项目从想法到实现,最深的体会是:物联网和AI的结合,并不是要做出多么高大上的东西,而是用技术去解决身边那些细小却真实存在的痛点。当你看到自己制作的设备不仅准确地显示出PM2.5的数值,还能在你准备做饭时提前提示“空气质量可能下降,建议开启油烟机”,那种成就感远超单纯购买一个成品。过程中遇到的每一个传感器通信问题、每一个数据跳变的排查、每一次AI模型效果的调优,都是实实在在的经验积累。希望我的这份详细拆解,能帮你少走些弯路,更快地做出属于你自己的、会“思考”的空气质量管家。