基于FireBeetle 2 ESP32-C5的天气时钟

2025-09-213911
AI 快速预览详细 收起
本项目使用FireBeetle 2 ESP32-C5开发板和0.96英寸OLED显示屏制作一个桌面时钟,显示实时时间、日期和天气信息。硬件清单包括FireBeetle 2 ESP32-C5开发板、0.96英寸OLED显示屏以及扩展板。最终效果是通过WiFi连接互联网,从NTP服务器获取准确时间,并从天气API获取实时天气数据。难度等级适中,适合有一定基础的初学者。


一、设计思路
  • 项目概述本项目旨在利用FireBeetle 2 ESP32-C5开发板和0.96英寸OLED显示屏,制作一个能够显示实时时间、日期和天气信息的桌面时钟。设备通过WiFi连接到互联网,从NTP服务器获取准确时间,并从天气API获取实时天气数据。
  • 硬件设计


  • 主控芯片:FireBeetle 2 ESP32-C5(RISC-V架构,支持WiFi和蓝牙)
  • 显示屏:0.96英寸OLED(SSD1306驱动,128x64分辨率,I2C接口)
  • 连接方式:使用ESP32-C5的专用I2C引脚(SDA:9, SCL:10)连接OLED


  • 软件设计


  • 开发环境:Arduino IDE
  • 主要库:U8g2(显示)、WiFiMulti(WiFi连接)、ArduinoJson(JSON解析)、NTPClient(时间同步)


  • 界面设计考虑到OLED屏幕尺寸较小(128x64),界面设计采用简洁的三行布局:


  • 第一行(顶部):显示日期和星期,格式:MM月DD日 星期X(例如:09月21日 星期六)
  • 第二行(中部):显示时间,格式:HH:MM:SS(例如:07:53:30)
  • 第三行(底部):显示天气信息,格式:天气状况 温度℃ 湿度(例如:晴 25.5℃ 50%)


二、硬件介绍

1. 主控芯片:FireBeetle 2 ESP32-C5开发板



基于FireBeetle 2 ESP32-C5的天气时钟图13

  • 核心模组:乐鑫ESP32-C5(试用版为ECO1,正式版为ECO2),是一款支持Wi-Fi 6和蓝牙5.0的RISC-V单核芯片,兼具高性能与低功耗特性。
  • 丰富资源:内置4MB Flash,512KB SRAM,提供充足的程序存储和运行空间。
  • 开发优势:兼容Arduino IDE,开发便捷,生态丰富。


2. 扩展板:FireBeetle 2 ESP32-C5专用IO扩展板
基于FireBeetle 2 ESP32-C5的天气时钟图1
  • 功能:将开发板的引脚以排针形式引出,无需焊接即可快速连接各种传感器和执行器,极大方便了原型开发。


3. 显示设备:0.96英寸OLED显示屏
  • 接口:I2C通信协议,地址0x3C。
  • 特性:分辨率128x64,SSD1306驱动。
  • 连接:通过扩展板的I2C接口(IO10, IO9)与主板连接。


三、引脚及说明

基于FireBeetle 2 ESP32-C5的天气时钟图14

四、流程图
基于FireBeetle 2 ESP32-C5的天气时钟图2
五、项目制作过程


