..
2026-07-13 10:28:20 +04:00
2026-07-10 11:24:55 +04:00
2026-07-16 14:14:48 +04:00
2026-07-10 11:24:55 +04:00
2026-07-10 11:24:55 +04:00
2026-07-13 10:28:20 +04:00
2026-07-10 11:24:55 +04:00
2026-07-13 10:28:20 +04:00
2026-07-16 14:14:48 +04:00

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:

{ "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-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.