Files
Kiwy-Signage/linux/RPI_TRIXIE_PORT_PLAN.md
ske087 3ac7f836c4 Port the player to Raspberry Pi OS Trixie 64-bit (Linux-only branch)
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.
2026-09-13 21:57:49 +03:00

17 KiB

Kiwy Signage Player — Raspberry Pi OS "Trixie" 64-bit Port Plan

Branch: Linux-RPI-Player (created from Windows-Player HEAD f437aba) Status: Phase 1 COMPLETE — player runs on the Pi. See development-track.md for 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):

  1. Kivy's PyPI wheel bundles an SDL2 without the wayland driver → no window on Trixie. Fixed by linux/fix_kivy_sdl2.sh. (Open question: apt python3-kivy may avoid this entirely.)
  2. SDL2 needs WAYLAND_DISPLAY set; the socket alone is not enough, which breaks every systemd/cron/autostart launch.
  3. Kivy's Clock re-resolves callbacks by func.__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.py already ships a working Linux path (ChromiumSubprocessAdapter is the default adapter).
  • src/network_monitor.py already has IS_WINDOWS branching.
  • src/video_safety.py patches Kivy's VideoFFPy provider — the same provider used on the Pi.

So this is not a rewrite. It is:

  1. A new linux/ companion to windows/ — an entry point that injects the Linux adapter + platform behaviour (mirrors run_win.py, keeps main.py clean).
  2. Repairing the existing Linux code paths that were never validated against Trixie (Wayland/labwc, NetworkManager, no tvservice, no xdotool).
  3. Fixing the install/deployment layer, which is currently broken for Trixie/aarch64 (empty offline package repo, wrong wheel platform, broken shebangs, boot-time sudo calls).

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/chromiumChromium 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/dhclientifconfig+dhclient not installed, all sudo → password prompt 🔴 move to nmcli, passwordless sudoers
20 setup_wifi_control.sh n/a allows /sbin/ifconfig, /sbin/dhclientpaths 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):

  1. Set Linux env vars before importing main (SDL_VIDEODRIVER=wayland,x11,dummy, KIVY_GL_BACKEND, SDL_AUDIODRIVER, KIVY_VIDEO/KIVY_AUDIO=ffpyplayer).
  2. Import main, then assign SignagePlayer.weblink_adapter_factory[LinuxChromiumAdapter].
  3. Patch signal_screen_activitylinux_display implementation.
  4. Skip the Win32-only focus guardian / bring-to-front paths.
  5. 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.sh shebangs (#\!#!) so scripts are executable.
  • Boot the player on this Pi manually (python3 src/main.py from 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_activity replacement.
  • Fix the SDL2/Wayland blocker (linux/fix_kivy_sdl2.sh).
  • signal_screen_activity(): wlopm --on '*' via subprocess (no os.system), vcgencmd display_power backstop, X11 fallbacks; dropped tvservice/xdotool/ydotool.
  • Neutralise the swayidle idle blanker.
  • Gate the Win32 focus guardian/keeper on Linux.
  • Fix the duplicate SDL_AUDIODRIVER line.
  • Point the launchers at linux/run_linux.py instead of src/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).

  • LinuxChromiumAdapter(ChromiumSubprocessAdapter): --kiosk, --ozone-platform=wayland (with auto fallback), dedicated --user-data-dir, --autoplay-policy=no-user-gesture-required, --hide-scrollbars, --disable-pinch, --start-fullscreen only where --kiosk misbehaves.
  • Implement wait_visible() for Linux — verify the Chromium window exists (/proc/<pid>/ + Wayland toplevel check, or a --remote-debugging-port probe) instead of only "process still alive".
  • Transition masking without Win32: Kivy-side black cover + verify no desktop flash under labwc; if needed, an rc.xml window 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 of taskkill /T).
  • Default weblink.prewarm=false on 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: replace rfkill/ifconfig/dhclient with nmcli (nmcli radio wifi off/on, nmcli device disconnect/connect) — Trixie uses NetworkManager.
  • kiwy-signage-wifi.sudoers: passwordless rules for exactly the nmcli/rfkill binaries used.
  • Install python3-evdev (apt) + udev rule so the card reader works without input group hacks.

Phase 4 — Install & autostart (Trixie-native)

  • build_offline_aarch64.sh: populate repo/python-wheels/ with cp313 aarch64 wheels (kivy, ffpyplayer, evdev, requests, bcrypt, aiohttp, …). Replace the --platform linux_armv7l logic.
  • install_linux.sh: prefer apt python3-kivy/python3-evdev (fast, no build toolchain); venv with --system-site-packages; drop the --break-system-packages fallback.
  • Autostart: append to ~/.config/labwc/autostart and ship a systemd unit (verify which one RPi OS Trixie rpd-labwc actually honours — validation item).
  • watchdog.sh: repair configure_display_resolution() (wrong /boot/config.txt path, sudo at boot = hang risk); make all boot-time steps non-interactive.
  • Orientation / max_resolution: implement rotation via wlr-randr (Wayland) or cmdline.txt video=...,rotate=; document that Window.size cannot 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 (ps chromium 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_VERSION and 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_compile on 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 chromium processes 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 -9 the player → watchdog restarts within 60 s

7. Open decisions (need input before Phase 1)

  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)?
  2. Session target: stay on the stock rpd-labwc kiosk session, or ship a dedicated minimal labwc session (no panel, no swayidle) for the player?
  3. Autostart mechanism: systemd (system or user) vs ~/.config/labwc/autostart?
  4. Orientation: is Portrait support actually required for this deployment?
  5. Offline install: must the Pi install work with no internet (i.e. vendor the aarch64 wheels into repo/), or is online install acceptable?
  6. Card reader: is it in scope for the Pi, or is authentication keypad/quickconnect only?