86 lines
4.3 KiB
Markdown

# 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.