# 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/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`): 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_activity` → `linux_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`) - [x] Create the venv and install deps (`kivy`, `ffpyplayer`, `evdev`, `requests`, `bcrypt`, `aiohttp`). - [x] `run_linux.py`: env setup + adapter injection + `signal_screen_activity` replacement. - [x] Fix the SDL2/Wayland blocker (`linux/fix_kivy_sdl2.sh`). - [x] `signal_screen_activity()`: `wlopm --on '*'` via `subprocess` (no `os.system`), `vcgencmd display_power` backstop, X11 fallbacks; dropped `tvservice`/`xdotool`/`ydotool`. - [x] Neutralise the `swayidle` idle blanker. - [x] Gate the Win32 focus guardian/keeper on Linux. - [x] Fix the duplicate `SDL_AUDIODRIVER` line. - [x] Point the launchers at `linux/run_linux.py` instead of `src/main.py`. - [x] 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-fullscreen` only where `--kiosk` misbehaves. - [ ] Implement `wait_visible()` for Linux — verify the Chromium window exists (`/proc//` + 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?