feat: Implement Vision System API Client with comprehensive endpoints and utility functions

- Added VisionApiClient class to interact with the vision system API.
- Defined interfaces for system status, machine status, camera status, recordings, and storage stats.
- Implemented methods for health checks, system status retrieval, camera control, and storage management.
- Introduced utility functions for formatting bytes, durations, and uptime.

test: Create manual verification script for Vision API functionality

- Added a test script to verify utility functions and API endpoints.
- Included tests for health check, system status, cameras, machines, and storage stats.

feat: Create experiment repetitions system migration

- Added experiment_repetitions table to manage experiment repetitions with scheduling.
- Implemented triggers and functions for validation and timestamp management.
- Established row-level security policies for user access control.

feat: Introduce phase-specific draft management system migration

- Created experiment_phase_drafts and experiment_phase_data tables for managing phase-specific drafts and measurements.
- Added pecan_diameter_measurements table for individual diameter measurements.
- Implemented row-level security policies for user access control.

fix: Adjust draft constraints to allow multiple drafts while preventing multiple submitted drafts

- Modified constraints on experiment_phase_drafts to allow multiple drafts in 'draft' or 'withdrawn' status.
- Ensured only one 'submitted' draft per user per phase per repetition.
This commit is contained in:
Alireza Vaezi
2025-07-28 16:30:56 -04:00
parent 8a1b22234b
commit 7c7115a0f4
27 changed files with 4219 additions and 683 deletions

336
src/lib/visionApi.ts Normal file
View File

@@ -0,0 +1,336 @@
// Vision System API Client
// Base URL for the vision system API
const VISION_API_BASE_URL = 'http://localhost:8000'
// Types based on the API documentation
export interface SystemStatus {
system_started: boolean
mqtt_connected: boolean
last_mqtt_message: string
machines: Record<string, MachineStatus>
cameras: Record<string, CameraStatus>
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
}
current_recording_file: string | null
recording_start_time: string | null
}
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<string, {
file_count: number
total_size_bytes: number
}>
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 CameraTestResponse {
success: boolean
message: string
camera_name: string
timestamp: string
}
export interface CameraRecoveryResponse {
success: boolean
message: string
camera_name: string
operation: string
timestamp: 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<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
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<SystemStatus> {
return this.request('/system/status')
}
// Machine endpoints
async getMachines(): Promise<Record<string, MachineStatus>> {
return this.request('/machines')
}
// MQTT endpoints
async getMqttStatus(): Promise<MqttStatus> {
return this.request('/mqtt/status')
}
async getMqttEvents(limit: number = 10): Promise<MqttEventsResponse> {
return this.request(`/mqtt/events?limit=${limit}`)
}
// Camera endpoints
async getCameras(): Promise<Record<string, CameraStatus>> {
return this.request('/cameras')
}
async getCameraStatus(cameraName: string): Promise<CameraStatus> {
return this.request(`/cameras/${cameraName}/status`)
}
// Recording control
async startRecording(cameraName: string, params: StartRecordingRequest = {}): Promise<StartRecordingResponse> {
return this.request(`/cameras/${cameraName}/start-recording`, {
method: 'POST',
body: JSON.stringify(params),
})
}
async stopRecording(cameraName: string): Promise<StopRecordingResponse> {
return this.request(`/cameras/${cameraName}/stop-recording`, {
method: 'POST',
})
}
// Camera diagnostics
async testCameraConnection(cameraName: string): Promise<CameraTestResponse> {
return this.request(`/cameras/${cameraName}/test-connection`, {
method: 'POST',
})
}
async reconnectCamera(cameraName: string): Promise<CameraRecoveryResponse> {
return this.request(`/cameras/${cameraName}/reconnect`, {
method: 'POST',
})
}
async restartCameraGrab(cameraName: string): Promise<CameraRecoveryResponse> {
return this.request(`/cameras/${cameraName}/restart-grab`, {
method: 'POST',
})
}
async resetCameraTimestamp(cameraName: string): Promise<CameraRecoveryResponse> {
return this.request(`/cameras/${cameraName}/reset-timestamp`, {
method: 'POST',
})
}
async fullCameraReset(cameraName: string): Promise<CameraRecoveryResponse> {
return this.request(`/cameras/${cameraName}/full-reset`, {
method: 'POST',
})
}
async reinitializeCamera(cameraName: string): Promise<CameraRecoveryResponse> {
return this.request(`/cameras/${cameraName}/reinitialize`, {
method: 'POST',
})
}
// Recording sessions
async getRecordings(): Promise<Record<string, RecordingInfo>> {
return this.request('/recordings')
}
// Storage endpoints
async getStorageStats(): Promise<StorageStats> {
return this.request('/storage/stats')
}
async getFiles(params: FileListRequest = {}): Promise<FileListResponse> {
return this.request('/storage/files', {
method: 'POST',
body: JSON.stringify(params),
})
}
async cleanupStorage(params: CleanupRequest = {}): Promise<CleanupResponse> {
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`
}
}