1. 安装Arduino IDE
基于FireBeetle 2 ESP32-C5的天气时钟图3
打开Arduino IDE安装包,按默认选项,点击下一步进行安装。
2. 安装ESP32开发板支持包
1、打开Arduino IDE,进入文件 -> 首选项,在附加开发板管理器网址中输入:https://jihulab.com/esp-mirror/e ... esp32_index_cn.json
基于FireBeetle 2 ESP32-C5的天气时钟图4
2、打开工具 -> 开发板 -> 开发板管理器,搜索esp32。
3、在开发板管理器中,找到**esp32 by Espressif Systems**,点击版本下拉菜单,选择 3.3.0-alpha1-cn 进行安装。
基于FireBeetle 2 ESP32-C5的天气时钟图5
3. 安装相应的库
1、U8g2库:用于驱动OLED屏幕,功能强大,支持大量显示控件和字体。通过项目 -> 加载库 -> 管理库搜索U8g2安装。
基于FireBeetle 2 ESP32-C5的天气时钟图6
2、ArduinoJson库:用于解析从天气API返回的JSON数据。同样通过库管理器搜索ArduinoJson安装(v7.x版本)。
基于FireBeetle 2 ESP32-C5的天气时钟图7
4. 编写代码
   创建新的Arduino项目,并编写代码:
  1. <font face="微软雅黑">#include <Arduino.h>
  2. #include <U8g2lib.h>
  3. #include "WiFiMulti.h"
  4. #include <time.h>
  5. #include <WiFiClient.h>
  6. #include <ArduinoJson.h>
  7. ​
  8. // -------------------------- 1. 硬件与配置参数 --------------------------
  9. #define I2C_SDA 9    // ESP32-C5专用I2C引脚
  10. #define I2C_SCL 10  
  11. const char* WIFI_SSID =     "你的WiFi名称";        // 你的WiFi名称
  12. const char* WIFI_PWD =      "你的WiFi密码";        // 你的WiFi密码
  13. const char* YT_APPID =      "易天API APPID";      // 易天API APPID
  14. const char* YT_APPSECRET =  "易天API密钥";         // 易天API密钥
  15. const char* YT_API_URL =    "v1.yiketianqi.com";
  16. const char* YT_CITYID =     "101110101";         // 西安城市ID
  17. ​
  18. // NTP服务器配置
  19. const char* ntpServer1 = "pool.ntp.org";
  20. const char* ntpServer2 = "time.nist.gov";
  21. const char* ntpServer3 = "ntp.aliyun.com";
  22. ​
  23. // 时区配置(东8区,无夏令时)
  24. const long gmtOffset_sec = 8 * 3600;
  25. const int daylightOffset_sec = 0;
  26. ​
  27. WiFiMulti wifiMulti;
  28. WiFiClient httpClient;
  29. ​
  30. // 初始化OLED屏幕
  31. U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ I2C_SCL, /* data=*/ I2C_SDA, /* reset=*/ U8X8_PIN_NONE);
  32. ​
  33. // 天气数据结构体
  34. struct WeatherInfo {
  35. String city;       // 城市名
  36. String condition;  // 天气状况
  37. float temp;        // 实时温度(℃)
  38. String humidity;   // 湿度
  39. String wind;       // 风向和风力
  40. bool isValid;      // 数据是否有效
  41. };
  42. WeatherInfo g_weather = {"未知", "未知", 0, "未知", "未知", false};
  43. ​
  44. // 天气更新计时器
  45. unsigned long g_lastWeatherUpdate = 0;
  46. const unsigned long WEATHER_UPDATE_INTERVAL = 300000; // 5分钟
  47. ​
  48. // -------------------------- 函数声明 --------------------------
  49. void initWiFiConn();
  50. void initNTPTime();
  51. bool updateWeather();
  52. bool parseWeatherResponse(const String& response);
  53. String getWeekdayCN(int wday, bool fullFormat = true); // 在这里指定默认参数
  54. void printLocalTime();
  55. void logDebug(const String& msg);
  56. void drawCenteredString(int y, const String &str, const uint8_t *font = u8g2_font_wqy14_t_gb2312);
  57. ​
  58. // -------------------------- 主函数 --------------------------
  59. void setup() {
  60. Serial.begin(115200);
  61. // 初始化OLED屏幕
  62. u8g2.begin();
  63. u8g2.enableUTF8Print();  // 启用UTF8打印支持
  64. ​
  65. Serial.println();
  66. Serial.println("开始连接WiFi...");
  67. WiFi.mode(WIFI_STA);
  68. wifiMulti.addAP(WIFI_SSID, WIFI_PWD);
  69. ​
  70. // 显示连接提示
  71. u8g2.firstPage();
  72. do {
  73.    u8g2.setFont(u8g2_font_wqy14_t_gb2312);
  74.    u8g2.setCursor(10, 30);
  75.    u8g2.print("连接WiFi中...");
  76. } while (u8g2.nextPage());
  77. ​
  78. // 等待WiFi连接
  79. unsigned long startTime = millis();
  80. while (wifiMulti.run() != WL_CONNECTED) {
  81.    delay(500);
  82.    Serial.print(".");
  83.    
  84.    // 超时处理(30秒)
  85.    if (millis() - startTime > 30000) {
  86.      Serial.println("WiFi连接超时");
  87.      u8g2.firstPage();
  88.      do {
  89.        u8g2.setFont(u8g2_font_wqy14_t_gb2312);
  90.        u8g2.setCursor(10, 30);
  91.        u8g2.print("WiFi连接超时");
  92.     } while (u8g2.nextPage());
  93.      delay(2000);
  94.      break;
  95.   }
  96. }
  97. ​
  98. if (WiFi.status() == WL_CONNECTED) {
  99.    Serial.println();
  100.    Serial.println("WiFi连接成功!");
  101.    Serial.print("IP地址: ");
  102.    Serial.println(WiFi.localIP());
  103. ​
  104.    // 显示连接成功
  105.    u8g2.firstPage();
  106.    do {
  107.      u8g2.setFont(u8g2_font_wqy14_t_gb2312);
  108.      u8g2.setCursor(10, 30);
  109.      u8g2.print("WiFi连接成功");
  110.      u8g2.setCursor(10, 50);
  111.      u8g2.print("IP: ");
  112.      u8g2.print(WiFi.localIP().toString());
  113.   } while (u8g2.nextPage());
  114.    delay(1000);
  115. ​
  116.    // 配置NTP时间同步
  117.    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3);
  118.    Serial.println("NTP配置完成,等待时间同步...");
  119.    
  120.    // 显示同步提示
  121.    u8g2.firstPage();
  122.    do {
  123.      u8g2.setFont(u8g2_font_wqy14_t_gb2312);
  124.      u8g2.setCursor(10, 30);
  125.      u8g2.print("同步时间中...");
  126.   } while (u8g2.nextPage());
  127. ​
  128.    // 等待时间同步完成
  129.    struct tm timeinfo;
  130.    startTime = millis();
  131.    while (!getLocalTime(&timeinfo)) {
  132.      delay(500);
  133.      Serial.print(".");
  134.      
  135.      // 超时处理(15秒)
  136.      if (millis() - startTime > 15000) {
  137.        Serial.println("时间同步超时");
  138.        u8g2.firstPage();
  139.        do {
  140.          u8g2.setFont(u8g2_font_wqy14_t_gb2312);
  141.          u8g2.setCursor(10, 30);
  142.          u8g2.print("时间同步超时");
  143.       } while (u8g2.nextPage());
  144.        delay(2000);
  145.        break;
  146.     }
  147.   }
  148.    
  149.    if (getLocalTime(&timeinfo)) {
  150.      Serial.println("时间同步成功");
  151.      printLocalTime();
  152.   }
  153. ​
  154.    // 首次获取天气
  155.    updateWeather();
  156.    g_lastWeatherUpdate = millis();
  157. }
  158. }
  159. ​
  160. void loop() {
  161. // 检查WiFi连接
  162. if (wifiMulti.run() != WL_CONNECTED) {
  163.    u8g2.firstPage();
  164.    do {
  165.      u8g2.setFont(u8g2_font_wqy14_t_gb2312);
  166.      u8g2.setCursor(10, 30);
  167.      u8g2.print("WiFi已断开");
  168.   } while (u8g2.nextPage());
  169.    delay(1000);
  170.    return;
  171. }
  172. ​
  173. // 定时更新天气(每5分钟)
  174. if (millis() - g_lastWeatherUpdate >= WEATHER_UPDATE_INTERVAL) {
  175.    if (updateWeather()) {
  176.      g_lastWeatherUpdate = millis();
  177.   } else {
  178.      // 如果更新失败,10秒后重试
  179.      g_lastWeatherUpdate = millis() - WEATHER_UPDATE_INTERVAL + 10000;
  180.   }
  181. }
  182. ​
  183. // 获取当前时间并显示
  184. struct tm timeinfo;
  185. if (!getLocalTime(&timeinfo)) {
  186.    u8g2.firstPage();
  187.    do {
  188.      u8g2.setFont(u8g2_font_wqy14_t_gb2312);
  189.      u8g2.setCursor(10, 30);
  190.      u8g2.print("时间获取失败");
  191.   } while (u8g2.nextPage());
  192.    delay(1000);
  193.    return;
  194. }
  195. ​
  196. // 在OLED上显示所有信息
  197. u8g2.firstPage();
  198. do {
  199.    // 第一行:日期和星期
  200.    char dateStr[12];
  201.    sprintf(dateStr, "%02d月%02d日", timeinfo.tm_mon + 1, timeinfo.tm_mday);
  202.    String dateWeekStr = String(dateStr) + " " + getWeekdayCN(timeinfo.tm_wday, true); // 明确指定参数
  203.    drawCenteredString(13, dateWeekStr);
  204.    
  205.    // 第二行:时间(大字体)
  206.    char timeStr[9];
  207.    sprintf(timeStr, "%02d:%02d:%02d",
  208.            timeinfo.tm_hour,
  209.            timeinfo.tm_min,
  210.            timeinfo.tm_sec);
  211.    
  212.    // 居中显示时间
  213.    drawCenteredString(42, timeStr, u8g2_font_logisoso24_tn);
  214.    
  215.    // 第三行:天气信息
  216.    if (g_weather.isValid) {
  217.      String weatherStr = g_weather.condition + " " + String(g_weather.temp, 1) + "℃ " + g_weather.humidity;
  218.      drawCenteredString(62, weatherStr);
  219.   } else {
  220.      // 居中显示"天气获取中..."
  221.      drawCenteredString(62, "天气获取中...");
  222.   }
  223. } while (u8g2.nextPage());
  224. ​
  225. delay(1000);
  226. }
  227. ​
  228. // -------------------------- 功能函数 --------------------------
  229. bool updateWeather() {
  230. Serial.println("开始更新天气数据...");
  231. // 连接服务器
  232. if (!httpClient.connect(YT_API_URL, 80)) {
  233.    Serial.println("天气服务器连接失败");
  234.    g_weather.isValid = false;
  235.    return false;
  236. }
  237. ​
  238. // 发送API请求
  239. String request = "GET /free/day?appid=" + String(YT_APPID) +
  240.                   "&appsecret=" + String(YT_APPSECRET) +
  241.                   "&cityid=" + String(YT_CITYID) +
  242.                   "&unescape=1 HTTP/1.1\r\n" +
  243.                   "Host: " + YT_API_URL + "\r\n" +
  244.                   "Connection: close\r\n\r\n";
  245. ​
  246. httpClient.print(request);
  247. // 等待响应
  248. unsigned long startTime = millis();
  249. while (!httpClient.available() && millis() - startTime < 5000) {
  250.    delay(100);
  251. }
  252. if (!httpClient.available()) {
  253.    Serial.println("天气响应超时");
  254.    httpClient.stop();
  255.    g_weather.isValid = false;
  256.    return false;
  257. }
  258. ​
  259. // 读取HTTP状态行
  260. String statusLine = httpClient.readStringUntil('\n');
  261. Serial.println("HTTP状态: " + statusLine);
  262. // 检查HTTP状态码
  263. if (statusLine.indexOf("200") == -1) {
  264.    Serial.println("HTTP错误: " + statusLine);
  265.    httpClient.stop();
  266.    g_weather.isValid = false;
  267.    return false;
  268. }
  269. // 跳过HTTP头部
  270. while (httpClient.available()) {
  271.    String line = httpClient.readStringUntil('\n');
  272.    if (line == "\r") {
  273.      break; // 头部结束
  274.   }
  275. }
  276. ​
  277. // 读取JSON响应
  278. String response = "";
  279. while (httpClient.available()) {
  280.    response += httpClient.readString();
  281. }
  282. httpClient.stop();
  283. ​
  284. // 打印响应前200字符用于调试
  285. Serial.println("响应内容: " + response.substring(0, 200));
  286. // 处理空响应
  287. if (response.isEmpty()) {
  288.    Serial.println("天气响应为空");
  289.    g_weather.isValid = false;
  290.    return false;
  291. }
  292. ​
  293. // 解析天气数据
  294. if (parseWeatherResponse(response)) {
  295.    Serial.println("天气更新成功: " + g_weather.city + " " + g_weather.condition + " " + String(g_weather.temp, 1) + "℃ " + g_weather.humidity);
  296.    return true;
  297. } else {
  298.    Serial.println("天气解析失败");
  299.    g_weather.isValid = false;
  300.    return false;
  301. }
  302. }
  303. ​
  304. bool parseWeatherResponse(const String& response) {
  305. // 清理响应中的可能存在的非法字符
  306. String cleanResponse = response;
  307. cleanResponse.trim();
  308. // 检查响应是否以{开头,以}结尾(基本JSON验证)
  309. if (!cleanResponse.startsWith("{") || !cleanResponse.endsWith("}")) {
  310.    Serial.println("响应不是有效的JSON格式");
  311.    
  312.    // 尝试找到JSON开始和结束位置
  313.    int jsonStart = cleanResponse.indexOf('{');
  314.    int jsonEnd = cleanResponse.lastIndexOf('}');
  315.    
  316.    if (jsonStart >= 0 && jsonEnd > jsonStart) {
  317.      cleanResponse = cleanResponse.substring(jsonStart, jsonEnd + 1);
  318.      Serial.println("提取的JSON: " + cleanResponse);
  319.   } else {
  320.      return false;
  321.   }
  322. }
  323. ​
  324. JsonDocument doc;
  325. DeserializationError error = deserializeJson(doc, cleanResponse);
  326. ​
  327. if (error) {
  328.    Serial.println("JSON解析错误: " + String(error.c_str()));
  329.    return false;
  330. }
  331. ​
  332. // 检查核心字段
  333. if (doc["city"].isNull() || doc["wea"].isNull() || doc["tem"].isNull()) {
  334.    Serial.println("JSON字段缺失");
  335.    
  336.    // 打印所有可用字段用于调试
  337.    Serial.println("可用字段:");
  338.    for (JsonPair kv : doc.as<JsonObject>()) {
  339.      Serial.printf(" %s: %s\n", kv.key().c_str(), kv.value().as<String>().c_str());
  340.   }
  341.    
  342.    return false;
  343. }
  344. ​
  345. // 根据API响应格式更新解析逻辑
  346. g_weather.city = doc["city"].as<String>();
  347. g_weather.condition = doc["wea"].as<String>();
  348. // 温度字段可能是字符串,需要转换为整数
  349. String tempStr = doc["tem"].as<String>();
  350. g_weather.temp = tempStr.toFloat();
  351. ​
  352. // 添加湿度信息
  353. if (!doc["humidity"].isNull()) {
  354.    g_weather.humidity = doc["humidity"].as<String>();
  355. } else {
  356.    g_weather.humidity = "未知";
  357. }
  358. // 添加风力信息
  359. if (!doc["win"].isNull() && !doc["win_speed"].isNull()) {
  360.    g_weather.wind = doc["win"].as<String>() + doc["win_speed"].as<String>();
  361. } else {
  362.    g_weather.wind = "未知";
  363. }
  364. g_weather.isValid = true;
  365. ​
  366. return true;
  367. }
  368. ​
  369. // 修改函数定义,移除默认参数
  370. String getWeekdayCN(int wday, bool fullFormat) {
  371. const char* fullDays[] = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"};
  372. const char* shortDays[] = {"日", "一", "二", "三", "四", "五", "六"};
  373. if (wday < 0 || wday > 6) {
  374.    return fullFormat ? "未知" : "?";
  375. }
  376. return fullFormat ? fullDays[wday] : shortDays[wday];
  377. }
  378. ​
  379. void printLocalTime() {
  380. struct tm timeinfo;
  381. if (!getLocalTime(&timeinfo)) {
  382.    Serial.println("获取时间失败");
  383.    return;
  384. }
  385. Serial.printf("当前时间: %04d-%02d-%02d %02d:%02d:%02d 星期%s\n",
  386.        timeinfo.tm_year + 1900, timeinfo.tm_mon + 1,
  387.        timeinfo.tm_mday, timeinfo.tm_hour,
  388.        timeinfo.tm_min, timeinfo.tm_sec,
  389.        getWeekdayCN(timeinfo.tm_wday, false).c_str());
  390. }
  391. ​
  392. void logDebug(const String& msg) {
  393. struct tm timeinfo;
  394. if (getLocalTime(&timeinfo)) {
  395.    char timeBuf[20];
  396.    strftime(timeBuf, sizeof(timeBuf), "[%H:%M:%S] ", &timeinfo);
  397.    Serial.println(String(timeBuf) + msg);
  398. } else {
  399.    Serial.println("[未知时间] " + msg);
  400. }
  401. }
  402. ​
  403. // 居中显示文本的辅助函数
  404. void drawCenteredString(int y, const String &str, const uint8_t *font) {
  405. u8g2.setFont(font);
  406. int width = u8g2.getUTF8Width(str.c_str());
  407. int x = (128 - width) / 2;
  408. u8g2.setCursor(x, y);
  409. u8g2.print(str);
  410. }</font>
