diff --git a/components/control_plane/src/runtime/control_plane_runtime_workers.c b/components/control_plane/src/runtime/control_plane_runtime_workers.c index 4ff9001..eb0a8ce 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -156,6 +156,14 @@ #define CONFIG_TQ_LOCAL_TASK_CORE 1 #endif +#ifndef CONFIG_TQ_TIME_SNTP_SERVER +#define CONFIG_TQ_TIME_SNTP_SERVER "ntp.aliyun.com" +#endif + +#ifndef CONFIG_TQ_TIME_POSIX_TZ +#define CONFIG_TQ_TIME_POSIX_TZ "CST-8" +#endif + #if CONFIG_TQ_NETWORK_TASK_CORE < 0 #define CONTROL_PLANE_NETWORK_TASK_CORE tskNO_AFFINITY #else @@ -168,10 +176,6 @@ #define CONTROL_PLANE_LOCAL_TASK_CORE ((BaseType_t)CONFIG_TQ_LOCAL_TASK_CORE) #endif -#ifndef CONFIG_FORTUNE_SNTP_SERVER -#define CONFIG_FORTUNE_SNTP_SERVER "ntp.aliyun.com" -#endif - #ifndef CONFIG_FORTUNE_USER_CONFIG_HTTP_PORT #define CONFIG_FORTUNE_USER_CONFIG_HTTP_PORT 80 #endif @@ -188,8 +192,6 @@ #define CONTROL_PLANE_FORTUNE_CONNECT_POLL_MS 1000 #define CONTROL_PLANE_FORTUNE_REFRESH_POLL_MS 1000 #define CONTROL_PLANE_FORTUNE_WS_SETTLE_MAX_MS (30 * 1000) -/* China Standard Time (UTC+8, no DST). Used to detect local midnight. */ -#define CONTROL_PLANE_FORTUNE_POSIX_TZ "CST-8" #endif static const char *TAG = "cp_rt_workers"; @@ -2004,6 +2006,16 @@ static void control_plane_stop_user_config_server(void) #endif } +static void control_plane_start_wall_clock_sync(void) +{ + esp_err_t err = domain_runtime_time_sync_start(CONFIG_TQ_TIME_SNTP_SERVER, + CONFIG_TQ_TIME_POSIX_TZ); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Start wall-clock synchronization failed: %s", + esp_err_to_name(err)); + } +} + static void control_plane_wifi_state_task(void *arg) { (void)arg; @@ -2047,6 +2059,7 @@ static void control_plane_wifi_state_task(void *arg) disconnected_expression_active = true; last_disconnected_request_tick = xTaskGetTickCount(); } else { + control_plane_start_wall_clock_sync(); (void)control_plane_schedule_websocket_wait_task(); esp_err_t web_err = control_plane_ensure_user_config_server(); if (web_err != ESP_OK) { @@ -2208,6 +2221,7 @@ static void control_plane_wifi_state_task(void *arg) if (connected && (!was_connected || disconnected_expression_active)) { ESP_LOGI(TAG, "Wi-Fi STA connected; restoring idle expression"); + control_plane_start_wall_clock_sync(); control_plane_request_wifi_state_expression(DOMAIN_EXPRESSION_IDLE, DOMAIN_EXPRESSION_TRIGGER_WIFI_RECONNECTED, "Wi-Fi reconnected"); @@ -2318,8 +2332,8 @@ static void control_plane_fortune_delay_ms(uint32_t delay_ms) static esp_err_t control_plane_wait_for_synced_time(void) { - esp_err_t err = domain_runtime_time_sync_start(CONFIG_FORTUNE_SNTP_SERVER, - CONTROL_PLANE_FORTUNE_POSIX_TZ); + esp_err_t err = domain_runtime_time_sync_start(CONFIG_TQ_TIME_SNTP_SERVER, + CONFIG_TQ_TIME_POSIX_TZ); if (err != ESP_OK) { return err; } diff --git a/components/platform/src/lcd_direct.c b/components/platform/src/lcd_direct.c index fe700c2..f4dc29f 100644 --- a/components/platform/src/lcd_direct.c +++ b/components/platform/src/lcd_direct.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "driver/gpio.h" @@ -65,6 +66,16 @@ #define LCD_DIRECT_BACKLIGHT_PWM_TIMER LEDC_TIMER_0 #define LCD_DIRECT_BACKLIGHT_PWM_CHANNEL LEDC_CHANNEL_0 #define LCD_DIRECT_BACKLIGHT_LOCK_TIMEOUT_MS 100U +#define LCD_DIRECT_CLOCK_X 12U +#define LCD_DIRECT_CLOCK_Y 10U +#define LCD_DIRECT_CLOCK_GLYPH_WIDTH 5U +#define LCD_DIRECT_CLOCK_GLYPH_HEIGHT 7U +#define LCD_DIRECT_CLOCK_GLYPH_SCALE 4U +#define LCD_DIRECT_CLOCK_GLYPH_ADVANCE 24U +#define LCD_DIRECT_CLOCK_TEXT_LENGTH 5U +#define LCD_DIRECT_CLOCK_OUTLINE_PX 1 +#define LCD_DIRECT_CLOCK_COLOR 0xFFFFU +#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U #define LCD_CMD_MADCTL 0x36 #define LCD_CMD_RAMCTRL 0xB0 @@ -137,6 +148,22 @@ static const lcd_direct_init_cmd_t s_st7789_init_cmds[] = { {0, {0}, 0xFF}, }; +/* Five-bit rows, most-significant displayed column first. Kept in flash. */ +static const uint8_t s_clock_glyph_rows[][LCD_DIRECT_CLOCK_GLYPH_HEIGHT] = { + {0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E}, /* 0 */ + {0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E}, /* 1 */ + {0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F}, /* 2 */ + {0x1E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x1E}, /* 3 */ + {0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02}, /* 4 */ + {0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x1E}, /* 5 */ + {0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E}, /* 6 */ + {0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, /* 7 */ + {0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E}, /* 8 */ + {0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E}, /* 9 */ + {0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x00}, /* : */ + {0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00}, /* - */ +}; + static esp_lcd_panel_io_handle_t s_panel_io; static esp_lcd_panel_handle_t s_panel; static SemaphoreHandle_t s_lock; @@ -165,6 +192,11 @@ static lcd_direct_decoded_image_t s_frame_cache[LCD_DIRECT_FRAME_CACHE_MAX]; static uint32_t s_cache_use_counter; static void lcd_direct_signal_completion(void); +static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]); +static void lcd_direct_overlay_clock(uint16_t *pixel_buffer, + uint16_t row_start, + uint16_t row_count, + const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]); static bool IRAM_ATTR lcd_direct_on_color_trans_done(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *event_data, @@ -651,6 +683,9 @@ static uint16_t lcd_direct_color_for_row(uint16_t row, const lcd_direct_request_ static esp_err_t lcd_direct_draw_test_pattern(const lcd_direct_request_t *request) { + char clock_text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U] = {0}; + lcd_direct_clock_text(clock_text); + for (uint16_t row_start = 0; row_start < LCD_DIRECT_HEIGHT; row_start += LCD_DIRECT_ROWS_PER_TRANSFER) { uint16_t row_count = LCD_DIRECT_HEIGHT - row_start; if (row_count > LCD_DIRECT_ROWS_PER_TRANSFER) { @@ -662,6 +697,7 @@ static esp_err_t lcd_direct_draw_test_pattern(const lcd_direct_request_t *reques s_line_buffer[(uint32_t)y * LCD_DIRECT_WIDTH + x] = color; } } + lcd_direct_overlay_clock(s_line_buffer, row_start, row_count, clock_text); ESP_RETURN_ON_ERROR(lcd_direct_draw_rows(row_start, row_count), TAG, "write pixels failed"); @@ -825,6 +861,111 @@ static void lcd_direct_overlay_icons(uint16_t *pixel_buffer, } } +static size_t lcd_direct_clock_glyph_index(char character) +{ + if (character >= '0' && character <= '9') { + return (size_t)(character - '0'); + } + return character == ':' ? 10U : 11U; +} + +static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]) +{ + if (!platform_time_is_synced()) { + memcpy(text, "--:--", LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U); + return; + } + + time_t now = time(NULL); + struct tm local_tm = {0}; + if (localtime_r(&now, &local_tm) == NULL) { + memcpy(text, "--:--", LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U); + return; + } + + text[0] = (char)('0' + local_tm.tm_hour / 10); + text[1] = (char)('0' + local_tm.tm_hour % 10); + text[2] = ':'; + text[3] = (char)('0' + local_tm.tm_min / 10); + text[4] = (char)('0' + local_tm.tm_min % 10); + text[5] = '\0'; +} + +static void lcd_direct_draw_clock_pass(uint16_t *pixel_buffer, + uint16_t row_start, + uint16_t row_count, + const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U], + int x_offset, + int y_offset, + uint16_t color) +{ + const int row_end = (int)row_start + (int)row_count; + for (size_t character_index = 0; character_index < LCD_DIRECT_CLOCK_TEXT_LENGTH; + ++character_index) { + const uint8_t *glyph = + s_clock_glyph_rows[lcd_direct_clock_glyph_index(text[character_index])]; + const int glyph_x = (int)LCD_DIRECT_CLOCK_X + + (int)(character_index * LCD_DIRECT_CLOCK_GLYPH_ADVANCE) + + x_offset; + const int glyph_y = (int)LCD_DIRECT_CLOCK_Y + y_offset; + + for (uint16_t source_y = 0; source_y < LCD_DIRECT_CLOCK_GLYPH_HEIGHT; ++source_y) { + for (uint16_t scale_y = 0; scale_y < LCD_DIRECT_CLOCK_GLYPH_SCALE; ++scale_y) { + const int destination_y = glyph_y + + (int)(source_y * LCD_DIRECT_CLOCK_GLYPH_SCALE + scale_y); + if (destination_y < (int)row_start || destination_y >= row_end) { + continue; + } + + for (uint16_t source_x = 0; source_x < LCD_DIRECT_CLOCK_GLYPH_WIDTH; + ++source_x) { + if ((glyph[source_y] & + (1U << (LCD_DIRECT_CLOCK_GLYPH_WIDTH - 1U - source_x))) == 0U) { + continue; + } + for (uint16_t scale_x = 0; scale_x < LCD_DIRECT_CLOCK_GLYPH_SCALE; + ++scale_x) { + const int destination_x = glyph_x + + (int)(source_x * LCD_DIRECT_CLOCK_GLYPH_SCALE + scale_x); + if (destination_x < 0 || destination_x >= (int)LCD_DIRECT_WIDTH) { + continue; + } + const uint32_t destination_index = + (uint32_t)(destination_y - (int)row_start) * LCD_DIRECT_WIDTH + + (uint32_t)destination_x; + pixel_buffer[destination_index] = lcd_direct_swap_color(color); + } + } + } + } + } +} + +static void lcd_direct_overlay_clock(uint16_t *pixel_buffer, + uint16_t row_start, + uint16_t row_count, + const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]) +{ + if (pixel_buffer == NULL || text == NULL) { + return; + } + + lcd_direct_draw_clock_pass(pixel_buffer, + row_start, + row_count, + text, + LCD_DIRECT_CLOCK_OUTLINE_PX, + LCD_DIRECT_CLOCK_OUTLINE_PX, + LCD_DIRECT_CLOCK_OUTLINE_COLOR); + lcd_direct_draw_clock_pass(pixel_buffer, + row_start, + row_count, + text, + 0, + 0, + LCD_DIRECT_CLOCK_COLOR); +} + static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image, const lcd_direct_icon_draw_t *icons, size_t icon_count) @@ -870,6 +1011,11 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image, combined_icon_count, prepared_icons, sizeof(prepared_icons) / sizeof(prepared_icons[0])); + const bool clock_visible = strstr(image->path, "/qrcode.png") == NULL; + char clock_text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U] = {0}; + if (clock_visible) { + lcd_direct_clock_text(clock_text); + } esp_err_t err = ESP_OK; for (uint16_t row_start = 0; row_start < LCD_DIRECT_HEIGHT; row_start += LCD_DIRECT_ROWS_PER_TRANSFER) { @@ -890,6 +1036,9 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image, } } + if (clock_visible) { + lcd_direct_overlay_clock(s_line_buffer, row_start, row_count, clock_text); + } lcd_direct_overlay_icons(s_line_buffer, row_start, row_count, prepared_icons, prepared_icon_count); err = lcd_direct_draw_rows(row_start, row_count); if (err != ESP_OK) { diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 27dde33..b08c7de 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -43,6 +43,20 @@ menu "Example Configuration" firmware uses a longer timeout to tolerate brief beacon loss on crowded 2.4 GHz networks. + config TQ_TIME_SNTP_SERVER + string "Wall-clock SNTP server" + default "ntp.aliyun.com" + help + SNTP server used to establish wall-clock time after Wi-Fi + obtains an IP address. + + config TQ_TIME_POSIX_TZ + string "Wall-clock POSIX time zone" + default "CST-8" + help + POSIX TZ string used for local wall-clock display and daily + scheduling. For China Standard Time use "CST-8". + menu "OTA Firmware Update" config TQ_OTA_ENABLE @@ -1115,14 +1129,6 @@ menu "Example Configuration" help IANA time-zone name sent to the fortune API. - config FORTUNE_SNTP_SERVER - string "SNTP server" - depends on FORTUNE_ENABLE - default "ntp.aliyun.com" - help - SNTP server used to establish wall-clock time before daily - scheduling starts. - config FORTUNE_CONNECT_TIMEOUT_MS int "Fortune HTTP timeout (ms)" depends on FORTUNE_ENABLE diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 06b622f..ae63977 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -148,6 +148,8 @@ CONFIG_ESP_WIFI_RX_BA_WIN=6 # beacon stability over modem-sleep savings. # CONFIG_TQ_WIFI_STA_POWER_SAVE is not set CONFIG_TQ_WIFI_STA_INACTIVE_TIME_SEC=15 +CONFIG_TQ_TIME_SNTP_SERVER="ntp.aliyun.com" +CONFIG_TQ_TIME_POSIX_TZ="CST-8" # ESP-SR recommended performance/cache profile on ESP32-S3 # CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set @@ -185,7 +187,6 @@ CONFIG_MULTIMODAL_DEVICE_TOKEN_URL="https://gz.api.talkingq.com/token/api/device CONFIG_FORTUNE_ENABLE=y CONFIG_FORTUNE_API_URL="https://gz.api.talkingq.com/token/api/device/fortune/daily-wealth" CONFIG_FORTUNE_TIMEZONE="Asia/Shanghai" -CONFIG_FORTUNE_SNTP_SERVER="ntp.aliyun.com" CONFIG_FORTUNE_CONNECT_TIMEOUT_MS=10000 CONFIG_FORTUNE_USER_CONFIG_HTTP_PORT=80 CONFIG_FORTUNE_USER_CONFIG_MDNS_HOSTNAME="tq-cat"