# ๐Ÿงช Development Track โ€” Kiwy Signage Player (Raspberry Pi / Linux Edition) > Read this FIRST before debugging or coding on the Pi port. It records what was > measured on real hardware, not what was assumed. --- ## ๐Ÿ“… Session โ€” 2026-09-13 | Field | Value | |-------|-------| | **Branch** | `Linux-RPI-Player` (from `Windows-Player` @ `f437aba`) | | **Hardware** | Raspberry Pi 4 Model B Rev 1.4, kernel `6.18.39+rpt-rpi-v8` | | **Python** | 3.13.5 in `.venv` (created with `--system-site-packages`) | | **Kivy** | 2.3.1 (PyPI wheel, cp313 aarch64) | | **ffpyplayer** | 4.5.3 (cp313 aarch64 wheel) | | **evdev** | 2.0.0 (built from sdist โ€” no cp313 wheel published) | | **Session** | Wayland, `rpd-labwc` (labwc โ€” **not** Wayfire, not X11) | | **Browser** | `/usr/bin/chromium` 152.0.7977.82 | | **Entry point** | `linux/run_linux.py` (**not** `src/main.py`) | ### โœ… Status: the player runs on the Pi Verified by real runs (not inference): * Kivy window created on **Wayland** (`wayland,x11,dummy` โ†’ wayland driver) * 60 s continuous run, **0 errors, 0 tracebacks, clean shutdown** * Heartbeat file written and refreshed every 10 s * Playlist polling loop active (30 s interval) * **Video playback works**: intro1.mp4 โ†’ `duration=6.0`, position advanced `0.56 โ†’ 2.07 โ†’ 3.56 โ†’ 5.06`, texture rendered, EOS handled, state auto-reset * Screensaver/blanker neutralised; `wlopm --on` confirmed working Blocked on: no server reachable from this network (`192.168.0.110` โ†’ *No route to host*), so no playlist/media content could be downloaded. That is an environment limitation, not a code defect. --- ## ๏ฟฝ๏ธ Windows code removal (2026-09-13, second pass) This branch is now **Linux-only**. All Windows code and assets were removed; they remain available on the `Windows-Player` branch. **Deleted:** * `windows/` โ€” the whole directory (32 files): `run_win.py`, `win_card_reader.py`, `webview2_browser.py`, `webview2_runtime.py`, `cef_browser.py`, `build.spec`, `watchdog.ps1`, the PyInstaller/PowerShell tooling and the bundled Windows binaries (`webview2_sdk/` DLLs, `app_icon.ico`). * `.github/instructions/kiwy-build-and-development.instructions.md` (the exe build guide) * `documentation/CODE_SIGNING_SMART_APP_CONTROL.md` (Windows-only signing constraint) **Removed later: the rest of the Windows-era leftovers** (all recoverable from the `Windows-Player` branch, so nothing was lost): | Item | Why it went | |------|-------------| | `working_files/` (29 files) | Dev-era scratch: `install.sh.bak`, the superseded `get_playlists.py` (v1, replaced by `get_playlists_v2.py`), one-off `test_*.py` probes, and `MIGRATION_GUIDE.md` / `INVESTIGATION_RESULTS.md`. Also held real server captures (`server_response_debug.json`: `player_id`, player name, playlist) which had no business in the repo. | | `documentation/` (5 files) | All described the Windows-era HTTPS integration work. | | `python version` | Contained `Python 3.12.9` โ€” the Windows build interpreter. The Pi runs 3.13.5. | | `test_edited_media_upload.py` | Parentless debug script, referenced by nothing. | | `.display-keepalive.sh`, `.keep-screen-alive.sh`, `.wait-for-display.sh` | Orphan X11 helper scripts, referenced by nothing. Superseded by `linux/linux_display.py`. | | `.video-optimization.sh`, `.run-background.sh`, `.start-player-cron.sh` | **Kept** โ€” `install.sh` references all three. | **Removed from shared `src/`** โ€” these were live code paths, so this was a real behavioural change, not just a comment cleanup: | File | Removed | |------|---------| | `main.py` | The whole Win32 focus subsystem: `_bring_window_to_front_nonblocking`, `_start/_stop_focus_keeper`, `_focus_keeper_tick`, `_start/_stop_focus_guardian`, `_focus_guardian_tick`, their `__init__` state, the `_start_focus_keeper()` call at video start, the guardian start in `__init__`, the two `_stop_focus_keeper()` calls, and the `_is_foreground_win` / `_bring_kivy_to_front_win` getattr hooks. Also the duplicate `Window.raise_window()` block and `_weblink_proc`. | | `network_monitor.py` | `IS_WINDOWS`, the `ping -n/-w` branch, `_restart_wifi_windows()` (netsh), and the `sudo ifconfig`/`dhclient` calls (`ifconfig`/`dhclient` are **not installed** on Trixie). | | `weblink_session.py` | `WebInputSources.pointer_moved()` (`GetCursorPos`), the `use_pointer` plumbing, and `msedge` from the browser search list. | | `video_safety.py` | Windows framing in the docstring only โ€” the fix itself is cross-platform and was kept. | **Replaced, not just deleted:** * `_restart_wifi_linux()` โ†’ `_restart_wifi_nmcli()`. Trixie uses **NetworkManager**, so `nmcli radio wifi off/on` + `nmcli device connect` is the correct interface and needs no `sudo`. The old path depended on packages that are absent. * `ifconfig wlan0 down/up` โ†’ `ip link set wlan0 down/up`. * New `.github/instructions/kiwy-linux-rpi.instructions.md` (the Pi counterpart of the deleted build guide). * `PLAYER_WEBLINK_INTEGRATION.md` ยง6.2 rewritten: "Windows: WebView2 Runtime" โ†’ "Linux: Chromium kiosk on Wayland". * `.gitignore`: dropped the Windows venv/dist/WebView2 entries, kept the `.kiosk-profile/` ignore and clarified the credential rules. * `config/app_config.json`: blanked the Windows host values (`screen_name: DESKTOP-NJLBQKH`, the server IP and quick-connect key) and set `weblink.prewarm: false` โ€” pre-warming costs 2 extra Chromium processes, which is not worth it on a Pi 4. **Also removed:** the `_patch_focus_handlers()` patch from `run_linux.py`. It had become vestigial once the focus methods were deleted โ€” it would have created no-op attributes the app never calls. `test_linux_patches.py` now **asserts those attributes do not exist**, so reintroducing Win32 focus code fails the suite. **Verified after the removal:** 21/21 patch checks pass, video still decodes and plays (position advancing, texture rendered), 60 s player run with 0 errors and no spurious crash log. --- ## ๏ฟฝ๐Ÿ”ด The five bugs that actually prevented the port ### [PI-013] Chromium asked for the keyring password at every weblink * **Symptom:** "user and password" prompt appeared when a weblink launched (reproducible also by starting Chromium manually). Impossible to answer on an unattended signage screen, and it blocked the page. * **Root cause (two separate bugs, neither sufficient alone):** 1. **Flag list was dead code.** `APPLIANCE_FLAGS` contained `--password-store=basic` and `--use-mock-keychain`, but the list was **never referenced** from `extra_launch_args()` โ€” so those flags never reached the command line. The comment looked like a fix; nothing was applied. 2. **The D-Bus session bus was inherited.** `ChromiumSubprocessAdapter.launch()` used a bare `subprocess.Popen(args)` with **no `env=`**, and `gnome-keyring-daemon --components=secrets` is running with `DBUS_SESSION_BUS_ADDRESS` set. Chromium's password-store backend therefore resolved to `gnome-libsecret` (Secret Service) and tried to unlock the login keyring to hold its encryption key. Flags alone cannot fix this. * **Fix:** * New `launch_env()` hook on the base adapter (returns `None` = inherit); `LinuxChromiumAdapter` returns `_browser_env()`, which strips `DBUS_SESSION_BUS_ADDRESS`/`DBUS_SESSION_BUS_PID`, empties `GNOME_KEYRING_CONTROL` and sets `CHROME_PASSWORD_STORE=basic`. Scoped to the **child** โ€” the player keeps its real session bus. * Every flag list is now referenced from `extra_launch_args()`, and `linux/test_linux_browser_flags.py` fails if any list becomes dead code. * Added `--disable-save-password-bubble`. * **Verified:** the live process's `/proc//environ` contains no `DBUS_SESSION_BUS_ADDRESS`, and Chromium logs `dbus/bus.cc:405] Failed to connect to the bus` โ€” i.e. it structurally cannot reach the keyring. No prompt appears. ### [PI-014] `--ozone-platform-hint=auto` aborts on Chromium 152 * **Symptom:** no weblink ever displayed under labwc. * **Root cause:** measured, the hint flag does **not** fall back to Wayland when there is no X server โ€” it simply fails: ``` (no flag) -> "Missing X server or $DISPLAY", aborts --ozone-platform-hint=auto -> "Missing X server or $DISPLAY", aborts --ozone-platform=wayland -> starts ``` * **Fix:** the platform is chosen explicitly from the detected session (`--ozone-platform=wayland`), with `WAYLAND_FLAGS` omitted entirely when there is no Wayland socket so Chromium uses its own X11 default. ### [PI-015] Process-group teardown could never run * **Symptom:** every weblink risked leaving Chromium's GPU/zygote/renderer children behind (the Linux twin of the Windows leak). * **Root cause:** `linux_browser.teardown()` calls `os.killpg`, but it only did so when `self._pgid` was a **different** group โ€” and because the base `Popen(args)` had no `start_new_session=True`, the browser shared the player's own process group. Killing that group would have killed the player itself, so the code correctly refused and fell back to `terminate()`, orphaning children. * **Fix:** new `start_new_session()` hook; the Linux adapter returns `True`. `test_linux_browser_flags.py` asserts the live process is its own group leader. --- ## ๐ŸŽฌ Video normalisation โ€” 4K cannot play on a Pi 4 ### [PI-016] A 4K video shows one frozen frame instead of playing * **Symptom:** the new 4K sample (`16118765_3840_2160_30fps.mp4`, 3840ร—2160 H.264 High@5.1) displayed a still frame while the playlist timer ticked on. The 1080p sample played perfectly. * **Root cause:** ffpyplayer decodes in **software** โ€” there is no hardware H.264 *decode* wired into its pipeline. Measured on this Pi 4: | File | Resolution | Software decode speed | |------|-----------|----------------------| | `sample-30s.mp4` | 1920ร—1080 | **3.03ร— realtime** โœ… | | `16118765_3840_2160_30fps.mp4` | 3840ร—2160 | **0.90ร— realtime** โŒ | Below 1ร— realtime the decoder can never feed the display, so the picture effectively stops. The player *did* advance correctly at the 19 s duration โ€” there was never a hang, just a video that cannot be rendered in time. * **Important:** the file plays fine in an *isolated* Kivy probe (position advances in realtime). The stall only appears under the real player, where decode competes with rendering, the weblink browser and the GUI. Do not conclude "the file is fine" from a standalone probe. ### The fix: `linux/video_normalizer.py` Oversized media is downscaled to at most **1920ร—1080** once, at sync time. * **Triggered by resolution only** โ€” `width > 1920 or height > 1080`. A file already within the limit is left byte-identical, so nothing that already plays is ever re-encoded. * **Hardware encoding** via `h264_v4l2m2m` (verified working โ€” the Pi 4's H.264 *encoder* is a separate block from its decoder), with `libx264` as the fallback. Measured: **31 s** for an 18 s 4K clip. * **Audio preserved** (`-c:a copy`, AAC fallback). This matters: a *silent* video hits the separate SDL2_mixer bug documented in `_video_has_audio`, so the normaliser must not create one. * Output lands next to the source as `_kiwy1080p.mp4` plus a `.kiwy-normalized.json` metadata file. ### Skip-while-converting, loop-the-intro-if-nothing-else The conversion is asynchronous, so the playlist can reach the item before it is ready. `src/media_state.py` owns one on-disk contract shared by the player and the normaliser: | Marker | Meaning | |--------|---------| | `.kiwy-converting` | conversion in flight โ†’ **skip this item** | | `_kiwy1080p.mp4` + `.kiwy-normalized.json` | done โ†’ **play this file instead** | Player behaviour per item: * **ready** โ€” play it (the converted file when one exists, else the original) * **converting** โ€” skip the lap immediately, no dwell delay * **pending** โ€” oversized and not converted: skip, and request conversion Skipped items are advanced with `_advance_without_wait()` rather than occupying their configured duration, so a 19 s slot that cannot be shown does not add 19 s of nothing to every lap. **When nothing at all is playable** (e.g. a single-item playlist that is one 4K video still converting), the player loops `config/resources/intro1.mp4` (`eos: loop`) and polls every 3 s. The moment a converted item appears it stops the loop and restarts the playlist **from index 0** โ€” so a one-item playlist gets a clean full run rather than joining mid-clip. A blank screen is never left on a signage display. ### Traps found while building this 1. **`resolve_playable()` initially skipped every normal video.** It checked for a conversion output and a marker but never whether the file was oversized at all, so a plain 1080p file (no output, no marker) fell through to `pending`. The test caught it before it shipped: *skip-the-whole-playlist* is a far worse failure than the freeze it was meant to fix. 2. **The media cleanup would have deleted the converted file.** `delete_unused_media()` prunes anything not named in the playlist โ€” and `_kiwy1080p.mp4` is, by design, not named there. It is now explicitly protected, along with both marker types. 3. **ffmpeg cannot infer the muxer from a `.part` temp name** ("Unable to choose an output format"). The temp file keeps its real extension. 4. **Metadata is written last.** Its presence is what marks a conversion complete, so it must never exist for a half-written output. 5. **A stale marker must not park an item forever** โ€” markers older than 30 min are ignored, so a crash mid-conversion cannot make a video unplayable. ### Verified end to end (real server, real playlist) ``` 20:33:05 sync detects 3840x2160 -> starts the background conversion 20:33:36 conversion completes (31s) -> _kiwy1080p.mp4 + metadata 20:36:01 video_using_normalized source=...3840_2160... normalized=..._kiwy1080p.mp4 20:36:01 starting_video path=..._kiwy1080p.mp4 20:36:20 next_media_called was_index=7 (played 18.7s, advanced on EOS) ``` Suites: `test_media_state.py` 18/18 ยท `test_linux_patches.py` 21/21 ยท `test_linux_browser_flags.py` 27/27. **Recommendation for the server side:** normalising the source to 1920ร—1080 before upload is still better โ€” it avoids the 4K download *and* the 31 s conversion. This player-side path exists so that an oversized upload degrades gracefully instead of breaking the screen. --- ## ๐ŸŸก Chromium footprint โ€” measured, not assumed The Pi 4 has 3.8 GB total and the desktop already uses ~1.9 GB, so a weblink needs to be lightweight. Numbers below are **PSS** summed over the browser tree for the real page (`https://moto-adv.com/`), via `linux/_probe_chromium_footprint.py`. | Profile | Processes | PSS | |---------|-----------|-----| | `safe` (no footprint flags) | 10 | 513 MB | | **`light` (default)** | **9** | **507 MB** | | `minimal` (`--single-process`) | 4 | 438 MB | **Honest conclusion: flag tuning buys very little.** Chromium's baseline is simply ~500 MB and the remaining levers are single-digit percentages. `minimal` saves ~15% but halves the process count, and upstream labels `--single-process` unsupported, so it stays opt-in. **Two measurement traps worth remembering:** 1. **RSS is the wrong metric.** Chromium shares libraries and file-backed pages across processes; summing RSS double-counts and produced numbers ~2x too high โ€” it even ranked a *smaller* configuration as *larger* (1132 MB safe vs 1529 MB light). Use `Pmi/smaps_rollup`. 2. **`--disable-gpu` makes it worse.** It looks like an obvious win for a static page, but it moves rasterization out of the GPU process into the renderer: **1038 MB vs 513 MB**. It is deliberately absent from every list. Use `KIWY_CHROMIUM_MODE=light|minimal|safe` to select a profile. If memory is genuinely the constraint, the structural fix is an **embedded** engine rendering inside the Kivy window (WebKitGTK, available on aarch64) โ€” not more flags. > **Ultralight is not an option on this hardware.** Despite the vendor's site > claiming "Linux (x64 / arm64)", enumerating the entire SDK bucket returns only > `mac-x64, linux-x64, win-x64, win-uwp-x64, win-x86`; > `ultralight-sdk-latest-linux-arm64.7z` is a **404**. There is no ARM64 build of > any kind, and it is closed-source C++ with no Python binding. --- ## ๐Ÿ”ด The five bugs that actually prevented the port ### [PI-001] Kivy's bundled SDL2 has NO Wayland driver โ€” **no window at all** * **Symptom:** ``` [CRITICAL] Unable to find any valuable Window provider. sdl2 - RuntimeError: b'wayland,x11,dummy not available' [CRITICAL] SignagePlayerApp: Window is None - display server not available ``` * **Root cause:** Kivy's PyPI wheel ships a **private SDL2** in `Kivy.libs/` (name has a hash: `libSDL2-2-87637523.0.so.0.3000.7`). Driver enumeration proved it is built **without Wayland**: | Library | Video drivers | |---------|---------------| | `Kivy.libs/libSDL2-2-*.so` (bundled) | `x11, KMSDRM, offscreen, dummy, evdev` | | `/usr/lib/aarch64-linux-gnu/libSDL2-2.0.so.0` (system) | `x11, **wayland**, KMSDRM, offscreen, dummy, evdev` | Trixie runs a Wayland session and there is no X server, so the bundled build cannot create a window. Note the driver *name* does not change: the bundled SDL2 resolves `b'wayland'` as "driver unavailable", not "unknown driver". * **Fix:** `linux/fix_kivy_sdl2.sh` symlinks the system SDL2 over the bundled filename (idempotent, reversible, survives only until the next `pip install --upgrade kivy`). * **Verified:** `WINDOW OK size=(800, 600)` * **Rejected alternatives:** * โŒ Editing `~/.kivy/config.ini` โ€” irrelevant, the provider never loads. * โŒ `KIVY_WINDOW=...` variations โ€” the library lacks the driver, full stop. * *(Still untested)* apt `python3-kivy` likely bundles no SDL2 and would sidestep this entirely โ€” worth evaluating for the installer. ### [PI-002] SDL2 requires `WAYLAND_DISPLAY` โ€” the socket alone is NOT enough * **Symptom:** window creation fails when the player is launched from systemd, cron, an autostart entry or SSH. * **Root cause:** those contexts set `XDG_RUNTIME_DIR` but leave `WAYLAND_DISPLAY` **empty** (the compositor only exports it inside the desktop session). Measured: | `WAYLAND_DISPLAY` | result | |-------------------|--------| | unset | `sdl2 - RuntimeError: b'wayland not available'` | | `wayland-0` | `WINDOW OK (800, 600)` | Notably `wlopm` **does** fall back to scanning `XDG_RUNTIME_DIR`, but SDL2 does not โ€” so this cannot be inferred from the display tools working. * **Fix:** `linux_display.ensure_session_environment()` detects the socket, derives the name from its filename (so `wayland-1` works) and exports it. Called at the very top of `run_linux.py`, before Kivy is imported. * **Verified:** `session environment filled in: {'WAYLAND_DISPLAY': 'wayland-0'}` ### [PI-003] Kivy `WeakMethod` name trap โ€” crash ~20 s AFTER startup * **Symptom:** the app starts fine, then dies on the first Clock tick: ``` [CRITICAL] Fatal error: 'SignagePlayer' object has no attribute 'linux_screen_activity' [CRITICAL] Fatal error: 'SignagePlayer' object has no attribute '_noop' ``` * **Root cause:** Kivy's `Clock` stores a callback's `__func__.__name__` in a `WeakMethod` and later resolves it with `getattr(instance, that_name)`. A replacement assigned under a *different* name than the function was defined with raises `AttributeError` โ€” but only when the Clock next fires, so the traceback points nowhere near the patch. Two variants bit us: a name mismatch (`linux_screen_activity` vs `signal_screen_activity`) and a shared helper (`_noop` used for three different methods). * **Fix:** `_bind_name()` sets `__name__`/`__qualname__` on each replacement, and the focus no-ops are generated per-attribute rather than shared. * **Verified:** `linux/test_linux_patches.py` asserts that `getattr(SignagePlayer, func.__name__)` resolves *and* returns the patched function โ€” for every replaced method. * This is the same trap that used to be documented in the Windows port; the lesson is now enforced by a test instead of by a comment. ### [PI-004] The inherited `signal_screen_activity()` never kept the screen on * **Symptom:** the panel blanks after the 10-minute idle timeout. * **Root causes (three compounding):** 1. `swayidle -w timeout 600 'wlopm --off *'` runs from `~/.config/labwc/autostart` โ€” measured: PID 1431, confirmed running. 2. The handler shells out to `tvservice`, `xdotool`, `ydotool` โ€” **all three are absent on Trixie** (`tvservice` is gone with the legacy firmware stack; `xdotool`/`ydotool` are not installed). 3. It passed a shell-escaped `wlopm --on \*`, so the compositor matched an output literally named `*` and did nothing. * **Fix:** `linux/linux_display.py` โ€” real `wlopm --on '*'` (list argv, no shell), `vcgencmd display_power 1` as a firmware backstop, X11 fallbacks for non-Wayland sessions, and `neutralise_idle_blanker()` to stop `swayidle`. * **Verified:** `keep_display_awake` โ†’ `True`; `swayidle` killed; `wlopm` reports `HDMI-A-1 on`. ### [PI-005] Launcher scripts bypassed the platform layer entirely * **Symptom:** the player runs but blanks, shows no web links, and any fix to the platform layer has no effect. * **Root cause:** `start.sh` did `cd src && python3 main.py`, and `run_player.sh` did the same. `src/main.py` is a *module* of the shared core, not the Pi entry point โ€” running it directly skips every patch in `linux/run_linux.py`. Additionally `src/` is not the data directory, so `base_dir` resolved one level too high. * **Fix:** both scripts now run `.venv/bin/python linux/run_linux.py` from the project root. `check_player_status.sh` / `stop_player.sh` match `run_linux.py` instead of `python3 main.py` (they would otherwise never find the process). * **Verified:** `bash run_player.sh` runs clean; `bash -n` passes on all four. --- ## ๐ŸŸก Also fixed (real but non-blocking) | # | Issue | Fix | |---|-------|-----| | [PI-006] | `main.py` set `SDL_AUDIODRIVER` **twice**; the second (`'alsa'`) was a silent no-op because `setdefault` never overwrites โ€” it obscured which driver was live | Removed the duplicate, documented why | | [PI-007] | `SettingsPopup.test_connection` hard-coded `/tmp/temp_auth_test.json` | Uses `tempfile.gettempdir()`; needed `import tempfile` added to `main.py`. Files with credentials are now always removed | | [PI-008] | `player_auth.json` resolved against the *cwd*, so systemd/autostart launches "forgot" authentication and re-registered every start | `run_linux.py` pins it to an absolute path in the data dir | | [PI-009] | `_bring_window_to_front_nonblocking` / focus keeper are Win32-only; on Wayland they ran every 0.5 s and logged "focus lost" forever | No-oped on Linux (`KIWY_FOCUS_KEEPER=1` restores) | | [PI-010] | Clean shutdown (SIGTERM from the watchdog) wrote a bogus `FATAL: 0` crash log | `SystemExit(0)` is now a clean exit | | [PI-011] | Web-link hand-off: without a dedicated `--user-data-dir` Chromium delegates the URL to an existing instance and exits in ~2 s | `LinuxChromiumAdapter` always uses a private profile, and clears stale `SingletonLock` | | [PI-012] | `proc.terminate()` left Chromium's GPU/zygote/renderer children running (Leak โ†’ OOM over 24/7) | Kill the whole process group (`os.killpg`), the Linux twin of `taskkill /T` | --- ## ๐Ÿงช Tested & Rejected Solutions Log | Date | What was tested | Result | Reason it failed | |------|----------------|--------|-----------------| | 2026-09-13 | `SDL_VIDEODRIVER=wayland` with the bundled SDL2 | โŒ | Bundled SDL2 has no wayland driver | | 2026-09-13 | `SDL_VIDEODRIVER=x11` (+ XWayland present) | โŒ | No X server running on labwc; `x11 not available` | | 2026-09-13 | `KIVY_GL_BACKEND=gles` vs `gl` | โž– | Both equivalent; the failure was the driver, not GL | | 2026-09-13 | Comma-separated `SDL_VIDEODRIVER` list | โœ… | Works โ€” Kivy splits on `,` and SDL2 takes the first that initialises | | 2026-09-13 | Relying on the Wayland socket without `WAYLAND_DISPLAY` | โŒ | SDL2 does not scan `XDG_RUNTIME_DIR` (unlike `wlopm`) | | 2026-09-13 | System SDL2 symlinked over the bundled name | โœ… | Full driver set incl. wayland โ€” **the fix** | --- ## ๐Ÿงฐ Environment / dependency facts * **Must exist before the player will start:** * `libsdl2-2.0-0` (system SDL2 **with** wayland) * `libgl1-mesa-dri`, `libgles2` * a running Wayland session (labwc) and a valid `XDG_RUNTIME_DIR` * **Python packages** (all cp313 aarch64 wheels exist **except** evdev): | Package | Source | Note | |---------|--------|------| | kivy 2.3.1 | PyPI wheel | needs `fix_kivy_sdl2.sh` on Trixie | | ffpyplayer 4.5.3 | PyPI wheel | video/audio backend | | aiohttp, requests, bcrypt | PyPI wheel / apt | already installed system-wide | | **evdev 2.0.0** | **sdist only** | `python3-dev` + `build-essential` required, or use apt `python3-evdev` | * **Tools present:** `wlopm`, `wlr-randr`, `vcgencmd`, `swayidle`, `chromium`, `ffprobe`, `labwc`, `systemd-inhibit`, `zenity` * **Tools MISSING:** `tvservice`, `xdotool`, `ydotool`, `chromium-browser`, `glxinfo`, `ifconfig`, `dhclient` (last two matter for WiFi restart โ€” Phase 3) * **Networking:** NetworkManager is active; `dhcpcd`/`ifupdown` are not * **`sudo` requires a password** โ†’ every boot-time `sudo` call is a hang risk (see Phase 4 / `start.sh` `configure_display_resolution()`) --- ## ๐Ÿงช How to verify ```bash cd /home/pi/Desktop/Kiwy-Signage # 1. SDL2 has the wayland driver (the #1 blocker) bash linux/fix_kivy_sdl2.sh --check # 2. Platform patches wired correctly (WeakMethod trap, keep-awake, SDL2) .venv/bin/python linux/test_linux_patches.py # expect: 21/21 passed # 3. Keyring bypass + footprint flags actually applied .venv/bin/python linux/test_linux_browser_flags.py # expect: 27/27 passed # 4. Video decode + playback .venv/bin/python linux/_probe_video.py # expect: position advances # 5. Chromium footprint per profile (PSS, real page) .venv/bin/python linux/_probe_chromium_footprint.py https://moto-adv.com/ # 6. The player itself bash run_player.sh # or: bash start.sh (watchdog) # 7. Diagnostics .venv/bin/python linux/linux_display.py # backend/outputs/tools bash linux/fix_kivy_sdl2.sh --revert # undo the SDL2 symlink ``` Useful escape hatches: | Variable | Effect | |----------|--------| | `KIWY_DISPLAY_TOOLS_DISABLED=1` | Disable all `wlopm`/`vcgencmd`/`swayidle` work | | `KIWY_CHROMIUM_MODE=light\|minimal\|safe` | Chromium footprint profile (default `light`) | | `KIWY_VENV=/path` | Point the SDL2 fix script at another virtualenv | --- ## ๐Ÿ“ Next session * [ ] **Phase 2** โ€” web links: verify Chromium kiosk on labwc end-to-end (fullscreen, no desktop flash, no leaked processes, interaction postpones) * [ ] Evaluate **apt `python3-kivy`** โ€” it bundles no SDL2 and may remove the need for `fix_kivy_sdl2.sh` entirely * [ ] **Phase 4** โ€” autostart via `~/.config/labwc/autostart` + systemd unit; repair `start.sh`'s `configure_display_resolution()` (wrong `/boot/config.txt` path on Trixie โ†’ `/boot/firmware/config.txt`) * [ ] **Phase 3** โ€” card reader (evdev/udev) and WiFi restart via `nmcli` (the current `ifconfig`/`dhclient` path depends on packages that are gone) * [ ] Orientation/rotation via `wlr-randr` (implemented, untested on hardware) * [ ] 24/7 soak test with real playlist content * [ ] Confirm the `dist/` deployment hazard documented for Windows has no Linux equivalent (it does not โ€” there is no bundle on Linux)