feat: initial commit — orchestratore presa-mano con AI assistant + dual-robot

Pipeline completa per calibrazione hand-eye e grasp planning su robot
Unitree G1/H1 con camera depth Nuwa-HP60C. Esposta come FastAPI + GUI vanilla
JS, con assistente AI (OpenRouter + tool calling curato) per pilotare il
sistema in linguaggio naturale.

Architettura:
- FastAPI orchestrator (porta da .env, default 8890) che coordina 4
  microservizi upstream: Nuwa-HP60C, marker-finder, YOLO, robot bridge.
- Dual-robot: services.g1 e services.h1 in config.yaml; switchabili a
  runtime via PUT /api/home/robot.
- Calibrazione hand-eye eye-to-hand split per-side (right/left).
- Marker pose: marker-finder riceve gli intrinseci RGB della Nuwa al boot,
  PnP server-side con quei dati. Nessun re-PnP locale richiesto.
- Depth-to-RGB: registrazione iterativa IR↔RGB per back-projection 3D
  corretta (Nuwa /api/measure usa coords IR; il nostro /api/perception/
  measure_rgb prende pixel RGB e ritorna 3D nel frame RGB OpenCV).
- Correzione affine sul depth bias (calibrabile via marker come
  ground-truth) — riduce errore Z da ~6% a ~1.5% nei test.
- AI assistant: 16 tool curati (move_wrist_*, hand_action, get_marker[_3d],
  detect_object, calibration_*, set_active_robot…), system prompt
  dinamico che riflette robot attivo + marker config + addendum
  customizzabile da GUI. Temperature di default 0.1 per evitare invenzioni.

GUI (7 tab):
- Home: stato comunicazione device + selettore robot attivo G1/H1
- Assistente AI: chat con OpenRouter, history persistita lato browser,
  task riutilizzabili (prompt salvati), trace tool calls
- Impostazioni AI: modello da combo curata, temperature, max iterations,
  prompt addendum, anteprima system prompt
- Calibrazione, Auto-Calibrazione, TrovaUtensile, Servo Visuale, Vista 3D,
  Movimento EE — invariate dalle versioni precedenti

Setup:
- Variabili in presa-mano/.env (template in .env.example)
- OPENROUTER_API_KEY richiesta solo per il tab Assistente AI
- PRESA_MANO_PORT configurabile

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal PastroandClaude Opus 4.7 committed 2026-05-06 13:28:23 +02:00
commit ac573505b7
121 files changed
+67199

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
"""Configuration loader with runtime override support."""
from __future__ import annotations
import os
import threading
from copy import deepcopy
from pathlib import Path
from typing import Any
import yaml
_LOCK = threading.RLock()
_STATE: dict[str, Any] = {"config": None, "path": None}
def _default_config_path() -> Path:
env = os.environ.get("PRESA_MANO_CONFIG")
if env:
return Path(env)
return Path(__file__).resolve().parents[2] / "config.yaml"
def data_dir() -> Path:
env = os.environ.get("PRESA_MANO_DATA")
p = Path(env) if env else _default_config_path().parent / "data"
p.mkdir(parents=True, exist_ok=True)
return p
def load_config(path: Path | None = None) -> dict[str, Any]:
with _LOCK:
if _STATE["config"] is not None and path is None:
return deepcopy(_STATE["config"])
cfg_path = path or _default_config_path()
with open(cfg_path, "r") as f:
cfg = yaml.safe_load(f) or {}
_STATE["config"] = cfg
_STATE["path"] = cfg_path
return deepcopy(cfg)
def get_config() -> dict[str, Any]:
with _LOCK:
if _STATE["config"] is None:
load_config()
return deepcopy(_STATE["config"])
def update_config(patch: dict[str, Any]) -> dict[str, Any]:
"""Merge a patch into the in-memory config and persist to disk."""
with _LOCK:
if _STATE["config"] is None:
load_config()
merged = _deep_merge(_STATE["config"], patch)
_STATE["config"] = merged
if _STATE["path"]:
with open(_STATE["path"], "w") as f:
yaml.safe_dump(merged, f, sort_keys=False)
return deepcopy(merged)
def _deep_merge(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
out = deepcopy(base)
for k, v in patch.items():
if isinstance(v, dict) and isinstance(out.get(k), dict):
out[k] = _deep_merge(out[k], v)
else:
out[k] = v
return out