Compare commits

...

2 Commits

Author SHA1 Message Date
06daec791e 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>
2026-02-13 21:07:03 +01:00
3728e9499c Implement power button monitor, document device input devices
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 20:07:37 +01:00
4 changed files with 122 additions and 58 deletions

View File

@ -23,7 +23,7 @@ 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): 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 | | Library | Soname | Notes |
|---------|--------|-------| | :------------ | :----------------------------- | :---------------------------------- |
| SDL2 | `libSDL2-2.0.so.0.12.0` | SDL 2.0.12 | | 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 | | 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 | | libavcodec | `libavcodec.so.58` | FFmpeg ~4.x |
@ -33,10 +33,23 @@ All required shared libraries are pre-installed. Most are at `/usr/lib/`, some a
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. 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.
## Input Devices
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 |
- **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 ## Partition Layout
| Device | Mount point | Filesystem | Contents | | Device | Mount point | Filesystem | Contents |
|--------|-------------|------------|----------| | :--------------- | :------------ | :--------- | :---------------------- |
| `/dev/mmcblk0p5` | `/` | ext4 | Root filesystem | | `/dev/mmcblk0p5` | `/` | ext4 | Root filesystem |
| `/dev/mmcblk0p6` | `/mnt/vendor` | ext4 | Firmware, apps, scripts | | `/dev/mmcblk0p6` | `/mnt/vendor` | ext4 | Firmware, apps, scripts |
| `/dev/mmcblk0p7` | `/mnt/data` | ext4 | User data | | `/dev/mmcblk0p7` | `/mnt/data` | ext4 | User data |
@ -79,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. 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 ## Deploying sdlamp2
### Overview ### Overview
@ -90,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: 1. **Copy the binary and wrapper** to the device:
```sh ```sh
scp build/sdlamp2 root@<device>:/mnt/vendor/bin/sdlamp2 scp build/sdlamp2 root@rg35xx:/mnt/vendor/bin/sdlamp2
scp tools/rg35xx-wrapper.sh root@<device>:/mnt/vendor/bin/rg35xx-wrapper.sh 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: 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:
@ -122,7 +158,7 @@ Everything else in the boot chain continues to work:
- **Charging mode** — handled in `loadapp.sh` before the restart loop - **Charging mode** — handled in `loadapp.sh` before the restart loop
- **LED/backlight control**`brightCtrl.bin` started by `launcher.sh` - **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 - **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 - **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 - **Easy revert** — removing `sdlamp2.ini` restores the stock menu on next boot

View File

@ -43,6 +43,12 @@ This document specifies the functional requirements for an SDL2 based media play
## 6. Changelog ## 6. Changelog
### 2026-02-13 — Hold-to-shutdown with stock shutdown screen
- **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 ### 2026-02-13 — Clean shutdown on SIGTERM/SIGINT
- **Signal handling**: sdlamp2 now catches SIGTERM and SIGINT via a `sig_atomic_t` flag checked in the main loop. On signal, the existing cleanup path runs (save position, save volume, close decoder, SDL_Quit) instead of being killed instantly. This ensures position is saved when the system shuts down or the process is terminated by a wrapper script. - **Signal handling**: sdlamp2 now catches SIGTERM and SIGINT via a `sig_atomic_t` flag checked in the main loop. On signal, the existing cleanup path runs (save position, save volume, close decoder, SDL_Quit) instead of being killed instantly. This ensures position is saved when the system shuts down or the process is terminated by a wrapper script.

View File

