fleet/agents/g1/README.md

131 lines
5.0 KiB
Markdown

# sanad_api_g1 — G1 fleet **map** uploader
A tiny, self-contained agent that pushes the G1's navigation **map** to the
YS Lootah fleet server. One robot type = one folder = its own Docker image, so
it drops onto any new G1 unchanged. (Sibling folders `sanad_api_r1`,
`sanad_api_go2` will do the same for those robots.)
This build covers **only the map** — the "Maps sync" row of the fleet spec:
```
POST {SERVER_URL}/api/v1/fleet/ingest/{sn}/map Authorization: Bearer <device_token>
```
Telemetry (battery/status/position/faults), commands, alerts and logs are
separate agents and are **not** in this script.
---
## What it uploads — and the one thing the server must do
The G1's map is built by the **web_nav3** (Nav2 + RTAB-Map) stack and stored on
disk as a **RTAB-Map SQLite `.db`** (`maps/<robot>/<name>.db`), with each map's
named places in `web/data/<robot>/places/<name>.json`. **There is no rendered
PNG on disk** — the dashboard draws the occupancy grid live over rosbridge.
By design this agent uploads the **raw `.db`** (the real map artifact) plus its
places, instead of rendering an image. So:
> ⚠️ **The fleet server's map endpoint must accept a `rtabmap_db` artifact.**
> The spec's documented body (`image_base64` / `resolution` / `origin`) is for a
> *rendered* map. If you need that instead, render the live `/map` OccupancyGrid
> over rosbridge — that's the telemetry-agent path, not this one.
Two wire formats are supported (`MAP_UPLOAD_MODE`), pick what your server takes:
| mode | request |
|---|---|
| `multipart` (default) | `multipart/form-data`: file field **`db`** (the `.db`) + form field **`meta`** (JSON) |
| `base64json` | JSON body: all meta fields + the `.db` as **`db_base64`** |
`meta` / JSON body shape:
```json
{ "sn": "g1_7892", "name": "floor-1", "file": "floor-1.db", "format": "rtabmap_db",
"size_bytes": 6994944, "sha256": "…", "mtime": 1731000000, "description": "ground floor",
"points": [ { "name": "dock", "type": "waypoint", "x": 1.2, "y": 3.4, "yaw": 0.0 } ] }
```
Each map is fingerprinted (size+mtime, then sha256); a map is (re)uploaded only
**on change**. State lives in `STATE_DIR/uploaded.json` so restarts don't re-push.
**No ROS, no DDS** — just file reads + outbound HTTPS. web_nav3's HTTP API is
used only (optionally) to learn which map is *active* (`MAP_SELECT=active`).
---
## Install (full Docker)
One command — builds the 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, MAPS_HOST_DIR, DATA_HOST_DIR)
./install.sh --logs # follow logs
./install.sh --status # container state + list maps it sees
./install.sh --down # stop + remove
```
Or the raw compose flow:
```bash
cp .env.example .env # set SERVER_URL + DEVICE_TOKEN, and the map paths
docker compose up -d --build
docker compose logs -f
```
Handy one-shots (no loop):
```bash
docker compose run --rm sanad-api-g1 --list # what maps do I see?
docker compose run --rm sanad-api-g1 --dry-run # build payloads, never POST
docker compose run --rm sanad-api-g1 --once # one upload pass, then exit
```
Runs bare too (for a quick check on the workstation):
```bash
pip install -r requirements.txt
SERVER_URL=DEVICE_TOKEN=SN=g1_7892 ROBOT=sanad \
MAPS_DIR=…/web_nav3/maps DATA_DIR=…/web_nav3/web/data \
python3 sanad_api_g1.py --list
```
### Key env (full list in `.env.example`)
| var | meaning |
|---|---|
| `SERVER_URL`, `DEVICE_TOKEN` | given by YS Lootah — **required** |
| `SN` | fleet id / URL key — default `g1_7892` |
| `ROBOT` | web_nav3 robot name = maps subdir + `X-Robot-Name` — default `sanad` |
| `MAPS_HOST_DIR` / `DATA_HOST_DIR` | host paths bind-mounted read-only (see below) |
| `MAP_SELECT` | `all` (default) · `active` · `newest` |
| `MAP_UPLOAD_MODE` | `multipart` (default) · `base64json` |
| `POLL_INTERVAL` | scan cadence, seconds (default 30) |
---
## ⚠️ Mounting the maps directory
The uploader needs **read access to wherever web_nav3 actually keeps the `.db`
files.** That location is deployment-specific:
- **Workstation dev copy:** `Project/G1/Nav2_Projects/web_nav3/{maps,web/data}`
- **Package_4 robot:** the `sanad-nav` container stores named maps *inside itself*
at `/home/unitree/marcus_nav2_test/maps` — that path is **not** bind-mounted to
the host by default. To let this uploader read them, either add a host
bind-mount for `maps/` to the `sanad-nav` service, or put both containers on a
**shared named volume** for the maps dir, then point `MAPS_HOST_DIR` at it.
Set `MAPS_HOST_DIR` / `DATA_HOST_DIR` in `.env` accordingly.
---
## Porting to R1 / Go2
Copy this folder to `sanad_api_r1` / `sanad_api_go2`, rename the script/service,
and change `SN` (`r1_…`, `go2_…`). If those robots store their map the same way
(web_nav3 `.db` + places) nothing else changes. If a robot has no web_nav3 `.db`
(e.g. a different SLAM), adjust `discover_maps()` / `load_points()` for that
robot's on-disk map format.