Enhance time synchronization checks, update storage paths, and improve camera recording management
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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"""
|
||||
|
||||
Binary file not shown.
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Binary file not shown.
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user