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>
8.2 KiB
Anbernic RG35XX Plus
Device-specific reference for the target hardware. For build instructions see TOOLCHAIN.md.
Hardware
- Device: Anbernic RG35XX Plus
- SoC: Allwinner H700 — quad-core ARM Cortex-A53 @ 1.5 GHz (AArch64 / ARMv8-A)
- GPU: Mali G31 MP2
- RAM: 1 GB LPDDR4
- Display: 640×480 IPS
Software
- OS: Ubuntu 22.04 LTS (Jammy Jellyfish) — modified stock firmware
- Kernel: Linux 4.9.170 aarch64 (proprietary, no source released by Anbernic)
- Architecture: aarch64 / arm64 (confirmed via
dpkg --print-architecture) - glibc: 2.35
- Userland: Debian/Ubuntu-based (
dpkg,apt-get,systemd)
Libraries on Device
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 |
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=ignorein/etc/systemd/logind.conf— systemd does not act on the power button, leaving it free for userspace handling. - evtest: Available at
/usr/bin/evtestfor 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 |
Boot Chain
The device uses systemd but boots its UI through a SysV init script that systemd auto-wraps via systemd-sysv-generator. The chain is intentionally layered: launcher.sh runs before partitions are mounted (hardware setup), loadapp.sh runs after mounts (filesystem/network setup + app restart loop), and dmenu_ln is the app dispatcher.
systemd (graphical.target)
└─ launcher.service (SysV → auto-generated by systemd-sysv-generator)
└─ /etc/init.d/launcher.sh start
├── mounts /mnt/vendor
├── starts brightCtrl.bin (backlight daemon) &
├── starts cexpert &
└── starts loadapp.sh &
├── resizes root partition (first boot only)
├── mounts /mnt/data, /mnt/mmc
├── charging mode: if boot_mode==1 → charger UI → poweroff
├── normal mode: starts NetworkManager, bluetooth, wifi
├── syncs RTC, loads kernel modules, mounts SD card
├── runs /mnt/mod/ctrl/autostart (if exists)
└── RESTART LOOP: runs /mnt/vendor/ctrl/dmenu_ln repeatedly
└─ /mnt/vendor/ctrl/dmenu_ln
├── selects binary (default: dmenu.bin)
├── config overrides: muos.ini → muos.bin, vpRun.ini → ...
├── runs binary via app_scheduling()
└── on exit: executes /tmp/.next (app dispatch for menu)
Why the indirection?
launcher.sh— Pre-mount hardware setup: mounts/mnt/vendor, starts backlight and system daemons. Runs as a SysV init script so it integrates with systemd's boot ordering.loadapp.sh— Post-mount filesystem/network setup: mounts data partitions, handles charging mode, starts networking, then enters the restart loop. Runs in the background (&) solauncher.shcan return.dmenu_ln— App dispatcher: selects which binary to run based on config files, wraps execution inapp_scheduling()(which sleeps 30s on crash before retrying), and supports/tmp/.nextfor menu-driven app switching.
app_scheduling behavior
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
The dmenu_ln script already supports switching the startup binary via config files (e.g. muos.ini → muos.bin). We follow the same pattern to launch sdlamp2 instead of the default menu.
Setup
-
Copy the binary and wrapper to the device:
scp build/sdlamp2 root@rg35xx:/mnt/vendor/bin/sdlamp2 scp tools/rg35xx-wrapper.sh root@rg35xx:/mnt/vendor/bin/rg35xx-wrapper.sh -
Add the config check to
/mnt/vendor/ctrl/dmenu_ln. In the section whereCMDoverrides are checked (after the existingmuos.ini/vpRun.inichecks, before theapp_schedulingcall), add:if [ -f "/mnt/vendor/sdlamp2.ini" ];then CMD="/mnt/vendor/bin/rg35xx-wrapper.sh" fiThe wrapper script handles device-specific concerns (WiFi hotspot, power button monitoring) and launches sdlamp2 as its main foreground process. See
tools/rg35xx-wrapper.shfor details. -
Enable sdlamp2 on boot:
touch /mnt/vendor/sdlamp2.ini -
Disable (revert to normal menu):
rm /mnt/vendor/sdlamp2.ini
What's preserved
Everything else in the boot chain continues to work:
- Charging mode — handled in
loadapp.shbefore the restart loop - LED/backlight control —
brightCtrl.binstarted bylauncher.sh - Clean shutdown — sdlamp2 handles SIGTERM/SIGINT, saving position and volume before exit. The wrapper displays the stock
goodbye.pngon the framebuffer and callspoweroff - Restart on exit — if sdlamp2 exits cleanly (status 0), the restart loop in
loadapp.shre-launches it immediately - Crash recovery — if sdlamp2 crashes (non-zero exit),
app_schedulingsleeps 30s then the loop retries - Easy revert — removing
sdlamp2.inirestores the stock menu on next boot