复制代码


5. 编译与上传
  • 在Arduino IDE中,选择正确的开发板和端口。


    • 开发板:ESP32C5 Dev Module
    • 端口:选择对应的COM口,COM4(ESP32 Family Device)。


  • 点击验证(✓)编译代码,确保无错误。
  • 点击上传(→)将编译后的程序烧录至开发板。


6. 运行与测试
   上传成功后,开发板将自动重启。OLED屏幕将依次显示“连接WiFi中...”、“WiFi连接成功”、"同步时间中...",最后进入主界面,显示时间、日期和天气信息。观察屏幕显示是否正常,并通过串口监视器(波特率115200)查看详细的调试日志,以便在出现问题时进行排查。
六、主要代码及说明


1. 硬件配置与库引入
  1. <font face="微软雅黑">#include <Arduino.h>
  2. #include <U8g2lib.h>
  3. #include "WiFiMulti.h"
  4. #include <time.h>
  5. #include <WiFiClient.h>
  6. #include <ArduinoJson.h>
  7. ​
  8. // 硬件引脚定义
  9. #define I2C_SDA 9    // ESP32-C5专用I2C引脚
  10. #define I2C_SCL 10
  11. ​
  12. // 网络和API配置
  13. const char* WIFI_SSID =     "你的WiFi名称";        // 你的WiFi名称
  14. const char* WIFI_PWD =      "你的WiFi密码";        // 你的WiFi密码
  15. const char* YT_APPID =      "易天API APPID";      // 易天API APPID
  16. const char* YT_APPSECRET =  "易天API密钥";         // 易天API密钥
  17. const char* YT_API_URL =    "v1.yiketianqi.com";
  18. const char* YT_CITYID =     "101110101";         // 西安城市ID
  19. ​
  20. // NTP服务器配置
  21. const char* ntpServer1 = "pool.ntp.org";
  22. const char* ntpServer2 = "time.nist.gov";
  23. const char* ntpServer3 = "ntp.aliyun.com";
  24. ​
  25. // 时区配置(东8区)
  26. const long gmtOffset_sec = 8 * 3600;
  27. const int daylightOffset_sec = 0;</font>
