feat(lcd): use Montserrat clock and battery icon assets
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 HUD 字形子集和无损调色板游程数据;无需运行时分配/解压缓存。
|
||||
|
||||
--import-zip 只导入明确列出的 40x20 PNG,不执行压缩包中的任何内容。
|
||||
原 PNG 保留;优化 PNG 与生成 C 是可再生文件。--check 只校验不写入。
|
||||
"""
|
||||
import argparse
|
||||
import io
|
||||
import re
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
from PIL import Image
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ASSETS = ROOT / 'components/platform/assets/status_hud'
|
||||
OUTPUT = ROOT / 'components/platform/src/generated/lcd_status_assets.c'
|
||||
NAMES = [f'battery_{n}.png' for n in (1, 25, 50, 75, 100)] + [
|
||||
f'battery_charging_{n}.png' for n in (1, 25, 50, 75, 100)]
|
||||
|
||||
def array(name, data):
|
||||
rows = [' ' + ', '.join(f'0x{n:02x}' for n in data[i:i+24]) + ','
|
||||
for i in range(0, len(data), 24)]
|
||||
return f'static const uint8_t {name}[] = {{\n' + '\n'.join(rows) + '\n};\n'
|
||||
|
||||
def generate(check=False):
|
||||
font = (ROOT / 'managed_components/lvgl__lvgl/src/font/lv_font_montserrat_14.c').read_text()
|
||||
assert '.bpp = 4,' in font and '.bitmap_format = 0,' in font
|
||||
assert re.search(r'\.line_height\s*=\s*16\b', font)
|
||||
assert re.search(r'\.base_line\s*=\s*3\b', font)
|
||||
assert '.range_start = 32, .range_length = 95, .glyph_id_start = 1,' in font
|
||||
block = re.search(r'glyph_bitmap\[\]\s*=\s*\{(.*?)\n};', font, re.S)[1]
|
||||
block = re.sub(r'/\*.*?\*/', '', block, flags=re.S)
|
||||
bitmap = bytes(int(n, 16) for n in re.findall(r'0x[0-9a-fA-F]+', block))
|
||||
block = re.search(r'glyph_dsc\[\]\s*=\s*\{(.*?)\n};', font, re.S)[1]
|
||||
glyphs = [dict((key, int(value)) for key, value in re.findall(r'\.(\w+)\s*=\s*(-?\d+)', row))
|
||||
for row in re.findall(r'\{([^{}]+)\}', block)]
|
||||
subset = bytearray(); descriptions = []
|
||||
for c in '0123456789:-':
|
||||
g = glyphs[ord(c)-32+1]
|
||||
count = (g['box_w'] * g['box_h'] + 1) // 2
|
||||
raw = bitmap[g['bitmap_index']:g['bitmap_index']+count]
|
||||
assert len(raw) == count
|
||||
descriptions.append(f" {{{len(subset)}, {(g['adv_w']+8)//16}, {g['box_w']}, {g['box_h']}, {g['ofs_x']}, {g['ofs_y']}}}, /* {c} */")
|
||||
subset.extend(raw)
|
||||
text = ('/* Generated by tools/prepare_status_hud_assets.py; do not edit.\n'
|
||||
' * Font subset derived from LVGL Montserrat 14, Copyright (c) 2025 LVGL Kft.\n'
|
||||
' * See components/platform/assets/status_hud/LVGL-LICENSE.txt. */\n'
|
||||
'#include "lcd_status_assets.h"\n\n')
|
||||
text += array('lcd_status_font_bitmap', subset).replace('static const', 'const')
|
||||
text += 'const lcd_status_glyph_t lcd_status_font_glyphs[12] = {\n' + '\n'.join(descriptions) + '\n};\n'
|
||||
payload = 0
|
||||
for i, name in enumerate(NAMES):
|
||||
source = (ASSETS / 'battery_source' / name).read_bytes()
|
||||
image = Image.open(io.BytesIO(source)).convert('RGBA')
|
||||
assert image.size == (40, 20), name
|
||||
out = io.BytesIO(); image.save(out, format='PNG', optimize=True, compress_level=9)
|
||||
optimized = min((source, out.getvalue()), key=len)
|
||||
assert Image.open(io.BytesIO(optimized)).convert('RGBA').tobytes() == image.tobytes()
|
||||
destination = ASSETS / 'battery_optimized' / name
|
||||
if check:
|
||||
assert destination.read_bytes() == optimized, name
|
||||
else:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True); destination.write_bytes(optimized)
|
||||
rgba = image.tobytes()
|
||||
pixels = [(((rgba[j] >> 3) << 11) | ((rgba[j+1] >> 2) << 5) | (rgba[j+2] >> 3), rgba[j+3])
|
||||
for j in range(0, len(rgba), 4)]
|
||||
palette = list(dict.fromkeys(pixels)); assert len(palette) <= 256
|
||||
indices = {p: n for n, p in enumerate(palette)}; runs = bytearray()
|
||||
for p in pixels:
|
||||
index = indices[p]
|
||||
if runs and runs[-1] == index and runs[-2] < 255: runs[-2] += 1
|
||||
else: runs.extend((1, index))
|
||||
decoded = [palette[runs[j+1]] for j in range(0, len(runs), 2) for _ in range(runs[j])]
|
||||
assert decoded == pixels, name
|
||||
pal = bytes(v for color, alpha in palette for v in (color >> 8, color & 255, alpha))
|
||||
text += array(f'battery_palette_{i}', pal) + array(f'battery_runs_{i}', runs)
|
||||
payload += len(pal)+len(runs)
|
||||
print(f'{name}: PNG {len(source)} -> {len(optimized)} B; indexed RLE {len(pal)+len(runs)} B')
|
||||
text += 'const lcd_status_icon_t lcd_status_battery_icons[10] = {\n'
|
||||
text += ''.join(f' {{battery_palette_{i}, battery_runs_{i}, sizeof(battery_runs_{i})}},\n' for i in range(10)) + '};\n'
|
||||
if check: assert OUTPUT.read_text() == text
|
||||
else: OUTPUT.parent.mkdir(parents=True, exist_ok=True); OUTPUT.write_text(text)
|
||||
print(f'Font bitmap {len(subset)} B; battery payload {payload} B; no runtime pixel allocations')
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(); parser.add_argument('--import-zip', type=Path); parser.add_argument('--check', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if args.import_zip:
|
||||
assert not args.check
|
||||
with ZipFile(args.import_zip) as archive:
|
||||
for name in NAMES:
|
||||
data = archive.read('out/40x20/' + name)
|
||||
image = Image.open(io.BytesIO(data)); assert image.size == (40, 20)
|
||||
path = ASSETS / 'battery_source' / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True); path.write_bytes(data)
|
||||
generate(args.check)
|
||||
@@ -1,68 +1,120 @@
|
||||
"""实际 HUD 函数与生成资产的 ASan/UBSan 回归;不启动 LVGL/GFX 引擎。"""
|
||||
from pathlib import Path
|
||||
import subprocess, tempfile, re
|
||||
src=(Path(__file__).resolve().parents[1]/'components/platform/src/lcd_direct.c').read_text()
|
||||
import subprocess
|
||||
import tempfile
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
src = (ROOT/'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
|
||||
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
|
||||
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 <stdint.h>
|
||||
prefix = '''#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#define LCD_DIRECT_WIDTH 320
|
||||
#define LCD_DIRECT_CLOCK_GLYPH_HEIGHT 7
|
||||
#define CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES 1
|
||||
#include "lcd_status_assets.h"
|
||||
#define LCD_DIRECT_CLOCK_X 12U
|
||||
#define LCD_DIRECT_CLOCK_Y 14U
|
||||
#define LCD_DIRECT_CLOCK_TEXT_LENGTH 5U
|
||||
#define LCD_DIRECT_CLOCK_COLOR 0xffffU
|
||||
#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0U
|
||||
#define LCD_DIRECT_BATTERY_WIDTH 40U
|
||||
#define LCD_DIRECT_BATTERY_HEIGHT 20U
|
||||
#define LCD_DIRECT_BATTERY_X (LCD_DIRECT_WIDTH - 48U)
|
||||
#define LCD_DIRECT_BATTERY_Y 12U
|
||||
#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;
|
||||
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];
|
||||
test = '''int main(int argc, char **argv) {
|
||||
uint16_t full[LCD_DIRECT_WIDTH*240+2], blocks[LCD_DIRECT_WIDTH*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) {
|
||||
for (unsigned percent=0;percent<=255;++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;
|
||||
for (unsigned i=0;i<LCD_DIRECT_WIDTH*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);
|
||||
lcd_direct_overlay_battery(blocks+1+row*LCD_DIRECT_WIDTH,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);
|
||||
assert(full[0]==0x1234 && full[LCD_DIRECT_WIDTH*240+1]==0x1234);
|
||||
unsigned level=percent<=10?0:percent<=25?1:percent<=50?2:percent<=75?3:4;
|
||||
assert(lcd_direct_battery_icon_index(percent,charge)==level+(charge?5:0));
|
||||
for(unsigned y=0;y<240;++y) for(unsigned x=0;x<LCD_DIRECT_WIDTH;++x) {
|
||||
if(!valid || x<LCD_DIRECT_BATTERY_X || x>=LCD_DIRECT_BATTERY_X+40 || y<12 || y>=32)
|
||||
assert(full[1+y*LCD_DIRECT_WIDTH+x]==0x1234);
|
||||
}
|
||||
if(valid) {
|
||||
const lcd_status_icon_t *icon=&lcd_status_battery_icons[level+(charge?5:0)];
|
||||
unsigned p=0;
|
||||
for(unsigned r=0;r<icon->run_bytes;r+=2) {
|
||||
const uint8_t *value=icon->palette+icon->runs[r+1]*3;
|
||||
for(unsigned n=0;n<icon->runs[r];++n,++p) {
|
||||
unsigned index=1+(12+p/40)*LCD_DIRECT_WIDTH+LCD_DIRECT_BATTERY_X+p%40;
|
||||
uint16_t color=((uint16_t)value[0]<<8)|value[1];
|
||||
assert(full[index]==lcd_direct_swap_color(lcd_direct_blend_rgb565(lcd_direct_swap_color(0x1234),color,value[2])));
|
||||
}
|
||||
}
|
||||
assert(p==800);
|
||||
}
|
||||
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;
|
||||
for(unsigned hour=0;hour<24;++hour) for(unsigned minute=0;minute<60;++minute) {
|
||||
char text[6]; snprintf(text,sizeof(text),"%02u:%02u",hour,minute);
|
||||
for(unsigned i=0;i<LCD_DIRECT_WIDTH*240+2;++i) full[i]=blocks[i]=0x1234;
|
||||
lcd_direct_overlay_clock(full+1,0,240,text);
|
||||
for(unsigned row=0;row<240;row+=8)
|
||||
lcd_direct_overlay_clock(blocks+1+row*LCD_DIRECT_WIDTH,row,8,text);
|
||||
assert(memcmp(full,blocks,sizeof(full))==0);
|
||||
assert(full[0]==0x1234 && full[LCD_DIRECT_WIDTH*240+1]==0x1234);
|
||||
for(unsigned y=0;y<240;++y) for(unsigned x=0;x<LCD_DIRECT_WIDTH;++x)
|
||||
if(x<12 || x>=64 || y<14 || y>=31) assert(full[1+y*LCD_DIRECT_WIDTH+x]==0x1234);
|
||||
}
|
||||
lcd_direct_overlay_clock(full+1,0,240,"--:--");
|
||||
lcd_direct_overlay_clock(NULL,0,240,"--:--");
|
||||
lcd_direct_overlay_clock(full+1,0,240,NULL);
|
||||
lcd_direct_overlay_battery(NULL,0,240);
|
||||
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");
|
||||
if(argc==2) {
|
||||
memset(full,0,sizeof(full));
|
||||
lcd_direct_overlay_clock(full,0,240,"12:34");
|
||||
s_status_overlay=(platform_lcd_status_overlay_t){false,true,true,50};
|
||||
lcd_direct_overlay_battery(full,0,240);
|
||||
FILE *f=fopen(argv[1],"wb"); assert(f); fwrite(full,2,LCD_DIRECT_WIDTH*240,f); fclose(f);
|
||||
}
|
||||
puts("PASS: 2048 battery states, all 1440 times, alpha/background, byte order, 8-row equivalence, bounds and idle guards");
|
||||
}'''
|
||||
names = ['lcd_direct_swap_color', 'lcd_direct_rgb888_to_rgb565', 'lcd_direct_blend_rgb565', 'lcd_direct_clock_glyph_index',
|
||||
'lcd_direct_hud_blend_pixel', 'lcd_direct_draw_clock_pass', 'lcd_direct_overlay_clock',
|
||||
'lcd_direct_battery_icon_index', 'lcd_direct_overlay_battery', 'lcd_direct_queue_idle_if_available']
|
||||
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)
|
||||
c = Path(tmp)/'hud.c'; exe = Path(tmp)/'hud'
|
||||
c.write_text(prefix+'\n'+'\n'.join(function(n) for n in names)+'\n'+test)
|
||||
for width in (320, 240):
|
||||
for swap in (0, 1):
|
||||
subprocess.run(['clang','-std=c11','-fsanitize=address,undefined','-g',
|
||||
f'-DLCD_DIRECT_WIDTH={width}', f'-DCONFIG_TQ_SCREEN_SWAP_COLOR_BYTES={swap}',
|
||||
'-I'+str(ROOT/'components/platform/internal'),str(c),
|
||||
str(ROOT/'components/platform/src/generated/lcd_status_assets.c'),
|
||||
'-o',str(exe)],check=True)
|
||||
preview = ['/tmp/lucky-status-hud.rgb565'] if width == 320 and swap == 0 else []
|
||||
subprocess.run([str(exe), *preview],check=True)
|
||||
|
||||
Reference in New Issue
Block a user