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, })