- Deleted unused API test files, RTSP diagnostic scripts, and development utility scripts to reduce clutter. - Removed outdated database schema and modularization proposal documents to maintain focus on current architecture. - Cleaned up configuration files and logging scripts that are no longer in use, enhancing project maintainability.
271 lines
12 KiB
Python
271 lines
12 KiB
Python
"""
|
|
Camera Monitor for the USDA Vision Camera System.
|
|
|
|
This module monitors camera status and availability at regular intervals.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import threading
|
|
import time
|
|
import logging
|
|
from typing import Dict, List, Optional, Any
|
|
|
|
# Add camera SDK to path
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "camera_sdk"))
|
|
import mvsdk
|
|
|
|
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
|
|
from .utils import suppress_camera_errors
|
|
from .constants import CAMERA_TEST_CAPTURE_TIMEOUT
|
|
|
|
|
|
class CameraMonitor:
|
|
"""Monitors camera status and availability"""
|
|
|
|
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager=None):
|
|
self.config = config
|
|
self.state_manager = state_manager
|
|
self.event_system = event_system
|
|
self.camera_manager = camera_manager # Reference to camera manager
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
# Monitoring settings
|
|
self.check_interval = config.system.camera_check_interval_seconds
|
|
|
|
# Threading
|
|
self.running = False
|
|
self._thread: Optional[threading.Thread] = None
|
|
self._stop_event = threading.Event()
|
|
|
|
# Status tracking
|
|
self.last_check_time: Optional[float] = None
|
|
self.check_count = 0
|
|
self.error_count = 0
|
|
|
|
def start(self) -> bool:
|
|
"""Start camera monitoring"""
|
|
if self.running:
|
|
self.logger.warning("Camera monitor is already running")
|
|
return True
|
|
|
|
self.logger.info(f"Starting camera monitor (check interval: {self.check_interval}s)")
|
|
self.running = True
|
|
self._stop_event.clear()
|
|
|
|
# Start monitoring thread
|
|
self._thread = threading.Thread(target=self._monitoring_loop, daemon=True)
|
|
self._thread.start()
|
|
|
|
return True
|
|
|
|
def stop(self) -> None:
|
|
"""Stop camera monitoring"""
|
|
if not self.running:
|
|
return
|
|
|
|
self.logger.info("Stopping camera monitor...")
|
|
self.running = False
|
|
self._stop_event.set()
|
|
|
|
if self._thread and self._thread.is_alive():
|
|
self._thread.join(timeout=5)
|
|
|
|
self.logger.info("Camera monitor stopped")
|
|
|
|
def _monitoring_loop(self) -> None:
|
|
"""Main monitoring loop"""
|
|
self.logger.info("Camera monitoring loop started")
|
|
|
|
while self.running and not self._stop_event.is_set():
|
|
try:
|
|
self.last_check_time = time.time()
|
|
self.check_count += 1
|
|
|
|
# Check all configured cameras
|
|
self._check_all_cameras()
|
|
|
|
# Wait for next check
|
|
if self._stop_event.wait(self.check_interval):
|
|
break
|
|
|
|
except Exception as e:
|
|
self.error_count += 1
|
|
self.logger.error(f"Error in camera monitoring loop: {e}")
|
|
|
|
# Wait a bit before retrying
|
|
if self._stop_event.wait(min(self.check_interval, 10)):
|
|
break
|
|
|
|
self.logger.info("Camera monitoring loop ended")
|
|
|
|
def _check_all_cameras(self) -> None:
|
|
"""Check status of all configured cameras"""
|
|
for camera_config in self.config.cameras:
|
|
if not camera_config.enabled:
|
|
continue
|
|
|
|
try:
|
|
self._check_camera_status(camera_config.name)
|
|
except Exception as e:
|
|
self.logger.error(f"Error checking camera {camera_config.name}: {e}")
|
|
|
|
def _check_camera_status(self, camera_name: str) -> None:
|
|
"""Check status of a specific camera"""
|
|
try:
|
|
# Get current status from state manager
|
|
current_info = self.state_manager.get_camera_status(camera_name)
|
|
|
|
# Perform actual camera check
|
|
status, details, device_info = self._perform_camera_check(camera_name)
|
|
|
|
# Update state if changed
|
|
old_status = current_info.status.value if current_info else "unknown"
|
|
if old_status != status:
|
|
self.state_manager.update_camera_status(name=camera_name, status=status, error=details if status == "error" else None, device_info=device_info)
|
|
|
|
# Publish status change event
|
|
publish_camera_status_changed(camera_name=camera_name, status=status, details=details)
|
|
|
|
self.logger.info(f"Camera {camera_name} status changed: {old_status} -> {status}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error checking camera {camera_name}: {e}")
|
|
|
|
# Update to error state
|
|
self.state_manager.update_camera_status(name=camera_name, status="error", error=str(e))
|
|
|
|
def _perform_camera_check(self, camera_name: str) -> tuple[str, str, Optional[Dict[str, Any]]]:
|
|
"""Perform actual camera availability check"""
|
|
try:
|
|
# Get camera device info from camera manager
|
|
if not self.camera_manager:
|
|
return "error", "Camera manager not available", None
|
|
|
|
device_info = self.camera_manager._find_camera_device(camera_name)
|
|
if not device_info:
|
|
return "disconnected", "Camera device not found", None
|
|
|
|
# ALWAYS check our streamer state first, before doing any camera availability tests
|
|
streamer = self.camera_manager.camera_streamers.get(camera_name)
|
|
self.logger.info(f"Checking streamer for {camera_name}: {streamer}")
|
|
if streamer and streamer.is_streaming():
|
|
self.logger.info(f"Camera {camera_name} is streaming - setting status to streaming")
|
|
return "streaming", "Camera streaming (live preview)", self._get_device_info_dict(device_info)
|
|
|
|
# Also check if our recorder is active
|
|
recorder = self.camera_manager.camera_recorders.get(camera_name)
|
|
if recorder and recorder.hCamera and recorder.recording:
|
|
self.logger.info(f"Camera {camera_name} is recording - setting status to available")
|
|
return "available", "Camera recording (in use by system)", self._get_device_info_dict(device_info)
|
|
|
|
# Check if camera is already opened by another process
|
|
try:
|
|
self.logger.info(f"Checking if camera {camera_name} is opened...")
|
|
is_opened = mvsdk.CameraIsOpened(device_info)
|
|
self.logger.info(f"CameraIsOpened result for {camera_name}: {is_opened}")
|
|
|
|
if is_opened:
|
|
self.logger.info(f"Camera {camera_name} is opened by another process - setting status to busy")
|
|
return "busy", "Camera opened by another process", self._get_device_info_dict(device_info)
|
|
else:
|
|
self.logger.info(f"Camera {camera_name} is not opened, will try initialization")
|
|
# Camera is not opened, so we can try to initialize it
|
|
pass
|
|
|
|
except Exception as e:
|
|
self.logger.warning(f"CameraIsOpened failed for {camera_name}: {e}")
|
|
# If we can't determine the status, try to initialize to see what happens
|
|
self.logger.info(f"CameraIsOpened failed for {camera_name}, will try initialization: {e}")
|
|
|
|
# Try to initialize camera briefly to test availability
|
|
try:
|
|
# Ensure SDK is initialized
|
|
ensure_sdk_initialized()
|
|
|
|
self.logger.info(f"Attempting to initialize camera {camera_name} for availability test...")
|
|
|
|
# Suppress output to avoid MVCAMAPI error messages during camera testing
|
|
hCamera = None
|
|
try:
|
|
with suppress_camera_errors():
|
|
hCamera = mvsdk.CameraInit(device_info, -1, -1)
|
|
self.logger.info(f"Camera {camera_name} initialized successfully, starting test capture...")
|
|
except mvsdk.CameraException as init_e:
|
|
self.logger.warning(f"CameraInit failed for {camera_name}: {init_e.message} (error_code: {init_e.error_code})")
|
|
return "error", f"Camera initialization failed: {init_e.message}", self._get_device_info_dict(device_info)
|
|
|
|
# Quick test - try to get one frame
|
|
try:
|
|
mvsdk.CameraSetTriggerMode(hCamera, 0)
|
|
mvsdk.CameraPlay(hCamera)
|
|
|
|
self.logger.info(f"Camera {camera_name} test: Attempting to capture frame with {CAMERA_TEST_CAPTURE_TIMEOUT}ms timeout...")
|
|
# Try to capture with short timeout
|
|
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(hCamera, CAMERA_TEST_CAPTURE_TIMEOUT)
|
|
mvsdk.CameraReleaseImageBuffer(hCamera, pRawData)
|
|
|
|
# Success - camera is available
|
|
mvsdk.CameraUnInit(hCamera)
|
|
self.logger.info(f"Camera {camera_name} test successful - camera is available")
|
|
return "available", "Camera test successful", self._get_device_info_dict(device_info)
|
|
|
|
except mvsdk.CameraException as capture_e:
|
|
if hCamera:
|
|
mvsdk.CameraUnInit(hCamera)
|
|
self.logger.warning(f"Camera {camera_name} capture test failed: {capture_e.message} (error_code: {capture_e.error_code})")
|
|
if capture_e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT:
|
|
return "available", "Camera available but slow response", self._get_device_info_dict(device_info)
|
|
else:
|
|
return "error", f"Camera test failed: {capture_e.message}", self._get_device_info_dict(device_info)
|
|
|
|
except mvsdk.CameraException as e:
|
|
self.logger.error(f"CameraException during initialization test for {camera_name}: {e.message} (error_code: {e.error_code})")
|
|
return "error", f"Camera initialization failed: {e.message}", self._get_device_info_dict(device_info) if device_info else None
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Unexpected exception during camera check for {camera_name}: {e}", exc_info=True)
|
|
return "error", f"Camera check failed: {str(e)}", None
|
|
|
|
def _get_device_info_dict(self, device_info) -> Dict[str, Any]:
|
|
"""Convert device info to dictionary"""
|
|
try:
|
|
return {"friendly_name": device_info.GetFriendlyName(), "port_type": device_info.GetPortType(), "serial_number": getattr(device_info, "acSn", "Unknown"), "last_checked": time.time()}
|
|
except Exception as e:
|
|
self.logger.error(f"Error getting device info: {e}")
|
|
return {"error": str(e)}
|
|
|
|
def check_camera_now(self, camera_name: str) -> Dict[str, Any]:
|
|
"""Manually check a specific camera status"""
|
|
try:
|
|
status, details, device_info = self._perform_camera_check(camera_name)
|
|
|
|
# Update state
|
|
self.state_manager.update_camera_status(name=camera_name, status=status, error=details if status == "error" else None, device_info=device_info)
|
|
|
|
return {"camera_name": camera_name, "status": status, "details": details, "device_info": device_info, "check_time": time.time()}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Manual camera check failed: {e}"
|
|
self.logger.error(error_msg)
|
|
return {"camera_name": camera_name, "status": "error", "details": error_msg, "device_info": None, "check_time": time.time()}
|
|
|
|
def check_all_cameras_now(self) -> Dict[str, Dict[str, Any]]:
|
|
"""Manually check all cameras"""
|
|
results = {}
|
|
for camera_config in self.config.cameras:
|
|
if camera_config.enabled:
|
|
results[camera_config.name] = self.check_camera_now(camera_config.name)
|
|
return results
|
|
|
|
def get_monitoring_stats(self) -> Dict[str, Any]:
|
|
"""Get monitoring statistics"""
|
|
return {"running": self.running, "check_interval_seconds": self.check_interval, "total_checks": self.check_count, "error_count": self.error_count, "last_check_time": self.last_check_time, "success_rate": (self.check_count - self.error_count) / max(self.check_count, 1) * 100}
|
|
|
|
def is_running(self) -> bool:
|
|
"""Check if monitor is running"""
|
|
return self.running
|