henrytherobotandClaude Opus 4.7 1451d51211 docs: rewrite README for G1 deploy
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>
2026-05-18 17:18:34 +02:00
2026-05-18 17:18:34 +02:00

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 color
  • extrinsics_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/FPS su 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, firmware 05.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:

  1. video_hub_pc4 è ancora attivo e tiene /dev/video4. Verifica con lsof /dev/video4 o fuser /dev/video4. Stoppalo come sopra.
  2. Hai (re)buildato il container con una librealsense diversa da v2.50.0. Il D435i fw 05.15.x non funziona con v2.55+. Controlla Dockerfile riga 11 — deve essere ARG 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):

  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-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.0 per compatibilità col firmware 05.15.x del 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
S
Description
No description provided
Readme
389 KiB
0 Stars 2 Watchers 0 Forks
Languages
Python 72.8%
JavaScript 23.5%
Dockerfile 2.7%
CSS 0.5%
Shell 0.3%
Other 0.2%