# 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 ``` 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//.db`), with each map's named places in `web/data//places/.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.