// Vision System API Client // Base URL for the vision system API - Use environment variable or default to vision container // The API is accessible at vision:8000 in the current setup const VISION_API_BASE_URL = import.meta.env.VITE_VISION_API_URL || 'http://vision:8000' // Types based on the API documentation export interface SystemStatus { system_started: boolean mqtt_connected: boolean last_mqtt_message: string machines: Record cameras: Record active_recordings: number total_recordings: number uptime_seconds: number } export interface MachineStatus { name: string state: string last_updated: string last_message?: string mqtt_topic?: string } export interface CameraStatus { name?: string status: string is_recording: boolean last_checked: string last_error?: string | null device_info?: { friendly_name?: string serial_number?: string port_type?: string model?: string firmware_version?: string last_checked?: number } current_recording_file?: string | null recording_start_time?: string | null last_frame_time?: string frame_rate?: number // NEW AUTO-RECORDING FIELDS auto_recording_enabled: boolean auto_recording_active: boolean auto_recording_failure_count: number auto_recording_last_attempt?: string auto_recording_last_error?: string } export interface RecordingInfo { camera_name: string filename: string start_time: string state: string end_time?: string file_size_bytes?: number frame_count?: number duration_seconds?: number error_message?: string | null } export interface StorageStats { base_path: string total_files: number total_size_bytes: number cameras: Record disk_usage: { total: number used: number free: number } } export interface RecordingFile { filename: string camera_name: string file_size_bytes: number created_date: string duration_seconds?: number } export interface StartRecordingRequest { filename?: string exposure_ms?: number gain?: number fps?: number } export interface StartRecordingResponse { success: boolean message: string filename: string } export interface StopRecordingResponse { success: boolean message: string duration_seconds: number } export interface StreamStartResponse { success: boolean message: string } export interface StreamStopResponse { success: boolean message: string } export interface CameraTestResponse { success: boolean message: string camera_name: string timestamp: string } export interface CameraRecoveryResponse { success: boolean message: string camera_name: string operation: string timestamp: string } // Auto-Recording Response Types export interface AutoRecordingConfigResponse { success: boolean message: string camera_name: string enabled: boolean } export interface AutoRecordingStatusResponse { running: boolean auto_recording_enabled: boolean retry_queue: Record enabled_cameras: string[] } // Camera Configuration Types export interface CameraConfig { // READ-ONLY SYSTEM FIELDS name: string machine_topic: string storage_path: string enabled: boolean // READ-ONLY AUTO-RECORDING FIELDS auto_start_recording_enabled: boolean auto_recording_max_retries: number auto_recording_retry_delay_seconds: number // BASIC SETTINGS (real-time configurable) exposure_ms: number gain: number target_fps: number // VIDEO RECORDING SETTINGS (restart required) video_format: string // 'mp4' or 'avi' video_codec: string // 'mp4v', 'XVID', 'MJPG' video_quality: number // 0-100 (higher = better quality) // IMAGE QUALITY SETTINGS (real-time configurable) sharpness: number contrast: number saturation: number gamma: number // COLOR SETTINGS (real-time configurable) auto_white_balance: boolean color_temperature_preset: number // WHITE BALANCE RGB GAINS (real-time configurable) wb_red_gain: number wb_green_gain: number wb_blue_gain: number // ADVANCED SETTINGS anti_flicker_enabled: boolean light_frequency: number // NOISE REDUCTION (restart required) noise_filter_enabled: boolean denoise_3d_enabled: boolean // SYSTEM SETTINGS (restart required) bit_depth: number // HDR SETTINGS (real-time configurable) hdr_enabled: boolean hdr_gain_mode: number } export interface CameraConfigUpdate { // BASIC SETTINGS (real-time configurable) exposure_ms?: number gain?: number target_fps?: number // IMAGE QUALITY SETTINGS (real-time configurable) sharpness?: number contrast?: number saturation?: number gamma?: number // COLOR SETTINGS (real-time configurable) auto_white_balance?: boolean color_temperature_preset?: number // WHITE BALANCE RGB GAINS (real-time configurable) wb_red_gain?: number wb_green_gain?: number wb_blue_gain?: number // ADVANCED SETTINGS (real-time configurable) anti_flicker_enabled?: boolean light_frequency?: number // HDR SETTINGS (real-time configurable) hdr_enabled?: boolean hdr_gain_mode?: number // NOTE: Video format settings and noise reduction settings are not included // as they are either read-only or require restart via apply-config endpoint } export interface CameraConfigUpdateResponse { success: boolean message: string updated_settings: string[] } export interface CameraConfigApplyResponse { success: boolean message: string } export interface MqttMessage { timestamp: string topic: string message: string source: string } export interface MqttStatus { connected: boolean broker_host: string broker_port: number subscribed_topics: string[] last_message_time: string message_count: number error_count: number uptime_seconds: number } export interface MqttEvent { machine_name: string topic: string payload: string normalized_state: string timestamp: string message_number: number } export interface MqttEventsResponse { events: MqttEvent[] total_events: number last_updated: string } export interface FileListRequest { camera_name?: string start_date?: string end_date?: string limit?: number } export interface FileListResponse { files: RecordingFile[] total_count: number } export interface CleanupRequest { max_age_days?: number } export interface CleanupResponse { files_removed: number bytes_freed: number errors: string[] } // API Client Class class VisionApiClient { private baseUrl: string constructor(baseUrl: string = VISION_API_BASE_URL) { this.baseUrl = baseUrl } private async request(endpoint: string, options: RequestInit = {}): Promise { const url = `${this.baseUrl}${endpoint}` const response = await fetch(url, { headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }) if (!response.ok) { const errorText = await response.text() throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`) } return response.json() } // System endpoints async getHealth(): Promise<{ status: string; timestamp: string }> { return this.request('/health') } async getSystemStatus(): Promise { return this.request('/system/status') } // Machine endpoints async getMachines(): Promise> { return this.request('/machines') } // MQTT endpoints async getMqttStatus(): Promise { return this.request('/mqtt/status') } async getMqttEvents(limit: number = 10): Promise { return this.request(`/mqtt/events?limit=${limit}`) } // Camera endpoints async getCameras(): Promise> { return this.request('/cameras') } async getCameraStatus(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/status`) } // Recording control async startRecording(cameraName: string, params: StartRecordingRequest = {}): Promise { return this.request(`/cameras/${cameraName}/start-recording`, { method: 'POST', body: JSON.stringify(params), }) } async stopRecording(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/stop-recording`, { method: 'POST', }) } // Streaming control async startStream(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/start-stream`, { method: 'POST', }) } async stopStream(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/stop-stream`, { method: 'POST', }) } getStreamUrl(cameraName: string): string { return `${this.baseUrl}/cameras/${cameraName}/stream` } // Camera diagnostics async testCameraConnection(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/test-connection`, { method: 'POST', }) } async reconnectCamera(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/reconnect`, { method: 'POST', }) } async restartCameraGrab(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/restart-grab`, { method: 'POST', }) } async resetCameraTimestamp(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/reset-timestamp`, { method: 'POST', }) } async fullCameraReset(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/full-reset`, { method: 'POST', }) } async reinitializeCamera(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/reinitialize`, { method: 'POST', }) } // Camera configuration async getCameraConfig(cameraName: string): Promise { try { const config = await this.request(`/cameras/${cameraName}/config`) as any // Map API field names to UI expected field names and ensure auto-recording fields have default values if missing return { ...config, // Map auto_start_recording_enabled from API to auto_record_on_machine_start for UI auto_record_on_machine_start: config.auto_start_recording_enabled ?? false, auto_start_recording_enabled: config.auto_start_recording_enabled ?? false, auto_recording_max_retries: config.auto_recording_max_retries ?? 3, auto_recording_retry_delay_seconds: config.auto_recording_retry_delay_seconds ?? 5 } } catch (error: any) { // If the error is related to missing auto-recording fields, try to handle it gracefully if (error.message?.includes('auto_start_recording_enabled') || error.message?.includes('auto_recording_max_retries') || error.message?.includes('auto_recording_retry_delay_seconds')) { // Try to get the raw camera data and add default auto-recording fields try { const response = await fetch(`${this.baseUrl}/cameras/${cameraName}/config`, { headers: { 'Content-Type': 'application/json', } }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const rawConfig = await response.json() // Add missing auto-recording fields with defaults and map field names return { ...rawConfig, // Map auto_start_recording_enabled from API to auto_record_on_machine_start for UI auto_record_on_machine_start: rawConfig.auto_start_recording_enabled ?? false, auto_start_recording_enabled: rawConfig.auto_start_recording_enabled ?? false, auto_recording_max_retries: rawConfig.auto_recording_max_retries ?? 3, auto_recording_retry_delay_seconds: rawConfig.auto_recording_retry_delay_seconds ?? 5 } } catch (fallbackError) { throw new Error(`Failed to load camera configuration: ${error.message}`) } } throw error } } async updateCameraConfig(cameraName: string, config: CameraConfigUpdate): Promise { // Map UI field names to API field names const apiConfig = { ...config } // If auto_record_on_machine_start is present, map it to auto_start_recording_enabled for the API if ('auto_record_on_machine_start' in config) { apiConfig.auto_start_recording_enabled = config.auto_record_on_machine_start // Remove the UI field name to avoid confusion delete apiConfig.auto_record_on_machine_start } return this.request(`/cameras/${cameraName}/config`, { method: 'PUT', body: JSON.stringify(apiConfig), }) } async applyCameraConfig(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/apply-config`, { method: 'POST', }) } // Auto-Recording endpoints async enableAutoRecording(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/auto-recording/enable`, { method: 'POST', }) } async disableAutoRecording(cameraName: string): Promise { return this.request(`/cameras/${cameraName}/auto-recording/disable`, { method: 'POST', }) } async getAutoRecordingStatus(): Promise { return this.request('/auto-recording/status') } // Recording sessions async getRecordings(): Promise> { return this.request('/recordings') } // Storage endpoints async getStorageStats(): Promise { return this.request('/storage/stats') } async getFiles(params: FileListRequest = {}): Promise { return this.request('/storage/files', { method: 'POST', body: JSON.stringify(params), }) } async cleanupStorage(params: CleanupRequest = {}): Promise { return this.request('/storage/cleanup', { method: 'POST', body: JSON.stringify(params), }) } } // Export a singleton instance export const visionApi = new VisionApiClient() // Utility functions export const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 Bytes' const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } export const formatDuration = (seconds: number): string => { const hours = Math.floor(seconds / 3600) const minutes = Math.floor((seconds % 3600) / 60) const secs = Math.floor(seconds % 60) if (hours > 0) { return `${hours}h ${minutes}m ${secs}s` } else if (minutes > 0) { return `${minutes}m ${secs}s` } else { return `${secs}s` } } export const formatUptime = (seconds: number): string => { const days = Math.floor(seconds / 86400) const hours = Math.floor((seconds % 86400) / 3600) const minutes = Math.floor((seconds % 3600) / 60) if (days > 0) { return `${days}d ${hours}h ${minutes}m` } else if (hours > 0) { return `${hours}h ${minutes}m` } else { return `${minutes}m` } }