From e75846ea15d33b09d4d2d9159f2866ecbe075b9c Mon Sep 17 00:00:00 2001 From: chenjintao Date: Fri, 9 Oct 2026 14:38:17 +0800 Subject: [PATCH] feat(lcd): unify idle restoration and add clock battery HUD --- .../src/runtime/domain_runtime_adapters.c | 2 + components/platform/include/platform.h | 2 + components/platform/src/lcd_direct.c | 224 ++++++++++++++---- docs/lcd-idle-status.zh-CN.md | 44 ++++ tools/test_lcd_status_hud.py | 68 ++++++ 5 files changed, 294 insertions(+), 46 deletions(-) create mode 100644 docs/lcd-idle-status.zh-CN.md create mode 100644 tools/test_lcd_status_hud.py diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index 4f1e021..9a73261 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -1416,6 +1416,8 @@ esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *s status->level_state == DOMAIN_BATTERY_LEVEL_CRITICAL), .charging_visible = status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING, + .battery_valid = status->valid, + .battery_percent = status->percentage, }; return platform_lcd_set_status_overlay(&overlay); } diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index f057a4e..02a5d8e 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -683,6 +683,8 @@ typedef struct { typedef struct { bool low_battery_visible; bool charging_visible; + bool battery_valid; /* false 时显示未知值,不伪造 0%。 */ + uint8_t battery_percent; /* domain 提供的 SOC,0..100。 */ } platform_lcd_status_overlay_t; /* 状态覆盖层是持久状态,不会随一次 presentation 播放结束而自动清除。 */ diff --git a/components/platform/src/lcd_direct.c b/components/platform/src/lcd_direct.c index 321bc68..b672c31 100644 --- a/components/platform/src/lcd_direct.c +++ b/components/platform/src/lcd_direct.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -98,6 +99,7 @@ #define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U #if CONFIG_TQ_LCD_EMOTE_GFX_TEST +#define LCD_DIRECT_HUD_ROWS 40U #define LCD_DIRECT_EMOTE_FPS 30U #define LCD_DIRECT_EMOTE_BUFFER_ROWS 4U #define LCD_DIRECT_EMOTE_EYES_X 41 @@ -241,6 +243,12 @@ static gfx_image_dsc_t s_emote_eye_image; static uint8_t *s_emote_base_pixels; static uint8_t *s_emote_eye_pixels; static bool s_emote_ready; +static gfx_obj_t *s_emote_hud; +static gfx_image_dsc_t s_emote_hud_image; +static uint16_t *s_emote_hud_pixels; +static char s_emote_hud_clock[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]; +static platform_lcd_status_overlay_t s_emote_hud_status; +static bool s_emote_hud_valid; static gfx_handle_t s_emote_handle; static gfx_disp_t *s_emote_disp; @@ -258,6 +266,18 @@ static void lcd_direct_overlay_clock(uint16_t *pixel_buffer, uint16_t row_count, const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]); +static void lcd_direct_overlay_battery(uint16_t *pixels, uint16_t row_start, uint16_t rows); +static void lcd_direct_queue_idle_if_available(void) +{ + /* 自动恢复不能覆盖动画结束瞬间到达的新请求。 */ + portENTER_CRITICAL(&s_state_lock); + if (!s_pending_valid && !s_stop_requested) { + s_pending = (lcd_direct_request_t) { .kind = PLATFORM_LCD_PRESENTATION_IDLE }; + s_pending_valid = true; + } + portEXIT_CRITICAL(&s_state_lock); +} + 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, void *user_ctx) @@ -423,8 +443,20 @@ static esp_err_t lcd_direct_emote_init(void) ESP_RETURN_ON_ERROR(gfx_mesh_img_get_point_q8(s_emote_eyes_open, i, &s_emote_eye_rest[i]), TAG, "read eye mesh failed"); } - ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock esp_emote_gfx failed"); - ESP_RETURN_ON_ERROR(gfx_emote_unlock(s_emote_handle), TAG, "unlock esp_emote_gfx failed"); + s_emote_hud_pixels = heap_caps_calloc(LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS, + sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + ESP_RETURN_ON_FALSE(s_emote_hud_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate HUD failed"); + s_emote_hud_image = (gfx_image_dsc_t) { + .header.cf = GFX_COLOR_FORMAT_RGB565, + .header.magic = C_ARRAY_HEADER_MAGIC, + .header.w = LCD_DIRECT_WIDTH, + .header.h = LCD_DIRECT_HUD_ROWS, + .data_size = LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS * sizeof(uint16_t), + .data = (const uint8_t *)s_emote_hud_pixels, + }; + s_emote_hud = gfx_img_create(s_emote_disp); + ESP_RETURN_ON_FALSE(s_emote_hud != NULL, ESP_ERR_NO_MEM, TAG, "create HUD failed"); + ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_hud, &s_emote_hud_image), TAG, "set HUD failed"); s_emote_ready = true; ESP_LOGI(TAG, "esp_emote_gfx idle test ready (%u fps, 2 x %u-row DMA buffers)", @@ -442,6 +474,47 @@ static bool lcd_direct_should_stop_idle(void) return stop; } +/* 调用方持有 GFX 锁;字形复用 PNG 路径,无字体包和额外 PNG 解码。 */ +static esp_err_t lcd_direct_emote_update_hud(bool force, bool *changed) +{ + if (changed != NULL) { + *changed = false; + } + char clock[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]; + lcd_direct_clock_text(clock); + platform_lcd_status_overlay_t status; + portENTER_CRITICAL(&s_state_lock); + status = s_status_overlay; + portEXIT_CRITICAL(&s_state_lock); + if (!force && s_emote_hud_valid && strcmp(clock, s_emote_hud_clock) == 0 && + status.battery_valid == s_emote_hud_status.battery_valid && + status.battery_percent == s_emote_hud_status.battery_percent && + status.low_battery_visible == s_emote_hud_status.low_battery_visible && + status.charging_visible == s_emote_hud_status.charging_visible) { + return ESP_OK; + } + memset(s_emote_hud_pixels, 0, s_emote_hud_image.data_size); + lcd_direct_overlay_clock(s_emote_hud_pixels, 0, LCD_DIRECT_HUD_ROWS, clock); + lcd_direct_overlay_battery(s_emote_hud_pixels, 0, LCD_DIRECT_HUD_ROWS); +#if !CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES + /* GFX 图像使用高字节在前,PNG 行缓冲可能使用主机字节序。 */ + for (size_t i = 0; i < LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS; ++i) { + uint16_t color = s_emote_hud_pixels[i]; + s_emote_hud_pixels[i] = (color >> 8) | (color << 8); + } +#endif + esp_err_t err = gfx_img_set_src(s_emote_hud, &s_emote_hud_image); + if (err == ESP_OK) { + memcpy(s_emote_hud_clock, clock, sizeof(clock)); + s_emote_hud_status = status; + s_emote_hud_valid = true; + if (changed != NULL) { + *changed = true; + } + } + return err; +} + static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms) { uint32_t remaining_ms = duration_ms; @@ -449,6 +522,16 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms) if (lcd_direct_should_stop_idle()) { return ESP_ERR_INVALID_STATE; } + ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock HUD failed"); + bool changed = false; + esp_err_t hud_err = lcd_direct_emote_update_hud(false, &changed); + if (hud_err == ESP_OK && changed) { + hud_err = gfx_refr_now(s_emote_handle); + } + esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle); + if (hud_err != ESP_OK || unlock_err != ESP_OK) { + return hud_err != ESP_OK ? hud_err : unlock_err; + } uint32_t slice_ms = remaining_ms > LCD_DIRECT_PRESENTATION_WAIT_POLL_MS ? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS : remaining_ms; @@ -495,7 +578,7 @@ static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure) LCD_DIRECT_EMOTE_EYES_Y + offset_y); } -static esp_err_t lcd_direct_play_emote_face_until_pending(void) +static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_request_t *request) { ESP_RETURN_ON_FALSE(s_emote_handle != NULL && s_emote_eyes_open != NULL, ESP_ERR_INVALID_STATE, @@ -505,7 +588,10 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(void) portENTER_CRITICAL(&s_state_lock); s_emote_active = true; portEXIT_CRITICAL(&s_state_lock); - esp_err_t result = lcd_direct_emote_set_eye_closure(0U); + esp_err_t result = lcd_direct_emote_update_hud(true, NULL); + if (result == ESP_OK) { + result = lcd_direct_emote_set_eye_closure(0U); + } if (result == ESP_OK) { gfx_disp_refresh_all(s_emote_disp); result = gfx_refr_now(s_emote_handle); @@ -521,8 +607,12 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(void) 24U, 80U, 160U, 232U, 255U, 255U, 232U, 192U, 128U, 64U, 16U, 0U, }; - while (result == ESP_OK) { - result = lcd_direct_emote_wait(LCD_DIRECT_EMOTE_OPEN_MS); + uint16_t cycles = 0; + const uint16_t repeats = request != NULL ? request->repeat_count : 0; + const uint32_t open_ms = repeats > 0 && request->frame_ms > 0 + ? request->frame_ms : LCD_DIRECT_EMOTE_OPEN_MS; + while (result == ESP_OK && (repeats == 0 || cycles < repeats)) { + result = lcd_direct_emote_wait(open_ms); if (result != ESP_OK) { break; } @@ -543,8 +633,12 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(void) result = lcd_direct_emote_wait(LCD_DIRECT_EMOTE_STEP_MS); } } + ++cycles; } + if (result == ESP_OK && repeats > 0) { + lcd_direct_signal_completion(); + } esp_err_t lock_err = gfx_emote_lock(s_emote_handle); if (lock_err == ESP_OK) { portENTER_CRITICAL(&s_state_lock); @@ -1117,13 +1211,7 @@ static size_t lcd_direct_append_status_icons(const lcd_direct_image_t *image, .y = LCD_DIRECT_LOW_BATTERY_ICON_Y, }; } - if (overlay.charging_visible && icon_count < icon_capacity) { - icons[icon_count++] = (lcd_direct_icon_draw_t) { - .icon_id = LCD_DIRECT_ICON_CHARGING, - .x = LCD_DIRECT_CHARGING_ICON_X, - .y = LCD_DIRECT_CHARGING_ICON_Y, - }; - } + /* 充电状态改由右上角程序绘制的电池图标表示,避免和百分比重叠。 */ return icon_count; } @@ -1291,6 +1379,59 @@ static void lcd_direct_overlay_clock(uint16_t *pixel_buffer, LCD_DIRECT_CLOCK_COLOR); } +/* 小型电池 + 数字百分比,沿用 5x7 点阵;所有写入都裁剪到当前行块。 */ +static void lcd_direct_hud_pixel(uint16_t *pixels, uint16_t row_start, + uint16_t rows, int x, int y, uint16_t color) +{ + if (x >= 0 && x < LCD_DIRECT_WIDTH && y >= row_start && y < row_start + rows) { + pixels[(y - row_start) * LCD_DIRECT_WIDTH + x] = lcd_direct_swap_color(color); + } +} + +static void lcd_direct_overlay_battery(uint16_t *pixels, uint16_t row_start, uint16_t rows) +{ + platform_lcd_status_overlay_t status; + portENTER_CRITICAL(&s_state_lock); + status = s_status_overlay; + portEXIT_CRITICAL(&s_state_lock); + const uint16_t color = status.charging_visible ? 0x07E0U : + status.low_battery_visible ? 0xF800U : 0xFFFFU; + const int left = LCD_DIRECT_WIDTH - 108; + char text[5]; + if (status.battery_valid) { + snprintf(text, sizeof(text), "%u%%", (unsigned)status.battery_percent); + } else { + memcpy(text, "--%", 4); + } + static const uint8_t percent[7] = {0x19, 0x1A, 0x02, 0x04, 0x08, 0x0B, 0x13}; + for (size_t i = 0; text[i] != '\0'; ++i) { + const uint8_t *glyph = text[i] == '%' ? percent : + s_clock_glyph_rows[lcd_direct_clock_glyph_index(text[i])]; + for (int y = 0; y < 21; ++y) { + for (int x = 0; x < 15; ++x) { + if (glyph[y / 3] & (1U << (4 - x / 3))) { + lcd_direct_hud_pixel(pixels, row_start, rows, left + i * 18 + x, 13 + y, color); + } + } + } + } + const int icon_x = LCD_DIRECT_WIDTH - 32; + const int fill = status.battery_valid ? status.battery_percent * 16 / 100 : 0; + for (int y = 0; y < 20; ++y) { + for (int x = 0; x < 24; ++x) { + bool on = x < 22 && (x < 2 || x >= 20 || y < 2 || y >= 18); + on |= x >= 22 && y >= 6 && y < 14; + on |= status.battery_valid && x >= 3 && x < 3 + fill && y >= 3 && y < 17; + /* 充电时在电池中挖出黑色闪电,绿色外框仍可辨识。 */ + if (status.charging_visible && y >= 3 && y < 17 && + x >= 11 - (y - 3) / 3 && x <= 14 - (y - 3) / 3) { + on = false; + } + lcd_direct_hud_pixel(pixels, row_start, rows, icon_x + x, 14 + y, on ? color : 0); + } + } +} + 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) @@ -1367,6 +1508,9 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image, 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); + if (clock_visible) { + lcd_direct_overlay_battery(s_line_buffer, row_start, row_count); + } err = lcd_direct_draw_rows(row_start, row_count); if (err != ESP_OK) { ESP_LOGE(TAG, "write PNG image pixels failed path=%s", image->path); @@ -1772,23 +1916,23 @@ static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *requ TAG, "invalid asset sequence"); +#if CONFIG_TQ_LCD_EMOTE_GFX_TEST + /* 兼容历史基础表情请求(包括服务器错误的有限播放),统一到新眼睛。 */ + if (s_emote_ready && request->duration_ms == 0U && + strcmp(request->asset_dir, "jichubiaoqing") == 0) { + return lcd_direct_play_emote_face_until_pending(request); + } +#endif + if (request->asset_dir[0] == '\0' || lcd_direct_asset_frame(request->asset_dir, request->frame_order_count > 0U ? request->frame_order[0] : 0U) == NULL) { - ESP_LOGW(TAG, "LCD asset sequence unavailable, fallback to basic face asset=%s", request->asset_dir); - const lcd_direct_request_t fallback = { - .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, - .asset_dir = "jichubiaoqing", - .frame_order = {0, 1, 2}, - .frame_order_count = 3, - .repeat_count = 1, - .frame_ms = 100, - }; - esp_err_t fallback_err = lcd_direct_draw_asset_sequence_once(&fallback); - if (fallback_err == ESP_OK && request->repeat_count > 0U) { + ESP_LOGW(TAG, "LCD asset unavailable, restore idle asset=%s", request->asset_dir); + lcd_direct_queue_idle_if_available(); + if (request->repeat_count > 0U || request->duration_ms > 0U) { lcd_direct_signal_completion(); } - return fallback_err; + return ESP_OK; } if (request->duration_ms > 0U) { @@ -1878,7 +2022,7 @@ static esp_err_t lcd_direct_play_basic_face_until_pending(void) { #if CONFIG_TQ_LCD_EMOTE_GFX_TEST if (s_emote_ready) { - return lcd_direct_play_emote_face_until_pending(); + return lcd_direct_play_emote_face_until_pending(NULL); } #endif const lcd_direct_request_t request = { @@ -2083,29 +2227,15 @@ void lcd_direct_start_render_loop(void) draw_err = lcd_direct_play_basic_face_until_pending(); } else if (request.kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { draw_err = lcd_direct_play_asset_sequence(&request); - if (draw_err == ESP_OK && request.return_to_idle) { - lcd_direct_request_t idle = { - .kind = PLATFORM_LCD_PRESENTATION_IDLE, - }; - portENTER_CRITICAL(&s_state_lock); - s_pending = idle; - s_pending_valid = true; - portEXIT_CRITICAL(&s_state_lock); - } } else if (request.kind == PLATFORM_LCD_PRESENTATION_STEP_SEQUENCE) { draw_err = lcd_direct_play_step_sequence(&request); - if (draw_err == ESP_OK && request.return_to_idle) { - lcd_direct_request_t idle = { - .kind = PLATFORM_LCD_PRESENTATION_IDLE, - }; - portENTER_CRITICAL(&s_state_lock); - s_pending = idle; - s_pending_valid = true; - portEXIT_CRITICAL(&s_state_lock); - } } else { draw_err = lcd_direct_draw_test_pattern(&request); } + if ((draw_err == ESP_OK && request.return_to_idle) || + (draw_err != ESP_OK && draw_err != ESP_ERR_INVALID_STATE)) { + lcd_direct_queue_idle_if_available(); + } xSemaphoreGive(s_lock); if (draw_err != ESP_OK && draw_err != ESP_ERR_INVALID_STATE) { ESP_LOGW(TAG, "LCD presentation draw failed: %s", esp_err_to_name(draw_err)); @@ -2187,7 +2317,7 @@ esp_err_t lcd_direct_stop_render_loop(int timeout_ms) esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *overlay) { - if (overlay == NULL) { + if (overlay == NULL || overlay->battery_percent > 100U) { return ESP_ERR_INVALID_ARG; } @@ -2195,7 +2325,9 @@ esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *ove TaskHandle_t render_task = NULL; portENTER_CRITICAL(&s_state_lock); changed = s_status_overlay.low_battery_visible != overlay->low_battery_visible || - s_status_overlay.charging_visible != overlay->charging_visible; + s_status_overlay.charging_visible != overlay->charging_visible || + s_status_overlay.battery_valid != overlay->battery_valid || + s_status_overlay.battery_percent != overlay->battery_percent; s_status_overlay = *overlay; render_task = s_render_task; portEXIT_CRITICAL(&s_state_lock); diff --git a/docs/lcd-idle-status.zh-CN.md b/docs/lcd-idle-status.zh-CN.md new file mode 100644 index 0000000..dbd5b4c --- /dev/null +++ b/docs/lcd-idle-status.zh-CN.md @@ -0,0 +1,44 @@ +# 待机恢复与时间 / 电量状态栏 + +当前实现(2026-10-09)以本文为准,旧 lcd-expressions.md 中的测试图案、 +优先级仲裁描述不代表当前运行行为。 + +## 返回待机 + +- 开机 / 默认空闲、显式 IDLE 请求、有限动画 return_to_idle、缺失素材回退和 + 非取消的播放错误都进入同一个待机后端。 +- CONFIG_TQ_LCD_EMOTE_GFX_TEST 启用且初始化成功时使用单纹理网格眨眼; + 禁用或初始化失败才使用历史三帧 PNG 安全后备。 +- 历史 jichubiaoqing 请求(duration_ms=0)也适配到 GFX。 + 有限播放保留 repeat_count,每轮为 frame_ms 睁眼停留 + 12 个 34ms 眨眼步骤; + 不承诺与旧 PNG 完全相同的持续时间。服务错误因此不再临时退回三帧。 +- 自动恢复只在没有 pending 请求且未停止时排入 IDLE,不能覆盖新来的请求。 +- 其他表情仍为 PNG / step sequence,未改动其图像或动画顺序。 + pending 仍是单槽替换,不是完整优先级状态机。 + +## 状态栏 + +- 左上 HH:MM,沿用 SNTP 和系统时区;未同步显示 --:--。 +- 右上百分比和电池;未知值显示 --%,充电绿色并带闪电缺口,低电量红色。 +- SOC、有效性和充电状态来自现有 domain battery 服务,不从 ADC 重算。 +- GFX 顶部 320x40 RGB565 图层,常驻 25,600 B PSRAM;只在分钟或电池状态 + 改变时使图层失效并刷新,眨眼仍只改变眼睛网格,不解码新 PNG。 +- PNG 表情复用相同点阵 / 电池绘制器,在每帧合成时更新;二维码和 LCD + 边界测试页面不显示状态栏。PNG 无限等待期间不会独立重绘状态栏。 +- 不需要字体包或新图片。旧 PNG 低电量底部告警保留;旧充电 PNG 不再自动 + 叠加,避免与数字电量冲突。GFX 待机低电量以红色电池表示。 + +## 验证 + +`python3 tools/test_lcd_status_hud.py` 从实际源码提取绘制函数,用 Clang +AddressSanitizer / UndefinedBehaviorSanitizer 检查 808 组有效性、充电、 +低电量、0..100% 组合,验证行块和整帧等价、边界及颜色。 + +使用 ESP-IDF v5.5.4 执行 idf.py build。实机需验证: + +1. 联网后 --:-- 变成本地时间,跨分钟更新;未同步不显示伪造时间。 +2. 插拔充电线、电量变化时状态更新,眼睛位置 / 眨眼无变化或残影。 +3. 各有限动画、Wi-Fi 恢复、PTT 结束、OTA 退出及服务错误后回 GFX 待机。 +4. 动画结束同时到达新请求时不被自动 IDLE 覆盖。 + +编译 / 主机测试不等同于上述实机验证。 diff --git a/tools/test_lcd_status_hud.py b/tools/test_lcd_status_hud.py new file mode 100644 index 0000000..7752077 --- /dev/null +++ b/tools/test_lcd_status_hud.py @@ -0,0 +1,68 @@ +from pathlib import Path +import subprocess, tempfile, re +src=(Path(__file__).resolve().parents[1]/'components/platform/src/lcd_direct.c').read_text() +def function(name): + # Locate definition rather than the forward declaration. + matches=list(re.finditer(r'static [^;{}]+\b'+name+r'\([^;{}]*\)\s*\{',src)) + assert len(matches)==1,name + start=matches[0].start(); p=src.index('{',start); depth=1; end=p+1 + while depth: + if src[end]=='{':depth+=1 + if src[end]=='}':depth-=1 + end+=1 + return src[start:end] +table=re.search(r'static const uint8_t s_clock_glyph_rows.*?\n};',src,re.S).group(0) +prefix='''#include +#include +#include +#include +#include +#define LCD_DIRECT_WIDTH 320 +#define LCD_DIRECT_CLOCK_GLYPH_HEIGHT 7 +#define CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES 1 +#define portENTER_CRITICAL(x) ((void)0) +#define portEXIT_CRITICAL(x) ((void)0) +typedef struct {bool low_battery_visible, charging_visible, battery_valid; uint8_t battery_percent;} platform_lcd_status_overlay_t; +static platform_lcd_status_overlay_t s_status_overlay; +#define PLATFORM_LCD_PRESENTATION_IDLE 0 +typedef struct { int kind; } lcd_direct_request_t; +static lcd_direct_request_t s_pending; +static bool s_pending_valid, s_stop_requested; +''' +test='''int main(void) { + uint16_t full[320*240+2], blocks[320*240+2]; + for (unsigned valid=0;valid<2;++valid) + for (unsigned charge=0;charge<2;++charge) + for (unsigned low=0;low<2;++low) + for (unsigned percent=0;percent<=100;++percent) { + s_status_overlay=(platform_lcd_status_overlay_t){low,charge,valid,percent}; + for (unsigned i=0;i<320*240+2;++i) full[i]=blocks[i]=0x1234; + lcd_direct_overlay_battery(full+1,0,240); + for(unsigned row=0;row<240;row+=8) + lcd_direct_overlay_battery(blocks+1+row*320,row,8); + assert(memcmp(full,blocks,sizeof(full))==0); + assert(full[0]==0x1234 && full[320*240+1]==0x1234); + for(unsigned y=0;y<240;++y) for(unsigned x=0;x<320;++x) { + if(x<212 || y<13 || y>=34) assert(full[1+y*320+x]==0x1234); + } + uint16_t expected=lcd_direct_swap_color(charge?0x07e0:low?0xf800:0xffff); + assert(full[1+14*320+288]==expected); + unsigned fill=percent*16/100; + if(valid && !charge && fill) assert(full[1+17*320+291]==expected); + if(!valid) assert(full[1+17*320+291]==0); + } + for (unsigned pending=0;pending<2;++pending) + for (unsigned stopping=0;stopping<2;++stopping) { + s_pending=(lcd_direct_request_t){99}; + s_pending_valid=pending; s_stop_requested=stopping; + lcd_direct_queue_idle_if_available(); + assert(s_pending.kind==((pending || stopping)?99:PLATFORM_LCD_PRESENTATION_IDLE)); + assert(s_pending_valid==(pending || !stopping)); + } + puts("PASS: idle queue preserves pending requests and stop; 808 battery combinations; full-frame/8-row equivalence, bounds, colors and fill"); +}''' +with tempfile.TemporaryDirectory(prefix='lucky-hud-') as tmp: + c=Path(tmp)/'hud.c'; exe=Path(tmp)/'hud' + c.write_text(prefix+table+'\n'+'\n'.join(function(n) for n in ['lcd_direct_swap_color','lcd_direct_clock_glyph_index','lcd_direct_hud_pixel','lcd_direct_overlay_battery','lcd_direct_queue_idle_if_available'])+'\n'+test) + subprocess.run(['clang','-std=c11','-fsanitize=address,undefined','-g',str(c),'-o',str(exe)],check=True) + subprocess.run([str(exe)],check=True)