Files
Presa-Mano/src/presa_mano/main.py
T
Adriano Dal PastroandClaude Opus 4.7 eb53f30e23 refactor: remove visual-servo feature
The marker-relative servo (endpoints /api/grasp/visual_servo/{plan,step,
plane/*}, helpers in grasp_routes.py, the bulk of grasp/visual_servo.py,
web/servo.js, and the Servo Visuale GUI tab) is gone. Plane fitting now
lives in /api/work_plane/* and perception/work_plane.py; Task 5 wires the
new Calibrazione Piano tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:04:00 +02:00

113 lines
4.1 KiB
Python

"""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,
work_plane_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"])
app.include_router(work_plane_routes.router, prefix="/api/work_plane", tags=["work_plane"])
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", "work_plane.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"})