From 3fcae8ea5e8c3b195ef92750347dc4999f4d8108 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Feb 2026 21:47:48 +0100 Subject: [PATCH] Fix power button shutdown by consolidating input handling in screen monitor Both the evtest-based power monitor and the Python screen monitor were reading /dev/input/event0 simultaneously, causing missed events on the device's Linux 4.9 kernel. Moved long-press shutdown into the screen monitor (which already reads event0 directly) and removed the evtest dependency entirely. Co-Authored-By: Claude Opus 4.6 --- docs/sdlamp2-fsd.md | 6 ++++ tools/rg35xx-screen-monitor.py | 59 ++++++++++++++++++++++++++++------ tools/rg35xx-wrapper.sh | 46 +++++--------------------- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/docs/sdlamp2-fsd.md b/docs/sdlamp2-fsd.md index 59830fe..b814a23 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 — Fix power button shutdown regression + +- **Consolidated input handling**: Power button long-press shutdown is now handled by `rg35xx-screen-monitor.py` instead of a separate `evtest`-based monitor in the wrapper. Both monitors were reading `/dev/input/event0` simultaneously, causing the `evtest` parser to miss power button events on the device's Linux 4.9 kernel. +- **Timer-based long press**: The screen monitor uses a dynamic `select()` timeout to detect the 3-second hold threshold while the button is still held, rather than waiting for release. On long press, it touches `/tmp/.sdlamp2_shutdown` and sends SIGTERM to sdlamp2. +- **Removed evtest dependency**: The `monitor_power_button()` function and `evtest` pipe are removed from `rg35xx-wrapper.sh`. The screen monitor accepts the sdlamp2 PID as a command-line argument and launches after sdlamp2. + ### 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. diff --git a/tools/rg35xx-screen-monitor.py b/tools/rg35xx-screen-monitor.py index 2b3d85f..be36a1d 100755 --- a/tools/rg35xx-screen-monitor.py +++ b/tools/rg35xx-screen-monitor.py @@ -1,23 +1,27 @@ #!/usr/bin/env python3 """ -Screen idle timeout and power button toggle for RG35XX Plus. +Screen idle timeout, power button toggle, and long-press shutdown 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. +toggles the screen on/off. A long press (3s+) triggers clean shutdown. Launched by rg35xx-wrapper.sh alongside sdlamp2. Killed on cleanup. + +Usage: rg35xx-screen-monitor.py """ import fcntl import os +import select import signal import struct import sys import time IDLE_TIMEOUT = 15 # seconds -POWER_HOLD_THRESHOLD = 2.0 # seconds — short press if released before this +POWER_SHORT_THRESHOLD = 2.0 # seconds — short press if released before this +POWER_LONG_THRESHOLD = 3.0 # seconds — shutdown if held this long # Allwinner /dev/disp ioctl commands DISP_GET_BRIGHTNESS = 0x103 @@ -50,6 +54,12 @@ def set_brightness(disp_fd, value): def main(): + if len(sys.argv) < 2: + print("Usage: rg35xx-screen-monitor.py ", file=sys.stderr) + sys.exit(1) + + sdlamp2_pid = int(sys.argv[1]) + disp_fd = os.open("/dev/disp", os.O_RDWR) original_brightness = get_brightness(disp_fd) if original_brightness == 0: @@ -82,14 +92,29 @@ def main(): os.close(disp_fd) sys.exit(1) - import select - while True: - readable, _, _ = select.select(event_fds, [], [], IDLE_TIMEOUT) + # Dynamic timeout: if power button is held, shorten timeout to detect 3s mark + timeout = IDLE_TIMEOUT + if power_press_time is not None: + remaining = POWER_LONG_THRESHOLD - (time.monotonic() - power_press_time) + if remaining <= 0: + # Already past threshold — trigger shutdown now + touch_and_shutdown(disp_fd, original_brightness, screen_on, sdlamp2_pid) + return + timeout = min(timeout, remaining) + + readable, _, _ = select.select(event_fds, [], [], timeout) + + # Check long-press threshold (whether select returned due to timeout or input) + if power_press_time is not None: + held = time.monotonic() - power_press_time + if held >= POWER_LONG_THRESHOLD: + touch_and_shutdown(disp_fd, original_brightness, screen_on, sdlamp2_pid) + return if not readable: - # Timeout — no input for IDLE_TIMEOUT seconds - if screen_on: + # Timeout with no input — turn off screen if idle + if screen_on and power_press_time is None: set_brightness(disp_fd, 0) screen_on = False continue @@ -118,7 +143,7 @@ def main(): 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: + if hold_duration < POWER_SHORT_THRESHOLD: # Short press — toggle screen if screen_on: set_brightness(disp_fd, 0) @@ -126,6 +151,7 @@ def main(): else: 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 @@ -134,5 +160,20 @@ def main(): screen_on = True +def touch_and_shutdown(disp_fd, original_brightness, screen_on, sdlamp2_pid): + """Signal sdlamp2 to exit and flag for shutdown.""" + if not screen_on: + set_brightness(disp_fd, original_brightness) + os.close(disp_fd) + try: + open("/tmp/.sdlamp2_shutdown", "w").close() + except OSError: + pass + try: + os.kill(sdlamp2_pid, signal.SIGTERM) + except OSError: + pass + + if __name__ == "__main__": main() diff --git a/tools/rg35xx-wrapper.sh b/tools/rg35xx-wrapper.sh index 1382ec5..08564e6 100755 --- a/tools/rg35xx-wrapper.sh +++ b/tools/rg35xx-wrapper.sh @@ -4,10 +4,9 @@ # # Launched by dmenu_ln instead of sdlamp2 directly. Handles device-specific # concerns that don't belong in the player binary: -# 1. Monitor power button and trigger clean shutdown +# 1. Screen idle timeout, power button screen toggle, and long-press shutdown # 2. Display the stock firmware's shutdown screen (goodbye.png → /dev/fb0) -# 3. Screen idle timeout (off after 15s) and power button screen toggle -# 4. Launch sdlamp2 as the main process +# 3. 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 @@ -21,53 +20,24 @@ SCREEN_MONITOR="/mnt/vendor/bin/rg35xx-screen-monitor.py" # works fine even when sdlamp2 replaces the stock menu. Hotspot/AP mode isn't # needed — SSH access works over the shared network. -# --- Power button monitor --- -# axp2202-pek on /dev/input/event0 sends KEY_POWER (code 116). -# logind has HandlePowerKey=ignore, so we handle it here. -POWER_EVENT_DEV="/dev/input/event0" - -monitor_power_button() { - POWER_TIMER_PID="" - evtest "$POWER_EVENT_DEV" 2>/dev/null | while read -r line; do - case "$line" in - *"code 116"*"value 1"*) - # Power button pressed — start 3-second hold timer - ( trap 'exit 0' TERM; sleep 3; touch /tmp/.sdlamp2_shutdown; kill -TERM "$SDLAMP2_PID" 2>/dev/null ) & - POWER_TIMER_PID=$! - ;; - *"code 116"*"value 0"*) - # Power button released — cancel timer if still running - if [ -n "$POWER_TIMER_PID" ]; then - kill "$POWER_TIMER_PID" 2>/dev/null - wait "$POWER_TIMER_PID" 2>/dev/null - POWER_TIMER_PID="" - fi - ;; - esac - 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" & SDLAMP2_PID=$! -monitor_power_button & -MONITOR_PID=$! +# --- Screen monitor (idle timeout + power button toggle + long-press shutdown) --- +# Must launch after sdlamp2 so PID is available. Reads /dev/input/event0 +# directly for power button events — no evtest dependency. +python3 "$SCREEN_MONITOR" "$SDLAMP2_PID" & +SCREEN_MONITOR_PID=$! # Wait for sdlamp2 to finish (signal or normal exit). wait "$SDLAMP2_PID" SDLAMP2_EXIT=$? # --- Cleanup --- -# Kill the screen monitor (SIGTERM restores brightness) and power button monitor. +# Kill the screen monitor (SIGTERM restores brightness). 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 # loop doesn't relaunch dmenu_ln (which would take over the framebuffer and