纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。 Co-authored-by: Cursor <[email protected]>
739 lines
25 KiB
C
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;
|
|
}
|