131 lines
5.0 KiB
Markdown
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.
|