118 lines
6.4 KiB
Bash
Executable File
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
|