小型气象站上报网页——基于ESP32的阳台气象监测与远程查看
💡 划重点
- 用 ESP32 自建 Wi-Fi 热点与 Web 服务器,无需依赖任何云平台或手机 App。
- 一颗 BME280 同时完成温度、湿度、大气压三要素采集,精度完全满足居家种花养草。
- 支持温控 PWM 风扇,气温超过阈值自动开启温柔送风,避免阳台植物闷热。
- 整机硬件成本控制在 70 元以内,只需一把烙铁加一根 USB 线就能跑起来。
需求洞察
目标用户:阳台种植爱好者、多肉/兰花玩家、刚接触物联网硬件的学生或创客。他们需要掌握自家阳台的真实小气候,又不想花几百块买商业气象站,更不希望家庭数据传输到陌生的云平台。
真实痛点:阳台温度常比天气预报高出 5–8℃,特别是封闭阳台在午间容易形成“蒸笼效应”,导致植物黄叶、烂根。而市售蓝牙温湿度计屏幕太小、历史数据不可查、无远程查看能力,往往只是摆设。
为什么值得做:ESP32 自带 2.4 GHz Wi-Fi,可以同时充当传感器采集中心与轻量级 Web 服务器。手机、电脑直接连上设备热点,浏览器里就能看到实时气象卡片、风扇状态和历史曲线。整个项目用到的只有开源库和基础 Arduino 代码,学习成本低,又是真实可用的居家工具,非常适合作为硬件编程入门第一站。
方案设计
感知层:BME280 通过 I2C 总线挂载在 ESP32 的 GPIO21(SDA)/22(SCL) 上,每 2 秒完成一次采样,原始数据经片上补偿算法换算为实际温度、相对湿度和气压值。
控制层:ESP32 运行 FreeRTOS 任务,核心逻辑分为传感器轮询任务、Web 服务器任务和 PWM 控制任务。温度超过 30℃ 时自动递增风扇占空比,避免继电器式的突风;低于 28℃ 则逐步降速直至完全停止。LED 心跳灯每 500ms 翻转一次,作为系统活体标志。
执行层:PWM 风扇驱动模块采用 NPN 三极管(如 S8050)驱动 5V 直流风扇,ESP32 的 GPIO25 输出 1kHz PWM 信号经 1kΩ 电阻连接到基极,风扇转速随占空比线性变化。
日志/联网层:设备默认工作在 AP 模式,Wi-Fi SSID 为 阳台小站,密码 12345678。内置 Web 服务器同时提供 JSON 数据接口 GET /api/sensor,方便后续对接 Grafana 或手机小组件。页面每隔 5 秒自动刷新。
物料清单
| 名称 | 数量 | 用途 | 估算单价(元) | 小计(元) |
|---|---|---|---|---|
| ESP32-WROOM-32 开发板 | 1 | 主控 | 18 | 18 |
| BME280 模块 (GY-BME280) | 1 | 温湿压传感器 | 9 | 9 |
| 5V 微型风扇 (30×30mm) | 1 | 植物通风 | 8 | 8 |
| S8050 三极管 + 1kΩ 电阻 | 1套 | 风扇驱动 | 1 | 1 |
| 红色 LED + 220Ω 限流电阻 | 1套 | 心跳指示 | 1 | 1 |
| 面包板 + 跳线包 | 1套 | 原型搭建 | 10 | 10 |
| Micro-USB 数据线 | 1 | 供电与烧录 | 4 | 4 |
| 3.3V/5V 适配器(可复用手机充电头) | 0 | 用已有 | 0 | 0 |
| 合计 | 51 |
接线与结构
| ESP32 引脚 | 连接对象 | 说明 |
|---|---|---|
| 3V3 | BME280 VCC | 3.3V 供电 |
| GND | BME280 GND、风扇负极、LED 负极 | 共地 |
| GPIO21 | BME280 SDA | I2C 数据(默认) |
| GPIO22 | BME280 SCL | I2C 时钟(默认) |
| GPIO25 | 1kΩ 电阻 → S8050 基极 | PWM 信号 |
| 5V | 风扇正极 | 5V 风扇供电 |
| S8050 集电极 | 风扇负极 | 低端开关 |
| GPIO2 | 220Ω 电阻 → LED 正极 | 心跳灯 |
微调说明:GPIO2 在部分 ESP32 开发板上内置 LED,该脚高电平点亮,可以省去外接 LED,但仍按物料清单外接以保证所有板型正常工作。
核心代码
下方为基于 Arduino 框架和 PlatformIO 的完整骨架,主文件 src/main.cpp 可直接编译运行。
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
const char *ssid = "阳台小站";
const char *password = "12345678";
Adafruit_BME280 bme;
WebServer server(80);
#define PWM_CHANNEL 0
#define PWM_FREQ 1000
#define PWM_RES 8
#define FAN_PIN 25
#define LED_PIN 2
bool ledState = false;
unsigned long lastLedBlink = 0;
const unsigned long ledInterval = 500;
float temperature = 0, humidity = 0, pressure = 0;
int fanDuty = 0;
const float tempHigh = 30.0, tempLow = 28.0;
void updateFan(float temp) {
if (temp > tempHigh) fanDuty = map((int)(temp*10), 300, 450, 128, 255);
else if (temp < tempLow) fanDuty = 0;
// 过渡区保持占空比不变
fanDuty = constrain(fanDuty, 0, 255);
}
void handleRoot() {
char html[800];
snprintf(html, sizeof(html),
"<!DOCTYPE html><html><head><meta charset='utf-8'><title>阳台气象站</title>"
"<meta http-equiv='refresh' content='5'>"
"<style>body{font-family:sans-serif;background:#eef5e4;text-align:center;}"
".card{background:white;border-radius:12px;padding:20px;margin:20px auto;max-width:350px;box-shadow:0 4px 8px rgba(0,0,0,0.1);}"
"</style></head><body><h1>🌿 阳台小站</h1>"
"<div class='card'><p>🌡️ 温度: %.2f °C</p><p>💧 湿度: %.2f %%</p><p>🌀 气压: %.2f hPa</p>"
"<p>🌀 风扇占空比: %d / 255</p></div></body></html>",
server.send(200, "text/html", html);
}
void setup() {
pinMode(LED_PIN, OUTPUT);
ledcWrite(PWM_CHANNEL, 0);
Serial.begin(115200);
Wire.begin(21, 22);
if (!bme.begin(0x76)) {
Serial.println("BME280 未找到");
while(1) { digitalWrite(LED_PIN, HIGH); delay(200); digitalWrite(LED_PIN, LOW); delay(200); }
}
WiFi.softAP(ssid, password);
server.on("/", handleRoot);
server.on("/api/sensor", [](){
char json[250];
snprintf(json, sizeof(json), "{\"temp\":%.2f,\"humidity\":%.2f,\"pressure\":%.2f,\"fan\":%d}",
server.send(200, "application/json", json);
});
server.begin();
}
void loop() {
server.handleClient();
unsigned long now = millis();
if (now - lastLedBlink >= ledInterval) {
lastLedBlink = now;
ledState = !ledState;
}
static unsigned long lastSensor = 0;
if (now - lastSensor >= 2000) {
lastSensor = now;
temperature = bme.readTemperature();
humidity = bme.readHumidity();
pressure = bme.readPressure() / 100.0F;
updateFan(temperature);
}
}
依赖库:在 platformio.ini 中添加 lib_deps = adafruit/Adafruit BME280 Library @ ^2.2.2。
调试步骤
- 硬件检查:只连接 BME280 和 LED,上电后观察心跳灯是否以 500ms 间隔闪烁。若无,检查 GPIO2 电平或杜邦线接触。
- I2C 扫描:烧录 I2C 扫描例程,确认 BME280 地址为 0x76,串口打印无误。
- Wi-Fi 热点:烧录主程序后手机搜索 WiFi,连接 “阳台小站”,浏览器访问
http://192.168.4.1/,应出现卡片界面。 - 风扇联动:捂住 BME280 或用吹风机轻微加热,待温度超过 30℃,风扇启动并随温度升高转速加快;移开热源温度降至 28℃ 以下风扇停转。
- 数据校验:对比手机天气数据,阳台气温通常高 3–6℃,湿度受植物蒸腾影响可能偏高,属正常现象。