#!/usr/bin/env bash # Offline installer for the move-detect edge daemon. Runs on the air-gapped Pi 5 # (NO internet). Unpack the bundle produced by build_offline_bundle.sh, cd into it, # then: sudo ./install_offline.sh # # Installs everything from the bundled wheelhouse with --no-index (zero fetches), # runs the package straight from source (no build backend needed offline). set -euo pipefail if [ "$(id -u)" -ne 0 ]; then echo "must run as root (sudo ./install_offline.sh)" >&2 exit 1 fi HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PREFIX=/opt/move-detect STATE=/var/lib/move-detect ETC=/etc/move-detect SVC_USER=movedetect WHEELHOUSE="$HERE/wheelhouse" # Mandatory for every backend. onnxruntime is deliberately NOT here — see the optional # install below; a Hailo bundle need not carry its wheel. RUNTIME_PKGS=(numpy requests opencv-python-headless) OPTIONAL_PKGS=(onnxruntime) echo ">> [1/6] service account + groups" if ! id -u "$SVC_USER" >/dev/null 2>&1; then useradd --system --no-create-home --shell /usr/sbin/nologin "$SVC_USER" fi # camera (video), Hailo accelerator (render/plugdev vary by image) — ignore missing groups for g in video render plugdev hailo; do getent group "$g" >/dev/null 2>&1 && usermod -aG "$g" "$SVC_USER" || true done echo ">> [2/6] lay down source, model, docs under $PREFIX" install -d -m 0755 "$PREFIX" "$PREFIX/src" "$PREFIX/models" "$PREFIX/docs" # Fully replace the package tree on re-install/upgrade. Without the rm, `cp -r SRC DEST` # nests SRC *inside* an existing DEST (edge/edge/...) and the OLD code keeps running. rm -rf "$PREFIX/src/edge" cp -r "$HERE/src/edge" "$PREFIX/src/edge" cp "$HERE/src/pyproject.toml" "$PREFIX/src/" 2>/dev/null || true # Copy whichever pose models the bundle carries. Each is optional on its own — a Hailo bundle # ships only the .hef, a CPU bundle only the .onnx — so a missing glob must not abort the install # (the [5/6] backend gate below is what refuses a bundle with NO usable backend). cp "$HERE/models/"*.hef "$PREFIX/models/" 2>/dev/null || true cp "$HERE/models/"*.onnx "$PREFIX/models/" 2>/dev/null || true cp "$HERE/docs/"*.md "$PREFIX/docs/" 2>/dev/null || true echo ">> [3/6] python venv + offline wheel install (--no-index)" # --system-site-packages so the venv can import the Pi SYSTEM packages that are NOT # pip-installable: hailo_platform (HailoRT, apt: hailo-all) for POSE_BACKEND=hailo, and # picamera2 (apt: python3-picamera2) for CAMERA_BACKEND=csi. The wheelhouse install # below adds our deps into the venv; where a system copy already satisfies a wheel's # version constraint pip keeps it (ABI-compatible on the target images), otherwise the # bundled wheel installs into the venv and shadows the system copy. python3 -m venv --system-site-packages "$PREFIX/venv" "$PREFIX/venv/bin/pip" install --no-index --find-links "$WHEELHOUSE" --upgrade pip >/dev/null 2>&1 || true "$PREFIX/venv/bin/pip" install --no-index --find-links "$WHEELHOUSE" "${RUNTIME_PKGS[@]}" # Optional: only present if the bundle was built for the CPU MoveNet backend. Still --no-index # (zero fetches, air-gap intact); absent simply means Hailo-only. "$PREFIX/venv/bin/pip" install --no-index --find-links "$WHEELHOUSE" "${OPTIONAL_PKGS[@]}" \ || echo " -> onnxruntime not in the wheelhouse; Hailo-only bundle (CPU MoveNet unavailable)." echo ">> [4/6] verify the edge package imports offline" # cv2/numpy/requests are mandatory for every backend (snapshot.py imports cv2 at module level # on the unconditional startup chain; the daemon's cloud clients import requests at startup). PYTHONPATH="$PREFIX/src" "$PREFIX/venv/bin/python" -c "import edge.daemon, cv2, numpy, requests; print('edge import OK')" # onnxruntime is the CPU-MoveNet-only dep — the Hailo path never imports it, and onnxruntime # ships no sdist and no 32-bit-ARM wheel, so a bundle built for a Hailo Pi legitimately may # not carry it. Report, don't fail: a hard `import onnxruntime` here refuses the install over # a package the daemon never loads. if PYTHONPATH="$PREFIX/src" "$PREFIX/venv/bin/python" -c "import onnxruntime" >/dev/null 2>&1; then echo " -> onnxruntime present; CPU MoveNet available" else echo " -> onnxruntime absent; CPU MoveNet unavailable (set POSE_BACKEND=hailo in $ETC/edge.env)" fi echo ">> [5/6] pose backend gate + state dir + env file" # Do NOT hand over a Pi whose POSE_BACKEND cannot load. The template ships POSE_BACKEND=movenet, # but a Hailo-only bundle carries no onnxruntime/ONNX — installing it verbatim would crash-loop. # Probe what can ACTUALLY run (like setup_online's [6/8]) and reconcile, or refuse. HEF_DEST="$PREFIX/models/yolov8s_pose.hef" ONNX_DEST="$PREFIX/models/movenet_lightning.onnx" pyhas() { PYTHONPATH="$PREFIX/src" "$PREFIX/venv/bin/python" -c "import $1" >/dev/null 2>&1; } state() { if "$@" >/dev/null 2>&1; then echo present; else echo MISSING; fi; } HAILO_OK=0; MOVENET_OK=0 if [ -f "$HEF_DEST" ] && pyhas hailo_platform; then HAILO_OK=1; fi if [ -f "$ONNX_DEST" ] && pyhas onnxruntime; then MOVENET_OK=1; fi if [ "$HAILO_OK" = 1 ]; then POSE_BACKEND=hailo echo " -> Hailo HEF + hailo_platform present; POSE_BACKEND=hailo" elif [ "$MOVENET_OK" = 1 ]; then POSE_BACKEND=movenet echo " -> CPU MoveNet usable; POSE_BACKEND=movenet" else echo "" >&2 echo "ERROR: no usable pose backend in this bundle — refusing to install a daemon that" >&2 echo "cannot start." >&2 echo " Hailo : HEF=$(state test -f "$HEF_DEST") hailo_platform=$(state "$PREFIX/venv/bin/python" -c 'import hailo_platform')" >&2 echo " MoveNet: onnx=$(state test -f "$ONNX_DEST") onnxruntime=$(state "$PREFIX/venv/bin/python" -c 'import onnxruntime')" >&2 echo " Rebuild the bundle for the target backend (deploy/bundle/build_offline_bundle.sh)." >&2 exit 1 fi install -d -m 0750 "$STATE" chown "$SVC_USER":"$SVC_USER" "$STATE" install -d -m 0750 "$ETC" if [ ! -f "$ETC/edge.env" ]; then install -m 0640 "$HERE/deploy/systemd/move-detect-edge.env.example" "$ETC/edge.env" # Reconcile the template's POSE_BACKEND to the one that actually works in this bundle. sed -i "s|^POSE_BACKEND=.*|POSE_BACKEND=$POSE_BACKEND|" "$ETC/edge.env" if [ "$POSE_BACKEND" = hailo ]; then grep -q '^HAILO_HEF=' "$ETC/edge.env" || printf 'HAILO_HEF=%s\n' "$HEF_DEST" >> "$ETC/edge.env" fi chown root:"$SVC_USER" "$ETC/edge.env" echo " -> wrote $ETC/edge.env (POSE_BACKEND=$POSE_BACKEND; EDIT IT: DEVICE_ID, DEVICE_SECRET, CAFE24_URL)" else echo " -> $ETC/edge.env already exists; left unchanged" fi echo ">> [6/6] systemd unit" install -m 0644 "$HERE/deploy/systemd/move-detect-edge.service" /etc/systemd/system/ systemctl daemon-reload echo "" echo "Installed. Next:" echo " 1. edit $ETC/edge.env (device id/secret, cloud URL; MOVENET_ONNX=$PREFIX/models/movenet_lightning.onnx)" echo " 2. sudo systemctl enable --now move-detect-edge" echo " 3. journalctl -u move-detect-edge -f"