From 30dc48086ce95229eeb17bcb72a138b43d090387 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 4 Mar 2026 13:50:20 -0500 Subject: [PATCH] Add splash screen icon and build-time SVG-to-RGB565 generator - pyxis-icon.svg: Pyxis constellation icon (3 stars with connecting lines) - generate_splash.py: PlatformIO pre-build script that renders the SVG to a 160x160 RGB565 PROGMEM header (SplashImage.h) using cairosvg + Pillow - .gitignore: Exclude generated SplashImage.h - platformio.ini: Add generate_splash.py to both build environments Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + generate_splash.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++ platformio.ini | 5 ++- pyxis-icon.svg | 21 ++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 generate_splash.py create mode 100644 pyxis-icon.svg diff --git a/.gitignore b/.gitignore index 873aa97..c6c6a74 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .vscode/ *.pyc docs/flasher/firmware/ +lib/tdeck_ui/Hardware/TDeck/SplashImage.h diff --git a/generate_splash.py b/generate_splash.py new file mode 100644 index 0000000..46d5da8 --- /dev/null +++ b/generate_splash.py @@ -0,0 +1,98 @@ +""" +PlatformIO pre-build script: Convert pyxis-icon.svg to RGB565 PROGMEM header. + +Generates lib/tdeck_ui/Hardware/TDeck/SplashImage.h containing a 160x160 +pixel splash image as a PROGMEM byte array. Skips regeneration if the +header is already newer than the source SVG. + +Requires: cairosvg, Pillow (gracefully skips if not installed) +""" + +Import("env") + +import os +import struct + +ICON_SIZE = 160 +SVG_FILE = "pyxis-icon.svg" +HEADER_FILE = os.path.join("lib", "tdeck_ui", "Hardware", "TDeck", "SplashImage.h") +BG_COLOR = (0x1D, 0x1A, 0x1E) # #1D1A1E — matches fill_screen in show_splash() + + +def rgb888_to_rgb565(r, g, b): + """Convert 8-bit RGB to 16-bit RGB565 (big-endian bytes).""" + rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) + return struct.pack(">H", rgb565) + + +def should_regenerate(svg_path, header_path): + """Return True if the header needs regeneration.""" + if not os.path.exists(header_path): + return True + return os.path.getmtime(svg_path) > os.path.getmtime(header_path) + + +project_dir = env.get("PROJECT_DIR", os.getcwd()) +svg_path = os.path.join(project_dir, SVG_FILE) +header_path = os.path.join(project_dir, HEADER_FILE) + +if not os.path.exists(svg_path): + print(f"[generate_splash] {SVG_FILE} not found, skipping splash generation") +elif not should_regenerate(svg_path, header_path): + print(f"[generate_splash] {HEADER_FILE} is up-to-date, skipping") +else: + try: + import cairosvg + from PIL import Image + import io + except ImportError as e: + print(f"[generate_splash] Missing dependency ({e}), skipping splash generation") + print(f"[generate_splash] Install with: pip install cairosvg Pillow") + else: + print(f"[generate_splash] Rendering {SVG_FILE} -> {HEADER_FILE} ({ICON_SIZE}x{ICON_SIZE} RGB565)") + + # Render SVG to PNG at target size + png_data = cairosvg.svg2png( + url=svg_path, + output_width=ICON_SIZE, + output_height=ICON_SIZE, + ) + + img = Image.open(io.BytesIO(png_data)).convert("RGBA") + + # Composite onto background color (handles transparency) + bg = Image.new("RGBA", (ICON_SIZE, ICON_SIZE), BG_COLOR + (255,)) + bg.paste(img, (0, 0), img) + img = bg.convert("RGB") + + # Convert to RGB565 big-endian byte array + pixels = list(img.getdata()) + rgb565_bytes = bytearray() + for r, g, b in pixels: + rgb565_bytes.extend(rgb888_to_rgb565(r, g, b)) + + # Write C header + os.makedirs(os.path.dirname(header_path), exist_ok=True) + + with open(header_path, "w") as f: + f.write("// AUTO-GENERATED by generate_splash.py — do not edit\n") + f.write("#ifndef SPLASH_IMAGE_H\n") + f.write("#define SPLASH_IMAGE_H\n\n") + f.write("#include \n\n") + f.write(f"#define SPLASH_WIDTH {ICON_SIZE}\n") + f.write(f"#define SPLASH_HEIGHT {ICON_SIZE}\n") + f.write(f"#define HAS_SPLASH_IMAGE 1\n\n") + f.write(f"// {ICON_SIZE}x{ICON_SIZE} RGB565 big-endian ({len(rgb565_bytes)} bytes)\n") + f.write("static const uint8_t PROGMEM splash_image[] = {\n") + + # Write bytes, 16 per line + for i in range(0, len(rgb565_bytes), 16): + chunk = rgb565_bytes[i:i+16] + hex_str = ", ".join(f"0x{b:02X}" for b in chunk) + trailing = "," if i + 16 < len(rgb565_bytes) else "" + f.write(f" {hex_str}{trailing}\n") + + f.write("};\n\n") + f.write("#endif // SPLASH_IMAGE_H\n") + + print(f"[generate_splash] Generated {len(rgb565_bytes)} bytes ({ICON_SIZE}x{ICON_SIZE} RGB565)") diff --git a/platformio.ini b/platformio.ini index bce445e..a7f8778 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,8 @@ ; T-Deck environment using Bluedroid BLE stack (fallback, uses more RAM) [env:tdeck-bluedroid] -extra_scripts = pre:version.py +extra_scripts = + pre:version.py + pre:generate_splash.py platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino @@ -79,6 +81,7 @@ build_flags = [env:tdeck] extra_scripts = pre:version.py + pre:generate_splash.py pre:patch_nimble.py platform = espressif32 board = esp32-s3-devkitc-1 diff --git a/pyxis-icon.svg b/pyxis-icon.svg new file mode 100644 index 0000000..87e9d51 --- /dev/null +++ b/pyxis-icon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file