From 8a638acdd89f435c48e030ad4f09a2ff108d41f7 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 14 Feb 2026 23:47:21 +0100 Subject: [PATCH] Skin template system, separate prev sprite, reorganize device scripts Add tools/gen_skin_template.py to generate a labeled 642x420 PNG template for creating custom spritesheets. Move rg35xx device scripts from tools/ to device/rg35xx/. Point prev_sprite at its own cell (bottom-center) so Prev and Next can have distinct icons. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- assets/skin_template.png | Bin 0 -> 3931 bytes .../rg35xx}/rg35xx-screen-monitor.py | 0 {tools => device/rg35xx}/rg35xx-wrapper.sh | 0 docs/rg35xx-plus.md | 6 +- docs/sdlamp2-fsd.md | 6 ++ src/sdlamp2.c | 2 +- tools/gen_skin_template.py | 96 ++++++++++++++++++ 8 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 assets/skin_template.png rename {tools => device/rg35xx}/rg35xx-screen-monitor.py (100%) rename {tools => device/rg35xx}/rg35xx-wrapper.sh (100%) create mode 100644 tools/gen_skin_template.py diff --git a/CLAUDE.md b/CLAUDE.md index e471005..7d48925 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ No test suite, no linter. ## Architecture -Single-file C program: `src/sdlamp2.c` (~650 lines). One generated header: `src/controls_png.h` (embedded PNG byte array — regenerate with `./tools/embed_png.py assets/controls.png src/controls_png.h` if the spritesheet changes). +Single-file C program: `src/sdlamp2.c` (~650 lines). One generated header: `src/controls_png.h` (embedded PNG byte array — regenerate with `python3 tools/embed_png.py assets/controls.png src/controls_png.h` if the spritesheet changes). Skin template: `python3 tools/gen_skin_template.py [output.png]` generates a labeled grid template for creating custom spritesheets (requires Pillow). Device-specific scripts live in `device/rg35xx/`. Key sections in order: - **Decoder struct** — holds all FFmpeg state (format/codec contexts, swr resampler, album art texture) diff --git a/assets/skin_template.png b/assets/skin_template.png new file mode 100644 index 0000000000000000000000000000000000000000..3ebd3812ce5785c3c03202407f8758eb3f9c728e GIT binary patch literal 3931 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 R;Lr>??S1ZK`Hw%{{67+wZ#)11 literal 0 HcmV?d00001 diff --git a/tools/rg35xx-screen-monitor.py b/device/rg35xx/rg35xx-screen-monitor.py similarity index 100% rename from tools/rg35xx-screen-monitor.py rename to device/rg35xx/rg35xx-screen-monitor.py diff --git a/tools/rg35xx-wrapper.sh b/device/rg35xx/rg35xx-wrapper.sh similarity index 100% rename from tools/rg35xx-wrapper.sh rename to device/rg35xx/rg35xx-wrapper.sh diff --git a/docs/rg35xx-plus.md b/docs/rg35xx-plus.md index b986759..d033fb0 100644 --- a/docs/rg35xx-plus.md +++ b/docs/rg35xx-plus.md @@ -143,8 +143,8 @@ The `dmenu_ln` script already supports switching the startup binary via config f ```sh scp build/sdlamp2 root@rg35xx:/mnt/vendor/bin/sdlamp2 - scp tools/rg35xx-wrapper.sh root@rg35xx:/mnt/vendor/bin/rg35xx-wrapper.sh - scp tools/rg35xx-screen-monitor.py root@rg35xx:/mnt/vendor/bin/rg35xx-screen-monitor.py + scp device/rg35xx/rg35xx-wrapper.sh root@rg35xx:/mnt/vendor/bin/rg35xx-wrapper.sh + scp device/rg35xx/rg35xx-screen-monitor.py root@rg35xx:/mnt/vendor/bin/rg35xx-screen-monitor.py ``` 2. **Add the config check** to `/mnt/vendor/ctrl/dmenu_ln`. In the section where `CMD` overrides are checked (after the existing `muos.ini` / `vpRun.ini` checks, before the `app_scheduling` call), add: @@ -155,7 +155,7 @@ The `dmenu_ln` script already supports switching the startup binary via config f fi ``` - The wrapper script handles device-specific concerns (WiFi hotspot, power button monitoring) and launches sdlamp2 as its main foreground process. See `tools/rg35xx-wrapper.sh` for details. + The wrapper script handles device-specific concerns (WiFi hotspot, power button monitoring) and launches sdlamp2 as its main foreground process. See `device/rg35xx/rg35xx-wrapper.sh` for details. 3. **Enable sdlamp2 on boot:** diff --git a/docs/sdlamp2-fsd.md b/docs/sdlamp2-fsd.md index aa174e2..ded41ea 100644 --- a/docs/sdlamp2-fsd.md +++ b/docs/sdlamp2-fsd.md @@ -43,6 +43,12 @@ This document specifies the functional requirements for an SDL2 based media play ## 6. Changelog +### 2026-02-14 — Skin template system and device script reorganization + +- **Skin template generator**: New `tools/gen_skin_template.py` (requires Pillow) generates a 642x420 PNG template showing the sprite grid layout with labeled gutters. Skin creators can draw over the white 200x200 cells; the 20px gray gutters (never rendered by the app) identify each cell's purpose. +- **Separate Prev sprite**: `prev_sprite` now uses the bottom-center cell `{220, 220}` instead of sharing the bottom-right cell with `next_sprite`. This gives Prev and Next distinct sprites in the spritesheet. +- **Device scripts moved**: `rg35xx-wrapper.sh` and `rg35xx-screen-monitor.py` moved from `tools/` to `device/rg35xx/`, separating device-specific scripts from dev tools. + ### 2026-02-14 — Softer background, remove panel divider - **Background color**: Changed from white (`#FFFFFF`) to a medium gray (`#979797`) for a gentler appearance. diff --git a/src/sdlamp2.c b/src/sdlamp2.c index d9db19e..126a555 100644 --- a/src/sdlamp2.c +++ b/src/sdlamp2.c @@ -612,7 +612,7 @@ int main(int argc, char** argv) { const SDL_Rect play_sprite = {220, 0, 200, 200}; const SDL_Rect ff_sprite = {440, 0, 200, 200}; const SDL_Rect stop_sprite = {0, 220, 200, 200}; - const SDL_Rect prev_sprite = {440, 220, 200, 200}; + const SDL_Rect prev_sprite = {220, 220, 200, 200}; const SDL_Rect next_sprite = {440, 220, 200, 200}; const SDL_Rect circle_sprite = {440, 220, 200, 200}; /* placeholder for no-art */ diff --git a/tools/gen_skin_template.py b/tools/gen_skin_template.py new file mode 100644 index 0000000..dd58a1a --- /dev/null +++ b/tools/gen_skin_template.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +gen_skin_template.py — Generate a skin template PNG for sdlamp2. + +Creates a 642x420 PNG showing the sprite grid layout with labeled gutters. +Each 200x200 cell is blank white (ready to draw on). The 20px gutters between +cells are gray with small text labels identifying adjacent cells. + +Grid layout: + Col 0 (0-199) Col 1 (220-419) Col 2 (440-639) + Row 0: REWIND PLAY FF + ----gutter y=200-219 with labels---- + Row 1: STOP PREV NEXT + +Two extra pixels on the right (640-641) are gutter fill to reach 642px width, +matching the spritesheet dimensions. + +Usage: python3 tools/gen_skin_template.py [output.png] + +Requires Pillow. +""" +import sys + +from PIL import Image, ImageDraw, ImageFont + +CELL = 200 +GAP = 20 +COLS = 3 +ROWS = 2 +WIDTH = COLS * CELL + (COLS - 1) * GAP + 2 # 642 +HEIGHT = ROWS * CELL + (ROWS - 1) * GAP # 420 + +CELL_COLOR = (255, 255, 255) +GUTTER_COLOR = (180, 180, 180) +TEXT_COLOR = (80, 80, 80) + +LABELS = [ + ["REWIND", "PLAY", "FF"], + ["STOP", "PREV", "NEXT"], +] + + +def cell_x(col): + return col * (CELL + GAP) + + +def cell_y(row): + return row * (CELL + GAP) + + +def main(): + output_path = sys.argv[1] if len(sys.argv) > 1 else "skin_template.png" + + img = Image.new("RGB", (WIDTH, HEIGHT), GUTTER_COLOR) + draw = ImageDraw.Draw(img) + + # Try to load a small font for labels + try: + font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11) + except OSError: + try: + font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 11) + except OSError: + font = ImageFont.load_default() + + # Draw white cells + for row in range(ROWS): + for col in range(COLS): + x = cell_x(col) + y = cell_y(row) + draw.rectangle([x, y, x + CELL - 1, y + CELL - 1], fill=CELL_COLOR) + + # Label the horizontal gutter (y = 200..219) + gutter_y = CELL # 200 + for col in range(COLS): + cx = cell_x(col) + CELL // 2 + + # Row 0 label above center of gutter + label_0 = LABELS[0][col] + bbox = draw.textbbox((0, 0), label_0, font=font) + tw = bbox[2] - bbox[0] + draw.text((cx - tw // 2, gutter_y + 1), label_0, fill=TEXT_COLOR, font=font) + + # Row 1 label below center of gutter + label_1 = LABELS[1][col] + bbox = draw.textbbox((0, 0), label_1, font=font) + tw = bbox[2] - bbox[0] + th = bbox[3] - bbox[1] + draw.text((cx - tw // 2, gutter_y + GAP - th - 2), label_1, fill=TEXT_COLOR, font=font) + + img.save(output_path) + print(f"Skin template saved to {output_path} ({WIDTH}x{HEIGHT})") + + +if __name__ == "__main__": + main()