Enhance time synchronization checks, update storage paths, and improve camera recording management

This commit is contained in:
Alireza Vaezi
2025-07-25 22:38:33 -04:00
parent 69966519b0
commit 731d8cd9ff
13 changed files with 283 additions and 60 deletions

View File

@@ -90,6 +90,7 @@ class APIServer:
self.server_start_time = datetime.now()
self.running = False
self._server_thread: Optional[threading.Thread] = None
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
# Setup CORS
self.app.add_middleware(
@@ -349,8 +350,15 @@ class APIServer:
"timestamp": event.timestamp.isoformat()
}
# Use asyncio to broadcast (need to handle thread safety)
asyncio.create_task(self.websocket_manager.broadcast(message))
# Schedule the broadcast in the event loop thread-safely
if self._event_loop and not self._event_loop.is_closed():
# Use call_soon_threadsafe to schedule the coroutine from another thread
asyncio.run_coroutine_threadsafe(
self.websocket_manager.broadcast(message),
self._event_loop
)
else:
self.logger.debug("Event loop not available for broadcasting")
except Exception as e:
self.logger.error(f"Error broadcasting event: {e}")
@@ -399,6 +407,10 @@ class APIServer:
def _run_server(self) -> None:
"""Run the uvicorn server"""
try:
# Capture the event loop for thread-safe event broadcasting
self._event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._event_loop)
uvicorn.run(
self.app,
host=self.config.system.api_host,
@@ -409,6 +421,7 @@ class APIServer:
self.logger.error(f"Error running API server: {e}")
finally:
self.running = False
self._event_loop = None
def is_running(self) -> bool:
"""Check if API server is running"""

View File

@@ -147,21 +147,44 @@ class CameraManager:
device_info = self._find_camera_device(camera_config.name)
if device_info is None:
self.logger.warning(f"No physical camera found for configured camera: {camera_config.name}")
# Update state to indicate camera is not available
self.state_manager.update_camera_status(
name=camera_config.name,
status="not_found",
device_info=None
)
continue
# Create recorder
# Create recorder (this will attempt to initialize the camera)
recorder = CameraRecorder(
camera_config=camera_config,
device_info=device_info,
state_manager=self.state_manager,
event_system=self.event_system
)
# Check if camera initialization was successful
if recorder.hCamera is None:
self.logger.warning(f"Camera {camera_config.name} failed to initialize, skipping")
# Update state to indicate camera initialization failed
self.state_manager.update_camera_status(
name=camera_config.name,
status="initialization_failed",
device_info={"error": "Camera initialization failed"}
)
continue
self.camera_recorders[camera_config.name] = recorder
self.logger.info(f"Initialized recorder for camera: {camera_config.name}")
self.logger.info(f"Successfully initialized recorder for camera: {camera_config.name}")
except Exception as e:
self.logger.error(f"Error initializing recorder for {camera_config.name}: {e}")
# Update state to indicate error
self.state_manager.update_camera_status(
name=camera_config.name,
status="error",
device_info={"error": str(e)}
)
def _find_camera_device(self, camera_name: str) -> Optional[Any]:
"""Find physical camera device for a configured camera"""

View File

@@ -27,12 +27,13 @@ from ..core.timezone_utils import now_atlanta, format_filename_timestamp
class CameraRecorder:
"""Handles video recording for a single camera"""
def __init__(self, camera_config: CameraConfig, device_info: Any, state_manager: StateManager, event_system: EventSystem):
def __init__(self, camera_config: CameraConfig, device_info: Any, state_manager: StateManager, event_system: EventSystem, storage_manager=None):
self.camera_config = camera_config
self.device_info = device_info
self.state_manager = state_manager
self.event_system = event_system
self.storage_manager = storage_manager
self.logger = logging.getLogger(f"{__name__}.{camera_config.name}")
# Camera handle and properties
@@ -61,39 +62,47 @@ class CameraRecorder:
"""Initialize the camera with configured settings"""
try:
self.logger.info(f"Initializing camera: {self.camera_config.name}")
# Check if device_info is valid
if self.device_info is None:
self.logger.error("No device info provided for camera initialization")
return False
# Initialize camera
self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1)
self.logger.info("Camera initialized successfully")
# Get camera capabilities
self.cap = mvsdk.CameraGetCapability(self.hCamera)
self.monoCamera = self.cap.sIspCapacity.bMonoSensor != 0
self.logger.info(f"Camera type: {'Monochrome' if self.monoCamera else 'Color'}")
# Set output format
if self.monoCamera:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO8)
else:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR8)
# Configure camera settings
self._configure_camera_settings()
# Allocate frame buffer
self.frame_buffer_size = (self.cap.sResolutionRange.iWidthMax *
self.cap.sResolutionRange.iHeightMax *
self.frame_buffer_size = (self.cap.sResolutionRange.iWidthMax *
self.cap.sResolutionRange.iHeightMax *
(1 if self.monoCamera else 3))
self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16)
# Start camera
mvsdk.CameraPlay(self.hCamera)
self.logger.info("Camera started successfully")
return True
except mvsdk.CameraException as e:
self.logger.error(f"Camera initialization failed({e.error_code}): {e.message}")
error_msg = f"Camera initialization failed({e.error_code}): {e.message}"
if e.error_code == 32774:
error_msg += " - This may indicate the camera is already in use by another process or there's a resource conflict"
self.logger.error(error_msg)
return False
except Exception as e:
self.logger.error(f"Unexpected error during camera initialization: {e}")
@@ -251,8 +260,9 @@ class CameraRecorder:
# Release buffer
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
# Control frame rate
time.sleep(1.0 / self.camera_config.target_fps)
# Control frame rate (skip sleep if target_fps is 0 for maximum speed)
if self.camera_config.target_fps > 0:
time.sleep(1.0 / self.camera_config.target_fps)
except mvsdk.CameraException as e:
if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT:
@@ -284,10 +294,13 @@ class CameraRecorder:
fourcc = cv2.VideoWriter_fourcc(*'XVID')
frame_size = (FrameHead.iWidth, FrameHead.iHeight)
# 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
self.video_writer = cv2.VideoWriter(
self.output_filename,
fourcc,
self.camera_config.target_fps,
video_fps,
frame_size
)
@@ -305,14 +318,17 @@ class CameraRecorder:
def _convert_frame_to_opencv(self, frame_head) -> Optional[np.ndarray]:
"""Convert camera frame to OpenCV format"""
try:
# Convert the frame buffer memory address to a proper buffer
# that numpy can work with using mvsdk.c_ubyte
frame_data_buffer = (mvsdk.c_ubyte * frame_head.uBytes).from_address(self.frame_buffer)
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint8)
if self.monoCamera:
# Monochrome camera - convert to BGR
frame_data = np.frombuffer(self.frame_buffer, dtype=np.uint8)
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
else:
# Color camera - already in BGR format
frame_data = np.frombuffer(self.frame_buffer, dtype=np.uint8)
frame_bgr = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
return frame_bgr

