From 2142ed76299bb40dab9dddd6b3772e4532982520 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Feb 2026 21:34:43 +0100 Subject: [PATCH] Add screen idle timeout and power button screen toggle 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 --- docs/rg35xx-plus.md | 19 ++++- docs/sdlamp2-fsd.md | 7 ++ tools/rg35xx-screen-monitor.py | 138 +++++++++++++++++++++++++++++++++ tools/rg35xx-wrapper.sh | 12 ++- 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100755 tools/rg35xx-screen-monitor.py diff --git a/docs/rg35xx-plus.md b/docs/rg35xx-plus.md index 9d12487..b986759 100644 --- a/docs/rg35xx-plus.md +++ b/docs/rg35xx-plus.md @@ -104,6 +104,22 @@ The `app_scheduling` function in `dmenu_ln` runs the selected binary. If it exit 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/`: @@ -123,11 +139,12 @@ The `dmenu_ln` script already supports switching the startup binary via config f ### Setup -1. **Copy the binary and wrapper** to the device: +1. **Copy the binary, wrapper, and screen monitor** to the device: ```sh 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 ``` 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: diff --git a/docs/sdlamp2-fsd.md b/docs/sdlamp2-fsd.md index 73ca995..59830fe 100644 --- a/docs/sdlamp2-fsd.md +++ b/docs/sdlamp2-fsd.md @@ -43,6 +43,13 @@ This document specifies the functional requirements for an SDL2 based media play ## 6. Changelog +### 2026-02-13 — Screen idle timeout and power button toggle + +- **Screen idle timeout**: New Python screen monitor (`rg35xx-screen-monitor.py`) turns off the display after 15 seconds of no input on any device. Audio continues playing. Any button press wakes the screen. +- **Power button screen toggle**: A short press (<2s) of the power button toggles the screen on/off. Long press (3s+) still triggers shutdown via the existing wrapper logic. +- **Allwinner /dev/disp backlight**: Uses `SET_BRIGHTNESS` / `GET_BRIGHTNESS` ioctls on `/dev/disp` since the device has no sysfs backlight interface. Brightness is restored on SIGTERM so the shutdown screen remains visible. +- **Wrapper integration**: `rg35xx-wrapper.sh` launches the screen monitor alongside sdlamp2 and kills it during cleanup. + ### 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. diff --git a/tools/rg35xx-screen-monitor.py b/tools/rg35xx-screen-monitor.py new file mode 100755 index 0000000..2b3d85f --- /dev/null +++ b/tools/rg35xx-screen-monitor.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Screen idle timeout and power button toggle for RG35XX Plus. + +Monitors /dev/input/event{0,1,2} for activity. Turns off the screen +(via Allwinner /dev/disp SET_BRIGHTNESS ioctl) after 15s of no input. +Any button press wakes the screen. A short power button press (<2s) +toggles the screen on/off. + +Launched by rg35xx-wrapper.sh alongside sdlamp2. Killed on cleanup. +""" +import fcntl +import os +import signal +import struct +import sys +import time + +IDLE_TIMEOUT = 15 # seconds +POWER_HOLD_THRESHOLD = 2.0 # seconds — short press if released before this + +# Allwinner /dev/disp ioctl commands +DISP_GET_BRIGHTNESS = 0x103 +DISP_SET_BRIGHTNESS = 0x102 + +# Input event constants +EV_KEY = 1 +KEY_POWER = 116 + +# struct input_event on aarch64: struct timeval (2x long=8 bytes each) + __u16 type + __u16 code + __s32 value +INPUT_EVENT_FORMAT = "@llHHi" +INPUT_EVENT_SIZE = struct.calcsize(INPUT_EVENT_FORMAT) + +EVENT_DEVICES = ["/dev/input/event0", "/dev/input/event1", "/dev/input/event2"] + + +def disp_ioctl(fd, cmd, screen=0, value=0): + # Allwinner disp ioctls take an array of 4 unsigned longs as arg + args = struct.pack("@4L", screen, value, 0, 0) + result = fcntl.ioctl(fd, cmd, args) + return struct.unpack("@4L", result)[0] + + +def get_brightness(disp_fd): + return disp_ioctl(disp_fd, DISP_GET_BRIGHTNESS) + + +def set_brightness(disp_fd, value): + disp_ioctl(disp_fd, DISP_SET_BRIGHTNESS, value=value) + + +def main(): + disp_fd = os.open("/dev/disp", os.O_RDWR) + original_brightness = get_brightness(disp_fd) + if original_brightness == 0: + original_brightness = 50 # sensible default if already off + + screen_on = True + power_press_time = None + + # Restore brightness on exit so goodbye.png is visible during shutdown + def restore_and_exit(signum, frame): + if not screen_on: + set_brightness(disp_fd, original_brightness) + os.close(disp_fd) + sys.exit(0) + + signal.signal(signal.SIGTERM, restore_and_exit) + signal.signal(signal.SIGINT, restore_and_exit) + + # Open input devices (non-blocking) + event_fds = [] + for path in EVENT_DEVICES: + try: + fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) + event_fds.append(fd) + except OSError as e: + print(f"screen-monitor: cannot open {path}: {e}", file=sys.stderr) + + if not event_fds: + print("screen-monitor: no input devices available, exiting", file=sys.stderr) + os.close(disp_fd) + sys.exit(1) + + import select + + while True: + readable, _, _ = select.select(event_fds, [], [], IDLE_TIMEOUT) + + if not readable: + # Timeout — no input for IDLE_TIMEOUT seconds + if screen_on: + set_brightness(disp_fd, 0) + screen_on = False + continue + + # Process input events from all readable fds + for fd in readable: + while True: + try: + data = os.read(fd, INPUT_EVENT_SIZE) + except BlockingIOError: + break + if len(data) < INPUT_EVENT_SIZE: + break + + _sec, _usec, ev_type, ev_code, ev_value = struct.unpack( + INPUT_EVENT_FORMAT, data + ) + + if ev_type != EV_KEY: + continue + + # Power button handling + if ev_code == KEY_POWER: + if ev_value == 1: # press + power_press_time = time.monotonic() + elif ev_value == 0 and power_press_time is not None: # release + hold_duration = time.monotonic() - power_press_time + power_press_time = None + if hold_duration < POWER_HOLD_THRESHOLD: + # Short press — toggle screen + if screen_on: + set_brightness(disp_fd, 0) + screen_on = False + else: + set_brightness(disp_fd, original_brightness) + screen_on = True + continue + + # Any other key press — wake screen if off + if ev_value == 1 and not screen_on: + set_brightness(disp_fd, original_brightness) + screen_on = True + + +if __name__ == "__main__": + main() diff --git a/tools/rg35xx-wrapper.sh b/tools/rg35xx-wrapper.sh index 74df41f..1382ec5 100755 --- a/tools/rg35xx-wrapper.sh +++ b/tools/rg35xx-wrapper.sh @@ -6,13 +6,15 @@ # concerns that don't belong in the player binary: # 1. Monitor power button and trigger clean shutdown # 2. Display the stock firmware's shutdown screen (goodbye.png → /dev/fb0) -# 3. Launch sdlamp2 as the main process +# 3. Screen idle timeout (off after 15s) and power button screen toggle +# 4. 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 SDLAMP2="/mnt/vendor/bin/sdlamp2" AUDIO_DIR="/mnt/sdcard/Music" +SCREEN_MONITOR="/mnt/vendor/bin/rg35xx-screen-monitor.py" # --- WiFi hotspot --- # Deprioritized: connecting the device as a WiFi client to a shared network @@ -45,6 +47,10 @@ monitor_power_button() { done } +# --- Screen idle timeout + power button screen toggle --- +python3 "$SCREEN_MONITOR" & +SCREEN_MONITOR_PID=$! + # --- Launch sdlamp2 --- # Run in background so we can capture PID for the power button monitor. "$SDLAMP2" "$AUDIO_DIR" & @@ -58,7 +64,9 @@ wait "$SDLAMP2_PID" SDLAMP2_EXIT=$? # --- Cleanup --- -# Kill the power button monitor if it's still running +# Kill the screen monitor (SIGTERM restores brightness) and power button monitor. +kill "$SCREEN_MONITOR_PID" 2>/dev/null +wait "$SCREEN_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