1. 项目概述
去年在给朋友改造智能家居系统时,我发现市面上大多数环境监测设备要么功能单一,要么价格昂贵。于是萌生了自己动手开发一套多功能环境监测系统的想法。这个基于ESP32-S3的环境监测系统,不仅能够实时采集温湿度、烟雾浓度和光照强度,还能通过Web界面进行远程监控和控制,整套硬件成本不到100元。
这个项目最吸引人的地方在于它完美结合了硬件采集和Web交互。ESP32-S3作为主控,通过各类传感器采集环境数据,然后构建本地Web服务器,用户在任何联网设备上都能查看实时数据并控制系统。相比商业产品,这个DIY方案不仅成本低,而且完全开源可定制。
2. 硬件设计与选型
2.1 核心硬件选型解析
选择ESP32-S3开发板作为主控主要基于以下几点考虑:
- 双核240MHz处理器性能足够运行Web服务器
- 内置WiFi模块省去了额外通信模块
- 丰富的外设接口(ADC、PWM、I2C等)
- 相比ESP8266有更多的GPIO和内存资源
传感器选型方面,我经过多次测试确定了以下组合:
- DHT11温湿度传感器:虽然精度不如DHT22,但成本更低且完全满足室内监测需求
- MQ-2烟雾传感器:对液化气、丙烷、烟雾等有良好灵敏度
- BH1750光照传感器:数字输出,精度高且无需额外校准
- WS2812 RGB LED:单总线控制,可实现丰富的光效反馈
2.2 硬件连接细节
实际接线时有几个关键点需要注意:
- DHT11的数据线需要接4.7K上拉电阻,否则读取可能失败
- MQ-2传感器需要预热3-5分钟才能稳定工作
- WS2812 LED的供电要充足,最好单独5V供电
- I2C设备的SCL/SDA线要正确对应,BH1750对线序敏感
具体引脚定义如下表:
| 模块 | ESP32-S3引脚 | 备注 |
|---|---|---|
| DHT11 | GPIO13 | 需4.7K上拉 |
| MQ-2 | GPIO7 | 模拟输入 |
| 蜂鸣器 | GPIO20 | PWM控制 |
| BH1750 SCL | GPIO39 | I2C时钟 |
| BH1750 SDA | GPIO40 | I2C数据 |
| WS2812 | GPIO48 | 数据线 |
3. 软件架构设计
3.1 系统框架解析
整个系统采用分层设计,各模块职责明确:
- 硬件驱动层:负责传感器数据采集和外围设备控制
- 数据处理层:对原始数据进行滤波和处理
- 业务逻辑层:实现报警规则和联动控制
- 网络通信层:提供Web服务和实时数据传输
- 用户界面层:呈现数据可视化界面
这种架构的最大优势是模块间耦合度低,比如要更换温湿度传感器,只需修改硬件驱动层代码,其他层几乎不用改动。
3.2 关键数据结构
系统使用两个核心结构体来管理数据:
cpp复制struct SensorData {
float temperature; // 温度(°C)
float humidity; // 湿度(%)
int mq2Value; // 烟感ADC值
float lightValue; // 光照(lux)
unsigned long updateTime; // 时间戳
};
struct AlarmConfig {
int mq2Threshold = 800; // 烟雾报警阈值
bool mq2AutoAlarm = true; // 自动报警开关
bool mq2AlarmState; // 当前报警状态
int lightLowThreshold = 100; // 光照低阈值
int lightHighThreshold = 500; // 光照高阈值
bool lightAutoMode = true; // 自动联动开关
};
这种设计将实时数据与配置参数分离,便于管理和持久化。在实际使用中,我发现将默认值直接定义在结构体中,可以避免初始化遗漏的问题。
4. 核心功能实现
4.1 传感器数据采集
数据采集采用1Hz的固定频率,通过millis()实现非阻塞定时:
cpp复制void updateSensorData() {
static unsigned long lastUpdate = 0;
if(millis() - lastUpdate < 1000) return;
// 读取DHT11
float t = dht.readTemperature();
float h = dht.readHumidity();
currentData.temperature = isnan(t) ? 0 : t;
currentData.humidity = isnan(h) ? 0 : h;
// 读取MQ-2
currentData.mq2Value = analogRead(MQ2_PIN);
// 读取BH1750
float l = lightMeter.readLightLevel();
currentData.lightValue = isnan(l) ? 0 : l;
currentData.updateTime = millis();
lastUpdate = millis();
}
这里有几个实用技巧:
- 使用isnan()检查传感器异常返回值
- 异常时返回0而不是上次值,避免错误累积
- 记录最后更新时间戳便于调试
4.2 智能联动控制
烟雾报警逻辑实现了分级响应:
cpp复制void handleMQ2Alarm() {
bool shouldAlarm = alarmConfig.mq2AutoAlarm &&
(currentData.mq2Value > alarmConfig.mq2Threshold);
alarmConfig.mq2AlarmState = shouldAlarm;
if(shouldAlarm) {
// 间歇报警:响500ms,停500ms
bool alarmOn = (millis() % 1000) < 500;
ledcWrite(BUZZER_CHANNEL, alarmOn ? 200 : 0);
} else {
ledcWrite(BUZZER_CHANNEL, 0); // 关闭蜂鸣器
}
}
光照联动则采用平滑过渡算法,避免灯光频繁切换:
cpp复制void handleLightLED() {
if(!alarmConfig.lightAutoMode) {
strip.setPixelColor(0, 0);
strip.show();
return;
}
uint32_t color;
if(currentData.lightValue < alarmConfig.lightLowThreshold) {
color = strip.Color(0, 0, 128); // 蓝色
} else if(currentData.lightValue > alarmConfig.lightHighThreshold) {
color = strip.Color(128, 128, 0); // 黄色
} else {
color = strip.Color(0, 128, 0); // 绿色
}
strip.setPixelColor(0, color);
strip.show();
}
5. Web服务与通信
5.1 Web服务器搭建
使用ESPAsyncWebServer库创建高性能Web服务:
cpp复制AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
void setupWebServer() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", htmlPage);
});
ws.onEvent(onWsEvent);
server.addHandler(&ws);
server.begin();
}
这里将HTML页面直接内嵌在代码中,省去了文件系统操作。实际开发时,我建议先使用SPIFFS存储页面文件,开发完成后再转换为内嵌形式。
5.2 WebSocket实时通信
双向通信协议设计如下:
- 设备→前端:每秒发送一次JSON格式的传感器数据
json复制{
"temp": 25.3,
"humi": 45.2,
"mq2": 623,
"light": 320,
"mq2Alarm": false,
"config": {
"mq2Threshold": 800,
"lightLow": 100,
"lightHigh": 500
}
}
- 前端→设备:当用户修改配置时发送更新
json复制{
"cmd": "updateConfig",
"mq2Threshold": 850,
"lightLow": 150
}
WebSocket事件处理函数需要处理连接、断开、消息等各种事件:
cpp复制void onWsEvent(AsyncWebSocket *server,
AsyncWebSocketClient *client,
AwsEventType type,
void *arg, uint8_t *data, size_t len) {
if(type == WS_EVT_DATA) {
DynamicJsonDocument doc(256);
deserializeJson(doc, data, len);
if(doc["cmd"] == "updateConfig") {
// 更新配置参数
alarmConfig.mq2Threshold = doc["mq2Threshold"] | alarmConfig.mq2Threshold;
// 触发立即响应
handleMQ2Alarm();
handleLightLED();
}
}
}
6. 前端界面设计
6.1 响应式布局方案
使用CSS Grid结合媒体查询实现多端适配:
css复制.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
@media (max-width: 480px) {
.dashboard {
grid-template-columns: 1fr;
}
.card {
padding: 10px;
font-size: 14px;
}
}
这种设计在PC端显示为多列,在手机上自动变为单列,确保所有信息一屏可见。
6.2 实时数据可视化
通过WebSocket实现无刷新数据更新:
javascript复制let ws = new WebSocket(`ws://${location.host}/ws`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 更新温度显示
document.getElementById('temp-value').textContent = data.temp.toFixed(1);
updateGauge('temp-gauge', data.temp, 0, 50);
// 更新烟雾状态
const mq2Status = document.getElementById('mq2-status');
mq2Status.textContent = data.mq2Alarm ? "报警中" : "正常";
mq2Status.className = data.mq2Alarm ? "badge danger" : "badge success";
};
仪表盘使用纯CSS实现,避免引入额外库:
css复制.gauge {
height: 10px;
background: #eee;
border-radius: 5px;
overflow: hidden;
}
.gauge-fill {
height: 100%;
background: linear-gradient(to right, #4CAF50, #FFC107, #F44336);
transition: width 0.5s ease;
}
7. 系统优化与调试
7.1 性能优化技巧
在开发过程中,我发现并解决了几个性能瓶颈:
- JSON序列化优化:使用StaticJsonDocument替代DynamicJsonDocument,内存占用从2KB降到256字节
- WebSocket广播优化:只向活跃客户端发送数据,避免资源浪费
- 主循环优化:将1秒定时器改为事件驱动,减少不必要的处理
7.2 常见问题排查
-
DHT11读取失败:
- 检查接线是否正确,特别是上拉电阻
- 增加读取间隔,DHT11两次读取至少间隔1秒
-
WebSocket连接不稳定:
- 确保WiFi信号强度良好
- 适当增加WebSocket超时时间
- 定期调用cleanupClients()释放资源
-
WS2812显示异常:
- 检查供电是否充足,最好单独5V供电
- 数据线串联100Ω电阻减少干扰
- 确保代码中正确设置了LED数量
8. 项目扩展方向
这个基础框架可以进一步扩展:
- 增加更多传感器:如CO2、PM2.5等空气质量监测
- 添加历史数据存储:利用ESP32的RTC内存或外接SD卡
- 接入云平台:通过MQTT协议将数据同步到云端
- 低功耗优化:使用深度睡眠模式实现电池供电
我在实际项目中尝试了接入Home Assistant,通过MQTT协议将数据桥接到智能家居系统,效果非常好。ESP32的丰富功能为系统扩展提供了无限可能。