2026-07-10 11:24:55 +04:00
2026-07-10 11:24:55 +04:00
2026-07-10 11:24:55 +04:00
2026-07-10 11:24:55 +04:00

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 runno 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 mapPOST /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 telemetryPOST /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): 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.
Description
No description provided
Readme
Languages
Python 90.8%
Shell 6.9%
Dockerfile 2.3%