..
2026-07-10 11:24:55 +04:00
2026-07-10 11:24:55 +04:00
2026-07-16 14:14:48 +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
2026-07-10 11:24:55 +04:00
2026-07-10 11:24:55 +04:00
2026-07-16 14:14:48 +04:00

sanad_api_r1 — R1 fleet telemetry agent

Pushes the Unitree R1 EDU's live status to the YS Lootah fleet server. One robot type = one folder = its own Docker image (sibling of sanad_api_g1).

This build covers the "Main statuses" endpoint — the R1 map is skipped by request:

POST {SERVER_URL}/api/v1/fleet/ingest/telemetry        Authorization: Bearer <device_token>
{ "sn": "r1_0000", "mac": "…", "battery": 74, "charging": false,
  "status": "idle", "position": {"x": …, "y": …}|null, "faults": [] }

Sent every ~2 s. If DDS state can't be read it still sends a heartbeat (battery: null, status: "offline") so the robot shows as online.

Where each field comes from (R1 EDU, unitree_hg DDS)

field source
battery, charging rt/lf/bmsstate (BmsState_): soc 0100; charging = current > +0.05 A — same as SanadR1 arm_controller.get_battery
faults[] rt/lowstate (LowState_): low battery, motor over-temp, comms-stale
status derived (charging / moving / idle / offline); optional loco FSM read (ids 0/1/4/811, GET-only) with R1_READ_FSM=1
position R1 localizes with stereo VSLAM (ROS side). This agent is ROS-free, so position is null unless R1_POSITION_SOURCE=rosbridge (reads /odom)
mac primary NIC hardware address

Safety: never commands motion — only the read-only GET_FSM_ID RPC is ever issued to the R1.

Install (full Docker) — on the R1 backpack

One command — builds the DDS image, creates .env, starts the container (auto-restarts on boot):

./install.sh                 # creates .env if missing, then build + up -d
# (edit .env when prompted: SERVER_URL, DEVICE_TOKEN, SN, DDS_INTERFACE=eth10)
./install.sh --logs          # follow logs
./install.sh --simulate      # one synthetic post (server smoke test, no robot)
./install.sh --down          # stop + remove

Or the raw compose flow:

cp .env.example .env       # SERVER_URL, DEVICE_TOKEN, SN, DDS_INTERFACE=eth10
docker compose up -d --build
docker compose logs -f

Test without a robot (synthetic state, exercises the full upload path):

docker compose run --rm sanad-api-r1 --simulate --once   # one synthetic post
docker compose run --rm sanad-api-r1 --dry-run           # print telemetry, no POST
# or bare:
SERVER_URL=DEVICE_TOKEN=SN=r1_0000 python3 sanad_api_r1.py --simulate -v

Key env (full list in .env.example)

var meaning
SERVER_URL, DEVICE_TOKEN given by YS Lootah — required
SN fleet id, e.g. r1_0000
DDS_INTERFACE robot link — eth10 on the R1 backpack
R1_READ_FSM 1 = read loco FSM for status (GET-only)
R1_POSITION_SOURCE none (default) · rosbridge
POLL_INTERVAL seconds between posts (default 2)

Build note

Build on the R1 backpack (arm64). The image bundles the DDS stack (CycloneDDS + the vendored unitree_sdk2py wheel under vendor/), mirroring SanadR1's recipe. network_mode: host is required for DDS visibility. If unitree_sdk2py can't load, the agent logs a warning and sends heartbeats.

Porting

sanad_api_go2 is the same pattern; the Go2 uses unitree_go (not unitree_hg) DDS, so swap the message imports (BmsState_/LowState_ idl path) and topic names for the Go2 SDK.