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.
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.
Quick start
Sul robot (deploy)
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):
# 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: <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
frontend Nuwa, ma su D4xx significano:
ir= intrinseci del sensore depth (post-rectification dello stereo IR)rgb= intrinseci del sensore colorextrinsics_ir_to_rgb= trasformazione depth → color (translation in mm)
Configurazione (.env)
API_PORT=8878
API_KEY=<chiave-segreta>
CAMERA_WIDTH=640
CAMERA_HEIGHT=480
CAMERA_FPS=30
CAMERA_AUTO_OPEN=1
CORS_ORIGINS=*
Note:
CAMERA_WIDTH/HEIGHT/FPSsono 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, firmware05.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
# 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
# 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:
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:
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.
Per attivare il detector inline (sconsigliato per la pipeline pestello, ma utile per test):
- Aggiungere
onnxruntimenelDockerfile - Copiare un modello in
models/best.onnx - 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.
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 — buildato da source perché manca wheel ARM64 (
librealsense@v2.55.1) - Nuwa-HP60C — container originale di cui questo è il drop-in equivalent
- yolo-server — microservizio YOLO usato da pestello-pose-detector
- pestello-pose-detector — consumer reale di questo stream