工程解析
1. 工程目标与功能概览
这是一个基于 ESP32 的嵌入式天气预警终端,核心思路是:
- 通过 Wi-Fi 联网,周期性拉取天气预报与老黄历信息
- 通过 NTP 同步当前时间,用于显示与预警时段判断
- 通过 OLED 实时展示时间、当前小时天气、老黄历(宜/忌)
- 在特定时段(早/中/晚)根据“低温/降雨概率”点亮 WS2812B 指示灯
- 通过 DFPlayer Mini 播放对应的语音提示(按键触发播报)
2. 工程结构(架构视图)
当前工程只有一个 Arduino 草图文件,整体架构可以按“分层/模块”理解:
- 配置层:Wi-Fi/接口 Key/阈值/引脚定义
- 驱动层:OLED(U8g2)、NeoPixel、DFPlayer、网络(WiFi/HTTPClient)、时间(NTPClient)
- 数据层:WeatherData / AlmanacData 两个结构体缓存 API 数据
- 业务层:
- 定时拉取数据(3 小时一次)
- 预警判定(温度与降水概率阈值)
- 时段映射(早/中/晚三个监控窗口)
- 表现层:OLED UI 渲染、LED 灯光显示、语音播报
数据流示意:
- NTPClient → 当前时间 → OLED 显示 & 时段判断
- HTTPClient → JSON → 解析到结构体 → 预警逻辑 → LED/语音/OLED
3. 依赖库与职责(“用什么库做什么”)
在 weather_alert_system.ino 的开头引入了这些库:
WiFi.h:连接 Wi-Fi、获取连接状态与 IPHTTPClient.h:向 HTTP/HTTPS API 发起 GET 请求ArduinoJson.h:解析 JSON 响应并提取字段U8g2lib.h:驱动 SSD1306 OLED,负责绘制文本/UIDFRobotDFPlayerMini.h:控制 DFPlayer Mini 播放语音文件Adafruit_NeoPixel.h:驱动 WS2812B RGB LED 灯NTPClient.h+WiFiUdp.h:通过 UDP 从 NTP 服务器同步时间
4. 配置区解析(你需要改哪些)
4.1 Wi-Fi 参数
在文件顶部定义:
ssid、password:Wi-Fi 名称与密码
建议:不要把真实 Wi-Fi 密码提交到仓库或公开分享工程。
4.2 和风天气 API(QWeather)
QWEATHER_KEY:和风天气 KeyQWEATHER_LOCATION:城市 Location ID(需要替换为真实值)QWEATHER_URL:逐小时预报接口地址前缀
代码里会拼出完整 URL:
https://devapi.qweather.com/v7/weather/24h?location=<LOCATION>&key=<KEY>&hours=24
注意:/v7/weather/24h 是否支持 hours=24 取决于实际 API 规范;如果接口不支持该参数,可能会被忽略或导致错误(以实际返回为准)。
4.3 老黄历 API(聚合数据)
ALMANAC_KEY:聚合数据 KeyALMANAC_URL:接口前缀
完整 URL 类似:
http://v.juhe.cn/laohuangli/d?key=<KEY>&date=YYYY-MM-DD
4.4 硬件引脚与阈值
- OLED:
GPIO21(SDA) / GPIO22(SCL) - DFPlayer:
GPIO16/17使用硬件串口 2 - NeoPixel:
GPIO13,灯珠数量9 - Button:
GPIO34(ESP32 输入专用脚之一) - 阈值:
TEMP_LOW_THRESHOLD = 5:低温阈值(°C)RAIN_PROB_THRESHOLD = 50:降雨概率阈值(%)
5. 关键数据结构(数据怎么存)
5.1 WeatherData
用于缓存未来 24 小时逐小时预报,包含:
fxTime[24]:每小时的时间字符串(代码截取为HH:MM)temp[24]:温度(int)pop[24]:降水概率(int)text[24]:天气描述(String)isValid:数据是否有效(成功拉取并解析)
作用:把“网络请求”与“业务计算/UI 显示”解耦,避免每次显示都请求网络。
5.2 AlmanacData
yi/ji:当天宜/忌isValid:是否有效
6. Setup 初始化流程(上电后做什么)
入口函数 setup() 的逻辑顺序非常关键,整体是“先把外设点亮 → 再联网 → 再取数据”:
- 串口调试初始化:
Serial.begin(115200) - OLED 初始化:
u8g2.begin()并显示 “System Init...” - 按键初始化:
pinMode(BUTTON_PIN, INPUT_PULLUP) - NeoPixel 初始化:
pixels.begin()、亮度、清屏 - 连接 Wi-Fi:调用
connectWiFi() - 初始化 DFPlayer:调用
initDFPlayer() - 初始化 NTP:
timeClient.begin()、timeClient.update() - 首次拉取数据:
getWeatherData()getAlmanacData()
7. Loop 主循环(系统如何持续运行)
loop() 是典型的 Arduino “轮询式调度”:
- 更新时间:
timeClient.update(),读取newHour/newMinute - 定时拉数据:每
DATA_FETCH_INTERVAL(3 小时)触发一次 - 预警检查:调用
checkWarning()决定 LED 状态 - 刷新 OLED:调用
updateOLED() - 按键触发语音:按下按键后调用
playWarningVoice("0001")(当前写法固定传 "0001",但函数内部会再计算预警级别并选择播放文件) delay(1000):主循环每秒跑一次
7.1 一个重要逻辑细节(当前代码会导致“每秒检查预警”)
loop() 里先把 currentMinute = newMinute,后面又用:
if (newMinute != currentMinute || (currentHour == newHour && currentMinute == newMinute))
由于前面已经赋值,newMinute != currentMinute 永远为假,而第二个条件基本永远为真,因此 checkWarning() 会在每次循环(每秒)被调用。
这不会“功能错误”,但会导致串口输出更频繁、LED 更频繁刷新,整体更耗电/更“吵”。
8. 各函数代码片段解析(做什么、输入输出、关键点)
8.1 connectWiFi()
职责:
- 发起 Wi-Fi 连接并等待最多约 15 秒(30 次 * 500ms)
- OLED 显示连接状态
- 串口打印连接结果
关键点:
- 连接失败后只是提示,不会进入重试策略/休眠策略
8.2 initDFPlayer()
职责:
- 在硬件串口 2 上以 9600 初始化 DFPlayer
- 设置音量并播放一个“测试音/欢迎语”
关键点:
- 若初始化失败,仅打印错误,不会重试
8.3 getWeatherData()
职责:
- 拼接 QWeather API URL 并 GET
- 使用 ArduinoJson 解析
hourly[] - 将 24 条数据写入
hourlyWeather并置isValid
关键点:
- JSON 解析使用
DynamicJsonDocument capacity=10240,属于“经验估算”,若返回内容更大可能解析失败 fxTime只保留T和+之间的HH:MM,方便显示/匹配小时
8.4 getAlmanacData()
职责:
- 通过 NTP 时间生成
YYYY-MM-DD - 请求老黄历接口并解析
result.yi/result.ji
关键点:
- 使用
gmtime()生成日期:由于 NTPClient 已设置时区偏移,得到的 epochTime 已经是本地时间偏移后的值,gmtime()输出的“UTC 结构体”恰好对应本地时间的日期,这种写法在很多项目中可用,但要注意时区/夏令时需求时可能需要更严谨的处理
8.5 getTimeSlot(hour)
职责:
- 将小时映射为 3 个“重点提醒时段”
返回值:
1:06-092:12-153:17-190:其他时间
8.6 checkWarning()
职责:
- 根据当前小时在
hourlyWeather里找到对应的那条逐小时数据 - 判断低温、降雨,组合出
warningLevel - 调用
updateLEDs(currentSlot, warningLevel)
关键点:
warningLevel使用位运算组合:- 低温:
warningLevel |= 1 - 降雨:
warningLevel |= 2 - 双重:最终值为
3
- 低温:
- 若找不到对应小时的数据,会打印提示,但不会回退到“最近小时”或“默认策略”
8.7 updateLEDs(time_slot, warning_level)
职责:
- 将 9 颗灯分成 3 组,每组 3 颗对应一个时段
- 当前时段点亮对应颜色,其余灯清空
颜色规则:
- 低温:蓝
- 降雨:黄
- 双重:红
- 无预警:灭
关键点:
- 只显示“当前时段”的状态,而不是同时显示三个时段各自的预警(这是当前设计选择)
8.8 updateOLED()
职责:
- 顶部:当前时间(HH:MM 居中)
- 左侧:当前小时的天气(温度、降水概率、天气描述)
- 右侧:老黄历(宜/忌,截断为较短字符串)
- 底部:当前是否处在监控时段(Slot Active / Normal Time)
关键点:
- 截断宜/忌时使用
indexOf(' ')找第一个空格。如果返回 -1(没有空格),substring(0, -1)在 Arduino String 上的行为可能导致显示异常,建议后续改为更稳健的截断逻辑
8.9 playWarningVoice(message)
职责:
- 再次计算当前小时的
warningLevel - 根据级别选择播放的文件编号:
- 1:低温
- 2:降雨
- 3:双重
- 4:无预警
- 调用
myDFPlayer.play(file_number)
关键点:
- 函数入参
message目前没有参与选择逻辑(调用处传 "0001",但内部最终播放的并不一定是 1) if (myDFPlayer.available())的用途通常是“是否有来自 DFPlayer 的消息/状态可读”,并不等价于“忙/不可用”,这段判断可能与作者意图不一致(以实际运行表现为准)
8.10 checkAlarm()(闹钟框架)
职责:
- 当到达
ALARM_HOUR:ALARM_MINUTE时播放文件 5 alarmTriggeredToday避免当天重复触发- 在 00:01 重置状态
当前状态:
- 该函数没有被
loop()调用,属于预留功能框架
9. 工程“可扩展点”(后续怎么演进)
- 把“每秒刷新 OLED”改为“每 N 秒刷新”或“分钟级刷新”,降低功耗并减少 I2C 负载
- 把三个时段的预警分别显示为三组 LED(而不是只点亮当前时段)
- 增加“Wi-Fi 断线重连策略”和“请求失败退避重试”
- 把 Key/密码移出代码(例如编译时配置或本地私有文件),避免泄露