@ -683,7 +683,9 @@ int main(int argc, char** argv) {
SDL_Event e; SDL_Event e;
while (running) { while (running) {
if (got_signal) running = SDL_FALSE; if (got_signal) {
running = SDL_FALSE;
}
/* --- Event handling --- */ /* --- Event handling --- */
while (SDL_PollEvent(&e)) { while (SDL_PollEvent(&e)) {

View File

@ -4,9 +4,9 @@
# #
# Launched by dmenu_ln instead of sdlamp2 directly. Handles device-specific # Launched by dmenu_ln instead of sdlamp2 directly. Handles device-specific
# concerns that don't belong in the player binary: # concerns that don't belong in the player binary:
# 1. Start WiFi hotspot (AP mode for SSH access) # 1. Monitor power button and trigger clean shutdown
# 2. Launch sdlamp2 as the foreground process # 2. Display the stock firmware's shutdown screen (goodbye.png → /dev/fb0)
# 3. Monitor power button and trigger clean shutdown # 3. Launch sdlamp2 as the main process
# #
# Install: copy to /mnt/vendor/bin/rg35xx-wrapper.sh # Install: copy to /mnt/vendor/bin/rg35xx-wrapper.sh
# Config: set CMD in dmenu_ln to point here instead of sdlamp2 directly # Config: set CMD in dmenu_ln to point here instead of sdlamp2 directly
@ -15,49 +15,69 @@ SDLAMP2="/mnt/vendor/bin/sdlamp2"
AUDIO_DIR="/mnt/sdcard/Music" AUDIO_DIR="/mnt/sdcard/Music"
# --- WiFi hotspot --- # --- WiFi hotspot ---
# TODO: Investigate how the stock menu starts AP mode. # Deprioritized: connecting the device as a WiFi client to a shared network
# Likely candidates: hostapd, nmcli, or a vendor script in /mnt/vendor/ctrl/. # works fine even when sdlamp2 replaces the stock menu. Hotspot/AP mode isn't
# Run these on the device to find out: # needed — SSH access works over the shared network.
# grep -r 'hostapd\|hotspot\|ap_mode\|wifi' /mnt/vendor/ctrl/
# systemctl list-units | grep -i net
# nmcli general status && nmcli connection show
#
# Placeholder — uncomment/replace once the mechanism is known:
# nmcli connection up hotspot 2>/dev/null || true
# --- Power button monitor --- # --- Power button monitor ---
# TODO: Investigate power button input device. # axp2202-pek on /dev/input/event0 sends KEY_POWER (code 116).
# Run on device: # logind has HandlePowerKey=ignore, so we handle it here.
# cat /proc/bus/input/devices (find the power button event device) POWER_EVENT_DEV="/dev/input/event0"
# evtest /dev/input/eventN (confirm KEY_POWER event code)
# cat /etc/systemd/logind.conf (check HandlePowerKey setting) monitor_power_button() {
# POWER_TIMER_PID=""
# Strategy: read key events from the power button input device in background. evtest "$POWER_EVENT_DEV" 2>/dev/null | while read -r line; do
# When KEY_POWER (code 116) is detected, send SIGTERM to sdlamp2 and poweroff. case "$line" in
# *"code 116"*"value 1"*)
# POWER_EVENT_DEV="/dev/input/eventN" # TBD: set after investigation # 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 ) &
# monitor_power_button() { POWER_TIMER_PID=$!
# # evtest writes one line per event; filter for KEY_POWER press (value 1) ;;
# evtest "$POWER_EVENT_DEV" 2>/dev/null | while read -r line; do *"code 116"*"value 0"*)
# case "$line" in # Power button released — cancel timer if still running
# *"code 116"*"value 1"*) if [ -n "$POWER_TIMER_PID" ]; then
# kill -TERM "$SDLAMP2_PID" 2>/dev/null kill "$POWER_TIMER_PID" 2>/dev/null
# sleep 1 wait "$POWER_TIMER_PID" 2>/dev/null
# poweroff POWER_TIMER_PID=""
# ;; fi
# esac ;;
# done esac
# } done
# monitor_power_button & }
# MONITOR_PID=$!
# --- Launch sdlamp2 --- # --- Launch sdlamp2 ---
"$SDLAMP2" "$AUDIO_DIR" # Run in background so we can capture PID for the power button monitor.
"$SDLAMP2" "$AUDIO_DIR" &
SDLAMP2_PID=$!
monitor_power_button &
MONITOR_PID=$!
# Wait for sdlamp2 to finish (signal or normal exit).
wait "$SDLAMP2_PID"
SDLAMP2_EXIT=$? SDLAMP2_EXIT=$?
# --- Cleanup --- # --- Cleanup ---
# Kill the power button monitor if it's still running # Kill the power button monitor if it's still running
# kill "$MONITOR_PID" 2>/dev/null 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" exit "$SDLAMP2_EXIT"