项目仓库源码:
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 - 空气质量:接口已包含在上述请求中,无需单独调用
- 实时天气 + 5 天预报:

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 class="header">
<h1>高德开放平台天气API 测试工具(PC端)</h1>
<p class="tip">用于调试extensions=base(实时)/all(实时+预报)模式,展示完整原始JSON数据 | 数据来源:<a href="https://lbs.amap.com/api/webservice/guide/api/weatherinfo" target="_blank">高德天气API官方文档</a></p>
</div>
<div class="config-panel">
<div class="config-item">
<label>高德API Key:</label>
<input type="text" id="amapKey" placeholder="请输入你的高德Web服务API Key" value="">
</div>
<div class="config-item">
<label>城市ADCODE:</label>
<input type="text" id="cityAdcode" placeholder="6位数字,如北京110000、上海310000" value="310000">
</div>
<div class="config-item">
<label>请求模式:</label>
<select id="extensionsMode">
<option value="base">base - 仅实时天气</option>
<option value="all" selected>all - 实时+未来预报</option>
</select>
</div>
<div class="btn-group">
<button class="btn btn-primary" id="sendRequest">发送请求</button>
<button class="btn btn-reset" id="resetForm">重置</button>
</div>
</div>
<div class="result-panel">
<div class="result-header">
<h3>API返回原始数据(格式化)</h3>
<div class="result-status" id="requestStatus">未发起请求</div>
</div>
<div class="result-content" id="resultContent">请点击「发送请求」获取高德天气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、关键使用说明
两种模式数据差异对比
|----------|------------------------|----------------------------------------------------|
| 请求模式 | extensions=base | extensions=all |
| 核心数据 | 仅实时天气 (lives 数组) | 实时天气(lives )+未来预报 (forecasts 数组) |
| 数据项 | 实时温湿度、风力、风向、空气质量、发布时间等 | 包含 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。