86 lines
4.3 KiB
Markdown
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.
|