SanadR1/docker/install.sh

118 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# SanadR1 turnkey installer — run this ON A FRESH R1 EDU BACKPACK.
#
# tar xzf sanadr1-bundle-*.tar.gz && cd sanadr1-bundle && ./install.sh
#
# Auto-detects the robot NIC, the current user/home, and USB audio — no values
# are hardcoded to any one robot. Idempotent: safe to re-run to update the image.
#
# The Gemini API key is taken from (first found):
# 1. $SANAD_GEMINI_API_KEY in the environment
# 2. an existing <install>/.env
# 3. an interactive prompt
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMG_TAR="$HERE/sanadr1-latest-arm64.tar.gz"
say() { printf '\033[1;36m>> %s\033[0m\n' "$*"; }
die() { printf '\033[1;31m!! %s\033[0m\n' "$*" >&2; exit 1; }
# ── 1. preflight ────────────────────────────────────────────────────────────
command -v docker >/dev/null 2>&1 || die "Docker is not installed. Install Docker Engine first, then re-run."
docker info >/dev/null 2>&1 || die "Docker daemon not reachable. Start it (sudo systemctl start docker) or add yourself to the 'docker' group."
[ -f "$IMG_TAR" ] || die "missing image tarball: $IMG_TAR (run this from the extracted bundle dir)."
arch="$(uname -m)"
[ "$arch" = "aarch64" ] || say "WARNING: arch=$arch — the image is arm64/aarch64; it will only run on an ARM64 backpack."
# sudo helper (for the systemd unit); prompts once if needed.
SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo"
$SUDO true 2>/dev/null || die "need sudo to install the systemd auto-start unit."
# ── 2. auto-detect the robot facts ──────────────────────────────────────────
IFACE="$(ip -o -4 addr show 2>/dev/null | awk '$4 ~ /^192\.168\.123\./{print $2; exit}')"
[ -n "$IFACE" ] || { IFACE=eth10; say "no NIC on 192.168.123.x yet (robot link down?) — defaulting to eth10"; }
RUN_USER="$(id -un)"; RUN_HOME="$HOME"
DEST="$RUN_HOME/SanadR1/docker"
say "robot NIC: $IFACE"
say "install as: user=$RUN_USER home=$RUN_HOME"
say "install to: $DEST"
# ── 3. load the image ───────────────────────────────────────────────────────
say "loading sanadr1:latest (this can take ~30-60 s)…"
gunzip -c "$IMG_TAR" | docker load
docker image inspect sanadr1:latest >/dev/null 2>&1 || die "image did not load."
# ── 4. drop the run recipe ──────────────────────────────────────────────────
mkdir -p "$DEST"
cp "$HERE/run.sh" "$DEST/run.sh"; chmod +x "$DEST/run.sh"
cp "$HERE/healthcheck.sh" "$DEST/healthcheck.sh"; chmod +x "$DEST/healthcheck.sh"
# ── 5. .env (key + detected interface) ──────────────────────────────────────
ENV_FILE="$DEST/.env"
if [ -f "$ENV_FILE" ] && grep -q '^SANAD_GEMINI_API_KEY=.\+' "$ENV_FILE"; then
say "keeping existing $ENV_FILE (already has a key)"
else
KEY="${SANAD_GEMINI_API_KEY:-}"
if [ -z "$KEY" ]; then
printf 'Enter SANAD_GEMINI_API_KEY (paste, then Enter): '
read -r KEY
fi
[ -n "$KEY" ] || die "no Gemini API key given — set SANAD_GEMINI_API_KEY or paste it when prompted."
umask 077
printf 'SANAD_GEMINI_API_KEY=%s\nSANAD_DDS_INTERFACE=%s\n' "$KEY" "$IFACE" > "$ENV_FILE"
say "wrote $ENV_FILE (mode 600)"
fi
# ensure the interface line is present even if .env was pre-supplied
grep -q '^SANAD_DDS_INTERFACE=' "$ENV_FILE" || printf 'SANAD_DDS_INTERFACE=%s\n' "$IFACE" >> "$ENV_FILE"
# ── 6. systemd auto-start unit (generated with the detected user/path) ──────
say "installing systemd unit → /etc/systemd/system/sanadr1.service"
$SUDO tee /etc/systemd/system/sanadr1.service >/dev/null <<EOF
# Auto-generated by SanadR1 install.sh — manages the Docker container on boot.
[Unit]
Description=SanadR1 (Docker) - Gemini voice + dashboard on the R1 EDU backpack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
# Self-healing: recreate from run.sh if the container was removed (e.g. image
# reload); otherwise just (re)start the existing one.
ExecStartPre=-/bin/sh -c 'docker inspect sanadr1 >/dev/null 2>&1 || $DEST/run.sh'
ExecStart=/usr/bin/docker start sanadr1
ExecStop=/usr/bin/docker stop sanadr1
[Install]
WantedBy=multi-user.target
EOF
$SUDO systemctl daemon-reload
# ── 7. first run + enable on boot ───────────────────────────────────────────
say "starting the container…"
"$DEST/run.sh"
$SUDO systemctl enable sanadr1 >/dev/null 2>&1 || true
# Activate the oneshot too (container is already up; ExecStartPre sees it and
# skips run.sh, ExecStart's `docker start` is a no-op) so `systemctl is-active`
# reads 'active' now, not just after the next reboot.
$SUDO systemctl start sanadr1 >/dev/null 2>&1 || true
# ── 8. verify (full health check) ───────────────────────────────────────────
say "waiting for the dashboard…"
up=0
for i in $(seq 1 25); do curl -fsS -m3 http://127.0.0.1:8001/ >/dev/null 2>&1 && { up=1; break; }; sleep 2; done
[ "$up" = 1 ] || die "dashboard did not answer in ~50 s. Check: docker logs -f sanadr1"
WIFI="$(ip -o -4 addr show 2>/dev/null | awk '$4 ~ /^10\.|^192\.168\.[0-9]+\./ && $2!="lo"{print $4}' | cut -d/ -f1 | head -1)"
say "dashboard up → http://${WIFI:-<backpack-ip>}:8001 — running full health check…"
echo
if "$DEST/healthcheck.sh"; then
say "auto-start enabled. Manage with: sudo systemctl {status,restart,stop} sanadr1"
exit 0
else
say "install finished but the health check reported failures (see above). Re-run: $DEST/healthcheck.sh"
exit 1
fi