Enhance time synchronization checks, update storage paths, and improve camera recording management
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
80
api-tests.http
Normal 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
|
||||||
@@ -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()
|
||||||
|
|||||||
10
config.json
10
config.json
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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"""
|
||||||
|
|||||||
Binary file not shown.
@@ -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"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Binary file not shown.
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user