# RealSense D435 — API Server 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. 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). --- ## Architettura ``` ┌────────────────────┐ 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 container fa **solo** acquisizione e serve frame — niente inferenza, niente pose estimation. Quella logica vive in container separati su harry. --- ## 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 cd ~/Robot_2026/RealSense-D435 # 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 container ha `restart: unless-stopped`, quindi resta su dopo reboot del robot (purché `docker.service` parta — `systemctl enable docker` se non lo è già). ### 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 | Endpoint | Metodo | Descrizione | |---|---|---| | `/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 (`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 su disco | | `/api/recording/{start,stop,status}` | POST/GET | Registrazione continua | | `/ws` | WebSocket | Streaming (text: `start`, `stop`, `close`) | | `/gui` | GET | Frontend React | | `/apidocs` | GET | Swagger UI | ### 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 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** (translation in mm) --- ## Configurazione (`.env`) ```env API_PORT=8878 API_KEY= CAMERA_WIDTH=640 CAMERA_HEIGHT=480 CAMERA_FPS=30 CAMERA_AUTO_OPEN=1 CORS_ORIGINS=* ``` 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. --- ## 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 # 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 # 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/ # 3. Push da harry ssh harry@192.168.123.100 cd /opt/docker/robotserver/RealSense-D435 git add . && git commit -m "..." && git push ``` ### Opzione B — Modifichi su harry, deploy sul robot ```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