💡 划重点
- 主控:ESP32,自带 Wi-Fi,可跑本地 Web Server。
- 感知:BME280 读取温度、湿度、气压,I2C 接 GPIO21/22。
- 执行:GPIO25 输出 PWM 驱动小风扇,温湿度超阈值自动调速。
- 提示:GPIO2 接 LED 心跳,方便判断程序是否卡死。
- 成本:物料控制在 70 元内,适合阳台、花园、小型温室。
- 结果:手机连同一 Wi-Fi,打开 ESP32 的 IP 即可看实时数据。
需求洞察
目标用户是阳台种植者、花园爱好者、小型温室玩家。他们通常种番茄、草莓、多肉、香草或兰花,面积不大,但环境变化很敏感。真实痛点有三个:第一,阳台和花园的温湿度与室内差异大,闷热高湿容易导致霉病、烂根,但人不可能一直守着。第二,很多成品气象站只能看数据,不能联动通风,发现时已经晚了。第三,商用物联网设备依赖云平台,断网就失效,还存在隐私和长期费用问题。
为什么值得做?ESP32 加 BME280 的方案成本低、资料多、可本地运行。网页直接由 ESP32 提供,不依赖外部服务器。风扇按阈值自动启停,既能降温除湿,又能减少人工干预。对新手来说,这是一个完整的“感知—控制—执行—上报”闭环项目,学到 I2C、PWM、Web Server 和 Wi-Fi 联网,后续还能加土壤湿度、光照、水泵。
方案设计
感知层:BME280 通过 I2C 与 ESP32 通信,周期读取温度、湿度、气压。气压可辅助判断天气趋势,例如短时下降可能预示降雨。
控制层:ESP32 做主循环,每 2 秒采样一次。设定两级阈值:温度大于 28℃或湿度大于 70% 时风扇低速;温度大于 30℃或湿度大于 75% 时风扇高速。阈值可写在代码顶部,方便调整。
执行层:GPIO25 输出 PWM,经过小功率 MOSFET 驱动 5V 风扇。LED 接 GPIO2,每秒翻转一次作为心跳。风扇启停时网页同步显示状态。
日志/联网层:ESP32 连接家庭 Wi-Fi,启动本地 Web Server。根路径返回自动刷新的 HTML 页面,/data 返回 JSON。最近 60 条采样可保存在内存环形缓冲区,网页显示趋势简表。串口同时输出日志,便于调试。
物料清单
| 物料 | 数量 | 用途 | 估算单价 |
|---|---|---|---|
| ESP32 开发板 | 1 | 主控与 Wi-Fi | 25 元 |
| BME280 模块 | 1 | 温湿度气压 | 15 元 |
| 5V 小风扇 | 1 | 通风降温 | 10 元 |
| MOSFET 模块 | 1 | 风扇 PWM 驱动 | 3 元 |
| LED 与电阻 | 1 套 | 心跳指示 | 1 元 |
| 杜邦线、面包板 | 1 套 | 接线 | 6 元 |
| USB 电源与线 | 1 套 | 供电 | 10 元 |
| 合计 | 70 元 |
接线与结构
- BME280:VCC 接 3.3V,GND 接 GND,SDA 接 GPIO21,SCL 接 GPIO22。
- 风扇 PWM:GPIO25 接 MOSFET 信号脚,风扇接 5V 与 MOSFET 输出,共地。
- LED 心跳:GPIO2 串 220Ω 电阻接 LED 正极,负极接 GND。
- 微调说明:保留给定 GPIO 不变。若风扇电流大于 500mA,建议独立 5V 供电并共地;GPIO25 只负责 PWM 信号,不直接带负载。BME280 使用 3.3V,避免电平不匹配。
结构上,ESP32 和模块放入小防水盒,BME280 开百叶通风口,避免阳光直射和雨水。风扇对准盒内或植物区。电源线从盒底进出,留滴水弯。
核心代码
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#define SDA_PIN 21
#define SCL_PIN 22
#define FAN_PWM 25
#define LED_PIN 2
const char* ssid = "你的WiFi";
const char* password = "你的密码";
Adafruit_BME280 bme;
WebServer server(80);
float temp = 0, hum = 0, pres = 0;
int fanDuty = 0;
unsigned long lastSample = 0;
void handleRoot() {
String html = "<meta http-equiv='refresh' content='3'>";
html += "<h2>小型气象站</h2>";
html += "<p>温度: " + String(temp) + " C</p>";
html += "<p>湿度: " + String(hum) + " %</p>";
html += "<p>气压: " + String(pres) + " hPa</p>";
html += "<p>风扇 PWM: " + String(fanDuty) + "</p>";
server.send(200, "text/html; charset=utf-8", html);
}
void handleData() {
String json = "{\"t\":" + String(temp) + ",\"h\":" + String(hum) +
",\"p\":" + String(pres) + ",\"fan\":" + String(fanDuty) + "}";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
ledcSetup(0, 25000, 8);
ledcAttachPin(FAN_PWM, 0);
Wire.begin(SDA_PIN, SCL_PIN);
if (!bme.begin(0x76)) {
while (1) { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); delay(200); }
}
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
server.on("/", handleRoot);
server.on("/data", handleData);
server.begin();
}
void loop() {
server.handleClient();
if (millis() - lastSample > 2000) {
lastSample = millis();
temp = bme.readTemperature();
hum = bme.readHumidity();
pres = bme.readPressure() / 100.0F;
if (temp > 30 || hum > 75) fanDuty = 180;
else if (temp > 28 || hum > 70) fanDuty = 120;
else fanDuty = 0;
ledcWrite(0, fanDuty);
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
Serial.printf("T=%.1f H=%.1f P=%.1f FAN=%d\n", temp, hum, pres, fanDuty);
}
}
调试步骤
- 先只接 BME280,烧录 I2C 扫描程序,确认地址为 0x76 或 0x77。
- 打开串口监视器,检查温度、湿度、气压是否合理,用手捂住传感器看湿度变化。
- 接入 LED,确认每秒闪烁,说明主循环未阻塞。
- 接入风扇和 MOSFET,先用固定 PWM 测试,再改回阈值控制。
- 连接 Wi-Fi,记录串口打印的 IP,手机浏览器访问根路径。
- 访问
/data检查 JSON 格式,便于后续接入其他页面。 - 长时间运行 2 小时,观察是否掉线、发热、风扇启停是否频繁。