83 lines
3.3 KiB
Markdown
83 lines
3.3 KiB
Markdown
# 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` 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.
|