From 5dbce22f18e2b0af20fe22b61e36153d3ee4cce9 Mon Sep 17 00:00:00 2001 From: Petrus Pennanen Date: Tue, 15 Sep 2026 15:55:44 +0300 Subject: [PATCH 1/4] Add dash kiosk installer for boxes with an HDMI panel scripts/kiosk: a cage + Chromium kiosk unit on tty1 that shows the local dash with the owner token, waits for the dash before launching, restarts on failure, and an idempotent installer (apt, unit, getty@tty1 handoff, graphical.target). First target: Vadelma's 1024x600 panel; same script for the VTA. Co-Authored-By: Claude Fable 5.1 --- scripts/kiosk/carwatch-kiosk.service | 25 +++++++++++++++++++++++++ scripts/kiosk/carwatch-kiosk.sh | 24 ++++++++++++++++++++++++ scripts/kiosk/install-kiosk.sh | 18 ++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 scripts/kiosk/carwatch-kiosk.service create mode 100755 scripts/kiosk/carwatch-kiosk.sh create mode 100755 scripts/kiosk/install-kiosk.sh diff --git a/scripts/kiosk/carwatch-kiosk.service b/scripts/kiosk/carwatch-kiosk.service new file mode 100644 index 0000000..e594487 --- /dev/null +++ b/scripts/kiosk/carwatch-kiosk.service @@ -0,0 +1,25 @@ +[Unit] +Description=CarWatch dash kiosk on the local HDMI panel (cage + Chromium) +After=carwatch-chat.service systemd-user-sessions.service +Wants=carwatch-chat.service +Conflicts=getty@tty1.service + +[Service] +Type=simple +User=%i +PAMName=login +TTYPath=/dev/tty1 +StandardInput=tty-force +StandardOutput=journal +StandardError=journal +UtmpIdentifier=tty1 +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes +Environment=XDG_SESSION_TYPE=wayland +ExecStart=/usr/local/bin/carwatch-kiosk.sh +Restart=always +RestartSec=3 + +[Install] +WantedBy=graphical.target diff --git a/scripts/kiosk/carwatch-kiosk.sh b/scripts/kiosk/carwatch-kiosk.sh new file mode 100755 index 0000000..a34d897 --- /dev/null +++ b/scripts/kiosk/carwatch-kiosk.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# CarWatch dash kiosk: full-screen Chromium under the cage compositor on the +# local HDMI panel, pointed at the local dash with the owner token. Started by +# carwatch-kiosk.service on tty1. Waits for the dash before launching so a +# reboot does not leave an error page on the car screen. +set -u +TOKEN_FILE="${CARWATCH_DASH_TOKEN_FILE:-$HOME/.carwatch/dash-token}" +DASH_URL="${CARWATCH_DASH_URL:-http://127.0.0.1:8088/dash}" +for _ in $(seq 1 120); do + curl -sf -m 2 -o /dev/null "http://127.0.0.1:8088/" && break + sleep 2 +done +TOKEN="" +[ -r "$TOKEN_FILE" ] && TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" +URL="$DASH_URL" +[ -n "$TOKEN" ] && URL="$DASH_URL?t=$TOKEN" +export XDG_SESSION_TYPE=wayland +export WLR_LIBINPUT_NO_DEVICES=1 +exec /usr/bin/cage -d -- /usr/bin/chromium \ + --kiosk --ozone-platform=wayland --no-first-run --noerrdialogs \ + --disable-infobars --disable-session-crashed-bubble --disable-pinch \ + --overscroll-history-navigation=0 --check-for-update-interval=31536000 \ + --password-store=basic --user-data-dir="$HOME/.carwatch/kiosk-profile" \ + "$URL" diff --git a/scripts/kiosk/install-kiosk.sh b/scripts/kiosk/install-kiosk.sh new file mode 100755 index 0000000..820a4ad --- /dev/null +++ b/scripts/kiosk/install-kiosk.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Install the CarWatch dash kiosk on a box with an HDMI panel (Pi 5 / VTA). +# Idempotent. Usage: sudo scripts/kiosk/install-kiosk.sh [user] (default: the invoking user) +set -euo pipefail +KUSER="${1:-${SUDO_USER:-$USER}}" +HERE="$(cd -- "$(dirname -- "$0")" && pwd)" +export DEBIAN_FRONTEND=noninteractive +apt-get update -q +apt-get install -y -q --no-install-recommends cage chromium seatd grim fonts-noto-color-emoji +install -m 755 "$HERE/carwatch-kiosk.sh" /usr/local/bin/carwatch-kiosk.sh +install -m 644 "$HERE/carwatch-kiosk.service" /etc/systemd/system/carwatch-kiosk@.service +usermod -aG video,input,render "$KUSER" 2>/dev/null || true +systemctl daemon-reload +systemctl disable --now getty@tty1.service || true +systemctl set-default graphical.target +systemctl enable --now "carwatch-kiosk@${KUSER}.service" +sleep 6 +systemctl --no-pager --lines=8 status "carwatch-kiosk@${KUSER}.service" || true From 2188202710701c9ac1b2165eda46c3ca3fb1c18b Mon Sep 17 00:00:00 2001 From: Petrus Pennanen Date: Tue, 15 Sep 2026 15:57:28 +0300 Subject: [PATCH 2/4] Kiosk: Ubuntu variant (snap Chromium, keep an existing tty1 console on tty2) The VTA runs Ubuntu 26.04 where apt has only a Chromium shim, and its vta-console.service already owns tty1. The variant installs Chromium from snap, points the wrapper at /snap/bin/chromium, moves vta-console to tty2 with a drop-in, and puts the kiosk on tty1. Co-Authored-By: Claude Fable 5.1 --- scripts/kiosk/install-kiosk-ubuntu.sh | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 scripts/kiosk/install-kiosk-ubuntu.sh diff --git a/scripts/kiosk/install-kiosk-ubuntu.sh b/scripts/kiosk/install-kiosk-ubuntu.sh new file mode 100755 index 0000000..e7fb4fd --- /dev/null +++ b/scripts/kiosk/install-kiosk-ubuntu.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Ubuntu variant of install-kiosk.sh (VTA-439): Chromium comes from snap on +# Ubuntu (apt has only a shim), and a box that already runs a text console on +# tty1 (vta-console.service) keeps it on tty2 so both survive. +# Idempotent. Usage: sudo scripts/kiosk/install-kiosk-ubuntu.sh [user] +set -euo pipefail +KUSER="${1:-${SUDO_USER:-$USER}}" +HERE="$(cd -- "$(dirname -- "$0")" && pwd)" +export DEBIAN_FRONTEND=noninteractive +apt-get update -q +apt-get install -y -q --no-install-recommends cage seatd grim fonts-noto-color-emoji curl +snap list chromium >/dev/null 2>&1 || snap install chromium +install -m 755 "$HERE/carwatch-kiosk.sh" /usr/local/bin/carwatch-kiosk.sh +sed -i 's#/usr/bin/chromium #/snap/bin/chromium #' /usr/local/bin/carwatch-kiosk.sh +install -m 644 "$HERE/carwatch-kiosk.service" /etc/systemd/system/carwatch-kiosk@.service +usermod -aG video,input,render "$KUSER" 2>/dev/null || true +# keep an existing tty1 text console, moved to tty2 (Ctrl+Alt+F2) +if systemctl cat vta-console.service >/dev/null 2>&1; then + mkdir -p /etc/systemd/system/vta-console.service.d + cat > /etc/systemd/system/vta-console.service.d/tty2.conf <<'CONF' +[Unit] +Conflicts= +Conflicts=getty@tty2.service +ConditionPathExists=/dev/tty2 +[Service] +ExecStartPre= +ExecStartPre=/bin/sh -c 'setterm -blank 0 -powerdown 0 > /dev/tty2 2>/dev/null || true; printf "\033[H\033[2J" > /dev/tty2' +TTYPath=/dev/tty2 +CONF +fi +systemctl daemon-reload +systemctl disable --now getty@tty1.service getty@tty2.service 2>/dev/null || true +systemctl restart vta-console.service 2>/dev/null || true +systemctl set-default graphical.target +systemctl enable --now "carwatch-kiosk@${KUSER}.service" +sleep 6 +systemctl --no-pager --lines=6 status "carwatch-kiosk@${KUSER}.service" || true +systemctl --no-pager --lines=3 status vta-console.service 2>/dev/null | head -8 || true From ce17edeebf0d2b3257c1ca2af597301be18a56c3 Mon Sep 17 00:00:00 2001 From: Petrus Pennanen Date: Tue, 15 Sep 2026 16:01:22 +0300 Subject: [PATCH 3/4] Kiosk: fail without a browser when the dash never answers; snap-safe profile Review by codexmb on #55: (1) snap Chromium cannot write a dot-directory in the real home, so the profile now defaults to ~/snap/chromium/common/ carwatch-kiosk when the binary is the snap, and the Ubuntu installer sets the binary through a unit drop-in instead of sed; (2) the launcher exits nonzero after the readiness window instead of starting Chromium against a dead dash, so systemd's Restart keeps retrying. tests/test_kiosk_launcher.py runs the real script with stubbed curl and cage: no browser on a dead dash, tokenized URL on a live one, snap profile path for the snap binary. Co-Authored-By: Claude Fable 5.1 --- scripts/kiosk/carwatch-kiosk.sh | 47 +++++++++++++++++---- scripts/kiosk/install-kiosk-ubuntu.sh | 6 ++- tests/test_kiosk_launcher.py | 59 +++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 tests/test_kiosk_launcher.py diff --git a/scripts/kiosk/carwatch-kiosk.sh b/scripts/kiosk/carwatch-kiosk.sh index a34d897..8b3b0f9 100755 --- a/scripts/kiosk/carwatch-kiosk.sh +++ b/scripts/kiosk/carwatch-kiosk.sh @@ -1,24 +1,57 @@ #!/bin/bash # CarWatch dash kiosk: full-screen Chromium under the cage compositor on the # local HDMI panel, pointed at the local dash with the owner token. Started by -# carwatch-kiosk.service on tty1. Waits for the dash before launching so a -# reboot does not leave an error page on the car screen. +# carwatch-kiosk@.service on tty1. +# +# Waits for the dash before launching. If the dash never answers within the +# readiness window the script exits nonzero WITHOUT starting Chromium, so +# systemd's Restart=always keeps retrying instead of leaving a stale +# connection-error page on the car screen. +# +# Env (set by the unit or a drop-in): +# CARWATCH_CHROMIUM browser binary (default /usr/bin/chromium; the +# Ubuntu installer sets /snap/bin/chromium) +# CARWATCH_KIOSK_PROFILE Chromium profile dir; defaults depend on the binary: +# snap Chromium can only write under ~/snap/chromium, +# so the snap gets ~/snap/chromium/common/carwatch-kiosk +# CARWATCH_KIOSK_TRIES / CARWATCH_KIOSK_SLEEP readiness attempts (120) and +# seconds between them (2) +# CARWATCH_DASH_TOKEN_FILE, CARWATCH_DASH_URL set -u TOKEN_FILE="${CARWATCH_DASH_TOKEN_FILE:-$HOME/.carwatch/dash-token}" DASH_URL="${CARWATCH_DASH_URL:-http://127.0.0.1:8088/dash}" -for _ in $(seq 1 120); do - curl -sf -m 2 -o /dev/null "http://127.0.0.1:8088/" && break - sleep 2 +HEALTH_URL="${DASH_URL%/dash}/" +CHROMIUM="${CARWATCH_CHROMIUM:-/usr/bin/chromium}" +TRIES="${CARWATCH_KIOSK_TRIES:-120}" +SLEEP="${CARWATCH_KIOSK_SLEEP:-2}" +CAGE="${CARWATCH_CAGE:-/usr/bin/cage}" + +case "$CHROMIUM" in + /snap/*) DEFAULT_PROFILE="$HOME/snap/chromium/common/carwatch-kiosk" ;; + *) DEFAULT_PROFILE="$HOME/.carwatch/kiosk-profile" ;; +esac +PROFILE="${CARWATCH_KIOSK_PROFILE:-$DEFAULT_PROFILE}" + +ready=0 +for _ in $(seq 1 "$TRIES"); do + if curl -sf -m 2 -o /dev/null "$HEALTH_URL"; then ready=1; break; fi + sleep "$SLEEP" done +if [ "$ready" != 1 ]; then + echo "carwatch-kiosk: dash not reachable at $HEALTH_URL after $TRIES attempts, not starting the browser" >&2 + exit 1 +fi + TOKEN="" [ -r "$TOKEN_FILE" ] && TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" URL="$DASH_URL" [ -n "$TOKEN" ] && URL="$DASH_URL?t=$TOKEN" +mkdir -p "$PROFILE" 2>/dev/null || true export XDG_SESSION_TYPE=wayland export WLR_LIBINPUT_NO_DEVICES=1 -exec /usr/bin/cage -d -- /usr/bin/chromium \ +exec "$CAGE" -d -- "$CHROMIUM" \ --kiosk --ozone-platform=wayland --no-first-run --noerrdialogs \ --disable-infobars --disable-session-crashed-bubble --disable-pinch \ --overscroll-history-navigation=0 --check-for-update-interval=31536000 \ - --password-store=basic --user-data-dir="$HOME/.carwatch/kiosk-profile" \ + --password-store=basic --user-data-dir="$PROFILE" \ "$URL" diff --git a/scripts/kiosk/install-kiosk-ubuntu.sh b/scripts/kiosk/install-kiosk-ubuntu.sh index e7fb4fd..9f0b0aa 100755 --- a/scripts/kiosk/install-kiosk-ubuntu.sh +++ b/scripts/kiosk/install-kiosk-ubuntu.sh @@ -11,7 +11,11 @@ apt-get update -q apt-get install -y -q --no-install-recommends cage seatd grim fonts-noto-color-emoji curl snap list chromium >/dev/null 2>&1 || snap install chromium install -m 755 "$HERE/carwatch-kiosk.sh" /usr/local/bin/carwatch-kiosk.sh -sed -i 's#/usr/bin/chromium #/snap/bin/chromium #' /usr/local/bin/carwatch-kiosk.sh +mkdir -p /etc/systemd/system/carwatch-kiosk@.service.d +cat > /etc/systemd/system/carwatch-kiosk@.service.d/snap-chromium.conf <<'CONF' +[Service] +Environment=CARWATCH_CHROMIUM=/snap/bin/chromium +CONF install -m 644 "$HERE/carwatch-kiosk.service" /etc/systemd/system/carwatch-kiosk@.service usermod -aG video,input,render "$KUSER" 2>/dev/null || true # keep an existing tty1 text console, moved to tty2 (Ctrl+Alt+F2) diff --git a/tests/test_kiosk_launcher.py b/tests/test_kiosk_launcher.py new file mode 100644 index 0000000..9ac0d9f --- /dev/null +++ b/tests/test_kiosk_launcher.py @@ -0,0 +1,59 @@ +"""The kiosk launcher must not start the browser when the dash never answers, +and must start it with the tokenized URL when the dash is up. Runs the real +bash script with stubbed curl and cage on PATH.""" +import os, stat, subprocess, tempfile, unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "kiosk" / "carwatch-kiosk.sh" + + +def _stub(dirpath: Path, name: str, body: str) -> None: + p = dirpath / name + p.write_text("#!/bin/sh\n" + body) + p.chmod(p.stat().st_mode | stat.S_IEXEC) + + +class KioskLauncherTest(unittest.TestCase): + def _run(self, curl_ok: bool): + tmp = Path(tempfile.mkdtemp()) + home = tmp / "home"; (home / ".carwatch").mkdir(parents=True) + (home / ".carwatch" / "dash-token").write_text("tok123\n") + binp = tmp / "bin"; binp.mkdir() + _stub(binp, "curl", "exit 0" if curl_ok else "exit 22") + cage_log = tmp / "cage.log" + _stub(binp, "cage", f'echo "$@" > "{cage_log}"; exit 0') + env = dict(os.environ, HOME=str(home), PATH=f"{binp}:{os.environ['PATH']}", + CARWATCH_KIOSK_TRIES="3", CARWATCH_KIOSK_SLEEP="0", + CARWATCH_CAGE=str(binp / "cage"), CARWATCH_CHROMIUM="/usr/bin/chromium") + res = subprocess.run(["bash", str(SCRIPT)], env=env, capture_output=True, text=True, timeout=30) + return res, cage_log, home + + def test_no_browser_when_dash_never_answers(self): + res, cage_log, _ = self._run(curl_ok=False) + self.assertNotEqual(res.returncode, 0, res.stderr) + self.assertFalse(cage_log.exists(), "cage/chromium was started although the dash never answered") + self.assertIn("not starting the browser", res.stderr) + + def test_browser_started_with_token_when_dash_is_up(self): + res, cage_log, home = self._run(curl_ok=True) + self.assertEqual(res.returncode, 0, res.stderr) + args = cage_log.read_text() + self.assertIn("--kiosk", args) + self.assertIn("http://127.0.0.1:8088/dash?t=tok123", args) + self.assertIn(f"--user-data-dir={home}/.carwatch/kiosk-profile", args) + + def test_snap_chromium_uses_snap_writable_profile(self): + tmp = Path(tempfile.mkdtemp()); home = tmp / "home"; (home / ".carwatch").mkdir(parents=True) + binp = tmp / "bin"; binp.mkdir(); _stub(binp, "curl", "exit 0") + cage_log = tmp / "cage.log"; _stub(binp, "cage", f'echo "$@" > "{cage_log}"; exit 0') + env = dict(os.environ, HOME=str(home), PATH=f"{binp}:{os.environ['PATH']}", + CARWATCH_KIOSK_TRIES="1", CARWATCH_KIOSK_SLEEP="0", + CARWATCH_CAGE=str(binp / "cage"), CARWATCH_CHROMIUM="/snap/bin/chromium") + res = subprocess.run(["bash", str(SCRIPT)], env=env, capture_output=True, text=True, timeout=30) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertIn(f"--user-data-dir={home}/snap/chromium/common/carwatch-kiosk", cage_log.read_text()) + + +if __name__ == "__main__": + unittest.main() From b394f2f0418285c0f0bd4c3453df97b21d2b4ab4 Mon Sep 17 00:00:00 2001 From: Petrus Pennanen Date: Tue, 15 Sep 2026 16:08:50 +0300 Subject: [PATCH 4/4] install.sh: keep the journal across reboots on Raspberry Pi OS The vendor drop-in 40-rpi-volatile-storage.conf makes journald volatile, so the previous boot's log vanishes and an unexplained reboot (Vadelma, 15 Sep 2026 15:03) cannot be diagnosed. Override with Storage=persistent, capped at 200M for the SD card. Co-Authored-By: Claude Fable 5.1 --- install.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/install.sh b/install.sh index cfa744b..0b7e116 100755 --- a/install.sh +++ b/install.sh @@ -158,3 +158,14 @@ cat </dev/null 2>&1; then + sudo mkdir -p /etc/systemd/journald.conf.d /var/log/journal + printf '[Journal]\nStorage=persistent\nSystemMaxUse=200M\n' | sudo tee /etc/systemd/journald.conf.d/carwatch-persistent.conf >/dev/null + sudo systemctl restart systemd-journald 2>/dev/null || true + sudo journalctl --flush 2>/dev/null || true +fi