Files
P4-USB-transfer/main/UI_DISPATCH/ui_dispatch.c
T
zhengxinandCursor 851e347cf5 初始提交卡包 USB 工程
纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。

Co-authored-by: Cursor <[email protected]>
2026-10-13 21:54:50 +08:00

739 lines
25 KiB
C

#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;
}