diff --git a/.gitignore b/.gitignore index 081d6ea..8b17556 100644 --- a/.gitignore +++ b/.gitignore @@ -80,4 +80,5 @@ ehthumbs.db Thumbs.db # Old test files (keep in repo for reference) -# old tests/ \ No newline at end of file +# old tests/ +Camera/log/* diff --git a/api-tests.http b/api-tests.http new file mode 100644 index 0000000..23c58e1 --- /dev/null +++ b/api-tests.http @@ -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 \ No newline at end of file diff --git a/check_time.py b/check_time.py index a8ee0c5..50c7916 100755 --- a/check_time.py +++ b/check_time.py @@ -27,32 +27,51 @@ def check_system_time(): print(f"Atlanta time: {atlanta_time}") print(f"Timezone: {atlanta_time.tzname()}") - # Check against world time API - try: - print("\n🌐 Checking against world time API...") - response = requests.get("http://worldtimeapi.org/api/timezone/America/New_York", timeout=5) - if response.status_code == 200: - data = response.json() - api_time = datetime.datetime.fromisoformat(data['datetime'].replace('Z', '+00:00')) - - # 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 + # Check against multiple time APIs for reliability + time_apis = [ + { + "name": "WorldTimeAPI", + "url": "http://worldtimeapi.org/api/timezone/America/New_York", + "parser": lambda data: datetime.datetime.fromisoformat(data['datetime'].replace('Z', '+00:00')) + }, + { + "name": "WorldClockAPI", + "url": "http://worldclockapi.com/api/json/est/now", + "parser": lambda data: datetime.datetime.fromisoformat(data['currentDateTime']) + } + ] + + for api in time_apis: + try: + 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: - print("❌ Time is NOT synchronized (difference > 5 seconds)") - return False - else: - print("⚠️ Could not reach time API") - return None - except Exception as e: - print(f"⚠️ Error checking time API: {e}") - return None + print(f"⚠️ {api['name']} returned status {response.status_code}") + continue + except Exception as e: + print(f"⚠️ Error checking {api['name']}: {e}") + continue + + print("⚠️ Could not reach any time API services") + print("⚠️ This may be due to network connectivity issues") + print("⚠️ System will continue but time synchronization cannot be verified") + return None if __name__ == "__main__": check_system_time() diff --git a/config.json b/config.json index dd5f01f..ce985ea 100644 --- a/config.json +++ b/config.json @@ -10,7 +10,7 @@ } }, "storage": { - "base_path": "./storage", + "base_path": "/storage", "max_file_size_mb": 1000, "max_recording_duration_minutes": 60, "cleanup_older_than_days": 30 @@ -28,19 +28,19 @@ { "name": "camera1", "machine_topic": "vibratory_conveyor", - "storage_path": "./storage/camera1", + "storage_path": "/storage/camera1", "exposure_ms": 1.0, "gain": 3.5, - "target_fps": 3.0, + "target_fps": 0, "enabled": true }, { "name": "camera2", "machine_topic": "blower_separator", - "storage_path": "./storage/camera2", + "storage_path": "/storage/camera2", "exposure_ms": 1.0, "gain": 3.5, - "target_fps": 3.0, + "target_fps": 0, "enabled": true } ] diff --git a/usda_vision_system/__pycache__/main.cpython-311.pyc b/usda_vision_system/__pycache__/main.cpython-311.pyc index 188e82d..f19dd0f 100644 Binary files a/usda_vision_system/__pycache__/main.cpython-311.pyc and b/usda_vision_system/__pycache__/main.cpython-311.pyc differ diff --git a/usda_vision_system/api/__pycache__/server.cpython-311.pyc b/usda_vision_system/api/__pycache__/server.cpython-311.pyc index c8312c4..814eeed 100644 Binary files a/usda_vision_system/api/__pycache__/server.cpython-311.pyc and b/usda_vision_system/api/__pycache__/server.cpython-311.pyc differ diff --git a/usda_vision_system/api/server.py b/usda_vision_system/api/server.py index 5849b20..75c6a9f 100644 --- a/usda_vision_system/api/server.py +++ b/usda_vision_system/api/server.py @@ -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""" diff --git a/usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc b/usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc index 64c6629..6b42b89 100644 Binary files a/usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc and b/usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc differ diff --git a/usda_vision_system/camera/manager.py b/usda_vision_system/camera/manager.py index d797096..dd5a899 100644 --- a/usda_vision_system/camera/manager.py +++ b/usda_vision_system/camera/manager.py @@ -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""" diff --git a/usda_vision_system/camera/recorder.py b/usda_vision_system/camera/recorder.py index 1c9eaa7..44e3839 100644 --- a/usda_vision_system/camera/recorder.py +++ b/usda_vision_system/camera/recorder.py @@ -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 diff --git a/usda_vision_system/main.py b/usda_vision_system/main.py index 4144d8c..1c3d2e6 100644 --- a/usda_vision_system/main.py +++ b/usda_vision_system/main.py @@ -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( diff --git a/usda_vision_system/storage/__pycache__/manager.cpython-311.pyc b/usda_vision_system/storage/__pycache__/manager.cpython-311.pyc index 3628abb..19acc5c 100644 Binary files a/usda_vision_system/storage/__pycache__/manager.cpython-311.pyc and b/usda_vision_system/storage/__pycache__/manager.cpython-311.pyc differ diff --git a/usda_vision_system/storage/manager.py b/usda_vision_system/storage/manager.py index 33ecb26..5e959bb 100644 --- a/usda_vision_system/storage/manager.py +++ b/usda_vision_system/storage/manager.py @@ -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: