feat(lcd): use Montserrat clock and battery icon assets

This commit is contained in:
chenjintao
2026-10-10 10:12:11 +08:00
parent 22f5ac70a2
commit bf06618848
31 changed files with 970 additions and 187 deletions
+79 -133
View File
@@ -8,6 +8,7 @@
#include "lcd_direct.h"
#include "lcd_direct_assets.h"
#include "platform_asset_bundle.h"
#include "lcd_status_assets.h"
#include <stdbool.h>
#include <stdint.h>
@@ -87,17 +88,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
/* 5x7 点阵放大4倍,24像素步进为字形留出间隔;颜色为 RGB565 白字黑描边。 */
/* Montserrat 14 子集直接合成,无 label 字形缓存或新增动态缓冲。 */
#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_Y 14U
#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_DIRECT_CLOCK_COLOR 0xFFFFU
#define LCD_DIRECT_BATTERY_WIDTH 40U
#define LCD_DIRECT_BATTERY_HEIGHT 20U
#define LCD_DIRECT_BATTERY_X (LCD_DIRECT_WIDTH - LCD_DIRECT_BATTERY_WIDTH - 8U)
#define LCD_DIRECT_BATTERY_Y 12U
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
#define LCD_DIRECT_HUD_ROWS 40U
@@ -202,21 +202,6 @@ 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;
@@ -646,7 +631,7 @@ static bool lcd_direct_should_stop_idle(void)
return stop;
}
/* 调用方持有 GFX 锁;字形复用 PNG 路径,无字体包和额外 PNG 解码。 */
/* 调用方持有 GFX 锁;两种后端共用 Montserrat 子集和电池资产,无动态解码缓存。 */
static esp_err_t lcd_direct_emote_update_hud(bool force, bool *changed)
{
if (changed != NULL) {
@@ -1745,7 +1730,7 @@ static size_t lcd_direct_append_status_icons(const lcd_direct_image_t *image,
.y = LCD_DIRECT_LOW_BATTERY_ICON_Y,
};
}
/* 充电状态改由右上角程序绘制的电池图标表示,避免和百分比重叠。 */
/* 充电状态由右上角新电池素材表示;保留底部低电量提醒。 */
return icon_count;
}
@@ -1837,131 +1822,92 @@ static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U])
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)
/* 输入/输出沿用 PNG 行缓冲字节序;GFX HUD 在提交前统一转换。 */
static void lcd_direct_hud_blend_pixel(uint16_t *pixels, uint16_t row_start,
uint16_t rows, int x, int y,
uint16_t color, uint8_t alpha)
{
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])
{
/* 时钟作为最终覆盖层绘制,保证 presentation 与状态图标不会遮住时间。 */
if (pixel_buffer == NULL || text == NULL) {
if (pixels == NULL || alpha == 0 || x < 0 || x >= (int)LCD_DIRECT_WIDTH ||
y < (int)row_start || y >= (int)row_start + (int)rows) {
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);
const size_t index = (size_t)(y - row_start) * LCD_DIRECT_WIDTH + (size_t)x;
const uint16_t background = lcd_direct_swap_color(pixels[index]);
pixels[index] = lcd_direct_swap_color(lcd_direct_blend_rgb565(background, color, alpha));
}
/* 小型电池 + 数字百分比,沿用 5x7 点阵;所有写入都裁剪到当前行块。 */
static void lcd_direct_hud_pixel(uint16_t *pixels, uint16_t row_start,
uint16_t rows, int x, int y, uint16_t color)
static void lcd_direct_draw_clock_pass(uint16_t *pixels, uint16_t row_start,
uint16_t rows,
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U],
int x_offset, int y_offset, 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);
int cursor = LCD_DIRECT_CLOCK_X;
for (size_t i = 0; i < LCD_DIRECT_CLOCK_TEXT_LENGTH; ++i) {
const lcd_status_glyph_t *glyph = &lcd_status_font_glyphs[lcd_direct_clock_glyph_index(text[i])];
/* 原字体 line_height=16、base_line=3;不缩放,保留 4-bit 抗锯齿。 */
const int top = (int)LCD_DIRECT_CLOCK_Y + 16 - 3 - glyph->height - glyph->y;
for (unsigned y = 0; y < glyph->height; ++y) {
for (unsigned x = 0; x < glyph->width; ++x) {
const size_t pixel = y * glyph->width + x;
const uint8_t packed = lcd_status_font_bitmap[glyph->offset + pixel / 2];
const uint8_t alpha = ((pixel & 1U) ? packed & 15U : packed >> 4) * 17U;
lcd_direct_hud_blend_pixel(pixels, row_start, rows,
cursor + glyph->x + x_offset + (int)x,
top + y_offset + (int)y, color, alpha);
}
}
cursor += glyph->advance;
}
}
static void lcd_direct_overlay_clock(uint16_t *pixels, uint16_t row_start,
uint16_t rows,
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U])
{
if (pixels == NULL || text == NULL ||
(int)row_start >= (int)LCD_DIRECT_CLOCK_Y + 17 ||
(int)row_start + rows <= (int)LCD_DIRECT_CLOCK_Y) {
return;
}
lcd_direct_draw_clock_pass(pixels, row_start, rows, text, 1, 1, LCD_DIRECT_CLOCK_OUTLINE_COLOR);
lcd_direct_draw_clock_pass(pixels, row_start, rows, text, 0, 0, LCD_DIRECT_CLOCK_COLOR);
}
/* 档位向上分组:0..10→1,11..25→25,26..50→50,51..75→75,76..100→100。 */
static unsigned lcd_direct_battery_icon_index(uint8_t percent, bool charging)
{
unsigned level = percent <= 10 ? 0 : percent <= 25 ? 1 :
percent <= 50 ? 2 : percent <= 75 ? 3 : 4;
return level + (charging ? 5U : 0U);
}
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);
/* 素材没有未知电量图标;读取有效电量前不显示,避免暗示错误档位。 */
if (pixels == NULL || !status.battery_valid ||
(int)row_start >= (int)(LCD_DIRECT_BATTERY_Y + LCD_DIRECT_BATTERY_HEIGHT) ||
(int)row_start + rows <= (int)LCD_DIRECT_BATTERY_Y) {
return;
}
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 lcd_status_icon_t *icon = &lcd_status_battery_icons[
lcd_direct_battery_icon_index(status.battery_percent, status.charging_visible)];
size_t pixel = 0;
/* 从 Flash 流式读 palette/RLE,保留源图 Alpha,不创建解码图缓存。 */
for (size_t run = 0; run + 1 < icon->run_bytes; run += 2) {
const uint8_t *value = icon->palette + (size_t)icon->runs[run + 1] * 3;
const uint16_t color = ((uint16_t)value[0] << 8) | value[1];
for (unsigned count = 0; count < icon->runs[run]; ++count, ++pixel) {
if (pixel >= LCD_DIRECT_BATTERY_WIDTH * LCD_DIRECT_BATTERY_HEIGHT) {
return;
}
}
}
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);
lcd_direct_hud_blend_pixel(pixels, row_start, rows,
LCD_DIRECT_BATTERY_X + pixel % LCD_DIRECT_BATTERY_WIDTH,
LCD_DIRECT_BATTERY_Y + pixel / LCD_DIRECT_BATTERY_WIDTH,
color, value[2]);
}
}
}