diff --git a/camera-management-api/usda_vision_system/camera/monitor.py b/camera-management-api/usda_vision_system/camera/monitor.py index ca79f4a..a55f9f6 100644 --- a/camera-management-api/usda_vision_system/camera/monitor.py +++ b/camera-management-api/usda_vision_system/camera/monitor.py @@ -196,7 +196,13 @@ class CameraMonitor: self.logger.info(f"Camera {camera_name} initialized successfully, starting test capture...") except mvsdk.CameraException as init_e: self.logger.warning(f"CameraInit failed for {camera_name}: {init_e.message} (error_code: {init_e.error_code})") - return "error", f"Camera initialization failed: {init_e.message}", self._get_device_info_dict(device_info) + # Get device info dict before returning - wrap in try/except in case device_info is corrupted + try: + device_info_dict = self._get_device_info_dict(device_info) + except Exception as dev_info_e: + self.logger.warning(f"Failed to get device info dict after CameraInit failure: {dev_info_e}") + device_info_dict = None + return "error", f"Camera initialization failed: {init_e.message}", device_info_dict # Quick test - try to get one frame try: @@ -232,10 +238,38 @@ class CameraMonitor: def _get_device_info_dict(self, device_info) -> Dict[str, Any]: """Convert device info to dictionary""" + if device_info is None: + return {"error": "device_info is None"} + try: - return {"friendly_name": device_info.GetFriendlyName(), "port_type": device_info.GetPortType(), "serial_number": getattr(device_info, "acSn", "Unknown"), "last_checked": time.time()} + # Safely access device info methods - wrap each in try/except to prevent segfaults + friendly_name = "Unknown" + port_type = "Unknown" + serial_number = "Unknown" + + try: + friendly_name = device_info.GetFriendlyName() + except Exception as e: + self.logger.warning(f"Failed to get friendly name: {e}") + + try: + port_type = device_info.GetPortType() + except Exception as e: + self.logger.warning(f"Failed to get port type: {e}") + + try: + serial_number = getattr(device_info, "acSn", "Unknown") + except Exception as e: + self.logger.warning(f"Failed to get serial number: {e}") + + return { + "friendly_name": friendly_name, + "port_type": port_type, + "serial_number": serial_number, + "last_checked": time.time() + } except Exception as e: - self.logger.error(f"Error getting device info: {e}") + self.logger.error(f"Error getting device info: {e}", exc_info=True) return {"error": str(e)} def check_camera_now(self, camera_name: str) -> Dict[str, Any]: diff --git a/camera-management-api/usda_vision_system/camera/streamer.py b/camera-management-api/usda_vision_system/camera/streamer.py index 04d7e98..8b14e6c 100644 --- a/camera-management-api/usda_vision_system/camera/streamer.py +++ b/camera-management-api/usda_vision_system/camera/streamer.py @@ -273,33 +273,99 @@ class CameraStreamer: return False # Initialize camera (suppress output to avoid MVCAMAPI error messages) - with suppress_camera_errors(): - self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1) - self.logger.info("Camera initialized successfully for streaming") + try: + with suppress_camera_errors(): + self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1) + self.logger.info("Camera initialized successfully for streaming") + except mvsdk.CameraException as init_e: + self.logger.error(f"CameraInit failed for streaming {self.camera_config.name}: {init_e.message} (error_code: {init_e.error_code})") + self.hCamera = None + return False + + # Ensure hCamera is valid before proceeding + if self.hCamera is None: + self.logger.error("Camera initialization returned None handle") + return False # Get camera capabilities - self.cap = mvsdk.CameraGetCapability(self.hCamera) + try: + self.cap = mvsdk.CameraGetCapability(self.hCamera) + except mvsdk.CameraException as cap_e: + self.logger.error(f"CameraGetCapability failed for {self.camera_config.name}: {cap_e.message} (error_code: {cap_e.error_code})") + if self.hCamera: + try: + mvsdk.CameraUnInit(self.hCamera) + except: + pass + self.hCamera = None + return False # Determine if camera is monochrome self.monoCamera = self.cap.sIspCapacity.bMonoSensor != 0 # Set output format based on camera type and bit depth - if self.monoCamera: - mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO8) - else: - mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR8) + try: + if self.monoCamera: + mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO8) + else: + mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR8) + except mvsdk.CameraException as fmt_e: + self.logger.error(f"CameraSetIspOutFormat failed for {self.camera_config.name}: {fmt_e.message} (error_code: {fmt_e.error_code})") + if self.hCamera: + try: + mvsdk.CameraUnInit(self.hCamera) + except: + pass + self.hCamera = None + return False # Configure camera settings for streaming (optimized for preview) - self._configure_streaming_settings() + try: + self._configure_streaming_settings() + except Exception as config_e: + self.logger.error(f"Failed to configure streaming settings for {self.camera_config.name}: {config_e}") + if self.hCamera: + try: + mvsdk.CameraUnInit(self.hCamera) + except: + pass + self.hCamera = None + return False # Allocate frame buffer - bytes_per_pixel = 1 if self.monoCamera else 3 - self.frame_buffer_size = self.cap.sResolutionRange.iWidthMax * self.cap.sResolutionRange.iHeightMax * bytes_per_pixel - self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16) + try: + bytes_per_pixel = 1 if self.monoCamera else 3 + self.frame_buffer_size = self.cap.sResolutionRange.iWidthMax * self.cap.sResolutionRange.iHeightMax * bytes_per_pixel + self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16) + except Exception as buf_e: + self.logger.error(f"Failed to allocate frame buffer for {self.camera_config.name}: {buf_e}") + if self.hCamera: + try: + mvsdk.CameraUnInit(self.hCamera) + except: + pass + self.hCamera = None + return False # Start camera - mvsdk.CameraPlay(self.hCamera) - self.logger.info("Camera started successfully for streaming") + try: + mvsdk.CameraPlay(self.hCamera) + self.logger.info("Camera started successfully for streaming") + except mvsdk.CameraException as play_e: + self.logger.error(f"CameraPlay failed for {self.camera_config.name}: {play_e.message} (error_code: {play_e.error_code})") + if self.frame_buffer: + try: + mvsdk.CameraAlignFree(self.frame_buffer) + except: + pass + self.frame_buffer = None + if self.hCamera: + try: + mvsdk.CameraUnInit(self.hCamera) + except: + pass + self.hCamera = None + return False return True diff --git a/docker-compose.yml b/docker-compose.yml index 6ed54e5..1289e25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -104,6 +104,26 @@ services: ports: - "3002:3002" + scheduling-remote: + image: node:20-alpine + working_dir: /app + env_file: + - ./management-dashboard-web-app/.env + environment: + - CHOKIDAR_USEPOLLING=true + - TZ=America/New_York + volumes: + - ./scheduling-remote:/app + command: > + sh -lc " + npm install; + npm run dev:watch + " + extra_hosts: + - "host.docker.internal:host-gateway" + ports: + - "3003:3003" + media-api: build: context: ./media-api diff --git a/management-dashboard-web-app/index.html b/management-dashboard-web-app/index.html index 59470a2..3e91753 100755 --- a/management-dashboard-web-app/index.html +++ b/management-dashboard-web-app/index.html @@ -5,6 +5,15 @@ Experiments Dashboard +
diff --git a/management-dashboard-web-app/src/App.tsx b/management-dashboard-web-app/src/App.tsx index da1e8a3..08647f5 100755 --- a/management-dashboard-web-app/src/App.tsx +++ b/management-dashboard-web-app/src/App.tsx @@ -115,7 +115,7 @@ function App() { if (loading) { return ( -
+

Loading...

@@ -127,7 +127,7 @@ function App() { // Handle signout route if (currentRoute === '/signout') { return ( -
+

Signing out...

diff --git a/management-dashboard-web-app/src/components/DashboardLayout.tsx b/management-dashboard-web-app/src/components/DashboardLayout.tsx index a4a4d5e..c16d4a2 100755 --- a/management-dashboard-web-app/src/components/DashboardLayout.tsx +++ b/management-dashboard-web-app/src/components/DashboardLayout.tsx @@ -7,7 +7,8 @@ import { ExperimentManagement } from './ExperimentManagement' import { DataEntry } from './DataEntry' // VisionSystem is now loaded as a microfrontend - see RemoteVisionSystem below // import { VisionSystem } from './VisionSystem' -import { Scheduling } from './Scheduling' +// Scheduling is now loaded as a microfrontend - see RemoteScheduling below +// import { Scheduling } from './Scheduling' import React, { Suspense } from 'react' import { loadRemoteComponent } from '../lib/loadRemote' import { ErrorBoundary } from './ErrorBoundary' @@ -172,6 +173,13 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps LocalVisionSystemPlaceholder as any ) as unknown as React.ComponentType + const LocalSchedulingPlaceholder = () => (
Scheduling module not enabled.
) + const RemoteScheduling = loadRemoteComponent( + isFeatureEnabled('enableSchedulingModule'), + () => import('schedulingRemote/App'), + LocalSchedulingPlaceholder as any + ) as unknown as React.ComponentType<{ user: User; currentRoute: string }> + const renderCurrentView = () => { if (!user) return null @@ -216,7 +224,13 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps ) case 'scheduling': - return + return ( + Failed to load scheduling module. Please try again.
}> + Loading scheduling module...
}> + + + + ) case 'video-library': return ( Failed to load video module. Please try again.
}> @@ -234,7 +248,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps if (loading) { return ( -
+

Loading dashboard...

@@ -245,7 +259,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps if (error) { return ( -
+
{error}
@@ -263,7 +277,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps if (!user) { return ( -
+
No user data available
@@ -108,8 +108,8 @@ export function DataEntry() { if (error) { return (
-
-
{error}
+
+
{error}
) @@ -129,8 +129,8 @@ export function DataEntry() { return (
-

Data Entry

-

+

Data Entry

+

Select a repetition to enter measurement data

@@ -142,13 +142,13 @@ export function DataEntry() { return (
{/* Past/Completed Repetitions */} -
-
-

+
+
+

Past/Completed ({pastRepetitions.length})

-

+

Completed or past scheduled repetitions

@@ -164,7 +164,7 @@ export function DataEntry() { /> ))} {pastRepetitions.length === 0 && ( -

+

No completed repetitions

)} @@ -173,13 +173,13 @@ export function DataEntry() {
{/* In Progress Repetitions */} -
-
-

+
+
+

In Progress ({inProgressRepetitions.length})

-

+

Currently scheduled or active repetitions

@@ -195,7 +195,7 @@ export function DataEntry() { /> ))} {inProgressRepetitions.length === 0 && ( -

+

No repetitions in progress

)} @@ -204,13 +204,13 @@ export function DataEntry() {
{/* Upcoming Repetitions */} -
-
-

+
+
+

Upcoming ({upcomingRepetitions.length})

-

+

Future scheduled repetitions

@@ -226,7 +226,7 @@ export function DataEntry() { /> ))} {upcomingRepetitions.length === 0 && ( -

+

No upcoming repetitions

)} @@ -239,7 +239,7 @@ export function DataEntry() { {experiments.length === 0 && (
-
+
No experiments available for data entry
@@ -291,35 +291,35 @@ function RepetitionCard({ experiment, repetition, onSelect, status }: Repetition
{/* Large, bold experiment number */} - + #{experiment.experiment_number} {/* Smaller repetition number */} - + Rep #{repetition.repetition_number} - {getStatusIcon()} + {getStatusIcon()}
{repetition.scheduled_date ? 'scheduled' : 'pending'}
{/* Experiment details */} -
+
{experiment.soaking_duration_hr}h soaking • {experiment.air_drying_time_min}min drying
{repetition.scheduled_date && ( -
- Scheduled: {new Date(repetition.scheduled_date).toLocaleString()} +
+ Scheduled: {new Date(repetition.scheduled_date).toLocaleString()}
)} -
+
Click to enter data for this repetition
diff --git a/management-dashboard-web-app/src/components/ExperimentPhases.tsx b/management-dashboard-web-app/src/components/ExperimentPhases.tsx index a9aac80..3994db3 100644 --- a/management-dashboard-web-app/src/components/ExperimentPhases.tsx +++ b/management-dashboard-web-app/src/components/ExperimentPhases.tsx @@ -61,9 +61,9 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
-

Experiment Phases

-

Select an experiment phase to view and manage its experiments

-

Experiment phases help organize experiments into logical groups for easier navigation and management.

+

Experiment Phases

+

Select an experiment phase to view and manage its experiments

+

Experiment phases help organize experiments into logical groups for easier navigation and management.

{canManagePhases && ( + {/* User Area */}