Drop-in replacement for the Nuwa-HP60C camera container, exposing RGB +
aligned depth + intrinsics via REST/WebSocket so the existing
pestello-pose-detector pipeline can consume frames without changes.
Key adaptations for D4xx on USB 2.0 (this robot has no USB 3 port exposed
externally — see STATUS.md):
- pyrealsense2 built from source; the librealsense v2.55.1 install rule
doesn't ship the main .so, so we copy it from the build dir and add an
__init__.py that re-exports the submodule.
- Color stream uses YUYV format on USB 2.x (BGR8/RGB8 time out on this
bus); converted to BGR via cv2.cvtColor in capture().
- rs.align is skipped on USB 2 (incompatible with YUYV); color is
cv2-resized to depth shape for approximate per-pixel alignment.
- Driver auto-detects usb_type_descriptor and falls back to the
full-resolution BGR8 + rs.align pipeline when on USB 3.
Tested: /api/{health,status,discover,rgb,depth,depth/color,capture,
intrinsics} all return 200 with valid payloads at 640x480.
148 lines
4.7 KiB
Python
148 lines
4.7 KiB
Python
import logging
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.responses import FileResponse
|
|
|
|
from src.backend.config import settings
|
|
from src.backend.api.routes import (
|
|
router,
|
|
set_camera as set_routes_camera,
|
|
set_image_saver as set_routes_saver,
|
|
)
|
|
from src.backend.api.websocket import (
|
|
ws_router,
|
|
set_camera as set_ws_camera,
|
|
set_image_saver as set_ws_saver,
|
|
set_detector as set_ws_detector,
|
|
)
|
|
from src.backend.nats_client.connection import nats_manager
|
|
from src.backend.nats_client.handlers import set_camera as set_nats_camera
|
|
from src.backend.services.camera import RealSenseD435
|
|
from src.backend.services.image_saver import ImageSaver
|
|
|
|
try:
|
|
from src.backend.services.detector import Detector
|
|
except Exception:
|
|
Detector = None # YOLO/onnxruntime optional
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Startup
|
|
camera = RealSenseD435(
|
|
width=settings.camera_width,
|
|
height=settings.camera_height,
|
|
fps=settings.camera_fps,
|
|
)
|
|
sdk_ok = camera.init()
|
|
if sdk_ok:
|
|
logger.info("SDK initialized")
|
|
else:
|
|
logger.error(
|
|
"SDK init failed — API will stay up so /api/health can be queried, "
|
|
"but camera endpoints will return SDK_NOT_INITIALIZED"
|
|
)
|
|
|
|
if sdk_ok and settings.camera_auto_open:
|
|
try:
|
|
devices = camera.discover()
|
|
if devices:
|
|
if camera.open(0):
|
|
logger.info(
|
|
f"Auto-opened camera 0 (serial={devices[0].serial})"
|
|
)
|
|
else:
|
|
logger.warning("Auto-open requested but open(0) failed")
|
|
else:
|
|
logger.warning("Auto-open requested but no RealSense devices found")
|
|
except Exception as exc:
|
|
logger.warning(f"Auto-open failed: {exc}")
|
|
|
|
# Inject camera into all layers
|
|
set_routes_camera(camera)
|
|
set_ws_camera(camera)
|
|
set_nats_camera(camera)
|
|
|
|
# Image saver (snapshot + continuous recording to images_dir)
|
|
saver = ImageSaver(Path(settings.images_dir))
|
|
set_routes_saver(saver)
|
|
set_ws_saver(saver)
|
|
logger.info(f"Image saver ready: {settings.images_dir}")
|
|
|
|
# YOLO detector (optional — disabled if onnxruntime or model file is missing)
|
|
if Detector is not None:
|
|
try:
|
|
detector = Detector(
|
|
settings.yolo_onnx_path,
|
|
settings.yolo_class_list,
|
|
settings.yolo_conf_default,
|
|
)
|
|
set_ws_detector(detector)
|
|
except FileNotFoundError as exc:
|
|
logger.warning(f"YOLO detector disabled (model not found): {exc}")
|
|
except Exception as exc:
|
|
logger.warning(f"YOLO detector disabled: {exc}")
|
|
else:
|
|
logger.info("YOLO detector disabled (onnxruntime not installed)")
|
|
|
|
# Connect NATS (non-blocking, graceful if unavailable)
|
|
await nats_manager.connect()
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
await nats_manager.disconnect()
|
|
camera.close()
|
|
camera.deinit()
|
|
logger.info("SDK deinitialized")
|
|
|
|
|
|
app = FastAPI(
|
|
title=settings.service_name,
|
|
description="REST API + WebSocket for Intel RealSense D4xx depth camera (pyrealsense2)",
|
|
version="1.0.0",
|
|
docs_url="/apidocs",
|
|
redoc_url="/redoc",
|
|
openapi_url="/openapi.json",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origins_list,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(router, prefix="/api")
|
|
app.include_router(ws_router)
|
|
|
|
# Serve React frontend at /gui (built files in src/frontend/react/dist)
|
|
_GUI_DIR = Path(__file__).parent.parent / "frontend" / "react" / "dist"
|
|
if _GUI_DIR.is_dir():
|
|
app.mount("/gui", StaticFiles(directory=str(_GUI_DIR), html=True), name="gui")
|
|
logger.info(f"React GUI mounted at /gui from {_GUI_DIR}")
|
|
|
|
|
|
# Public health endpoint (no API key required) for Docker healthchecks
|
|
@app.get("/api/health", summary="Health check", tags=["system"])
|
|
async def health():
|
|
"""Service health check — no API key required."""
|
|
from src.backend.models.api import ok
|
|
from src.backend.nats_client.connection import nats_manager
|
|
from src.backend.api.routes import _camera
|
|
|
|
return ok({
|
|
"status": "ok",
|
|
"sdk_initialized": _camera.is_initialized() if _camera else False,
|
|
"camera_open": _camera.is_open() if _camera else False,
|
|
"nats_connected": nats_manager.is_connected,
|
|
})
|