复制代码

说明:
  • 引入了必要的库文件,包括显示驱动、网络连接、时间处理和JSON解析等功能
  • 定义了ESP32-C5专用的I2C引脚(9和10),这与常见的ESP32引脚不同
  • 配置了WiFi连接信息和天气API参数,需要根据实际环境修改
  • 设置了多个NTP服务器以提高时间同步的可靠性
  • 配置了东8区时区,无夏令时


2. 对象初始化与数据结构
  1. <font face="微软雅黑">// 初始化网络和显示对象
  2. WiFiMulti wifiMulti;
  3. WiFiClient httpClient;
  4. U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, I2C_SCL, I2C_SDA, U8X8_PIN_NONE);
  5. ​
  6. // 天气数据结构体
  7. struct WeatherInfo {
  8. String city;       // 城市名
  9. String condition;  // 天气状况
  10. float temp;        // 实时温度(℃)
  11. String humidity;   // 湿度
  12. String wind;       // 风向和风力
  13. bool isValid;      // 数据是否有效
  14. };
  15. WeatherInfo g_weather = {"未知", "未知", 0, "未知", "未知", false};
  16. ​
  17. // 天气更新计时器
  18. unsigned long g_lastWeatherUpdate = 0;
  19. const unsigned long WEATHER_UPDATE_INTERVAL = 300000; // 5分钟</font>
