17浏览
查看: 17|回复: 1

[ESP8266/ESP32] 基于FireBeetle 2 ESP32-C5的天气时钟

[复制链接]
本帖最后由 豆爸 于 2025-9-21 10:28 编辑

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





PY学习笔记  中级技师

发表于 20 分钟前

可以用lvgl+micropython做一个
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail