docs: rewrite README with architecture, USB 2 quirks, dev workflow

This commit is contained in:
robot committed 2026-05-18 11:03:42 +02:00
1 parent 07d95792e3
commit 739afd465b
1 file changed
+287 -128
+287 -128
View File
@@ -1,168 +1,327 @@
# Intel RealSense D435 — API Server # RealSense D435 — API Server
Server FastAPI per la depth camera Intel RealSense D4xx (testato su D435) Container Docker che espone una Intel RealSense D4xx (testata su D435) via
con accesso remoto via REST/WebSocket. Esecuzione tramite Docker. Mantiene REST + WebSocket, con la **stessa shape API del Nuwa-HP60C**: envelope
la stessa shape API del servizio Nuwa HP60C (envelope `ApiResponse`, `ApiResponse`, endpoint `/api/...`, header `X-Api-Key`, WebSocket `/ws`.
endpoint `/api/...`, header `X-Api-Key`, WebSocket `/ws`) così che i client Pensato come drop-in replacement del container Nuwa nella pipeline del
e il frontend React esistenti funzionano senza modifiche. pestello, così i client esistenti (`pestello-pose-detector`, frontend
React, ecc.) consumano frame senza modifiche.
## Caratteristiche Gira sul robot **Unitree H1** (Jetson Xavier NX, JetPack 5.1.x). Il repo
è su [git.humion.it](https://git.humion.it/Labotics-OnGianni/RealSense-D435-H1).
- **RGB**: configurabile (default 640x480) BGR8, allineato al depth ---
- **Depth**: 640x480 uint16 in millimetri (allineato al color via `rs.align`)
- **Intrinsics**: parametri intrinseci color + depth e estrinseci depth→color
- **Discovery**: enumerazione devices via `pyrealsense2`
- **API Server**: REST + WebSocket con envelope `ApiResponse` standard
- **Sicurezza**: API Key opzionale (`X-Api-Key`) su tutti gli endpoint REST
- **NATS**: messaging asincrono opzionale (profilo `nats`)
- **Frontend React**: viewer RGB, depth e 3D point cloud (riusato dal Nuwa)
- **Swagger**: documentazione interattiva API a `/apidocs`
## Hardware ## Architettura
| Parametro | Valore |
|-----------|--------|
| Camera | Intel RealSense D435 |
| Serial number (questo robot) | `933322070696` |
| Firmware | `05.16.00.01` |
| Range depth | 0.1m – 10m |
| Interfaccia | USB 3.0 |
| SDK | librealsense 2.55.1 + pyrealsense2 (built from source) |
| Host | Jetson Xavier NX, Ubuntu 20.04 (JetPack 5.1.x) |
## Configurazione
Crea `.env` (vedi `.env.example`):
```env
API_PORT=8878
API_KEY=cambia-questa-chiave-segreta
CAMERA_WIDTH=640
CAMERA_HEIGHT=480
CAMERA_FPS=30
CAMERA_AUTO_OPEN=1 # apre la camera al boot, niente /api/open manuale
CORS_ORIGINS=* # LAN-only OK
```
## Accesso dalla LAN
Il container gira sul robot e binda la porta `8878` su tutte le interfacce
(`0.0.0.0`). Dal vostro PC sulla stessa LAN raggiungete il servizio via:
``` ```
http://<ip-robot>:8878/api/health # health check ┌────────────────────┐ RGB JPEG (HTTP) ┌────────────────────────────┐
http://<ip-robot>:8878/apidocs # Swagger UI │ yolo-server │ ◄──────────────────── │ pestello-pose-detector │
http://<ip-robot>:8878/gui # frontend React │ (su harry) │ ──► bbox + conf │ (su harry) │
ws://<ip-robot>:8878/ws # stream WebSocket └────────────────────┘ │ │
│ │ │
│ │ /api/rgb │
│ │ /api/depth │
│ │ /api/intrinsics │
│ ▼ │
┌───────────────────┴────┐ │
│ RealSense-D435-H1 ⬅️ │ ← QUESTO REPO │
│ (sul robot, port 8878)│ │
└────────────────────────┘ │
(era: Nuwa-HP60C) │
│
``` ```
Il robot ha Avahi attivo, quindi `ubuntu.local:8878` funziona se mDNS Il container fa **solo** acquisizione e serve frame — niente inferenza,
risolve sul vostro PC (`avahi-resolve -n ubuntu.local`). Altrimenti niente pose estimation. Quella logica vive in container separati su harry.
usate l'IP diretto (`ip a` sul robot per scoprirlo).
> Lo streaming **non richiede internet** — solo la build (~25 min al primo ---
> avvio) deve raggiungere registry Docker/apt/PyPI/npm via il tinyproxy
> locale del robot.
## Build & avvio ## Hardware constraint importante: D435 su USB 2.0
Sul robot H1 la D435 è collegata a una porta USB-C frontale che, anche se
fisicamente è USB-C, è **cablata USB 2.0** lato Jetson (verificato: anche
un adapter ax88179 sullo stesso bus gira a 480 Mbps invece dei suoi 5
Gbps nativi). Il controller USB 3 del Jetson esiste ma non è esposto
esternamente sulla scocca.
Per cui il driver, all'apertura del device, controlla
`usb_type_descriptor` e si adatta:
| | **USB 2.x** (situazione attuale) | **USB 3.x** (se mai) |
|---|---|---|
| Color format | YUYV (l'unico che il D4xx sostiene su USB 2) | BGR8 |
| Color resolution | 640x480 @ 15 fps (≈4 fps reali) | 640x480 @ 30 fps |
| Depth resolution | 640x480 @ 15 fps | 640x480 @ 30 fps |
| Alignment depth↔color | Approximate (resize numpy) | Hardware (rs.align) |
| Conversione YUYV→BGR | sì, via `cv2.cvtColor` | non serve |
I dettagli sui workaround applicati sono in [STATUS.md](STATUS.md).
---
## Quick start
### Sul robot (deploy)
```bash ```bash
# Backend (porta 8878) cd ~/Robot_2026/RealSense-D435
docker compose up --build -d
# Backend + NATS # Build + run (la prima volta build di librealsense da source: ~25 min)
docker compose --profile nats up --build -d sg docker -c "docker compose up -d"
# Stato
curl http://localhost:8878/api/health
# → {"success":true,"data":{"status":"ok","sdk_initialized":true,"camera_open":true,...}}
# Stop
sg docker -c "docker compose down"
``` ```
> Il primo build compila i bindings Python di `pyrealsense2` da source — Il container ha `restart: unless-stopped`, quindi resta su dopo reboot
> ~20–30 min su Jetson. I build successivi sono cached. del robot (purché `docker.service` parta — `systemctl enable docker` se
non lo è già).
Il container: ### Dal tuo PC (consumare lo stream)
- non fa unbind UVC (a differenza del Nuwa) — RealSense usa direttamente uvcvideo
- monta `/dev/bus/usb` in privileged mode per accesso diretto USB Dall'IP LAN del robot (`192.168.123.99`) o via mDNS (`ubuntu.local`):
- riparte automaticamente (`restart: unless-stopped`)
```bash
# Verifica connettività
curl http://192.168.123.99:8878/api/health
# Frame singoli
curl http://192.168.123.99:8878/api/rgb -o rgb.jpg
curl http://192.168.123.99:8878/api/depth -o depth.png
# Frontend interattivo
firefox http://192.168.123.99:8878/gui
# Swagger UI
firefox http://192.168.123.99:8878/apidocs
```
Da harry (192.168.123.100) sostituisci nell'`.env` del
`pestello-pose-detector` l'URL della camera con
`http://192.168.123.99:8878`.
---
## Endpoints ## Endpoints
Identici al Nuwa per drop-in compatibility:
| Endpoint | Metodo | Descrizione | | Endpoint | Metodo | Descrizione |
|----------|--------|-------------| |---|---|---|
| `/api/health` | GET | Health check (no auth) | | `/api/health` | GET | Health check (no API key) |
| `/api/status` | GET | Stato SDK/camera e calibrazione | | `/api/status` | GET | Stato SDK/camera, `color_available`, `usb_type`, intrinsics |
| `/api/discover` | GET | Rileva camere RealSense su USB | | `/api/discover` | GET | Enumera D4xx su USB |
| `/api/open` | POST | Apre camera per indice (`device_index`) | | `/api/open` | POST | Apre camera per `device_index` (di default partita in auto-open) |
| `/api/close` | POST | Chiude la camera | | `/api/close` | POST | Chiude camera |
| `/api/capture` | GET | Frame RGB+depth (JSON, base64) | | `/api/capture` | GET | RGB+depth in JSON (base64) |
| `/api/rgb` | GET | Frame RGB come JPEG | | `/api/rgb` | GET | RGB come JPEG |
| `/api/depth` | GET | Frame depth come PNG 16-bit (mm) | | `/api/depth` | GET | Depth come PNG 16-bit (mm) |
| `/api/depth/color` | GET | Depth colormap JET come JPEG | | `/api/depth/color` | GET | Depth colormap JET come JPEG |
| `/api/intrinsics` | GET | Calibrazione completa | | `/api/intrinsics` | GET | Calibrazione completa (`ir`+`rgb`+`extrinsics_ir_to_rgb`) |
| `/api/pointcloud` | GET | Point cloud (subsampling `step`) | | `/api/pointcloud` | GET | Point cloud `[x,y,z,r,g,b]` (param `step`, `max_depth_mm`) |
| `/api/pointcloud/roi` | GET | Point cloud su ROI pixel-space | | `/api/pointcloud/roi` | GET | Point cloud su ROI pixel-space |
| `/api/measure` | GET | Backprojection 3D di un pixel | | `/api/measure` | GET | Backprojection 3D di un pixel |
| `/api/snapshot` | POST | Salva snapshot RGB | | `/api/snapshot` | POST | Salva snapshot RGB su disco |
| `/api/recording/{start,stop,status}` | POST/GET | Registrazione continua | | `/api/recording/{start,stop,status}` | POST/GET | Registrazione continua |
| `/ws` | WS | Streaming WebSocket (text msgs: `start`, `stop`, `close`) | | `/ws` | WebSocket | Streaming (text: `start`, `stop`, `close`) |
| `/gui` | GET | Frontend React |
| `/apidocs` | GET | Swagger UI |
#### Payload calibrazione ### Header autenticazione
Tutti gli endpoint sotto `/api/` (eccetto `/api/health`) richiedono
header `X-Api-Key: <valore di API_KEY nel .env>`. Per disabilitare l'auth
in dev, mettere `API_KEY=` (vuoto) — sconsigliato in produzione.
### Payload calibrazione
`/api/intrinsics` ritorna i campi `ir` e `rgb` per compatibilità con il `/api/intrinsics` ritorna i campi `ir` e `rgb` per compatibilità con il
frontend Nuwa, ma in questo servizio: frontend Nuwa, ma su D4xx significano:
- `ir` = intrinseci del **sensore depth** (post-rectification dello stereo IR) - `ir` = intrinseci del **sensore depth** (post-rectification dello stereo IR)
- `rgb` = intrinseci del **sensore color** - `rgb` = intrinseci del **sensore color**
- `extrinsics_ir_to_rgb` = trasformazione **depth → color** - `extrinsics_ir_to_rgb` = trasformazione **depth → color** (translation in mm)
```json ---
{
"ir": { "fx": ..., "fy": ..., "cx": ..., "cy": ..., ## Configurazione (`.env`)
"distortion": { "k1":..., "k2":..., "k3":..., "p1":..., "p2":... } },
"rgb": { "fx": ..., "fy": ..., "cx": ..., "cy": ..., ```env
"distortion": { ... } }, API_PORT=8878
"extrinsics_ir_to_rgb": { API_KEY=<chiave-segreta>
"rotation": [r00, r01, r02, r10, r11, r12, r20, r21, r22], CAMERA_WIDTH=640
"translation": [tx, ty, tz] CAMERA_HEIGHT=480
} CAMERA_FPS=30
} CAMERA_AUTO_OPEN=1
CORS_ORIGINS=*
``` ```
- Rotation row-major 3x3 Note:
- Translation in **millimetri** (la D4xx ritorna `extrinsics.translation` in - `CAMERA_WIDTH/HEIGHT/FPS` sono **ignorati su USB 2** (forziamo 640x480@15
metri; convertito qui per allinearsi alla convenzione Nuwa) YUYV perché è l'unica combinazione che la D4xx sostiene). Su USB 3 invece
- D4xx pubblica tipicamente coefficienti di distorsione tutti zero (immagini vengono rispettati.
già rettificate dall'ASIC) - `.env` è nel `.gitignore` — non viene committato. Tienine una copia di
backup; `.env.example` è la struttura di riferimento.
### Esempi curl ---
## Modello hardware corrente del robot
```
Jetson Xavier NX
├─ Bus 1 (USB 2.0, 480M)
│ ├─ Hub interno → Inspire Hands (cdc_acm /dev/ttyACM0) + ch341
│ │ ⚠️ Non staccare questi — sono elettronica del robot
│ └─ Hub esterno (porta USB-C frontale)
│ ├─ Tastiera, mouse, ax88179 (Ethernet)
│ └─ D435 ← qui
└─ Bus 2 (USB 3.0, 10000M) — VUOTO, non esposto esternamente
```
- D435 serial: `933322070696`, firmware `05.16.00.01`, `usb_type = 2.1`
- Robot IP: `192.168.123.99` (eth0), `10.99.99.101` (ZeroTier)
---
## Workflow di sviluppo
Il codice "live" gira sul robot. Per modifiche:
### Opzione A — Modifichi sul robot, sync su harry, push da harry
```bash ```bash
# Discover # 1. Sul robot
curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/discover cd ~/Robot_2026/RealSense-D435
# ...modifiche...
sg docker -c "docker compose build backend && docker compose up -d --force-recreate"
curl http://localhost:8878/api/health # verifica
# Apri (D435 in genere index 0) # 2. Rsync su harry (esclude .env, models/, images/, __pycache__)
curl -X POST -H "X-Api-Key: $API_KEY" \ rsync -avz --delete \
"http://localhost:8878/api/open?device_index=0" --exclude='.env' --exclude='__pycache__' --exclude='*.pyc' \
--exclude='.pytest_cache' --exclude='.venv' \
--exclude='node_modules' --exclude='src/frontend/react/dist' \
--exclude='images/*' --exclude='models/*' \
--exclude='.claude' --exclude='.omc' \
~/Robot_2026/RealSense-D435/ \
harry@192.168.123.100:/opt/docker/robotserver/RealSense-D435/
# Frame RGB # 3. Push da harry
curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/rgb -o rgb.jpg ssh harry@192.168.123.100
cd /opt/docker/robotserver/RealSense-D435
# Frame depth (PNG 16-bit, valori in mm) git add . && git commit -m "..." && git push
curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/depth -o depth.png
# Intrinsics
curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/intrinsics | jq
# Close
curl -X POST -H "X-Api-Key: $API_KEY" http://localhost:8878/api/close
``` ```
## Note ### Opzione B — Modifichi su harry, deploy sul robot
- `librealsense2` runtime nel container è installato dal repo apt ufficiale ```bash
Intel (pinned a `2.55.1-0~realsense.3337`). I bindings Python sono # 1. Su harry
buildati da `IntelRealSense/librealsense@v2.55.1`. cd /opt/docker/robotserver/RealSense-D435
- Il frontend React originale (`/gui`) referenzia `ir` e `rgb` come label di git pull # o sviluppa direttamente
display — su questo servizio quei nomi mappano depth/color (vedi sopra). # ...modifiche...
- L'autostart è gestito da Docker (`restart: unless-stopped`). Per partire git add . && git commit -m "..." && git push
automaticamente al boot del robot serve `systemctl enable docker`.
- Per disabilitare l'auth API setta `API_KEY=` (vuoto). Sconsigliato in produzione. # 2. Sul robot
cd ~/Robot_2026/RealSense-D435
# ⚠️ Il robot non può push/pull diretto verso git.humion.it (proxy lo blocca).
# Sync via rsync nella direzione opposta:
rsync -avz --delete \
--exclude='.env' --exclude='__pycache__' --exclude='*.pyc' \
--exclude='images/*' --exclude='models/*' \
--exclude='.claude' --exclude='.omc' \
harry@192.168.123.100:/opt/docker/robotserver/RealSense-D435/ \
~/Robot_2026/RealSense-D435/
sg docker -c "docker compose build backend && docker compose up -d --force-recreate"
```
Nota: sul robot l'SSH verso `git.humion.it:222` viene bloccato dal
tinyproxy locale. Si può risolvere con una regola in `~/.ssh/config` se
in futuro vorrai pushare diretto dal robot.
---
## Risoluzione di problemi noti
### `pyrealsense2` ha `AttributeError: module ... has no attribute 'context'`
Il bug install di `librealsense@v2.55.1` (il `.so` principale non viene
installato dal CMake) è risolto nel `Dockerfile`. Se vuoi ricontrollare:
```bash
sg docker -c "docker compose exec backend python3 -c \
'import pyrealsense2 as rs; print(hasattr(rs, \"context\"))'"
# → True
```
Vedi `Dockerfile` righe ~36-46 e il commento.
### `wait_for_frames failed: Frame didn't arrive within 5000`
Il device è su USB 2 e il driver è in modalità errata (es. configurato per
BGR8). Verifica:
```bash
curl http://localhost:8878/api/status | jq .data.usb_type
# → "2.1" → il driver dovrebbe essere in modalità YUYV
```
Se il problema persiste, controlla `docker compose logs backend` per
warning con "USB2 profile".
### Voglio attivare il bottone "Predict" nella GUI
Quel bottone è un avanzo del codice copiato dal Nuwa, ma in produzione
non viene usato — l'inferenza YOLO gira sul container `yolo-server` su
harry (con il modello custom `best.onnx` per il pestello). Vedi
[architettura](#architettura).
Per attivare il detector inline (sconsigliato per la pipeline pestello,
ma utile per test):
1. Aggiungere `onnxruntime` nel `Dockerfile`
2. Copiare un modello in `models/best.onnx`
3. Settare `YOLO_*` nel `.env`
### Posso pushare/pullare git direttamente dal robot?
Al momento no — il tinyproxy del robot blocca l'SSH verso `git.humion.it:222`.
Workaround: `rsync` da/verso harry e push da lì. Vedi
[Workflow di sviluppo](#workflow-di-sviluppo).
---
## Struttura del repo
```
RealSense-D435/
├── Dockerfile # multi-stage: librealsense + frontend + runtime
├── docker-compose.yml # backend + nats opzionale + frontend-dev
├── .env / .env.example # config (NON committare .env)
├── pyproject.toml # deps Python (per uv/pip)
├── README.md # questo file
├── STATUS.md # snapshot ultima sessione di lavoro
├── scripts/
│ └── entrypoint.sh
├── src/backend/
│ ├── main.py # FastAPI app
│ ├── config.py # pydantic settings
│ ├── api/
│ │ ├── routes.py # REST endpoints
│ │ ├── websocket.py # /ws streaming
│ │ └── dependencies.py # API key auth
│ └── services/
│ ├── camera.py # ⭐ driver RealSense (detection USB 2/3, YUYV, etc.)
│ ├── detector.py # wrapper ONNX YOLO (non usato in produzione)
│ └── image_saver.py
├── src/frontend/react/ # GUI a /gui (built via stage 2 del Dockerfile)
├── tests/ # pytest, da rilanciare quando si tocca services/
├── models/ # vuoto — modello sta su yolo-server, non qui
└── images/ # output snapshot/recording, gitignored
```
---
## Riferimenti
- [librealsense](https://github.com/IntelRealSense/librealsense) — buildato da source perché manca wheel ARM64 (`librealsense@v2.55.1`)
- [Nuwa-HP60C](https://git.humion.it/Labotics-gianniact/) — container originale di cui questo è il drop-in equivalent
- [yolo-server](https://git.humion.it/Labotics-gianniact/) — microservizio YOLO usato da pestello-pose-detector
- [pestello-pose-detector](https://git.humion.it/Labotics-gianniact/pestello-pose-detector) — consumer reale di questo stream