The PHP app runs on Cafe24 shared web hosting -- calling it 'cloud' was misleading. Renames the directory, the edge client module, and every doc/script/identifier that used the term. - cloud/ -> server/ (incl. server/deploy/build_cafe24_package.sh paths) - edge/cloudclient.py -> edge/serverclient.py, CloudClient -> ServerClient - cloud/docs/cloud-deploy.md -> server/docs/server-deploy.md - CLOUD_* env/config keys -> SERVER_* - tests renamed to match Tests: PHP 109 pass (4 skipped), pytest 167 pass. dist/ regenerated.
231 lines
13 KiB
Bash
Executable file
231 lines
13 KiB
Bash
Executable file
#!/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 ">> [0/6] verify bundle integrity BEFORE touching the system"
|
|
# Verify EVERY bundled file against the build-time SHA256SUMS before any mutation of $PREFIX. A
|
|
# truncated/tampered bundle must abort HERE — with the previous install still intact — never after
|
|
# rm -rf has already destroyed it and left the Restart=always unit to run half-copied code. The
|
|
# build now hashes the daemon source, unit and this installer too, so tampered code cannot pass.
|
|
ALLOW_UNVERIFIED="${ALLOW_UNVERIFIED:-0}"
|
|
if [ -f "$HERE/SHA256SUMS" ]; then
|
|
( cd "$HERE" && sha256sum -c SHA256SUMS ) \
|
|
|| { echo "ERROR: bundle integrity check failed (SHA256SUMS mismatch) — refusing to install." >&2; exit 1; }
|
|
# sha256sum -c only checks LISTED files; an INJECTED extra wheel (which could win pip's version
|
|
# resolution) would pass unnoticed. Refuse any staged file not present in the manifest.
|
|
EXTRA="$( cd "$HERE" && comm -23 \
|
|
<(find . -type f ! -name SHA256SUMS ! -name 'SHA256SUMS.asc' | sed 's|^\./||' | sort) \
|
|
<(awk '{print $2}' SHA256SUMS | sed 's|^[*]||; s|^\./||' | sort) )"
|
|
if [ -n "$EXTRA" ]; then
|
|
echo "ERROR: bundle contains file(s) not listed in SHA256SUMS (possible injection):" >&2
|
|
echo "$EXTRA" >&2
|
|
exit 1
|
|
fi
|
|
# A detached signature proves PROVENANCE — an unsigned SHA256SUMS only detects corruption, since
|
|
# an attacker who can modify the bundle can strip the .asc and regenerate the hashes. Set
|
|
# REQUIRE_SIGNATURE=1 for untrusted transport to make a missing/invalid signature fatal.
|
|
REQUIRE_SIGNATURE="${REQUIRE_SIGNATURE:-0}"
|
|
if [ -f "$HERE/SHA256SUMS.asc" ]; then
|
|
gpg --verify "$HERE/SHA256SUMS.asc" "$HERE/SHA256SUMS" 2>/dev/null \
|
|
|| { echo "ERROR: SHA256SUMS.asc present but its signature did NOT verify — refusing to install." >&2; exit 1; }
|
|
echo " -> SHA256SUMS signature verified"
|
|
elif [ "$REQUIRE_SIGNATURE" = 1 ]; then
|
|
echo "ERROR: REQUIRE_SIGNATURE=1 but this bundle has no SHA256SUMS.asc. An unsigned bundle" >&2
|
|
echo "detects corruption only, not tampering — refusing. Build with SIGN_KEY, or clear the flag." >&2
|
|
exit 1
|
|
else
|
|
echo " -> NOTE: unsigned bundle (corruption-detection only; set REQUIRE_SIGNATURE=1 to require a signature)."
|
|
fi
|
|
echo " -> bundle integrity OK"
|
|
elif [ "$ALLOW_UNVERIFIED" = 1 ]; then
|
|
echo " -> WARNING: no SHA256SUMS; ALLOW_UNVERIFIED=1 set — installing WITHOUT integrity check." >&2
|
|
else
|
|
echo "ERROR: no SHA256SUMS in this bundle — refusing to install unverified code. Rebuild with a" >&2
|
|
echo "current build_offline_bundle.sh, or (NOT recommended) re-run with ALLOW_UNVERIFIED=1." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo ">> [0b/6] Hailo arch preflight (BEFORE any mutation)"
|
|
# A hailo8 HEF on a hailo8l board (or vice-versa) imports fine but the daemon dies with
|
|
# HAILO_INVALID_HEF(26) on every start, forever. Catch the WRONG bundle here — using MANIFEST.txt
|
|
# (staged) + hailortcli (a system CLI, no venv needed) — so a mismatch aborts with the previous
|
|
# install still intact, not after the HEF/source/venv have already been overwritten.
|
|
BUNDLE_HAILO_ARCH="$(awk -F': ' '/^hailo_arch:/{gsub(/[[:space:]]/,"",$2); print tolower($2); exit}' "$HERE/MANIFEST.txt" 2>/dev/null || true)"
|
|
if [ -n "$BUNDLE_HAILO_ARCH" ]; then
|
|
BOARD_HAILO_ARCH=""
|
|
if command -v hailortcli >/dev/null 2>&1; then
|
|
BOARD_HAILO_ARCH="$(hailortcli fw-control identify 2>/dev/null \
|
|
| awk -F': ' '/[Aa]rchitecture/{gsub(/[[:space:]]/,"",$2); print tolower($2); exit}')"
|
|
fi
|
|
if [ -n "$BOARD_HAILO_ARCH" ] && [ "$BUNDLE_HAILO_ARCH" != "$BOARD_HAILO_ARCH" ]; then
|
|
echo "ERROR: Hailo arch mismatch — this bundle's HEF is '$BUNDLE_HAILO_ARCH' but the board is" >&2
|
|
echo "'$BOARD_HAILO_ARCH'. It would crash-loop with HAILO_INVALID_HEF(26). Rebuild with" >&2
|
|
echo "HAILO_ARCH=$BOARD_HAILO_ARCH (deploy/bundle/build_offline_bundle.sh)." >&2
|
|
exit 1
|
|
elif [ -z "$BOARD_HAILO_ARCH" ]; then
|
|
echo " -> NOTE: could not read the board's Hailo arch (hailortcli?) to verify the bundle's" >&2
|
|
echo " '$BUNDLE_HAILO_ARCH'; if the daemon later fails HAILO_INVALID_HEF(26), the arch is wrong." >&2
|
|
else
|
|
echo " -> Hailo arch OK ($BUNDLE_HAILO_ARCH)"
|
|
fi
|
|
fi
|
|
|
|
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, atomically. Stage the new tree beside the
|
|
# old and rename over it: an interrupted copy leaves the previous WORKING tree untouched (the
|
|
# Restart=always unit keeps running old code) instead of a half-copied one. The rename also avoids
|
|
# `cp -r SRC DEST` nesting SRC *inside* an existing DEST (edge/edge/...) and running the OLD code.
|
|
rm -rf "$PREFIX/src/edge.new"
|
|
cp -r "$HERE/src/edge" "$PREFIX/src/edge.new"
|
|
rm -rf "$PREFIX/src/edge"
|
|
mv "$PREFIX/src/edge.new" "$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)"
|
|
# Integrity was already verified up-front in step [0/6], before any system mutation.
|
|
# --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 server 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 ] && [ "$MOVENET_OK" != 1 ]; then
|
|
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
|
|
# Prefer Hailo (the accelerator) when usable; MoveNet is the CPU fallback. (Arch already
|
|
# preflighted in [0b/6].)
|
|
if [ "$HAILO_OK" = 1 ]; then DEFAULT_BACKEND=hailo; else DEFAULT_BACKEND=movenet; fi
|
|
backend_usable() { # 0 (true) iff the named backend can actually load in THIS bundle
|
|
case "$1" in
|
|
hailo) [ "$HAILO_OK" = 1 ] ;;
|
|
movenet) [ "$MOVENET_OK" = 1 ] ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
install -d -m 0750 "$STATE"
|
|
chown "$SVC_USER":"$SVC_USER" "$STATE"
|
|
install -d -m 0750 "$ETC"
|
|
if [ ! -f "$ETC/edge.env" ]; then
|
|
POSE_BACKEND="$DEFAULT_BACKEND"
|
|
install -m 0640 "$HERE/deploy/systemd/move-detect-edge.env.example" "$ETC/edge.env"
|
|
sed -i "s|^POSE_BACKEND=.*|POSE_BACKEND=$POSE_BACKEND|" "$ETC/edge.env"
|
|
echo " -> wrote $ETC/edge.env (POSE_BACKEND=$POSE_BACKEND; EDIT IT: DEVICE_ID, DEVICE_SECRET, CAFE24_URL)"
|
|
else
|
|
# Preserve the operator's DELIBERATE backend choice when it still works in this bundle; only
|
|
# switch when the current one can't load — the crash-loop this reconcile exists to prevent
|
|
# (e.g. a stale POSE_BACKEND=movenet on a Hailo-only bundle with no onnxruntime).
|
|
CURRENT_BACKEND="$(sed -n 's|^POSE_BACKEND=\(.*\)|\1|p' "$ETC/edge.env" | head -n1)"
|
|
if backend_usable "$CURRENT_BACKEND"; then
|
|
POSE_BACKEND="$CURRENT_BACKEND"
|
|
echo " -> $ETC/edge.env POSE_BACKEND=$POSE_BACKEND is usable in this bundle; kept"
|
|
else
|
|
POSE_BACKEND="$DEFAULT_BACKEND"
|
|
if grep -q '^POSE_BACKEND=' "$ETC/edge.env"; then
|
|
sed -i "s|^POSE_BACKEND=.*|POSE_BACKEND=$POSE_BACKEND|" "$ETC/edge.env"
|
|
else
|
|
printf 'POSE_BACKEND=%s\n' "$POSE_BACKEND" >> "$ETC/edge.env"
|
|
fi
|
|
echo " -> reconciled $ETC/edge.env POSE_BACKEND: '${CURRENT_BACKEND:-unset}' (unusable) -> '$POSE_BACKEND'"
|
|
fi
|
|
fi
|
|
# For the hailo backend, HAILO_HEF must point at a HEF that actually EXISTS — correct a missing
|
|
# key OR one left pointing at a stale/absent path, else the daemon crash-loops on a bad path.
|
|
if [ "$POSE_BACKEND" = hailo ]; then
|
|
CURRENT_HEF="$(sed -n 's|^HAILO_HEF=\(.*\)|\1|p' "$ETC/edge.env" | head -n1)"
|
|
if [ -z "$CURRENT_HEF" ] || [ ! -f "$CURRENT_HEF" ]; then
|
|
if grep -q '^HAILO_HEF=' "$ETC/edge.env"; then
|
|
sed -i "s|^HAILO_HEF=.*|HAILO_HEF=$HEF_DEST|" "$ETC/edge.env"
|
|
else
|
|
printf 'HAILO_HEF=%s\n' "$HEF_DEST" >> "$ETC/edge.env"
|
|
fi
|
|
echo " -> set HAILO_HEF=$HEF_DEST (was '${CURRENT_HEF:-unset}')"
|
|
fi
|
|
fi
|
|
chown root:"$SVC_USER" "$ETC/edge.env"
|
|
|
|
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, server 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"
|