复制代码

说明:
  • 创建了WiFi多连接管理对象、HTTP客户端和OLED显示对象
  • 定义了WeatherInfo结构体来组织天气数据,使代码更加清晰
  • 使用全局变量g_weather存储当前天气信息,并初始化默认值
  • 设置了天气更新间隔为5分钟,避免频繁请求API


3. 初始化设置
  1. <font face="微软雅黑">void setup() {
  2. Serial.begin(115200);
  3. // 初始化OLED屏幕
  4. u8g2.begin();
  5. u8g2.enableUTF8Print();  // 启用UTF8打印支持
  6. ​
  7. // WiFi连接设置
  8. WiFi.mode(WIFI_STA);
  9. wifiMulti.addAP(WIFI_SSID, WIFI_PWD);
  10. // 显示连接提示
  11. u8g2.firstPage();
  12. do {
  13.    u8g2.setFont(u8g2_font_wqy14_t_gb2312);
  14.    u8g2.setCursor(10, 30);
  15.    u8g2.print("连接WiFi中...");
  16. } while (u8g2.nextPage());
  17. // 等待WiFi连接(含超时处理)
  18. // ...
  19. // 配置NTP时间同步
  20. configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3);
  21. // 首次获取天气
  22. updateWeather();
  23. g_lastWeatherUpdate = millis();
  24. }</font>
