diff --git a/docs/sdlamp2-fsd.md b/docs/sdlamp2-fsd.md index 18ddf80..69dc60b 100644 --- a/docs/sdlamp2-fsd.md +++ b/docs/sdlamp2-fsd.md @@ -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//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. diff --git a/tools/rg35xx-screen-monitor.py b/tools/rg35xx-screen-monitor.py index be36a1d..8cb04c3 100755 --- a/tools/rg35xx-screen-monitor.py +++ b/tools/rg35xx-screen-monitor.py @@ -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 ", 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)