fleet/README.md

95 lines
4.1 KiB
Markdown

# 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 <g1|r1|go2> <ip> [--sn NAME] [--server-ip IP] [--port N] [--token TOK]
./fleet_install.sh data <g1|r1|go2> <ip> # show what it's currently sending
./fleet_install.sh status <g1|r1|go2> <ip>
./fleet_install.sh logs <g1|r1|go2> <ip>
./fleet_install.sh test <g1|r1|go2> <ip> # end-to-end vs workstation server
./fleet_install.sh uninstall <g1|r1|go2> <ip> # 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-<type>.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@<ip> 'systemctl --user status sanad-api-r1'
ssh unitree@<ip> '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/<robot>/*.db` + `web/data/<robot>/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 <device_token>`.
## Notes / follow-ups
- **Point at the real fleet server:** re-run `install … --server-ip <fleet> --token <real> --sn <id>`.
- **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.