# Edge deployment (Raspberry Pi 5, air-gapped / 폐쇄망) Offline-installable bundle + systemd unit for the `move-detect` edge daemon. The bundle is fully self-contained (all wheels + pose model + source): the Pi performs **zero network fetches** at install or run time. The only runtime network use is the server POST / config-poll / heartbeat, and even that tolerates being offline (events buffer in the local sqlite queue and flush on reconnect). > Scope: the delivered bundle is **Hailo-8 + `yolov8s_pose.hef`**, which > `build_offline_bundle.sh` fetches (sha256-pinned) and stages into `models/`. > The CPU MoveNet fallback is **not** bundled: its upstream ONNX URL 404s, so > `install_offline.sh` configures the unit for whatever backend is actually > present and refuses to install if none is. Camera side: USB (UVC) and CSI are > both wired; CSI needs `hailo-all`/`picamera2` from the Pi OS image. > Device validation for both is `docs/hardware-bringup.md` (still OFF-HW). ## Files | File | Runs on | Purpose | |------|---------|---------| | `bundle/build_offline_bundle.sh` | build machine (has internet) | downloads wheels + model, stages source, emits one tarball | | `bundle/install_offline.sh` | the Pi (no internet) | venv + `--no-index` install, systemd unit, service account | | `systemd/move-detect-edge.service` | the Pi | the daemon unit (auto-restart, device access, hardening) | | `systemd/move-detect-edge.env.example` | the Pi | env template → `/etc/move-detect/edge.env` | ## Quick start — Pi with internet (one command) If the Pi itself has internet, skip the offline bundle entirely. Copy the built delivery folder onto the Pi — **copy the working tree, not a fresh `git clone`**: the pose model `models/yolov8s_pose.hef` is git-ignored (a build artifact), so a clone arrives without it. Use `scp -r`/USB from a build machine that has already staged `models/` (see step 1). Then: ```bash sudo deploy/setup_online.sh ``` It installs the system packages (`python3-picamera2`, and `hailo-all` if present), builds the venv, pip-installs the runtime deps, **places the pose model from the delivery's `models/`** (it does NOT download models on the Pi — the HEF/ONNX are fetched on the build machine per the air-gap rule, `build pulls, target doesn't`), writes `/etc/move-detect/edge.env` (prompting for `DEVICE_ID` / `DEVICE_SECRET` / `CAFE24_URL` / `CAMERAS`, or take them from env vars), probes every camera, installs the systemd unit, and starts the service. It uses Hailo when `models/yolov8s_pose.hef` is present (or pass `HAILO_HEF=/path/to.hef`), else CPU MoveNet when `models/movenet_lightning.onnx` + onnxruntime are present. The rest of this doc is the **air-gapped** path. ### Secret handling (`deploy/device.env`) If you pre-provision with `deploy/device.env`, that file holds this device's **HMAC secret in plaintext**. It is gitignored (never committed), but it rides onto the Pi with the "copy the working tree" transfer. After a successful install: 1. **Delete it:** `shred -u deploy/device.env` (the live secret now lives only in `/etc/move-detect/edge.env`, `0640 root:movedetect`). The installer already `chmod 600`s it while running, but that is defense-in-depth, not a substitute for removing it. 2. **Rotate if it was ever exposed** (shared over chat, a shell history, a shared drive): update `move_devices.secret` on the server and the `DEVICE_SECRET` in the Pi's `/etc/move-detect/edge.env`, then restart the service. A leaked secret lets anyone forge signed events/heartbeats for this device. Prefer `device.env` over passing `DEVICE_SECRET=...` on the `sudo` command line, which leaves the secret in the shell history. --- ## 1. Build the bundle (on a machine WITH internet) **Most reliable: build on an aarch64 host** (a Pi 5, or an arm64 CI runner) so the downloaded wheels match the target exactly: ```bash deploy/bundle/build_offline_bundle.sh # native aarch64 wheels ``` From an x86 box you can attempt a cross build (best-effort — onnxruntime/opencv wheel tags may not resolve; if it fails, build natively): ```bash deploy/bundle/build_offline_bundle.sh --cross # target aarch64 / cp311 ``` Output: the folder `dist/2_pi_install/` (+ `MANIFEST.txt` listing the bundled wheels). Copy the WHOLE FOLDER across the air gap as-is — no archive step. Set `MAKE_TARBALL=1` if you would rather transfer one file. The tarball is named `dist/move-detect-pi---aarch64-cp311-hailo8.tar.gz` — version and build date so a site can say exactly what it is running, and the arch/ABI/Hailo triple because a hailo8 and a hailo8l bundle are byte-different and crash-loop on the wrong board. ## 2. Transfer + install (on the air-gapped Pi) Copy `dist/2_pi_install/` across the gap (USB, one-way transfer, etc.), then: ```bash # tarball variant only (MAKE_TARBALL=1): tar xzf move-detect-pi--<날짜>-aarch64-cp311-hailo8.tar.gz cd 2_pi_install sudo ./install_offline.sh ``` The installer: creates the `movedetect` service account (in `video`/`render`/ `plugdev`/`hailo` groups), lays down source + model under `/opt/move-detect`, builds a venv and installs the bundled wheels with `--no-index`, verifies the package imports, seeds `/etc/move-detect/edge.env`, and installs the systemd unit. ## 3. Configure + start ```bash sudo nano /etc/move-detect/edge.env # DEVICE_ID, DEVICE_SECRET, CAFE24_URL, ... sudo systemctl enable --now move-detect-edge journalctl -u move-detect-edge -f ``` `DEVICE_ID`/`DEVICE_SECRET` must match a `move_devices` row in the server (see `server/docs/server-deploy.md` §6). `HAILO_HEF` points at the bundled model (`/opt/move-detect/models/yolov8s_pose.hef`); the installer sets it for you and only falls back to `MOVENET_ONNX` if a MoveNet bundle was built. ## 4. Verify the air gap (do this before trusting a build) - [ ] On a host with **networking disabled**, `install_offline.sh` completes — the `--no-index` installs prove nothing is fetched. If pip tries to reach PyPI, a wheel is missing from the bundle: rebuild (step 1) natively. - [ ] Step [4/6] of the installer prints `edge import OK` (cv2 + numpy + requests import from the venv; onnxruntime only if a MoveNet bundle was built). - [ ] `systemctl status move-detect-edge` is `active (running)`; the log shows frames processed and (if the server is reachable) heartbeats. - [ ] Pull the network cable → events keep queueing (`QUEUE_DB` grows) → restore → the queue flushes. (End-to-end behavior verified in the edge e2e harness.) ## Hailo-8 HAT + CSI camera path (optional, faster) The default bundle runs CPU MoveNet + USB webcam. To use the Hailo-8 accelerator and/or a CSI camera, add these SYSTEM packages on the Pi (they are apt packages, not pip wheels — the venv sees them because it is created with `--system-site-packages`): ```bash sudo apt install -y hailo-all python3-picamera2 python3-opencv ``` - `hailo-all` — HailoRT runtime + driver + the Python `hailo_platform` bindings. - `python3-picamera2` — CSI camera stack (libcamera). Then place the pose model and switch backends in `/etc/move-detect/edge.env`: ```ini POSE_BACKEND=hailo HAILO_HEF=/opt/move-detect/models/yolov8s_pose.hef # copy the compiled HEF here CAMERA_BACKEND=csi CSI_DEVICE=0 ``` Validate on device per `docs/hardware-bringup.md` before trusting output (the Hailo decode is unit-locked, but the HailoRT inference + picamera2 color order are hardware- validated). USB webcam stays available anytime via `CAMERA_BACKEND=uvc`. **Multiple cameras at once.** Set `CAMERAS=backend:device:camera_id,...` to run several cameras in one daemon — each gets its own detection pipeline and `camera_id`, they share the Hailo device (round-robin) and the offline queue, and server zones are routed to the matching camera. Example — CSI + USB together: ```ini CAMERAS=csi:0:cam-01,uvc:0:cam-02 ``` Each `camera_id` must match the `camera_id` on that camera's zones in the server. > **Air gap:** `hailo-all` / `python3-picamera2` are the ONE piece this bundle can't > carry (Hailo's own driver + apt tree). Pre-install them on the Pi image, or transfer > their `.deb`s separately, before running `install_offline.sh`. The pose HEF is > likewise supplied out-of-band (compiled from the Hailo Model Zoo). ## Not bundled (by design) - **MySQL / the server app** — a separate host; see `server/docs/server-deploy.md`. - **HailoRT + picamera2** — Hailo's runtime/driver and the CSI camera stack are Pi system packages, installed out-of-band (above). Note the HEF itself **is** bundled (`models/yolov8s_pose.hef`); only the driver is not. - **`python3-venv`** — `install_offline.sh` runs `python3 -m venv`, which Pi OS Lite does not always include. Install it with the Hailo packages, before going air-gapped. - **MoveNet ONNX (CPU fallback)** — its upstream URL 404s, so the bundle is Hailo-only. If you need the CPU path, supply the ONNX to the build machine and rebuild.