Skip to content

工程解析

项目文件:weather_alert_system.ino

1. 工程目标与功能概览

这是一个基于 ESP32 的嵌入式天气预警终端,核心思路是:

  • 通过 Wi-Fi 联网,周期性拉取天气预报与老黄历信息
  • 通过 NTP 同步当前时间,用于显示与预警时段判断
  • 通过 OLED 实时展示时间、当前小时天气、老黄历(宜/忌)
  • 在特定时段(早/中/晚)根据“低温/降雨概率”点亮 WS2812B 指示灯
  • 通过 DFPlayer Mini 播放对应的语音提示(按键触发播报)

2. 工程结构(架构视图)

当前工程只有一个 Arduino 草图文件,整体架构可以按“分层/模块”理解:

  1. 配置层:Wi-Fi/接口 Key/阈值/引脚定义
  2. 驱动层:OLED(U8g2)、NeoPixel、DFPlayer、网络(WiFi/HTTPClient)、时间(NTPClient)
  3. 数据层:WeatherData / AlmanacData 两个结构体缓存 API 数据
  4. 业务层:
    • 定时拉取数据(3 小时一次)
    • 预警判定(温度与降水概率阈值)
    • 时段映射(早/中/晚三个监控窗口)
  5. 表现层:OLED UI 渲染、LED 灯光显示、语音播报

数据流示意:

  • NTPClient → 当前时间 → OLED 显示 & 时段判断
  • HTTPClient → JSON → 解析到结构体 → 预警逻辑 → LED/语音/OLED

3. 依赖库与职责(“用什么库做什么”)

weather_alert_system.ino 的开头引入了这些库:

  • WiFi.h:连接 Wi-Fi、获取连接状态与 IP
  • HTTPClient.h:向 HTTP/HTTPS API 发起 GET 请求
  • ArduinoJson.h:解析 JSON 响应并提取字段
  • U8g2lib.h:驱动 SSD1306 OLED,负责绘制文本/UI
  • DFRobotDFPlayerMini.h:控制 DFPlayer Mini 播放语音文件
  • Adafruit_NeoPixel.h:驱动 WS2812B RGB LED 灯
  • NTPClient.h + WiFiUdp.h:通过 UDP 从 NTP 服务器同步时间

4. 配置区解析(你需要改哪些)

4.1 Wi-Fi 参数

在文件顶部定义:

  • ssidpassword:Wi-Fi 名称与密码

建议:不要把真实 Wi-Fi 密码提交到仓库或公开分享工程。

4.2 和风天气 API(QWeather)

  • QWEATHER_KEY:和风天气 Key
  • QWEATHER_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:聚合数据 Key
  • ALMANAC_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() 的逻辑顺序非常关键,整体是“先把外设点亮 → 再联网 → 再取数据”:

  1. 串口调试初始化:Serial.begin(115200)
  2. OLED 初始化:u8g2.begin() 并显示 “System Init...”
  3. 按键初始化:pinMode(BUTTON_PIN, INPUT_PULLUP)
  4. NeoPixel 初始化:pixels.begin()、亮度、清屏
  5. 连接 Wi-Fi:调用 connectWiFi()
  6. 初始化 DFPlayer:调用 initDFPlayer()
  7. 初始化 NTP:timeClient.begin()timeClient.update()
  8. 首次拉取数据:
    • getWeatherData()
    • getAlmanacData()

7. Loop 主循环(系统如何持续运行)

loop() 是典型的 Arduino “轮询式调度”:

  1. 更新时间:timeClient.update(),读取 newHour/newMinute
  2. 定时拉数据:每 DATA_FETCH_INTERVAL(3 小时)触发一次
  3. 预警检查:调用 checkWarning() 决定 LED 状态
  4. 刷新 OLED:调用 updateOLED()
  5. 按键触发语音:按下按键后调用 playWarningVoice("0001")(当前写法固定传 "0001",但函数内部会再计算预警级别并选择播放文件)
  6. 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-09
  • 2:12-15
  • 3:17-19
  • 0:其他时间

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/密码移出代码(例如编译时配置或本地私有文件),避免泄露