#!/usr/bin/env bash # scripts/install/server.sh - install and setup all server related apps and environments set -euo pipefail ORIG_USER=${SUDO_USER:-$(id -un)} SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) SCRIPT_REPO_ROOT=$(cd -- "$SCRIPT_DIR/../.." && pwd) DEFAULT_REPO_URL=ssh://git@scm.bstein.dev:2242/bstein/lesavka.git export TMPDIR=${TMPDIR:-/var/tmp} REF=${LESAVKA_REF:-master} # fallback REPO_URL=${LESAVKA_REPO_URL:-} USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6) udc_state() { local udc="" udc=$(ls /sys/class/udc 2>/dev/null | head -n1 || true) if [[ -z $udc ]]; then echo "unknown" return 0 fi cat "/sys/class/udc/$udc/state" 2>/dev/null || echo "unknown" } is_attached_state() { case "$1" in configured|addressed|default|suspended|unknown) return 0 ;; esac return 1 } detect_connected_hdmi_connector() { local dev name stable_name status score suffix local best="" best_score=0 best_suffix=0 declare -A scores=() # HDMI status can briefly flap during gadget/display bring-up. Sample a few # times and prefer the connector that stays connected; ties prefer HDMI-A-2, # which is the dedicated downstream capture leg on the current Pi layout. # Store the logical connector suffix, not the DRM card prefix, so this stays # valid if Linux renumbers cardN across boots. for _ in 1 2 3 4 5; do for dev in /sys/class/drm/card*-HDMI-A-*; do [[ -e $dev/status ]] || continue name=$(basename "$dev") stable_name=$name if [[ $name =~ (HDMI-A-[0-9]+)$ ]]; then stable_name=${BASH_REMATCH[1]} fi status=$(cat "$dev/status" 2>/dev/null || true) if [[ $status == connected ]]; then scores[$stable_name]=$(( ${scores[$stable_name]:-0} + 1 )) fi done sleep 0.2 done for name in "${!scores[@]}"; do score=${scores[$name]} suffix=${name##*-} if (( score > best_score || (score == best_score && suffix > best_suffix) )); then best=$name best_score=$score best_suffix=$suffix fi done printf '%s\n' "$best" } run_as_user() { sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@" } while [[ $# -gt 0 ]]; do case $1 in -r|--ref) REF="$2"; shift 2 ;; -h|--help) echo "Usage: $0 [--ref ]"; exit 0 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done echo "==> Using git ref: $REF" mkdir -p "$TMPDIR" if [[ -z $REPO_URL ]] && [[ -d $SCRIPT_REPO_ROOT/.git ]]; then REPO_URL=$(git -C "$SCRIPT_REPO_ROOT" config --get remote.origin.url || true) fi REPO_URL=${REPO_URL:-$DEFAULT_REPO_URL} echo "==> 1a. Base packages" sudo pacman -Sq --needed --noconfirm git \ rustup \ protobuf \ abseil-cpp \ gcc \ alsa-utils \ pipewire \ pipewire-pulse \ tailscale \ base-devel \ v4l-utils \ gstreamer \ gst-plugins-base \ gst-plugins-base-libs \ gst-plugins-good \ gst-plugins-bad \ gst-plugins-bad-libs \ gst-plugins-ugly \ gst-libav \ tcpdump \ lsof if ! command -v yay >/dev/null 2>&1; then echo "==> 1b. installing yay from AUR ..." run_as_user env TMPDIR="$TMPDIR" bash -c ' rm -rf "$TMPDIR/yay" && cd "$TMPDIR" && git clone --depth 1 https://aur.archlinux.org/yay.git && cd yay && makepkg -si --noconfirm' fi # yay -S --noconfirm grpcurl-bin echo "==> 1c. GPIO permissions for relay" echo 'z /dev/gpiochip* 0660 root gpio -' | sudo tee /etc/tmpfiles.d/gpiochip.conf >/dev/null sudo systemd-tmpfiles --create /etc/tmpfiles.d/gpiochip.conf || true echo "==> 2a. Kernel-driver tweaks" cat <<'EOF' | sudo tee /etc/modprobe.d/gc311-stream.conf >/dev/null options uvcvideo quirks=0x200 timeout=10000 EOF echo "==> 2b. Predictable /dev names for each capture card" # ensure relay (GPIO power) is on if present if systemctl list-unit-files | grep -q '^relay.service'; then sudo systemctl enable --now relay.service sleep 2 fi # probe v4l2 devices for GC311s (07ca:3311) mapfile -t GC_VIDEOS < <( sudo v4l2-ctl --list-devices 2>/dev/null | awk '/Live Gamer MINI/{getline; print $1}' ) # fallback via udev if v4l2-ctl output is empty/partial if [ "${#GC_VIDEOS[@]}" -ne 2 ]; then mapfile -t GC_VIDEOS < <( for dev in /dev/video*; do props=$(sudo udevadm info -q property -n "$dev" 2>/dev/null || true) if echo "$props" | grep -q 'ID_VENDOR_ID=07ca' && echo "$props" | grep -q 'ID_MODEL_ID=3311'; then echo "$dev" fi done | sort -u ) fi if [ "${#GC_VIDEOS[@]}" -ne 2 ]; then echo "⚠️ GC311 capture cards not fully present; skipping udev eye-link refresh." >&2 if [ "${#GC_VIDEOS[@]}" -eq 0 ]; then echo " Detected: none" >&2 else printf ' Detected: %s\n' "${GC_VIDEOS[@]}" >&2 fi echo " The server install will continue, and existing /dev/lesavka_* links stay untouched." >&2 else mapfile -t TAGS < <( for v in "${GC_VIDEOS[@]}"; do sudo udevadm info -q property -n "$v" | awk -F= '/^ID_PATH_TAG=/{print $2}' done ) if [ -z "${TAGS[0]:-}" ] || [ -z "${TAGS[1]:-}" ]; then echo "⚠️ GC311 cards were detected, but ID_PATH_TAG lookup was incomplete." >&2 echo " Skipping udev eye-link refresh and preserving any existing /dev/lesavka_* links." >&2 else printf ' ↪ Left card: %s (%s)\n' "${GC_VIDEOS[0]}" "${TAGS[0]}" printf ' ↪ Right card: %s (%s)\n' "${GC_VIDEOS[1]}" "${TAGS[1]}" LEFT_TAG=${TAGS[0]} RIGHT_TAG=${TAGS[1]} sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null < 3. Rust toolchain" sudo rustup default stable run_as_user rustup default stable echo "==> 4a. Source checkout" SRC_DIR=/var/src/lesavka if [[ ! -d $SRC_DIR ]]; then sudo mkdir -p /var/src sudo chown "$ORIG_USER":"$ORIG_USER" /var/src fi if [[ -d $SRC_DIR/.git ]]; then run_as_user git -C "$SRC_DIR" fetch --all --tags --prune else run_as_user git clone "$REPO_URL" "$SRC_DIR" fi if run_as_user git -C "$SRC_DIR" rev-parse --verify --quiet "origin/$REF" >/dev/null; then run_as_user git -C "$SRC_DIR" checkout -B "$REF" "origin/$REF" else run_as_user git -C "$SRC_DIR" checkout --force "$REF" fi echo "==> 4b. Kernel upgrade (optional)" if [[ "${LESAVKA_KERNEL_UPDATE:-0}" != "0" ]]; then sudo LESAVKA_KERNEL_BUILD_USER="$ORIG_USER" bash "$SRC_DIR/scripts/kernel/build-linux-rpi.sh" else echo "⚠️ skipping kernel upgrade (LESAVKA_KERNEL_UPDATE=0)" fi echo "==> 4c. Source build" run_as_user env TMPDIR="$TMPDIR" bash -c "cd '$SRC_DIR/server' && cargo clean && cargo build --release --bins" echo "==> 5. Install binaries" sudo install -Dm755 "$SRC_DIR/target/release/lesavka-server" /usr/local/bin/lesavka-server sudo install -Dm755 "$SRC_DIR/target/release/lesavka-uvc" /usr/local/bin/lesavka-uvc sudo install -Dm755 "$SRC_DIR/scripts/daemon/lesavka-core.sh" /usr/local/bin/lesavka-core.sh sudo install -Dm755 "$SRC_DIR/scripts/daemon/lesavka-uvc.sh" /usr/local/bin/lesavka-uvc.sh echo "==> 5b. Runtime environment defaults" sudo install -d -m 0755 /etc/lesavka HDMI_CONNECTOR=${LESAVKA_HDMI_CONNECTOR:-$(detect_connected_hdmi_connector)} if [[ -n $HDMI_CONNECTOR ]]; then echo " ↪ HDMI connector: $HDMI_CONNECTOR" else echo "⚠️ no connected HDMI connector detected; leaving LESAVKA_HDMI_CONNECTOR unset." >&2 fi { echo "# generated by lesavka/scripts/install/server.sh" echo "# Edit only for local hardware overrides; rerunning the installer refreshes defaults." if [[ -n $HDMI_CONNECTOR ]]; then printf 'LESAVKA_HDMI_CONNECTOR=%s\n' "$HDMI_CONNECTOR" fi printf 'LESAVKA_HDMI_DRIVER=%s\n' "${LESAVKA_HDMI_DRIVER:-vc4}" printf 'LESAVKA_UAC_DEV=%s\n' "${LESAVKA_UAC_DEV:-hw:UAC2Gadget,0}" printf 'LESAVKA_ALSA_DEV=%s\n' "${LESAVKA_ALSA_DEV:-hw:UAC2Gadget,0}" } | sudo tee /etc/lesavka/server.env >/dev/null echo "==> 6a. Systemd units - lesavka-core" cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-core.service >/dev/null [Unit] Description=lesavka USB gadget bring-up After=sys-kernel-config.mount Requires=sys-kernel-config.mount [Service] Type=oneshot ExecStart=/usr/local/bin/lesavka-core.sh RemainAfterExit=yes Environment=LESAVKA_UVC_FALLBACK=0 Environment=LESAVKA_UVC_CODEC=mjpeg CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE AmbientCapabilities=CAP_SYS_MODULE MountFlags=slave [Install] WantedBy=multi-user.target UNIT echo "==> 6b. Systemd units - lesavka-server" cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-server.service >/dev/null [Unit] Description=lesavka gRPC relay After=network.target lesavka-core.service StartLimitIntervalSec=30 StartLimitBurst=10 [Service] ExecStartPre=/usr/local/bin/lesavka-core.sh --attach ExecStart=/usr/local/bin/lesavka-server TimeoutStopSec=10 KillSignal=SIGTERM KillMode=process Restart=always Environment=RUST_LOG=lesavka_server=info,lesavka_server::audio=info,lesavka_server::video=debug,lesavka_server::gadget=info Environment=RUST_BACKTRACE=1 Environment=GST_DEBUG="*:2,alsasink:6,alsasrc:6" Environment=LESAVKA_UVC_CODEC=mjpeg Environment=LESAVKA_UVC_EXTERNAL=1 Environment=LESAVKA_EYE_ADAPTIVE=1 Environment=LESAVKA_EYE_MIN_FPS=12 Environment=LESAVKA_EYE_FPS=20 Environment=LESAVKA_MIC_INIT_ATTEMPTS=5 Environment=LESAVKA_MIC_INIT_DELAY_MS=250 Environment=LESAVKA_ALLOW_GADGET_CYCLE=1 EnvironmentFile=-/etc/lesavka/server.env Restart=always RestartSec=5 StandardError=append:/tmp/lesavka-server.stderr User=root [Install] WantedBy=multi-user.target UNIT echo "==> 6c. Systemd units - initialization" sudo truncate -s 0 /tmp/lesavka-server.log sudo systemctl daemon-reload sudo systemctl enable lesavka-core lesavka-server UDC_STATE=$(udc_state) if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || ! is_attached_state "$UDC_STATE"; then echo "⚠️ UDC state is '$UDC_STATE' - forcing a Lesavka gadget rebuild before server start." sudo env \ LESAVKA_ALLOW_GADGET_RESET=1 \ LESAVKA_ATTACH_WRITE_UDC=1 \ LESAVKA_DETACH_CLEAR_UDC=1 \ LESAVKA_RELOAD_UVCVIDEO=1 \ LESAVKA_UVC_FALLBACK=1 \ LESAVKA_UVC_CODEC=mjpeg \ /usr/local/bin/lesavka-core.sh sudo systemctl restart lesavka-core echo "✅ lesavka-core installed and restarted..." else echo "⚠️ UDC state is '$UDC_STATE' - skipping lesavka-core restart." echo " Set LESAVKA_ALLOW_GADGET_RESET=1 to force." fi cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-uvc.service >/dev/null [Unit] Description=lesavka UVC control helper After=lesavka-core.service Requires=lesavka-core.service RefuseManualStop=yes [Service] ExecStart=/usr/local/bin/lesavka-uvc.sh Restart=always RestartSec=2 KillSignal=SIGTERM KillMode=process TimeoutStopSec=10 StandardError=append:/tmp/lesavka-uvc.stderr User=root EnvironmentFile=-/etc/lesavka/uvc.env [Install] WantedBy=multi-user.target UNIT sudo systemctl daemon-reload sudo systemctl enable lesavka-uvc echo "==> 6d. Systemd units - remove legacy reboot watchdog" sudo systemctl stop lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true sudo systemctl disable lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true sudo systemctl unmask lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true sudo rm -f /etc/systemd/system/lesavka-watchdog.timer \ /etc/systemd/system/lesavka-watchdog.service \ /usr/local/bin/lesavka-watchdog.sh \ /etc/lesavka/watchdog.touch sudo systemctl daemon-reload if systemctl is-active --quiet lesavka-uvc; then echo "✅ lesavka-uvc is active (dependency-managed; manual restart disabled)." else echo "⚠️ lesavka-uvc is not active; start via lesavka-core dependency path." fi sudo systemctl restart lesavka-server echo "✅ lesavka-server installed and restarted..." echo "➡️ Status: sudo systemctl status lesavka-server --no-pager" echo "➡️ Logs: sudo journalctl -u lesavka-server -f --no-pager"