From 731d8cd9ffcd26b901b4977f47928ae2aabb1dcd Mon Sep 17 00:00:00 2001 From: Alireza Vaezi Date: Fri, 25 Jul 2025 22:38:33 -0400 Subject: [PATCH] Enhance time synchronization checks, update storage paths, and improve camera recording management --- .gitignore | 3 +- api-tests.http | 80 ++++++++++++++++++ check_time.py | 69 +++++++++------ config.json | 10 +-- .../__pycache__/main.cpython-311.pyc | Bin 15384 -> 15401 bytes .../api/__pycache__/server.cpython-311.pyc | Bin 26378 -> 26943 bytes usda_vision_system/api/server.py | 17 +++- .../__pycache__/recorder.cpython-311.pyc | Bin 20089 -> 20293 bytes usda_vision_system/camera/manager.py | 33 ++++++-- usda_vision_system/camera/recorder.py | 52 ++++++++---- usda_vision_system/main.py | 2 +- .../__pycache__/manager.cpython-311.pyc | Bin 18362 -> 22520 bytes usda_vision_system/storage/manager.py | 77 ++++++++++++++++- 13 files changed, 283 insertions(+), 60 deletions(-) create mode 100644 api-tests.http 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 188e82d03a15d1cb0e306e70bc702645e56f159d..f19dd0f1336e7274486a0f90d0482235b90c8a91 100644 GIT binary patch delta 61 zcmbPHv9f}9IWI340}xc$wq$(R$h(4>(P;BF=824x&$G&Ij$mzMW(=Qvi(?`8iIOY+ R2^ZxPugE8EZseS91OS147BBz+ delta 59 zcmZ2kF{6TaIWI340}%YyZ^^j3k#_|%quSemN79ftY+FA$=bur7&Q4A$3nJ? P@_tw3{WeeHoNfdFIb9Q% diff --git a/usda_vision_system/api/__pycache__/server.cpython-311.pyc b/usda_vision_system/api/__pycache__/server.cpython-311.pyc index c8312c41dbbb27a3ecfc983e0c10fca7ae0bd5ff..814eeedc9af3293347d6c7cd1f882c546d2b9857 100644 GIT binary patch delta 2754 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 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 64c662962fd35ef4a42a9464e1ff837b09449366..6b42b895a173c88ec351852ec4f69d667ab323e5 100644 GIT binary patch delta 3510 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 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 3628abb1404413d78c5b73ab6663501c53d2db18..19acc5c241df7cf3a3b129719fa9f13eab681964 100644 GIT binary patch delta 6233 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 Pj(6GfKR=|E?bH7Pg?HiL delta 2938 zcmZ`*Yitx%6rMXfv(Ikd+uc5Bw=An{3#A3xQbj3Z%S%=eM6Keo^xiFl`)KaWR%xoO zq`ngsuSPV{U?dvU#K-Cn{h=|@#Kgq#4>KV#Cj8KZgesbdzdYwoyQLLov)|l%?z!iE z&h9@SldoPPmG1|Geg&R?*32I5?>|`?A*W8>{CHGVwky;(t5E-Ys-k=dU*{RAr|Nzs z5%~0Hl}wK=Q1M+kzq#@iQIyveri>7(jHr~1xTwlB>SB6EquQ)C;-;)}! ztYOu(7MN=mg#&Lb@YVruolkK-Gz#JuL^=}dVIREaR%qG2!I4_KU^IxWcRUf_y22?x z4TFNv4ZMqhR}J7(qa)-R8UK%kMw>uVn4(KM@JkNR(b(S%QX=#8T8L{4>>F>OaRr<; zQ8%QmUDRm1#UU*m9(64!hPMV^++XPuZ)gMJ>Oh5C|W;pE1U4!laI9G|FRmw@LH`){ z2HJ@DsmE9Pyx}aVYsRA%yzXX?8K!0OHZdM)ShEi4SR1?>;H-;F6(qyg!*;3QsQ4_h zifk;Mk6cknpZKwPXMa0h+=S4EFaTg{w3r*`OXc!g;bc*o*xk}c21_Sec9V^EVBVcP zcx8JtMwZEy-?Y-)N?8U_t8(elqeh;kEN+aAG5+qaE7ajTQR5J5^x&nGLHRK5yev=W zuSvRTOtRFG;}$dRYFgxJ%P8biCWGivlW!L_(Fo}k9nqcBiy`tJNt@CNz7>^t5e5;Y zzuRynL)L{W=_v*;;g!MTWy7RBd^)`5;Mp4h(8LT)_EEJk?}PwoOp@&kaAqAlLsI9U;0 zz2N*U(MeZVPi9gz69*>6=J?9#M1|uSI%RtvP4m2wA9H*!2Zi^`!!c%IK)DRU_5ELJ zv^0X>1D9p8uIIUqlEY#y9u~jESCdTXV#jx`KrddAu^bkqt_^jV+_Or8*yJ%2MzR9s zF0OWM3%w0%6KDR6a(dtCsWYh=g~!Fdbpxbbys&OVYH1}V{MRID1a9#M5gtO28m0AU zuI+cq2g}g08kNka3tqC@>^iZvyH0d>uOd&E?(O#Kq*XlJ8zY_KMDMOxzARPx(Ft3a z5RTl@LNRagoVeWE(w2pT-_a*-Az<=&4gs^yi=v~ibGkei__CC^&7(!j$Z}L>>sG-^ zXKk%$GHPoX&X~-s?Vd*Y^e z?z@A0P+H&rrza$1h^cbw@F1?976V(_-Lhhz5{I`$+_OlX5Kn9|NJ#v)W#!>PB=`_y zhf!QHgbaer;8L&o{cwPjGgp{k*EA#cp{z~YWm+8S8pj!bQXCuX36(R;4J6`#<3r+; z!A=qr7X~+}7nD-N)(5mWx{y#EOQxK)l!Et76##QDULEQt5%K-dO(b5rG}MiKxnf5X z84{azbh2lFE13v5GUTYZ4=Bqr$Dc!*>>=4ri-SaW%w9BD#tscOu~%dbf(bwDSVLM$ zfA1L9LHXunx<#_Rj#A%5m;jj6JG#;n20tafOx{0@;YlV!whM-=Qy5wnehjz_IQqKW zL>YhiV}`}cu><%T09zZ&7LM2&j%xlqQg1jFebO1b+Q?hm*%&vh<0;O@3(!={{Oc(T z_fHs<6;jxLPQ77iNUssV^YeEPlipJ6-o0wQ%5Q?hWP&6US`wZ_GmnT9_w-D!LUBz9 zI9VMd-MES(NOR?2$B6R%0JfgV7SfjO0!wY}NTHDB58#=Ga13Dv;bnwZ5MD)i2O)(J zM~ER*1Kc684p!zND4Ln_=Vxu(Ic?jV7SPw9nwV4I^+{H~mP3;`xxZd~y?=&mE=?VH zjkHJwC_542XHbvq3H)Ed9~Em4-bMz*BL_EhNX5%c`q#TTSD?i#yPbao{5WgO%Zj*o Z@QbZ;?p-=rHK$x3|B&>*Zg47n{tscGa-{$O 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: