# SanadV3 Navigation Thin HTTP **client** to the external `web_nav3` Nav2 stack. This module owns **no** ROS2/Nav2 code — it lets SanadV3 (dashboard + voice) drive autonomous navigation over plain HTTP. If `web_nav3` is down, nav features degrade gracefully and the rest of SanadV3 keeps running. ## What this module does - `web_nav3_client.py` — `WebNav3Client`, a loose-coupled `requests` client. **By contract no method ever raises into the caller**: each returns `{"ok": bool, "error": str|None, ...}` or a `NavStatus`. If `web_nav3` is unreachable, callers get a clean failure result instead of an exception. - `NavStatus` — health snapshot from `GET /api/status` (`bringup_alive`, `rosbridge_alive`, `reachable`, `log_tail`). ## Architecture ``` SanadV3 dashboard (:8000) ─┐ Navigation tab │ HTTP ┌── Nav2 ──┐ ├──────────────▶│ web_nav3 │──▶ cmd_vel_loco_bridge ──▶ LocoClient (G1 legs) SanadV3 voice (Gemini) ──┘ (:8765) └──────────┘ movement_dispatch.py rosbridge :9090 (live map / TF) ``` - SanadV3 plane = Python/asyncio, non-ROS. Dashboard on **:8000**. - `web_nav3` = standalone FastAPI on **:8765** wrapping ROS2 Nav2 + rosbridge on **:9090**. It owns SLAM, Nav2, and the `cmd_vel_loco_bridge` that drives the G1 legs via `LocoClient`. ## Configure Connection is resolved with precedence **env var → dashboard config → default**: - `WEB_NAV3_URL` (default `http://127.0.0.1:8765`) — the `web_nav3` FastAPI base. - `ROSBRIDGE_URL` (default `ws://127.0.0.1:9090`) — live map / TF stream. - `SANAD_ROBOT_NAME` (default `sanad`) — sent as the `X-Robot-Name` header. `config.py` exposes `WEB_NAV3_URL`. `main.py` builds the shared `nav_client` singleton; `dashboard/routes/navigation.py` builds its own module-level client (both use the same resolution). A broken nav package never blocks the dashboard. ## Dashboard Navigation tab Backend proxy lives under `/api/nav/*` (prefix applied in `dashboard/app.py`). The "Navigation" SPA tab lists saved **places** and **missions**, sends goto / cancel, saves the current pose, and embeds the live `web_nav3` map iframe from the robot at `:8765`. When the client is unavailable, status returns `{"available": false}` and action endpoints return 503. ## API endpoints (`/api/nav/*`) | Method | Path | Action | |--------|------------------|------------------------------------------| | GET | `/status` | health; `{available:false}` if degraded | | GET | `/config` | web_nav3 / rosbridge URLs + robot name | | GET | `/places` | list saved places | | POST | `/goto` | navigate to a saved place by `name` | | POST | `/cancel` | best-effort cancel (stops bringup) | | POST | `/save_here` | save current pose as a named place | | GET | `/maps` | list maps | | GET | `/missions` | list missions | | POST | `/missions/run` | run a saved mission by `id` | ## NEXT STEPS 1. **Voice bridge (not yet wired).** `voice/movement_dispatch.py` currently drives discrete `loco_controller` steps only. Add a path so destination phrases ("go to the lobby" / "اذهب إلى الردهة") map to `nav_client.goto()` instead of stepping. Keep it gated on the existing `recognition_state.movement_enabled` toggle. 2. **CRITICAL — LocoClient arbitration (prerequisite, do before #1).** `web_nav3`'s `cmd_vel_loco_bridge` and SanadV3's `loco_controller` **must never drive `LocoClient` simultaneously** — two velocity sources to the G1 legs at once is unsafe. Only ONE may hold the legs. Before enabling voice-driven autonomous nav, build a hand-off: when a Nav2 goto is active, `loco_controller` must release / be disarmed, and vice versa (fail-closed). No `goto()` voice wiring lands until this interlock exists. 3. **Single DDS participant ordering.** SanadV3 and `web_nav3` share one Unitree DDS domain on the G1. Initialize the DDS channel factory **exactly once**, before any consumer. Decide startup order (whoever owns `LocoClient` inits first) and ensure the other side never re-inits the participant.