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

3
.gitignore vendored
View File

@@ -80,4 +80,5 @@ ehthumbs.db
Thumbs.db Thumbs.db
# Old test files (keep in repo for reference) # Old test files (keep in repo for reference)
# old tests/ # old tests/
Camera/log/*

80
api-tests.http Normal file
View File

@@ -0,0 +1,80 @@
### Get system status
GET http://localhost:8000/system/status
###
### Get camera1 status
GET http://localhost:8000/cameras/camera1/status
###
### Get camera2 status
GET http://localhost:8000/cameras/camera2/status
###
### Start recording camera1
POST http://localhost:8000/cameras/camera1/start-recording
Content-Type: application/json
{
"camera_name": "camera1",
"filename": "manual_test_cam1.avi"
}
###
### Start recording camera2
POST http://localhost:8000/cameras/camera2/start-recording
Content-Type: application/json
{
"camera_name": "camera2",
"filename": "manual_test_cam2.avi"
}
###
### Stop camera1 recording
POST http://localhost:8000/cameras/camera1/stop-recording
###
### Stop camera2 recording
POST http://localhost:8000/cameras/camera2/stop-recording
###
### Get all cameras status
GET http://localhost:8000/cameras
###
### Get storage statistics
GET http://localhost:8000/storage/stats
###
### Get storage files list
POST http://localhost:8000/storage/files
Content-Type: application/json
{
"camera_name": "camera1",
"limit": 10
}
###
### Get storage files list (all cameras)
POST http://localhost:8000/storage/files
Content-Type: application/json
{
"limit": 20
}
###
### Health check
GET http://localhost:8000/health

View File

@@ -27,32 +27,51 @@ def check_system_time():
print(f"Atlanta time: {atlanta_time}") print(f"Atlanta time: {atlanta_time}")
print(f"Timezone: {atlanta_time.tzname()}") print(f"Timezone: {atlanta_time.tzname()}")
# Check against world time API # Check against multiple time APIs for reliability
try: time_apis = [
print("\n🌐 Checking against world time API...") {
response = requests.get("http://worldtimeapi.org/api/timezone/America/New_York", timeout=5) "name": "WorldTimeAPI",
if response.status_code == 200: "url": "http://worldtimeapi.org/api/timezone/America/New_York",
data = response.json() "parser": lambda data: datetime.datetime.fromisoformat(data['datetime'].replace('Z', '+00:00'))
api_time = datetime.datetime.fromisoformat(data['datetime'].replace('Z', '+00:00')) },
{
# Compare times (allow 5 second difference) "name": "WorldClockAPI",
time_diff = abs((atlanta_time.replace(tzinfo=None) - api_time.replace(tzinfo=None)).total_seconds()) "url": "http://worldclockapi.com/api/json/est/now",
"parser": lambda data: datetime.datetime.fromisoformat(data['currentDateTime'])
print(f"API time: {api_time}") }
print(f"Time difference: {time_diff:.2f} seconds") ]
if time_diff < 5: for api in time_apis:
print("✅ Time is synchronized (within 5 seconds)") try:
return True print(f"\n🌐 Checking against {api['name']}...")
response = requests.get(api['url'], timeout=5)
if response.status_code == 200:
data = response.json()
api_time = api['parser'](data)
# Compare times (allow 5 second difference)
time_diff = abs((atlanta_time.replace(tzinfo=None) - api_time.replace(tzinfo=None)).total_seconds())
print(f"API time: {api_time}")
print(f"Time difference: {time_diff:.2f} seconds")
if time_diff < 5:
print("✅ Time is synchronized (within 5 seconds)")
return True
else:
print("❌ Time is NOT synchronized (difference > 5 seconds)")
return False
else: else:
print("❌ Time is NOT synchronized (difference > 5 seconds)") print(f"⚠️ {api['name']} returned status {response.status_code}")
return False continue
else: except Exception as e:
print("⚠️ Could not reach time API") print(f"⚠️ Error checking {api['name']}: {e}")
return None continue
except Exception as e:
print(f"⚠️ Error checking time API: {e}") print("⚠️ Could not reach any time API services")
return None print("⚠️ This may be due to network connectivity issues")
print("⚠️ System will continue but time synchronization cannot be verified")
return None
if __name__ == "__main__": if __name__ == "__main__":
check_system_time() check_system_time()

View File

@@ -10,7 +10,7 @@
} }
}, },
"storage": { "storage": {
"base_path": "./storage", "base_path": "/storage",
"max_file_size_mb": 1000, "max_file_size_mb": 1000,
"max_recording_duration_minutes": 60, "max_recording_duration_minutes": 60,
"cleanup_older_than_days": 30 "cleanup_older_than_days": 30
@@ -28,19 +28,19 @@
{ {
"name": "camera1", "name": "camera1",
"machine_topic": "vibratory_conveyor", "machine_topic": "vibratory_conveyor",
"storage_path": "./storage/camera1", "storage_path": "/storage/camera1",
"exposure_ms": 1.0, "exposure_ms": 1.0,
"gain": 3.5, "gain": 3.5,
"target_fps": 3.0, "target_fps": 0,
"enabled": true "enabled": true
}, },
{ {
"name": "camera2", "name": "camera2",
"machine_topic": "blower_separator", "machine_topic": "blower_separator",
"storage_path": "./storage/camera2", "storage_path": "/storage/camera2",
"exposure_ms": 1.0, "exposure_ms": 1.0,
"gain": 3.5, "gain": 3.5,
"target_fps": 3.0, "target_fps": 0,
"enabled": true "enabled": true
} }
] ]

View File

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

View File

@@ -147,21 +147,44 @@ class CameraManager:
device_info = self._find_camera_device(camera_config.name) device_info = self._find_camera_device(camera_config.name)
if device_info is None: if device_info is None:
self.logger.warning(f"No physical camera found for configured camera: {camera_config.name}") 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 continue
# Create recorder # Create recorder (this will attempt to initialize the camera)
recorder = CameraRecorder( recorder = CameraRecorder(
camera_config=camera_config, camera_config=camera_config,
device_info=device_info, device_info=device_info,
state_manager=self.state_manager, state_manager=self.state_manager,
event_system=self.event_system 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.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: except Exception as e:
self.logger.error(f"Error initializing recorder for {camera_config.name}: {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]: def _find_camera_device(self, camera_name: str) -> Optional[Any]:
"""Find physical camera device for a configured camera""" """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: class CameraRecorder:
"""Handles video recording for a single camera""" """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.camera_config = camera_config
self.device_info = device_info self.device_info = device_info
self.state_manager = state_manager self.state_manager = state_manager
self.event_system = event_system self.event_system = event_system
self.storage_manager = storage_manager
self.logger = logging.getLogger(f"{__name__}.{camera_config.name}") self.logger = logging.getLogger(f"{__name__}.{camera_config.name}")
# Camera handle and properties # Camera handle and properties
@@ -61,39 +62,47 @@ class CameraRecorder:
"""Initialize the camera with configured settings""" """Initialize the camera with configured settings"""
try: try:
self.logger.info(f"Initializing camera: {self.camera_config.name}") 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 # Initialize camera
self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1) self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1)
self.logger.info("Camera initialized successfully") self.logger.info("Camera initialized successfully")
# Get camera capabilities # Get camera capabilities
self.cap = mvsdk.CameraGetCapability(self.hCamera) self.cap = mvsdk.CameraGetCapability(self.hCamera)
self.monoCamera = self.cap.sIspCapacity.bMonoSensor != 0 self.monoCamera = self.cap.sIspCapacity.bMonoSensor != 0
self.logger.info(f"Camera type: {'Monochrome' if self.monoCamera else 'Color'}") self.logger.info(f"Camera type: {'Monochrome' if self.monoCamera else 'Color'}")
# Set output format # Set output format
if self.monoCamera: if self.monoCamera:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO8) mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO8)
else: else:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR8) mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR8)
# Configure camera settings # Configure camera settings
self._configure_camera_settings() self._configure_camera_settings()
# Allocate frame buffer # Allocate frame buffer
self.frame_buffer_size = (self.cap.sResolutionRange.iWidthMax * self.frame_buffer_size = (self.cap.sResolutionRange.iWidthMax *
self.cap.sResolutionRange.iHeightMax * self.cap.sResolutionRange.iHeightMax *
(1 if self.monoCamera else 3)) (1 if self.monoCamera else 3))
self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16) self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16)
# Start camera # Start camera
mvsdk.CameraPlay(self.hCamera) mvsdk.CameraPlay(self.hCamera)
self.logger.info("Camera started successfully") self.logger.info("Camera started successfully")
return True return True
except mvsdk.CameraException as e: 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 return False
except Exception as e: except Exception as e:
self.logger.error(f"Unexpected error during camera initialization: {e}") self.logger.error(f"Unexpected error during camera initialization: {e}")
@@ -251,8 +260,9 @@ class CameraRecorder:
# Release buffer # Release buffer
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData) mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
# Control frame rate # Control frame rate (skip sleep if target_fps is 0 for maximum speed)
time.sleep(1.0 / self.camera_config.target_fps) if self.camera_config.target_fps > 0:
time.sleep(1.0 / self.camera_config.target_fps)
except mvsdk.CameraException as e: except mvsdk.CameraException as e:
if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT: if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT:
@@ -284,10 +294,13 @@ class CameraRecorder:
fourcc = cv2.VideoWriter_fourcc(*'XVID') fourcc = cv2.VideoWriter_fourcc(*'XVID')
frame_size = (FrameHead.iWidth, FrameHead.iHeight) 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.video_writer = cv2.VideoWriter(
self.output_filename, self.output_filename,
fourcc, fourcc,
self.camera_config.target_fps, video_fps,
frame_size frame_size
) )
@@ -305,14 +318,17 @@ class CameraRecorder:
def _convert_frame_to_opencv(self, frame_head) -> Optional[np.ndarray]: def _convert_frame_to_opencv(self, frame_head) -> Optional[np.ndarray]:
"""Convert camera frame to OpenCV format""" """Convert camera frame to OpenCV format"""
try: 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: if self.monoCamera:
# Monochrome camera - convert to BGR # 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 = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR) frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
else: else:
# Color camera - already in BGR format # 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)) frame_bgr = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
return frame_bgr return frame_bgr

View File

@@ -45,7 +45,7 @@ class USDAVisionSystem:
self.event_system = EventSystem() self.event_system = EventSystem()
# Initialize system components # 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.mqtt_client = MQTTClient(self.config, self.state_manager, self.event_system)
self.camera_manager = CameraManager(self.config, self.state_manager, self.event_system) self.camera_manager = CameraManager(self.config, self.state_manager, self.event_system)
self.api_server = APIServer( self.api_server = APIServer(

View File

@@ -14,23 +14,29 @@ import json
from ..core.config import Config, StorageConfig from ..core.config import Config, StorageConfig
from ..core.state_manager import StateManager from ..core.state_manager import StateManager
from ..core.events import EventSystem, EventType, Event
class StorageManager: class StorageManager:
"""Manages storage and file organization for recorded videos""" """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.config = config
self.storage_config = config.storage self.storage_config = config.storage
self.state_manager = state_manager self.state_manager = state_manager
self.event_system = event_system
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
# Ensure base storage directory exists # Ensure base storage directory exists
self._ensure_storage_structure() self._ensure_storage_structure()
# File tracking # File tracking
self.file_index_path = os.path.join(self.storage_config.base_path, "file_index.json") self.file_index_path = os.path.join(self.storage_config.base_path, "file_index.json")
self.file_index = self._load_file_index() 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: def _ensure_storage_structure(self) -> None:
"""Ensure storage directory structure exists""" """Ensure storage directory structure exists"""
@@ -48,6 +54,44 @@ class StorageManager:
except Exception as e: except Exception as e:
self.logger.error(f"Error creating storage structure: {e}") self.logger.error(f"Error creating storage structure: {e}")
raise 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]: def _load_file_index(self) -> Dict[str, Any]:
"""Load file index from disk""" """Load file index from disk"""
@@ -98,6 +142,33 @@ class StorageManager:
except Exception as e: except Exception as e:
self.logger.error(f"Error registering recording file: {e}") self.logger.error(f"Error registering recording file: {e}")
return "" 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, def finalize_recording_file(self, file_id: str, end_time: datetime,
duration_seconds: float, frame_count: Optional[int] = None) -> bool: duration_seconds: float, frame_count: Optional[int] = None) -> bool: