# Sanad Fleet Agents Per-robot agents that report to the YS Lootah fleet server, each shipped as **full Docker** with a user-level **systemd auto-start service**. Deploy, manage, and remove them over SSH with one interactive script. ``` Project/fleet/ fleet_install.sh # interactive + scriptable deploy/manage tool fleet_test_server.py # your workstation standing in as the fleet server (tests) agents/ g1/ sanad_api_g1.py — MAP uploader (POST …/{sn}/map) r1/ sanad_api_r1.py — TELEMETRY unitree_hg (POST …/telemetry) go2/ sanad_api_go2.py — TELEMETRY unitree_go (POST …/telemetry) [unverified on hw] ``` All three are self-contained (own `Dockerfile`, `.env.example`). Deploy uses plain `docker build`/`docker run` — **no docker-compose needed on the robot** (R1/Go2 don't have it). Source is pushed from this canonical workstation copy via `rsync`; images build natively on each arm64 robot; a **user systemd unit** (linger-enabled, **no sudo**) owns start/stop/auto-start-on-boot. ## Install / manage Interactive (asks robot type → IP → detects installed-or-not → right actions): ```bash ./fleet_install.sh ``` Scriptable: ```bash ./fleet_install.sh install [--sn NAME] [--server-ip IP] [--port N] [--token TOK] ./fleet_install.sh data # show what it's currently sending ./fleet_install.sh status ./fleet_install.sh logs ./fleet_install.sh test # end-to-end vs workstation server ./fleet_install.sh uninstall # service + container + image + dir ``` `install`/`uninstall` are detected automatically in interactive mode: if the agent is already on the robot it offers **data / status / logs / reinstall / uninstall**; otherwise it prompts for **robot name (SN)**, server IP, port, token and installs. ### Auto-start service Each install writes `~/.config/systemd/user/sanad-api-.service`, enables linger (`loginctl enable-linger`, no sudo), and `systemctl --user enable --now`s it. It starts on boot and restarts on crash. Manage with: ```bash ssh unitree@ 'systemctl --user status sanad-api-r1' ssh unitree@ 'systemctl --user restart sanad-api-r1' ``` ## Fleet (this deployment) | robot | agent | ip | ssh user | arch | DDS iface | |---|---|---|---|---|---| | G1 | map | `10.255.254.58` | `unitree` (key) | arm64 | — | | R1 | telemetry | `10.255.254.82` | `unitree` (key) | arm64 | `eth10` | | Go2 | telemetry | *(when available)* | `unitree` | arm64 | `eth0` | Workstation fleet server (for tests): **10.255.254.83** via `fleet_test_server.py`. ## What each agent sends, and when **G1 map** → `POST /api/v1/fleet/ingest/{sn}/map` — on change (scan ~30 s; size+mtime→sha256). Sends the RTAB-Map `.db` + places as `multipart` (file `db` + `meta` JSON `{sn,name,format:"rtabmap_db",size_bytes,sha256,points[…]}`) or `base64json`. Reads web_nav3 `maps//*.db` + `web/data//places/*.json`. **R1 / Go2 telemetry** → `POST /api/v1/fleet/ingest/telemetry` — every ~2 s; heartbeat (`battery:null,status:offline`) when DDS is silent so it stays online: ```json { "sn":"r1_82", "mac":"…", "battery":74, "charging":false, "status":"idle", "position":{"x":…,"y":…}|null, "faults":[], "ts":… } ``` - R1 (`unitree_hg`): battery `rt/lf/bmsstate`, faults/liveness `rt/lowstate`. - Go2 (`unitree_go`): battery from **`rt/lowstate.bms_state`** (nested), no separate BMS topic. - Both: `status` derived (charging/moving/idle/offline); position optional; read-only (never moves). Auth on every request: `Authorization: Bearer `. ## Notes / follow-ups - **Point at the real fleet server:** re-run `install … --server-ip --token --sn `. - **G1 real maps** live *inside* the `p4_Foxy_sanad` nav container (not host-mounted). Add a `maps/` bind-mount to Package_4's nav service, or share a volume, so the uploader sees real maps (it reports 0 until then; `test` seeds a fixture). - **Go2** agent is written from the `unitree_go` SDK layout but **not yet run on a Go2** — confirm the `bms_state` current sign (charging) and sportmodestate fields.