Skin template system, separate prev sprite, reorganize device scripts
Add tools/gen_skin_template.py to generate a labeled 642x420 PNG template for creating custom spritesheets. Move rg35xx device scripts from tools/ to device/rg35xx/. Point prev_sprite at its own cell (bottom-center) so Prev and Next can have distinct icons. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
253
device/rg35xx/rg35xx-screen-monitor.py
Executable file
253
device/rg35xx/rg35xx-screen-monitor.py
Executable file
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
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 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.
|
||||
|
||||
Usage: rg35xx-screen-monitor.py <sdlamp2_pid>
|
||||
"""
|
||||
import fcntl
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
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
|
||||
|
||||
# Allwinner /dev/disp ioctl commands
|
||||
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)
|
||||
|
||||
EVENT_DEVICES = ["/dev/input/event0", "/dev/input/event1", "/dev/input/event2"]
|
||||
|
||||
|
||||
def disp_ioctl(fd, cmd, screen=0, value=0):
|
||||
# Allwinner disp ioctls take an array of 4 unsigned longs as arg
|
||||
args = struct.pack("@4L", screen, value, 0, 0)
|
||||
result = fcntl.ioctl(fd, cmd, args)
|
||||
return struct.unpack("@4L", result)[0]
|
||||
|
||||
|
||||
def get_brightness(disp_fd):
|
||||
return disp_ioctl(disp_fd, DISP_GET_BRIGHTNESS)
|
||||
|
||||
|
||||
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)
|
||||
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:
|
||||
original_brightness = 50 # sensible default if already off
|
||||
|
||||
screen_on = True
|
||||
power_press_time = None
|
||||
|
||||
# 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)
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM, restore_and_exit)
|
||||
signal.signal(signal.SIGINT, restore_and_exit)
|
||||
|
||||
# Open input devices (non-blocking)
|
||||
event_fds = []
|
||||
for path in EVENT_DEVICES:
|
||||
try:
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
|
||||
event_fds.append(fd)
|
||||
except OSError as e:
|
||||
print(f"screen-monitor: cannot open {path}: {e}", file=sys.stderr)
|
||||
|
||||
if not event_fds:
|
||||
print("screen-monitor: no input devices available, exiting", file=sys.stderr)
|
||||
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
|
||||
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
|
||||
grab_inputs(event_fds, False)
|
||||
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:
|
||||
grab_inputs(event_fds, False)
|
||||
touch_and_shutdown(disp_fd, original_brightness, screen_on, sdlamp2_pid)
|
||||
return
|
||||
|
||||
if not readable:
|
||||
# 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
|
||||
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:
|
||||
data = os.read(fd, INPUT_EVENT_SIZE)
|
||||
except BlockingIOError:
|
||||
break
|
||||
if len(data) < INPUT_EVENT_SIZE:
|
||||
break
|
||||
|
||||
_sec, _usec, ev_type, ev_code, ev_value = struct.unpack(
|
||||
INPUT_EVENT_FORMAT, data
|
||||
)
|
||||
|
||||
if ev_type == EV_SYN:
|
||||
continue
|
||||
|
||||
# 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
|
||||
hold_duration = time.monotonic() - power_press_time
|
||||
power_press_time = None
|
||||
if hold_duration < POWER_SHORT_THRESHOLD:
|
||||
# Short press — toggle screen
|
||||
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_activity = 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.
|
||||
|
||||
Caller must release EVIOCGRAB before calling this.
|
||||
"""
|
||||
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()
|
||||
69
device/rg35xx/rg35xx-wrapper.sh
Executable file
69
device/rg35xx/rg35xx-wrapper.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# RG35XX Plus wrapper for sdlamp2
|
||||
#
|
||||
# Launched by dmenu_ln instead of sdlamp2 directly. Handles device-specific
|
||||
# concerns that don't belong in the player binary:
|
||||
# 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. 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
|
||||
|
||||
SDLAMP2="/mnt/vendor/bin/sdlamp2"
|
||||
AUDIO_DIR="/mnt/sdcard/Music"
|
||||
SCREEN_MONITOR="/mnt/vendor/bin/rg35xx-screen-monitor.py"
|
||||
|
||||
# --- WiFi hotspot ---
|
||||
# Deprioritized: connecting the device as a WiFi client to a shared network
|
||||
# works fine even when sdlamp2 replaces the stock menu. Hotspot/AP mode isn't
|
||||
# needed — SSH access works over the shared network.
|
||||
|
||||
# --- Launch sdlamp2 ---
|
||||
"$SDLAMP2" "$AUDIO_DIR" &
|
||||
SDLAMP2_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).
|
||||
kill "$SCREEN_MONITOR_PID" 2>/dev/null
|
||||
wait "$SCREEN_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
|
||||
# overwrite sdlamp2's shutdown screen).
|
||||
if [ -f /tmp/.sdlamp2_shutdown ]; then
|
||||
rm -f /tmp/.sdlamp2_shutdown
|
||||
# Restore backlight brightness before writing the shutdown screen.
|
||||
# The screen monitor's SIGTERM handler tries to restore brightness, but
|
||||
# the Allwinner /dev/disp driver resets it to 0 when the fd is closed.
|
||||
# Re-set it here so goodbye.png is actually visible.
|
||||
# Then display the stock firmware's shutdown screen via framebuffer.
|
||||
# goodbye.png is 640x480 RGB — exactly matches the display.
|
||||
# /dev/fb0 is 32bpp BGRA, so we swap R/B channels and write raw pixels.
|
||||
python3 -c "
|
||||
import struct, fcntl
|
||||
disp = open('/dev/disp', 'wb')
|
||||
fcntl.ioctl(disp, 0x102, struct.pack('@4L', 0, 50, 0, 0))
|
||||
disp.close()
|
||||
from PIL import Image
|
||||
img = Image.open('/mnt/vendor/res1/shutdown/goodbye.png').convert('RGBA')
|
||||
r, g, b, a = img.split()
|
||||
with open('/dev/fb0', 'wb') as f:
|
||||
f.write(Image.merge('RGBA', (b, g, r, a)).tobytes())
|
||||
" 2>/dev/null
|
||||
poweroff
|
||||
sleep 30
|
||||
fi
|
||||
|
||||
exit "$SDLAMP2_EXIT"
|
||||
Reference in New Issue
Block a user