Fix screen wake for d-pad, grab inputs while screen off, auto-shutdown on idle

- Broaden wake events beyond EV_KEY so d-pad (EV_ABS) wakes the screen
- Use EVIOCGRAB for exclusive input while screen is off, preventing the
  wake button press from also acting in sdlamp2
- Auto-shutdown after 10 minutes of no input and no audio playback,
  detected by monitoring the audio file's read position via /proc fdinfo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Smith 2026-02-13 22:47:47 +01:00
parent 8123b9a99f
commit fbd32d7fb8
2 changed files with 92 additions and 12 deletions

View File

@ -43,6 +43,12 @@ This document specifies the functional requirements for an SDL2 based media play
## 6. Changelog
### 2026-02-13 — Screen wake fixes and idle auto-shutdown
- **D-pad wakes screen**: The screen monitor now wakes on any input event type (EV_ABS, EV_KEY, etc.), not just EV_KEY. This fixes d-pad presses (which generate EV_ABS hat events) not waking the screen.
- **Wake button doesn't act in app**: Uses EVIOCGRAB to take exclusive access to input devices while the screen is off. SDL in sdlamp2 receives no events while grabbed, so the button press that wakes the screen doesn't also trigger play/stop or switch cassettes. Grabs are released when the screen turns back on.
- **Idle auto-shutdown**: After 10 minutes of no input AND no audio playback, the device auto-shuts down to save battery. Playback detection works by monitoring the audio file's read position via `/proc/<pid>/fdinfo/` — if the file offset is advancing, audio is being decoded. The timer resets on any input event or any detected playback activity.
### 2026-02-13 — Remember last cassette and pause on switch
- **Remember last cassette**: The current cassette filename is saved to `last_cassette.txt` in the audio directory on every file switch. On startup, the player resumes the last-loaded cassette instead of always starting with the first file. Falls back to the first file if the saved file is missing or not found.

View File

