初始提交卡包 USB 工程
纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
#include "UI_DISPATCH/almanac_service.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cjson.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static almanac_provider_t s_provider = {0};
|
||||
static const char *TAG = "almanac_service";
|
||||
|
||||
/* main/CMakeLists.txt: EMBED_TXTFILES "UI_DISPATCH/yiji_data_2026_2030.json" */
|
||||
extern const char yiji_json_start[] asm("_binary_yiji_data_2026_2030_json_start");
|
||||
extern const char yiji_json_end[] asm("_binary_yiji_data_2026_2030_json_end");
|
||||
|
||||
static cJSON *s_yiji_root = NULL;
|
||||
static bool s_yiji_load_attempted = false;
|
||||
|
||||
/* 1900~2099 农历数据,bit 编码规则沿用常见农历算法实现 */
|
||||
static const uint32_t LUNAR_INFO[] = {
|
||||
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
|
||||
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
|
||||
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
|
||||
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
|
||||
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
|
||||
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5d0, 0x14573, 0x052d0, 0x0a9a8, 0x0e950, 0x06aa0,
|
||||
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
|
||||
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
|
||||
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
|
||||
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0,
|
||||
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
|
||||
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
|
||||
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
|
||||
0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
|
||||
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
|
||||
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06aa0, 0x1a6c4, 0x0aae0,
|
||||
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
|
||||
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
|
||||
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
|
||||
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
bool is_leap_month;
|
||||
} lunar_date_t;
|
||||
|
||||
static int64_t days_from_civil(int y, unsigned m, unsigned d)
|
||||
{
|
||||
y -= (m <= 2);
|
||||
{
|
||||
const int era = (y >= 0 ? y : y - 399) / 400;
|
||||
const unsigned yoe = (unsigned)(y - era * 400);
|
||||
const unsigned doy = (153 * (m + (m > 2 ? (unsigned)-3 : 9)) + 2) / 5 + d - 1;
|
||||
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
return era * 146097 + (int64_t)doe - 719468;
|
||||
}
|
||||
}
|
||||
|
||||
static int lunar_leap_month(int year)
|
||||
{
|
||||
return (int)(LUNAR_INFO[year - 1900] & 0x0f);
|
||||
}
|
||||
|
||||
static int lunar_leap_days(int year)
|
||||
{
|
||||
const int lm = lunar_leap_month(year);
|
||||
if (lm == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (LUNAR_INFO[year - 1900] & 0x10000) ? 30 : 29;
|
||||
}
|
||||
|
||||
static int lunar_month_days(int year, int month)
|
||||
{
|
||||
return (LUNAR_INFO[year - 1900] & (0x10000 >> month)) ? 30 : 29;
|
||||
}
|
||||
|
||||
static int lunar_year_days(int year)
|
||||
{
|
||||
int sum = 348;
|
||||
uint32_t mask = 0;
|
||||
for (mask = 0x8000; mask > 0x8; mask >>= 1) {
|
||||
if (LUNAR_INFO[year - 1900] & mask) {
|
||||
sum += 1;
|
||||
}
|
||||
}
|
||||
return sum + lunar_leap_days(year);
|
||||
}
|
||||
|
||||
static bool solar_to_lunar(int year, int month, int day, lunar_date_t *out)
|
||||
{
|
||||
int64_t offset = 0;
|
||||
int lunar_year = 0;
|
||||
int year_days = 0;
|
||||
int leap = 0;
|
||||
bool in_leap = false;
|
||||
int lunar_month = 0;
|
||||
int month_days = 0;
|
||||
|
||||
if (out == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (year < 1900 || year > 2099 || month < 1 || month > 12 || day < 1 || day > 31) {
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = days_from_civil(year, (unsigned)month, (unsigned)day) - days_from_civil(1900, 1, 31);
|
||||
if (offset < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lunar_year = 1900;
|
||||
year_days = lunar_year_days(lunar_year);
|
||||
while (lunar_year < 2100 && offset >= year_days) {
|
||||
offset -= year_days;
|
||||
lunar_year++;
|
||||
if (lunar_year <= 2099) {
|
||||
year_days = lunar_year_days(lunar_year);
|
||||
}
|
||||
}
|
||||
if (lunar_year > 2099) {
|
||||
return false;
|
||||
}
|
||||
|
||||
leap = lunar_leap_month(lunar_year);
|
||||
lunar_month = 1;
|
||||
while (lunar_month <= 12) {
|
||||
if (leap > 0 && lunar_month == (leap + 1) && !in_leap) {
|
||||
lunar_month -= 1;
|
||||
in_leap = true;
|
||||
month_days = lunar_leap_days(lunar_year);
|
||||
} else {
|
||||
month_days = lunar_month_days(lunar_year, lunar_month);
|
||||
}
|
||||
if (offset < month_days) {
|
||||
break;
|
||||
}
|
||||
offset -= month_days;
|
||||
if (in_leap && lunar_month == leap) {
|
||||
in_leap = false;
|
||||
}
|
||||
lunar_month++;
|
||||
}
|
||||
|
||||
out->year = lunar_year;
|
||||
out->month = lunar_month;
|
||||
out->day = (int)offset + 1;
|
||||
out->is_leap_month = in_leap;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *lunar_month_to_text(int month)
|
||||
{
|
||||
static const char *month_text[] = {
|
||||
"", "正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊",
|
||||
};
|
||||
if (month < 1 || month > 12) {
|
||||
return "";
|
||||
}
|
||||
return month_text[month];
|
||||
}
|
||||
|
||||
static const char *lunar_day_to_text(int day)
|
||||
{
|
||||
static const char *day_text[] = {
|
||||
"", "初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
|
||||
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
|
||||
"廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十",
|
||||
};
|
||||
if (day < 1 || day > 30) {
|
||||
return "";
|
||||
}
|
||||
return day_text[day];
|
||||
}
|
||||
|
||||
static bool almanac_builtin_get(uint16_t year, uint8_t month, uint8_t day, almanac_entry_t *out)
|
||||
{
|
||||
lunar_date_t lunar = {0};
|
||||
cJSON *item = NULL;
|
||||
cJSON *node_year = NULL;
|
||||
cJSON *node_month = NULL;
|
||||
cJSON *node_day = NULL;
|
||||
cJSON *yi = NULL;
|
||||
cJSON *ji = NULL;
|
||||
int n = 0;
|
||||
int i = 0;
|
||||
size_t used = 0;
|
||||
const char *word = NULL;
|
||||
|
||||
if (out == NULL) {
|
||||
return false;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
if (solar_to_lunar((int)year, (int)month, (int)day, &lunar)) {
|
||||
snprintf(out->lunar, sizeof(out->lunar), "%s%s月%s",
|
||||
lunar.is_leap_month ? "闰" : "",
|
||||
lunar_month_to_text(lunar.month),
|
||||
lunar_day_to_text(lunar.day));
|
||||
}
|
||||
|
||||
/* 内置 JSON 懒加载:只解析一次,失败后不重复尝试 */
|
||||
if (!s_yiji_load_attempted) {
|
||||
const char *json_start = yiji_json_start;
|
||||
const char *json_end = yiji_json_end;
|
||||
s_yiji_load_attempted = true;
|
||||
if (json_end > json_start) {
|
||||
const size_t json_len = (size_t)(json_end - json_start);
|
||||
char *json_buf = (char *)malloc(json_len + 1);
|
||||
if (json_buf != NULL) {
|
||||
memcpy(json_buf, json_start, json_len);
|
||||
json_buf[json_len] = '\0';
|
||||
s_yiji_root = cJSON_Parse(json_buf);
|
||||
free(json_buf);
|
||||
if (s_yiji_root == NULL) {
|
||||
ESP_LOGW(TAG, "parse yiji_data json failed");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "malloc yiji_data buffer failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s_yiji_root != NULL && cJSON_IsArray(s_yiji_root)) {
|
||||
n = cJSON_GetArraySize(s_yiji_root);
|
||||
for (i = 0; i < n; i++) {
|
||||
item = cJSON_GetArrayItem(s_yiji_root, i);
|
||||
if (!cJSON_IsObject(item)) {
|
||||
continue;
|
||||
}
|
||||
node_year = cJSON_GetObjectItem(item, "lunar_year");
|
||||
node_month = cJSON_GetObjectItem(item, "lunar_month");
|
||||
node_day = cJSON_GetObjectItem(item, "lunar_day");
|
||||
if (!cJSON_IsNumber(node_year) || !cJSON_IsNumber(node_month) || !cJSON_IsNumber(node_day)) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* 约定:JSON 中 lunar_year/lunar_month/lunar_day 作为“查表日期键”使用,
|
||||
* 这里按传入公历日期(year/month/day)直接匹配。
|
||||
* 这样修改 JSON 的 2026-09-17 会直接影响界面显示。
|
||||
*/
|
||||
if (node_year->valueint != (int)year ||
|
||||
node_month->valueint != (int)month ||
|
||||
node_day->valueint != (int)day) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yi = cJSON_GetObjectItem(item, "yi");
|
||||
ji = cJSON_GetObjectItem(item, "ji");
|
||||
|
||||
used = 0;
|
||||
if (cJSON_IsArray(yi)) {
|
||||
const int yi_n = cJSON_GetArraySize(yi) < 4 ? cJSON_GetArraySize(yi) : 4;
|
||||
for (int k = 0; k < yi_n; k++) {
|
||||
cJSON *w = cJSON_GetArrayItem(yi, k);
|
||||
if (!cJSON_IsString(w) || w->valuestring == NULL) {
|
||||
continue;
|
||||
}
|
||||
word = w->valuestring;
|
||||
if (used > 0 && used < sizeof(out->dos) - 1) {
|
||||
out->dos[used++] = ' ';
|
||||
out->dos[used] = '\0';
|
||||
}
|
||||
if (used < sizeof(out->dos) - 1) {
|
||||
used += (size_t)snprintf(out->dos + used, sizeof(out->dos) - used, "%s", word);
|
||||
if (used >= sizeof(out->dos)) {
|
||||
used = sizeof(out->dos) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
used = 0;
|
||||
if (cJSON_IsArray(ji)) {
|
||||
const int ji_n = cJSON_GetArraySize(ji) < 4 ? cJSON_GetArraySize(ji) : 4;
|
||||
for (int k = 0; k < ji_n; k++) {
|
||||
cJSON *w = cJSON_GetArrayItem(ji, k);
|
||||
if (!cJSON_IsString(w) || w->valuestring == NULL) {
|
||||
continue;
|
||||
}
|
||||
word = w->valuestring;
|
||||
if (used > 0 && used < sizeof(out->donts) - 1) {
|
||||
out->donts[used++] = ' ';
|
||||
out->donts[used] = '\0';
|
||||
}
|
||||
if (used < sizeof(out->donts) - 1) {
|
||||
used += (size_t)snprintf(out->donts + used, sizeof(out->donts) - used, "%s", word);
|
||||
if (used >= sizeof(out->donts)) {
|
||||
used = sizeof(out->donts) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (out->dos[0] == '\0') {
|
||||
snprintf(out->dos, sizeof(out->dos), "-- -- -- --");
|
||||
}
|
||||
if (out->donts[0] == '\0') {
|
||||
snprintf(out->donts, sizeof(out->donts), "-- -- -- --");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void almanac_service_set_provider(const almanac_provider_t *provider)
|
||||
{
|
||||
if (provider == NULL || provider->get == NULL) {
|
||||
s_provider.get = NULL;
|
||||
s_provider.ctx = NULL;
|
||||
return;
|
||||
}
|
||||
s_provider = *provider;
|
||||
}
|
||||
|
||||
bool almanac_service_get(uint16_t year, uint8_t month, uint8_t day, almanac_entry_t *out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s_provider.get != NULL && s_provider.get(year, month, day, out, s_provider.ctx)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return almanac_builtin_get(year, month, day, out);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef ALMANAC_SERVICE_H
|
||||
#define ALMANAC_SERVICE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
char lunar[24];
|
||||
char dos[64];
|
||||
char donts[64];
|
||||
} almanac_entry_t;
|
||||
|
||||
typedef bool (*almanac_get_fn_t)(uint16_t year, uint8_t month, uint8_t day, almanac_entry_t *out, void *ctx);
|
||||
|
||||
typedef struct {
|
||||
almanac_get_fn_t get;
|
||||
void *ctx;
|
||||
} almanac_provider_t;
|
||||
|
||||
/* 设置外部数据提供者(例如 JSON 查表);传 NULL 则回退内置实现 */
|
||||
void almanac_service_set_provider(const almanac_provider_t *provider);
|
||||
|
||||
/* 按公历日期获取农历+宜忌;无外部 provider 时使用内置回退逻辑 */
|
||||
bool almanac_service_get(uint16_t year, uint8_t month, uint8_t day, almanac_entry_t *out);
|
||||
|
||||
#endif /* ALMANAC_SERVICE_H */
|
||||
@@ -0,0 +1,738 @@
|
||||
#include "UI_DISPATCH/ui_dispatch.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "LVGL/lvgl_init.h"
|
||||
#include "gui_guider.h"
|
||||
#include "BLE/ble_send.h"
|
||||
#include "SCREEN/screen_brightness.h"
|
||||
#include "UI_DISPATCH/almanac_service.h"
|
||||
|
||||
#define UI_DISPATCH_QUEUE_LEN 16
|
||||
#define UI_DISPATCH_TASK_STACK_SIZE 4096
|
||||
#define UI_DISPATCH_TASK_PRIORITY 6
|
||||
|
||||
/* Weather keyword constants - UNUSED: BLE uses direct path via ble_data_ansy.c */
|
||||
#if 0
|
||||
static const char WEATHER_HAIL[] = {0xe5, 0x86, 0xb0, 0xe9, 0x9b, 0xb9, 0x00}; /* 冰雹 */
|
||||
static const char WEATHER_SLEET[] = {0xe9, 0x9b, 0xa8, 0xe5, 0xa4, 0xb9, 0xe9, 0x9b, 0xaa, 0x00}; /* 雨夹雪 */
|
||||
static const char WEATHER_FREEZING_RAIN[] = {0xe5, 0x86, 0xbb, 0xe9, 0x9b, 0xa8, 0x00}; /* 冻雨 */
|
||||
static const char WEATHER_SNOW[] = {0xe9, 0x9b, 0xaa, 0x00}; /* 雪 */
|
||||
static const char WEATHER_GRAUPEL[] = {0xe9, 0x9c, 0xb0, 0x00}; /* 霰 */
|
||||
static const char WEATHER_THUNDER[] = {0xe9, 0x9b, 0xb7, 0x00}; /* 雷 */
|
||||
static const char WEATHER_RAIN[] = {0xe9, 0x9b, 0xa8, 0x00}; /* 雨 */
|
||||
static const char WEATHER_FOG[] = {0xe9, 0x9b, 0xbe, 0x00}; /* 雾 */
|
||||
static const char WEATHER_HAZE[] = {0xe9, 0x9c, 0xbe, 0x00}; /* 霾 */
|
||||
static const char WEATHER_DUST[] = {0xe5, 0xb0, 0x98, 0x00}; /* 尘 */
|
||||
static const char WEATHER_SAND[] = {0xe6, 0xb2, 0x99, 0x00}; /* 沙 */
|
||||
static const char WEATHER_FLOATING_DUST[] = {0xe6, 0xb5, 0xae, 0xe5, 0xb0, 0x98, 0x00}; /* 浮尘 */
|
||||
static const char WEATHER_CLOUD[] = {0xe4, 0xba, 0x91, 0x00}; /* 云 */
|
||||
static const char WEATHER_OVERCAST[] = {0xe9, 0x98, 0xb4, 0x00}; /* 阴 */
|
||||
|
||||
/* Weather text display constants */
|
||||
static const char WEATHER_TEXT_RAIN[] = {0xe9, 0x9b, 0xa8, 0x00}; /* 雨 */
|
||||
static const char WEATHER_TEXT_THUNDER[] = {0xe9, 0x9b, 0xb7, 0xe9, 0x98, 0xb5, 0xe9, 0x9b, 0xa8, 0x00}; /* 雷阵雨 */
|
||||
static const char WEATHER_TEXT_SNOW[] = {0xe9, 0x9b, 0xaa, 0x00}; /* 雪 */
|
||||
static const char WEATHER_TEXT_SLEET[] = {0xe9, 0x9b, 0xa8, 0xe5, 0xa4, 0xb9, 0xe9, 0x9b, 0xaa, 0x00}; /* 雨夹雪 */
|
||||
static const char WEATHER_TEXT_HAIL[] = {0xe5, 0x86, 0xb0, 0xe9, 0x9b, 0xb9, 0x00}; /* 冰雹 */
|
||||
static const char WEATHER_TEXT_FOG[] = {0xe9, 0x9b, 0xbe, 0x00}; /* 雾 */
|
||||
static const char WEATHER_TEXT_CLOUDY[] = {0xe5, 0xa4, 0x9a, 0xe4, 0xba, 0x91, 0x00}; /* 多云 */
|
||||
static const char WEATHER_TEXT_SUNNY[] = {0xe6, 0x99, 0xb4, 0x00}; /* 晴 */
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
UI_DISPATCH_CMD_SET_BRIGHTNESS = 0,
|
||||
UI_DISPATCH_CMD_SET_DATE,
|
||||
UI_DISPATCH_CMD_SET_TIME,
|
||||
// UI_DISPATCH_CMD_SET_WEATHER, // UNUSED: BLE uses direct path
|
||||
} ui_dispatch_cmd_t;
|
||||
|
||||
typedef struct {
|
||||
ui_dispatch_cmd_t type;
|
||||
union {
|
||||
struct {
|
||||
uint8_t brightness_percent;
|
||||
} brightness;
|
||||
struct {
|
||||
uint16_t year;
|
||||
uint8_t month;
|
||||
uint8_t day;
|
||||
uint8_t week;
|
||||
} date;
|
||||
struct {
|
||||
uint8_t hour;
|
||||
uint8_t minute;
|
||||
uint8_t second;
|
||||
} time;
|
||||
#if 0 // UNUSED: BLE uses direct path
|
||||
struct {
|
||||
uint8_t timing;
|
||||
uint16_t temp_enc;
|
||||
char weather[32];
|
||||
} weather;
|
||||
#endif
|
||||
} data;
|
||||
} ui_dispatch_msg_t;
|
||||
|
||||
static const char *TAG = "ui_dispatch";
|
||||
static QueueHandle_t s_ui_dispatch_queue = NULL;
|
||||
static TaskHandle_t s_ui_dispatch_task = NULL;
|
||||
|
||||
extern lv_ui guider_ui;
|
||||
extern int screen_time_digital_clock_now_hour_value;
|
||||
extern int screen_time_digital_clock_now_min_value;
|
||||
extern int screen_time_digital_clock_now_sec_value;
|
||||
|
||||
static const char *WEEK_TEXT[] = {
|
||||
"星期一",
|
||||
"星期二",
|
||||
"星期三",
|
||||
"星期四",
|
||||
"星期五",
|
||||
"星期六",
|
||||
"星期日",
|
||||
};
|
||||
|
||||
/** Same order as time page `WEEK_TEXT` (Mon=0 ... Sun=6), short format for perpetual calendar */
|
||||
static const char *WEEK_SHORT_TEXT[] = {
|
||||
"周一", "周二", "周三", "周四", "周五", "周六", "周日",
|
||||
};
|
||||
|
||||
static int s_perpetual_last_week_idx = -1;
|
||||
static uint16_t s_perpetual_last_year = 0;
|
||||
static uint8_t s_perpetual_last_month = 0;
|
||||
static uint8_t s_perpetual_last_day = 0;
|
||||
|
||||
/** 与界面一致的「当前日历」,供午夜翻日;由 SET_DATE / BLE 日期同步更新 */
|
||||
static portMUX_TYPE s_clock_cal_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
static bool s_clock_cal_valid = false;
|
||||
static uint16_t s_clock_cal_y = 0;
|
||||
static uint8_t s_clock_cal_m = 0;
|
||||
static uint8_t s_clock_cal_d = 0;
|
||||
static uint8_t s_clock_cal_w = 0;
|
||||
|
||||
/** 方案 A:校时锚点 + esp_timer 推算,减轻 lv_timer 累计误差 */
|
||||
static bool s_mono_anchor_valid = false;
|
||||
static uint32_t s_mono_anchor_sec_in_day = 0;
|
||||
static int64_t s_mono_anchor_us = 0;
|
||||
static uint64_t s_mono_last_day_index = 0;
|
||||
|
||||
static void ui_dispatch_perpetual_calendar_set_labels(int week_idx, uint16_t year, uint8_t month, uint8_t day)
|
||||
{
|
||||
almanac_entry_t almanac = {0};
|
||||
if (week_idx < 0 || week_idx > 6 || year == 0 || month == 0 || month > 12 || day == 0 || day > 31) {
|
||||
return;
|
||||
}
|
||||
|
||||
char day_buf[8];
|
||||
snprintf(day_buf, sizeof(day_buf), "%02u", (unsigned int)day);
|
||||
|
||||
if (guider_ui.screen_Perpetual_Calendar_label_week != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_Perpetual_Calendar_label_week)) {
|
||||
lv_label_set_text(guider_ui.screen_Perpetual_Calendar_label_week, WEEK_SHORT_TEXT[week_idx]);
|
||||
}
|
||||
if (guider_ui.screen_Perpetual_Calendar_label_day != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_Perpetual_Calendar_label_day)) {
|
||||
lv_label_set_text(guider_ui.screen_Perpetual_Calendar_label_day, day_buf);
|
||||
}
|
||||
if (almanac_service_get(year, month, day, &almanac) &&
|
||||
almanac.lunar[0] != '\0' &&
|
||||
guider_ui.screen_Perpetual_Calendar_label_Lunar_Calendar != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_Perpetual_Calendar_label_Lunar_Calendar)) {
|
||||
lv_label_set_text(guider_ui.screen_Perpetual_Calendar_label_Lunar_Calendar, almanac.lunar);
|
||||
}
|
||||
if (almanac.dos[0] != '\0' &&
|
||||
guider_ui.screen_Perpetual_Calendar_label_Dos != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_Perpetual_Calendar_label_Dos)) {
|
||||
lv_label_set_text(guider_ui.screen_Perpetual_Calendar_label_Dos, almanac.dos);
|
||||
}
|
||||
if (almanac.donts[0] != '\0' &&
|
||||
guider_ui.screen_Perpetual_Calendar_label_Donts != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_Perpetual_Calendar_label_Donts)) {
|
||||
lv_label_set_text(guider_ui.screen_Perpetual_Calendar_label_Donts, almanac.donts);
|
||||
}
|
||||
}
|
||||
|
||||
/* 调用方须已持有 lvgl_api_lock(与 ui_dispatch_apply_date 一致) */
|
||||
static void ui_dispatch_put_date_on_ui_locked(uint16_t year, uint8_t month, uint8_t day, uint8_t week)
|
||||
{
|
||||
char date[32] = {0};
|
||||
|
||||
snprintf(date, sizeof(date), "%04u年%02u月%02u日",
|
||||
(unsigned int)year,
|
||||
(unsigned int)month,
|
||||
(unsigned int)day);
|
||||
|
||||
if (guider_ui.screen_time_label_date != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_time_label_date)) {
|
||||
lv_label_set_text(guider_ui.screen_time_label_date, date);
|
||||
}
|
||||
|
||||
uint8_t w = week;
|
||||
int idx = -1;
|
||||
if (w >= 1 && w <= 7) {
|
||||
idx = (int)w - 1;
|
||||
} else if (w <= 6) {
|
||||
idx = ((int)w + 6) % 7;
|
||||
}
|
||||
if (idx >= 0 && idx < 7) {
|
||||
s_perpetual_last_week_idx = idx;
|
||||
s_perpetual_last_year = year;
|
||||
s_perpetual_last_month = month;
|
||||
s_perpetual_last_day = day;
|
||||
ui_dispatch_perpetual_calendar_set_labels(idx, year, month, day);
|
||||
if (guider_ui.screen_time_label_week != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_time_label_week)) {
|
||||
lv_label_set_text(guider_ui.screen_time_label_week, WEEK_TEXT[idx]);
|
||||
}
|
||||
portENTER_CRITICAL(&s_clock_cal_mux);
|
||||
s_clock_cal_valid = true;
|
||||
s_clock_cal_y = year;
|
||||
s_clock_cal_m = month;
|
||||
s_clock_cal_d = day;
|
||||
s_clock_cal_w = w;
|
||||
portEXIT_CRITICAL(&s_clock_cal_mux);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "date week out of range: %u", (unsigned int)w);
|
||||
portENTER_CRITICAL(&s_clock_cal_mux);
|
||||
s_clock_cal_valid = false;
|
||||
portEXIT_CRITICAL(&s_clock_cal_mux);
|
||||
}
|
||||
}
|
||||
|
||||
#if 0 // UNUSED: BLE uses direct path via ble_data_ansy.c
|
||||
typedef enum {
|
||||
UI_WEATHER_CLASS_SUNNY = 0, // 晴
|
||||
UI_WEATHER_CLASS_CLOUDY, // 多云/阴
|
||||
UI_WEATHER_CLASS_FOG, // 雾/霾/沙尘
|
||||
UI_WEATHER_CLASS_RAIN, // 雨
|
||||
UI_WEATHER_CLASS_THUNDER, // 雷阵雨
|
||||
UI_WEATHER_CLASS_SNOW, // 雪
|
||||
UI_WEATHER_CLASS_SLEET, // 雨夹雪/冻雨
|
||||
UI_WEATHER_CLASS_HAIL, // 冰雹
|
||||
} ui_weather_class_t;
|
||||
|
||||
static ui_weather_class_t ui_dispatch_classify_weather_8class(const char *weather)
|
||||
{
|
||||
if (weather == NULL || weather[0] == '\0') {
|
||||
return UI_WEATHER_CLASS_SUNNY;
|
||||
}
|
||||
|
||||
/* Priority 1: Hail */
|
||||
if (strstr(weather, WEATHER_HAIL) != NULL) {
|
||||
return UI_WEATHER_CLASS_HAIL;
|
||||
}
|
||||
|
||||
/* Priority 2: Sleet/Freezing rain */
|
||||
if (strstr(weather, WEATHER_SLEET) != NULL || strstr(weather, WEATHER_FREEZING_RAIN) != NULL) {
|
||||
return UI_WEATHER_CLASS_SLEET;
|
||||
}
|
||||
|
||||
/* Priority 3: Snow */
|
||||
if (strstr(weather, WEATHER_SNOW) != NULL || strstr(weather, WEATHER_GRAUPEL) != NULL) {
|
||||
return UI_WEATHER_CLASS_SNOW;
|
||||
}
|
||||
|
||||
/* Priority 4: Thunderstorm */
|
||||
if (strstr(weather, WEATHER_THUNDER) != NULL) {
|
||||
return UI_WEATHER_CLASS_THUNDER;
|
||||
}
|
||||
|
||||
/* Priority 5: Rain */
|
||||
if (strstr(weather, WEATHER_RAIN) != NULL) {
|
||||
return UI_WEATHER_CLASS_RAIN;
|
||||
}
|
||||
|
||||
/* Priority 6: Fog/Haze/Dust */
|
||||
if (strstr(weather, WEATHER_FOG) != NULL || strstr(weather, WEATHER_HAZE) != NULL ||
|
||||
strstr(weather, WEATHER_DUST) != NULL || strstr(weather, WEATHER_SAND) != NULL ||
|
||||
strstr(weather, WEATHER_FLOATING_DUST) != NULL) {
|
||||
return UI_WEATHER_CLASS_FOG;
|
||||
}
|
||||
|
||||
/* Priority 7: Cloudy */
|
||||
if (strstr(weather, WEATHER_CLOUD) != NULL || strstr(weather, WEATHER_OVERCAST) != NULL) {
|
||||
return UI_WEATHER_CLASS_CLOUDY;
|
||||
}
|
||||
|
||||
/* Default: Sunny */
|
||||
return UI_WEATHER_CLASS_SUNNY;
|
||||
}
|
||||
|
||||
static const char *ui_dispatch_weather_text_from_class(ui_weather_class_t weather_class)
|
||||
{
|
||||
switch (weather_class) {
|
||||
case UI_WEATHER_CLASS_RAIN:
|
||||
return WEATHER_TEXT_RAIN;
|
||||
case UI_WEATHER_CLASS_THUNDER:
|
||||
return WEATHER_TEXT_THUNDER;
|
||||
case UI_WEATHER_CLASS_SNOW:
|
||||
return WEATHER_TEXT_SNOW;
|
||||
case UI_WEATHER_CLASS_SLEET:
|
||||
return WEATHER_TEXT_SLEET;
|
||||
case UI_WEATHER_CLASS_HAIL:
|
||||
return WEATHER_TEXT_HAIL;
|
||||
case UI_WEATHER_CLASS_FOG:
|
||||
return WEATHER_TEXT_FOG;
|
||||
case UI_WEATHER_CLASS_CLOUDY:
|
||||
return WEATHER_TEXT_CLOUDY;
|
||||
case UI_WEATHER_CLASS_SUNNY:
|
||||
default:
|
||||
return WEATHER_TEXT_SUNNY;
|
||||
}
|
||||
}
|
||||
|
||||
static const lv_image_dsc_t *ui_dispatch_weather_icon_from_class(ui_weather_class_t weather_class)
|
||||
{
|
||||
switch (weather_class) {
|
||||
case UI_WEATHER_CLASS_RAIN:
|
||||
return &_rainy_RGB565_60x50;
|
||||
case UI_WEATHER_CLASS_THUNDER:
|
||||
return &_thunder_RGB565_60x50;
|
||||
case UI_WEATHER_CLASS_SNOW:
|
||||
return &_snow_RGB565_60x50;
|
||||
case UI_WEATHER_CLASS_SLEET:
|
||||
return &_sleet_RGB565_60x50;
|
||||
case UI_WEATHER_CLASS_HAIL:
|
||||
return &_hail_RGB565_60x50;
|
||||
case UI_WEATHER_CLASS_FOG:
|
||||
return &_fog_RGB565_60x50;
|
||||
case UI_WEATHER_CLASS_CLOUDY:
|
||||
return &_cloudy_RGB565_60x50;
|
||||
case UI_WEATHER_CLASS_SUNNY:
|
||||
default:
|
||||
return &_sun_RGB565_60x50;
|
||||
}
|
||||
}
|
||||
|
||||
static void ui_dispatch_set_weather_icon_if_valid(lv_obj_t *img_obj, ui_weather_class_t weather_class)
|
||||
{
|
||||
if (img_obj != NULL && lv_obj_is_valid(img_obj)) {
|
||||
lv_image_set_src(img_obj, ui_dispatch_weather_icon_from_class(weather_class));
|
||||
}
|
||||
}
|
||||
#endif // End of unused weather functions
|
||||
|
||||
/*
|
||||
* Synced with time page: `screen_time_digital_clock_now_*` global hour/min/sec.
|
||||
* `label_3hlate` = current hour +2, `label_5hlate` = current hour +4, both show HH:00 format.
|
||||
* Only update when hour value changes to avoid frequent label updates.
|
||||
*/
|
||||
void ui_dispatch_refresh_home_weather_slot_hour_labels(void)
|
||||
{
|
||||
int h = screen_time_digital_clock_now_hour_value;
|
||||
if (h < 0) {
|
||||
h = 0;
|
||||
} else if (h > 23) {
|
||||
h %= 24;
|
||||
}
|
||||
|
||||
static int s_last_applied_hour = -1;
|
||||
if (s_last_applied_hour >= 0 && h == s_last_applied_hour) {
|
||||
return;
|
||||
}
|
||||
s_last_applied_hour = h;
|
||||
|
||||
int h_plus2 = (h + 2) % 24;
|
||||
int h_plus4 = (h + 4) % 24;
|
||||
char buf[8];
|
||||
|
||||
snprintf(buf, sizeof(buf), "%02d:00", h_plus2);
|
||||
if (guider_ui.screen_home_weather_label_3hlate != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_home_weather_label_3hlate)) {
|
||||
lv_label_set_text(guider_ui.screen_home_weather_label_3hlate, buf);
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf), "%02d:00", h_plus4);
|
||||
if (guider_ui.screen_home_weather_label_5hlate != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_home_weather_label_5hlate)) {
|
||||
lv_label_set_text(guider_ui.screen_home_weather_label_5hlate, buf);
|
||||
}
|
||||
}
|
||||
|
||||
static bool ui_dispatch_send(const ui_dispatch_msg_t *msg)
|
||||
{
|
||||
if (msg == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (s_ui_dispatch_queue == NULL) {
|
||||
ESP_LOGW(TAG, "ui_dispatch 未初始化,丢弃 cmd=%d", (int)msg->type);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xQueueSend(s_ui_dispatch_queue, msg, pdMS_TO_TICKS(10)) != pdPASS) {
|
||||
ESP_LOGW(TAG, "ui dispatch queue full, drop cmd=%d", (int)msg->type);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ui_dispatch_apply_date(const ui_dispatch_msg_t *msg)
|
||||
{
|
||||
ui_dispatch_put_date_on_ui_locked(msg->data.date.year,
|
||||
msg->data.date.month,
|
||||
msg->data.date.day,
|
||||
msg->data.date.week);
|
||||
}
|
||||
|
||||
static void ui_dispatch_apply_time(const ui_dispatch_msg_t *msg)
|
||||
{
|
||||
screen_time_digital_clock_now_hour_value = (int)msg->data.time.hour;
|
||||
screen_time_digital_clock_now_min_value = (int)msg->data.time.minute;
|
||||
screen_time_digital_clock_now_sec_value = (int)msg->data.time.second;
|
||||
|
||||
{
|
||||
const uint32_t sid = (uint32_t)msg->data.time.hour * 3600u +
|
||||
(uint32_t)msg->data.time.minute * 60u + (uint32_t)msg->data.time.second;
|
||||
s_mono_anchor_sec_in_day = sid % 86400u;
|
||||
s_mono_anchor_us = esp_timer_get_time();
|
||||
s_mono_anchor_valid = true;
|
||||
s_mono_last_day_index = 0;
|
||||
}
|
||||
|
||||
if (guider_ui.screen_time_digital_clock_now != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_time_digital_clock_now)) {
|
||||
lv_label_set_text_fmt(guider_ui.screen_time_digital_clock_now, "%02u:%02u",
|
||||
(unsigned int)msg->data.time.hour,
|
||||
(unsigned int)msg->data.time.minute);
|
||||
}
|
||||
|
||||
ui_dispatch_refresh_home_weather_slot_hour_labels();
|
||||
}
|
||||
|
||||
#if 0 // UNUSED: BLE uses direct path via ble_data_ansy.c
|
||||
/*
|
||||
* BLE `w_t_info` handler: timing, temp_enc, weather string from vehicle.
|
||||
* Purpose: classify weather into 8 types and map to corresponding icons.
|
||||
* Update images by timing: timing=0 -> img_1+img_2; timing=2 -> img_3; timing=4 -> img_4.
|
||||
* Other timing values: no UI update (only 0/2/4 supported).
|
||||
*/
|
||||
static void ui_dispatch_apply_weather(const ui_dispatch_msg_t *msg)
|
||||
{
|
||||
const uint8_t ble_timing = msg->data.weather.timing;
|
||||
const char *ble_weather = msg->data.weather.weather;
|
||||
const ui_weather_class_t weather_class = ui_dispatch_classify_weather_8class(ble_weather);
|
||||
|
||||
if (ble_timing != 0x00 && ble_timing != 0x02 && ble_timing != 0x04)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ble_timing == 0x00)
|
||||
{
|
||||
/* Main slot: update weather text label and temperature */
|
||||
int tenths = (int)msg->data.weather.temp_enc - 1000;
|
||||
int temp_int = (tenths >= 0) ? (tenths + 5) / 10 : (tenths - 5) / 10;
|
||||
char temp_string[20] = {0};
|
||||
snprintf(temp_string, sizeof(temp_string), "%d", temp_int);
|
||||
|
||||
if (guider_ui.screen_home_weather_label_weather != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_home_weather_label_weather)) {
|
||||
lv_label_set_text(guider_ui.screen_home_weather_label_weather,
|
||||
ui_dispatch_weather_text_from_class(weather_class));
|
||||
}
|
||||
if (guider_ui.screen_home_weather_label_temp != NULL &&
|
||||
lv_obj_is_valid(guider_ui.screen_home_weather_label_temp)) {
|
||||
lv_label_set_text(guider_ui.screen_home_weather_label_temp, temp_string);
|
||||
}
|
||||
ui_dispatch_set_weather_icon_if_valid(guider_ui.screen_home_weather_img_1, weather_class);
|
||||
ui_dispatch_set_weather_icon_if_valid(guider_ui.screen_home_weather_img_2, weather_class);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ble_timing == 0x02) {
|
||||
ui_dispatch_set_weather_icon_if_valid(guider_ui.screen_home_weather_img_3, weather_class);
|
||||
return;
|
||||
}
|
||||
|
||||
/* ble_timing == 0x04 */
|
||||
ui_dispatch_set_weather_icon_if_valid(guider_ui.screen_home_weather_img_4, weather_class);
|
||||
}
|
||||
#endif // End of unused ui_dispatch_apply_weather
|
||||
|
||||
static void ui_dispatch_task(void *arg)
|
||||
{
|
||||
ui_dispatch_msg_t msg;
|
||||
|
||||
(void)arg;
|
||||
|
||||
while (1) {
|
||||
if (xQueueReceive(s_ui_dispatch_queue, &msg, portMAX_DELAY) != pdPASS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_lock_acquire(&lvgl_api_lock);
|
||||
switch (msg.type) {
|
||||
case UI_DISPATCH_CMD_SET_BRIGHTNESS: {
|
||||
int p = (int)msg.data.brightness.brightness_percent;
|
||||
if (p < 0) {
|
||||
p = 0;
|
||||
} else if (p > 100) {
|
||||
p = 100;
|
||||
}
|
||||
set_system_brightness(p);
|
||||
g_brightness = (uint8_t)p;
|
||||
break;
|
||||
}
|
||||
case UI_DISPATCH_CMD_SET_DATE:
|
||||
ui_dispatch_apply_date(&msg);
|
||||
break;
|
||||
case UI_DISPATCH_CMD_SET_TIME:
|
||||
ui_dispatch_apply_time(&msg);
|
||||
break;
|
||||
#if 0 // UNUSED: BLE uses direct path
|
||||
case UI_DISPATCH_CMD_SET_WEATHER:
|
||||
ui_dispatch_apply_weather(&msg);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
_lock_release(&lvgl_api_lock);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t ui_dispatch_init(void)
|
||||
{
|
||||
if (s_ui_dispatch_queue != NULL && s_ui_dispatch_task != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (s_ui_dispatch_queue == NULL) {
|
||||
s_ui_dispatch_queue = xQueueCreate(UI_DISPATCH_QUEUE_LEN, sizeof(ui_dispatch_msg_t));
|
||||
if (s_ui_dispatch_queue == NULL) {
|
||||
ESP_LOGE(TAG, "failed to create queue");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_ui_dispatch_task == NULL) {
|
||||
BaseType_t ret = xTaskCreate(ui_dispatch_task, "ui_dispatch",
|
||||
UI_DISPATCH_TASK_STACK_SIZE, NULL,
|
||||
UI_DISPATCH_TASK_PRIORITY, &s_ui_dispatch_task);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "failed to create task");
|
||||
vQueueDelete(s_ui_dispatch_queue);
|
||||
s_ui_dispatch_queue = NULL;
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void ui_dispatch_set_brightness(uint8_t brightness_percent)
|
||||
{
|
||||
ui_dispatch_msg_t msg = {
|
||||
.type = UI_DISPATCH_CMD_SET_BRIGHTNESS,
|
||||
};
|
||||
|
||||
msg.data.brightness.brightness_percent = brightness_percent;
|
||||
ui_dispatch_send(&msg);
|
||||
}
|
||||
|
||||
bool ui_dispatch_set_date(uint16_t year, uint8_t month, uint8_t day, uint8_t week)
|
||||
{
|
||||
ui_dispatch_msg_t msg;
|
||||
memset(&msg, 0, sizeof(msg));
|
||||
msg.type = UI_DISPATCH_CMD_SET_DATE;
|
||||
msg.data.date.year = year;
|
||||
msg.data.date.month = month;
|
||||
msg.data.date.day = day;
|
||||
msg.data.date.week = week;
|
||||
return ui_dispatch_send(&msg);
|
||||
}
|
||||
|
||||
void ui_dispatch_set_time(uint8_t hour, uint8_t minute, uint8_t second)
|
||||
{
|
||||
ui_dispatch_msg_t msg = {
|
||||
.type = UI_DISPATCH_CMD_SET_TIME,
|
||||
};
|
||||
|
||||
msg.data.time.hour = hour;
|
||||
msg.data.time.minute = minute;
|
||||
msg.data.time.second = second;
|
||||
ui_dispatch_send(&msg);
|
||||
}
|
||||
|
||||
#if 0 // UNUSED: BLE uses direct path via ble_data_ansy.c
|
||||
void ui_dispatch_set_weather(uint8_t timing, uint16_t temp_enc, const char *weather)
|
||||
{
|
||||
ui_dispatch_msg_t msg = {
|
||||
.type = UI_DISPATCH_CMD_SET_WEATHER,
|
||||
};
|
||||
|
||||
msg.data.weather.timing = timing;
|
||||
msg.data.weather.temp_enc = temp_enc;
|
||||
if (weather != NULL) {
|
||||
strncpy(msg.data.weather.weather, weather, sizeof(msg.data.weather.weather) - 1);
|
||||
msg.data.weather.weather[sizeof(msg.data.weather.weather) - 1] = '\0';
|
||||
}
|
||||
|
||||
ui_dispatch_send(&msg);
|
||||
}
|
||||
#endif // End of unused ui_dispatch_set_weather
|
||||
|
||||
void ui_dispatch_sync_perpetual_calendar_from_time(void)
|
||||
{
|
||||
if (s_perpetual_last_week_idx >= 0 &&
|
||||
s_perpetual_last_year >= 1900 &&
|
||||
s_perpetual_last_month >= 1 && s_perpetual_last_month <= 12 &&
|
||||
s_perpetual_last_day >= 1 && s_perpetual_last_day <= 31) {
|
||||
ui_dispatch_perpetual_calendar_set_labels(s_perpetual_last_week_idx,
|
||||
s_perpetual_last_year,
|
||||
s_perpetual_last_month,
|
||||
s_perpetual_last_day);
|
||||
portENTER_CRITICAL(&s_clock_cal_mux);
|
||||
s_clock_cal_valid = true;
|
||||
s_clock_cal_y = s_perpetual_last_year;
|
||||
s_clock_cal_m = s_perpetual_last_month;
|
||||
s_clock_cal_d = s_perpetual_last_day;
|
||||
s_clock_cal_w = (uint8_t)(s_perpetual_last_week_idx + 1);
|
||||
portEXIT_CRITICAL(&s_clock_cal_mux);
|
||||
return;
|
||||
}
|
||||
|
||||
if (guider_ui.screen_time_label_date == NULL || !lv_obj_is_valid(guider_ui.screen_time_label_date)) {
|
||||
return;
|
||||
}
|
||||
if (guider_ui.screen_time_label_week == NULL || !lv_obj_is_valid(guider_ui.screen_time_label_week)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *ds = lv_label_get_text(guider_ui.screen_time_label_date);
|
||||
const char *ws = lv_label_get_text(guider_ui.screen_time_label_week);
|
||||
unsigned y = 0;
|
||||
unsigned m = 0;
|
||||
unsigned day = 0;
|
||||
if (sscanf(ds, "%04u年%02u月%02u日", &y, &m, &day) != 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = -1;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
if (strcmp(ws, WEEK_TEXT[i]) == 0) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
s_perpetual_last_week_idx = idx;
|
||||
s_perpetual_last_year = (uint16_t)y;
|
||||
s_perpetual_last_month = (uint8_t)m;
|
||||
s_perpetual_last_day = (uint8_t)day;
|
||||
ui_dispatch_perpetual_calendar_set_labels(idx, (uint16_t)y, (uint8_t)m, (uint8_t)day);
|
||||
portENTER_CRITICAL(&s_clock_cal_mux);
|
||||
s_clock_cal_valid = true;
|
||||
s_clock_cal_y = (uint16_t)y;
|
||||
s_clock_cal_m = (uint8_t)m;
|
||||
s_clock_cal_d = (uint8_t)day;
|
||||
s_clock_cal_w = (uint8_t)(idx + 1);
|
||||
portEXIT_CRITICAL(&s_clock_cal_mux);
|
||||
}
|
||||
|
||||
static bool ui_dispatch_clock_calendar_advance_one_day(void)
|
||||
{
|
||||
portENTER_CRITICAL(&s_clock_cal_mux);
|
||||
const bool valid = s_clock_cal_valid;
|
||||
const uint16_t y = s_clock_cal_y;
|
||||
const uint8_t m = s_clock_cal_m;
|
||||
const uint8_t d = s_clock_cal_d;
|
||||
portEXIT_CRITICAL(&s_clock_cal_mux);
|
||||
|
||||
if (!valid || m < 1 || m > 12 || d < 1 || d > 31 || y < 1970) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct tm t;
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.tm_year = (int)y - 1900;
|
||||
t.tm_mon = (int)m - 1;
|
||||
t.tm_mday = (int)d + 1;
|
||||
t.tm_hour = 12;
|
||||
t.tm_min = 0;
|
||||
t.tm_sec = 0;
|
||||
t.tm_isdst = -1;
|
||||
|
||||
if (mktime(&t) == (time_t)-1) {
|
||||
ESP_LOGW(TAG, "mktime failed in midnight calendar roll");
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint16_t ny = (uint16_t)(t.tm_year + 1900);
|
||||
const uint8_t nm = (uint8_t)(t.tm_mon + 1);
|
||||
const uint8_t nd = (uint8_t)t.tm_mday;
|
||||
const int idx = (t.tm_wday + 6) % 7;
|
||||
const uint8_t nw = (uint8_t)(idx + 1);
|
||||
|
||||
ui_dispatch_put_date_on_ui_locked(ny, nm, nd, nw);
|
||||
ESP_LOGI(TAG, "时钟午夜翻日: %04u-%02u-%02u", (unsigned)ny, (unsigned)nm, (unsigned)nd);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ui_dispatch_clock_calendar_try_roll_at_midnight(int prev_h, int prev_m, int prev_s,
|
||||
int new_h, int new_m, int new_s)
|
||||
{
|
||||
if (prev_h != 23 || prev_m != 59 || prev_s != 59) {
|
||||
return;
|
||||
}
|
||||
if (new_h != 0 || new_m != 0 || new_s != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(void)ui_dispatch_clock_calendar_advance_one_day();
|
||||
}
|
||||
|
||||
bool ui_dispatch_digital_clock_monotonic_tick(void)
|
||||
{
|
||||
if (!s_mono_anchor_valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t now_us = esp_timer_get_time();
|
||||
int64_t d_us = now_us - s_mono_anchor_us;
|
||||
if (d_us < 0) {
|
||||
d_us = 0;
|
||||
}
|
||||
|
||||
const uint64_t elapsed = (uint64_t)(d_us / 1000000LL);
|
||||
const uint64_t total = (uint64_t)s_mono_anchor_sec_in_day + elapsed;
|
||||
const uint64_t day = total / 86400ULL;
|
||||
|
||||
const uint32_t max_steps = 400u;
|
||||
uint32_t guard = 0u;
|
||||
while (s_mono_last_day_index < day && guard < max_steps) {
|
||||
if (!ui_dispatch_clock_calendar_advance_one_day()) {
|
||||
ESP_LOGW(TAG, "mono 跨日翻页跳过(无有效车机日期),对齐 day_index");
|
||||
s_mono_last_day_index = day;
|
||||
break;
|
||||
}
|
||||
s_mono_last_day_index++;
|
||||
guard++;
|
||||
}
|
||||
if (s_mono_last_day_index < day) {
|
||||
ESP_LOGW(TAG, "mono 跨日次数超过上限 %lu,强制对齐 day_index", (unsigned long)max_steps);
|
||||
s_mono_last_day_index = day;
|
||||
}
|
||||
|
||||
const uint32_t sid = (uint32_t)(total % 86400ULL);
|
||||
screen_time_digital_clock_now_hour_value = (int)(sid / 3600u);
|
||||
screen_time_digital_clock_now_min_value = (int)((sid / 60u) % 60u);
|
||||
screen_time_digital_clock_now_sec_value = (int)(sid % 60u);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef UI_DISPATCH_H
|
||||
#define UI_DISPATCH_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
esp_err_t ui_dispatch_init(void);
|
||||
|
||||
void ui_dispatch_set_brightness(uint8_t brightness_percent);
|
||||
/** @return true 已入队,false 未初始化或队列满 */
|
||||
bool ui_dispatch_set_date(uint16_t year, uint8_t month, uint8_t day, uint8_t week);
|
||||
void ui_dispatch_set_time(uint8_t hour, uint8_t minute, uint8_t second);
|
||||
// void ui_dispatch_set_weather(uint8_t timing, uint16_t temp_enc, const char *weather); // UNUSED: BLE uses direct path
|
||||
void ui_dispatch_refresh_home_weather_slot_hour_labels(void);
|
||||
|
||||
/** 万年历 week/day 与时间页联动:与最近一次日期同步一致;进入万年历屏时可再调一次 */
|
||||
void ui_dispatch_sync_perpetual_calendar_from_time(void);
|
||||
|
||||
/**
|
||||
* 软件时钟整秒进位后调用:仅在 23:59:59 -> 00:00:00 且已有有效车机日历时,把日期推进一天。
|
||||
* 须在持有 lvgl_api_lock 的 LVGL 任务上下文中调用(如 LVGL 定时器回调内)。
|
||||
*/
|
||||
void ui_dispatch_clock_calendar_try_roll_at_midnight(int prev_h, int prev_m, int prev_s,
|
||||
int new_h, int new_m, int new_s);
|
||||
|
||||
/**
|
||||
* LVGL 秒定时器内调用:若已收到过 ui_dispatch 校时,则按 esp_timer 单调时钟推算时分秒并处理跨午夜翻日。
|
||||
* @return true 已更新全局 `screen_time_digital_clock_now_*`(勿再 clock_count / try_roll)
|
||||
*/
|
||||
bool ui_dispatch_digital_clock_monotonic_tick(void);
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user