""" Camera Manager for the USDA Vision Camera System. This module manages GigE camera discovery, initialization, and coordination with the recording system based on machine state changes. """ import sys import os import threading import logging from typing import Dict, List, Optional, Tuple, Any from datetime import datetime # Add python demo to path sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo')) import mvsdk from ..core.config import Config, CameraConfig from ..core.state_manager import StateManager, CameraStatus from ..core.events import EventSystem, EventType, Event, publish_camera_status_changed from ..core.timezone_utils import format_filename_timestamp from .recorder import CameraRecorder from .monitor import CameraMonitor class CameraManager: """Manages all cameras in the system""" def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem): self.config = config self.state_manager = state_manager self.event_system = event_system self.logger = logging.getLogger(__name__) # Camera management self.available_cameras: List[Any] = [] # mvsdk camera device info self.camera_recorders: Dict[str, CameraRecorder] = {} # camera_name -> recorder self.camera_monitor: Optional[CameraMonitor] = None # Threading self._lock = threading.RLock() self.running = False # Subscribe to machine state changes self.event_system.subscribe(EventType.MACHINE_STATE_CHANGED, self._on_machine_state_changed) # Initialize camera discovery self._discover_cameras() # Create camera monitor self.camera_monitor = CameraMonitor( config=config, state_manager=state_manager, event_system=event_system, camera_manager=self ) def start(self) -> bool: """Start the camera manager""" if self.running: self.logger.warning("Camera manager is already running") return True self.logger.info("Starting camera manager...") self.running = True # Start camera monitor if self.camera_monitor: self.camera_monitor.start() # Initialize camera recorders self._initialize_recorders() self.logger.info("Camera manager started successfully") return True def stop(self) -> None: """Stop the camera manager""" if not self.running: return self.logger.info("Stopping camera manager...") self.running = False # Stop camera monitor if self.camera_monitor: self.camera_monitor.stop() # Stop all active recordings with self._lock: for recorder in self.camera_recorders.values(): if recorder.is_recording(): recorder.stop_recording() recorder.cleanup() self.logger.info("Camera manager stopped") def _discover_cameras(self) -> None: """Discover available GigE cameras""" try: self.logger.info("Discovering GigE cameras...") # Enumerate cameras using mvsdk device_list = mvsdk.CameraEnumerateDevice() self.available_cameras = device_list self.logger.info(f"Found {len(device_list)} camera(s)") for i, dev_info in enumerate(device_list): try: name = dev_info.GetFriendlyName() port_type = dev_info.GetPortType() serial = getattr(dev_info, 'acSn', 'Unknown') self.logger.info(f" Camera {i}: {name} ({port_type}) - Serial: {serial}") # Update state manager with discovered camera camera_name = f"camera{i+1}" # Default naming self.state_manager.update_camera_status( name=camera_name, status="available", device_info={ "friendly_name": name, "port_type": port_type, "serial_number": serial, "device_index": i } ) except Exception as e: self.logger.error(f"Error processing camera {i}: {e}") except Exception as e: self.logger.error(f"Error discovering cameras: {e}") self.available_cameras = [] def _initialize_recorders(self) -> None: """Initialize camera recorders for configured cameras""" with self._lock: for camera_config in self.config.cameras: if not camera_config.enabled: continue try: # Find matching physical camera 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 (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"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""" # For now, use simple mapping: camera1 -> device 0, camera2 -> device 1, etc. # This could be enhanced to use serial numbers or other identifiers camera_index_map = { "camera1": 0, "camera2": 1, "camera3": 2, "camera4": 3 } device_index = camera_index_map.get(camera_name) if device_index is not None and device_index < len(self.available_cameras): return self.available_cameras[device_index] return None def _on_machine_state_changed(self, event: Event) -> None: """Handle machine state change events""" try: machine_name = event.data.get("machine_name") new_state = event.data.get("state") if not machine_name or not new_state: return self.logger.info(f"Handling machine state change: {machine_name} -> {new_state}") # Find camera associated with this machine camera_config = None for config in self.config.cameras: if config.machine_topic == machine_name: camera_config = config break if not camera_config: self.logger.warning(f"No camera configured for machine: {machine_name}") return # Get the recorder for this camera recorder = self.camera_recorders.get(camera_config.name) if not recorder: self.logger.warning(f"No recorder found for camera: {camera_config.name}") return # Handle state change if new_state == "on": self._start_recording(camera_config.name, recorder) elif new_state in ["off", "error"]: self._stop_recording(camera_config.name, recorder) except Exception as e: self.logger.error(f"Error handling machine state change: {e}") def _start_recording(self, camera_name: str, recorder: CameraRecorder) -> None: """Start recording for a camera""" try: if recorder.is_recording(): self.logger.info(f"Camera {camera_name} is already recording") return # Generate filename with Atlanta timezone timestamp timestamp = format_filename_timestamp() filename = f"{camera_name}_recording_{timestamp}.avi" # Start recording success = recorder.start_recording(filename) if success: self.logger.info(f"Started recording for camera {camera_name}: {filename}") else: self.logger.error(f"Failed to start recording for camera {camera_name}") except Exception as e: self.logger.error(f"Error starting recording for {camera_name}: {e}") def _stop_recording(self, camera_name: str, recorder: CameraRecorder) -> None: """Stop recording for a camera""" try: if not recorder.is_recording(): self.logger.info(f"Camera {camera_name} is not recording") return # Stop recording success = recorder.stop_recording() if success: self.logger.info(f"Stopped recording for camera {camera_name}") else: self.logger.error(f"Failed to stop recording for camera {camera_name}") except Exception as e: self.logger.error(f"Error stopping recording for {camera_name}: {e}") def get_camera_status(self, camera_name: str) -> Optional[Dict[str, Any]]: """Get status of a specific camera""" recorder = self.camera_recorders.get(camera_name) if not recorder: return None return recorder.get_status() def get_all_camera_status(self) -> Dict[str, Dict[str, Any]]: """Get status of all cameras""" status = {} with self._lock: for camera_name, recorder in self.camera_recorders.items(): status[camera_name] = recorder.get_status() return status def manual_start_recording(self, camera_name: str, filename: Optional[str] = None) -> bool: """Manually start recording for a camera""" recorder = self.camera_recorders.get(camera_name) if not recorder: self.logger.error(f"Camera not found: {camera_name}") return False if not filename: timestamp = format_filename_timestamp() filename = f"{camera_name}_manual_{timestamp}.avi" return recorder.start_recording(filename) def manual_stop_recording(self, camera_name: str) -> bool: """Manually stop recording for a camera""" recorder = self.camera_recorders.get(camera_name) if not recorder: self.logger.error(f"Camera not found: {camera_name}") return False return recorder.stop_recording() def get_available_cameras(self) -> List[Dict[str, Any]]: """Get list of available physical cameras""" cameras = [] for i, dev_info in enumerate(self.available_cameras): try: cameras.append({ "index": i, "name": dev_info.GetFriendlyName(), "port_type": dev_info.GetPortType(), "serial_number": getattr(dev_info, 'acSn', 'Unknown') }) except Exception as e: self.logger.error(f"Error getting info for camera {i}: {e}") return cameras def refresh_camera_discovery(self) -> int: """Refresh camera discovery and return number of cameras found""" self._discover_cameras() return len(self.available_cameras) def is_running(self) -> bool: """Check if camera manager is running""" return self.running