@ -4,8 +4,14 @@ Screen idle timeout, power button toggle, and long-press shutdown for RG35XX Plu
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. A long press (3s+) triggers clean shutdown.
Any input event (keys, d-pad, joystick) wakes the screen. While the
screen is off, inputs are grabbed (EVIOCGRAB) so SDL in sdlamp2 does
not receive the wake event.
A short power button press (<2s) toggles the screen on/off. A long
press (3s+) triggers clean shutdown. If the device is idle (no input
and no audio playback) for 10 minutes, it auto-shuts down to save
battery.
Launched by rg35xx-wrapper.sh alongside sdlamp2. Killed on cleanup.
@ -19,7 +25,8 @@ import struct
import sys
import time
IDLE_TIMEOUT = 15 # seconds
IDLE_TIMEOUT = 15 # seconds — screen off after no input
IDLE_SHUTDOWN_TIMEOUT = 600 # seconds — auto-shutdown after no input AND no playback
POWER_SHORT_THRESHOLD = 2.0 # seconds — short press if released before this
POWER_LONG_THRESHOLD = 3.0 # seconds — shutdown if held this long
@ -28,9 +35,15 @@ DISP_GET_BRIGHTNESS = 0x103
DISP_SET_BRIGHTNESS = 0x102
# Input event constants
EV_SYN = 0
EV_KEY = 1
KEY_POWER = 116
# EVIOCGRAB — exclusive access to input device (_IOW('E', 0x90, int))
EVIOCGRAB = 0x40044590
AUDIO_EXTENSIONS = ('.m4a', '.mp3', '.wav', '.ogg')
# 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)
@ -53,6 +66,34 @@ def set_brightness(disp_fd, value):
disp_ioctl(disp_fd, DISP_SET_BRIGHTNESS, value=value)
def grab_inputs(event_fds, grab):
"""Grab or release exclusive access to input devices."""
for fd in event_fds:
try:
fcntl.ioctl(fd, EVIOCGRAB, 1 if grab else 0)
except OSError:
pass
def get_audio_file_pos(pid):
"""Return the file offset of sdlamp2's open audio file, or None."""
fd_dir = f"/proc/{pid}/fd"
try:
for fd_name in os.listdir(fd_dir):
try:
target = os.readlink(f"{fd_dir}/{fd_name}")
if any(target.endswith(ext) for ext in AUDIO_EXTENSIONS):
with open(f"/proc/{pid}/fdinfo/{fd_name}") as f:
for line in f:
if line.startswith("pos:"):
return int(line.split()[1])
except OSError:
continue
except OSError:
pass
return None
def main():
if len(sys.argv) < 2:
print("Usage: rg35xx-screen-monitor.py <sdlamp2_pid>", file=sys.stderr)
@ -68,8 +109,9 @@ def main():
screen_on = True
power_press_time = None
# Restore brightness on exit so goodbye.png is visible during shutdown
# Restore brightness and release grabs on exit so goodbye.png is visible during shutdown
def restore_and_exit(signum, frame):
grab_inputs(event_fds, False)
if not screen_on:
set_brightness(disp_fd, original_brightness)
os.close(disp_fd)
@ -92,6 +134,10 @@ def main():
os.close(disp_fd)
sys.exit(1)
# Idle auto-shutdown state
last_active_time = time.monotonic()
last_audio_pos = get_audio_file_pos(sdlamp2_pid)
while True:
# Dynamic timeout: if power button is held, shorten timeout to detect 3s mark
timeout = IDLE_TIMEOUT
@ -99,6 +145,7 @@ def main():
remaining = POWER_LONG_THRESHOLD - (time.monotonic() - power_press_time)
if remaining <= 0:
# Already past threshold — trigger shutdown now
grab_inputs(event_fds, False)
touch_and_shutdown(disp_fd, original_brightness, screen_on, sdlamp2_pid)
return
timeout = min(timeout, remaining)
@ -109,6 +156,7 @@ def main():
if power_press_time is not None:
held = time.monotonic() - power_press_time
if held >= POWER_LONG_THRESHOLD:
grab_inputs(event_fds, False)
touch_and_shutdown(disp_fd, original_brightness, screen_on, sdlamp2_pid)
return
@ -117,9 +165,23 @@ def main():
if screen_on and power_press_time is None:
set_brightness(disp_fd, 0)
screen_on = False
grab_inputs(event_fds, True)
# Check audio playback activity for idle auto-shutdown
audio_pos = get_audio_file_pos(sdlamp2_pid)
if audio_pos is not None and audio_pos != last_audio_pos:
last_active_time = time.monotonic()
last_audio_pos = audio_pos
# Auto-shutdown if idle long enough (no input + no playback)
if time.monotonic() - last_active_time >= IDLE_SHUTDOWN_TIMEOUT:
grab_inputs(event_fds, False)
touch_and_shutdown(disp_fd, original_brightness, screen_on, sdlamp2_pid)
return
continue
# Process input events from all readable fds
any_activity = False
for fd in readable:
while True:
try:
@ -133,11 +195,13 @@ def main():
INPUT_EVENT_FORMAT, data
)
if ev_type != EV_KEY:
if ev_type == EV_SYN:
continue
# Power button handling
if ev_code == KEY_POWER:
any_activity = True
# Power button handling (EV_KEY only)
if ev_type == EV_KEY and 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
@ -148,20 +212,30 @@ def main():
if screen_on:
set_brightness(disp_fd, 0)
screen_on = False
grab_inputs(event_fds, True)
else:
grab_inputs(event_fds, False)
set_brightness(disp_fd, original_brightness)
screen_on = True
# Between SHORT and LONG threshold: ignore (release before 3s)
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
# Any input activity resets idle shutdown timer
if any_activity:
last_active_time = time.monotonic()
# Any activity wakes screen (d-pad, face buttons, etc.)
if any_activity and not screen_on:
grab_inputs(event_fds, False)
set_brightness(disp_fd, original_brightness)
screen_on = True
def touch_and_shutdown(disp_fd, original_brightness, screen_on, sdlamp2_pid):
"""Signal sdlamp2 to exit and flag for shutdown."""
"""Signal sdlamp2 to exit and flag for shutdown.
Caller must release EVIOCGRAB before calling this.
"""
if not screen_on:
set_brightness(disp_fd, original_brightness)
os.close(disp_fd)