feat: Add MQTT publisher and tester scripts for USDA Vision Camera System

- Implemented mqtt_publisher_test.py for manual MQTT message publishing
- Created mqtt_test.py to test MQTT message reception and display statistics
- Developed test_api_changes.py to verify API changes for camera settings and filename handling
- Added test_camera_recovery_api.py for testing camera recovery API endpoints
- Introduced test_max_fps.py to demonstrate maximum FPS capture functionality
- Implemented test_mqtt_events_api.py to test MQTT events API endpoint
- Created test_mqtt_logging.py for enhanced MQTT logging and API endpoint testing
- Added sdk_config.py for SDK initialization and configuration with error suppression
This commit is contained in:
Alireza Vaezi
2025-07-28 16:30:14 -04:00
parent e2acebc056
commit 9cb043ef5f
40 changed files with 4485 additions and 838 deletions

View File

@@ -13,7 +13,7 @@ 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'))
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "python demo"))
import mvsdk
from ..core.config import Config, CameraConfig
@@ -22,271 +22,233 @@ from ..core.events import EventSystem, EventType, Event, publish_camera_status_c
from ..core.timezone_utils import format_filename_timestamp
from .recorder import CameraRecorder
from .monitor import CameraMonitor
from .sdk_config import initialize_sdk_with_suppression
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__)
# Initialize SDK early to suppress error messages
initialize_sdk_with_suppression()
# 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
)
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')
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
}
)
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
)
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
# Create recorder (uses lazy initialization - camera will be initialized when recording starts)
recorder = CameraRecorder(camera_config=camera_config, device_info=device_info, state_manager=self.state_manager, event_system=self.event_system)
# Add recorder to the list (camera will be initialized lazily when needed)
self.camera_recorders[camera_config.name] = recorder
self.logger.info(f"Successfully initialized recorder for camera: {camera_config.name}")
self.logger.info(f"Successfully created recorder for camera: {camera_config.name} (lazy initialization)")
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)}
)
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
}
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 = {}
@@ -294,50 +256,174 @@ class CameraManager:
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"""
def manual_start_recording(self, camera_name: str, filename: Optional[str] = None, exposure_ms: Optional[float] = None, gain: Optional[float] = None, fps: Optional[float] = None) -> bool:
"""Manually start recording for a camera with optional camera settings"""
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()
# Update camera settings if provided
if exposure_ms is not None or gain is not None or fps is not None:
settings_updated = recorder.update_camera_settings(exposure_ms=exposure_ms, gain=gain, target_fps=fps)
if not settings_updated:
self.logger.warning(f"Failed to update camera settings for {camera_name}")
# Generate filename with datetime prefix
timestamp = format_filename_timestamp()
if filename:
# Always prepend datetime to the provided filename
filename = f"{timestamp}_{filename}"
else:
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')
})
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
def test_camera_connection(self, camera_name: str) -> bool:
"""Test connection for a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
return recorder.test_connection()
def reconnect_camera(self, camera_name: str) -> bool:
"""Attempt to reconnect a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
success = recorder.reconnect()
# Update camera status based on result
if success:
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
else:
self.state_manager.update_camera_status(name=camera_name, status="connection_failed", error="Reconnection failed")
return success
def restart_camera_grab(self, camera_name: str) -> bool:
"""Restart grab process for a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
success = recorder.restart_grab()
# Update camera status based on result
if success:
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
else:
self.state_manager.update_camera_status(name=camera_name, status="grab_failed", error="Grab restart failed")
return success
def reset_camera_timestamp(self, camera_name: str) -> bool:
"""Reset timestamp for a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
return recorder.reset_timestamp()
def full_reset_camera(self, camera_name: str) -> bool:
"""Perform full reset for a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
success = recorder.full_reset()
# Update camera status based on result
if success:
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
else:
self.state_manager.update_camera_status(name=camera_name, status="reset_failed", error="Full reset failed")
return success
def reinitialize_failed_camera(self, camera_name: str) -> bool:
"""Attempt to reinitialize a camera that failed to initialize"""
with self._lock:
# Find the camera config
camera_config = None
for config in self.config.cameras:
if config.name == camera_name:
camera_config = config
break
if not camera_config:
self.logger.error(f"No configuration found for camera: {camera_name}")
return False
if not camera_config.enabled:
self.logger.error(f"Camera {camera_name} is disabled in configuration")
return False
try:
# Remove existing recorder if any
if camera_name in self.camera_recorders:
old_recorder = self.camera_recorders[camera_name]
try:
old_recorder._cleanup_camera()
except:
pass # Ignore cleanup errors
del self.camera_recorders[camera_name]
# Find matching physical camera
device_info = self._find_camera_device(camera_name)
if device_info is None:
self.logger.warning(f"No physical camera found for configured camera: {camera_name}")
self.state_manager.update_camera_status(name=camera_name, status="not_found", device_info=None)
return False
# Create new recorder (uses lazy initialization)
recorder = CameraRecorder(camera_config=camera_config, device_info=device_info, state_manager=self.state_manager, event_system=self.event_system)
# Success - add to recorders (camera will be initialized lazily when needed)
self.camera_recorders[camera_name] = recorder
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
self.logger.info(f"Successfully reinitialized camera recorder: {camera_name} (lazy initialization)")
return True
except Exception as e:
self.logger.error(f"Error reinitializing camera {camera_name}: {e}")
self.state_manager.update_camera_status(name=camera_name, status="error", device_info={"error": str(e)})
return False

