Updates the README from the H1 description to the current G1 state: D435i over USB 3.2, librealsense pinned to v2.50.0 for FW 05.15.x compatibility, host-side React build, and the video_hub_pc4 stop prerequisite. Adds a G1-vs-H1 hardware comparison table and three new troubleshooting entries (wait_for_frames timeout, resolve request failure, npm DNS). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RealSense D435i — API Server (G1)
Container Docker che espone una Intel RealSense D4xx 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.
Questo branch (camera-g1) è la porta del repo originale H1 verso il
robot Unitree G1 (Jetson Xavier NX, JetPack 5.1.x). Il repo upstream
è su git.humion.it
— il nome …-H1 è storico, non cambia il fatto che gira anche su G1.
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 G1, 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 — G1 vs H1
Il robot G1 ha la D435i nella testa, collegata a un bus USB 3.2 nativo del Jetson. È una situazione molto più semplice della H1, dove la stessa famiglia di camera era cablata su USB 2 e richiedeva workaround pesanti nel driver.
| G1 (questo deploy) | H1 (vedi main) |
|
|---|---|---|
| Camera | Intel RealSense D435i | Intel RealSense D435 |
| Serial / FW | 243722073572 / 05.15.01.55 |
933322070696 / 05.16.00.01 |
| Bus | USB 3.2 (xhci 2-2.1) |
USB 2.1 (hub esterno frontale) |
| Color format | BGR8 nativo | YUYV (convertito in BGR via cv2) |
| Risoluzione effettiva | 640x480 @ 30 fps | 640x480 @ ~4 fps reali |
| Alignment depth↔color | Hardware (rs.align) |
Approximate (resize numpy) |
| librealsense SDK | v2.50.0 (pinned, vedi sotto) | v2.55.1 |
Il driver src/backend/services/camera.py rileva usb_type_descriptor
all'apertura e sceglie il profilo: il path USB 2 è ancora presente per
non rompere la build H1, ma su G1 non viene attivato.
Perché librealsense v2.50.0 e non l'ultima
Il firmware 05.15.01.55 del D435i della G1 è più vecchio della
"recommended" (05.13.00.50 ← sì, il numero più basso è più nuovo
nella numerazione D4xx, ma il punto è che la nostra è non-standard).
Con librealsense v2.55.x il device si apre regolarmente ma
wait_for_frames va in timeout indefinitamente — sembra essere una
regressione del nuovo USB backend contro firmware D4xx vecchio. Pinnando
il container a v2.50.0 (la stessa versione del clone di librealsense
installato sull'host in /home/unitree/librealsense) il flusso frame
torna a funzionare. Vedi Dockerfile riga 11.
Quick start
Prerequisito: liberare /dev/video4
Sul G1 c'è un servizio Unitree chiamato video_hub_pc4 che apre
/dev/video4 (uno dei nodi V4L2 esposti dal D435i) all'avvio del
sistema. Se è attivo, il container non riesce a inizializzare la
pipeline e si vede RuntimeError: Failed to resolve request. Fermarlo:
/unitree/sbin/mscli stopservice video_hub_pc4
⚠️ Questo va rifatto a ogni reboot. Per renderlo persistente bisognerebbe disattivare l'autostart Unitree — non lo abbiamo ancora fatto perché non sappiamo se serve a qualcos'altro nel sistema.
Build del frontend (una tantum, lato host)
Il vecchio Dockerfile H1 buildava il React frontend in uno stage Docker.
Sul Jetson del G1, però, npm dentro al container non riesce a
risolvere registry.npmjs.org (il resolver di default di Docker —
127.0.0.53 — è lo stub systemd-resolved dell'host, che non funziona
da dentro i container). Quindi buildiamo il frontend una volta sull'host,
e il Dockerfile copia solo il dist/ già pronto.
cd ~/robot/RealSense-D435-H1
docker run --rm --dns 8.8.8.8 \
-v "$(pwd)/src/frontend/react:/app" -w /app \
node:22-alpine sh -c 'npm install && npm run build'
# → produce src/frontend/react/dist/
Da rifare solo quando cambi qualcosa sotto src/frontend/react/.
Run
cd ~/robot/RealSense-D435-H1
# 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 e tu abbia rifatto lo stop di
video_hub_pc4).
Dal tuo PC (consumare lo stream)
Dall'IP LAN del robot (192.168.123.99, eth0) o ubuntu.local via mDNS:
# 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), nel pestello-pose-detector sostituisci nel
suo .env 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. Sul G1
attualmente .env ha API_KEY= vuoto, niente auth.
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_HOST=0.0.0.0
API_PORT=8878
API_KEY=
# NATS (disabled by default — attivabile col profilo `nats` del compose)
NATS_URL=
NATS_TOKEN=
# Identità servizio
SERVICE_NAME=realsense-g1
SERVICE_DOMAIN=vision
# Stream
CAMERA_WIDTH=640
CAMERA_HEIGHT=480
CAMERA_FPS=30
CAMERA_AUTO_OPEN=1
# CORS
CORS_ORIGINS=*
Note:
CAMERA_WIDTH/HEIGHT/FPSsu G1 vengono rispettati (USB 3 → tutto il range supportato dalla D435i è disponibile). Sul branch H1 erano ignorati e forzati a 640x480@15 YUYV..envè nel.gitignore— non viene committato. Tieni una copia di backup;.env.exampleè la struttura di riferimento.
Modello hardware corrente del robot G1
Jetson Xavier NX (G1)
├─ Bus 1 (USB 2.0, 480M)
│ └─ (varia in base a periferiche collegate — non usato dalla D435i)
└─ Bus 2 (USB 3.0, 10000M)
└─ Hub interno testa → D435i ← qui, /dev/video0…video5
- D435i serial:
243722073572, firmware05.15.01.55,usb_type = 3.2 - Bus path:
/sys/devices/platform/3610000.xhci/usb2/2-2/2-2.1 - VID:PID
8086:0b3a(D435i depth+IR+RGB module) - Robot IP:
192.168.123.99(eth0),192.168.1.85(wlan0 DHCP)
Servizio Unitree da fermare
video_hub_pc4 apre /dev/video4 all'avvio (vedi
Prerequisito). Se al boot lasci che
parta, il container backend non riesce a inizializzare la pipeline.
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 G1
cd ~/robot/RealSense-D435-H1
# ...modifiche...
# (se hai toccato il frontend)
docker run --rm --dns 8.8.8.8 \
-v "$(pwd)/src/frontend/react:/app" -w /app \
node:22-alpine sh -c 'npm install && npm run build'
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__, dist/)
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/RealSense-D435-H1/ \
harry@192.168.123.100:/opt/docker/robotserver/RealSense-D435-H1/
# 3. Push da harry (il G1 non può push diretto a git.humion.it — vedi sotto)
ssh harry@192.168.123.100
cd /opt/docker/robotserver/RealSense-D435-H1
git checkout camera-g1
git add . && git commit -m "..." && git push
Opzione B — Modifichi su harry, deploy sul robot
# 1. Su harry
cd /opt/docker/robotserver/RealSense-D435-H1
git checkout camera-g1
git pull # o sviluppa direttamente
# ...modifiche...
git add . && git commit -m "..." && git push
# 2. Sul robot G1
cd ~/robot/RealSense-D435-H1
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-H1/ \
~/robot/RealSense-D435-H1/
# rifare la build frontend se è cambiata, poi:
sg docker -c "docker compose build backend && docker compose up -d --force-recreate"
Nota: come sull'H1, anche sul G1 l'SSH verso git.humion.it:222 passa
tipicamente da tinyproxy locale e non funziona "out of the box". Se in
futuro vuoi pushare diretto dal robot, va aggiunta una regola in
~/.ssh/config. Per ora si passa da harry.
Risoluzione di problemi noti
wait_for_frames failed: Frame didn't arrive within 5000
Su G1 questo significa quasi sempre una di due cose:
video_hub_pc4è ancora attivo e tiene/dev/video4. Verifica conlsof /dev/video4ofuser /dev/video4. Stoppalo come sopra.- Hai (re)buildato il container con una librealsense diversa da
v2.50.0. Il D435i fw
05.15.xnon funziona con v2.55+. ControllaDockerfileriga 11 — deve essereARG RS_VERSION=v2.50.0.
Se invece sei sul branch main (H1), su USB 2 questo errore è quasi
sempre un mismatch di profilo color (BGR8 vs YUYV) — vedi STATUS.md.
RuntimeError: Failed to resolve request
Pipeline non riesce a fare match tra config richiesta e device. Su G1
in pratica significa: device non disponibile (occupato da
video_hub_pc4) o cavo USB scollegato.
pyrealsense2 ha AttributeError: module ... has no attribute 'context'
Il bug install di librealsense (il .so principale non viene installato
dal CMake in alcune versioni) è risolto nel Dockerfile copiando
direttamente i .so dal build dir. Per verificare:
sg docker -c "docker compose exec backend python3 -c \
'import pyrealsense2 as rs; print(hasattr(rs, \"context\"))'"
# → True
Vedi Dockerfile righe ~40-50 (sezione "Collect both the python
bindings and the native runtime .so").
npm install fallisce con getaddrinfo ENOTFOUND registry.npmjs.org
Stai cercando di buildare il frontend dentro Docker senza override DNS.
Il resolver di default di Docker eredita 127.0.0.53 (systemd-resolved
stub), che non funziona dentro i container. Usa --dns 8.8.8.8:
docker run --rm --dns 8.8.8.8 -v "$(pwd)/src/frontend/react:/app" \
-w /app node:22-alpine sh -c 'npm install && npm run build'
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-H1/
├── Dockerfile # multi-stage: librealsense v2.50.0 + 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 (G1)
├── STATUS.md # snapshot ultima sessione di lavoro (H1)
├── 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
│ └── dist/ # ⚠️ buildare a mano sull'host (vedi Quick start)
├── tests/ # pytest, da rilanciare quando si tocca services/
├── models/ # vuoto — modello sta su yolo-server, non qui
└── images/ # output snapshot/recording, gitignored
Branches
main— deploy storico H1 (D435 su USB 2, YUYV, ~4 fps)camera-g1— questo branch, deploy G1 (D435i su USB 3.2, BGR8, 30 fps)
Il driver Python è lo stesso su entrambi — distingue il path runtime
leggendo usb_type_descriptor del device. Le differenze tra branch sono
nel Dockerfile (versione SDK, stage frontend) e nel .env (identità
servizio).
Riferimenti
- librealsense — buildato da source perché manca wheel ARM64. Su G1 pinnato a
v2.50.0per compatibilità col firmware05.15.xdel D435i. - 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