View File

@@ -45,7 +45,7 @@ class USDAVisionSystem:
self.event_system = EventSystem()
# Initialize system components
self.storage_manager = StorageManager(self.config, self.state_manager)
self.storage_manager = StorageManager(self.config, self.state_manager, self.event_system)
self.mqtt_client = MQTTClient(self.config, self.state_manager, self.event_system)
self.camera_manager = CameraManager(self.config, self.state_manager, self.event_system)
self.api_server = APIServer(

View File

@@ -14,23 +14,29 @@ import json
from ..core.config import Config, StorageConfig
from ..core.state_manager import StateManager
from ..core.events import EventSystem, EventType, Event
class StorageManager:
"""Manages storage and file organization for recorded videos"""
def __init__(self, config: Config, state_manager: StateManager):
def __init__(self, config: Config, state_manager: StateManager, event_system: Optional[EventSystem] = None):
self.config = config
self.storage_config = config.storage
self.state_manager = state_manager
self.event_system = event_system
self.logger = logging.getLogger(__name__)
# Ensure base storage directory exists
self._ensure_storage_structure()
# File tracking
self.file_index_path = os.path.join(self.storage_config.base_path, "file_index.json")
self.file_index = self._load_file_index()
# Subscribe to recording events if event system is available
if self.event_system:
self._setup_event_subscriptions()
def _ensure_storage_structure(self) -> None:
"""Ensure storage directory structure exists"""
@@ -48,6 +54,44 @@ class StorageManager:
except Exception as e:
self.logger.error(f"Error creating storage structure: {e}")
raise
def _setup_event_subscriptions(self) -> None:
"""Setup event subscriptions for recording tracking"""
if not self.event_system:
return
def on_recording_started(event: Event):
"""Handle recording started event"""
try:
camera_name = event.data.get("camera_name")
filename = event.data.get("filename")
if camera_name and filename:
self.register_recording_file(
camera_name=camera_name,
filename=filename,
start_time=event.timestamp,
machine_trigger=event.data.get("machine_trigger")
)
except Exception as e:
self.logger.error(f"Error handling recording started event: {e}")
def on_recording_stopped(event: Event):
"""Handle recording stopped event"""
try:
filename = event.data.get("filename")
if filename:
file_id = os.path.basename(filename)
self.finalize_recording_file(
file_id=file_id,
end_time=event.timestamp,
duration_seconds=event.data.get("duration_seconds")
)
except Exception as e:
self.logger.error(f"Error handling recording stopped event: {e}")
# Subscribe to recording events
self.event_system.subscribe(EventType.RECORDING_STARTED, on_recording_started)
self.event_system.subscribe(EventType.RECORDING_STOPPED, on_recording_stopped)
def _load_file_index(self) -> Dict[str, Any]:
"""Load file index from disk"""
@@ -98,6 +142,33 @@ class StorageManager:
except Exception as e:
self.logger.error(f"Error registering recording file: {e}")
return ""
def finalize_recording_file(self, file_id: str, end_time: datetime, duration_seconds: Optional[float] = None) -> bool:
"""Finalize a recording file when recording stops"""
try:
if file_id not in self.file_index["files"]:
self.logger.warning(f"Recording file not found for finalization: {file_id}")
return False
file_info = self.file_index["files"][file_id]
file_info["end_time"] = end_time.isoformat()
file_info["status"] = "completed"
if duration_seconds is not None:
file_info["duration_seconds"] = duration_seconds
# Get file size if file exists
filename = file_info["filename"]
if os.path.exists(filename):
file_info["file_size_bytes"] = os.path.getsize(filename)
self._save_file_index()
self.logger.info(f"Finalized recording file: {file_id}")
return True
except Exception as e:
self.logger.error(f"Error finalizing recording file: {e}")
return False
def finalize_recording_file(self, file_id: str, end_time: datetime,
duration_seconds: float, frame_count: Optional[int] = None) -> bool: