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-coupledrequestsclient. By contract no method ever raises into the caller: each returns{"ok": bool, "error": str|None, ...}or aNavStatus. Ifweb_nav3is unreachable, callers get a clean failure result instead of an exception.NavStatus— health snapshot fromGET /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 thecmd_vel_loco_bridgethat drives the G1 legs viaLocoClient.
Configure
Connection is resolved with precedence env var → dashboard config → default:
WEB_NAV3_URL(defaulthttp://127.0.0.1:8765) — theweb_nav3FastAPI base.ROSBRIDGE_URL(defaultws://127.0.0.1:9090) — live map / TF stream.SANAD_ROBOT_NAME(defaultsanad) — sent as theX-Robot-Nameheader.
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
-
Voice bridge (not yet wired).
voice/movement_dispatch.pycurrently drives discreteloco_controllersteps only. Add a path so destination phrases ("go to the lobby" / "اذهب إلى الردهة") map tonav_client.goto()instead of stepping. Keep it gated on the existingrecognition_state.movement_enabledtoggle. -
CRITICAL — LocoClient arbitration (prerequisite, do before #1).
web_nav3'scmd_vel_loco_bridgeand SanadV3'sloco_controllermust never driveLocoClientsimultaneously — 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_controllermust release / be disarmed, and vice versa (fail-closed). Nogoto()voice wiring lands until this interlock exists. -
Single DDS participant ordering. SanadV3 and
web_nav3share one Unitree DDS domain on the G1. Initialize the DDS channel factory exactly once, before any consumer. Decide startup order (whoever ownsLocoClientinits first) and ensure the other side never re-inits the participant.