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:
commit
ac573505b7
121 files changed
+67199
No files matched your search
@@ -0,0 +1,110 @@
|
||||
"""FastAPI entrypoint for presa-mano."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from .api import (
|
||||
agent_routes,
|
||||
calibration_routes,
|
||||
config_routes,
|
||||
grasp_routes,
|
||||
home_routes,
|
||||
perception_routes,
|
||||
posture_routes,
|
||||
robot_routes,
|
||||
wrist_routes,
|
||||
)
|
||||
from .clients import G1StatusError
|
||||
from .config import load_config
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||
|
||||
app = FastAPI(
|
||||
title="presa-mano",
|
||||
description="Hand-eye calibration + object grasping pipeline (Nuwa + ArUco + YOLO + G1)",
|
||||
version="0.1.0",
|
||||
docs_url="/apidocs",
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
app.include_router(config_routes.router, prefix="/api/config", tags=["config"])
|
||||
app.include_router(home_routes.router, prefix="/api/home", tags=["home"])
|
||||
app.include_router(agent_routes.router, prefix="/api/agent", tags=["agent"])
|
||||
app.include_router(calibration_routes.router, prefix="/api/calibration", tags=["calibration"])
|
||||
app.include_router(perception_routes.router, prefix="/api/perception", tags=["perception"])
|
||||
app.include_router(grasp_routes.router, prefix="/api/grasp", tags=["grasp"])
|
||||
app.include_router(wrist_routes.router, prefix="/api/wrist", tags=["wrist"])
|
||||
app.include_router(posture_routes.router, prefix="/api/posture", tags=["posture"])
|
||||
app.include_router(robot_routes.router, prefix="/api/robot", tags=["robot"])
|
||||
|
||||
WEB_DIR = Path(__file__).resolve().parents[2] / "web"
|
||||
if WEB_DIR.exists():
|
||||
app.mount("/static", StaticFiles(directory=str(WEB_DIR)), name="static")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def _startup() -> None:
|
||||
load_config()
|
||||
_push_intrinsics_to_marker_finder()
|
||||
|
||||
|
||||
def _push_intrinsics_to_marker_finder() -> None:
|
||||
"""Read live RGB intrinsics from Nuwa and push them as the new default
|
||||
on marker-finder. After this every `/detect` produces poses computed with
|
||||
the correct intrinsics, so callers don't have to override per-request.
|
||||
|
||||
Best-effort: if Nuwa or marker-finder is down we just log and continue —
|
||||
presa-mano must come up regardless. Callers can also pass intrinsics
|
||||
per-request as a fallback.
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
try:
|
||||
from .api import _deps
|
||||
from .perception.aruco_pose import nuwa_intrinsics_to_opencv_payload
|
||||
intr = _deps.nuwa().intrinsics()
|
||||
payload = nuwa_intrinsics_to_opencv_payload(intr)
|
||||
_deps.marker().set_intrinsics(
|
||||
camera_matrix=payload["camera_matrix"],
|
||||
dist_coeffs=payload["dist_coeffs"],
|
||||
persist=False,
|
||||
)
|
||||
log.info("pushed Nuwa intrinsics to marker-finder")
|
||||
except Exception as e:
|
||||
log.warning("could not push Nuwa intrinsics to marker-finder: %s "
|
||||
"(callers can still pass them per-request)", e)
|
||||
|
||||
|
||||
@app.exception_handler(G1StatusError)
|
||||
def _g1_status_error(_: Request, exc: G1StatusError) -> JSONResponse:
|
||||
# Forward the upstream status code verbatim — 409 (conflict, arms busy),
|
||||
# 412 (precondition: DDS not ready), etc. are actionable by the caller and
|
||||
# should not be flattened into a generic 502.
|
||||
return JSONResponse(
|
||||
status_code=exc.status,
|
||||
content={"detail": f"G1 {exc.path}: {exc.detail}"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health", tags=["meta"])
|
||||
def health() -> dict:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
def index() -> HTMLResponse:
|
||||
idx = WEB_DIR / "index.html"
|
||||
html = idx.read_text(encoding="utf-8")
|
||||
# Cache-bust app.js and style.css with their mtime so a rebuild forces refetch.
|
||||
for name in ("app.js", "autocal3d.js", "servo.js", "style.css"):
|
||||
f = WEB_DIR / name
|
||||
if f.exists():
|
||||
v = int(f.stat().st_mtime)
|
||||
html = html.replace(f"/static/{name}", f"/static/{name}?v={v}")
|
||||
# Also tell the browser not to cache the shell itself.
|
||||
return HTMLResponse(html, headers={"Cache-Control": "no-store"})
|
||||
Reference in new issue
Block a user