#include "UI_DISPATCH/ui_dispatch.h" #include #include #include #include #include #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; }