Initial commit: RealSense D435 API server

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.
This commit is contained in:
robot committed 2026-05-19 00:58:23 +02:00
commit 07d95792e3
58 files changed
+6987

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
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,
})