Refactor API route setup and enhance modularity
- Consolidated API route definitions by registering routes from separate modules for better organization and maintainability. - Removed redundant route definitions from the APIServer class, improving code clarity. - Updated camera monitoring and recording modules to utilize a shared context manager for suppressing camera SDK errors, enhancing error handling. - Adjusted timeout settings in camera operations for improved reliability during frame capture. - Enhanced logging and error handling across camera operations to facilitate better debugging and monitoring.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
API route modules.
|
||||
"""
|
||||
|
||||
from .system_routes import register_system_routes
|
||||
from .camera_routes import register_camera_routes
|
||||
from .recording_routes import register_recording_routes
|
||||
from .mqtt_routes import register_mqtt_routes
|
||||
from .storage_routes import register_storage_routes
|
||||
from .auto_recording_routes import register_auto_recording_routes
|
||||
from .recordings_routes import register_recordings_routes
|
||||
|
||||
__all__ = [
|
||||
"register_system_routes",
|
||||
"register_camera_routes",
|
||||
"register_recording_routes",
|
||||
"register_mqtt_routes",
|
||||
"register_storage_routes",
|
||||
"register_auto_recording_routes",
|
||||
"register_recordings_routes",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Auto-recording configuration API routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from ...core.config import Config
|
||||
from ...core.state_manager import StateManager
|
||||
from ...recording.auto_manager import AutoRecordingManager
|
||||
from ..models import AutoRecordingConfigResponse, AutoRecordingStatusResponse
|
||||
|
||||
|
||||
def register_auto_recording_routes(
|
||||
app: FastAPI,
|
||||
config: Config,
|
||||
state_manager: StateManager,
|
||||
auto_recording_manager: Optional[AutoRecordingManager],
|
||||
logger: logging.Logger
|
||||
):
|
||||
"""Register auto-recording configuration routes"""
|
||||
|
||||
@app.post("/cameras/{camera_name}/auto-recording/enable", response_model=AutoRecordingConfigResponse)
|
||||
async def enable_auto_recording(camera_name: str):
|
||||
"""Enable auto-recording for a camera"""
|
||||
try:
|
||||
if not auto_recording_manager:
|
||||
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||
|
||||
# Update camera configuration
|
||||
camera_config = config.get_camera_by_name(camera_name)
|
||||
if not camera_config:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
camera_config.auto_start_recording_enabled = True
|
||||
config.save_config()
|
||||
|
||||
# Update camera status in state manager
|
||||
camera_info = state_manager.get_camera_status(camera_name)
|
||||
if camera_info:
|
||||
camera_info.auto_recording_enabled = True
|
||||
|
||||
return AutoRecordingConfigResponse(
|
||||
success=True,
|
||||
message=f"Auto-recording enabled for camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
enabled=True
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error enabling auto-recording for camera {camera_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/auto-recording/disable", response_model=AutoRecordingConfigResponse)
|
||||
async def disable_auto_recording(camera_name: str):
|
||||
"""Disable auto-recording for a camera"""
|
||||
try:
|
||||
if not auto_recording_manager:
|
||||
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||
|
||||
# Update camera configuration
|
||||
camera_config = config.get_camera_by_name(camera_name)
|
||||
if not camera_config:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
camera_config.auto_start_recording_enabled = False
|
||||
config.save_config()
|
||||
|
||||
# Update camera status in state manager
|
||||
camera_info = state_manager.get_camera_status(camera_name)
|
||||
if camera_info:
|
||||
camera_info.auto_recording_enabled = False
|
||||
camera_info.auto_recording_active = False
|
||||
|
||||
return AutoRecordingConfigResponse(
|
||||
success=True,
|
||||
message=f"Auto-recording disabled for camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
enabled=False
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error disabling auto-recording for camera {camera_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/auto-recording/status", response_model=AutoRecordingStatusResponse)
|
||||
async def get_auto_recording_status():
|
||||
"""Get auto-recording manager status"""
|
||||
try:
|
||||
if not auto_recording_manager:
|
||||
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||
|
||||
status = auto_recording_manager.get_status()
|
||||
return AutoRecordingStatusResponse(**status)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting auto-recording status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
"""
|
||||
Camera-related API routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from ...core.config import Config
|
||||
from ...core.state_manager import StateManager
|
||||
from ...camera.manager import CameraManager
|
||||
from ..models import (
|
||||
CameraStatusResponse,
|
||||
CameraTestResponse,
|
||||
CameraConfigResponse,
|
||||
CameraConfigRequest,
|
||||
CameraRecoveryResponse,
|
||||
MachineStatusResponse,
|
||||
)
|
||||
|
||||
|
||||
def register_camera_routes(
|
||||
app: FastAPI,
|
||||
config: Config,
|
||||
state_manager: StateManager,
|
||||
camera_manager: CameraManager,
|
||||
logger: logging.Logger
|
||||
):
|
||||
"""Register camera-related routes"""
|
||||
|
||||
@app.get("/machines", response_model=Dict[str, MachineStatusResponse])
|
||||
async def get_machines():
|
||||
"""Get all machine statuses"""
|
||||
try:
|
||||
machines = state_manager.get_all_machines()
|
||||
return {
|
||||
name: MachineStatusResponse(
|
||||
name=machine.name,
|
||||
state=machine.state.value,
|
||||
last_updated=machine.last_updated.isoformat(),
|
||||
last_message=machine.last_message,
|
||||
mqtt_topic=machine.mqtt_topic
|
||||
)
|
||||
for name, machine in machines.items()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting machines: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/cameras", response_model=Dict[str, CameraStatusResponse])
|
||||
async def get_cameras():
|
||||
"""Get all camera statuses"""
|
||||
try:
|
||||
cameras = state_manager.get_all_cameras()
|
||||
return {
|
||||
name: CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None,
|
||||
auto_recording_enabled=camera.auto_recording_enabled,
|
||||
auto_recording_active=camera.auto_recording_active,
|
||||
auto_recording_failure_count=camera.auto_recording_failure_count,
|
||||
auto_recording_last_attempt=camera.auto_recording_last_attempt.isoformat() if camera.auto_recording_last_attempt else None,
|
||||
auto_recording_last_error=camera.auto_recording_last_error,
|
||||
)
|
||||
for name, camera in cameras.items()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cameras: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/cameras/{camera_name}/status", response_model=CameraStatusResponse)
|
||||
async def get_camera_status(camera_name: str):
|
||||
"""Get specific camera status"""
|
||||
try:
|
||||
camera = state_manager.get_camera_status(camera_name)
|
||||
if not camera:
|
||||
raise HTTPException(status_code=404, detail=f"Camera not found: {camera_name}")
|
||||
|
||||
return CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting camera status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/test-connection", response_model=CameraTestResponse)
|
||||
async def test_camera_connection(camera_name: str):
|
||||
"""Test camera connection"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.test_camera_connection(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraTestResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} connection test passed",
|
||||
camera_name=camera_name
|
||||
)
|
||||
else:
|
||||
return CameraTestResponse(
|
||||
success=False,
|
||||
message=f"Camera {camera_name} connection test failed",
|
||||
camera_name=camera_name
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing camera connection: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/cameras/{camera_name}/stream")
|
||||
async def camera_stream(camera_name: str):
|
||||
"""Get live MJPEG stream from camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
# Get camera streamer
|
||||
streamer = camera_manager.get_camera_streamer(camera_name)
|
||||
if not streamer:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
# Start streaming if not already active
|
||||
if not streamer.is_streaming():
|
||||
success = streamer.start_streaming()
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start streaming for camera {camera_name}")
|
||||
|
||||
# Return MJPEG stream
|
||||
return StreamingResponse(streamer.get_frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/start-stream")
|
||||
async def start_camera_stream(camera_name: str):
|
||||
"""Start streaming for a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.start_camera_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Started streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to start streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/stop-stream")
|
||||
async def stop_camera_stream(camera_name: str):
|
||||
"""Stop streaming for a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.stop_camera_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Stopped streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to stop streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/start-rtsp")
|
||||
async def start_camera_rtsp_stream(camera_name: str):
|
||||
"""Start RTSP streaming for a camera to MediaMTX"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.start_camera_rtsp_streaming(camera_name)
|
||||
if success:
|
||||
rtsp_url = f"rtsp://{os.getenv('MEDIAMTX_HOST', 'localhost')}:{os.getenv('MEDIAMTX_RTSP_PORT', '8554')}/{camera_name}"
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Started RTSP streaming for camera {camera_name}",
|
||||
"rtsp_url": rtsp_url
|
||||
}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to start RTSP streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting RTSP stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/stop-rtsp")
|
||||
async def stop_camera_rtsp_stream(camera_name: str):
|
||||
"""Stop RTSP streaming for a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.stop_camera_rtsp_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Stopped RTSP streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to stop RTSP streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping RTSP stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/cameras/{camera_name}/config", response_model=CameraConfigResponse)
|
||||
async def get_camera_config(camera_name: str):
|
||||
"""Get camera configuration"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
camera_config = config.get_camera_by_name(camera_name)
|
||||
if not camera_config:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
return CameraConfigResponse(
|
||||
name=camera_config.name,
|
||||
machine_topic=camera_config.machine_topic,
|
||||
storage_path=camera_config.storage_path,
|
||||
enabled=camera_config.enabled,
|
||||
# Auto-recording settings
|
||||
auto_start_recording_enabled=camera_config.auto_start_recording_enabled,
|
||||
auto_recording_max_retries=camera_config.auto_recording_max_retries,
|
||||
auto_recording_retry_delay_seconds=camera_config.auto_recording_retry_delay_seconds,
|
||||
# Basic settings
|
||||
exposure_ms=camera_config.exposure_ms,
|
||||
gain=camera_config.gain,
|
||||
target_fps=camera_config.target_fps,
|
||||
# Video recording settings
|
||||
video_format=camera_config.video_format,
|
||||
video_codec=camera_config.video_codec,
|
||||
video_quality=camera_config.video_quality,
|
||||
# Image Quality Settings
|
||||
sharpness=camera_config.sharpness,
|
||||
contrast=camera_config.contrast,
|
||||
saturation=camera_config.saturation,
|
||||
gamma=camera_config.gamma,
|
||||
# Noise Reduction
|
||||
noise_filter_enabled=camera_config.noise_filter_enabled,
|
||||
denoise_3d_enabled=camera_config.denoise_3d_enabled,
|
||||
# Color Settings
|
||||
auto_white_balance=camera_config.auto_white_balance,
|
||||
color_temperature_preset=camera_config.color_temperature_preset,
|
||||
# Manual White Balance RGB Gains
|
||||
wb_red_gain=camera_config.wb_red_gain,
|
||||
wb_green_gain=camera_config.wb_green_gain,
|
||||
wb_blue_gain=camera_config.wb_blue_gain,
|
||||
# Advanced Settings
|
||||
anti_flicker_enabled=camera_config.anti_flicker_enabled,
|
||||
light_frequency=camera_config.light_frequency,
|
||||
bit_depth=camera_config.bit_depth,
|
||||
# HDR Settings
|
||||
hdr_enabled=camera_config.hdr_enabled,
|
||||
hdr_gain_mode=camera_config.hdr_gain_mode,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.put("/cameras/{camera_name}/config")
|
||||
async def update_camera_config(camera_name: str, request: CameraConfigRequest):
|
||||
"""Update camera configuration"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
# Convert request to dict, excluding None values
|
||||
config_updates = {k: v for k, v in request.dict().items() if v is not None}
|
||||
|
||||
if not config_updates:
|
||||
raise HTTPException(status_code=400, detail="No configuration updates provided")
|
||||
|
||||
success = camera_manager.update_camera_config(camera_name, **config_updates)
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Camera {camera_name} configuration updated",
|
||||
"updated_settings": list(config_updates.keys())
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found or update failed")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/apply-config")
|
||||
async def apply_camera_config(camera_name: str):
|
||||
"""Apply current configuration to active camera (requires camera restart)"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.apply_camera_config(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Configuration applied to camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to apply configuration to camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/reconnect", response_model=CameraRecoveryResponse)
|
||||
async def reconnect_camera(camera_name: str):
|
||||
"""Reconnect to a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.reconnect_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} reconnected successfully",
|
||||
camera_name=camera_name,
|
||||
operation="reconnect"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to reconnect camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="reconnect"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error reconnecting camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/restart-grab", response_model=CameraRecoveryResponse)
|
||||
async def restart_camera_grab(camera_name: str):
|
||||
"""Restart camera grab process"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.restart_camera_grab(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} grab process restarted successfully",
|
||||
camera_name=camera_name,
|
||||
operation="restart-grab"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to restart grab process for camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="restart-grab"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error restarting camera grab: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/reset-timestamp", response_model=CameraRecoveryResponse)
|
||||
async def reset_camera_timestamp(camera_name: str):
|
||||
"""Reset camera timestamp"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.reset_camera_timestamp(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} timestamp reset successfully",
|
||||
camera_name=camera_name,
|
||||
operation="reset-timestamp"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to reset timestamp for camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="reset-timestamp"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting camera timestamp: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/full-reset", response_model=CameraRecoveryResponse)
|
||||
async def full_reset_camera(camera_name: str):
|
||||
"""Perform full camera reset (uninitialize and reinitialize)"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.full_reset_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} full reset completed successfully",
|
||||
camera_name=camera_name,
|
||||
operation="full-reset"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to perform full reset for camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="full-reset"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error performing full camera reset: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/reinitialize", response_model=CameraRecoveryResponse)
|
||||
async def reinitialize_camera(camera_name: str):
|
||||
"""Reinitialize a failed camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.reinitialize_failed_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(
|
||||
success=True,
|
||||
message=f"Camera {camera_name} reinitialized successfully",
|
||||
camera_name=camera_name,
|
||||
operation="reinitialize"
|
||||
)
|
||||
else:
|
||||
return CameraRecoveryResponse(
|
||||
success=False,
|
||||
message=f"Failed to reinitialize camera {camera_name}",
|
||||
camera_name=camera_name,
|
||||
operation="reinitialize"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error reinitializing camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/debug/camera-manager")
|
||||
async def debug_camera_manager():
|
||||
"""Debug endpoint to check camera manager state"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
return {"error": "Camera manager not available"}
|
||||
|
||||
return {
|
||||
"available_cameras": len(camera_manager.available_cameras),
|
||||
"camera_recorders": list(camera_manager.camera_recorders.keys()),
|
||||
"camera_streamers": list(camera_manager.camera_streamers.keys()),
|
||||
"streamer_states": {
|
||||
name: {
|
||||
"exists": streamer is not None,
|
||||
"is_streaming": streamer.is_streaming() if streamer else False,
|
||||
"streaming": getattr(streamer, 'streaming', False) if streamer else False
|
||||
}
|
||||
for name, streamer in camera_manager.camera_streamers.items()
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
MQTT-related API routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from ...core.state_manager import StateManager
|
||||
from ...mqtt.client import MQTTClient
|
||||
from ..models import MQTTStatusResponse, MQTTEventsHistoryResponse, MQTTEventResponse
|
||||
|
||||
|
||||
def register_mqtt_routes(
|
||||
app: FastAPI,
|
||||
mqtt_client: MQTTClient,
|
||||
state_manager: StateManager,
|
||||
logger: logging.Logger
|
||||
):
|
||||
"""Register MQTT-related routes"""
|
||||
|
||||
@app.get("/mqtt/status", response_model=MQTTStatusResponse)
|
||||
async def get_mqtt_status():
|
||||
"""Get MQTT client status and statistics"""
|
||||
try:
|
||||
status = mqtt_client.get_status()
|
||||
return MQTTStatusResponse(
|
||||
connected=status["connected"],
|
||||
broker_host=status["broker_host"],
|
||||
broker_port=status["broker_port"],
|
||||
subscribed_topics=status["subscribed_topics"],
|
||||
last_message_time=status["last_message_time"],
|
||||
message_count=status["message_count"],
|
||||
error_count=status["error_count"],
|
||||
uptime_seconds=status["uptime_seconds"]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting MQTT status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/mqtt/events", response_model=MQTTEventsHistoryResponse)
|
||||
async def get_mqtt_events(
|
||||
limit: int = Query(default=5, ge=1, le=50, description="Number of recent events to retrieve")
|
||||
):
|
||||
"""Get recent MQTT events history"""
|
||||
try:
|
||||
events = state_manager.get_recent_mqtt_events(limit)
|
||||
total_events = state_manager.get_mqtt_event_count()
|
||||
|
||||
# Convert events to response format
|
||||
event_responses = [
|
||||
MQTTEventResponse(
|
||||
machine_name=event.machine_name,
|
||||
topic=event.topic,
|
||||
payload=event.payload,
|
||||
normalized_state=event.normalized_state,
|
||||
timestamp=event.timestamp.isoformat(),
|
||||
message_number=event.message_number
|
||||
)
|
||||
for event in events
|
||||
]
|
||||
|
||||
last_updated = events[0].timestamp.isoformat() if events else None
|
||||
|
||||
return MQTTEventsHistoryResponse(
|
||||
events=event_responses,
|
||||
total_events=total_events,
|
||||
last_updated=last_updated
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting MQTT events: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Recording-related API routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from ...camera.manager import CameraManager
|
||||
from ..models import StartRecordingResponse, StopRecordingResponse, StartRecordingRequest
|
||||
from ...core.timezone_utils import format_filename_timestamp
|
||||
|
||||
|
||||
def register_recording_routes(
|
||||
app: FastAPI,
|
||||
camera_manager: CameraManager,
|
||||
logger: logging.Logger
|
||||
):
|
||||
"""Register recording-related routes"""
|
||||
|
||||
@app.post("/cameras/{camera_name}/start-recording", response_model=StartRecordingResponse)
|
||||
async def start_recording(camera_name: str, request: StartRecordingRequest):
|
||||
"""Manually start recording for a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.manual_start_recording(
|
||||
camera_name=camera_name,
|
||||
filename=request.filename,
|
||||
exposure_ms=request.exposure_ms,
|
||||
gain=request.gain,
|
||||
fps=request.fps
|
||||
)
|
||||
|
||||
if success:
|
||||
# Get the actual filename that was used (with datetime prefix)
|
||||
actual_filename = request.filename
|
||||
if request.filename:
|
||||
timestamp = format_filename_timestamp()
|
||||
actual_filename = f"{timestamp}_{request.filename}"
|
||||
|
||||
return StartRecordingResponse(
|
||||
success=True,
|
||||
message=f"Recording started for {camera_name}",
|
||||
filename=actual_filename
|
||||
)
|
||||
else:
|
||||
return StartRecordingResponse(
|
||||
success=False,
|
||||
message=f"Failed to start recording for {camera_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/cameras/{camera_name}/stop-recording", response_model=StopRecordingResponse)
|
||||
async def stop_recording(camera_name: str):
|
||||
"""Manually stop recording for a camera"""
|
||||
try:
|
||||
if not camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = camera_manager.manual_stop_recording(camera_name)
|
||||
|
||||
if success:
|
||||
return StopRecordingResponse(
|
||||
success=True,
|
||||
message=f"Recording stopped for {camera_name}"
|
||||
)
|
||||
else:
|
||||
return StopRecordingResponse(
|
||||
success=False,
|
||||
message=f"Failed to stop recording for {camera_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Recording session listing API routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from ...core.state_manager import StateManager
|
||||
from ..models import RecordingInfoResponse
|
||||
|
||||
|
||||
def register_recordings_routes(
|
||||
app: FastAPI,
|
||||
state_manager: StateManager,
|
||||
logger: logging.Logger
|
||||
):
|
||||
"""Register recordings listing routes"""
|
||||
|
||||
@app.get("/recordings", response_model=Dict[str, RecordingInfoResponse])
|
||||
async def get_recordings():
|
||||
"""Get all recording sessions"""
|
||||
try:
|
||||
recordings = state_manager.get_all_recordings()
|
||||
return {
|
||||
rid: RecordingInfoResponse(
|
||||
camera_name=recording.camera_name,
|
||||
filename=recording.filename,
|
||||
start_time=recording.start_time.isoformat(),
|
||||
state=recording.state.value,
|
||||
end_time=recording.end_time.isoformat() if recording.end_time else None,
|
||||
file_size_bytes=recording.file_size_bytes,
|
||||
frame_count=recording.frame_count,
|
||||
duration_seconds=(recording.end_time - recording.start_time).total_seconds() if recording.end_time else None,
|
||||
error_message=recording.error_message,
|
||||
)
|
||||
for rid, recording in recordings.items()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting recordings: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Storage-related API routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from ...storage.manager import StorageManager
|
||||
from ..models import StorageStatsResponse, FileListResponse, CleanupResponse, FileListRequest, CleanupRequest
|
||||
|
||||
|
||||
def register_storage_routes(
|
||||
app: FastAPI,
|
||||
storage_manager: StorageManager,
|
||||
logger: logging.Logger
|
||||
):
|
||||
"""Register storage-related routes"""
|
||||
|
||||
@app.get("/storage/stats", response_model=StorageStatsResponse)
|
||||
async def get_storage_stats():
|
||||
"""Get storage statistics"""
|
||||
try:
|
||||
stats = storage_manager.get_storage_statistics()
|
||||
return StorageStatsResponse(**stats)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting storage stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/storage/files", response_model=FileListResponse)
|
||||
async def get_files(request: FileListRequest):
|
||||
"""Get list of recording files"""
|
||||
try:
|
||||
start_date = None
|
||||
end_date = None
|
||||
|
||||
if request.start_date:
|
||||
start_date = datetime.fromisoformat(request.start_date)
|
||||
if request.end_date:
|
||||
end_date = datetime.fromisoformat(request.end_date)
|
||||
|
||||
files = storage_manager.get_recording_files(
|
||||
camera_name=request.camera_name,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
limit=request.limit
|
||||
)
|
||||
|
||||
return FileListResponse(files=files, total_count=len(files))
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting files: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/storage/cleanup", response_model=CleanupResponse)
|
||||
async def cleanup_storage(request: CleanupRequest):
|
||||
"""Clean up old storage files"""
|
||||
try:
|
||||
result = storage_manager.cleanup_old_files(request.max_age_days)
|
||||
return CleanupResponse(**result)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cleanup: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
System-related API routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
from ...core.config import Config
|
||||
from ...core.state_manager import StateManager
|
||||
from ...video.integration import VideoModule
|
||||
from ..models import SuccessResponse, SystemStatusResponse
|
||||
|
||||
|
||||
def register_system_routes(
|
||||
app: FastAPI,
|
||||
state_manager: StateManager,
|
||||
video_module: Optional[VideoModule],
|
||||
server_start_time: datetime,
|
||||
logger: logging.Logger
|
||||
):
|
||||
"""Register system-related routes"""
|
||||
|
||||
@app.get("/", response_model=SuccessResponse)
|
||||
async def root():
|
||||
return SuccessResponse(message="USDA Vision Camera System API")
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
||||
|
||||
@app.get("/system/status", response_model=SystemStatusResponse)
|
||||
async def get_system_status():
|
||||
"""Get overall system status"""
|
||||
try:
|
||||
summary = state_manager.get_system_summary()
|
||||
uptime = (datetime.now() - server_start_time).total_seconds()
|
||||
|
||||
return SystemStatusResponse(
|
||||
system_started=summary["system_started"],
|
||||
mqtt_connected=summary["mqtt_connected"],
|
||||
last_mqtt_message=summary["last_mqtt_message"],
|
||||
machines=summary["machines"],
|
||||
cameras=summary["cameras"],
|
||||
active_recordings=summary["active_recordings"],
|
||||
total_recordings=summary["total_recordings"],
|
||||
uptime_seconds=uptime
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting system status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/system/video-module")
|
||||
async def get_video_module_status():
|
||||
"""Get video module status and configuration"""
|
||||
try:
|
||||
if video_module:
|
||||
status = video_module.get_module_status()
|
||||
status["enabled"] = True
|
||||
return status
|
||||
else:
|
||||
return {"enabled": False, "error": "Video module not initialized"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting video module status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -23,6 +23,15 @@ from ..core.events import EventSystem, EventType, Event
|
||||
from ..storage.manager import StorageManager
|
||||
from ..video.integration import create_video_module, VideoModule
|
||||
from .models import *
|
||||
from .routes import (
|
||||
register_system_routes,
|
||||
register_camera_routes,
|
||||
register_recording_routes,
|
||||
register_mqtt_routes,
|
||||
register_storage_routes,
|
||||
register_auto_recording_routes,
|
||||
register_recordings_routes,
|
||||
)
|
||||
|
||||
|
||||
class WebSocketManager:
|
||||
@@ -114,594 +123,58 @@ class APIServer:
|
||||
|
||||
def _setup_routes(self):
|
||||
"""Setup API routes"""
|
||||
|
||||
@self.app.get("/", response_model=SuccessResponse)
|
||||
async def root():
|
||||
return SuccessResponse(message="USDA Vision Camera System API")
|
||||
|
||||
@self.app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
||||
|
||||
@self.app.get("/system/status", response_model=SystemStatusResponse)
|
||||
async def get_system_status():
|
||||
"""Get overall system status"""
|
||||
try:
|
||||
summary = self.state_manager.get_system_summary()
|
||||
uptime = (datetime.now() - self.server_start_time).total_seconds()
|
||||
|
||||
return SystemStatusResponse(system_started=summary["system_started"], mqtt_connected=summary["mqtt_connected"], last_mqtt_message=summary["last_mqtt_message"], machines=summary["machines"], cameras=summary["cameras"], active_recordings=summary["active_recordings"], total_recordings=summary["total_recordings"], uptime_seconds=uptime)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting system status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/system/video-module")
|
||||
async def get_video_module_status():
|
||||
"""Get video module status and configuration"""
|
||||
try:
|
||||
if self.video_module:
|
||||
status = self.video_module.get_module_status()
|
||||
status["enabled"] = True
|
||||
return status
|
||||
else:
|
||||
return {"enabled": False, "error": "Video module not initialized"}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting video module status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/machines", response_model=Dict[str, MachineStatusResponse])
|
||||
async def get_machines():
|
||||
"""Get all machine statuses"""
|
||||
try:
|
||||
machines = self.state_manager.get_all_machines()
|
||||
return {name: MachineStatusResponse(name=machine.name, state=machine.state.value, last_updated=machine.last_updated.isoformat(), last_message=machine.last_message, mqtt_topic=machine.mqtt_topic) for name, machine in machines.items()}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting machines: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/mqtt/status", response_model=MQTTStatusResponse)
|
||||
async def get_mqtt_status():
|
||||
"""Get MQTT client status and statistics"""
|
||||
try:
|
||||
status = self.mqtt_client.get_status()
|
||||
return MQTTStatusResponse(connected=status["connected"], broker_host=status["broker_host"], broker_port=status["broker_port"], subscribed_topics=status["subscribed_topics"], last_message_time=status["last_message_time"], message_count=status["message_count"], error_count=status["error_count"], uptime_seconds=status["uptime_seconds"])
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting MQTT status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/mqtt/events", response_model=MQTTEventsHistoryResponse)
|
||||
async def get_mqtt_events(limit: int = Query(default=5, ge=1, le=50, description="Number of recent events to retrieve")):
|
||||
"""Get recent MQTT events history"""
|
||||
try:
|
||||
events = self.state_manager.get_recent_mqtt_events(limit)
|
||||
total_events = self.state_manager.get_mqtt_event_count()
|
||||
|
||||
# Convert events to response format
|
||||
event_responses = [MQTTEventResponse(machine_name=event.machine_name, topic=event.topic, payload=event.payload, normalized_state=event.normalized_state, timestamp=event.timestamp.isoformat(), message_number=event.message_number) for event in events]
|
||||
|
||||
last_updated = events[0].timestamp.isoformat() if events else None
|
||||
|
||||
return MQTTEventsHistoryResponse(events=event_responses, total_events=total_events, last_updated=last_updated)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting MQTT events: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/cameras", response_model=Dict[str, CameraStatusResponse])
|
||||
async def get_cameras():
|
||||
"""Get all camera statuses"""
|
||||
try:
|
||||
cameras = self.state_manager.get_all_cameras()
|
||||
return {
|
||||
name: CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None,
|
||||
auto_recording_enabled=camera.auto_recording_enabled,
|
||||
auto_recording_active=camera.auto_recording_active,
|
||||
auto_recording_failure_count=camera.auto_recording_failure_count,
|
||||
auto_recording_last_attempt=camera.auto_recording_last_attempt.isoformat() if camera.auto_recording_last_attempt else None,
|
||||
auto_recording_last_error=camera.auto_recording_last_error,
|
||||
)
|
||||
for name, camera in cameras.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting cameras: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/cameras/{camera_name}/status", response_model=CameraStatusResponse)
|
||||
async def get_camera_status(camera_name: str):
|
||||
"""Get specific camera status"""
|
||||
try:
|
||||
camera = self.state_manager.get_camera_status(camera_name)
|
||||
if not camera:
|
||||
raise HTTPException(status_code=404, detail=f"Camera not found: {camera_name}")
|
||||
|
||||
return CameraStatusResponse(name=camera.name, status=camera.status.value, is_recording=camera.is_recording, last_checked=camera.last_checked.isoformat(), last_error=camera.last_error, device_info=camera.device_info, current_recording_file=camera.current_recording_file, recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting camera status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/start-recording", response_model=StartRecordingResponse)
|
||||
async def start_recording(camera_name: str, request: StartRecordingRequest):
|
||||
"""Manually start recording for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.manual_start_recording(camera_name=camera_name, filename=request.filename, exposure_ms=request.exposure_ms, gain=request.gain, fps=request.fps)
|
||||
|
||||
if success:
|
||||
# Get the actual filename that was used (with datetime prefix)
|
||||
actual_filename = request.filename
|
||||
if request.filename:
|
||||
from ..core.timezone_utils import format_filename_timestamp
|
||||
|
||||
timestamp = format_filename_timestamp()
|
||||
actual_filename = f"{timestamp}_{request.filename}"
|
||||
|
||||
return StartRecordingResponse(success=True, message=f"Recording started for {camera_name}", filename=actual_filename)
|
||||
else:
|
||||
return StartRecordingResponse(success=False, message=f"Failed to start recording for {camera_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/stop-recording", response_model=StopRecordingResponse)
|
||||
async def stop_recording(camera_name: str):
|
||||
"""Manually stop recording for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.manual_stop_recording(camera_name)
|
||||
|
||||
if success:
|
||||
return StopRecordingResponse(success=True, message=f"Recording stopped for {camera_name}")
|
||||
else:
|
||||
return StopRecordingResponse(success=False, message=f"Failed to stop recording for {camera_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/test-connection", response_model=CameraTestResponse)
|
||||
async def test_camera_connection(camera_name: str):
|
||||
"""Test camera connection"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.test_camera_connection(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraTestResponse(success=True, message=f"Camera {camera_name} connection test passed", camera_name=camera_name)
|
||||
else:
|
||||
return CameraTestResponse(success=False, message=f"Camera {camera_name} connection test failed", camera_name=camera_name)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error testing camera connection: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/cameras/{camera_name}/stream")
|
||||
async def camera_stream(camera_name: str):
|
||||
"""Get live MJPEG stream from camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
# Get camera streamer
|
||||
streamer = self.camera_manager.get_camera_streamer(camera_name)
|
||||
if not streamer:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
# Start streaming if not already active
|
||||
if not streamer.is_streaming():
|
||||
success = streamer.start_streaming()
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start streaming for camera {camera_name}")
|
||||
|
||||
# Return MJPEG stream
|
||||
return StreamingResponse(streamer.get_frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/start-stream")
|
||||
async def start_camera_stream(camera_name: str):
|
||||
"""Start streaming for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.start_camera_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Started streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to start streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/stop-stream")
|
||||
async def stop_camera_stream(camera_name: str):
|
||||
"""Stop streaming for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.stop_camera_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Stopped streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to stop streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping camera stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/start-rtsp")
|
||||
async def start_camera_rtsp_stream(camera_name: str):
|
||||
"""Start RTSP streaming for a camera to MediaMTX"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.start_camera_rtsp_streaming(camera_name)
|
||||
if success:
|
||||
rtsp_url = f"rtsp://{os.getenv('MEDIAMTX_HOST', 'localhost')}:{os.getenv('MEDIAMTX_RTSP_PORT', '8554')}/{camera_name}"
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Started RTSP streaming for camera {camera_name}",
|
||||
"rtsp_url": rtsp_url
|
||||
}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to start RTSP streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting RTSP stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/stop-rtsp")
|
||||
async def stop_camera_rtsp_stream(camera_name: str):
|
||||
"""Stop RTSP streaming for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.stop_camera_rtsp_streaming(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Stopped RTSP streaming for camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to stop RTSP streaming for camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping RTSP stream: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/cameras/{camera_name}/config", response_model=CameraConfigResponse)
|
||||
async def get_camera_config(camera_name: str):
|
||||
"""Get camera configuration"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
config = self.camera_manager.get_camera_config(camera_name)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
return CameraConfigResponse(
|
||||
name=config.name,
|
||||
machine_topic=config.machine_topic,
|
||||
storage_path=config.storage_path,
|
||||
enabled=config.enabled,
|
||||
# Auto-recording settings
|
||||
auto_start_recording_enabled=config.auto_start_recording_enabled,
|
||||
auto_recording_max_retries=config.auto_recording_max_retries,
|
||||
auto_recording_retry_delay_seconds=config.auto_recording_retry_delay_seconds,
|
||||
# Basic settings
|
||||
exposure_ms=config.exposure_ms,
|
||||
gain=config.gain,
|
||||
target_fps=config.target_fps,
|
||||
# Video recording settings
|
||||
video_format=config.video_format,
|
||||
video_codec=config.video_codec,
|
||||
video_quality=config.video_quality,
|
||||
# Image Quality Settings
|
||||
sharpness=config.sharpness,
|
||||
contrast=config.contrast,
|
||||
saturation=config.saturation,
|
||||
gamma=config.gamma,
|
||||
# Noise Reduction
|
||||
noise_filter_enabled=config.noise_filter_enabled,
|
||||
denoise_3d_enabled=config.denoise_3d_enabled,
|
||||
# Color Settings
|
||||
auto_white_balance=config.auto_white_balance,
|
||||
color_temperature_preset=config.color_temperature_preset,
|
||||
# Manual White Balance RGB Gains
|
||||
wb_red_gain=config.wb_red_gain,
|
||||
wb_green_gain=config.wb_green_gain,
|
||||
wb_blue_gain=config.wb_blue_gain,
|
||||
# Advanced Settings
|
||||
anti_flicker_enabled=config.anti_flicker_enabled,
|
||||
light_frequency=config.light_frequency,
|
||||
bit_depth=config.bit_depth,
|
||||
# HDR Settings
|
||||
hdr_enabled=config.hdr_enabled,
|
||||
hdr_gain_mode=config.hdr_gain_mode,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.put("/cameras/{camera_name}/config")
|
||||
async def update_camera_config(camera_name: str, request: CameraConfigRequest):
|
||||
"""Update camera configuration"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
# Convert request to dict, excluding None values
|
||||
config_updates = {k: v for k, v in request.dict().items() if v is not None}
|
||||
|
||||
if not config_updates:
|
||||
raise HTTPException(status_code=400, detail="No configuration updates provided")
|
||||
|
||||
success = self.camera_manager.update_camera_config(camera_name, **config_updates)
|
||||
if success:
|
||||
return {"success": True, "message": f"Camera {camera_name} configuration updated", "updated_settings": list(config_updates.keys())}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found or update failed")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/apply-config")
|
||||
async def apply_camera_config(camera_name: str):
|
||||
"""Apply current configuration to active camera (requires camera restart)"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.apply_camera_config(camera_name)
|
||||
if success:
|
||||
return {"success": True, "message": f"Configuration applied to camera {camera_name}"}
|
||||
else:
|
||||
return {"success": False, "message": f"Failed to apply configuration to camera {camera_name}"}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error applying camera config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reconnect", response_model=CameraRecoveryResponse)
|
||||
async def reconnect_camera(camera_name: str):
|
||||
"""Reconnect to a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reconnect_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} reconnected successfully", camera_name=camera_name, operation="reconnect")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reconnect camera {camera_name}", camera_name=camera_name, operation="reconnect")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reconnecting camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/restart-grab", response_model=CameraRecoveryResponse)
|
||||
async def restart_camera_grab(camera_name: str):
|
||||
"""Restart camera grab process"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.restart_camera_grab(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} grab process restarted successfully", camera_name=camera_name, operation="restart-grab")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to restart grab process for camera {camera_name}", camera_name=camera_name, operation="restart-grab")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error restarting camera grab: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reset-timestamp", response_model=CameraRecoveryResponse)
|
||||
async def reset_camera_timestamp(camera_name: str):
|
||||
"""Reset camera timestamp"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reset_camera_timestamp(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} timestamp reset successfully", camera_name=camera_name, operation="reset-timestamp")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reset timestamp for camera {camera_name}", camera_name=camera_name, operation="reset-timestamp")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error resetting camera timestamp: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/full-reset", response_model=CameraRecoveryResponse)
|
||||
async def full_reset_camera(camera_name: str):
|
||||
"""Perform full camera reset (uninitialize and reinitialize)"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.full_reset_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} full reset completed successfully", camera_name=camera_name, operation="full-reset")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to perform full reset for camera {camera_name}", camera_name=camera_name, operation="full-reset")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error performing full camera reset: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reinitialize", response_model=CameraRecoveryResponse)
|
||||
async def reinitialize_camera(camera_name: str):
|
||||
"""Reinitialize a failed camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reinitialize_failed_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} reinitialized successfully", camera_name=camera_name, operation="reinitialize")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reinitialize camera {camera_name}", camera_name=camera_name, operation="reinitialize")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reinitializing camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/auto-recording/enable", response_model=AutoRecordingConfigResponse)
|
||||
async def enable_auto_recording(camera_name: str):
|
||||
"""Enable auto-recording for a camera"""
|
||||
try:
|
||||
if not self.auto_recording_manager:
|
||||
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||
|
||||
# Update camera configuration
|
||||
camera_config = self.config.get_camera_by_name(camera_name)
|
||||
if not camera_config:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
camera_config.auto_start_recording_enabled = True
|
||||
self.config.save_config()
|
||||
|
||||
# Update camera status in state manager
|
||||
camera_info = self.state_manager.get_camera_status(camera_name)
|
||||
if camera_info:
|
||||
camera_info.auto_recording_enabled = True
|
||||
|
||||
return AutoRecordingConfigResponse(success=True, message=f"Auto-recording enabled for camera {camera_name}", camera_name=camera_name, enabled=True)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error enabling auto-recording for camera {camera_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/auto-recording/disable", response_model=AutoRecordingConfigResponse)
|
||||
async def disable_auto_recording(camera_name: str):
|
||||
"""Disable auto-recording for a camera"""
|
||||
try:
|
||||
if not self.auto_recording_manager:
|
||||
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||
|
||||
# Update camera configuration
|
||||
camera_config = self.config.get_camera_by_name(camera_name)
|
||||
if not camera_config:
|
||||
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||
|
||||
camera_config.auto_start_recording_enabled = False
|
||||
self.config.save_config()
|
||||
|
||||
# Update camera status in state manager
|
||||
camera_info = self.state_manager.get_camera_status(camera_name)
|
||||
if camera_info:
|
||||
camera_info.auto_recording_enabled = False
|
||||
camera_info.auto_recording_active = False
|
||||
|
||||
return AutoRecordingConfigResponse(success=True, message=f"Auto-recording disabled for camera {camera_name}", camera_name=camera_name, enabled=False)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error disabling auto-recording for camera {camera_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/auto-recording/status", response_model=AutoRecordingStatusResponse)
|
||||
async def get_auto_recording_status():
|
||||
"""Get auto-recording manager status"""
|
||||
try:
|
||||
if not self.auto_recording_manager:
|
||||
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||
|
||||
status = self.auto_recording_manager.get_status()
|
||||
return AutoRecordingStatusResponse(**status)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting auto-recording status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/recordings", response_model=Dict[str, RecordingInfoResponse])
|
||||
async def get_recordings():
|
||||
"""Get all recording sessions"""
|
||||
try:
|
||||
recordings = self.state_manager.get_all_recordings()
|
||||
return {
|
||||
rid: RecordingInfoResponse(
|
||||
camera_name=recording.camera_name,
|
||||
filename=recording.filename,
|
||||
start_time=recording.start_time.isoformat(),
|
||||
state=recording.state.value,
|
||||
end_time=recording.end_time.isoformat() if recording.end_time else None,
|
||||
file_size_bytes=recording.file_size_bytes,
|
||||
frame_count=recording.frame_count,
|
||||
duration_seconds=(recording.end_time - recording.start_time).total_seconds() if recording.end_time else None,
|
||||
error_message=recording.error_message,
|
||||
)
|
||||
for rid, recording in recordings.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting recordings: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/storage/stats", response_model=StorageStatsResponse)
|
||||
async def get_storage_stats():
|
||||
"""Get storage statistics"""
|
||||
try:
|
||||
stats = self.storage_manager.get_storage_statistics()
|
||||
return StorageStatsResponse(**stats)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting storage stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/storage/files", response_model=FileListResponse)
|
||||
async def get_files(request: FileListRequest):
|
||||
"""Get list of recording files"""
|
||||
try:
|
||||
start_date = None
|
||||
end_date = None
|
||||
|
||||
if request.start_date:
|
||||
start_date = datetime.fromisoformat(request.start_date)
|
||||
if request.end_date:
|
||||
end_date = datetime.fromisoformat(request.end_date)
|
||||
|
||||
files = self.storage_manager.get_recording_files(camera_name=request.camera_name, start_date=start_date, end_date=end_date, limit=request.limit)
|
||||
|
||||
return FileListResponse(files=files, total_count=len(files))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting files: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/storage/cleanup", response_model=CleanupResponse)
|
||||
async def cleanup_storage(request: CleanupRequest):
|
||||
"""Clean up old storage files"""
|
||||
try:
|
||||
result = self.storage_manager.cleanup_old_files(request.max_age_days)
|
||||
return CleanupResponse(**result)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during cleanup: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Register routes from modules
|
||||
register_system_routes(
|
||||
app=self.app,
|
||||
state_manager=self.state_manager,
|
||||
video_module=self.video_module,
|
||||
server_start_time=self.server_start_time,
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
register_camera_routes(
|
||||
app=self.app,
|
||||
config=self.config,
|
||||
state_manager=self.state_manager,
|
||||
camera_manager=self.camera_manager,
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
register_recording_routes(
|
||||
app=self.app,
|
||||
camera_manager=self.camera_manager,
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
register_mqtt_routes(
|
||||
app=self.app,
|
||||
mqtt_client=self.mqtt_client,
|
||||
state_manager=self.state_manager,
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
register_storage_routes(
|
||||
app=self.app,
|
||||
storage_manager=self.storage_manager,
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
register_auto_recording_routes(
|
||||
app=self.app,
|
||||
config=self.config,
|
||||
state_manager=self.state_manager,
|
||||
auto_recording_manager=self.auto_recording_manager,
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
register_recordings_routes(
|
||||
app=self.app,
|
||||
state_manager=self.state_manager,
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
# WebSocket endpoint (not in route modules)
|
||||
@self.app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for real-time updates"""
|
||||
|
||||
30
camera-management-api/usda_vision_system/camera/constants.py
Normal file
30
camera-management-api/usda_vision_system/camera/constants.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Constants for camera operations.
|
||||
"""
|
||||
|
||||
# Timeouts (milliseconds)
|
||||
CAMERA_GET_BUFFER_TIMEOUT = 200 # Standard frame capture timeout
|
||||
CAMERA_INIT_TIMEOUT = 1000 # Camera initialization timeout
|
||||
CAMERA_TEST_CAPTURE_TIMEOUT = 1000 # Test capture timeout
|
||||
CAMERA_GET_BUFFER_SHORT_TIMEOUT = 100 # Shorter timeout for quick checks
|
||||
|
||||
# Frame queue sizes
|
||||
MJPEG_QUEUE_MAXSIZE = 5 # Buffer for latest frames (for MJPEG streaming)
|
||||
RTSP_QUEUE_MAXSIZE = 10 # Buffer for RTSP frames (larger buffer for smoother streaming)
|
||||
RECORDING_QUEUE_MAXSIZE = 30 # Buffer for recording frames (shared with recorder)
|
||||
|
||||
# Frame rates (FPS)
|
||||
PREVIEW_FPS = 10.0 # Lower FPS for preview to reduce load
|
||||
RTSP_FPS = 15.0 # RTSP FPS (can be higher than MJPEG preview)
|
||||
DEFAULT_VIDEO_FPS = 30.0 # Default video FPS when target_fps is 0 or unspecified
|
||||
|
||||
# Sleep intervals (seconds)
|
||||
STREAMING_LOOP_SLEEP = 0.1 # Sleep interval in streaming loops when waiting
|
||||
BRIEF_PAUSE_SLEEP = 0.1 # Brief pause before retrying operations
|
||||
|
||||
# JPEG quality (0-100)
|
||||
PREVIEW_JPEG_QUALITY = 70 # JPEG quality for streaming preview
|
||||
|
||||
# Video writer buffer size
|
||||
VIDEO_WRITER_CHUNK_SIZE = 8192 # Buffer size for video writer operations
|
||||
|
||||
@@ -9,7 +9,6 @@ import os
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import contextlib
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
# Add camera SDK to path
|
||||
@@ -20,30 +19,8 @@ from ..core.config import Config
|
||||
from ..core.state_manager import StateManager, CameraStatus
|
||||
from ..core.events import EventSystem, publish_camera_status_changed
|
||||
from .sdk_config import ensure_sdk_initialized
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_camera_errors():
|
||||
"""Context manager to temporarily suppress camera SDK error output"""
|
||||
# Save original file descriptors
|
||||
original_stderr = os.dup(2)
|
||||
original_stdout = os.dup(1)
|
||||
|
||||
try:
|
||||
# Redirect stderr and stdout to devnull
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, 2) # stderr
|
||||
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
|
||||
os.close(devnull)
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Restore original file descriptors
|
||||
os.dup2(original_stderr, 2)
|
||||
os.dup2(original_stdout, 1)
|
||||
os.close(original_stderr)
|
||||
os.close(original_stdout)
|
||||
from .utils import suppress_camera_errors
|
||||
from .constants import CAMERA_TEST_CAPTURE_TIMEOUT
|
||||
|
||||
|
||||
class CameraMonitor:
|
||||
@@ -219,7 +196,7 @@ class CameraMonitor:
|
||||
mvsdk.CameraPlay(hCamera)
|
||||
|
||||
# Try to capture with short timeout
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(hCamera, 500)
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(hCamera, CAMERA_TEST_CAPTURE_TIMEOUT)
|
||||
mvsdk.CameraReleaseImageBuffer(hCamera, pRawData)
|
||||
|
||||
# Success - camera is available
|
||||
|
||||
@@ -11,7 +11,6 @@ import time
|
||||
import logging
|
||||
import cv2
|
||||
import numpy as np
|
||||
import contextlib
|
||||
import queue
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
@@ -26,30 +25,7 @@ from ..core.state_manager import StateManager
|
||||
from ..core.events import EventSystem, publish_recording_started, publish_recording_stopped, publish_recording_error
|
||||
from ..core.timezone_utils import now_atlanta, format_filename_timestamp
|
||||
from .sdk_config import ensure_sdk_initialized
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_camera_errors():
|
||||
"""Context manager to temporarily suppress camera SDK error output"""
|
||||
# Save original file descriptors
|
||||
original_stderr = os.dup(2)
|
||||
original_stdout = os.dup(1)
|
||||
|
||||
try:
|
||||
# Redirect stderr and stdout to devnull
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, 2) # stderr
|
||||
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
|
||||
os.close(devnull)
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Restore original file descriptors
|
||||
os.dup2(original_stderr, 2)
|
||||
os.dup2(original_stdout, 1)
|
||||
os.close(original_stderr)
|
||||
os.close(original_stdout)
|
||||
from .utils import suppress_camera_errors
|
||||
|
||||
|
||||
class CameraRecorder:
|
||||
@@ -537,7 +513,7 @@ class CameraRecorder:
|
||||
"""Test if camera can capture frames"""
|
||||
try:
|
||||
# Try to capture one frame
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 1000) # 1 second timeout
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, CAMERA_TEST_CAPTURE_TIMEOUT)
|
||||
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
|
||||
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
|
||||
return True
|
||||
@@ -686,7 +662,7 @@ class CameraRecorder:
|
||||
continue
|
||||
else:
|
||||
# Capture frame directly from camera
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 200) # 200ms timeout
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, CAMERA_GET_BUFFER_TIMEOUT)
|
||||
|
||||
# Process frame
|
||||
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
|
||||
@@ -770,7 +746,7 @@ class CameraRecorder:
|
||||
self.logger.info(f"Using frame dimensions from streamer frame: {frame_size}")
|
||||
elif self.hCamera:
|
||||
# Get frame dimensions by capturing a test frame from camera
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 1000)
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, CAMERA_INIT_TIMEOUT)
|
||||
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
|
||||
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
|
||||
frame_size = (FrameHead.iWidth, FrameHead.iHeight)
|
||||
@@ -779,7 +755,7 @@ class CameraRecorder:
|
||||
if self.streamer and self.streamer.hCamera:
|
||||
try:
|
||||
with suppress_camera_errors():
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.streamer.hCamera, 1000)
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.streamer.hCamera, CAMERA_INIT_TIMEOUT)
|
||||
mvsdk.CameraReleaseImageBuffer(self.streamer.hCamera, pRawData)
|
||||
frame_size = (FrameHead.iWidth, FrameHead.iHeight)
|
||||
self.logger.info(f"Got frame dimensions from streamer's camera: {frame_size}")
|
||||
@@ -798,8 +774,8 @@ class CameraRecorder:
|
||||
# Set up video writer with configured codec
|
||||
fourcc = cv2.VideoWriter_fourcc(*self.camera_config.video_codec)
|
||||
|
||||
# Use 30 FPS for video writer if target_fps is 0 (unlimited)
|
||||
video_fps = self.camera_config.target_fps if self.camera_config.target_fps > 0 else 30.0
|
||||
# Use default FPS for video writer if target_fps is 0 (unlimited)
|
||||
video_fps = self.camera_config.target_fps if self.camera_config.target_fps > 0 else DEFAULT_VIDEO_FPS
|
||||
|
||||
# Create video writer with quality settings
|
||||
self.video_writer = cv2.VideoWriter(self.output_filename, fourcc, video_fps, frame_size)
|
||||
@@ -883,7 +859,7 @@ class CameraRecorder:
|
||||
|
||||
# Small delay to ensure file system sync
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
time.sleep(BRIEF_PAUSE_SLEEP)
|
||||
|
||||
# Verify file exists and has content
|
||||
if self.output_filename and os.path.exists(self.output_filename):
|
||||
|
||||
@@ -12,7 +12,6 @@ import time
|
||||
import logging
|
||||
import cv2
|
||||
import numpy as np
|
||||
import contextlib
|
||||
import subprocess
|
||||
from typing import Optional, Dict, Any, Generator
|
||||
from datetime import datetime
|
||||
@@ -26,30 +25,19 @@ from ..core.config import CameraConfig
|
||||
from ..core.state_manager import StateManager
|
||||
from ..core.events import EventSystem
|
||||
from .sdk_config import ensure_sdk_initialized
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_camera_errors():
|
||||
"""Context manager to temporarily suppress camera SDK error output"""
|
||||
# Save original file descriptors
|
||||
original_stderr = os.dup(2)
|
||||
original_stdout = os.dup(1)
|
||||
|
||||
try:
|
||||
# Redirect stderr and stdout to devnull
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, 2) # stderr
|
||||
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
|
||||
os.close(devnull)
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Restore original file descriptors
|
||||
os.dup2(original_stderr, 2)
|
||||
os.dup2(original_stdout, 1)
|
||||
os.close(original_stderr)
|
||||
os.close(original_stdout)
|
||||
from .utils import suppress_camera_errors
|
||||
from .constants import (
|
||||
MJPEG_QUEUE_MAXSIZE,
|
||||
RTSP_QUEUE_MAXSIZE,
|
||||
RECORDING_QUEUE_MAXSIZE,
|
||||
PREVIEW_FPS,
|
||||
RTSP_FPS,
|
||||
PREVIEW_JPEG_QUALITY,
|
||||
CAMERA_GET_BUFFER_TIMEOUT,
|
||||
CAMERA_TEST_CAPTURE_TIMEOUT,
|
||||
STREAMING_LOOP_SLEEP,
|
||||
BRIEF_PAUSE_SLEEP,
|
||||
)
|
||||
|
||||
|
||||
class CameraStreamer:
|
||||
@@ -78,17 +66,17 @@ class CameraStreamer:
|
||||
self._rtsp_thread: Optional[threading.Thread] = None
|
||||
self._stop_streaming_event = threading.Event()
|
||||
self._stop_rtsp_event = threading.Event()
|
||||
self._frame_queue = queue.Queue(maxsize=5) # Buffer for latest frames (for MJPEG streaming)
|
||||
self._rtsp_frame_queue = queue.Queue(maxsize=10) # Buffer for RTSP frames (larger buffer for smoother streaming)
|
||||
self._recording_frame_queue = queue.Queue(maxsize=30) # Buffer for recording frames (shared with recorder)
|
||||
self._frame_queue = queue.Queue(maxsize=MJPEG_QUEUE_MAXSIZE) # Buffer for latest frames (for MJPEG streaming)
|
||||
self._rtsp_frame_queue = queue.Queue(maxsize=RTSP_QUEUE_MAXSIZE) # Buffer for RTSP frames (larger buffer for smoother streaming)
|
||||
self._recording_frame_queue = queue.Queue(maxsize=RECORDING_QUEUE_MAXSIZE) # Buffer for recording frames (shared with recorder)
|
||||
self._lock = threading.RLock()
|
||||
|
||||
# Stream settings (optimized for preview)
|
||||
self.preview_fps = 10.0 # Lower FPS for preview to reduce load
|
||||
self.preview_quality = 70 # JPEG quality for streaming
|
||||
self.preview_fps = PREVIEW_FPS # Lower FPS for preview to reduce load
|
||||
self.preview_quality = PREVIEW_JPEG_QUALITY # JPEG quality for streaming
|
||||
|
||||
# RTSP settings
|
||||
self.rtsp_fps = 15.0 # RTSP FPS (can be higher than MJPEG preview)
|
||||
self.rtsp_fps = RTSP_FPS # RTSP FPS (can be higher than MJPEG preview)
|
||||
# Use MEDIAMTX_HOST env var if set, otherwise default to localhost
|
||||
# Note: If API uses network_mode: host, MediaMTX container ports are exposed to host
|
||||
# So localhost should work, but MediaMTX must be accessible on that port
|
||||
@@ -254,7 +242,7 @@ class CameraStreamer:
|
||||
if frame_bytes:
|
||||
yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame_bytes + b"\r\n")
|
||||
else:
|
||||
time.sleep(0.1) # Wait a bit if no frame available
|
||||
time.sleep(STREAMING_LOOP_SLEEP) # Wait a bit if no frame available
|
||||
|
||||
def _initialize_camera(self) -> bool:
|
||||
"""Initialize camera for streaming (separate from recording)"""
|
||||
@@ -366,11 +354,11 @@ class CameraStreamer:
|
||||
try:
|
||||
# If using shared camera, skip capture - recorder will populate queues
|
||||
if self._using_shared_camera:
|
||||
time.sleep(0.1) # Just wait, recorder populates queues
|
||||
time.sleep(STREAMING_LOOP_SLEEP) # Just wait, recorder populates queues
|
||||
continue
|
||||
|
||||
# Capture frame with timeout
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 200) # 200ms timeout
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, CAMERA_GET_BUFFER_TIMEOUT)
|
||||
|
||||
# Process frame
|
||||
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
|
||||
@@ -431,7 +419,7 @@ class CameraStreamer:
|
||||
except Exception as e:
|
||||
if not self._stop_streaming_event.is_set():
|
||||
self.logger.error(f"Error in streaming loop: {e}")
|
||||
time.sleep(0.1) # Brief pause before retrying
|
||||
time.sleep(BRIEF_PAUSE_SLEEP) # Brief pause before retrying
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Fatal error in streaming loop: {e}")
|
||||
|
||||
31
camera-management-api/usda_vision_system/camera/utils.py
Normal file
31
camera-management-api/usda_vision_system/camera/utils.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Shared utilities for camera operations.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_camera_errors():
|
||||
"""Context manager to temporarily suppress camera SDK error output"""
|
||||
# Save original file descriptors
|
||||
original_stderr = os.dup(2)
|
||||
original_stdout = os.dup(1)
|
||||
|
||||
try:
|
||||
# Redirect stderr and stdout to devnull
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, 2) # stderr
|
||||
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
|
||||
os.close(devnull)
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Restore original file descriptors
|
||||
os.dup2(original_stderr, 2)
|
||||
os.dup2(original_stdout, 1)
|
||||
os.close(original_stderr)
|
||||
os.close(original_stdout)
|
||||
|
||||
Reference in New Issue
Block a user