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 color
  • extrinsics_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/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

# 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):

  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.


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
S
Description
No description provided
Readme
119 KiB
0 Stars 2 Watchers 0 Forks
Languages
Python 56.5%
JavaScript 37.4%
Dockerfile 4.5%
CSS 0.7%
Shell 0.6%
Other 0.3%