初始提交卡包 USB 工程

纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
zhengxin
2026-10-13 21:54:50 +08:00
co-authored by Cursor
commit 851e347cf5
260 changed files with 269349 additions and 0 deletions
+332
View File
@@ -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);
}
+26
View File
@@ -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 */
+738
View File
@@ -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;
}
+32
View File
@@ -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