Compare commits
2 Commits
2142ed7629
...
8123b9a99f
| Author | SHA1 | Date | |
|---|---|---|---|
| 8123b9a99f | |||
| 3fcae8ea5e |
@ -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.
|
||||||
|
|||||||
@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user