Files
Kiwy-Signage/linux/development-track.md
T
ske087 3ac7f836c4 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.
2026-09-13 21:57:49 +03:00

26 KiB
Raw Blame History

🧪 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.110No 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, 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/upip 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

  • 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/<pid>/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 onlywidth > 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 <stem>_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
<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

  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, ydotoolall 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_awakeTrue; 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

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)