ESP32+Web实现智能气象站
项目仓库源码:
https://gitee.com/vopo123/esp32-dev-kit/tree/master/ESP32S3-Weather-Station
基于 ESP32-S3 开发的智能气象站系统,核心功能是:通过多种传感器采集室内环境数据(温湿度、烟雾浓度、光照强度),结合高德天气 API 获取室外实时 / 预报天气数据,通过 Web 界面可视化展示所有数据,并支持前端实时配置报警阈值、联动规则,同时实现烟雾超标蜂鸣器报警、光照联动 WS2812 LED 灯变色的硬件交互。


一、项目概述
1、项目说明:
核心功能
- 实时天气:基于高德API获取当前天气状况,包含温度、湿度、风向、风力等信息
- 室内温湿度:通过DHT11传感器采集室内温度和湿度
- 室内环境:通过MQ2传感器监测烟雾浓度,BH1750传感器监测光照强度
- 天气预报:获取4天天气预报,包含白天和夜间天气信息
- 智能联动:烟雾浓度超标时自动触发蜂鸣器报警,光照强度不同时LED显示不同颜色
技术亮点
- 现代化Web界面:使用Tailwind CSS和Alpine.js实现响应式设计,优先适配手机屏幕
- 高性能服务器:使用ESPAsyncWebServer实现异步非阻塞Web服务器
- 实时数据更新:使用WebSocket实现传感器数据实时更新
- 智能报警系统:PWM控制蜂鸣器实现间歇报警,避免噪音污染
- 光照联动:根据光照强度自动调整WS2812 LED颜色
2、硬件需求
组件 | 型号 | 数量 | 用途 |
开发板 | ESP32-S3 | 1 | 主控板 |
温湿度传感器 | DHT11 | 1 | 采集室内温湿度 |
烟雾传感器 | MQ2 | 1 | 监测烟雾浓度 |
光照传感器 | BH1750 | 1 | 监测光照强度 |
蜂鸣器 | 无源蜂鸣器 | 1 | 烟雾报警 |
LED | WS2812 | 1 | 光照联动指示 |
面包板 | 通用 | 1 | 电路连接 |
杜邦线 | 公对公、公对母 | 若干 | 电路连接 |
3、软件需求
- 开发环境:Arduino IDE 1.8.10+ 或 PlatformIO
- 核心库:
- ESP32 Arduino Core
- ESPAsyncWebServer
- AsyncTCP
- ArduinoJson
- DHT sensor library
- Adafruit NeoPixel
- BH1750
4、硬件连接
引脚 | 功能 | 连接对象 |
13 | DHT11数据 | DHT11数据引脚 |
7 | MQ2模拟输入 | MQ2信号引脚 |
20 | 蜂鸣器PWM | 无源蜂鸣器正极 |
39 | BH1750 SCL | BH1750 SCL引脚 |
40 | BH1750 SDA | BH1750 SDA引脚 |
48 | WS2812数据 | WS2812数据引脚 |
GND | 接地 | 所有组件接地 |
3.3V/5V | 电源 | 所有组件电源 |
5、技术架构
前端
- Tailwind CSS:用于快速构建现代化的响应式界面
- Alpine.js:用于轻量级的前端交互和数据绑定
- Font Awesome:提供丰富的图标
后端
- ESPAsyncWebServer:高性能异步Web服务器
- WebSocket:实现实时双向通信
- HTTPClient:调用高德天气API
- ArduinoJson:解析和处理JSON数据
硬件控制
- DHT11:通过单总线协议读取温湿度
- MQ2:通过模拟输入读取烟雾浓度
- BH1750:通过I2C协议读取光照强度
- PWM:控制蜂鸣器频率和占空比
- WS2812:控制LED颜色
二、天气API使用说明
免费天气 API 获取(高德开放平台)
注意:高德 API 有免费限流(每日 10 万次),个人使用完全足够,无需担心超量。
选择高德开放平台天气 API(免费、国内节点快、支持 54天预报 + 空气质量),步骤如下:
- 打开高德开放平台,注册并登录账号
- 进入「控制台」→「应用管理」→「创建新应用」,添加「Web 服务 API」类型的 Key
- 记录你的Key和目标城市的城市 ADCODE(如北京 110000、上海 310000,可在高德 ADCODE 查询获取)
- API 接口说明(后续代码直接调用):
- 实时天气 + 5 天预报:
https://restapi.amap.com/v3/weather/weatherInfo?key=你的Key&city=城市ADCODE&extensions=all&output=json - 空气质量:接口已包含在上述请求中,无需单独调用

