New Python screen monitor uses Allwinner /dev/disp ioctls to turn off the display after 15s of no input and toggle it with a short power button press. Launched by the wrapper alongside sdlamp2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.3 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.
Backlight Control
There is no sysfs backlight interface (/sys/class/backlight/ is empty) and the stock brightCtrl.bin daemon does not expose a usable control mechanism. The backlight is controlled via the Allwinner /dev/disp ioctl interface:
| Ioctl | Number | Description |
|---|---|---|
DISP_SET_BRIGHTNESS |
0x102 |
Set backlight brightness (0–255) |
DISP_GET_BRIGHTNESS |
0x103 |
Get current backlight brightness |
Both ioctls take an argument buffer of 4 unsigned longs (struct { unsigned long args[4]; }):
args[0]= screen index (always 0)args[1]= brightness value (for SET; ignored for GET)- Return value (for GET) is in
args[0]after the ioctl call
Setting brightness to 0 turns the screen off completely. The original value (typically ~50) can be restored to turn it back on. This is used by rg35xx-screen-monitor.py for idle timeout and power button toggle.
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, wrapper, and screen monitor 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 scp tools/rg35xx-screen-monitor.py root@rg35xx:/mnt/vendor/bin/rg35xx-screen-monitor.py -
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