- Introduced MQTTPublishRequest and MQTTPublishResponse models for handling MQTT message publishing. - Implemented a new POST route for publishing MQTT messages, including error handling and logging. - Enhanced the StandaloneAutoRecorder with improved logging during manual recording start. - Updated the frontend to include an MQTT Debug Panel for better monitoring and debugging capabilities.
347 lines
9.3 KiB
TypeScript
347 lines
9.3 KiB
TypeScript
// Vision System API Client for vision-system-remote
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore - Vite provides import.meta.env
|
|
const VISION_API_BASE_URL = (import.meta.env?.VITE_VISION_API_URL as string | undefined) || 'http://exp-dash:8000'
|
|
|
|
// Types (simplified - we'll use the same types from the original)
|
|
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
|
|
port_type?: string
|
|
model?: string
|
|
firmware_version?: string
|
|
}
|
|
current_recording_file?: string | null
|
|
recording_start_time?: string | null
|
|
last_frame_time?: string
|
|
frame_rate?: number
|
|
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<string, {
|
|
file_count: number
|
|
total_size_bytes: number
|
|
}>
|
|
disk_usage: {
|
|
total: number
|
|
used: number
|
|
free: number
|
|
}
|
|
}
|
|
|
|
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 | null
|
|
}
|
|
|
|
export interface StartRecordingResponse {
|
|
success: boolean
|
|
message: string
|
|
filename: string
|
|
}
|
|
|
|
export interface StopRecordingResponse {
|
|
success: boolean
|
|
message: string
|
|
}
|
|
|
|
export interface StreamStopResponse {
|
|
success: boolean
|
|
message: string
|
|
}
|
|
|
|
export interface CameraConfig {
|
|
name: string
|
|
machine_topic: string
|
|
storage_path: string
|
|
enabled: boolean
|
|
auto_start_recording_enabled: boolean
|
|
auto_recording_max_retries: number
|
|
auto_recording_retry_delay_seconds: number
|
|
exposure_ms: number
|
|
gain: number
|
|
target_fps: number
|
|
video_format: string
|
|
video_codec: string
|
|
video_quality: number
|
|
sharpness: number
|
|
contrast: number
|
|
saturation: number
|
|
gamma: number
|
|
noise_filter_enabled: boolean
|
|
denoise_3d_enabled: boolean
|
|
auto_white_balance: boolean
|
|
color_temperature_preset: number
|
|
wb_red_gain: number
|
|
wb_green_gain: number
|
|
wb_blue_gain: number
|
|
anti_flicker_enabled: boolean
|
|
light_frequency: number
|
|
bit_depth: number
|
|
hdr_enabled: boolean
|
|
hdr_gain_mode: number
|
|
}
|
|
|
|
export interface CameraConfigUpdate {
|
|
exposure_ms?: number
|
|
gain?: number
|
|
target_fps?: number
|
|
sharpness?: number
|
|
contrast?: number
|
|
saturation?: number
|
|
gamma?: number
|
|
noise_filter_enabled?: boolean
|
|
denoise_3d_enabled?: boolean
|
|
auto_white_balance?: boolean
|
|
color_temperature_preset?: number
|
|
wb_red_gain?: number
|
|
wb_green_gain?: number
|
|
wb_blue_gain?: number
|
|
anti_flicker_enabled?: boolean
|
|
light_frequency?: number
|
|
hdr_enabled?: boolean
|
|
hdr_gain_mode?: number
|
|
auto_start_recording_enabled?: boolean
|
|
auto_recording_max_retries?: number
|
|
auto_recording_retry_delay_seconds?: number
|
|
}
|
|
|
|
export interface CameraConfigUpdateResponse {
|
|
success: boolean
|
|
message: string
|
|
updated_settings: string[]
|
|
}
|
|
|
|
export interface CameraRecoveryResponse {
|
|
success: boolean
|
|
message: string
|
|
camera_name: string
|
|
operation: string
|
|
}
|
|
|
|
export interface StreamStartResponse {
|
|
success: boolean
|
|
message: string
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
async getSystemStatus(): Promise<SystemStatus> {
|
|
return this.request('/system/status')
|
|
}
|
|
|
|
async getCameras(): Promise<Record<string, CameraStatus>> {
|
|
return this.request('/cameras')
|
|
}
|
|
|
|
async getRecordings(): Promise<Record<string, RecordingInfo>> {
|
|
return this.request('/recordings')
|
|
}
|
|
|
|
async getStorageStats(): Promise<StorageStats> {
|
|
return this.request('/storage/stats')
|
|
}
|
|
|
|
async getMqttStatus(): Promise<MqttStatus> {
|
|
return this.request('/mqtt/status')
|
|
}
|
|
|
|
async getMqttEvents(limit: number = 50): Promise<MqttEventsResponse> {
|
|
return this.request(`/mqtt/events?limit=${limit}`)
|
|
}
|
|
|
|
async publishMqttMessage(topic: string, payload: string, qos: number = 0, retain: boolean = false): Promise<{ success: boolean; message: string; topic: string; payload: string }> {
|
|
return this.request('/mqtt/publish', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ topic, payload, qos, retain }),
|
|
})
|
|
}
|
|
|
|
async startRecording(cameraName: string, filename?: string): Promise<StartRecordingResponse> {
|
|
return this.request(`/cameras/${cameraName}/start-recording`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ filename }),
|
|
})
|
|
}
|
|
|
|
async stopRecording(cameraName: string): Promise<StopRecordingResponse> {
|
|
return this.request(`/cameras/${cameraName}/stop-recording`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
async stopStream(cameraName: string): Promise<StreamStopResponse> {
|
|
return this.request(`/cameras/${cameraName}/stop-stream`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
getStreamUrl(cameraName: string): string {
|
|
return `${this.baseUrl}/cameras/${cameraName}/stream`
|
|
}
|
|
|
|
async startStream(cameraName: string): Promise<StreamStartResponse> {
|
|
return this.request(`/cameras/${cameraName}/start-stream`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
async getCameraConfig(cameraName: string): Promise<CameraConfig> {
|
|
return this.request(`/cameras/${cameraName}/config`)
|
|
}
|
|
|
|
async updateCameraConfig(cameraName: string, config: CameraConfigUpdate): Promise<CameraConfigUpdateResponse> {
|
|
return this.request(`/cameras/${cameraName}/config`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(config),
|
|
})
|
|
}
|
|
|
|
async applyCameraConfig(cameraName: string): Promise<{ success: boolean; message: string }> {
|
|
return this.request(`/cameras/${cameraName}/apply-config`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
async reinitializeCamera(cameraName: string): Promise<CameraRecoveryResponse> {
|
|
return this.request(`/cameras/${cameraName}/reinitialize`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
async fullResetCamera(cameraName: string): Promise<CameraRecoveryResponse> {
|
|
return this.request(`/cameras/${cameraName}/full-reset`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
}
|
|
|
|
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`
|
|
}
|
|
}
|
|
|