- server-deploy.md: state MySQL 8.0.13+ / MariaDB 10.2+ is required (expression defaults), not "5.7+" — an import against 5.7 is a day-1 ERROR 1064 blocker. - pilot-runbook: add the air-gapped Pi path and mark pip/fetch as build-machine only; stop the shipped doc from telling a Pi operator the bundle "does not exist". - install_offline.sh: instruct the camera probe before enable (mirrors setup_online). - setup_online.sh: chmod 600 device.env on read; README documents delete + rotate. - systemd unit: order After/Wants=time-sync.target so occurred_at is real wall time.
343 lines
20 KiB
Bash
Executable file
343 lines
20 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ONE-COMMAND online installer for an INTERNET-CONNECTED Raspberry Pi 5.
|
|
# Installs everything and starts the service. For AIR-GAPPED Pis use deploy/bundle/*.
|
|
#
|
|
# Run from a clone/copy of this repo, as root:
|
|
# sudo deploy/setup_online.sh
|
|
#
|
|
# Pre-provisioned (shortest path): put the values in deploy/device.env on the BUILD machine
|
|
# (DEVICE_ID / DEVICE_SECRET / CAFE24_URL / CAMERAS, one KEY=value per line, gitignored), copy
|
|
# the repo to the Pi, and the whole install is just:
|
|
# sudo deploy/setup_online.sh
|
|
#
|
|
# Non-interactive: pass values as env vars (these override device.env), e.g.
|
|
# sudo DEVICE_ID=pi-01 DEVICE_SECRET=... CAFE24_URL=https://server CAMERAS=csi:0:cam-01,uvc:0:cam-02 \
|
|
# deploy/setup_online.sh
|
|
# MODELS ARE NOT DOWNLOADED HERE. The pose HEF (and optional MoveNet ONNX) ship IN the delivery,
|
|
# staged on the BUILD machine (deploy/bundle/build_offline_bundle.sh) — air-gap rule "build pulls,
|
|
# target doesn't". Copy the delivery (with its models/) to the Pi and this installer places them.
|
|
# Have a HEF elsewhere on the Pi? Point at it: HAILO_HEF=/home/pi/yolov8s_pose.hef.
|
|
# Interactive (a real terminal): it prompts for anything not passed.
|
|
set -euo pipefail
|
|
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "run as root: sudo $0" >&2
|
|
exit 1
|
|
fi
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
PREFIX=/opt/move-detect
|
|
STATE=/var/lib/move-detect
|
|
ETC=/etc/move-detect
|
|
SVC_USER=movedetect
|
|
|
|
echo ">> [1/9] system packages (CSI camera, venv; Hailo runtime best-effort)"
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
# A freshly-booted Pi runs apt-daily / apt-daily-upgrade / unattended-upgrades, which hold
|
|
# /var/lib/apt/lists/lock and /var/lib/dpkg/lock-frontend for the first few minutes —
|
|
# exactly when someone runs a one-command installer. A bare apt-get then exits 100 with
|
|
# "E: Unable to lock directory /var/lib/apt/lists/" and `set -e` (set -e above) kills us here.
|
|
# -o DPkg::Lock::Timeout is not enough on its own: apt applies it to the dpkg locks, but
|
|
# `apt-get update`'s lists lock is taken with a single non-blocking fcntl(F_SETLK) and no
|
|
# retry. That wait has to live here, in the shell.
|
|
# Retry ONLY on lock contention; a real error (e.g. hailo-all absent from the repo) must
|
|
# still fail fast so the `|| echo WARN` fallback below stays instant.
|
|
apt_retry() {
|
|
local tries=30 i=1 rc log
|
|
log="$(mktemp)"
|
|
while :; do
|
|
rc=0
|
|
# LC_ALL=C is load-bearing, not cosmetic: apt gettext-translates its errors, and sudo
|
|
# passes the caller's LANG/LC_* straight through. On a ko_KR Pi the lists-lock message
|
|
# renders as "%s 디렉터리를 잠글 수 없습니다", the grep below misses it, and lock
|
|
# contention gets misread as a real error — fail-fast, zero retries, exactly the bug
|
|
# this function exists to fix. Pinning it per-call (not script-wide) keeps pip/venv and
|
|
# fetch_movenet at [4/9]-[5/9] on the system locale.
|
|
LC_ALL=C apt-get -o DPkg::Lock::Timeout=60 "$@" 2>&1 | tee "$log" || rc=${PIPESTATUS[0]}
|
|
if [ "$rc" -eq 0 ]; then rm -f "$log"; return 0; fi
|
|
if ! grep -qE 'Could not get lock|Unable to lock|Unable to acquire the dpkg' "$log"; then
|
|
rm -f "$log"; return "$rc"
|
|
fi
|
|
if [ "$i" -ge "$tries" ]; then
|
|
rm -f "$log"
|
|
echo " ERROR: apt/dpkg still locked after $((tries * 10))s. See who holds it with:" >&2
|
|
echo " systemctl status apt-daily.service apt-daily-upgrade.service unattended-upgrades.service" >&2
|
|
return "$rc"
|
|
fi
|
|
echo " apt/dpkg locked by another process (apt-daily/unattended-upgrades); retry $i/$tries in 10s..."
|
|
i=$((i + 1)); sleep 10
|
|
done
|
|
}
|
|
apt_retry update
|
|
apt_retry install -y python3-picamera2 python3-venv python3-pip
|
|
# hailo-all is the Hailo driver+runtime; only present in Pi repos with a Hailo HAT.
|
|
# Best-effort so a Pi without it still installs and runs on CPU MoveNet.
|
|
apt_retry install -y hailo-all || echo " WARN: hailo-all not installed — Hailo accel unavailable, CPU MoveNet will be used."
|
|
|
|
echo ">> [2/9] service account + device groups"
|
|
id -u "$SVC_USER" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$SVC_USER"
|
|
for g in video render plugdev hailo; do
|
|
getent group "$g" >/dev/null 2>&1 && usermod -aG "$g" "$SVC_USER" || true
|
|
done
|
|
|
|
echo ">> [3/9] source + dirs under $PREFIX"
|
|
install -d -m 0755 "$PREFIX" "$PREFIX/src" "$PREFIX/models" "$PREFIX/docs"
|
|
rm -rf "$PREFIX/src/edge" # full replace on re-run (no nested edge/edge)
|
|
cp -r "$REPO_ROOT/edge" "$PREFIX/src/edge"
|
|
cp "$REPO_ROOT/pyproject.toml" "$PREFIX/src/" 2>/dev/null || true
|
|
find "$PREFIX/src" -name __pycache__ -type d -prune -exec rm -rf {} +
|
|
cp "$REPO_ROOT/docs/pilot-runbook.md" "$PREFIX/docs/" 2>/dev/null || true
|
|
|
|
echo ">> [4/9] venv (+ system site-packages) + pip runtime deps"
|
|
python3 -m venv --system-site-packages "$PREFIX/venv"
|
|
"$PREFIX/venv/bin/pip" install --upgrade pip >/dev/null
|
|
# MANDATORY for every backend — do not soften these to `|| echo WARN`:
|
|
# cv2 edge/snapshot.py imports it at MODULE level, and that module is on the
|
|
# unconditional startup chain (daemon -> runtime -> snapshot). No POSE_BACKEND
|
|
# can reach build_pose() without it.
|
|
# requests the daemon builds its server/alert clients with no injected HTTP fn, so
|
|
# `import requests` fires at startup regardless of backend.
|
|
# numpy is still deliberately NOT named here, but the reason has moved: it no longer arrives
|
|
# transitively via onnxruntime (best-effort below, may be absent). opencv-python-headless
|
|
# hard-requires numpy and opencv is mandatory, so numpy still always arrives; leaving it
|
|
# unnamed lets the resolver keep the apt numpy that python3-picamera2 was compiled against
|
|
# whenever that copy satisfies opencv's floor.
|
|
"$PREFIX/venv/bin/pip" install requests opencv-python-headless
|
|
# onnxruntime is the CPU-MoveNet-ONLY dep: movenet_pose.py imports it inside
|
|
# MoveNetPoseAdapter.__init__, and the Hailo path never imports the module at all. It
|
|
# publishes NO sdist and NO 32-bit-ARM wheel, so pip can legitimately find zero candidates
|
|
# and exit non-zero in seconds — which under `set -euo pipefail` used to kill this installer
|
|
# on a Hailo-8 Pi that never needed it. Best-effort, like hailo-all at [1/9]; [6/9] decides
|
|
# whether a backend actually exists.
|
|
"$PREFIX/venv/bin/pip" install onnxruntime \
|
|
|| echo " WARN: onnxruntime did not install — CPU MoveNet unavailable (Hailo unaffected)."
|
|
|
|
echo ">> [5/9] MoveNet CPU model (from the delivery, not downloaded)"
|
|
# Models ship IN the delivery, staged on the BUILD machine (deploy/bundle/build_offline_bundle.sh
|
|
# fetches them there) and carried across with the repo copy — air-gap rule "build pulls, target
|
|
# doesn't". This installer NEVER downloads a model on the Pi. On-device fetch was removed because
|
|
# it was fragile in two ways at once: a partial repo copy made `-m edge.tools.fetch_*` fail with a
|
|
# ModuleNotFoundError that the `|| echo WARN` mislabeled as "download failed", and the MoveNet URL
|
|
# is dead upstream anyway. MoveNet is the OPTIONAL CPU fallback; stage it only if present.
|
|
if cp "$REPO_ROOT/models/movenet_lightning.onnx" "$PREFIX/models/" 2>/dev/null; then
|
|
echo " -> MoveNet ONNX staged from the delivery"
|
|
else
|
|
echo " note: no MoveNet ONNX in the delivery (expected for a Hailo device)."
|
|
fi
|
|
|
|
echo ">> [6/9] pose backend"
|
|
HEF_DEST="$PREFIX/models/yolov8s_pose.hef"
|
|
ONNX_DEST="$PREFIX/models/movenet_lightning.onnx"
|
|
if [ -n "${HAILO_HEF:-}" ]; then
|
|
# An explicit path that does not exist is a typo, not a request to fall back. The old single
|
|
# guarded `cp` skipped it with NO warning and produced the byte-identical HEF=MISSING table
|
|
# as the unset case, never echoing the bad value — and if an earlier run had left a
|
|
# $HEF_DEST behind, that stale HEF got adopted as though it were the one named.
|
|
[ -f "$HAILO_HEF" ] || { echo "ERROR: HAILO_HEF=$HAILO_HEF does not exist." >&2; exit 1; }
|
|
cp "$HAILO_HEF" "$HEF_DEST"
|
|
echo " -> HEF installed from HAILO_HEF=$HAILO_HEF"
|
|
elif [ -f "$REPO_ROOT/models/yolov8s_pose.hef" ]; then
|
|
# The delivery carries the board-matching HEF, staged on the BUILD machine
|
|
# (build_offline_bundle.sh runs fetch_hailo_hef there, where hailortcli or --arch picks
|
|
# hailo8 vs hailo8l). This installer only PLACES it — it never downloads or guesses an arch
|
|
# on the Pi. Air-gap rule: build pulls, target doesn't.
|
|
cp "$REPO_ROOT/models/yolov8s_pose.hef" "$HEF_DEST"
|
|
echo " -> HEF staged from the delivery (models/yolov8s_pose.hef)"
|
|
else
|
|
echo " WARN: no Hailo HEF in the delivery (models/yolov8s_pose.hef) and HAILO_HEF unset —"
|
|
echo " no Hailo backend. The probe below decides if CPU MoveNet can carry the install."
|
|
fi
|
|
|
|
# Probe what can ACTUALLY run, in the venv the service will use. A HEF on disk is not enough
|
|
# (hailo_platform comes from apt hailo-all, best-effort at [1/9]), and a pip exit code is not
|
|
# enough either (a wheel can install and still fail to load its .so). Both probes must sit
|
|
# inside `if` conditions — a bare `a && b` list that fails is fatal under set -e.
|
|
pyhas() { "$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"
|
|
[ "$MOVENET_OK" = 1 ] || echo " (CPU MoveNet fallback unavailable — Hailo does not need it)"
|
|
elif [ "$MOVENET_OK" = 1 ]; then
|
|
POSE_BACKEND=movenet
|
|
echo " -> no usable Hailo; starting on CPU MoveNet."
|
|
echo " For Hailo: copy yolov8s_pose.hef to $HEF_DEST, set POSE_BACKEND=hailo in"
|
|
echo " $ETC/edge.env, then: sudo systemctl restart move-detect-edge"
|
|
else
|
|
# Fail HERE, loudly, naming the fix — not at 3am. build_pose() runs outside the daemon's
|
|
# try/except, so a missing backend is an uncaught traceback (ModuleNotFoundError on the
|
|
# MoveNet path, KeyError 'HAILO_HEF' on the Hailo one) on every Restart=always cycle,
|
|
# forever — a unit that looks installed and never processes a frame.
|
|
# The board's own answer is the most diagnostic field here: it separates "no HEF" from
|
|
# "driver installed, reboot pending" from "no Hailo at all" — three different fixes.
|
|
HAILO_ARCH="$(hailortcli fw-control identify 2>/dev/null \
|
|
| sed -n 's/.*Device Architecture:[[:space:]]*//p' | head -1)"
|
|
echo "" >&2
|
|
echo "ERROR: no usable pose backend — refusing to install a daemon that cannot start." >&2
|
|
echo " Hailo : HEF=$(state test -f "$HEF_DEST") hailo_platform=$(state "$PREFIX/venv/bin/python" -c 'import hailo_platform') board=${HAILO_ARCH:-NOT DETECTED}" >&2
|
|
echo " MoveNet: onnx=$(state test -f "$ONNX_DEST") onnxruntime=$(state "$PREFIX/venv/bin/python" -c 'import onnxruntime')" >&2
|
|
echo "" >&2
|
|
echo " Fix ONE of these, then re-run: sudo deploy/setup_online.sh" >&2
|
|
echo "" >&2
|
|
echo " A) Hailo-8 HAT (the intended path). The HEF ships IN the delivery" >&2
|
|
echo " (models/yolov8s_pose.hef, staged on the build machine) — this installer does NOT" >&2
|
|
echo " download it. Read the table above:" >&2
|
|
echo " HEF=MISSING -> this Pi's copy of the delivery has no" >&2
|
|
echo " models/yolov8s_pose.hef. Re-copy it, or point at one:" >&2
|
|
echo " sudo HAILO_HEF=/path/to/yolov8s_pose.hef deploy/setup_online.sh" >&2
|
|
echo " hailo_platform=MISSING -> the HailoRT driver/runtime is not up yet:" >&2
|
|
echo " sudo apt-get install -y hailo-all && sudo reboot" >&2
|
|
echo " (/dev/hailo0 appears only after that reboot). Confirm:" >&2
|
|
echo " hailortcli fw-control identify # -> Device Architecture: HAILO8" >&2
|
|
echo " B) CPU MoveNet: put models/movenet_lightning.onnx in the delivery, and" >&2
|
|
echo " $PREFIX/venv/bin/pip install onnxruntime" >&2
|
|
echo "" >&2
|
|
echo " If onnxruntime AND hailo-all both refuse to install, check the userland arch:" >&2
|
|
echo " dpkg --print-architecture" >&2
|
|
echo " 'armhf' means 32-bit Pi OS. Neither package ships for it (uname -m still says" >&2
|
|
echo " aarch64 there — the kernel is 64-bit, the userland is not). Reflash 64-bit Pi OS." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo ">> [7/9] config -> $ETC/edge.env"
|
|
install -d -m 0750 "$STATE"; chown "$SVC_USER":"$SVC_USER" "$STATE"
|
|
install -d -m 0750 "$ETC"
|
|
if [ -f "$ETC/edge.env" ]; then
|
|
# Re-running IS the normal Hailo flow: install -> reboot so the driver creates /dev/hailo0
|
|
# -> re-run. So [6/9] can legitimately reach a different verdict than last time. Leaving the
|
|
# file wholly unchanged meant run #1's POSE_BACKEND=movenet survived while [6/9] printed
|
|
# "POSE_BACKEND=hailo" — and with onnxruntime absent that is a ModuleNotFoundError crash
|
|
# loop the installer just called a success.
|
|
# Reconcile ONLY the backend keys, in place: DEVICE_SECRET and the operator's other answers
|
|
# must survive untouched.
|
|
cur="$(sed -n 's/^POSE_BACKEND=//p' "$ETC/edge.env" | head -1)"
|
|
if [ "$cur" = "$POSE_BACKEND" ]; then
|
|
echo " -> $ETC/edge.env already exists; left unchanged"
|
|
else
|
|
if [ -n "$cur" ]; 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
|
|
grep -q '^HAILO_HEF=' "$ETC/edge.env" || printf 'HAILO_HEF=%s\n' "$HEF_DEST" >> "$ETC/edge.env"
|
|
grep -q '^MOVENET_ONNX=' "$ETC/edge.env" || printf 'MOVENET_ONNX=%s\n' "$ONNX_DEST" >> "$ETC/edge.env"
|
|
# sed -i and >> can leave the file owned root:root, and the unit reads it as movedetect
|
|
# via EnvironmentFile= — a wrong group here fails the start with a bare permission error.
|
|
chown root:"$SVC_USER" "$ETC/edge.env"
|
|
chmod 0640 "$ETC/edge.env"
|
|
echo " -> $ETC/edge.env: POSE_BACKEND ${cur:-<unset>} -> $POSE_BACKEND (re-run reconciled)"
|
|
fi
|
|
else
|
|
# Pre-provisioning file: values written on the BUILD machine so the operator only has to
|
|
# type `sudo deploy/setup_online.sh`. A real env var always wins over the file, so the
|
|
# long-form command documented at the top still overrides a stale device.env.
|
|
PROVISION="${PROVISION:-$REPO_ROOT/deploy/device.env}"
|
|
if [ -f "$PROVISION" ]; then
|
|
echo ">> reading pre-provisioned values from $PROVISION"
|
|
# device.env holds the plaintext HMAC secret. It arrived via "copy the working tree" and may
|
|
# be world-readable (umask 022). Lock it to root-only now so it is not left readable by every
|
|
# local account for the life of the device. The secret still lands in /etc/move-detect/edge.env
|
|
# (0640 root:movedetect); once the install succeeds you should DELETE this file and rotate the
|
|
# secret if it was ever exposed — see deploy/README.md "Secret handling".
|
|
chmod 600 "$PROVISION" 2>/dev/null || true
|
|
while IFS='=' read -r k v; do
|
|
case "$k" in DEVICE_ID|DEVICE_SECRET|CAFE24_URL|CAMERAS) ;; *) continue ;; esac
|
|
[ -n "${!k:-}" ] || printf -v "$k" '%s' "$v"
|
|
done < <(grep -E '^[A-Z_][A-Z0-9_]*=' "$PROVISION") # digits matter: CAFE24_URL
|
|
fi
|
|
: "${DEVICE_ID:=}"; : "${DEVICE_SECRET:=}"; : "${CAFE24_URL:=}"; : "${CAMERAS:=}"
|
|
if [ -t 0 ]; then
|
|
[ -n "$DEVICE_ID" ] || read -rp "DEVICE_ID [pi-01]: " DEVICE_ID
|
|
[ -n "$DEVICE_SECRET" ] || { read -rsp "DEVICE_SECRET (from server): " DEVICE_SECRET; echo; }
|
|
[ -n "$CAFE24_URL" ] || read -rp "CAFE24_URL (e.g. https://<domain>/move_detect — include the subdir!): " CAFE24_URL
|
|
[ -n "$CAMERAS" ] || read -rp "CAMERAS [csi:0:cam-01,uvc:0:cam-02]: " CAMERAS
|
|
fi
|
|
DEVICE_ID="${DEVICE_ID:-pi-01}"
|
|
CAMERAS="${CAMERAS:-csi:0:cam-01,uvc:0:cam-02}"
|
|
# Every line is a bare KEY=value (systemd EnvironmentFile= keeps inline text literally).
|
|
umask 077
|
|
cat > "$ETC/edge.env" <<EOF
|
|
DEVICE_ID=$DEVICE_ID
|
|
DEVICE_SECRET=$DEVICE_SECRET
|
|
CAFE24_URL=$CAFE24_URL
|
|
CAMERAS=$CAMERAS
|
|
POSE_BACKEND=$POSE_BACKEND
|
|
HAILO_HEF=$HEF_DEST
|
|
MOVENET_ONNX=$PREFIX/models/movenet_lightning.onnx
|
|
QUEUE_DB=$STATE/queue.db
|
|
TARGET_FPS=12
|
|
EOF
|
|
chown root:"$SVC_USER" "$ETC/edge.env"
|
|
chmod 0640 "$ETC/edge.env"
|
|
[ -n "$DEVICE_SECRET" ] || echo " NOTE: DEVICE_SECRET is empty — set it in $ETC/edge.env before the server will accept events."
|
|
echo " -> wrote $ETC/edge.env"
|
|
fi
|
|
|
|
echo ">> [8/9] camera check (open every camera in CAMERAS and grab a frame)"
|
|
# Do this BEFORE enabling the unit. build_runtimes() logs-and-skips a camera it cannot open,
|
|
# so a wrong /dev/video index or an unseated ribbon would otherwise produce a green unit that
|
|
# silently monitors one camera less. Fail here, while someone is still at the Pi.
|
|
# SKIP_CAMERA_CHECK=1 for a headless/bench install with no cameras attached yet.
|
|
if [ "${SKIP_CAMERA_CHECK:-0}" = "1" ]; then
|
|
echo " -> skipped (SKIP_CAMERA_CHECK=1)"
|
|
else
|
|
# Probe exactly what the unit will run with: the CAMERAS line as written to edge.env, and
|
|
# the installed copy of the package ($PREFIX/src, not the repo checkout).
|
|
probe_cams="$(sed -n 's/^CAMERAS=//p' "$ETC/edge.env" | head -1)"
|
|
if ( cd "$PREFIX/src" && CAMERAS="$probe_cams" "$PREFIX/venv/bin/python" -m edge.probe \
|
|
--save "$STATE/probe" ); then
|
|
echo " -> every camera delivered a frame (previews in $STATE/probe)"
|
|
chown -R "$SVC_USER":"$SVC_USER" "$STATE/probe" 2>/dev/null || true
|
|
else
|
|
echo "" >&2
|
|
echo "ERROR: camera check failed — refusing to enable the service." >&2
|
|
echo " Fix CAMERAS in $ETC/edge.env and re-run, or install with SKIP_CAMERA_CHECK=1" >&2
|
|
echo " to enable the service anyway (it will monitor only the cameras that work)." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo ">> [9/9] systemd service"
|
|
install -m 0644 "$REPO_ROOT/deploy/systemd/move-detect-edge.service" /etc/systemd/system/
|
|
systemctl daemon-reload
|
|
|
|
diagnose() {
|
|
echo "" >&2
|
|
echo "ERROR: move-detect-edge did not come up. systemd's reason:" >&2
|
|
echo "--------------------------------------------------------------------" >&2
|
|
systemctl status --no-pager --full move-detect-edge >&2 || true
|
|
journalctl -xeu move-detect-edge -n 50 --no-pager >&2 || true
|
|
echo "--------------------------------------------------------------------" >&2
|
|
echo "Full log: sudo journalctl -xeu move-detect-edge" >&2
|
|
exit 1
|
|
}
|
|
|
|
# enable + restart rather than `enable --now`, so a re-run also picks up a rewritten
|
|
# edge.env instead of no-op'ing on an already-running unit.
|
|
systemctl enable move-detect-edge
|
|
systemctl restart move-detect-edge || diagnose
|
|
# Type=exec fails the job on exec-step errors, but a Python traceback *after* execve still
|
|
# leaves `restart` returning 0. RestartSec=5 means a crash-looping unit reads as
|
|
# "activating" here, which is-active correctly rejects. This is a crash detector, not a
|
|
# readiness gate: under Type=exec the unit is active the moment execve() succeeds, so a
|
|
# slow camera/Hailo init cannot false-fail it.
|
|
sleep 5
|
|
systemctl is-active --quiet move-detect-edge || diagnose
|
|
# is-active alone is phase-dependent: a crash at T<=~0.2s after execve is already back in
|
|
# "active" by the time we sample (T + RestartSec=5 <= 5.2), so a crash-looping daemon can
|
|
# read as healthy. NRestarts is phase-independent — for a unit we just started, any restart
|
|
# at all means it has already died once.
|
|
if [ "$(systemctl show -p NRestarts --value move-detect-edge 2>/dev/null || echo 0)" != "0" ]; then
|
|
echo "" >&2
|
|
echo "ERROR: move-detect-edge has already restarted at least once — it is crash-looping." >&2
|
|
diagnose
|
|
fi
|
|
|
|
echo ""
|
|
echo "Done. The edge daemon is installed and started."
|
|
echo " logs: journalctl -u move-detect-edge -f"
|
|
echo " config: sudo nano $ETC/edge.env (then: sudo systemctl restart move-detect-edge)"
|