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.
26 KiB
🧪 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 advanced0.56 → 2.07 → 3.56 → 5.06, texture rendered, EOS handled, state auto-reset - Screensaver/blanker neutralised;
wlopm --onconfirmed 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 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, sonmcli radio wifi off/on+nmcli device connectis the correct interface and needs nosudo. 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 setweblink.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):
- Flag list was dead code.
APPLIANCE_FLAGScontained--password-store=basicand--use-mock-keychain, but the list was never referenced fromextra_launch_args()— so those flags never reached the command line. The comment looked like a fix; nothing was applied. - The D-Bus session bus was inherited.
ChromiumSubprocessAdapter.launch()used a baresubprocess.Popen(args)with noenv=, andgnome-keyring-daemon --components=secretsis running withDBUS_SESSION_BUS_ADDRESSset. Chromium's password-store backend therefore resolved tognome-libsecret(Secret Service) and tried to unlock the login keyring to hold its encryption key. Flags alone cannot fix this.
- Flag list was dead code.
- Fix:
- New
launch_env()hook on the base adapter (returnsNone= inherit);LinuxChromiumAdapterreturns_browser_env(), which stripsDBUS_SESSION_BUS_ADDRESS/DBUS_SESSION_BUS_PID, emptiesGNOME_KEYRING_CONTROLand setsCHROME_PASSWORD_STORE=basic. Scoped to the child — the player keeps its real session bus. - Every flag list is now referenced from
extra_launch_args(), andlinux/test_linux_browser_flags.pyfails if any list becomes dead code. - Added
--disable-save-password-bubble.
- New
- Verified: the live process's
/proc/<pid>/environcontains noDBUS_SESSION_BUS_ADDRESS, and Chromium logsdbus/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), withWAYLAND_FLAGSomitted 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()callsos.killpg, but it only did so whenself._pgidwas a different group — and because the basePopen(args)had nostart_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 toterminate(), orphaning children. - Fix: new
start_new_session()hook; the Linux adapter returnsTrue.test_linux_browser_flags.pyasserts 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.mp41920×1080 3.03× realtime ✅ 16118765_3840_2160_30fps.mp43840×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), withlibx264as 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
<stem>_kiwy1080p.mp4plus a.kiwy-normalized.jsonmetadata 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 |
|---|---|
<media>.kiwy-converting |
conversion in flight → skip this item |
<media>_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
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 topending. The test caught it before it shipped: skip-the-whole-playlist is a far worse failure than the freeze it was meant to fix.- The media cleanup would have deleted the converted file.
delete_unused_media()prunes anything not named in the playlist — and_kiwy1080p.mp4is, by design, not named there. It is now explicitly protected, along with both marker types. - ffmpeg cannot infer the muxer from a
.parttemp name ("Unable to choose an output format"). The temp file keeps its real extension. - Metadata is written last. Its presence is what marks a conversion complete, so it must never exist for a half-written output.
- 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:
- 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. --disable-gpumakes 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.7zis 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, evdevTrixie 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.shsymlinks the system SDL2 over the bundled filename (idempotent, reversible, survives only until the nextpip 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-kivylikely bundles no SDL2 and would sidestep this entirely — worth evaluating for the installer.
- ❌ Editing
[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_DIRbut leaveWAYLAND_DISPLAYempty (the compositor only exports it inside the desktop session). Measured:WAYLAND_DISPLAYresult unset sdl2 - RuntimeError: b'wayland not available'wayland-0WINDOW OK (800, 600)Notably
wlopmdoes fall back to scanningXDG_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 (sowayland-1works) and exports it. Called at the very top ofrun_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
Clockstores a callback's__func__.__name__in aWeakMethodand later resolves it withgetattr(instance, that_name). A replacement assigned under a different name than the function was defined with raisesAttributeError— but only when the Clock next fires, so the traceback points nowhere near the patch. Two variants bit us: a name mismatch (linux_screen_activityvssignal_screen_activity) and a shared helper (_noopused 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.pyasserts thatgetattr(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):
swayidle -w timeout 600 'wlopm --off *'runs from~/.config/labwc/autostart— measured: PID 1431, confirmed running.- The handler shells out to
tvservice,xdotool,ydotool— all three are absent on Trixie (tvserviceis gone with the legacy firmware stack;xdotool/ydotoolare not installed). - It passed a shell-escaped
wlopm --on \*, so the compositor matched an output literally named*and did nothing.
- Fix:
linux/linux_display.py— realwlopm --on '*'(list argv, no shell),vcgencmd display_power 1as a firmware backstop, X11 fallbacks for non-Wayland sessions, andneutralise_idle_blanker()to stopswayidle. - Verified:
keep_display_awake→True;swayidlekilled;wlopmreportsHDMI-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.shdidcd src && python3 main.py, andrun_player.shdid the same.src/main.pyis a module of the shared core, not the Pi entry point — running it directly skips every patch inlinux/run_linux.py. Additionallysrc/is not the data directory, sobase_dirresolved one level too high. - Fix: both scripts now run
.venv/bin/python linux/run_linux.pyfrom the project root.check_player_status.sh/stop_player.shmatchrun_linux.pyinstead ofpython3 main.py(they would otherwise never find the process). - Verified:
bash run_player.shruns clean;bash -npasses 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.shon Trixieffpyplayer 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-essentialrequired, or use aptpython3-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/ifupdownare not -
sudorequires a password → every boot-timesudocall is a hang risk (see Phase 4 /start.shconfigure_display_resolution())
🧪 How to verify
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 forfix_kivy_sdl2.shentirely - Phase 4 — autostart via
~/.config/labwc/autostart+ systemd unit; repairstart.sh'sconfigure_display_resolution()(wrong/boot/config.txtpath on Trixie →/boot/firmware/config.txt) - Phase 3 — card reader (evdev/udev) and WiFi restart via
nmcli(the currentifconfig/dhclientpath 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)