复制代码

说明:
  • 初始化串口通信用于调试
  • 启动OLED显示屏并启用UTF-8支持,这是显示中文的必要设置
  • 设置WiFi为站模式并添加网络凭据
  • 在OLED上显示连接状态,提供用户反馈
  • 配置NTP服务器进行时间同步
  • 首次启动时立即获取天气信息


4. 主循环
  1. <font face="微软雅黑">void loop() {
  2. // 检查WiFi连接
  3. if (wifiMulti.run() != WL_CONNECTED) {
  4.    // 显示断开信息
  5.    // ...
  6.    delay(1000);
  7.    return;
  8. }
  9. ​
  10. // 定时更新天气(每5分钟)
  11. if (millis() - g_lastWeatherUpdate >= WEATHER_UPDATE_INTERVAL) {
  12.    if (updateWeather()) {
  13.      g_lastWeatherUpdate = millis();
  14.   } else {
  15.      // 如果更新失败,10秒后重试
  16.      g_lastWeatherUpdate = millis() - WEATHER_UPDATE_INTERVAL + 10000;
  17.   }
  18. }
  19. ​
  20. // 获取并显示当前时间
  21. struct tm timeinfo;
  22. if (!getLocalTime(&timeinfo)) {
  23.    // 显示时间获取失败
  24.    // ...
  25.    delay(1000);
  26.    return;
  27. }
  28. ​
  29. // 在OLED上显示所有信息
  30. u8g2.firstPage();
  31. do {
  32.    // 显示日期和星期(居中)
  33.    // 显示时间(大字体,居中)
  34.    // 显示天气信息(居中)
  35. } while (u8g2.nextPage());
  36. ​
  37. delay(1000); // 每秒更新一次
  38. }</font>
