# Pudu Map GUI — fleet map dashboard Convert maps scanned by the Pudu commercial robots (CC2 / BellaBot / …) into ROS2 Nav2 maps, then run the whole map lifecycle for the Unitree fleet from one browser dashboard: **convert → preview → edit → stage → deploy → install the robot's nav stack → RViz → send goals**. ``` cd Project/Other/pudu_map_gui ./start.sh # ← the ONLY command: starts it + opens the browser ``` ``` ./start.sh logs follow the server log ./start.sh stop stop it (same as the dashboard's ⏻ Shutdown button) ./start.sh restart stop + start ./start.sh build rebuild the image, then start ./start.sh fg run attached in this terminal (debugging only) ``` ## One bash script, fully docker `start.sh` is the single entry point (`run_docker.sh` still works — it forwards there). First run builds the `pudu-map-gui` image (ros:jazzy + rviz2 + converter deps + ssh/rsync + zenity/tk picker + roslibpy); every run after that starts in seconds. The host needs **docker and an X server only** — no ROS, no pip, no conda on the host. It runs **detached** and only reports success once the URL actually answers. That is deliberate: the dashboard is a service, and `docker run -it` can hang forever on a host whose docker attach stream is wedged (observed on this workstation — `create`/`start`/`stop` fine, every attached run hung). Use `./start.sh fg` only when you want the raw attached output. **⏻ Shutdown** (dashboard header) stops everything this GUI started on the workstation — RViz, the rosbridge relay, the local map publisher — and then the server itself, so the container exits. **Robots are never touched**: their nav stacks keep running. Start again with `./start.sh`. The container runs with `--net=host`, X11 passthrough, and the host paths mounted verbatim (`~/Robotics_workspace`, `~/Downloads`, `~/.ssh` read-only), so every absolute path behaves identically in and out of docker. A host-side fallback (`python3 pudu_gui.py`) exists but is never required. The robot sides are docker too: | robot | stack | runtime | localization for the shared map | |---|---|---|---| | **R1** | `r1-nav2` + `r1-vslam` containers (`Project/R1/nav`) | fully docker on the backpack | **stereo VSLAM** (RTAB-Map) + `map_align` static TF onto the Pudu frame | | **G1** | `sanad_nav3` → `~/nav_g1` (`Project/G1/Nav2_Projects/sanad_nav3`) | ⚠ native on the Jetson (see below) | **MID-360 LiDAR** SLAM / `STATIC_MAP=1` map_server + AMCL | | **Go2** | `go2-nav2` container (`Project/GO2/nav/nav2`) | fully docker on the EDU computer | **L1 LiDAR** synthetic `/scan` → slam_toolbox (mapping) / AMCL (saved map) | All three are **Nav2 Foxy 0.4.7**. The workstation never joins their DDS — every live view goes through the robot's rosbridge websocket into local RViz (Go2/R1 on `:9090`, the G1's own stack on `:9091` — `:9090` on that Jetson belongs to an unrelated pre-existing service and is never used). **G1 exception, stated honestly:** the standalone G1 stack interoperates with the Jetson's existing host processes (livox SDK bridge, LIO workspace, shared ZMQ ports) and the robot is under a standing deploy hold — so its *install is a file sync onto the Jetson's existing ROS runtime*, not a container. ## Program flow — inside each robot One naming scheme everywhere — the nav stack installs to **`~/nav_`** on every robot: | robot | install folder | ROS 2 | SLAM | runs as | |---|---|---|---|---| | **Go2** | `/home/unitree/nav_go2` | **Foxy** (docker) | **slam_toolbox** on the L1 LiDAR (synthetic `/scan`); **AMCL** for saved/Pudu maps | `go2-nav2` container | | **G1** | `/home/unitree/nav_g1` | **Foxy** on the Jetson | **RTAB-Map** on the MID-360 LiDAR; `STATIC_MAP=1` = map_server + AMCL on the Pudu map | host scripts (own rosbridge on `:9091`) | | **R1** | `/home/unitree/nav_r1` + `/home/unitree/nav_r1_vslam` | **Foxy** (docker) | **RTAB-Map stereo VSLAM** (no LiDAR) + `map_align` static TF onto the Pudu frame | `r1-nav2` + `r1-vslam` containers (both halves installed together) | RTAB-Map is the SLAM library on the G1 (LiDAR mode) and the R1 (stereo visual mode) — it builds the 2D occupancy grids Nav2 consumes. The Go2 uses slam_toolbox instead (its L1 data arrives as a laser scan, where slam_toolbox is the lighter, proven fit on Foxy). **Go2 — what runs inside `nav_go2`:** ``` L1 LiDAR ─ rt/utlidar/cloud + robot_odom ─► SDK reader (DDS domain 0) └─ ZMQ ─► /scan + /odom + TF (domain 42, in-container) /scan ─► slam_toolbox (slam mode) OR map_server + AMCL (nav mode, your map) ─► Nav2 Foxy ─► /cmd_vel ─ ZMQ ─► SportClient consumer ─► robot walks rosbridge :9090 ─► dashboard / workstation RViz folder: run.sh build.sh Dockerfile entrypoint.sh bridge/ params/ launch/ maps/ sdk/ ``` **G1 — what runs from `nav_g1` (standalone flavor):** ``` MID-360 ─► livox bridge ─► pointcloud→laserscan ─► /scan /scan + leg odom TF ─► RTAB-Map (SLAM) OR map_server + AMCL (STATIC_MAP=1) ─► Nav2 ─► /cmd_vel ─► loco bridge (ZMQ) ─► gait folder: scripts/ (bringup.sh …) launch/ config/ maps/ web/ rviz/ ``` **R1 — what runs inside `nav_r1` (+ the vslam containers):** ``` stereo head cams ─► rectify ─► RTAB-Map VSLAM (r1-vslam-map) ─► odom + grid grid (+ Pudu map via map_align TF in --pudu mode) ─► Nav2 (r1-nav2) ─► /cmd_vel ─► IPC bridge ─► LocoClient — moves ONLY after YOU arm FSM 811 folder: run.sh build.sh bridge/ params/ launch/ maps/ sdk/ ``` Robots installed under the old folder names (`go2_nav2_docker`, `r1_nav2_docker`, `marcus_nav2_test`) are detected as **legacy** — press Install once to migrate; Uninstall cleans both old and new names. ## Dashboard features 1. **Convert** — path, `.zip`, or native file/folder picker. Emits `map.pgm/.yaml`, `annotations.yaml` (homes/chargers as goal poses), `keepout_mask.*`, and **always** `map_keepout_baked.*` (Foxy has no KeepoutFilter — keepouts are baked as lethal; the robot stacks serve that exact filename). 2. **Map library** — every converted AND every edited map is saved **in the project**: `Project/Other/pudu_map_gui/maps//`, one folder per set (a 📁 chip shows the exact location after each convert). Editing a set that lives elsewhere (a stage dir, `~/Downloads`, a robot save) relocates it into the library first, so staged/deployed copies are never mutated in place. Reconverting an export overwrites its set unless it was hand-edited — then the edit is kept as a `.bak_` folder. The list also scans the robot stage dirs and `~/Downloads`; each row has **Load** and **🗑 delete** (removes only that set's files, never a maps folder itself, and refuses anything outside the known map roots). 3. **Preview** — pan/zoom occupancy + keepout + route graph + poses with world-meter readout; 🎯 goal mode click-drags a heading and publishes `/goal_pose` via rosbridge. 4. **Edit** — the full Office Map Editor (served un-forked from the R1 tree) with auto-load of the current map, *Save to Pudu GUI* (rebuilds the baked pair), and a Ctrl+C transform panel (move / rotate the whole map). 5. **Stage** — copies into the robot's workstation-canonical maps dir (old set backed up, stale files cleared). 6. **Deploy** — scp the map set to the robot over the IP you pick (key auth, BatchMode; docker-proof ssh with explicit identities). 7. **Nav stack install / uninstall / scan** (step 4): - **🔍 Check** — one ssh round-trip: files present? docker image present? stack running right now? (R1 also reports whether vslam is up.) - **⬇ Install** — runs that robot's one-shot installer (`install.sh `): rsync code+maps+SDK, then **docker build on the robot**. The R1 install ships BOTH halves — `nav_r1` (r1-nav2 image) *and* `nav_r1_vslam` (r1-vslam image, RTAB-Map stereo VSLAM). The Go2 installer ends with a ~35 s sensors-only smoke run (starts the stack briefly, no motion, then removes it); the R1/G1 installers start nothing. Output streams live into the Logs tab. Refuses — unconditionally — while the stack (or the R1's vslam) is running. - **🗑 Uninstall** — removes what install ships: the docker image(s), the stack's runtime containers (Go2: `go2-nav2` + cli helpers; R1: `r1-nav2`, `r1-imu`, `r1-vslam-map`, `r1-rosbridge`, `r1-cam`), and the install dir(s), legacy names included. Exact-name matching only — nothing else on the robot is ever touched. Refuses while anything is running. 8. **RViz** (three views): *preview converted map* (no robot), *live robot* (robot's own `/map` via relay), *loaded map + live robot* (your chosen map locally + live TF/odom; 2D Pose Estimate + 2D Goal Pose forwarded to the robot). 9. **📡 Lidar tab** — the connected robot's live sensor view, to confirm localization. G1/Go2: the live `/scan` streamed straight from the robot's rosbridge into the browser (plain WebSocket, no external library) and drawn **on the loaded map** at the robot's AMCL/odom pose — if the red points sit on the walls, the robot is localized correctly; without a pose it falls back to a robot-centric polar view with range rings. **R1**: it has no LiDAR, so the tab shows its camera instead (MJPEG from `http://:8088/stream`). 10. **Logs** — every action in the Logs tab (copy all / export / pop-out), plus toasts for every success and failure. ## Typical loops **Go2 (verified live 2026-07-16):** ``` Install (dashboard) → run.sh slam → walk the robot → run.sh save-map office → run.sh use-map office → run.sh nav → RViz 2D Pose Estimate → goals ``` **R1 (Pudu map):** Install (dashboard) → deploy the map → `start_r1_nav.sh --nav --pudu` from the workstation → fit `map_align.yaml` once (`R1/nav/tools/fit_map_alignment.py`) → goals. The robot moves only after the user arms FSM 811. **G1 (held):** stage + deploy from the dashboard when the hold lifts → `STATIC_MAP=1 ~/nav_g1/scripts/bringup.sh`. ## Requirements & safety - Workstation: docker + X11. Robot: docker + internet (first build only). - ssh **key auth** must exist once per robot: `ssh-copy-id unitree@` (the GUI never types passwords). - The GUI **never arms or moves a robot**: install/uninstall touch software only (the Go2 install's smoke run brings up sensors briefly, never motion), and both refuse while a stack is live, with no override. Goals: the **R1** walks only after the user arms FSM 811; the **Go2** boots in sport mode, so a goal makes it walk as soon as its nav stack runs (velocity-capped, with a deadman) — treat every goal click as a movement command. - G1 standing hold: the dashboard warns before any G1 deploy/install. ## Files ``` run_docker.sh ← the one entry script (build-on-first-run + run) Dockerfile image: ros:jazzy + rviz2 + deps (self-contained runtime) pudu_gui.py server: convert/stage/deploy/install/rviz/goal endpoints index.html the dashboard local_map_pub.py local /map publisher for the RViz previews maps/ ← the map library: one folder per converted/edited set ``` Robot installers the dashboard drives: `Project/GO2/nav/nav2/docker/install.sh`, `Project/R1/nav/nav2/docker/install.sh`, `Project/G1/Nav2_Projects/sanad_nav3/install.sh`. ## Troubleshooting - **Install button says "stack is RUNNING"** — that's the guard: stop the robot's session first, then install. Go2: `~/nav_go2/run.sh estop` (robot sits) then `docker rm -f go2-nav2` on the robot. R1: on the robot `docker exec r1-nav2 /entrypoint.sh estop; docker rm -f r1-nav2` (there is no `--stop` flag on start_r1_nav.sh — running it bare STARTS the stack). - **"no key auth yet"** — run `ssh-copy-id unitree@` in a terminal once. - **Combo RViz poses don't line up** — the robot must actually *run* the same map in nav mode (`use-map` + `run.sh nav`), then give a 2D Pose Estimate. - **G1/R1 share 192.168.123.164** — whichever robot is on the wire answers; check the status line says the stack you expect.