4.1 KiB
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):
./fleet_install.sh
Scriptable:
./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 --nows
it. It starts on boot and restarts on crash. Manage with:
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:
{ "sn":"r1_82", "mac":"…", "battery":74, "charging":false,
"status":"idle", "position":{"x":…,"y":…}|null, "faults":[], "ts":… }
- R1 (
unitree_hg): batteryrt/lf/bmsstate, faults/livenessrt/lowstate. - Go2 (
unitree_go): battery fromrt/lowstate.bms_state(nested), no separate BMS topic. - Both:
statusderived (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_sanadnav container (not host-mounted). Add amaps/bind-mount to Package_4's nav service, or share a volume, so the uploader sees real maps (it reports 0 until then;testseeds a fixture). - Go2 agent is written from the
unitree_goSDK layout but not yet run on a Go2 — confirm thebms_statecurrent sign (charging) and sportmodestate fields.