From 739afd465bdf15dad646875e001fca7dd52d34f5 Mon Sep 17 00:00:00 2001 From: robot Date: Mon, 18 May 2026 11:03:42 +0200 Subject: [PATCH] docs: rewrite README with architecture, USB 2 quirks, dev workflow --- README.md | 415 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 287 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 3a244c1..86f8e7e 100644 --- a/README.md +++ b/README.md @@ -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) -con accesso remoto via REST/WebSocket. Esecuzione tramite Docker. Mantiene -la stessa shape API del servizio Nuwa HP60C (envelope `ApiResponse`, -endpoint `/api/...`, header `X-Api-Key`, WebSocket `/ws`) così che i client -e il frontend React esistenti funzionano senza modifiche. +Container Docker che espone una Intel RealSense D4xx (testata su D435) via +REST + WebSocket, con la **stessa shape API del Nuwa-HP60C**: envelope +`ApiResponse`, endpoint `/api/...`, header `X-Api-Key`, WebSocket `/ws`. +Pensato come drop-in replacement del container Nuwa nella pipeline del +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 - -| 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: +## Architettura ``` -http://:8878/api/health # health check -http://:8878/apidocs # Swagger UI -http://:8878/gui # frontend React -ws://:8878/ws # stream WebSocket +┌────────────────────┐ RGB JPEG (HTTP) ┌────────────────────────────┐ +│ yolo-server │ ◄──────────────────── │ pestello-pose-detector │ +│ (su harry) │ ──► bbox + conf │ (su harry) │ +└────────────────────┘ │ │ + │ │ │ + │ │ /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 -risolve sul vostro PC (`avahi-resolve -n ubuntu.local`). Altrimenti -usate l'IP diretto (`ip a` sul robot per scoprirlo). +Il container fa **solo** acquisizione e serve frame — niente inferenza, +niente pose estimation. Quella logica vive in container separati su harry. -> 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 -# Backend (porta 8878) -docker compose up --build -d +cd ~/Robot_2026/RealSense-D435 -# Backend + NATS -docker compose --profile nats up --build -d +# Build + run (la prima volta build di librealsense da source: ~25 min) +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 — -> ~20–30 min su Jetson. I build successivi sono cached. +Il container ha `restart: unless-stopped`, quindi resta su dopo reboot +del robot (purché `docker.service` parta — `systemctl enable docker` se +non lo è già). -Il container: -- non fa unbind UVC (a differenza del Nuwa) — RealSense usa direttamente uvcvideo -- monta `/dev/bus/usb` in privileged mode per accesso diretto USB -- riparte automaticamente (`restart: unless-stopped`) +### Dal tuo PC (consumare lo stream) + +Dall'IP LAN del robot (`192.168.123.99`) o via mDNS (`ubuntu.local`): + +```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 -Identici al Nuwa per drop-in compatibility: - | Endpoint | Metodo | Descrizione | -|----------|--------|-------------| -| `/api/health` | GET | Health check (no auth) | -| `/api/status` | GET | Stato SDK/camera e calibrazione | -| `/api/discover` | GET | Rileva camere RealSense su USB | -| `/api/open` | POST | Apre camera per indice (`device_index`) | -| `/api/close` | POST | Chiude la camera | -| `/api/capture` | GET | Frame RGB+depth (JSON, base64) | -| `/api/rgb` | GET | Frame RGB come JPEG | -| `/api/depth` | GET | Frame depth come PNG 16-bit (mm) | +|---|---|---| +| `/api/health` | GET | Health check (no API key) | +| `/api/status` | GET | Stato SDK/camera, `color_available`, `usb_type`, intrinsics | +| `/api/discover` | GET | Enumera D4xx su USB | +| `/api/open` | POST | Apre camera per `device_index` (di default partita in auto-open) | +| `/api/close` | POST | Chiude camera | +| `/api/capture` | GET | RGB+depth in JSON (base64) | +| `/api/rgb` | GET | RGB come JPEG | +| `/api/depth` | GET | Depth come PNG 16-bit (mm) | | `/api/depth/color` | GET | Depth colormap JET come JPEG | -| `/api/intrinsics` | GET | Calibrazione completa | -| `/api/pointcloud` | GET | Point cloud (subsampling `step`) | +| `/api/intrinsics` | GET | Calibrazione completa (`ir`+`rgb`+`extrinsics_ir_to_rgb`) | +| `/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/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 | -| `/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: `. 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 -frontend Nuwa, ma in questo servizio: - +frontend Nuwa, ma su D4xx significano: - `ir` = intrinseci del **sensore depth** (post-rectification dello stereo IR) - `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": ..., - "distortion": { "k1":..., "k2":..., "k3":..., "p1":..., "p2":... } }, - "rgb": { "fx": ..., "fy": ..., "cx": ..., "cy": ..., - "distortion": { ... } }, - "extrinsics_ir_to_rgb": { - "rotation": [r00, r01, r02, r10, r11, r12, r20, r21, r22], - "translation": [tx, ty, tz] - } -} +--- + +## Configurazione (`.env`) + +```env +API_PORT=8878 +API_KEY= +CAMERA_WIDTH=640 +CAMERA_HEIGHT=480 +CAMERA_FPS=30 +CAMERA_AUTO_OPEN=1 +CORS_ORIGINS=* ``` -- Rotation row-major 3x3 -- Translation in **millimetri** (la D4xx ritorna `extrinsics.translation` in - metri; convertito qui per allinearsi alla convenzione Nuwa) -- D4xx pubblica tipicamente coefficienti di distorsione tutti zero (immagini - già rettificate dall'ASIC) +Note: +- `CAMERA_WIDTH/HEIGHT/FPS` sono **ignorati su USB 2** (forziamo 640x480@15 + YUYV perché è l'unica combinazione che la D4xx sostiene). Su USB 3 invece + vengono rispettati. +- `.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 -# Discover -curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/discover +# 1. Sul robot +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) -curl -X POST -H "X-Api-Key: $API_KEY" \ - "http://localhost:8878/api/open?device_index=0" +# 2. Rsync su harry (esclude .env, models/, images/, __pycache__) +rsync -avz --delete \ + --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 -curl -H "X-Api-Key: $API_KEY" http://localhost:8878/api/rgb -o rgb.jpg - -# Frame depth (PNG 16-bit, valori in mm) -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 +# 3. Push da harry +ssh harry@192.168.123.100 +cd /opt/docker/robotserver/RealSense-D435 +git add . && git commit -m "..." && git push ``` -## Note +### Opzione B — Modifichi su harry, deploy sul robot -- `librealsense2` runtime nel container è installato dal repo apt ufficiale - Intel (pinned a `2.55.1-0~realsense.3337`). I bindings Python sono - buildati da `IntelRealSense/librealsense@v2.55.1`. -- Il frontend React originale (`/gui`) referenzia `ir` e `rgb` come label di - display — su questo servizio quei nomi mappano depth/color (vedi sopra). -- L'autostart è gestito da Docker (`restart: unless-stopped`). Per partire - automaticamente al boot del robot serve `systemctl enable docker`. -- Per disabilitare l'auth API setta `API_KEY=` (vuoto). Sconsigliato in produzione. +```bash +# 1. Su harry +cd /opt/docker/robotserver/RealSense-D435 +git pull # o sviluppa direttamente +# ...modifiche... +git add . && git commit -m "..." && git push + +# 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