5.0 KiB
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_dbartifact. The spec's documented body (image_base64/resolution/origin) is for a rendered map. If you need that instead, render the live/mapOccupancyGrid 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:
{ "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):
./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:
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):
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):
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-navcontainer 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 formaps/to thesanad-navservice, or put both containers on a shared named volume for the maps dir, then pointMAPS_HOST_DIRat 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.