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 <noreply@anthropic.com>
This commit is contained in:
parent
06daec791e
commit
2142ed7629
@ -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`.
|
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
|
## Stock Firmware Assets
|
||||||
|
|
||||||
Shutdown-related assets in `/mnt/vendor/res1/shutdown/`:
|
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
|
### Setup
|
||||||
|
|
||||||
1. **Copy the binary and wrapper** to the device:
|
1. **Copy the binary, wrapper, and screen monitor** to the device:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scp build/sdlamp2 root@rg35xx:/mnt/vendor/bin/sdlamp2
|
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-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:
|
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:
|
||||||
|
|||||||
@ -43,6 +43,13 @@ This document specifies the functional requirements for an SDL2 based media play
|
|||||||
|
|
||||||
## 6. Changelog
|
## 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
|
### 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.
|
- **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.
|
||||||
|
|||||||
138
tools/rg35xx-screen-monitor.py
Executable file
138
tools/rg35xx-screen-monitor.py
Executable file
@ -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()
|
||||||
@ -6,13 +6,15 @@
|
|||||||
# concerns that don't belong in the player binary:
|
# concerns that don't belong in the player binary:
|
||||||
# 1. Monitor power button and trigger clean shutdown
|
# 1. Monitor power button and trigger clean shutdown
|
||||||
# 2. Display the stock firmware's shutdown screen (goodbye.png → /dev/fb0)
|
# 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
|
# 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
|
||||||
|
|
||||||
SDLAMP2="/mnt/vendor/bin/sdlamp2"
|
SDLAMP2="/mnt/vendor/bin/sdlamp2"
|
||||||
AUDIO_DIR="/mnt/sdcard/Music"
|
AUDIO_DIR="/mnt/sdcard/Music"
|
||||||
|
SCREEN_MONITOR="/mnt/vendor/bin/rg35xx-screen-monitor.py"
|
||||||
|
|
||||||
# --- WiFi hotspot ---
|
# --- WiFi hotspot ---
|
||||||
# Deprioritized: connecting the device as a WiFi client to a shared network
|
# Deprioritized: connecting the device as a WiFi client to a shared network
|
||||||
@ -45,6 +47,10 @@ monitor_power_button() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Screen idle timeout + power button screen toggle ---
|
||||||
|
python3 "$SCREEN_MONITOR" &
|
||||||
|
SCREEN_MONITOR_PID=$!
|
||||||
|
|
||||||
# --- Launch sdlamp2 ---
|
# --- Launch sdlamp2 ---
|
||||||
# Run in background so we can capture PID for the power button monitor.
|
# Run in background so we can capture PID for the power button monitor.
|
||||||
"$SDLAMP2" "$AUDIO_DIR" &
|
"$SDLAMP2" "$AUDIO_DIR" &
|
||||||
@ -58,7 +64,9 @@ wait "$SDLAMP2_PID"
|
|||||||
SDLAMP2_EXIT=$?
|
SDLAMP2_EXIT=$?
|
||||||
|
|
||||||
# --- Cleanup ---
|
# --- 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
|
kill "$MONITOR_PID" 2>/dev/null
|
||||||
|
|
||||||
# If this was a shutdown, call poweroff and block so the loadapp.sh restart
|
# If this was a shutdown, call poweroff and block so the loadapp.sh restart
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user