"""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, models_routes, perception_routes, posture_routes, right_arm_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(models_routes.router, prefix="/api/models", tags=["models"]) 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(right_arm_routes.router, prefix="/api/right_arm", tags=["right_arm"]) 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"})