aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 18:13:38 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 18:13:38 +0100
commit4601ce03a1b41158f94e1e79edacf8540a89d15f (patch)
tree74d00d8dc24a1a400a82f4096b2fc0895b575593
parent2c098221f4ed09d772df9bece5c71912f1c5e849 (diff)
downloaddeck-it-4601ce03a1b41158f94e1e79edacf8540a89d15f.tar.gz
deck-it-4601ce03a1b41158f94e1e79edacf8540a89d15f.zip
feat: launcher, adb tunnel, Slackware autostart
-rwxr-xr-xdeck-it192
-rw-r--r--tests/test_launcher.py20
2 files changed, 212 insertions, 0 deletions
diff --git a/deck-it b/deck-it
new file mode 100755
index 0000000..a74fcdb
--- /dev/null
+++ b/deck-it
@@ -0,0 +1,192 @@
+#!/usr/bin/env bash
+# Usage: deck-it start the dashboard + extended display over USB
+# deck-it stop tear it all down
+# deck-it env print MON/PORT/VNC_PORT for this config
+# deck-it pair pair the tablet for Wi-Fi by scanning a QR code
+# deck-it wifi start over Wi-Fi
+# deck-it install start automatically when this tablet is plugged in (asks for sudo)
+# deck-it uninstall remove the automatic start
+set -u
+DIR="$(dirname "$(readlink -f "$0")")"
+SELF="$DIR/deck-it"
+CONF="${XDG_CONFIG_HOME:-$HOME/.config}/deck-it"
+VNC="$CONF/vnc"
+RUN="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/deck-it"
+RULE=/etc/udev/rules.d/70-deck-it.rules
+
+if [[ "${1:-}" == "env" ]]; then
+ (cd "$DIR" && exec python3 -m deckit env)
+fi
+
+MON=TAB PORT=8765 VNC_PORT=5900
+env_out=$(cd "$DIR" && python3 -m deckit env 2>&1) && eval "$env_out" || echo "Config problem: $env_out" >&2
+
+teardown() {
+ pkill -f "^bash $SELF watch" 2>/dev/null
+ pkill -x wayvnc
+ pkill -f '^python3 -m deckit' 2>/dev/null
+ hyprctl output remove "$MON" >/dev/null 2>&1
+ local s
+ for s in $(adb devices | awk 'NR > 1 && $2 == "device" {print $1}'); do
+ timeout 5 adb -s "$s" reverse --remove-all 2>/dev/null
+ done
+ rm -f "$RUN/serial"
+}
+
+tunnel() {
+ adb -s "$1" reverse "tcp:$VNC_PORT" "localfilesystem:$RUN/vnc.sock" >/dev/null &&
+ adb -s "$1" reverse "tcp:$PORT" "localfilesystem:$RUN/http.sock" >/dev/null
+}
+
+# The display stream needs a password over TLS; both live in a 0700 dir and AVNC gets them in the link.
+vnc_setup() {
+ mkdir -p -m 700 "$VNC"
+ (
+ umask 077
+ [[ -s "$VNC/password" ]] || tr -dc A-Za-z0-9 </dev/urandom | head -c 24 > "$VNC/password"
+ [[ -s "$VNC/tls_cert.pem" ]] || openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
+ -days 3650 -subj /CN=deck-it -keyout "$VNC/tls_key.pem" -out "$VNC/tls_cert.pem" 2>/dev/null
+ printf 'enable_auth=true\nusername=deck-it\npassword=%s\nprivate_key_file=%s\ncertificate_file=%s\n' \
+ "$(cat "$VNC/password")" "$VNC/tls_key.pem" "$VNC/tls_cert.pem" > "$VNC/config"
+ )
+}
+
+open_avnc() {
+ adb -s "$1" shell am start -a android.intent.action.VIEW \
+ -d "'vnc://127.0.0.1:$VNC_PORT?VncUsername=deck-it&VncPassword=$(cat "$VNC/password")'" com.gaurav.avnc >/dev/null 2>&1
+}
+
+# A dropped Wi-Fi link can still report "device", and adb calls on it hang, so probe with a timeout.
+alive() {
+ timeout 6 adb -s "$1" shell true >/dev/null 2>&1
+}
+
+wifi_connect() {
+ local addr
+ for addr in $(adb mdns services | awk '/_adb-tls-connect/ {print $NF}' | sort -u); do
+ timeout 5 adb connect "$addr" 2>/dev/null | grep -q '^connected\|already connected' &&
+ alive "$addr" && { echo "$addr"; return 0; }
+ done
+ return 1
+}
+
+usb_present() {
+ [[ "$(timeout 3 adb -d get-state 2>/dev/null)" == device ]]
+}
+
+# udev RUN= cannot carry a quoted shell command, so the rule calls a generated
+# no-argument script that backgrounds the launcher and returns immediately.
+install_udev() {
+ serial=$(timeout 5 adb -d get-serialno 2>/dev/null)
+ [[ "$serial" =~ ^[A-Za-z0-9]+$ ]] || { echo "Plug in the tablet with USB debugging allowed, then retry"; exit 1; }
+ mkdir -p "$CONF"
+ trigger="$CONF/trigger"
+ printf '#!/bin/sh\nsetsid "%s" start >/dev/null 2>&1 &\n' "$SELF" > "$trigger"
+ chmod 0755 "$trigger"
+ printf 'ACTION!="remove", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{serial}=="%s", RUN+="%s"\n' \
+ "$serial" "$trigger" | sudo tee "$RULE" >/dev/null || exit 1
+ sudo udevadm control --reload
+ echo "Installed for tablet $serial. It starts when plugged in and stops when removed."
+ echo "To also start at login when the tablet is already connected, add to hypr-theme's autostart:"
+ echo " exec-once = $SELF"
+ exit 0
+}
+
+case "${1:-}" in
+stop)
+ teardown
+ echo "Stopped"
+ exit 0
+ ;;
+teardown)
+ teardown
+ exit 0
+ ;;
+pair)
+ command -v qrencode >/dev/null || { echo "Install qrencode first"; exit 1; }
+ name="deck-it-$(tr -dc a-z0-9 </dev/urandom | head -c 8)"
+ pass=$(tr -dc 0-9 </dev/urandom | head -c 8)
+ echo "On the tablet: Developer options > Wireless debugging > Pair device with QR code, and scan this:"
+ echo
+ qrencode -t ansiutf8 -m 2 "WIFI:T:ADB;S:$name;P:$pass;;"
+ echo "Waiting for the scan (2 minutes)..."
+ for _ in {1..120}; do
+ addr=$(adb mdns services | awk -v n="$name" '$1 == n && /_adb-tls-pairing/ {print $NF; exit}')
+ if [[ -n "$addr" ]]; then
+ adb pair "$addr" "$pass" | grep -q 'Successfully paired' || { echo "Pairing failed"; exit 1; }
+ echo "Paired. Connecting..."
+ for _ in {1..10}; do addr=$(wifi_connect) && break; sleep 2; done
+ [[ -n "$addr" ]] && echo "Connected to $addr. Start with: deck-it wifi" || echo "Paired, but couldn't connect yet. Try: deck-it wifi"
+ exit 0
+ fi
+ sleep 1
+ done
+ echo "No scan detected. Is the tablet on the same Wi-Fi network as this PC?"
+ exit 1
+ ;;
+watch)
+ # Runs in the background in Wi-Fi mode: reconnect when the tablet drops, then restore the tunnels.
+ while sleep 5; do
+ serial=$(cat "$RUN/serial" 2>/dev/null) || exit 0
+ alive "$serial" && timeout 6 adb -s "$serial" reverse --list 2>/dev/null | grep -q "tcp:$PORT" && continue
+ timeout 5 adb disconnect "$serial" >/dev/null 2>&1
+ new=$(wifi_connect) || continue
+ tunnel "$new" && echo "$new" > "$RUN/serial" && open_avnc "$new"
+ done
+ ;;
+install)
+ install_udev
+ ;;
+uninstall)
+ if [[ -f "$RULE" ]]; then sudo rm -f "$RULE" && sudo udevadm control --reload; fi
+ rm -f "$CONF/trigger"
+ echo "Uninstalled"
+ exit 0
+ ;;
+wifi)
+ echo "Looking for the tablet on Wi-Fi..."
+ while :; do
+ [[ "${2:-}" == --wait ]] && usb_present && { echo "Tablet is on USB, leaving it to the USB start"; exit 0; }
+ ANDROID_SERIAL=$(wifi_connect) && break
+ [[ "${2:-}" == --wait ]] || { echo "Not found. Turn on Wireless debugging on the tablet, or pair first with: deck-it pair"; exit 1; }
+ sleep 10
+ done
+ ;;
+"")
+ echo "Waiting for tablet on USB (plug it in, allow USB debugging)..."
+ timeout 60 adb wait-for-usb-device || { echo "No tablet found"; exit 1; }
+ ANDROID_SERIAL=$(adb -d get-serialno)
+ ;;
+*)
+ sed -n '2,8p' "$SELF" | sed 's/^# \{0,1\}//'
+ exit 1
+ ;;
+esac
+export ANDROID_SERIAL
+
+for _ in {1..30}; do hyprctl monitors >/dev/null 2>&1 && break; sleep 1; done
+
+(cd "$DIR" && python3 -m deckit setup-display) || exit 1
+
+# Both servers listen on sockets in a 0700 dir, so only this user and its adb tunnel can reach them.
+mkdir -p -m 700 "$RUN"
+vnc_setup
+# wayvnc leaves its socket behind when killed and then refuses to bind, so clear it first.
+pgrep -x wayvnc >/dev/null || { rm -f "$RUN/vnc.sock"; setsid wayvnc -C "$VNC/config" -u -o "$MON" "$RUN/vnc.sock" >/dev/null 2>&1 & }
+pgrep -f '^python3 -m deckit' >/dev/null || (cd "$DIR" && setsid python3 -m deckit >server.log 2>&1 &)
+sleep 1
+
+tunnel "$ANDROID_SERIAL" || { echo "Could not set up the tunnel to the tablet"; exit 1; }
+# Opening the page with the secret stores it as a cookie in the tablet's browser, which unlocks the API.
+token="$CONF/token"
+for _ in {1..10}; do [[ -s "$token" ]] && break; sleep 0.5; done
+adb shell am start -a android.intent.action.VIEW -d "http://127.0.0.1:$PORT/?t=$(cat "$token")" >/dev/null 2>&1
+sleep 1
+open_avnc "$ANDROID_SERIAL"
+
+if [[ "${1:-}" == wifi ]]; then
+ echo "$ANDROID_SERIAL" > "$RUN/serial"
+ pgrep -f "^bash $SELF watch" >/dev/null || setsid bash "$SELF" watch >/dev/null 2>&1 &
+fi
+
+echo "Ready. Display is in AVNC, dashboard at http://127.0.0.1:$PORT"
diff --git a/tests/test_launcher.py b/tests/test_launcher.py
new file mode 100644
index 0000000..bfcbdec
--- /dev/null
+++ b/tests/test_launcher.py
@@ -0,0 +1,20 @@
+import subprocess
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+
+
+class Launcher(unittest.TestCase):
+ def test_syntax(self):
+ self.assertEqual(subprocess.run(["bash", "-n", str(ROOT / "deck-it")]).returncode, 0)
+
+ def test_env_prints_keys(self):
+ out = subprocess.run(["bash", str(ROOT / "deck-it"), "env"],
+ capture_output=True, text=True, cwd=ROOT).stdout
+ for key in ("MON=", "PORT=", "VNC_PORT="):
+ self.assertIn(key, out)
+
+
+if __name__ == "__main__":
+ unittest.main()