Replaces the Windows port with a Raspberry Pi / Linux implementation on Raspberry Pi OS "Trixie" (Debian 13, aarch64, Wayland/labwc). The Windows code is removed here but preserved on the Windows-Player branch. Entry point ----------- linux/run_linux.py replaces windows/run_win.py. src/main.py stays platform-neutral; all Pi-specific behaviour is injected from linux/. Five bugs that prevented the port (all measured on real hardware) ---------------------------------------------------------------- 1. Kivy's PyPI wheel bundles an SDL2 built WITHOUT the wayland driver, so no window could be created (Trixie has no X server). linux/fix_kivy_sdl2.sh symlinks the system SDL2 over the bundled filename. 2. SDL2 requires WAYLAND_DISPLAY to be *set* - the socket alone is not enough, unlike wlopm. This broke every systemd/cron/autostart launch. linux_display.ensure_session_environment() detects and exports it. 3. Kivy's Clock resolves callbacks via func.__name__; a patch assigned under a different name crashed the player ~20s after a successful start. 4. The inherited signal_screen_activity() shelled out to tvservice, xdotool and ydotool - none exist on Trixie - and mis-escaped 'wlopm --on \*', so the display blanked after 10 minutes. 5. The launchers ran src/main.py directly, bypassing every platform patch and resolving the data directory one level too high. Web links --------- - --ozone-platform-hint=auto does NOT fall back to Wayland on Chromium 152; it aborts. The platform is now chosen explicitly. - The keyring password prompt is suppressed via the ENVIRONMENT, not the flags: launch_env() strips DBUS_SESSION_BUS_ADDRESS for the child so Chromium cannot reach gnome-keyring-daemon. - Teardown kills the whole process group (needs start_new_session=True); previously it silently fell back to terminate() and orphaned children. Video normalisation ------------------- A 4K video cannot play on a Pi 4: ffpyplayer decodes in software, measured at 0.90x realtime (1080p is 3.03x). Oversized media is downscaled to 1920x1080 at sync time using the hardware h264_v4l2m2m encoder (~31s for an 18s clip), triggered by resolution only so already-playable files are untouched. src/media_state.py owns the shared on-disk contract: a .kiwy-converting marker makes the player skip the item while it is being rebuilt, then the converted file is played instead. If nothing is playable at all (a single-item playlist whose only video is converting), the player loops the intro video rather than leaving a blank screen. Also fixed ---------- - network_monitor: replaced netsh/ifconfig/dhclient with nmcli (Trixie uses NetworkManager; ifconfig and dhclient are not even installed). - Removed the Windows-only focus keeper/guardian from main.py. - main.py: duplicate SDL_AUDIODRIVER setdefault (a silent no-op); Settings "Test connection" now uses tempfile.gettempdir(). - config/app_config.json: credentials blanked so a fresh clone runs the first-run setup flow. Verification ------------ linux/test_media_state.py 18/18, test_linux_patches.py 21/21, test_linux_browser_flags.py 27/27. Verified live against a real DigiServer: image -> weblink -> image -> video with correct durations, zero leaked Chromium processes, and no throttling over a 10 minute monitored run.
17 KiB
Kiwy Signage Player — Raspberry Pi OS "Trixie" 64-bit Port Plan
Branch:
Linux-RPI-Player(created fromWindows-PlayerHEADf437aba) Status: Phase 1 COMPLETE — player runs on the Pi. Seedevelopment-track.mdfor measured results, the five blocking bugs and how they were fixed. Date: 2026-09-13
Progress snapshot
| Phase | State |
|---|---|
| 0 — Baseline | ✅ folded into Phase 1 |
| 1 — Runtime bring-up | ✅ done — window, video, heartbeat, clean 60 s run |
| 1b — Windows removal | ✅ done — branch is now Linux-only (see development-track.md) |
| 2 — Web links | 🟡 adapter written, needs end-to-end verification on labwc |
| 3 — Networking & card reader | 🟡 nmcli restart implemented; card reader pending |
| 4 — Install & autostart | ⬜ |
| 5 — 24/7 validation | ⬜ |
Three findings changed the plan materially (details in development-track.md):
- Kivy's PyPI wheel bundles an SDL2 without the wayland driver → no window
on Trixie. Fixed by
linux/fix_kivy_sdl2.sh. (Open question: aptpython3-kivymay avoid this entirely.) - SDL2 needs
WAYLAND_DISPLAYset; the socket alone is not enough, which breaks every systemd/cron/autostart launch. - Kivy's
Clockre-resolves callbacks byfunc.__name__; a mismatched patch crashes the app ~20 s after a successful start.
1. Executive summary
The Windows port works because windows/run_win.py (2306 lines) monkey-patches
src/main.py at runtime and injects Windows-specific web-link adapters into the
existing WeblinkSession abstraction. The src/ core is already
cross-platform by design — that is the single most important finding:
src/weblink_session.pyalready ships a working Linux path (ChromiumSubprocessAdapteris the default adapter).src/network_monitor.pyalready hasIS_WINDOWSbranching.src/video_safety.pypatches Kivy'sVideoFFPyprovider — the same provider used on the Pi.
So this is not a rewrite. It is:
- A new
linux/companion towindows/— an entry point that injects the Linux adapter + platform behaviour (mirrorsrun_win.py, keepsmain.pyclean). - Repairing the existing Linux code paths that were never validated against
Trixie (Wayland/labwc, NetworkManager, no
tvservice, noxdotool). - Fixing the install/deployment layer, which is currently broken for
Trixie/aarch64 (empty offline package repo, wrong wheel platform, broken
shebangs, boot-time
sudocalls).
Verified environment (measured on the target device)
| Item | Value |
|---|---|
| OS | Debian GNU/Linux 13 (trixie) |
| Model | Raspberry Pi 4 Model B Rev 1.4 |
| Kernel | 6.18.39+rpt-rpi-v8 aarch64 |
| Python | 3.13.5 (system) |
| Session | Type=wayland, Desktop=rpd-labwc (labwc, not Wayfire/X11) |
| Compositor | /usr/bin/labwc; user autostart file exists at ~/.config/labwc/autostart |
| Browser | /usr/bin/chromium — Chromium 152.0.7977.82 |
| Kivy | not installed; apt has python3-kivy 2.3.1-1+b1; PyPI has a |
cp313 manylinux_2_17_aarch64 wheel (verified downloadable) |
|
| ffpyplayer / evdev | not installed; apt has python3-evdev 1.9.1-1 |
| Display power | wlopm present; tvservice, xdotool, ydotool MISSING |
| Networking | NetworkManager active; dhcpcd/ifupdown inactive |
sudo |
requires a password (sudo -n fails) |
repo/python-wheels/ |
EMPTY (0 files) — offline install cannot work today |
2. Compatibility matrix
Legend: 🔴 breaks on Pi · 🟡 works but wrong/poor · 🟢 already fine
| # | Component | Windows assumption | Trixie reality | Action |
|---|---|---|---|---|
| 1 | Entry point | run_win.py patches everything |
no Linux equivalent — main.py used bare |
🟡 create linux/run_linux.py |
| 2 | Web-link engine | Chrome/Edge subprocess + WebView2/CEF | Chromium subprocess (default adapter) | 🟡 dedicated Linux adapter with --kiosk/Ozone flags |
| 3 | Web-link launch flags | --start-fullscreen --start-maximized, DPI-aware sizing |
labwc honours --kiosk; needs --ozone-platform=wayland |
🔴 Linux-specific flag set |
| 4 | Browser hand-off leak | fixed by --user-data-dir |
same failure mode exists | 🔴 add --user-data-dir on Linux too |
| 5 | Desktop flash on transition | _Win32Overlay + _Win32Backdrop |
no Win32; wayland compositor owns stacking | 🔴 needs Kivy-side black cover + labwc rule |
| 6 | Window raise/focus | SetForegroundWindow, AttachThreadInput |
Window.raise_window() is ~no-op on Wayland |
🟡 focus guardian is harmless no-op; disable on Linux |
| 7 | Keep display awake | SetThreadExecutionState, screensaver API |
signal_screen_activity() shells out to X11 tools |
🔴 rewrite for wlopm (see #8) |
| 8 | signal_screen_activity() |
— | runs tvservice(missing), xdotool(missing), xset/xrandr(no X DPMS), ydotool(missing), wlopm with a buggy \* escape |
🔴 rewrite, keep-awake via wlopm --on '*' |
| 9 | Idle/screensaver conflict | n/a | ~/.config/labwc/autostart runs swayidle -w timeout 600 'wlopm --off *' → fights the player |
🔴 disable/neutralise for kiosk |
| 10 | GL backend | angle_sdl2 |
Mesa/V3D; gl/gles/sdl2 |
🟡 set correct backend in run_linux.py |
| 11 | Audio driver | directsound |
PipeWire (ALSA-compat / Pulse) | 🟡 alsa,pulse,dummy; a duplicate SDL_AUDIODRIVER line makes the 2nd a no-op |
| 12 | Card reader | Raw Input + win_card_reader.py |
evdev (native path already in main.py) |
🟡 install python3-evdev; udev/permissions rule |
| 13 | Interaction watcher | GetCursorPos pointer tap |
/dev/input/event* raw fds (already implemented) |
🟡 needs input group perms (user pi already in input) |
| 14 | Offline install | n/a | repo/python-wheels/ empty; download_offline_packages.sh uses --platform linux_armv7l (32-bit) |
🔴 regenerate for aarch64 / cp313 |
| 15 | install.sh shebang |
n/a | literal #\!/bin/bash (escaped !) → not directly executable |
🔴 fix (3 places incl. heredocs) |
| 16 | install.sh deps |
n/a | installs libsdl2-dev/libav*-dev build toolchain; apt python3-kivy exists |
🟡 prefer apt Kivy + wheels |
| 17 | install.sh autostart |
n/a | writes only XDG .desktop; labwc uses ~/.config/labwc/autostart |
🔴 add labwc hook + systemd unit |
| 18 | start.sh display setup |
n/a | configure_display_resolution() writes /boot/config.txt (wrong path on Trixie → /boot/firmware/config.txt) and calls xrandr/tvservice; sudo will prompt at boot |
🔴 remove/repair — boot-hang risk |
| 19 | WiFi restart | netsh wlan |
rfkill/ifconfig/dhclient — ifconfig+dhclient not installed, all sudo → password prompt |
🔴 move to nmcli, passwordless sudoers |
| 20 | setup_wifi_control.sh |
n/a | allows /sbin/ifconfig, /sbin/dhclient — paths don't exist |
🔴 rewrite for Trixie |
| 21 | Orientation / max_resolution |
Window.size constraint |
fullscreen ignores Window.size; rotation is a compositor concern |
🔴 implement via wlr-randr/cmdline |
| 22 | src/signageplayer.ini |
— | never loaded by any code (dead file) | 🟡 delete or wire up |
| 23 | prewarm weblink browser |
desktop CPU budget | 2 extra Chromium processes on a Pi 4 | 🟡 default prewarm:false on Pi |
| 24 | Watchdog / 24-7 | watchdog.ps1 |
start.sh exists and is sound (heartbeat + stop flag) |
🟢 keep, with fixes from #18 |
3. Proposed architecture
Mirror the existing, proven Windows pattern. Keep src/main.py cross-platform
per the repo ground rules — no Linux-only imports added to main.py.
src/ # shared, cross-platform core (unchanged philosophy)
main.py # shared application; PI patches live in linux/
weblink_session.py # WeblinkSession + ChromiumSubprocessAdapter (default)
linux/ # NEW — the Linux counterpart of windows/
run_linux.py # ✅ entry point: env, session, adapter injection, patches
linux_display.py # ✅ wlopm keep-awake, swayidle neutralisation, rotation
linux_browser.py # ✅ LinuxChromiumAdapter (kiosk + Ozone + user-data-dir)
fix_kivy_sdl2.sh # ✅ system SDL2 (wayland-capable) over the bundled one
test_linux_patches.py # ✅ regression tests (29 checks)
_probe_video.py # ✅ ad-hoc video decode/playback probe
development-track.md # ✅ measured findings and bug log
RPI_TRIXIE_PORT_PLAN.md # ✅ this file
install_linux.sh # ⬜ upcoming — Trixie-correct installer
build_offline_aarch64.sh # ⬜ upcoming — cp313/aarch64 wheels
start_player.sh # ⬜ upcoming — kiosk launcher (fixed start.sh)
watchdog.sh # ⬜ upcoming — crash+heartbeat supervisor
kiwy-player.service # ⬜ upcoming — systemd unit
kiwy-signage-wifi.sudoers # ⬜ upcoming — nmcli-only passwordless rules
.github/instructions/
kiwy-linux-rpi.instructions.md # ⬜ upcoming — build/deploy rules for the Pi
run_linux.py responsibilities (deliberately the same shape as run_win.py):
- Set Linux env vars before importing
main(SDL_VIDEODRIVER=wayland,x11,dummy,KIVY_GL_BACKEND,SDL_AUDIODRIVER,KIVY_VIDEO/KIVY_AUDIO=ffpyplayer). - Import
main, then assignSignagePlayer.weblink_adapter_factory→[LinuxChromiumAdapter]. - Patch
signal_screen_activity→linux_displayimplementation. - Skip the Win32-only focus guardian / bring-to-front paths.
- Provide a fatal-error surface that works without a console (log file + optional on-screen error label).
Why an adapter instead of patching play_weblink(): the WeblinkSession
abstraction already exists precisely for this, and it is what removed the
z-order/leak bugs on Windows. Reusing it means the Pi inherits the verified
launch → visibility → interaction → teardown state machine for free.
4. Phased implementation plan
Phase 0 — Baseline (no product change)
- Fix the
install.sh/start.shshebangs (#\!→#!) so scripts are executable. - Boot the player on this Pi manually (
python3 src/main.pyfrom a venv) and capture a baseline: does Kivy start on labwc/Wayland? GL backend? audio? video? - Record findings in
linux/development-track.md.
Exit criteria: a screenshot/log showing the player window on the Pi, or a precise first-failure diagnosis.
Phase 1 — Runtime bring-up (linux/run_linux.py, linux/linux_display.py)
- Create the venv and install deps (
kivy,ffpyplayer,evdev,requests,bcrypt,aiohttp). run_linux.py: env setup + adapter injection +signal_screen_activityreplacement.- Fix the SDL2/Wayland blocker (
linux/fix_kivy_sdl2.sh). signal_screen_activity():wlopm --on '*'viasubprocess(noos.system),vcgencmd display_powerbackstop, X11 fallbacks; droppedtvservice/xdotool/ydotool.- Neutralise the
swayidleidle blanker. - Gate the Win32 focus guardian/keeper on Linux.
- Fix the duplicate
SDL_AUDIODRIVERline. - Point the launchers at
linux/run_linux.pyinstead ofsrc/main.py. - Regression test
linux/test_linux_patches.py(29 checks).
Exit criteria met: player runs fullscreen on labwc, video decodes and plays
(duration=6.0, position advancing, texture rendered), heartbeat written,
60 s run with zero errors and a clean shutdown. 24/7 soak still pending (Phase 5).
Phase 2 — Web links on Chromium/Wayland (linux/linux_browser.py)
LinuxChromiumAdapter(ChromiumSubprocessAdapter):--kiosk,--ozone-platform=wayland(with auto fallback), dedicated--user-data-dir,--autoplay-policy=no-user-gesture-required,--hide-scrollbars,--disable-pinch,--start-fullscreenonly where--kioskmisbehaves.- Implement
wait_visible()for Linux — verify the Chromium window exists (/proc/<pid>/+ Wayland toplevel check, or a--remote-debugging-portprobe) instead of only "process still alive". - Transition masking without Win32: Kivy-side black cover + verify no desktop flash
under labwc; if needed, an
rc.xmlwindow rule pinning the Kivy window. - Teardown: kill the full process tree (
start_new_session=True+os.killpg) so GPU/renderer children don't leak (the Linux twin oftaskkill /T). - Default
weblink.prewarm=falseon Pi to save CPU/RAM.
Exit criteria: image → video → weblink → image cycle with no flash, no leaked
chromium processes, correct duration + interaction postponement.
Phase 3 — Networking & card reader
src/network_monitor.py: replacerfkill/ifconfig/dhclientwithnmcli(nmcli radio wifi off/on,nmcli device disconnect/connect) — Trixie uses NetworkManager.kiwy-signage-wifi.sudoers: passwordless rules for exactly thenmcli/rfkillbinaries used.- Install
python3-evdev(apt) + udev rule so the card reader works withoutinputgroup hacks.
Phase 4 — Install & autostart (Trixie-native)
build_offline_aarch64.sh: populaterepo/python-wheels/with cp313 aarch64 wheels (kivy,ffpyplayer,evdev,requests,bcrypt,aiohttp, …). Replace the--platform linux_armv7llogic.install_linux.sh: prefer aptpython3-kivy/python3-evdev(fast, no build toolchain); venv with--system-site-packages; drop the--break-system-packagesfallback.- Autostart: append to
~/.config/labwc/autostartand ship a systemd unit (verify which one RPi OS Trixierpd-labwcactually honours — validation item). watchdog.sh: repairconfigure_display_resolution()(wrong/boot/config.txtpath,sudoat boot = hang risk); make all boot-time steps non-interactive.- Orientation /
max_resolution: implement rotation viawlr-randr(Wayland) orcmdline.txtvideo=...,rotate=; document thatWindow.sizecannot change a fullscreen mode.
Exit criteria: clean install on a wiped Pi → reboots into playback unattended.
Phase 5 — 24/7 validation & docs
- Overnight soak (≥12 h): mixed playlist, verify heartbeat, no leak growth
(
pschromium count), no memory growth. - Crash/hang drills: kill the player, freeze it, power-cut it → watchdog recovery.
linux/development-track.md+.github/instructions/kiwy-linux-rpi.instructions.md.- Update
PLAYER_VERSIONand the release checklist.
5. Risks & mitigations
| Risk | Mitigation |
|---|---|
| Wayland client stacking — Chromium kiosk may not reliably return focus to Kivy | validate early (Phase 2); fallback = labwc rc.xml rule or run the session under XWayland |
Chromium --kiosk + labwc fullscreen semantics |
empirical flag matrix recorded in the dev-track (like the Windows "tested & rejected" log) |
sudo password prompt at boot hangs the watchdog |
all boot-time steps must be non-interactive; sudoers written at install time |
| Pi 4 software H.265 decode is heavy | document supported codecs; prefer H.264; keep video_safety bounded join |
| Offline wheels for cp313/aarch64 may be incomplete | Phase 0 verifies pip download for every requirement before Phase 4 |
src/main.py drift — Linux fixes leaking into shared code |
follow the repo rule: platform code lives in linux/, main.py only gains guarded capability checks |
6. Validation checklist (per phase, on the real device)
python -m py_compileon every touched file- Player starts fullscreen on labwc without a desktop flash
- Display never blanks (24 h)
- Playlist: image → video → weblink → image, correct durations
- Weblink: kiosk fullscreen, no leaked
chromiumprocesses after the item - Touch interaction postpones the weblink advance
- Card reader authenticates
- WiFi restart recovers from an unplugged AP without a password prompt
- Reboot → playback resumes unattended
kill -9the player → watchdog restarts within 60 s
7. Open decisions (need input before Phase 1)
- Kivy source: apt
python3-kivy(fast, offline-friendly, distro-managed) vs PyPI wheel in a venv (matches the Windows Kivy 2.3.1 + full control)? - Session target: stay on the stock
rpd-labwckiosk session, or ship a dedicated minimal labwc session (no panel, noswayidle) for the player? - Autostart mechanism: systemd (system or user) vs
~/.config/labwc/autostart? - Orientation: is Portrait support actually required for this deployment?
- Offline install: must the Pi install work with no internet (i.e. vendor the
aarch64 wheels into
repo/), or is online install acceptable? - Card reader: is it in scope for the Pi, or is authentication keypad/quickconnect only?