3ac7f836c4
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.
266 lines
17 KiB
Markdown
266 lines
17 KiB
Markdown
# 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?
|