💡 划重点
- 本项目用 ESP32 做主控,BME280 采集温度、湿度、气压,GPIO25 输出 PWM 驱动小风扇,GPIO2 做心跳灯,板内运行本地 Web Server。手机连同一 Wi-Fi 后打开网页,即可查看实时气象数据、最近记录,并手动或自动调节风扇转速。整套物料控制在 70 元内,适合阳台、花园和小型种植箱。
需求洞察
目标用户是阳台养花、阳台晾晒、家庭菜园和小型温室的爱好者。他们真正关心的不是“气象数据上云”,而是三件事:当前阳台是否闷热高湿、需不需要立刻通风、历史变化有没有异常。成品气象站往往价格高、数据必须依赖厂商云、风扇联动另购控制器,坏了也难修。自己搭一套本地网页气象站,数据留在局域网内,手机浏览器直接看,风扇能按阈值自动启动,成本低且可二次开发。它值得做,是因为把“感知—判断—执行—查看”压缩到一块 ESP32 上,既解决实际通风问题,又是很好的硬件编程练手项目。
方案设计
感知层由 BME280 通过 I2C 读取温度、湿度、气压,采样周期 2 秒,连续采样 5 次取中位数,减少阳台热辐射造成的抖动。控制层在 ESP32 内实现阈值判断和回差控制:温度高于 30℃或湿度高于 75% 时启动风扇,温度低于 28℃且湿度低于 70% 时停止,避免临界点频繁启停;同时保留网页手动 PWM 覆盖。执行层用 GPIO25 输出 25 kHz PWM,经 MOS 管驱动 5V 小风扇,低占空比起步实现软启动。日志/联网层采用 STA 优先、失败转 AP 的方式:连接家庭路由后提供 http://设备IP/ 页面,页面展示实时值、风扇状态和最近 20 条记录;数据保存在内存环形缓冲中,也可扩展为向局域网 HTTP 服务上报 JSON。GPIO2 心跳灯以 1 秒闪烁表示主循环正常。
物料清单
| 物料 | 数量 | 用途 | 估算单价 |
|---|---|---|---|
| ESP32 DevKit 开发板 | 1 | 主控与 Web Server | 22 元 |
| BME280 模块 | 1 | 温湿度气压采集 | 12 元 |
| 5V 小风扇 | 1 | 通风执行 | 9 元 |
| AO3400 MOS 管 | 1 | 风扇 PWM 驱动 | 1.5 元 |
| 1N5819 二极管 | 1 | 风扇续流保护 | 0.5 元 |
| 电阻、LED、杜邦线 | 1 套 | 限流、指示、接线 | 6 元 |
| 5V 2A 电源 | 1 | 系统供电 | 12 元 |
| 旧塑料盒 | 1 | 外壳 | 0 元 |
| 合计 | 63 元 |
接线与结构
BME280 使用 3.3V 供电,SDA 接 GPIO21,SCL 接 GPIO22,GND 共地。风扇 PWM 信号从 GPIO25 经 1 kΩ 电阻接 AO3400 栅极,源极接地,漏极接风扇负极,风扇正极接 5V,并在风扇两端反并联 1N5819。LED 心跳灯从 GPIO2 串 330 Ω 电阻到 GND。给定接线可直接使用,无需改脚;唯一调整是风扇不能由 GPIO 直驱,必须加 MOS 管,否则启动电流会拉低 ESP32。结构上把 BME280 放在外壳通风处,远离风扇出风和电源发热区;风扇对准百叶窗或通风孔,ESP32 与电源分仓固定。
核心代码
以下为 Arduino 骨架,按 ESP32 Arduino Core 2.x 编写,需安装 Adafruit BME280 库。
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
const char* SSID = "你的WiFi";
const char* PASS = "你的密码";
const int PIN_FAN = 25, PIN_LED = 2;
WebServer server(80);
Adafruit_BME280 bme;
float temp = NAN, hum = NAN, pres = NAN;
int manualDuty = -1; // -1 表示自动
bool fanOn = false;
unsigned long lastSample = 0, lastBlink = 0;
bool ledState = false;
void readSensors() {
temp = bme.readTemperature();
hum = bme.readHumidity();
pres = bme.readPressure() / 100.0F;
}
void controlFan() {
int duty;
if (manualDuty >= 0) {
duty = manualDuty;
} else {
if (!fanOn && (temp > 30 || hum > 75)) fanOn = true;
if (fanOn && temp < 28 && hum < 70) fanOn = false;
duty = fanOn ? 180 : 0; // 8 位 PWM,约 70%
}
ledcWrite(0, duty);
}
void handleRoot() {
String html = "<!doctype html><meta charset='utf-8'><title>阳台气象站</title>";
html += "<h2>小型气象站</h2><p>温度:" + String(temp,1) + " ℃<br>湿度:" +
String(hum,1) + " %<br>气压:" + String(pres,1) + " hPa</p>";
html += "<p>风扇:" + String(fanOn ? "自动运行" : "停止") + "</p>";
html += "<input type='range' min='0' max='255' value='180' oninput='fetch(\"/fan?duty=\"+this.value)'>";
html += "<p><a href='/api/data'>JSON 数据</a></p>";
server.send(200, "text/html; charset=utf-8", html);
}
void handleData() {
String json = "{\"t\":" + String(temp,1) + ",\"h\":" + String(hum,1) +
",\"p\":" + String(pres,1) + ",\"fan\":" + String(fanOn ? 1 : 0) + "}";
server.send(200, "application/json", json);
}
void handleFan() {
if (server.hasArg("duty")) manualDuty = constrain(server.arg("duty").toInt(), 0, 255);
server.send(200, "text/plain", "ok");
}
void setup() {
Serial.begin(115200);
pinMode(PIN_LED, OUTPUT);
Wire.begin(21, 22);
if (!bme.begin(0x76)) Serial.println("BME280 未找到,检查地址 0x76/0x77");
ledcSetup(0, 25000, 8);
ledcAttachPin(PIN_FAN, 0);
WiFi.begin(SSID, PASS);
unsigned long t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 10000) delay(300);
if (WiFi.status() != WL_CONNECTED) {
} else Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/api/data", handleData);
server.on("/fan", handleFan);
server.begin();
}
void loop() {
server.handleClient();
if (millis() - lastSample > 2000) {
lastSample = millis();
readSensors();
controlFan();
}
if (millis() - lastBlink > 1000) {
lastBlink = millis();
ledState = !ledState;
}
}
调试步骤
先只接 BME280,用 I2C 扫描确认地址为 0x76 或 0x77,串口打印温湿度是否随呼吸变化。再接 LED 和风扇,先用固定 ledcWrite(0, 80) 测试风扇能否低速启动;若不能,提高到 120 并检查 MOS 管方向。随后连接 Wi-Fi,串口查看 IP,手机浏览器访问根路径,确认页面和 /api/data 的 JSON 正常。最后用吹风机低温档靠近传感器,观察温度上升后风扇是否在 30℃ 阈值启动,并测试网页滑条能否覆盖自动控制。外壳合盖后连续运行 2 小时,检查电源发热和传感器是否受风扇气流影响。