1、PC端测试源码
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>高德天气API PC端测试工具</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif; } body { max-width: 1200px; margin: 20px auto; padding: 0 20px; background-color: #f5f7fa; } .header { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #e5e7eb; } .header h1 { font-size: 24px; color: #165DFF; margin-bottom: 10px; } .config-panel { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 20px; } .config-item { margin-bottom: 15px; display: flex; align-items: center; gap: 10px; } .config-item label { width: 120px; font-weight: 500; color: #333; } .config-item input, .config-item select { flex: 1; padding: 8px 12px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; outline: none; } .config-item input:focus, .config-item select:focus { border-color: #165DFF; box-shadow: 0 0 0 2px rgba(22,93,255,0.1); } .btn-group { display: flex; gap: 10px; margin-top: 10px; } .btn { padding: 10px 20px; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; } .btn-primary { background-color: #165DFF; color: #fff; } .btn-primary:hover { background-color: #0d4ed9; } .btn-reset { background-color: #fff; color: #666; border: 1px solid #dcdfe6; } .btn-reset:hover { background-color: #f5f7fa; } .result-panel { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); } .result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #e5e7eb; } .result-status { font-size: 14px; } .status-success { color: #52c41a; } .status-error { color: #ff4d4f; } .result-content { width: 100%; min-height: 300px; padding: 15px; background-color: #f9fafc; border: 1px solid #e5e7eb; border-radius: 4px; font-family: "Consolas", "Monaco", monospace; font-size: 14px; line-height: 1.6; color: #333; white-space: pre-wrap; word-wrap: break-word; } .tip { margin-top: 10px; font-size: 12px; color: #909399; line-height: 1.5; } .tip a { color: #165DFF; text-decoration: none; } .tip a:hover { text-decoration: underline; } </style> </head> <body> <div> <h1>高德开放平台天气API 测试工具(PC端)</h1> <p>用于调试extensions=base(实时)/all(实时+预报)模式,展示完整原始JSON数据 | 数据来源:<a href="https://lbs.amap.com/api/webservice/guide/api/weatherinfo" target="_blank">高德天气API官方文档</a></p> </div> <div> <div> <label>高德API Key:</label> <input type="text" placeholder="请输入你的高德Web服务API Key"> </div> <div> <label>城市ADCODE:</label> <input type="text" placeholder="6位数字,如北京110000、上海310000" value="310000"> </div> <div> <label>请求模式:</label> <select> <option value="base">base - 仅实时天气</option> <option value="all" selected>all - 实时+未来预报</option> </select> </div> <div> <button>发送请求</button> <button>重置</button> </div> </div> <div> <div> <h3>API返回原始数据(格式化)</h3> <div>未发起请求</div> </div> <div>请点击「发送请求」获取高德天气API数据...</div> </div> <script> // 获取页面元素 const amapKey = document.getElementById('amapKey'); const cityAdcode = document.getElementById('cityAdcode'); const extensionsMode = document.getElementById('extensionsMode'); const sendRequest = document.getElementById('sendRequest'); const resetForm = document.getElementById('resetForm'); const requestStatus = document.getElementById('requestStatus'); const resultContent = document.getElementById('resultContent'); // 重置表单 resetForm.addEventListener('click', () => { amapKey.value = ''; cityAdcode.value = '310000'; extensionsMode.value = 'all'; requestStatus.innerText = '未发起请求'; requestStatus.className = 'result-status'; resultContent.innerText = '请点击「发送请求」获取高德天气API数据...'; }); // 发送API请求核心逻辑 sendRequest.addEventListener('click', async () => { // 表单验证 const key = amapKey.value.trim(); const adcode = cityAdcode.value.trim(); const mode = extensionsMode.value; if (!key) { alert('请输入你的高德Web服务API Key!'); amapKey.focus(); return; } if (!/^\d{6}$/.test(adcode)) { alert('城市ADCODE必须是6位数字!如北京110000、上海310000'); cityAdcode.focus(); return; } // 初始化请求状态 sendRequest.disabled = true; sendRequest.innerText = '请求中...'; requestStatus.innerText = '请求中,请稍候...'; requestStatus.className = 'result-status'; resultContent.innerText = '加载中...'; try { // 记录请求开始时间(计算耗时) const startTime = Date.now(); // 高德天气API请求地址(官方接口) const apiUrl = `https://restapi.amap.com/v3/weather/weatherInfo`; // 拼接请求参数 const params = new URLSearchParams({ key: key, city: adcode, extensions: mode, output: 'json', // 固定返回JSON格式 lang: 'zh-CN' // 中文返回 }); // 发送GET请求(跨域由高德API自动支持,无需额外处理) const response = await fetch(`${apiUrl}?${params.toString()}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, timeout: 10000 // 10秒超时 }); // 计算请求耗时 const costTime = (Date.now() - startTime) / 1000; // 获取响应数据 const result = await response.json(); // 格式化JSON(缩进2个空格,易读) const formatResult = JSON.stringify(result, null, 2); // 更新请求状态 requestStatus.innerText = `请求成功 | 响应码:${response.status} | 耗时:${costTime.toFixed(2)}s | 模式:${mode}`; requestStatus.className = 'result-status status-success'; resultContent.innerText = formatResult; // 控制台打印原始数据(方便二次调试) console.log(`高德天气API-${mode}模式返回数据:`, result); } catch (error) { // 捕获请求错误(网络错误、超时、解析错误等) requestStatus.innerText = `请求失败:${error.message}`; requestStatus.className = 'result-status status-error'; resultContent.innerText = `错误详情:\n${JSON.stringify(error, null, 2)}\n\n排查建议:\n1. 检查API Key是否正确(需为Web服务类型)\n2. 检查网络是否能访问高德API\n3. 检查城市ADCODE是否有效\n4. 确认API Key未超出每日调用限额`; } finally { // 恢复按钮状态 sendRequest.disabled = false; sendRequest.innerText = '发送请求'; } }); </script> </body> </html>2、快速使用步骤
步骤 1:准备高德 API Key
- 打开高德开放平台,登录后在「控制台」→「应用管理」获取Web 服务 API Key(必须是该类型,其他类型如 Android/iOS 会请求失败);
- 城市 ADCODE 获取:高德 ADCODE 官方查询工具,输入城市名获取 6 位数字 ADCODE(如北京 110000、广州 440100、深圳 440300)。
步骤 2:运行测试程序
- 将上述代码保存为
.html文件后,直接用 PC 浏览器双击打开(无需部署服务器,纯前端运行); - 在页面中输入「高德 API Key」和「城市 ADCODE」,选择请求模式(
base/all); - 点击「发送请求」,等待 1-3 秒即可看到 API 返回的完整格式化 JSON 数据。
3、关键使用说明
两种模式数据差异对比
请求模式 |
|
|
核心数据 | 仅实时天气( 数组) | 实时天气( )+未来预报( 数组) |
数据项 | 实时温湿度、风力、风向、空气质量、发布时间等 | 包含 base 所有数据 + 未来 3-7 天预报(日期、天气状况、最高 / 最低温、风力、风向等) |
适用场景 | 仅需实时天气数据 | 需要实时 + 未来多天预报数据 |
三、核心功能模块说明
1、 硬件层模块
模块 | 硬件型号 / 引脚 | 功能说明 |
温湿度采集 | DHT11(引脚 13) | 每秒读取温度 / 湿度,异常值置 0 |
烟雾检测 | MQ2(模拟引脚 7) | 读取模拟 ADC 值(0-4095),超过阈值触发报警 |
光照检测 | BH1750(I2C 39/40) | 读取环境光照强度(lux),按阈值控制 LED 颜色 |
报警输出 | 无源蜂鸣器(引脚 20) | PWM 通道 0,4500Hz 频率,超标时间歇鸣响(500ms 响 / 500ms 停) |
视觉反馈 | WS2812(引脚 48) | 光照 <100lux 蓝灯、100-500lux 绿灯、>500lux 黄灯,自动模式关闭则关灯 |
2、网络层模块
- WiFi 管理:连接指定 SSID/PWD,连接成功后打印设备 IP 地址,前端通过该 IP 访问 Web 界面;
- 异步 Web 服务器:基于
ESPAsyncWebServer,非阻塞式处理 HTTP 请求,支持高并发; - WebSocket 通信:路径
/ws,实现前端 - 设备双向通信:- 设备→前端:每秒推送传感器实时数据;
- 前端→设备:接收阈值 / 开关配置,立即更新设备参数并应用;
- 天气 API 调用:通过
HTTPClient调用高德天气 API,支持:extensions=base:获取实时天气(城市 / 温度 / 湿度 / 风向 / 更新时间);extensions=all:获取 4 天预报(日期 / 星期 / 昼夜天气 / 温度 / 风力);- 缓存机制:10 分钟更新一次,避免频繁请求 API。
3、前端交互模块
- 基于 TailwindCSS + Alpine.js 开发的响应式 Web 界面,适配移动端;
- 核心展示区域:
- 实时天气卡片:城市 / 温度 / 天气 / 风向 / 湿度 / 更新时间;
- 室内温湿度卡片:数值可视化展示;
- 天气预报卡片:4 天昼夜天气 / 温度列表;
- 室内环境卡片:烟雾浓度(超标标红)、光照强度(按阈值变色);
- 配置区域:滑动条调整烟雾 / 光照阈值,开关控制自动报警 / LED 联动。
4、数据处理模块
- 传感器数据封装:
SensorData结构体存储温湿度、烟雾、光照、更新时间; - 报警配置封装:
AlarmConfig结构体存储阈值、自动开关、报警状态,默认值可直接修改; - JSON 序列化 / 反序列化:基于
ArduinoJson:- 设备→前端:
getSensorJson()序列化传感器数据为 JSON; - 前端→设备:
onWsEvent反序列化配置消息,更新本地参数; - 天气数据:动态解析高德 API 返回的 JSON,提取核心字段并二次封装。
- 设备→前端:
四、重点功能与程序流程分析
1、主循环流程

2、web请求处理流程

3. WebSocket 双向通信(核心函数:onWsEvent())
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { if (type == WS_EVT_DATA) { String msg = String((char*)data, len); StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, msg); if (error) { return; } // 更新前端传递的配置参数 if (doc.containsKey("mq2Threshold")) { alarmConfig.mq2Threshold = doc["mq2Threshold"].as<int>(); } // 其他配置更新... // 立即应用配置+广播给所有客户端 handleMQ2Alarm(); handleLightLED(); server->textAll(getSensorJson()); } }关键说明:
- WebSocket 是实时通信的核心,相比 HTTP 轮询,减少网络开销;
server->textAll()实现 “一对多” 广播,所有连接的前端都会同步最新配置和数据。
4. 天气 API 解析(核心函数:updateWeatherData())
void updateWeatherData() { if (WiFi.status() != WL_CONNECTED) { return; } DynamicJsonDocument weatherDoc(3072); bool isLiveOk = false, isForecastOk = false; // 第一步:获取实时天气(base模式) HTTPClient httpLive; String liveUrl = getAmapApiUrl("base"); httpLive.begin(liveUrl); int httpCodeLive = httpLive.GET(); if (httpCodeLive == HTTP_CODE_OK) { // 解析实时天气字段:城市/温度/湿度/风向等 // 关键:docLive["lives"][0] 取第一个实时数据对象 } httpLive.end(); // 必须关闭连接,释放资源 // 第二步:获取预报天气(all模式) // 类似实时天气逻辑,解析4天预报数据,转换星期为中文 // 第三步:整合数据并序列化 if (isLiveOk || isForecastOk) { serializeJson(weatherDoc, weatherData); } }关键说明:
- 采用
HTTPClient分两次请求(base/all),避免单次请求数据过大; - 中文星期转换:
getWeekText()函数将数字(1-7)转为 “周一 - 周日”,提升用户体验; - 缓存天气数据到全局变量
weatherData,避免每次前端请求都调用 API。