复制代码

说明:
  • 主循环持续检查WiFi连接状态,确保设备在线
  • 每5分钟尝试更新一次天气信息,失败后10秒重试
  • 获取本地时间并处理可能的获取失败情况
  • 使用U8g2库的页面循环模式高效更新OLED显示内容
  • 每秒更新一次显示,保持时间的实时性


5. 天气更新功能
  1. <font face="微软雅黑">bool updateWeather() {
  2. // 连接天气API服务器
  3. if (!httpClient.connect(YT_API_URL, 80)) {
  4.    Serial.println("天气服务器连接失败");
  5.    g_weather.isValid = false;
  6.    return false;
  7. }
  8. ​
  9. // 构建并发送HTTP请求
  10. String request = "GET /free/day?appid=" + String(YT_APPID) +
  11.                   "&appsecret=" + String(YT_APPSECRET) +
  12.                   "&cityid=" + String(YT_CITYID) +
  13.                   "&unescape=1 HTTP/1.1\r\n" +
  14.                   "Host: " + YT_API_URL + "\r\n" +
  15.                   "Connection: close\r\n\r\n";
  16. httpClient.print(request);
  17. // 等待和读取响应
  18. // ...
  19. // 解析天气数据
  20. if (parseWeatherResponse(response)) {
  21.    Serial.println("天气更新成功");
  22.    return true;
  23. } else {
  24.    Serial.println("天气解析失败");
  25.    g_weather.isValid = false;
  26.    return false;
  27.   }
  28. }</font>
复制代码

说明:
  • 建立与天气API服务器的HTTP连接
  • 构建符合API要求的GET请求,包含必要的参数
  • 处理HTTP响应,包括状态码检查和头部跳过
  • 调用解析函数处理返回的JSON数据
  • 提供详细的错误处理和日志输出,便于调试