View File

@@ -9,240 +9,236 @@ import os
import threading
import time
import logging
import contextlib
from typing import Dict, List, Optional, Any
# Add python demo to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo'))
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "python demo"))
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
@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)
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
)
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
)
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)
)
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
# Check if camera is already opened by another process
if mvsdk.CameraIsOpened(device_info):
# Camera is opened - check if it's our recorder
# Camera is opened - check if it's our recorder that's currently recording
recorder = self.camera_manager.camera_recorders.get(camera_name)
if recorder and recorder.hCamera:
return "available", "Camera initialized and ready", self._get_device_info_dict(device_info)
if recorder and recorder.hCamera and recorder.recording:
return "available", "Camera recording (in use by system)", self._get_device_info_dict(device_info)
else:
return "busy", "Camera opened by another process", self._get_device_info_dict(device_info)
# Try to initialize camera briefly to test availability
try:
hCamera = mvsdk.CameraInit(device_info, -1, -1)
# Ensure SDK is initialized
ensure_sdk_initialized()
# Suppress output to avoid MVCAMAPI error messages during camera testing
with suppress_camera_errors():
hCamera = mvsdk.CameraInit(device_info, -1, -1)
# Quick test - try to get one frame
try:
mvsdk.CameraSetTriggerMode(hCamera, 0)
mvsdk.CameraPlay(hCamera)
# Try to capture with short timeout
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(hCamera, 500)
mvsdk.CameraReleaseImageBuffer(hCamera, pRawData)
# Success - camera is available
mvsdk.CameraUnInit(hCamera)
return "available", "Camera test successful", self._get_device_info_dict(device_info)
except mvsdk.CameraException as e:
mvsdk.CameraUnInit(hCamera)
if 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: {e.message}", self._get_device_info_dict(device_info)
except mvsdk.CameraException as e:
return "error", f"Camera initialization failed: {e.message}", self._get_device_info_dict(device_info)
except Exception as e:
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()
}
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()
}
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()
}
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 = {}
@@ -250,18 +246,11 @@ class CameraMonitor:
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
}
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

View File

