From 06daec791e1dc9c281a95125000aa92deb7bd494 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Feb 2026 21:07:03 +0100 Subject: [PATCH] Move shutdown screen to wrapper, reuse stock firmware goodbye.png The in-app shutdown visual didn't work because SDL cleanup wiped the framebuffer. Instead of hacking around that, move the shutdown display to the device wrapper where it belongs. The wrapper now decodes the stock firmware's goodbye.png with Python3+PIL and writes raw BGRA pixels directly to /dev/fb0 before calling poweroff. Co-Authored-By: Claude Opus 4.6 --- docs/rg35xx-plus.md | 69 +++++++++++++++++++++++++++-------------- docs/sdlamp2-fsd.md | 6 ++-- src/sdlamp2.c | 4 ++- tools/rg35xx-wrapper.sh | 38 ++++++++++++++++++++--- 4 files changed, 86 insertions(+), 31 deletions(-) diff --git a/docs/rg35xx-plus.md b/docs/rg35xx-plus.md index a0f11af..9d12487 100644 --- a/docs/rg35xx-plus.md +++ b/docs/rg35xx-plus.md @@ -22,14 +22,14 @@ Device-specific reference for the target hardware. For build instructions see [` All required shared libraries are pre-installed. Most are at `/usr/lib/`, some at `/usr/lib/aarch64-linux-gnu/` (Debian multiarch path): -| Library | Soname | Notes | -|---------|--------|-------| -| SDL2 | `libSDL2-2.0.so.0.12.0` | SDL 2.0.12 | -| SDL2_image | `libSDL2_image-2.0.so.0.900.0` | SDL2_image 2.0.9, at multiarch path | -| libavcodec | `libavcodec.so.58` | FFmpeg ~4.x | -| libavformat | `libavformat.so.58` | FFmpeg ~4.x | -| libavutil | `libavutil.so.56` | FFmpeg ~4.x | -| libswresample | `libswresample.so.3` | FFmpeg ~4.x | +| Library | Soname | Notes | +| :------------ | :----------------------------- | :---------------------------------- | +| SDL2 | `libSDL2-2.0.so.0.12.0` | SDL 2.0.12 | +| SDL2_image | `libSDL2_image-2.0.so.0.900.0` | SDL2_image 2.0.9, at multiarch path | +| libavcodec | `libavcodec.so.58` | FFmpeg ~4.x | +| libavformat | `libavformat.so.58` | FFmpeg ~4.x | +| libavutil | `libavutil.so.56` | FFmpeg ~4.x | +| libswresample | `libswresample.so.3` | FFmpeg ~4.x | Shared libraries are already present — no need to bundle or build them. A native aarch64 compile (e.g. inside the arm64 Docker container) produces working binaries. Must link against glibc ≤ 2.35. @@ -37,24 +37,24 @@ Shared libraries are already present — no need to bundle or build them. A nati Three input devices are registered via `/proc/bus/input/devices`: -| Device | Handlers | Purpose | -|--------|----------|---------| -| `axp2202-pek` | `event0` | Power button (`KEY_POWER`, code 116) | -| `ANBERNIC-keys` | `event1`, `js0` | Gamepad (d-pad, face buttons) | -| `dierct-keys-polled` | `event2` | Shoulder buttons, menu/function keys | +| Device | Handlers | Purpose | +| :------------------- | :-------------- | :----------------------------------- | +| `axp2202-pek` | `event0` | Power button (`KEY_POWER`, code 116) | +| `ANBERNIC-keys` | `event1`, `js0` | Gamepad (d-pad, face buttons) | +| `dierct-keys-polled` | `event2` | Shoulder buttons, menu/function keys | - **logind**: `HandlePowerKey=ignore` in `/etc/systemd/logind.conf` — systemd does not act on the power button, leaving it free for userspace handling. - **evtest**: Available at `/usr/bin/evtest` for debugging input events. ## Partition Layout -| Device | Mount point | Filesystem | Contents | -|--------|-------------|------------|----------| -| `/dev/mmcblk0p5` | `/` | ext4 | Root filesystem | -| `/dev/mmcblk0p6` | `/mnt/vendor` | ext4 | Firmware, apps, scripts | -| `/dev/mmcblk0p7` | `/mnt/data` | ext4 | User data | -| `/dev/mmcblk0p8` | `/mnt/mmc` | vfat | ROMs | -| SD card slot | `/mnt/sdcard` | (varies) | External storage | +| Device | Mount point | Filesystem | Contents | +| :--------------- | :------------ | :--------- | :---------------------- | +| `/dev/mmcblk0p5` | `/` | ext4 | Root filesystem | +| `/dev/mmcblk0p6` | `/mnt/vendor` | ext4 | Firmware, apps, scripts | +| `/dev/mmcblk0p7` | `/mnt/data` | ext4 | User data | +| `/dev/mmcblk0p8` | `/mnt/mmc` | vfat | ROMs | +| SD card slot | `/mnt/sdcard` | (varies) | External storage | ## Boot Chain @@ -92,6 +92,29 @@ systemd (graphical.target) The `app_scheduling` function in `dmenu_ln` runs the selected binary. If it exits with a non-zero status, it sleeps 30 seconds before returning, which prevents crash loops from consuming all CPU. The outer `while true` loop in `loadapp.sh` then re-invokes `dmenu_ln`, restarting the application. +## Framebuffer + +| Property | Value | +| :------------- | :------------------- | +| Device | `/dev/fb0` | +| Visible size | 640x480 | +| Virtual size | 640x960 (double-buf) | +| Bits per pixel | 32 (BGRA) | +| Stride | 2560 bytes (640 * 4) | + +No standard framebuffer image tools (`fbv`, `fbi`, `psplash`) are installed. To display a PNG on the framebuffer, decode with Python3+PIL and write raw BGRA pixels to `/dev/fb0`. + +## Stock Firmware Assets + +Shutdown-related assets in `/mnt/vendor/res1/shutdown/`: + +| File | Description | Size | +| :------------- | :------------------------------ | :------ | +| `goodbye.png` | Shutdown screen (RGB, 640x480) | Matches display | +| `lowpower.png` | Low battery warning | — | + +The boot logo is at `/mnt/vendor/res1/boot/logo.png`. + ## Deploying sdlamp2 ### Overview @@ -103,8 +126,8 @@ The `dmenu_ln` script already supports switching the startup binary via config f 1. **Copy the binary and wrapper** to the device: ```sh - scp build/sdlamp2 root@:/mnt/vendor/bin/sdlamp2 - scp tools/rg35xx-wrapper.sh root@:/mnt/vendor/bin/rg35xx-wrapper.sh + scp build/sdlamp2 root@rg35xx:/mnt/vendor/bin/sdlamp2 + scp tools/rg35xx-wrapper.sh root@rg35xx:/mnt/vendor/bin/rg35xx-wrapper.sh ``` 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: @@ -135,7 +158,7 @@ Everything else in the boot chain continues to work: - **Charging mode** — handled in `loadapp.sh` before the restart loop - **LED/backlight control** — `brightCtrl.bin` started by `launcher.sh` -- **Clean shutdown** — sdlamp2 handles SIGTERM/SIGINT, saving position and volume before exit. The wrapper script can trigger `poweroff` after sdlamp2 exits +- **Clean shutdown** — sdlamp2 handles SIGTERM/SIGINT, saving position and volume before exit. The wrapper displays the stock `goodbye.png` on the framebuffer and calls `poweroff` - **Restart on exit** — if sdlamp2 exits cleanly (status 0), the restart loop in `loadapp.sh` re-launches it immediately - **Crash recovery** — if sdlamp2 crashes (non-zero exit), `app_scheduling` sleeps 30s then the loop retries - **Easy revert** — removing `sdlamp2.ini` restores the stock menu on next boot diff --git a/docs/sdlamp2-fsd.md b/docs/sdlamp2-fsd.md index 587ba48..73ca995 100644 --- a/docs/sdlamp2-fsd.md +++ b/docs/sdlamp2-fsd.md @@ -43,9 +43,11 @@ This document specifies the functional requirements for an SDL2 based media play ## 6. Changelog -### 2026-02-13 — Power button monitor in device wrapper +### 2026-02-13 — Hold-to-shutdown with stock shutdown screen -- **Power button handling**: The `rg35xx-wrapper.sh` script now monitors `/dev/input/event0` (`axp2202-pek`) for `KEY_POWER` press events via `evtest`. On power button press, sends SIGTERM to sdlamp2 (which saves position and volume), waits 1 second, then calls `poweroff` for a clean shutdown. +- **Hold-to-shutdown**: The power button monitor in `rg35xx-wrapper.sh` requires a 3-second hold before triggering shutdown. A quick tap does nothing — a background timer subshell starts on press and is cancelled on release. If held past 3 seconds, sends SIGTERM to sdlamp2 then triggers shutdown. +- **Stock shutdown screen**: The wrapper displays the stock firmware's `goodbye.png` (`/mnt/vendor/res1/shutdown/goodbye.png`) by decoding it with Python3+PIL and writing raw BGRA pixels to `/dev/fb0`. This reuses the same shutdown image that `dmenu.bin` shows, keeping the experience consistent. The shutdown display is handled entirely in the wrapper — sdlamp2 just exits cleanly on SIGTERM. +- **Restart loop blocked**: The wrapper uses a `/tmp/.sdlamp2_shutdown` flag file to distinguish shutdown from normal exit. On shutdown, the wrapper calls `poweroff` and blocks (sleep 30), preventing `loadapp.sh`'s restart loop from relaunching `dmenu_ln` and overwriting the shutdown screen. ### 2026-02-13 — Clean shutdown on SIGTERM/SIGINT diff --git a/src/sdlamp2.c b/src/sdlamp2.c index 21bc41a..a460624 100644 --- a/src/sdlamp2.c +++ b/src/sdlamp2.c @@ -683,7 +683,9 @@ int main(int argc, char** argv) { SDL_Event e; while (running) { - if (got_signal) running = SDL_FALSE; + if (got_signal) { + running = SDL_FALSE; + } /* --- Event handling --- */ while (SDL_PollEvent(&e)) { diff --git a/tools/rg35xx-wrapper.sh b/tools/rg35xx-wrapper.sh index 8c4d259..74df41f 100755 --- a/tools/rg35xx-wrapper.sh +++ b/tools/rg35xx-wrapper.sh @@ -5,7 +5,8 @@ # Launched by dmenu_ln instead of sdlamp2 directly. Handles device-specific # concerns that don't belong in the player binary: # 1. Monitor power button and trigger clean shutdown -# 2. Launch sdlamp2 as the main process +# 2. Display the stock firmware's shutdown screen (goodbye.png → /dev/fb0) +# 3. Launch sdlamp2 as the main process # # Install: copy to /mnt/vendor/bin/rg35xx-wrapper.sh # Config: set CMD in dmenu_ln to point here instead of sdlamp2 directly @@ -24,13 +25,21 @@ AUDIO_DIR="/mnt/sdcard/Music" POWER_EVENT_DEV="/dev/input/event0" monitor_power_button() { - # evtest writes one line per event; filter for KEY_POWER press (value 1) + POWER_TIMER_PID="" evtest "$POWER_EVENT_DEV" 2>/dev/null | while read -r line; do case "$line" in *"code 116"*"value 1"*) - kill -TERM "$SDLAMP2_PID" 2>/dev/null - sleep 1 - poweroff + # Power button pressed — start 3-second hold timer + ( trap 'exit 0' TERM; sleep 3; touch /tmp/.sdlamp2_shutdown; kill -TERM "$SDLAMP2_PID" 2>/dev/null ) & + POWER_TIMER_PID=$! + ;; + *"code 116"*"value 0"*) + # Power button released — cancel timer if still running + if [ -n "$POWER_TIMER_PID" ]; then + kill "$POWER_TIMER_PID" 2>/dev/null + wait "$POWER_TIMER_PID" 2>/dev/null + POWER_TIMER_PID="" + fi ;; esac done @@ -52,4 +61,23 @@ SDLAMP2_EXIT=$? # Kill the power button monitor if it's still running kill "$MONITOR_PID" 2>/dev/null +# If this was a shutdown, call poweroff and block so the loadapp.sh restart +# loop doesn't relaunch dmenu_ln (which would take over the framebuffer and +# overwrite sdlamp2's shutdown screen). +if [ -f /tmp/.sdlamp2_shutdown ]; then + rm -f /tmp/.sdlamp2_shutdown + # Display the stock firmware's shutdown screen via framebuffer. + # goodbye.png is 640x480 RGB — exactly matches the display. + # /dev/fb0 is 32bpp BGRA, so we swap R/B channels and write raw pixels. + python3 -c " +from PIL import Image +img = Image.open('/mnt/vendor/res1/shutdown/goodbye.png').convert('RGBA') +r, g, b, a = img.split() +with open('/dev/fb0', 'wb') as f: + f.write(Image.merge('RGBA', (b, g, r, a)).tobytes()) +" 2>/dev/null + poweroff + sleep 30 +fi + exit "$SDLAMP2_EXIT"