6.  JSON数据解析
  1. <font face="微软雅黑">bool parseWeatherResponse(const String& response) {
  2. // 清理响应中的可能存在的非法字符
  3. String cleanResponse = response;
  4. cleanResponse.trim();
  5. // 检查JSON格式有效性
  6. if (!cleanResponse.startsWith("{") || !cleanResponse.endsWith("}")) {
  7.    // 尝试提取有效JSON部分
  8.    // ...
  9. }
  10. ​
  11. // 使用ArduinoJson解析JSON
  12. JsonDocument doc;
  13. DeserializationError error = deserializeJson(doc, cleanResponse);
  14. ​
  15. if (error) {
  16.    Serial.println("JSON解析错误: " + String(error.c_str()));
  17.    return false;
  18. }
  19. ​
  20. // 提取并存储天气信息
  21. g_weather.city = doc["city"].as<String>();
  22. g_weather.condition = doc["wea"].as<String>();
  23. String tempStr = doc["tem"].as<String>();
  24. g_weather.temp = tempStr.toFloat(); // 转换为浮点数
  25. ​
  26. // 提取其他可选字段
  27. if (!doc["humidity"].isNull()) {
  28.    g_weather.humidity = doc["humidity"].as<String>();
  29. }
  30. g_weather.isValid = true;
  31. return true;
  32. }</font>
复制代码

说明:
  • 对原始响应进行预处理,提高解析成功率
  • 使用ArduinoJson库解析JSON数据,该库特别适合嵌入式设备
  • 检查必要字段是否存在,确保数据的完整性
  • 处理温度数据的转换(从字符串到浮点数)
  • 提供灵活的字段处理,即使某些可选字段缺失也不会导致解析失败


7. 工具函数
  1. <font face="微软雅黑">// 统一的星期获取函数
  2. String getWeekdayCN(int wday, bool fullFormat) {
  3. const char* fullDays[] = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"};
  4. const char* shortDays[] = {"日", "一", "二", "三", "四", "五", "六"};
  5. if (wday < 0 || wday > 6) {
  6.    return fullFormat ? "未知" : "?";
  7. }
  8. return fullFormat ? fullDays[wday] : shortDays[wday];
  9. }
  10. ​
  11. // 居中显示文本的辅助函数
  12. void drawCenteredString(int y, const String &str, const uint8_t *font) {
  13. u8g2.setFont(font);
  14. int width = u8g2.getUTF8Width(str.c_str());
  15. int x = (128 - width) / 2;
  16. u8g2.setCursor(x, y);
  17. u8g2.print(str);
  18. }
  19. ​
  20. // 带时间戳的调试输出
  21. void logDebug(const String& msg) {
  22. struct tm timeinfo;
  23. if (getLocalTime(&timeinfo)) {
  24.    char timeBuf[20];
  25.    strftime(timeBuf, sizeof(timeBuf), "[%H:%M:%S] ", &timeinfo);
  26.    Serial.println(String(timeBuf) + msg);
  27. } else {
  28.    Serial.println("[未知时间] " + msg);
  29.   }
  30. }</font>
复制代码

说明:
  • getWeekdayCN函数将数字表示的星期几转换为中文名称,支持完整和简写两种格式
  • drawCenteredString函数实现了文本居中显示功能,简化了显示代码
  • logDebug函数提供带时间戳的调试信息,有助于问题排查和运行状态监控


七、效果展示
1. 连接Wifi中
基于FireBeetle 2 ESP32-C5的天气时钟图8
2. Wifi连接成功
基于FireBeetle 2 ESP32-C5的天气时钟图9
3. 同步时间中
基于FireBeetle 2 ESP32-C5的天气时钟图10
4. 显示天气与时钟
基于FireBeetle 2 ESP32-C5的天气时钟图12
5. 串口输出
基于FireBeetle 2 ESP32-C5的天气时钟图11
八、 总结
本项目成功实现了一个功能完整的网络天气时钟。FireBeetle 2 ESP32-C5凭借其强大的网络功能和兼容性,使得开发过程非常顺畅。
需要注意的是:1、 由于目前的测试版的Firebeetle 2 ESP32-C5开发板板载ESP32-C5模组为ECO1 版本,Arduino IDE开发环境中esp32必须选择3.3.0-alpha1,选择其他版本会出现无法正常上传程序的问题。2、需要串口打印输出时,需要将Arduino IDE工具里的USB CDC on Boot选项修改为Enabled(启用),默认值是Disabled(关闭)。
未来,可在本项目的基础上进一步完善:1、增加空气质量、最高气温、最低气温等预报信息的显示;2、增加天气图标显示;3、显示未来7天的天气预报。





创作许可协议

本项目采用 None(不开放任何权利,保留所有权利) 进行许可。

评论(2)
腿毛利小五郎
nice学到了,成功烧录
PY学习笔记
可以用lvgl+micropython做一个
豆爸
豆爸
作者
回复PY学习笔记
好的,感谢,大佬,我再试试micropython+LVGL。
- 没有更多了 -