Compare commits

..

2 Commits

Author SHA1 Message Date
8123b9a99f Remember last cassette on startup, always pause on cassette switch
Persist the current cassette filename to last_cassette.txt so the player
resumes the same cassette after restart. Switching cassettes now always
lands in a paused state to avoid jarring mid-playback jumps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 21:55:48 +01:00
3fcae8ea5e 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 <noreply@anthropic.com>
2026-02-13 21:47:48 +01:00
4 changed files with 105 additions and 49 deletions

View File

@ -43,6 +43,17 @@ This document specifies the functional requirements for an SDL2 based media play
## 6. Changelog ## 6. Changelog
### 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.
- **Pause on cassette switch**: Switching cassettes (prev/next) now always lands in a paused state, even if the player was playing. This avoids the jarring effect of immediately resuming from a saved position in a different cassette.
### 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 ### 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. - **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.

View File

@ -197,6 +197,37 @@ static void adjust_volume(float delta) {
save_volume(); save_volume();
} }
/* --- Last cassette persistence --- */
static void save_last_cassette(const char* filename) {
char path[1024];
snprintf(path, sizeof(path), "%s/last_cassette.txt", audio_dir);
FILE* f = fopen(path, "w");
if (f) {
fprintf(f, "%s\n", filename);
fclose(f);
}
}
static int load_last_cassette(void) {
char path[1024];
snprintf(path, sizeof(path), "%s/last_cassette.txt", audio_dir);
FILE* f = fopen(path, "r");
if (!f) return 0;
char name[256];
if (!fgets(name, sizeof(name), f)) {
fclose(f);
return 0;
}
fclose(f);
char* nl = strchr(name, '\n');
if (nl) *nl = '\0';
for (int i = 0; i < num_audio_files; i++) {
if (strcmp(audio_files[i], name) == 0) return i;
}
return 0;
}
/* --- File scanning --- */ /* --- File scanning --- */
static int has_audio_extension(const char* name) { static int has_audio_extension(const char* name) {
@ -484,11 +515,14 @@ static void switch_file(int index) {
decoder_seek(pos); decoder_seek(pos);
} }
save_last_cassette(current_file);
char title[768]; char title[768];
snprintf(title, sizeof(title), "SDLamp2 - %s", current_file); snprintf(title, sizeof(title), "SDLamp2 - %s", current_file);
SDL_SetWindowTitle(window, title); SDL_SetWindowTitle(window, title);
SDL_PauseAudioDevice(audio_device, paused); paused = SDL_TRUE;
SDL_PauseAudioDevice(audio_device, 1);
return; return;
} }
@ -674,7 +708,7 @@ int main(int argc, char** argv) {
scan_audio_files(audio_dir); scan_audio_files(audio_dir);
volume = load_volume(); volume = load_volume();
if (num_audio_files > 0) { if (num_audio_files > 0) {
switch_file(0); switch_file(load_last_cassette());
} else { } else {
SDL_SetWindowTitle(window, "SDLamp2 - No audio files found"); SDL_SetWindowTitle(window, "SDLamp2 - No audio files found");
} }

View File

@ -1,23 +1,27 @@
#!/usr/bin/env python3 #!/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 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. (via Allwinner /dev/disp SET_BRIGHTNESS ioctl) after 15s of no input.
Any button press wakes the screen. A short power button press (<2s) 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. Launched by rg35xx-wrapper.sh alongside sdlamp2. Killed on cleanup.
Usage: rg35xx-screen-monitor.py <sdlamp2_pid>
""" """
import fcntl import fcntl
import os import os
import select
import signal import signal
import struct import struct
import sys import sys
import time import time
IDLE_TIMEOUT = 15 # seconds 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 # Allwinner /dev/disp ioctl commands
DISP_GET_BRIGHTNESS = 0x103 DISP_GET_BRIGHTNESS = 0x103
@ -50,6 +54,12 @@ def set_brightness(disp_fd, value):
def main(): def main():
if len(sys.argv) < 2:
print("Usage: rg35xx-screen-monitor.py <sdlamp2_pid>", file=sys.stderr)
sys.exit(1)
sdlamp2_pid = int(sys.argv[1])
disp_fd = os.open("/dev/disp", os.O_RDWR) disp_fd = os.open("/dev/disp", os.O_RDWR)
original_brightness = get_brightness(disp_fd) original_brightness = get_brightness(disp_fd)
if original_brightness == 0: if original_brightness == 0:
@ -82,14 +92,29 @@ def main():
os.close(disp_fd) os.close(disp_fd)
sys.exit(1) sys.exit(1)
import select
while True: 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: if not readable:
# Timeout — no input for IDLE_TIMEOUT seconds # Timeout with no input — turn off screen if idle
if screen_on: if screen_on and power_press_time is None:
set_brightness(disp_fd, 0) set_brightness(disp_fd, 0)
screen_on = False screen_on = False
continue continue
@ -118,7 +143,7 @@ def main():
elif ev_value == 0 and power_press_time is not None: # release elif ev_value == 0 and power_press_time is not None: # release
hold_duration = time.monotonic() - power_press_time hold_duration = time.monotonic() - power_press_time
power_press_time = None power_press_time = None
if hold_duration < POWER_HOLD_THRESHOLD: if hold_duration < POWER_SHORT_THRESHOLD:
# Short press — toggle screen # Short press — toggle screen
if screen_on: if screen_on:
set_brightness(disp_fd, 0) set_brightness(disp_fd, 0)
@ -126,6 +151,7 @@ def main():
else: else:
set_brightness(disp_fd, original_brightness) set_brightness(disp_fd, original_brightness)
screen_on = True screen_on = True
# Between SHORT and LONG threshold: ignore (release before 3s)
continue continue
# Any other key press — wake screen if off # Any other key press — wake screen if off
@ -134,5 +160,20 @@ def main():
screen_on = True 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__": if __name__ == "__main__":
main() main()

View File

@ -4,10 +4,9 @@
# #
# Launched by dmenu_ln instead of sdlamp2 directly. Handles device-specific # Launched by dmenu_ln instead of sdlamp2 directly. Handles device-specific
# 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. Screen idle timeout, power button screen toggle, and long-press 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. Screen idle timeout (off after 15s) and power button screen toggle # 3. Launch sdlamp2 as the main process
# 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
@ -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 # works fine even when sdlamp2 replaces the stock menu. Hotspot/AP mode isn't
# needed — SSH access works over the shared network. # 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 --- # --- Launch sdlamp2 ---
# Run in background so we can capture PID for the power button monitor.
"$SDLAMP2" "$AUDIO_DIR" & "$SDLAMP2" "$AUDIO_DIR" &
SDLAMP2_PID=$! SDLAMP2_PID=$!
monitor_power_button & # --- Screen monitor (idle timeout + power button toggle + long-press shutdown) ---
MONITOR_PID=$! # 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 for sdlamp2 to finish (signal or normal exit).
wait "$SDLAMP2_PID" wait "$SDLAMP2_PID"
SDLAMP2_EXIT=$? SDLAMP2_EXIT=$?
# --- Cleanup --- # --- 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 kill "$SCREEN_MONITOR_PID" 2>/dev/null
wait "$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 # 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 # loop doesn't relaunch dmenu_ln (which would take over the framebuffer and