For aligning on the host: on the Jetson rs.align.process costs ~104 ms and holds the GIL (optimize-streams R04/R05). JSON in the usual envelope, one response: frameset_seq, camera_timestamp_ms, timestamp_domain, host_timestamp, color_jpeg_b64 (color grid), depth_png_b64 (16-bit PNG, mm, depth grid), color_shape, depth_shape, depth_units. after=N / timeout_ms as /api/color/latest. JSON + base64 like /api/capture: every client already parses that envelope, one request gets both images, base64 costs ~0.1 ms. Built once per frameset (EncodedRaw, last 4 kept); the color JPEG is the color channel's own (same bytes). The depth is converted only on request (camera.raw_frame, phase raw_depth; PNG in phase png_raw_depth). camera: _published() shared by color_frame() and raw_frame(); _depth_mm() extracted unchanged from _aligned_frame(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
507 lines
21 KiB
Markdown
507 lines
21 KiB
Markdown
# 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 repo è il deploy per il robot **Unitree G1** (Jetson Xavier NX,
|
||
JetPack 5.1.x): [git.humion.it/.../RealSense-D435i-G1](https://git.humion.it/Labotics-OnGianni/RealSense-D435i-G1).
|
||
Nasce come fork del repo H1 originale ([RealSense-D435-H1](https://git.humion.it/Labotics-OnGianni/RealSense-D435-H1)),
|
||
dal quale conserva la storia git completa fino al commit di porting su
|
||
G1. Da quel punto in poi i due progetti evolvono indipendentemente.
|
||
|
||
---
|
||
|
||
## Architettura
|
||
|
||
```
|
||
┌────────────────────┐ RGB JPEG (HTTP) ┌────────────────────────────┐
|
||
│ yolo-server │ ◄──────────────────── │ pestello-pose-detector │
|
||
│ (su harry) │ ──► bbox + conf │ (su harry) │
|
||
└────────────────────┘ │ │
|
||
│ │ │
|
||
│ │ /api/rgb │
|
||
│ │ /api/depth │
|
||
│ │ /api/intrinsics │
|
||
│ ▼ │
|
||
┌───────────────────┴────┐ │
|
||
│ RealSense-D435i-G1 ⬅️ │ ← 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:
|
||
|
||
```bash
|
||
/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-D435i-G1
|
||
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-D435i-G1
|
||
|
||
# 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:
|
||
|
||
```bash
|
||
# 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 (colore del frame allineato: stesso frame di `/api/depth`) |
|
||
| `/api/raw/latest` | GET | **Frameset grezzo per allineare sull'host**: JSON con colore (JPEG, griglia colore) e profondità (PNG 16 bit in mm, griglia depth) **dello stesso frameset, senza align**, più `frameset_seq` (stessa numerazione di `/api/color/latest` e del `frameset_seq` di `/api/capture`), `camera_timestamp_ms`, `timestamp_domain`, `host_timestamp`. `after=N` / `timeout_ms` come `/api/color/latest`. Calibrazione per allineare: `/api/intrinsics` |
|
||
| `/api/color/latest` | GET | **Canale colore veloce**: JPEG dell'ultimo frame colore, **senza align**, al ritmo della camera. `after=N` aspetta un frame numerato oltre N (fino a `timeout_ms`, default 1000, max 5000; 504 se non arriva). Header `X-Frame-Seq`, `X-Camera-Timestamp-Ms` (orologio della camera), `X-Camera-Timestamp-Domain`, `X-Host-Timestamp` (arrivo sull'host, s). Non va usato insieme a `/api/depth`: non è lo stesso frame |
|
||
| `/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)
|
||
|
||
**Quali intrinseci usa la back-projection** (`/api/measure`, `/api/pointcloud`,
|
||
`/api/pointcloud/roi`): dipende dall'allineamento depth→color (vedi
|
||
[bug intrinsics mismatch](#punti-3d-stirati-del-54-su-xy-intrinsics-mismatch)).
|
||
Su USB 3 (default) la depth è allineata al frame color, quindi si usano gli
|
||
intrinseci **`rgb`**; su USB 2 (no alignment) si usano gli **`ir`**. La scelta
|
||
è centralizzata in `_backprojection_intrinsics()` e il campo `intrinsics` nella
|
||
risposta riporta sempre quelli effettivamente usati.
|
||
|
||
---
|
||
|
||
## Configurazione (`.env`)
|
||
|
||
```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 repo 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](#prerequisito-liberare-devvideo4)). 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
|
||
|
||
```bash
|
||
# 1. Sul robot G1
|
||
cd ~/robot/RealSense-D435i-G1
|
||
# ...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-D435i-G1/ \
|
||
harry@192.168.123.100:/opt/docker/robotserver/RealSense-D435i-G1/
|
||
|
||
# 3. Push da harry (il G1 non può push diretto a git.humion.it — vedi sotto)
|
||
# Assicurati che harry abbia un clone del nuovo repo RealSense-D435i-G1
|
||
# in /opt/docker/robotserver/RealSense-D435i-G1 (clone una tantum).
|
||
ssh harry@192.168.123.100
|
||
cd /opt/docker/robotserver/RealSense-D435i-G1
|
||
git add . && git commit -m "..." && git push
|
||
```
|
||
|
||
### Opzione B — Modifichi su harry, deploy sul robot
|
||
|
||
```bash
|
||
# 1. Su harry
|
||
cd /opt/docker/robotserver/RealSense-D435i-G1
|
||
git pull # o sviluppa direttamente
|
||
# ...modifiche...
|
||
git add . && git commit -m "..." && git push
|
||
|
||
# 2. Sul robot G1
|
||
cd ~/robot/RealSense-D435i-G1
|
||
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-D435i-G1/ \
|
||
~/robot/RealSense-D435i-G1/
|
||
|
||
# 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 stai lavorando sul repo H1, su USB 2 questo errore è quasi
|
||
sempre un mismatch di profilo color (BGR8 vs YUYV) — vedi STATUS.md.
|
||
|
||
### Punti 3D stirati del ~54% su XY (intrinsics mismatch)
|
||
|
||
Riguarda `/api/measure`, `/api/pointcloud`, `/api/pointcloud/roi`.
|
||
|
||
**Sintomo.** Le coordinate `(X, Y, Z)` restituite dalla back-projection sono
|
||
gonfiate lateralmente di un fattore ~1.54 su X e Y, mentre Z (depth) è corretto.
|
||
Un marker ArUco da 103 mm a ~0.73 m veniva misurato ~157 mm; i point cloud del
|
||
D435i non si sovrapponevano a quelli di altre camere calibrate. Diagnosticato e
|
||
fixato il 2026-05-27 (vedi `bug_d435i_intrinsics_mismatch.md`).
|
||
|
||
**Root cause.** La pipeline allinea la depth al frame **color** con
|
||
`rs.align(rs.stream.color)` ([`services/camera.py`](src/backend/services/camera.py)),
|
||
quindi dopo l'alignment la depth vive sulla **griglia di pixel del color**. Ma
|
||
i tre endpoint di back-projection usavano gli intrinseci del **sensore depth**
|
||
(`fx_ir`, `cx_ir`, …) per de-proiettare quei pixel. Risultato: X e Y inflati
|
||
esattamente del rapporto delle focali
|
||
|
||
```
|
||
fx_rgb / fx_ir = 603.88 / 392.31 ≈ 1.539
|
||
```
|
||
|
||
Z resta corretto perché dipende solo dal valore di depth letto al pixel, non
|
||
dagli intrinseci. Verifica numerica sul pixel `(377, 68)`, `z = 0.732 m`:
|
||
|
||
```
|
||
IR (errato): X = (377 − 318.48) × 0.732 / 392.31 = 0.1092 m ← valore restituito (buggy)
|
||
RGB (corretto): X = (377 − 320.62) × 0.732 / 603.88 = 0.0683 m ← valore atteso
|
||
```
|
||
|
||
**Fix.** La scelta degli intrinseci è ora condizionale all'allineamento, in un
|
||
unico helper `_backprojection_intrinsics()` ([`api/routes.py`](src/backend/api/routes.py))
|
||
usato da `/api/measure`, `/api/pointcloud`, `/api/pointcloud/roi`:
|
||
|
||
- depth allineata al color (USB 3, default) → intrinseci **`rgb`**;
|
||
- nessun alignment (fallback USB 2, dove il color è invece *resize*-ato sulla
|
||
griglia depth) → intrinseci **`ir`** (comportamento corretto per quel ramo).
|
||
|
||
Lo stato di allineamento è esposto dal metodo `is_depth_aligned_to_color()`
|
||
(`= self._align is not None`) su `RealSenseD435`. Un fix incondizionato IR→RGB
|
||
avrebbe rotto il percorso USB 2 in modo speculare: per questo è condizionale.
|
||
|
||
**Verifica di non-regressione.** Dopo il fix, sul pixel `(377, 68)`:
|
||
|
||
```bash
|
||
curl -sS "http://localhost:8878/api/measure?u=377&v=68&radius=8"
|
||
# point_m.x ≈ +0.069 (RGB, corretto) — non più +0.109 (IR, buggy)
|
||
```
|
||
|
||
In generale, per ogni pixel `(u, v)` con depth `z` deve valere
|
||
`x ≈ (u − cx_rgb)·z / fx_rgb` (entro la tolleranza di distorsione), e un marker
|
||
di lato noto back-proiettato deve dare lo stesso lato ±5 mm a 0.7 m.
|
||
|
||
> **Nota lato client.** Il workaround temporaneo nel viewport 3D di presa-mano
|
||
> (campo `D435i scale XY` con default `0.65 = fx_ir/fx_rgb`) va riportato a
|
||
> `1.0` ora che il server è corretto.
|
||
|
||
### `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:
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```bash
|
||
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](#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](#workflow-di-sviluppo).
|
||
|
||
---
|
||
|
||
## Struttura del repo
|
||
|
||
```
|
||
RealSense-D435i-G1/
|
||
├── 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
|
||
```
|
||
|
||
---
|
||
|
||
## Repo correlati
|
||
|
||
- **[RealSense-D435i-G1](https://git.humion.it/Labotics-OnGianni/RealSense-D435i-G1)** — questo repo. Deploy G1 (D435i su USB 3.2, BGR8, 30 fps).
|
||
- **[RealSense-D435-H1](https://git.humion.it/Labotics-OnGianni/RealSense-D435-H1)** — repo di origine, deploy storico H1 (D435 su USB 2, YUYV, ~4 fps).
|
||
|
||
Il driver Python è lo stesso su entrambi i repo (almeno al momento del
|
||
fork): distingue il path runtime leggendo `usb_type_descriptor` del
|
||
device. Le differenze tra i due repo sono nel `Dockerfile` (versione
|
||
SDK librealsense, stage di build del frontend) e nel `.env` (identità
|
||
servizio). Patch ai due path runtime andrebbero portate manualmente da
|
||
un repo all'altro quando rilevanti.
|
||
|
||
---
|
||
|
||
## Riferimenti
|
||
|
||
- [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
|