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.
This commit is contained in:
ske087
2026-09-13 21:57:49 +03:00
parent f437aba1fc
commit 3ac7f836c4
68 changed files with 5604 additions and 9326 deletions
+265
View File
@@ -0,0 +1,265 @@
# 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/<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?