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 <noreply@anthropic.com>
This commit is contained in:
Michael Smith 2026-02-13 21:07:03 +01:00
parent 3728e9499c
commit 06daec791e
4 changed files with 86 additions and 31 deletions

View File

@ -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@<device>:/mnt/vendor/bin/sdlamp2
scp tools/rg35xx-wrapper.sh root@<device>:/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

View File

@ -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

View File

@ -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)) {

View File

@ -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"