项目演示:
tq
rl
目录
[1. 准备工作:注册心知天气并获取API密钥](#1. 准备工作:注册心知天气并获取API密钥)
[2. 添加必要的库](#2. 添加必要的库)
[3. 编写代码:data.c 深度解析](#3. 编写代码:data.c 深度解析)
[3.1 包含头文件和定义全局变量](#3.1 包含头文件和定义全局变量)
[3.2 获取时间函数 time_get()](#3.2 获取时间函数 time_get())
[3.3 获取天气函数 weather_get() (核心逻辑)](#3.3 获取天气函数 weather_get() (核心逻辑))
[3.4 数据更新任务函数 data_update()](#3.4 数据更新任务函数 data_update())
[3.5 创建独立的FreeRTOS任务](#3.5 创建独立的FreeRTOS任务)
[4. 总结与保姆级操作步骤](#4. 总结与保姆级操作步骤)
[5. 常见问题与调试](#5. 常见问题与调试)
时间和天气功能实现保姆级教程
本章的目标是让你的ESP32-S3智能终端能够:
-
从NTP服务器获取准确时间,并在屏幕上动态更新。
-
从心知天气API获取实时天气和未来天气预报,并显示在UI上。
-
通过 FreeRTOS任务,让这些网络请求和数据处理在后台自动运行,不影响界面刷新。
1. 准备工作:注册心知天气并获取API密钥
-
第一步:注册账号
访问心知天气官网,完成注册和登录。个人开发者通常有免费试用额度,足够学习使用。
-
第二步:找到你的私钥
登录后,在控制台或产品管理页面,找到你的"API密钥"(通常是一串字母和数字的组合,例如
SHdGnMJ4MzuCuy0-L)。这个密钥是你的程序访问天气数据的凭证,需要保密。 -
第三步:了解API文档(可选)
教程中使用的API地址是官方提供的:
-
实时天气:
https://api.seniverse.com/v3/weather/now.json -
逐日天气预报:
https://api.seniverse.com/v3/weather/daily.json -
你访问这些链接返回403是正常的,因为它们需要携带你的密钥作为参数才能获取数据。代码中正是通过
?key=将你的密钥附加在URL后面。
-
2. 添加必要的库
在PlatformIO项目配置文件 platformio.ini 的 lib_deps 中,确保已经添加了 ArduinoJson 库,用于解析API返回的JSON数据。
cs
ini
lib_deps =
bblanchon/ArduinoJson@^6.21.4
# ... 其他依赖
3. 编写代码:data.c 深度解析
这份代码是你提供的 data.c,我将逐段解释它的作用。
3.1 包含头文件和定义全局变量
cs
c
#include "data.h"
#include "lvgl.h"
#include "ui/src/ui.h"
#include <HTTPClient.h>
#include <ArduinoJson.h>
struct tm timeinfo; // 标准时间结构体,用于存储获取到的时间
// --- 心知天气API配置 ---
String url = "https://api.seniverse.com/v3/weather/now.json";
String key = "Sq9D2GX9Rie6Gljzv"; // 请务必替换成你自己的API密钥!
String locations = "深圳"; // 查询的城市,可以修改
String language = "zh-Hans";
String unit = "c";
String url_2 = "https://api.seniverse.com/v3/weather/daily.json";
String start = "0";
String days = "3"; // 获取未来几天的天气?原文档是7,这里改为了3
#define get_days 3 // 保持与days一致
// --- 用于存储解析结果的全局变量 ---
String city, temp, text, humi, last_update;
String data, date, text_day, low, high, precip;
String week_weather[7]={"周日","周一","周二","周三","周四","周五","周六"};
- 要点 :
key变量必须替换为你自己的心知天气私钥。locations和days可以根据需要修改。
3.2 获取时间函数 time_get()
cs
c
void time_get(void)
{
// configTime(时区偏移秒, 夏令时秒, NTP服务器1, 服务器2, 服务器3);
// 28800秒 = 8小时,即东八区(北京时间)
configTime(28800, 0, "ntp1.aliyun.com", "ntp2.aliyun.com", "ntp3.aliyun.com");
}
- 作用:这个函数配置并启动NTP时间同步。它告诉ESP32去连接阿里云的NTP服务器,获取网络时间,并自动处理时区转换。调用一次后,系统会定期后台同步。
3.3 获取天气函数 weather_get() (核心逻辑)
这个函数负责发起HTTP请求,并解析返回的JSON数据,最后更新UI。
cpp
c
void weather_get(void)
{
// 1. 创建HTTP客户端对象
HTTPClient http;
HTTPClient http_2;
// 2. 构造完整的请求URL并发起GET请求
http.begin(url+"?key="+key+"&location="+locations+"&language="+language+"&unit="+unit);
http_2.begin(url_2+"?key="+key+"&location="+locations+"&language="+language+"&unit="+unit+"&start="+start+"&days="+days);
int httpCode = http.GET(); // 执行请求,获取返回码
int httpCoed_2 = http_2.GET();
// 3. 读取服务器返回的JSON字符串
String response = http.getString();
String response_2 = http_2.getString();
http.end(); // 关闭连接
http_2.end();
// 4. 使用ArduinoJson解析JSON数据
// 创建DynamicJsonDocument对象,大小根据返回数据量预估
DynamicJsonDocument doc(1024);
DynamicJsonDocument doc_2(512 * get_days); // 根据天数动态分配内存
deserializeJson(doc, response); // 解析实时天气
deserializeJson(doc_2, response_2); // 解析天气预报
// 5. 从解析后的文档中提取数据,并赋值给全局变量
city = doc["results"][0]["location"]["name"].as<String>();
temp = doc["results"][0]["now"]["temperature"].as<String>();
text = doc["results"][0]["now"]["text"].as<String>();
humi = doc["results"][0]["now"]["humidity"].as<String>();
last_update = doc["results"][0]["last_update"].as<String>();
// 注意:这里取的是"明天"的天气 (daily[1])
text_day = doc_2["results"][0]["daily"][1]["text_day"].as<String>();
low = doc_2["results"][0]["daily"][1]["low"].as<String>();
high = doc_2["results"][0]["daily"][1]["high"].as<String>();
precip = doc_2["results"][0]["daily"][1]["precip"].as<String>();
// 6. 格式化未来几天的天气数据,用于滚动列表
data = ""; // 初始化空字符串
for(int i = 0; i < get_days; ++i)
{
if(i == 0) date = "今天";
// 根据当前星期几,计算未来i天是星期几
else date = week_weather[(timeinfo.tm_wday+i<=6) ? (timeinfo.tm_wday+i) : (timeinfo.tm_wday+i-7)];
data += date +
" " + doc_2["results"][0]["daily"][i]["text_day"].as<String>() +
" 温度:" + doc_2["results"][0]["daily"][i]["low"].as<String>() +
"~" + doc_2["results"][0]["daily"][i]["high"].as<String>() +
" 降水:" + doc_2["results"][0]["daily"][i]["precip"].as<String>() +
"\n";
}
// 7. 更新LVGL界面上各个部件的文本
lv_label_set_text_fmt( ui_LabelLocation, "地点:%s", city.c_str());
lv_label_set_text_fmt( ui_LabelTemp, "温度:%s度", temp.c_str()); // 原文档有湿度,这里简化了
lv_label_set_text_fmt( ui_LabelCode, "天气:%s", text.c_str());
lv_label_set_text_fmt( ui_LabelUpdateTime, "上次更新时间:%s", last_update.c_str());
// 更新"明天"天气预报的专用标签
lv_label_set_text_fmt( ui_LabelTomorrowCode, "明天%s,天气%s,温度%s~%s度,降水概率%s",
week_weather[(timeinfo.tm_wday+1<=6) ? (timeinfo.tm_wday+1) : (timeinfo.tm_wday+1-7)],
text_day.c_str(), low.c_str(), high.c_str(), precip.c_str());
// 更新未来天气的滚动列表
lv_roller_set_options( ui_RollerFuture, data.c_str(), LV_ROLLER_MODE_NORMAL );
}
-
要点:
-
JSON解析:
deserializeJson()是ArduinoJson库的核心函数。你需要根据API的实际返回结构,用doc["key"][index]["subkey"].as<类型>()的方式一层层取出数据。 -
UI更新:所有
lv_label_set_text_fmt函数都是在操作你在SquareLine Studio里设计的、名称以ui_开头的UI部件。
-
3.4 数据更新任务函数 data_update()
这个函数会被FreeRTOS任务周期性调用,负责更新时间和天气。
cs
c
void data_update()
{
if(Data.time_flag) // Data.time_flag 应该是一个全局标志,当需要更新时间时置1
{
if(getLocalTime(&timeinfo)) // 尝试获取本地时间
{
// 更新时间显示
const char* week[7]={"星期日","星期一","星期二","星期三","星期四","星期五","星期六"};
lv_label_set_text_fmt(ui_LabelTime1,"%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
lv_label_set_text_fmt(ui_LabelTime2,"%d/%02d/%02d %s", timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday, week[timeinfo.tm_wday]);
if(Data.weather_flag) // 如果需要更新天气
{
weather_get(); // 调用天气获取函数(这个函数可能耗时)
// 设置日历部件的高亮日期
lv_calendar_set_today_date(ui_Calendar, timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday);
lv_calendar_set_showed_date(ui_Calendar, timeinfo.tm_year+1900, timeinfo.tm_mon+1);
Data.weather_flag = 0; // 清除天气更新标志
}
}
}
}
- 要点 :这个函数本身不能 包含阻塞性的长延时,尤其是不能在里面直接调用
weather_get()而不采取措施,因为HTTP请求很慢。在实际项目中,通常会用另一个专门的任务去执行weather_get(),或者在这里使用非阻塞的方式。你文档最后提到的创建data_task就是为了解决这个问题。
3.5 创建独立的FreeRTOS任务
教学文档的最后,给出了创建任务的示例:
cs
c
xTaskCreatePinnedToCore(data_task, "data_task", 1024*5, NULL, 1, NULL, 0);
你需要实现 data_task 函数,让它在循环中调用 data_update() 并添加合适的延时:
cs
c
void data_task(void *pvParameters) {
while(1) {
data_update(); // 检查标志并更新UI
vTaskDelay(1000 / portTICK_PERIOD_MS); // 每秒执行一次
}
}
这样,时间可以每秒更新一次,而耗时的 weather_get() 只在 Data.weather_flag 被置1时才触发。


4. 总结与保姆级操作步骤
-
注册并获取密钥:去心知天气网站,注册账号,找到你的API密钥,复制它。
-
修改代码 :在你项目的
data.c(或类似文件)中,将String key = "..."替换为你自己的密钥。 -
添加依赖 :确认
platformio.ini中包含了bblanchon/ArduinoJson。 -
检查UI变量名 :确保代码中使用的
ui_LabelLocation,ui_LabelTemp等变量名,与你SquareLine Studio项目中实际定义的部件名称完全一致(包括大小写)。 -
集成到主程序:
-
在
setup()函数中调用time_get()启动NTP同步。 -
创建并启动
data_taskFreeRTOS任务。 -
当WiFi连接成功后(或根据需要),将
Data.time_flag和Data.weather_flag置1,触发时间和天气的首次更新。
-
-
编译并烧录,查看效果。
5. 常见问题与调试
-
时间显示为1970年或错误 :通常是NTP服务器连接失败或时区设置不对。检查WiFi是否正常,
configTime的第一个参数(时区偏移)是否正确。 -
天气数据不更新:
-
首先确认WiFi已连接。
-
检查API密钥是否有效,账户是否有剩余查询次数。
-
在
weather_get()中添加串口打印,输出response和response_2字符串,看API是否返回了错误信息(如"status_code"非0)。 -
检查JSON解析路径是否和API返回的结构完全匹配(可以用在线JSON解析器验证)。
-
-
LVGL界面卡死 :如果直接在LVGL刷新任务或高优先级任务中调用
weather_get(),会因网络阻塞导致界面卡顿。务必将其放在一个低优先级的独立任务中执行。