@@ -11,18 +11,44 @@ import time
import logging
import cv2
import numpy as np
import contextlib
from typing import Optional, Dict, Any
from datetime import datetime
from pathlib import Path
# Add python demo to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo'))
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "python demo"))
import mvsdk
from ..core.config import CameraConfig
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)
class CameraRecorder:
@@ -35,41 +61,46 @@ class CameraRecorder:
self.event_system = event_system
self.storage_manager = storage_manager
self.logger = logging.getLogger(f"{__name__}.{camera_config.name}")
# Camera handle and properties
self.hCamera: Optional[int] = None
self.cap = None
self.monoCamera = False
self.frame_buffer = None
self.frame_buffer_size = 0
# Recording state
self.recording = False
self.video_writer: Optional[cv2.VideoWriter] = None
self.output_filename: Optional[str] = None
self.frame_count = 0
self.start_time: Optional[datetime] = None
# Threading
self._recording_thread: Optional[threading.Thread] = None
self._stop_recording_event = threading.Event()
self._lock = threading.RLock()
# Initialize camera
self._initialize_camera()
# Don't initialize camera immediately - use lazy initialization
# Camera will be initialized when recording starts
self.logger.info(f"Camera recorder created for: {self.camera_config.name} (lazy initialization)")
def _initialize_camera(self) -> bool:
"""Initialize the camera with configured settings"""
try:
self.logger.info(f"Initializing camera: {self.camera_config.name}")
# Ensure SDK is initialized
ensure_sdk_initialized()
# 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)
# Initialize camera (suppress output to avoid MVCAMAPI error messages)
with suppress_camera_errors():
self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1)
self.logger.info("Camera initialized successfully")
# Get camera capabilities
@@ -104,9 +135,7 @@ class CameraRecorder:
# Allocate frame buffer based on bit depth
bytes_per_pixel = self._get_bytes_per_pixel()
self.frame_buffer_size = (self.cap.sResolutionRange.iWidthMax *
self.cap.sResolutionRange.iHeightMax *
bytes_per_pixel)
self.frame_buffer_size = self.cap.sResolutionRange.iWidthMax * self.cap.sResolutionRange.iHeightMax * bytes_per_pixel
self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16)
# Start camera
@@ -124,7 +153,30 @@ class CameraRecorder:
except Exception as e:
self.logger.error(f"Unexpected error during camera initialization: {e}")
return False
def _get_bytes_per_pixel(self) -> int:
"""Calculate bytes per pixel based on camera type and bit depth"""
if self.monoCamera:
# Monochrome camera
if self.camera_config.bit_depth >= 16:
return 2 # 16-bit mono
elif self.camera_config.bit_depth >= 12:
return 2 # 12-bit mono (stored in 16-bit)
elif self.camera_config.bit_depth >= 10:
return 2 # 10-bit mono (stored in 16-bit)
else:
return 1 # 8-bit mono
else:
# Color camera
if self.camera_config.bit_depth >= 16:
return 6 # 16-bit RGB (2 bytes × 3 channels)
elif self.camera_config.bit_depth >= 12:
return 6 # 12-bit RGB (stored as 16-bit)
elif self.camera_config.bit_depth >= 10:
return 6 # 10-bit RGB (stored as 16-bit)
else:
return 3 # 8-bit RGB
def _configure_camera_settings(self) -> None:
"""Configure camera settings from config"""
try:
@@ -174,8 +226,7 @@ class CameraRecorder:
if not self.monoCamera:
mvsdk.CameraSetSaturation(self.hCamera, self.camera_config.saturation)
self.logger.info(f"Image quality configured - Sharpness: {self.camera_config.sharpness}, "
f"Contrast: {self.camera_config.contrast}, Gamma: {self.camera_config.gamma}")
self.logger.info(f"Image quality configured - Sharpness: {self.camera_config.sharpness}, " f"Contrast: {self.camera_config.contrast}, Gamma: {self.camera_config.gamma}")
except Exception as e:
self.logger.warning(f"Error configuring image quality: {e}")
@@ -194,8 +245,7 @@ class CameraRecorder:
else:
mvsdk.CameraSetDenoise3DParams(self.hCamera, False, 2, None)
self.logger.info(f"Noise reduction configured - Filter: {self.camera_config.noise_filter_enabled}, "
f"3D Denoise: {self.camera_config.denoise_3d_enabled}")
self.logger.info(f"Noise reduction configured - Filter: {self.camera_config.noise_filter_enabled}, " f"3D Denoise: {self.camera_config.denoise_3d_enabled}")
except Exception as e:
self.logger.warning(f"Error configuring noise reduction: {e}")
@@ -210,8 +260,7 @@ class CameraRecorder:
if not self.camera_config.auto_white_balance:
mvsdk.CameraSetPresetClrTemp(self.hCamera, self.camera_config.color_temperature_preset)
self.logger.info(f"Color settings configured - Auto WB: {self.camera_config.auto_white_balance}, "
f"Color Temp Preset: {self.camera_config.color_temperature_preset}")
self.logger.info(f"Color settings configured - Auto WB: {self.camera_config.auto_white_balance}, " f"Color Temp Preset: {self.camera_config.color_temperature_preset}")
except Exception as e:
self.logger.warning(f"Error configuring color settings: {e}")
@@ -225,61 +274,104 @@ class CameraRecorder:
# Set light frequency (0=50Hz, 1=60Hz)
mvsdk.CameraSetLightFrequency(self.hCamera, self.camera_config.light_frequency)
# Configure HDR if enabled
if self.camera_config.hdr_enabled:
mvsdk.CameraSetHDR(self.hCamera, 1) # Enable HDR
mvsdk.CameraSetHDRGainMode(self.hCamera, self.camera_config.hdr_gain_mode)
self.logger.info(f"HDR enabled with gain mode: {self.camera_config.hdr_gain_mode}")
else:
mvsdk.CameraSetHDR(self.hCamera, 0) # Disable HDR
# Configure HDR if enabled (check if HDR functions are available)
try:
if self.camera_config.hdr_enabled:
mvsdk.CameraSetHDR(self.hCamera, 1) # Enable HDR
mvsdk.CameraSetHDRGainMode(self.hCamera, self.camera_config.hdr_gain_mode)
self.logger.info(f"HDR enabled with gain mode: {self.camera_config.hdr_gain_mode}")
else:
mvsdk.CameraSetHDR(self.hCamera, 0) # Disable HDR
except AttributeError:
self.logger.info("HDR functions not available in this SDK version, skipping HDR configuration")
self.logger.info(f"Advanced settings configured - Anti-flicker: {self.camera_config.anti_flicker_enabled}, "
f"Light Freq: {self.camera_config.light_frequency}Hz, HDR: {self.camera_config.hdr_enabled}")
self.logger.info(f"Advanced settings configured - Anti-flicker: {self.camera_config.anti_flicker_enabled}, " f"Light Freq: {self.camera_config.light_frequency}Hz, HDR: {self.camera_config.hdr_enabled}")
except Exception as e:
self.logger.warning(f"Error configuring advanced settings: {e}")
def update_camera_settings(self, exposure_ms: Optional[float] = None, gain: Optional[float] = None, target_fps: Optional[float] = None) -> bool:
"""Update camera settings dynamically"""
if not self.hCamera:
self.logger.error("Camera not initialized")
return False
try:
settings_updated = False
# Update exposure if provided
if exposure_ms is not None:
mvsdk.CameraSetAeState(self.hCamera, 0) # Disable auto exposure
exposure_us = int(exposure_ms * 1000) # Convert ms to microseconds
mvsdk.CameraSetExposureTime(self.hCamera, exposure_us)
self.camera_config.exposure_ms = exposure_ms
self.logger.info(f"Updated exposure time: {exposure_ms}ms")
settings_updated = True
# Update gain if provided
if gain is not None:
gain_value = int(gain * 100) # Convert to camera units
mvsdk.CameraSetAnalogGain(self.hCamera, gain_value)
self.camera_config.gain = gain
self.logger.info(f"Updated gain: {gain}x")
settings_updated = True
# Update target FPS if provided
if target_fps is not None:
self.camera_config.target_fps = target_fps
self.logger.info(f"Updated target FPS: {target_fps}")
settings_updated = True
return settings_updated
except Exception as e:
self.logger.error(f"Error updating camera settings: {e}")
return False
def start_recording(self, filename: str) -> bool:
"""Start video recording"""
with self._lock:
if self.recording:
self.logger.warning("Already recording!")
return False
# Initialize camera if not already initialized (lazy initialization)
if not self.hCamera:
self.logger.error("Camera not initialized")
return False
self.logger.info("Camera not initialized, initializing now...")
if not self._initialize_camera():
self.logger.error("Failed to initialize camera for recording")
return False
try:
# Prepare output path
output_path = os.path.join(self.camera_config.storage_path, filename)
Path(self.camera_config.storage_path).mkdir(parents=True, exist_ok=True)
# Test camera capture before starting recording
if not self._test_camera_capture():
self.logger.error("Camera capture test failed")
return False
# Initialize recording state
self.output_filename = output_path
self.frame_count = 0
self.start_time = now_atlanta() # Use Atlanta timezone
self._stop_recording_event.clear()
# Start recording thread
self._recording_thread = threading.Thread(target=self._recording_loop, daemon=True)
self._recording_thread.start()
# Update state
self.recording = True
recording_id = self.state_manager.start_recording(self.camera_config.name, output_path)
# Publish event
publish_recording_started(self.camera_config.name, output_path)
self.logger.info(f"Started recording to: {output_path}")
return True
except Exception as e:
self.logger.error(f"Error starting recording: {e}")
publish_recording_error(self.camera_config.name, str(e))
@@ -329,11 +421,11 @@ class CameraRecorder:
self.state_manager.stop_recording(self.output_filename, file_size, self.frame_count)
# Publish event
publish_recording_stopped(
self.camera_config.name,
self.output_filename or "unknown",
duration
)
publish_recording_stopped(self.camera_config.name, self.output_filename or "unknown", duration)
# Clean up camera resources after recording (lazy cleanup)
self._cleanup_camera()
self.logger.info("Camera resources cleaned up after recording")
self.logger.info(f"Stopped recording - Duration: {duration:.1f}s, Frames: {self.frame_count}")
return True
@@ -402,18 +494,13 @@ class CameraRecorder:
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
# Set up video writer
fourcc = cv2.VideoWriter_fourcc(*'XVID')
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,
video_fps,
frame_size
)
self.video_writer = cv2.VideoWriter(self.output_filename, fourcc, video_fps, frame_size)
if not self.video_writer.isOpened():
self.logger.error(f"Failed to open video writer for {self.output_filename}")
@@ -432,15 +519,34 @@ class CameraRecorder:
# 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 = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
# Handle different bit depths
if self.camera_config.bit_depth > 8:
# For >8-bit, data is stored as 16-bit values
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint16)
if self.monoCamera:
# Monochrome camera - convert to 8-bit BGR for video
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
# Scale down to 8-bit (simple right shift)
frame_8bit = (frame >> (self.camera_config.bit_depth - 8)).astype(np.uint8)
frame_bgr = cv2.cvtColor(frame_8bit, cv2.COLOR_GRAY2BGR)
else:
# Color camera - convert to 8-bit BGR
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
# Scale down to 8-bit
frame_bgr = (frame >> (self.camera_config.bit_depth - 8)).astype(np.uint8)
else:
# Color camera - already in BGR format
frame_bgr = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
# 8-bit data
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint8)
if self.monoCamera:
# Monochrome camera - convert to BGR
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_bgr = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
return frame_bgr
@@ -460,6 +566,175 @@ class CameraRecorder:
except Exception as e:
self.logger.error(f"Error during recording cleanup: {e}")
def test_connection(self) -> bool:
"""Test camera connection"""
try:
if self.hCamera is None:
self.logger.error("Camera not initialized")
return False
# Test connection using SDK function
result = mvsdk.CameraConnectTest(self.hCamera)
if result == 0: # CAMERA_STATUS_SUCCESS
self.logger.info("Camera connection test passed")
return True
else:
self.logger.error(f"Camera connection test failed with code: {result}")
return False
except Exception as e:
self.logger.error(f"Error testing camera connection: {e}")
return False
def reconnect(self) -> bool:
"""Attempt to reconnect to the camera"""
try:
if self.hCamera is None:
self.logger.error("Camera not initialized, cannot reconnect")
return False
self.logger.info("Attempting to reconnect camera...")
# Stop any ongoing operations
if self.recording:
self.logger.info("Stopping recording before reconnect")
self.stop_recording()
# Attempt reconnection using SDK function
result = mvsdk.CameraReConnect(self.hCamera)
if result == 0: # CAMERA_STATUS_SUCCESS
self.logger.info("Camera reconnected successfully")
# Restart camera if it was playing
try:
mvsdk.CameraPlay(self.hCamera)
self.logger.info("Camera restarted after reconnection")
except Exception as e:
self.logger.warning(f"Failed to restart camera after reconnection: {e}")
return True
else:
self.logger.error(f"Camera reconnection failed with code: {result}")
return False
except Exception as e:
self.logger.error(f"Error during camera reconnection: {e}")
return False
def restart_grab(self) -> bool:
"""Restart the camera grab process"""
try:
if self.hCamera is None:
self.logger.error("Camera not initialized")
return False
self.logger.info("Restarting camera grab process...")
# Stop any ongoing recording
if self.recording:
self.logger.info("Stopping recording before restart")
self.stop_recording()
# Restart grab using SDK function
result = mvsdk.CameraRestartGrab(self.hCamera)
if result == 0: # CAMERA_STATUS_SUCCESS
self.logger.info("Camera grab restarted successfully")
return True
else:
self.logger.error(f"Camera grab restart failed with code: {result}")
return False
except Exception as e:
self.logger.error(f"Error restarting camera grab: {e}")
return False
def reset_timestamp(self) -> bool:
"""Reset camera timestamp"""
try:
if self.hCamera is None:
self.logger.error("Camera not initialized")
return False
self.logger.info("Resetting camera timestamp...")
result = mvsdk.CameraRstTimeStamp(self.hCamera)
if result == 0: # CAMERA_STATUS_SUCCESS
self.logger.info("Camera timestamp reset successfully")
return True
else:
self.logger.error(f"Camera timestamp reset failed with code: {result}")
return False
except Exception as e:
self.logger.error(f"Error resetting camera timestamp: {e}")
return False
def full_reset(self) -> bool:
"""Perform a full camera reset (uninitialize and reinitialize)"""
try:
self.logger.info("Performing full camera reset...")
# Stop any ongoing recording
if self.recording:
self.logger.info("Stopping recording before reset")
self.stop_recording()
# Store device info for reinitialization
device_info = self.device_info
# Cleanup current camera
self._cleanup_camera()
# Wait a moment
time.sleep(1)
# Reinitialize camera
self.device_info = device_info
success = self._initialize_camera()
if success:
self.logger.info("Full camera reset completed successfully")
return True
else:
self.logger.error("Full camera reset failed during reinitialization")
return False
except Exception as e:
self.logger.error(f"Error during full camera reset: {e}")
return False
def _cleanup_camera(self) -> None:
"""Clean up camera resources"""
try:
# Stop camera if running
if self.hCamera is not None:
try:
mvsdk.CameraStop(self.hCamera)
except:
pass # Ignore errors during stop
# Uninitialize camera
try:
mvsdk.CameraUnInit(self.hCamera)
except:
pass # Ignore errors during uninit
self.hCamera = None
# Free frame buffer
if self.frame_buffer is not None:
try:
mvsdk.CameraAlignFree(self.frame_buffer)
except:
pass # Ignore errors during free
self.frame_buffer = None
self.logger.info("Camera resources cleaned up")
except Exception as e:
self.logger.error(f"Error during camera cleanup: {e}")
def cleanup(self) -> None:
"""Clean up camera resources"""
try:
@@ -488,12 +763,4 @@ class CameraRecorder:
def get_status(self) -> Dict[str, Any]:
"""Get recorder status"""
return {
"camera_name": self.camera_config.name,
"is_recording": self.recording,
"current_file": self.output_filename,
"frame_count": self.frame_count,
"start_time": self.start_time.isoformat() if self.start_time else None,
"camera_initialized": self.hCamera is not None,
"storage_path": self.camera_config.storage_path
}
return {"camera_name": self.camera_config.name, "is_recording": self.recording, "current_file": self.output_filename, "frame_count": self.frame_count, "start_time": self.start_time.isoformat() if self.start_time else None, "camera_initialized": self.hCamera is not None, "storage_path": self.camera_config.storage_path}

