# 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 { "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` 0–100; `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): ```bash ./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: ```bash 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): ```bash 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.