diff --git a/README.md b/README.md index 86f8e7e..085c3e3 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -# RealSense D435 — API Server +# RealSense D435i — API Server (G1) -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. +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. -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). +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](https://git.humion.it/Labotics-OnGianni/RealSense-D435-H1) +— il nome `…-H1` è storico, non cambia il fatto che gira anche su G1. --- @@ -26,7 +28,7 @@ Gira sul robot **Unitree H1** (Jetson Xavier NX, JetPack 5.1.x). Il repo │ ▼ │ ┌───────────────────┴────┐ │ │ RealSense-D435-H1 ⬅️ │ ← QUESTO REPO │ - │ (sul robot, port 8878)│ │ + │ (sul G1, port 8878) │ │ └────────────────────────┘ │ (era: Nuwa-HP60C) │ │ @@ -37,35 +39,81 @@ niente pose estimation. Quella logica vive in container separati su harry. --- -## Hardware constraint importante: D435 su USB 2.0 +## Hardware — G1 vs H1 -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. +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. -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) | +| | **G1 (questo deploy)** | H1 (vedi `main`) | |---|---|---| -| 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 | +| 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 | -I dettagli sui workaround applicati sono in [STATUS.md](STATUS.md). +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 -### Sul robot (deploy) +### 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: ```bash -cd ~/Robot_2026/RealSense-D435 +/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. + +```bash +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 + +```bash +cd ~/robot/RealSense-D435-H1 # Build + run (la prima volta build di librealsense da source: ~25 min) sg docker -c "docker compose up -d" @@ -79,12 +127,12 @@ 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à). +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`) o via mDNS (`ubuntu.local`): +Dall'IP LAN del robot (`192.168.123.99`, eth0) o `ubuntu.local` via mDNS: ```bash # Verifica connettività @@ -101,9 +149,8 @@ firefox http://192.168.123.99:8878/gui 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`. +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`. --- @@ -134,7 +181,8 @@ Da harry (192.168.123.100) sostituisci nell'`.env` del 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. +in dev, mettere `API_KEY=` (vuoto) — sconsigliato in produzione. Sul G1 +attualmente `.env` ha `API_KEY=` vuoto, niente auth. ### Payload calibrazione @@ -149,39 +197,57 @@ frontend Nuwa, ma su D4xx significano: ## Configurazione (`.env`) ```env +API_HOST=0.0.0.0 API_PORT=8878 -API_KEY= +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` 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 +- `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 +## Modello hardware corrente del robot G1 ``` -Jetson Xavier NX +Jetson Xavier NX (G1) ├─ 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 +│ └─ (varia in base a periferiche collegate — non usato dalla D435i) +└─ Bus 2 (USB 3.0, 10000M) + └─ Hub interno testa → D435i ← qui, /dev/video0…video5 ``` -- D435 serial: `933322070696`, firmware `05.16.00.01`, `usb_type = 2.1` -- Robot IP: `192.168.123.99` (eth0), `10.99.99.101` (ZeroTier) +- 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](#prerequisito-liberare-devvideo4)). Se al boot lasci che +parta, il container backend non riesce a inizializzare la pipeline. --- @@ -192,25 +258,32 @@ 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 +# 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__) +# 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_2026/RealSense-D435/ \ - harry@192.168.123.100:/opt/docker/robotserver/RealSense-D435/ + ~/robot/RealSense-D435-H1/ \ + harry@192.168.123.100:/opt/docker/robotserver/RealSense-D435-H1/ -# 3. Push da harry +# 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 +cd /opt/docker/robotserver/RealSense-D435-H1 +git checkout camera-g1 git add . && git commit -m "..." && git push ``` @@ -218,36 +291,58 @@ git add . && git commit -m "..." && git push ```bash # 1. Su harry -cd /opt/docker/robotserver/RealSense-D435 +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 -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: +# 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/ \ - ~/Robot_2026/RealSense-D435/ + 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: 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. +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@v2.55.1` (il `.so` principale non viene -installato dal CMake) è risolto nel `Dockerfile`. Se vuoi ricontrollare: + +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: ```bash sg docker -c "docker compose exec backend python3 -c \ @@ -255,21 +350,22 @@ sg docker -c "docker compose exec backend python3 -c \ # → True ``` -Vedi `Dockerfile` righe ~36-46 e il commento. +Vedi `Dockerfile` righe ~40-50 (sezione "Collect both the python +bindings and the native runtime .so"). -### `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: +### `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`: ```bash -curl http://localhost:8878/api/status | jq .data.usb_type -# → "2.1" → il driver dovrebbe essere in modalità YUYV +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' ``` -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 @@ -282,6 +378,7 @@ ma utile per test): 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). @@ -291,13 +388,13 @@ Workaround: `rsync` da/verso harry e push da lì. Vedi ## Struttura del repo ``` -RealSense-D435/ -├── Dockerfile # multi-stage: librealsense + frontend + runtime +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 -├── STATUS.md # snapshot ultima sessione di lavoro +├── README.md # questo file (G1) +├── STATUS.md # snapshot ultima sessione di lavoro (H1) ├── scripts/ │ └── entrypoint.sh ├── src/backend/ @@ -311,7 +408,8 @@ RealSense-D435/ │ ├── 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) +├── 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 @@ -319,9 +417,21 @@ RealSense-D435/ --- +## 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](https://github.com/IntelRealSense/librealsense) — buildato da source perché manca wheel ARM64 (`librealsense@v2.55.1`) +- [librealsense](https://github.com/IntelRealSense/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](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