View File

@@ -0,0 +1,89 @@
"""
SDK Configuration for the USDA Vision Camera System.
This module handles SDK initialization and configuration to suppress error messages.
"""
import sys
import os
import logging
# Add python demo to path
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "python demo"))
import mvsdk
logger = logging.getLogger(__name__)
# Global flag to track SDK initialization
_sdk_initialized = False
def initialize_sdk_with_suppression():
"""Initialize the camera SDK with error suppression"""
global _sdk_initialized
if _sdk_initialized:
return True
try:
# Initialize SDK with English language
result = mvsdk.CameraSdkInit(1)
if result == 0:
logger.info("Camera SDK initialized successfully")
# Try to set system options to suppress logging
try:
# These are common options that might control logging
# We'll try them and ignore failures since they might not be supported
# Try to disable debug output
try:
mvsdk.CameraSetSysOption("DebugLevel", "0")
except:
pass
# Try to disable console output
try:
mvsdk.CameraSetSysOption("ConsoleOutput", "0")
except:
pass
# Try to disable error logging
try:
mvsdk.CameraSetSysOption("ErrorLog", "0")
except:
pass
# Try to set log level to none
try:
mvsdk.CameraSetSysOption("LogLevel", "0")
except:
pass
# Try to disable verbose mode
try:
mvsdk.CameraSetSysOption("Verbose", "0")
except:
pass
logger.debug("Attempted to configure SDK logging options")
except Exception as e:
logger.debug(f"Could not configure SDK logging options: {e}")
_sdk_initialized = True
return True
else:
logger.error(f"SDK initialization failed with code: {result}")
return False
except Exception as e:
logger.error(f"SDK initialization failed: {e}")
return False
def ensure_sdk_initialized():
"""Ensure the SDK is initialized before camera operations"""
if not _sdk_initialized:
return initialize_sdk_with_suppression()
return True