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