feat: Integrate auto-recording feature into USDA Vision Camera System
- Added instructions for implementing auto-recording functionality in the React app. - Updated TypeScript interfaces to include new fields for auto-recording status and configuration. - Created new API endpoints for enabling/disabling auto-recording and retrieving system status. - Enhanced UI components to display auto-recording status, controls, and error handling. - Developed a comprehensive Auto-Recording Feature Implementation Guide. - Implemented a test script for validating auto-recording functionality, including configuration checks and API connectivity. - Introduced AutoRecordingManager to manage automatic recording based on machine state changes with retry logic. - Established a retry mechanism for failed recording attempts and integrated status tracking for auto-recording.
This commit is contained in:
175
AI_AGENT_INSTRUCTIONS.md
Normal file
175
AI_AGENT_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Instructions for AI Agent: Auto-Recording Feature Integration
|
||||||
|
|
||||||
|
## 🎯 Task Overview
|
||||||
|
Update the React application to support the new auto-recording feature that has been added to the USDA Vision Camera System backend.
|
||||||
|
|
||||||
|
## 📋 What You Need to Know
|
||||||
|
|
||||||
|
### System Context
|
||||||
|
- **Camera 1** monitors the **vibratory conveyor** (conveyor/cracker cam)
|
||||||
|
- **Camera 2** monitors the **blower separator** machine
|
||||||
|
- Auto-recording automatically starts when machines turn ON and stops when they turn OFF
|
||||||
|
- The system includes retry logic for failed recording attempts
|
||||||
|
- Manual recording always takes precedence over auto-recording
|
||||||
|
|
||||||
|
### New Backend Capabilities
|
||||||
|
The backend now supports:
|
||||||
|
1. **Automatic recording** triggered by MQTT machine state changes
|
||||||
|
2. **Retry mechanism** for failed recording attempts (configurable retries and delays)
|
||||||
|
3. **Status tracking** for auto-recording state, failures, and attempts
|
||||||
|
4. **API endpoints** for enabling/disabling and monitoring auto-recording
|
||||||
|
|
||||||
|
## 🔧 Required React App Changes
|
||||||
|
|
||||||
|
### 1. Update TypeScript Interfaces
|
||||||
|
|
||||||
|
Add these new fields to existing `CameraStatusResponse`:
|
||||||
|
```typescript
|
||||||
|
interface CameraStatusResponse {
|
||||||
|
// ... existing fields
|
||||||
|
auto_recording_enabled: boolean;
|
||||||
|
auto_recording_active: boolean;
|
||||||
|
auto_recording_failure_count: number;
|
||||||
|
auto_recording_last_attempt?: string;
|
||||||
|
auto_recording_last_error?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add new response types:
|
||||||
|
```typescript
|
||||||
|
interface AutoRecordingConfigResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
camera_name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoRecordingStatusResponse {
|
||||||
|
running: boolean;
|
||||||
|
auto_recording_enabled: boolean;
|
||||||
|
retry_queue: Record<string, any>;
|
||||||
|
enabled_cameras: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add New API Endpoints
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Enable auto-recording for a camera
|
||||||
|
POST /cameras/{camera_name}/auto-recording/enable
|
||||||
|
|
||||||
|
// Disable auto-recording for a camera
|
||||||
|
POST /cameras/{camera_name}/auto-recording/disable
|
||||||
|
|
||||||
|
// Get overall auto-recording system status
|
||||||
|
GET /auto-recording/status
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. UI Components to Add/Update
|
||||||
|
|
||||||
|
#### Camera Status Display
|
||||||
|
- Add auto-recording status badge/indicator
|
||||||
|
- Show auto-recording enabled/disabled state
|
||||||
|
- Display failure count if > 0
|
||||||
|
- Show last error message if any
|
||||||
|
- Distinguish between manual and auto-recording states
|
||||||
|
|
||||||
|
#### Auto-Recording Controls
|
||||||
|
- Toggle switch to enable/disable auto-recording per camera
|
||||||
|
- System-wide auto-recording status display
|
||||||
|
- Retry queue information
|
||||||
|
- Machine state correlation display
|
||||||
|
|
||||||
|
#### Error Handling
|
||||||
|
- Clear display of auto-recording failures
|
||||||
|
- Retry attempt information
|
||||||
|
- Last attempt timestamp
|
||||||
|
- Quick retry/reset actions
|
||||||
|
|
||||||
|
### 4. Visual Design Guidelines
|
||||||
|
|
||||||
|
**Status Priority (highest to lowest):**
|
||||||
|
1. Manual Recording (red/prominent) - user initiated
|
||||||
|
2. Auto-Recording Active (green) - machine ON, recording
|
||||||
|
3. Auto-Recording Enabled (blue) - ready but machine OFF
|
||||||
|
4. Auto-Recording Disabled (gray) - feature disabled
|
||||||
|
|
||||||
|
**Machine Correlation:**
|
||||||
|
- Show machine name next to camera (e.g., "Vibratory Conveyor", "Blower Separator")
|
||||||
|
- Display machine ON/OFF status
|
||||||
|
- Alert if machine is ON but auto-recording failed
|
||||||
|
|
||||||
|
## 🎨 Specific Implementation Tasks
|
||||||
|
|
||||||
|
### Task 1: Update Camera Cards
|
||||||
|
- Add auto-recording status indicators
|
||||||
|
- Add enable/disable toggle controls
|
||||||
|
- Show machine state correlation
|
||||||
|
- Display failure information when relevant
|
||||||
|
|
||||||
|
### Task 2: Create Auto-Recording Dashboard
|
||||||
|
- Overall system status
|
||||||
|
- List of enabled cameras
|
||||||
|
- Active retry queue display
|
||||||
|
- Recent events/errors
|
||||||
|
|
||||||
|
### Task 3: Update Recording Status Logic
|
||||||
|
- Distinguish between manual and auto-recording
|
||||||
|
- Show appropriate controls based on recording type
|
||||||
|
- Handle manual override scenarios
|
||||||
|
|
||||||
|
### Task 4: Add Error Handling
|
||||||
|
- Display auto-recording failures clearly
|
||||||
|
- Show retry attempts and timing
|
||||||
|
- Provide manual retry options
|
||||||
|
|
||||||
|
## 📱 User Experience Requirements
|
||||||
|
|
||||||
|
### Key Behaviors
|
||||||
|
1. **Non-Intrusive:** Auto-recording status shouldn't clutter the main interface
|
||||||
|
2. **Clear Hierarchy:** Manual controls should be more prominent than auto-recording
|
||||||
|
3. **Informative:** Users should understand why recording started/stopped
|
||||||
|
4. **Actionable:** Clear options to enable/disable or retry failed attempts
|
||||||
|
|
||||||
|
### Mobile Considerations
|
||||||
|
- Auto-recording controls should work well on mobile
|
||||||
|
- Status information should be readable on small screens
|
||||||
|
- Consider collapsible sections for detailed information
|
||||||
|
|
||||||
|
## 🔍 Testing Requirements
|
||||||
|
|
||||||
|
Ensure the React app correctly handles:
|
||||||
|
- [ ] Toggling auto-recording on/off per camera
|
||||||
|
- [ ] Displaying real-time status updates
|
||||||
|
- [ ] Showing error states and retry information
|
||||||
|
- [ ] Manual recording override scenarios
|
||||||
|
- [ ] Machine state changes and correlation
|
||||||
|
- [ ] Mobile interface functionality
|
||||||
|
|
||||||
|
## 📚 Reference Files
|
||||||
|
|
||||||
|
Key files to review for implementation details:
|
||||||
|
- `AUTO_RECORDING_FEATURE_GUIDE.md` - Comprehensive technical details
|
||||||
|
- `api-endpoints.http` - API endpoint documentation
|
||||||
|
- `config.json` - Configuration structure
|
||||||
|
- `usda_vision_system/api/models.py` - Response type definitions
|
||||||
|
|
||||||
|
## 🎯 Success Criteria
|
||||||
|
|
||||||
|
The React app should:
|
||||||
|
1. **Display** auto-recording status for each camera clearly
|
||||||
|
2. **Allow** users to enable/disable auto-recording per camera
|
||||||
|
3. **Show** machine state correlation and recording triggers
|
||||||
|
4. **Handle** error states and retry scenarios gracefully
|
||||||
|
5. **Maintain** existing manual recording functionality
|
||||||
|
6. **Provide** clear visual hierarchy between manual and auto-recording
|
||||||
|
|
||||||
|
## 💡 Implementation Tips
|
||||||
|
|
||||||
|
1. **Start Small:** Begin with basic status display, then add controls
|
||||||
|
2. **Use Existing Patterns:** Follow the current app's design patterns
|
||||||
|
3. **Test Incrementally:** Test each feature as you add it
|
||||||
|
4. **Consider State Management:** Update your state management to handle new data
|
||||||
|
5. **Mobile First:** Ensure mobile usability from the start
|
||||||
|
|
||||||
|
The goal is to seamlessly integrate auto-recording capabilities while maintaining the existing user experience and adding valuable automation features for the camera operators.
|
||||||
260
AUTO_RECORDING_FEATURE_GUIDE.md
Normal file
260
AUTO_RECORDING_FEATURE_GUIDE.md
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
# Auto-Recording Feature Implementation Guide
|
||||||
|
|
||||||
|
## 🎯 Overview for React App Development
|
||||||
|
|
||||||
|
This document provides a comprehensive guide for updating the React application to support the new auto-recording feature that was added to the USDA Vision Camera System.
|
||||||
|
|
||||||
|
## 📋 What Changed in the Backend
|
||||||
|
|
||||||
|
### New API Endpoints Added
|
||||||
|
|
||||||
|
1. **Enable Auto-Recording**
|
||||||
|
```http
|
||||||
|
POST /cameras/{camera_name}/auto-recording/enable
|
||||||
|
Response: AutoRecordingConfigResponse
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Disable Auto-Recording**
|
||||||
|
```http
|
||||||
|
POST /cameras/{camera_name}/auto-recording/disable
|
||||||
|
Response: AutoRecordingConfigResponse
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Get Auto-Recording Status**
|
||||||
|
```http
|
||||||
|
GET /auto-recording/status
|
||||||
|
Response: AutoRecordingStatusResponse
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updated API Responses
|
||||||
|
|
||||||
|
#### CameraStatusResponse (Updated)
|
||||||
|
```typescript
|
||||||
|
interface CameraStatusResponse {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
is_recording: boolean;
|
||||||
|
last_checked: string;
|
||||||
|
last_error?: string;
|
||||||
|
device_info?: any;
|
||||||
|
current_recording_file?: string;
|
||||||
|
recording_start_time?: string;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CameraConfigResponse (Updated)
|
||||||
|
```typescript
|
||||||
|
interface CameraConfigResponse {
|
||||||
|
name: string;
|
||||||
|
machine_topic: string;
|
||||||
|
storage_path: string;
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
// NEW AUTO-RECORDING CONFIG FIELDS
|
||||||
|
auto_start_recording_enabled: boolean;
|
||||||
|
auto_recording_max_retries: number;
|
||||||
|
auto_recording_retry_delay_seconds: number;
|
||||||
|
|
||||||
|
// ... existing fields (exposure_ms, gain, etc.)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### New Response Types
|
||||||
|
```typescript
|
||||||
|
interface AutoRecordingConfigResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
camera_name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoRecordingStatusResponse {
|
||||||
|
running: boolean;
|
||||||
|
auto_recording_enabled: boolean;
|
||||||
|
retry_queue: Record<string, any>;
|
||||||
|
enabled_cameras: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 React App UI Requirements
|
||||||
|
|
||||||
|
### 1. Camera Status Display Updates
|
||||||
|
|
||||||
|
**Add to Camera Cards/Components:**
|
||||||
|
- Auto-recording enabled/disabled indicator
|
||||||
|
- Auto-recording active status (when machine is ON and auto-recording)
|
||||||
|
- Failure count display (if > 0)
|
||||||
|
- Last auto-recording error (if any)
|
||||||
|
- Visual distinction between manual and auto-recording
|
||||||
|
|
||||||
|
**Example UI Elements:**
|
||||||
|
```jsx
|
||||||
|
// Auto-recording status badge
|
||||||
|
{camera.auto_recording_enabled && (
|
||||||
|
<Badge variant={camera.auto_recording_active ? "success" : "secondary"}>
|
||||||
|
Auto-Recording {camera.auto_recording_active ? "Active" : "Enabled"}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
// Failure indicator
|
||||||
|
{camera.auto_recording_failure_count > 0 && (
|
||||||
|
<Alert variant="warning">
|
||||||
|
Auto-recording failures: {camera.auto_recording_failure_count}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Auto-Recording Controls
|
||||||
|
|
||||||
|
**Add Toggle Controls:**
|
||||||
|
- Enable/Disable auto-recording per camera
|
||||||
|
- Global auto-recording status display
|
||||||
|
- Retry queue monitoring
|
||||||
|
|
||||||
|
**Example Control Component:**
|
||||||
|
```jsx
|
||||||
|
const AutoRecordingToggle = ({ camera, onToggle }) => {
|
||||||
|
const handleToggle = async () => {
|
||||||
|
const endpoint = camera.auto_recording_enabled ? 'disable' : 'enable';
|
||||||
|
await fetch(`/cameras/${camera.name}/auto-recording/${endpoint}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
onToggle();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={camera.auto_recording_enabled}
|
||||||
|
onChange={handleToggle}
|
||||||
|
label="Auto-Recording"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Machine State Integration
|
||||||
|
|
||||||
|
**Display Machine Status:**
|
||||||
|
- Show which machine each camera monitors
|
||||||
|
- Display current machine state (ON/OFF)
|
||||||
|
- Show correlation between machine state and recording status
|
||||||
|
|
||||||
|
**Camera-Machine Mapping:**
|
||||||
|
- Camera 1 → Vibratory Conveyor (conveyor/cracker cam)
|
||||||
|
- Camera 2 → Blower Separator (blower separator)
|
||||||
|
|
||||||
|
### 4. Auto-Recording Dashboard
|
||||||
|
|
||||||
|
**Create New Dashboard Section:**
|
||||||
|
- Overall auto-recording system status
|
||||||
|
- List of cameras with auto-recording enabled
|
||||||
|
- Active retry queue display
|
||||||
|
- Recent auto-recording events/logs
|
||||||
|
|
||||||
|
## 🔧 Implementation Steps for React App
|
||||||
|
|
||||||
|
### Step 1: Update TypeScript Interfaces
|
||||||
|
```typescript
|
||||||
|
// Update existing interfaces in your types file
|
||||||
|
// Add new interfaces for auto-recording responses
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Update API Service Functions
|
||||||
|
```typescript
|
||||||
|
// Add new API calls
|
||||||
|
export const enableAutoRecording = (cameraName: string) =>
|
||||||
|
fetch(`/cameras/${cameraName}/auto-recording/enable`, { method: 'POST' });
|
||||||
|
|
||||||
|
export const disableAutoRecording = (cameraName: string) =>
|
||||||
|
fetch(`/cameras/${cameraName}/auto-recording/disable`, { method: 'POST' });
|
||||||
|
|
||||||
|
export const getAutoRecordingStatus = () =>
|
||||||
|
fetch('/auto-recording/status').then(res => res.json());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Update Camera Components
|
||||||
|
- Add auto-recording status indicators
|
||||||
|
- Add enable/disable controls
|
||||||
|
- Update recording status display to distinguish auto vs manual
|
||||||
|
|
||||||
|
### Step 4: Create Auto-Recording Management Panel
|
||||||
|
- System-wide auto-recording status
|
||||||
|
- Per-camera auto-recording controls
|
||||||
|
- Retry queue monitoring
|
||||||
|
- Error reporting and alerts
|
||||||
|
|
||||||
|
### Step 5: Update State Management
|
||||||
|
```typescript
|
||||||
|
// Add auto-recording state to your store/context
|
||||||
|
interface AppState {
|
||||||
|
cameras: CameraStatusResponse[];
|
||||||
|
autoRecordingStatus: AutoRecordingStatusResponse;
|
||||||
|
// ... existing state
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Key User Experience Considerations
|
||||||
|
|
||||||
|
### Visual Indicators
|
||||||
|
1. **Recording Status Hierarchy:**
|
||||||
|
- Manual Recording (highest priority - red/prominent)
|
||||||
|
- Auto-Recording Active (green/secondary)
|
||||||
|
- Auto-Recording Enabled but Inactive (blue/subtle)
|
||||||
|
- Auto-Recording Disabled (gray/muted)
|
||||||
|
|
||||||
|
2. **Machine State Correlation:**
|
||||||
|
- Show machine ON/OFF status next to camera
|
||||||
|
- Indicate when auto-recording should be active
|
||||||
|
- Alert if machine is ON but auto-recording failed
|
||||||
|
|
||||||
|
3. **Error Handling:**
|
||||||
|
- Clear error messages for auto-recording failures
|
||||||
|
- Retry count display
|
||||||
|
- Last attempt timestamp
|
||||||
|
- Quick retry/reset options
|
||||||
|
|
||||||
|
### User Controls
|
||||||
|
1. **Quick Actions:**
|
||||||
|
- Toggle auto-recording per camera
|
||||||
|
- Force retry failed auto-recording
|
||||||
|
- Override auto-recording (manual control)
|
||||||
|
|
||||||
|
2. **Configuration:**
|
||||||
|
- Adjust retry settings
|
||||||
|
- Change machine-camera mappings
|
||||||
|
- Set recording parameters for auto-recording
|
||||||
|
|
||||||
|
## 🚨 Important Notes
|
||||||
|
|
||||||
|
### Behavior Rules
|
||||||
|
1. **Manual Override:** Manual recording always takes precedence over auto-recording
|
||||||
|
2. **Non-Blocking:** Auto-recording status checks don't interfere with camera operation
|
||||||
|
3. **Machine Correlation:** Auto-recording only activates when the associated machine turns ON
|
||||||
|
4. **Failure Handling:** Failed auto-recording attempts are retried automatically with exponential backoff
|
||||||
|
|
||||||
|
### API Polling Recommendations
|
||||||
|
- Poll camera status every 2-3 seconds for real-time updates
|
||||||
|
- Poll auto-recording status every 5-10 seconds
|
||||||
|
- Use WebSocket connections if available for real-time machine state updates
|
||||||
|
|
||||||
|
## 📱 Mobile Considerations
|
||||||
|
- Auto-recording controls should be easily accessible on mobile
|
||||||
|
- Status indicators should be clear and readable on small screens
|
||||||
|
- Consider collapsible sections for detailed auto-recording information
|
||||||
|
|
||||||
|
## 🔍 Testing Checklist
|
||||||
|
- [ ] Auto-recording toggle works for each camera
|
||||||
|
- [ ] Status updates reflect machine state changes
|
||||||
|
- [ ] Error states are clearly displayed
|
||||||
|
- [ ] Manual recording overrides auto-recording
|
||||||
|
- [ ] Retry mechanism is visible to users
|
||||||
|
- [ ] Mobile interface is functional
|
||||||
|
|
||||||
|
This guide provides everything needed to update the React app to fully support the new auto-recording feature!
|
||||||
@@ -291,6 +291,48 @@ POST http://localhost:8000/cameras/camera2/stop-recording
|
|||||||
# "duration_seconds": 45.2
|
# "duration_seconds": 45.2
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# AUTO-RECORDING CONTROL ENDPOINTS
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
### Enable auto-recording for a camera
|
||||||
|
POST http://localhost:8000/cameras/camera1/auto-recording/enable
|
||||||
|
POST http://localhost:8000/cameras/camera2/auto-recording/enable
|
||||||
|
# No request body required
|
||||||
|
# Response: AutoRecordingConfigResponse
|
||||||
|
# {
|
||||||
|
# "success": true,
|
||||||
|
# "message": "Auto-recording enabled for camera1",
|
||||||
|
# "camera_name": "camera1",
|
||||||
|
# "enabled": true
|
||||||
|
# }
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
### Disable auto-recording for a camera
|
||||||
|
POST http://localhost:8000/cameras/camera1/auto-recording/disable
|
||||||
|
POST http://localhost:8000/cameras/camera2/auto-recording/disable
|
||||||
|
# No request body required
|
||||||
|
# Response: AutoRecordingConfigResponse
|
||||||
|
# {
|
||||||
|
# "success": true,
|
||||||
|
# "message": "Auto-recording disabled for camera1",
|
||||||
|
# "camera_name": "camera1",
|
||||||
|
# "enabled": false
|
||||||
|
# }
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
### Get auto-recording manager status
|
||||||
|
GET http://localhost:8000/auto-recording/status
|
||||||
|
# Response: AutoRecordingStatusResponse
|
||||||
|
# {
|
||||||
|
# "running": true,
|
||||||
|
# "auto_recording_enabled": true,
|
||||||
|
# "retry_queue": {},
|
||||||
|
# "enabled_cameras": ["camera1", "camera2"]
|
||||||
|
# }
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# CAMERA RECOVERY & DIAGNOSTICS ENDPOINTS
|
# CAMERA RECOVERY & DIAGNOSTICS ENDPOINTS
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|||||||
25
config.json
25
config.json
@@ -22,24 +22,28 @@
|
|||||||
"api_host": "0.0.0.0",
|
"api_host": "0.0.0.0",
|
||||||
"api_port": 8000,
|
"api_port": 8000,
|
||||||
"enable_api": true,
|
"enable_api": true,
|
||||||
"timezone": "America/New_York"
|
"timezone": "America/New_York",
|
||||||
|
"auto_recording_enabled": true
|
||||||
},
|
},
|
||||||
"cameras": [
|
"cameras": [
|
||||||
{
|
{
|
||||||
"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": 0.5,
|
||||||
"gain": 3.5,
|
"gain": 0.5,
|
||||||
"target_fps": 0,
|
"target_fps": 0,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"auto_start_recording_enabled": true,
|
||||||
|
"auto_recording_max_retries": 3,
|
||||||
|
"auto_recording_retry_delay_seconds": 5,
|
||||||
"sharpness": 100,
|
"sharpness": 100,
|
||||||
"contrast": 100,
|
"contrast": 100,
|
||||||
"saturation": 100,
|
"saturation": 100,
|
||||||
"gamma": 85,
|
"gamma": 110,
|
||||||
"noise_filter_enabled": false,
|
"noise_filter_enabled": false,
|
||||||
"denoise_3d_enabled": false,
|
"denoise_3d_enabled": false,
|
||||||
"auto_white_balance": false,
|
"auto_white_balance": true,
|
||||||
"color_temperature_preset": 0,
|
"color_temperature_preset": 0,
|
||||||
"anti_flicker_enabled": false,
|
"anti_flicker_enabled": false,
|
||||||
"light_frequency": 1,
|
"light_frequency": 1,
|
||||||
@@ -51,14 +55,17 @@
|
|||||||
"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": 0.5,
|
||||||
"gain": 3.5,
|
"gain": 0.3,
|
||||||
"target_fps": 0,
|
"target_fps": 0,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"auto_start_recording_enabled": true,
|
||||||
|
"auto_recording_max_retries": 3,
|
||||||
|
"auto_recording_retry_delay_seconds": 5,
|
||||||
"sharpness": 100,
|
"sharpness": 100,
|
||||||
"contrast": 100,
|
"contrast": 100,
|
||||||
"saturation": 100,
|
"saturation": 75,
|
||||||
"gamma": 100,
|
"gamma": 110,
|
||||||
"noise_filter_enabled": false,
|
"noise_filter_enabled": false,
|
||||||
"denoise_3d_enabled": false,
|
"denoise_3d_enabled": false,
|
||||||
"auto_white_balance": true,
|
"auto_white_balance": true,
|
||||||
|
|||||||
227
test_auto_recording_simple.py
Normal file
227
test_auto_recording_simple.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple test script for auto-recording functionality.
|
||||||
|
|
||||||
|
This script performs basic checks to verify that the auto-recording feature
|
||||||
|
is properly integrated and configured.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Add the current directory to Python path
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
def test_config_structure():
|
||||||
|
"""Test that config.json has the required auto-recording fields"""
|
||||||
|
print("🔍 Testing configuration structure...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open("config.json", "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Check system-level auto-recording setting
|
||||||
|
system_config = config.get("system", {})
|
||||||
|
if "auto_recording_enabled" not in system_config:
|
||||||
|
print("❌ Missing 'auto_recording_enabled' in system config")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"✅ System auto-recording enabled: {system_config['auto_recording_enabled']}")
|
||||||
|
|
||||||
|
# Check camera-level auto-recording settings
|
||||||
|
cameras = config.get("cameras", [])
|
||||||
|
if not cameras:
|
||||||
|
print("❌ No cameras found in config")
|
||||||
|
return False
|
||||||
|
|
||||||
|
for camera in cameras:
|
||||||
|
camera_name = camera.get("name", "unknown")
|
||||||
|
required_fields = [
|
||||||
|
"auto_start_recording_enabled",
|
||||||
|
"auto_recording_max_retries",
|
||||||
|
"auto_recording_retry_delay_seconds"
|
||||||
|
]
|
||||||
|
|
||||||
|
missing_fields = [field for field in required_fields if field not in camera]
|
||||||
|
if missing_fields:
|
||||||
|
print(f"❌ Camera {camera_name} missing fields: {missing_fields}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"✅ Camera {camera_name} auto-recording config:")
|
||||||
|
print(f" - Enabled: {camera['auto_start_recording_enabled']}")
|
||||||
|
print(f" - Max retries: {camera['auto_recording_max_retries']}")
|
||||||
|
print(f" - Retry delay: {camera['auto_recording_retry_delay_seconds']}s")
|
||||||
|
print(f" - Machine topic: {camera.get('machine_topic', 'unknown')}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error reading config: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_module_imports():
|
||||||
|
"""Test that all required modules can be imported"""
|
||||||
|
print("\n🔍 Testing module imports...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from usda_vision_system.recording.auto_manager import AutoRecordingManager
|
||||||
|
print("✅ AutoRecordingManager imported successfully")
|
||||||
|
|
||||||
|
from usda_vision_system.core.config import Config
|
||||||
|
config = Config("config.json")
|
||||||
|
print("✅ Config loaded successfully")
|
||||||
|
|
||||||
|
from usda_vision_system.core.state_manager import StateManager
|
||||||
|
state_manager = StateManager()
|
||||||
|
print("✅ StateManager created successfully")
|
||||||
|
|
||||||
|
from usda_vision_system.core.events import EventSystem
|
||||||
|
event_system = EventSystem()
|
||||||
|
print("✅ EventSystem created successfully")
|
||||||
|
|
||||||
|
# Test creating AutoRecordingManager (without camera_manager for now)
|
||||||
|
auto_manager = AutoRecordingManager(config, state_manager, event_system, None)
|
||||||
|
print("✅ AutoRecordingManager created successfully")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Import error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_camera_mapping():
|
||||||
|
"""Test camera to machine topic mapping"""
|
||||||
|
print("\n🔍 Testing camera to machine mapping...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open("config.json", "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
cameras = config.get("cameras", [])
|
||||||
|
expected_mappings = {
|
||||||
|
"camera1": "vibratory_conveyor", # Conveyor/cracker cam
|
||||||
|
"camera2": "blower_separator" # Blower separator
|
||||||
|
}
|
||||||
|
|
||||||
|
for camera in cameras:
|
||||||
|
camera_name = camera.get("name")
|
||||||
|
machine_topic = camera.get("machine_topic")
|
||||||
|
|
||||||
|
if camera_name in expected_mappings:
|
||||||
|
expected_topic = expected_mappings[camera_name]
|
||||||
|
if machine_topic == expected_topic:
|
||||||
|
print(f"✅ {camera_name} correctly mapped to {machine_topic}")
|
||||||
|
else:
|
||||||
|
print(f"❌ {camera_name} mapped to {machine_topic}, expected {expected_topic}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Unknown camera: {camera_name}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error checking mappings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_api_models():
|
||||||
|
"""Test that API models include auto-recording fields"""
|
||||||
|
print("\n🔍 Testing API models...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from usda_vision_system.api.models import (
|
||||||
|
CameraStatusResponse,
|
||||||
|
CameraConfigResponse,
|
||||||
|
AutoRecordingConfigRequest,
|
||||||
|
AutoRecordingConfigResponse,
|
||||||
|
AutoRecordingStatusResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check CameraStatusResponse has auto-recording fields
|
||||||
|
camera_response = CameraStatusResponse(
|
||||||
|
name="test",
|
||||||
|
status="available",
|
||||||
|
is_recording=False,
|
||||||
|
last_checked="2024-01-01T00:00:00",
|
||||||
|
auto_recording_enabled=True,
|
||||||
|
auto_recording_active=False,
|
||||||
|
auto_recording_failure_count=0
|
||||||
|
)
|
||||||
|
print("✅ CameraStatusResponse includes auto-recording fields")
|
||||||
|
|
||||||
|
# Check CameraConfigResponse has auto-recording fields
|
||||||
|
config_response = CameraConfigResponse(
|
||||||
|
name="test",
|
||||||
|
machine_topic="test_topic",
|
||||||
|
storage_path="/test",
|
||||||
|
enabled=True,
|
||||||
|
auto_start_recording_enabled=True,
|
||||||
|
auto_recording_max_retries=3,
|
||||||
|
auto_recording_retry_delay_seconds=5,
|
||||||
|
exposure_ms=1.0,
|
||||||
|
gain=1.0,
|
||||||
|
target_fps=30.0,
|
||||||
|
sharpness=100,
|
||||||
|
contrast=100,
|
||||||
|
saturation=100,
|
||||||
|
gamma=100,
|
||||||
|
noise_filter_enabled=False,
|
||||||
|
denoise_3d_enabled=False,
|
||||||
|
auto_white_balance=True,
|
||||||
|
color_temperature_preset=0,
|
||||||
|
anti_flicker_enabled=False,
|
||||||
|
light_frequency=1,
|
||||||
|
bit_depth=8,
|
||||||
|
hdr_enabled=False,
|
||||||
|
hdr_gain_mode=0
|
||||||
|
)
|
||||||
|
print("✅ CameraConfigResponse includes auto-recording fields")
|
||||||
|
|
||||||
|
print("✅ All auto-recording API models available")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ API model error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run all basic tests"""
|
||||||
|
print("🧪 Auto-Recording Integration Test")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
test_config_structure,
|
||||||
|
test_module_imports,
|
||||||
|
test_camera_mapping,
|
||||||
|
test_api_models
|
||||||
|
]
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
total = len(tests)
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
try:
|
||||||
|
if test():
|
||||||
|
passed += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Test {test.__name__} failed with exception: {e}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 40)
|
||||||
|
print(f"📊 Results: {passed}/{total} tests passed")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("🎉 All integration tests passed!")
|
||||||
|
print("\n📝 Next steps:")
|
||||||
|
print("1. Start the system: python main.py")
|
||||||
|
print("2. Run full tests: python tests/test_auto_recording.py")
|
||||||
|
print("3. Test with MQTT messages to trigger auto-recording")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"⚠️ {total - passed} test(s) failed")
|
||||||
|
print("Please fix the issues before running the full system")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = main()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
267
tests/test_auto_recording.py
Normal file
267
tests/test_auto_recording.py
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for auto-recording functionality.
|
||||||
|
|
||||||
|
This script tests the auto-recording feature by simulating MQTT state changes
|
||||||
|
and verifying that cameras start and stop recording automatically.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add the parent directory to Python path
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from usda_vision_system.core.config import Config
|
||||||
|
from usda_vision_system.core.state_manager import StateManager
|
||||||
|
from usda_vision_system.core.events import EventSystem, publish_machine_state_changed
|
||||||
|
|
||||||
|
|
||||||
|
class AutoRecordingTester:
|
||||||
|
"""Test class for auto-recording functionality"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.api_base_url = "http://localhost:8000"
|
||||||
|
self.config = Config("config.json")
|
||||||
|
self.state_manager = StateManager()
|
||||||
|
self.event_system = EventSystem()
|
||||||
|
|
||||||
|
# Test results
|
||||||
|
self.test_results = []
|
||||||
|
|
||||||
|
def log_test(self, test_name: str, success: bool, message: str = ""):
|
||||||
|
"""Log a test result"""
|
||||||
|
status = "✅ PASS" if success else "❌ FAIL"
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||||
|
result = f"[{timestamp}] {status} {test_name}"
|
||||||
|
if message:
|
||||||
|
result += f" - {message}"
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
self.test_results.append({
|
||||||
|
"test_name": test_name,
|
||||||
|
"success": success,
|
||||||
|
"message": message,
|
||||||
|
"timestamp": timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
def check_api_available(self) -> bool:
|
||||||
|
"""Check if the API server is available"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_base_url}/cameras", timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_camera_status(self, camera_name: str) -> dict:
|
||||||
|
"""Get camera status from API"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_base_url}/cameras", timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
cameras = response.json()
|
||||||
|
return cameras.get(camera_name, {})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting camera status: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_auto_recording_status(self) -> dict:
|
||||||
|
"""Get auto-recording manager status"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_base_url}/auto-recording/status", timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting auto-recording status: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def enable_auto_recording(self, camera_name: str) -> bool:
|
||||||
|
"""Enable auto-recording for a camera"""
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{self.api_base_url}/cameras/{camera_name}/auto-recording/enable", timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error enabling auto-recording: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disable_auto_recording(self, camera_name: str) -> bool:
|
||||||
|
"""Disable auto-recording for a camera"""
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{self.api_base_url}/cameras/{camera_name}/auto-recording/disable", timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error disabling auto-recording: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def simulate_machine_state_change(self, machine_name: str, state: str):
|
||||||
|
"""Simulate a machine state change via event system"""
|
||||||
|
print(f"🔄 Simulating machine state change: {machine_name} -> {state}")
|
||||||
|
publish_machine_state_changed(machine_name, state, "test_script")
|
||||||
|
|
||||||
|
def test_api_connectivity(self) -> bool:
|
||||||
|
"""Test API connectivity"""
|
||||||
|
available = self.check_api_available()
|
||||||
|
self.log_test("API Connectivity", available,
|
||||||
|
"API server is reachable" if available else "API server is not reachable")
|
||||||
|
return available
|
||||||
|
|
||||||
|
def test_auto_recording_status(self) -> bool:
|
||||||
|
"""Test auto-recording status endpoint"""
|
||||||
|
status = self.get_auto_recording_status()
|
||||||
|
success = bool(status and "running" in status)
|
||||||
|
self.log_test("Auto-Recording Status API", success,
|
||||||
|
f"Status: {status}" if success else "Failed to get status")
|
||||||
|
return success
|
||||||
|
|
||||||
|
def test_camera_auto_recording_config(self) -> bool:
|
||||||
|
"""Test camera auto-recording configuration"""
|
||||||
|
success = True
|
||||||
|
|
||||||
|
# Test enabling auto-recording for camera1
|
||||||
|
enabled = self.enable_auto_recording("camera1")
|
||||||
|
if enabled:
|
||||||
|
self.log_test("Enable Auto-Recording (camera1)", True, "Successfully enabled")
|
||||||
|
else:
|
||||||
|
self.log_test("Enable Auto-Recording (camera1)", False, "Failed to enable")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# Check camera status
|
||||||
|
time.sleep(1)
|
||||||
|
camera_status = self.get_camera_status("camera1")
|
||||||
|
auto_enabled = camera_status.get("auto_recording_enabled", False)
|
||||||
|
self.log_test("Auto-Recording Status Check", auto_enabled,
|
||||||
|
f"Camera1 auto-recording enabled: {auto_enabled}")
|
||||||
|
|
||||||
|
if not auto_enabled:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def test_machine_state_simulation(self) -> bool:
|
||||||
|
"""Test machine state change simulation"""
|
||||||
|
try:
|
||||||
|
# Test vibratory conveyor (camera1)
|
||||||
|
self.simulate_machine_state_change("vibratory_conveyor", "on")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
camera_status = self.get_camera_status("camera1")
|
||||||
|
is_recording = camera_status.get("is_recording", False)
|
||||||
|
auto_active = camera_status.get("auto_recording_active", False)
|
||||||
|
|
||||||
|
self.log_test("Machine ON -> Recording Start", is_recording,
|
||||||
|
f"Camera1 recording: {is_recording}, auto-active: {auto_active}")
|
||||||
|
|
||||||
|
# Test turning machine off
|
||||||
|
time.sleep(3)
|
||||||
|
self.simulate_machine_state_change("vibratory_conveyor", "off")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
camera_status = self.get_camera_status("camera1")
|
||||||
|
is_recording_after = camera_status.get("is_recording", False)
|
||||||
|
auto_active_after = camera_status.get("auto_recording_active", False)
|
||||||
|
|
||||||
|
self.log_test("Machine OFF -> Recording Stop", not is_recording_after,
|
||||||
|
f"Camera1 recording: {is_recording_after}, auto-active: {auto_active_after}")
|
||||||
|
|
||||||
|
return is_recording and not is_recording_after
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_test("Machine State Simulation", False, f"Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_retry_mechanism(self) -> bool:
|
||||||
|
"""Test retry mechanism for failed recording attempts"""
|
||||||
|
# This test would require simulating camera failures
|
||||||
|
# For now, we'll just check if the retry queue is accessible
|
||||||
|
try:
|
||||||
|
status = self.get_auto_recording_status()
|
||||||
|
retry_queue = status.get("retry_queue", {})
|
||||||
|
|
||||||
|
self.log_test("Retry Queue Access", True,
|
||||||
|
f"Retry queue accessible, current items: {len(retry_queue)}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_test("Retry Queue Access", False, f"Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_all_tests(self):
|
||||||
|
"""Run all auto-recording tests"""
|
||||||
|
print("🧪 Starting Auto-Recording Tests")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Check if system is running
|
||||||
|
if not self.test_api_connectivity():
|
||||||
|
print("\n❌ Cannot run tests - API server is not available")
|
||||||
|
print("Please start the USDA Vision System first:")
|
||||||
|
print(" python main.py")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
tests = [
|
||||||
|
self.test_auto_recording_status,
|
||||||
|
self.test_camera_auto_recording_config,
|
||||||
|
self.test_machine_state_simulation,
|
||||||
|
self.test_retry_mechanism,
|
||||||
|
]
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
total = len(tests)
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
try:
|
||||||
|
if test():
|
||||||
|
passed += 1
|
||||||
|
time.sleep(1) # Brief pause between tests
|
||||||
|
except Exception as e:
|
||||||
|
self.log_test(test.__name__, False, f"Exception: {e}")
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print(f"📊 Test Summary: {passed}/{total} tests passed")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("🎉 All auto-recording tests passed!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"⚠️ {total - passed} test(s) failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Cleanup after tests"""
|
||||||
|
print("\n🧹 Cleaning up...")
|
||||||
|
|
||||||
|
# Disable auto-recording for test cameras
|
||||||
|
self.disable_auto_recording("camera1")
|
||||||
|
self.disable_auto_recording("camera2")
|
||||||
|
|
||||||
|
# Turn off machines
|
||||||
|
self.simulate_machine_state_change("vibratory_conveyor", "off")
|
||||||
|
self.simulate_machine_state_change("blower_separator", "off")
|
||||||
|
|
||||||
|
print("✅ Cleanup completed")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main test function"""
|
||||||
|
tester = AutoRecordingTester()
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = tester.run_all_tests()
|
||||||
|
return 0 if success else 1
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n⚠️ Tests interrupted by user")
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Test execution failed: {e}")
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
tester.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
exit_code = main()
|
||||||
|
sys.exit(exit_code)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -57,6 +57,13 @@ class CameraStatusResponse(BaseModel):
|
|||||||
current_recording_file: Optional[str] = None
|
current_recording_file: Optional[str] = None
|
||||||
recording_start_time: Optional[str] = None
|
recording_start_time: Optional[str] = None
|
||||||
|
|
||||||
|
# Auto-recording status
|
||||||
|
auto_recording_enabled: bool = False
|
||||||
|
auto_recording_active: bool = False
|
||||||
|
auto_recording_failure_count: int = 0
|
||||||
|
auto_recording_last_attempt: Optional[str] = None
|
||||||
|
auto_recording_last_error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class RecordingInfoResponse(BaseModel):
|
class RecordingInfoResponse(BaseModel):
|
||||||
"""Recording information response model"""
|
"""Recording information response model"""
|
||||||
@@ -120,6 +127,11 @@ class CameraConfigResponse(BaseModel):
|
|||||||
storage_path: str
|
storage_path: str
|
||||||
enabled: bool
|
enabled: bool
|
||||||
|
|
||||||
|
# Auto-recording settings
|
||||||
|
auto_start_recording_enabled: bool
|
||||||
|
auto_recording_max_retries: int
|
||||||
|
auto_recording_retry_delay_seconds: int
|
||||||
|
|
||||||
# Basic settings
|
# Basic settings
|
||||||
exposure_ms: float
|
exposure_ms: float
|
||||||
gain: float
|
gain: float
|
||||||
@@ -173,6 +185,30 @@ class StopRecordingResponse(BaseModel):
|
|||||||
duration_seconds: Optional[float] = None
|
duration_seconds: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AutoRecordingConfigRequest(BaseModel):
|
||||||
|
"""Auto-recording configuration request model"""
|
||||||
|
|
||||||
|
enabled: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AutoRecordingConfigResponse(BaseModel):
|
||||||
|
"""Auto-recording configuration response model"""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
camera_name: str
|
||||||
|
enabled: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AutoRecordingStatusResponse(BaseModel):
|
||||||
|
"""Auto-recording manager status response model"""
|
||||||
|
|
||||||
|
running: bool
|
||||||
|
auto_recording_enabled: bool
|
||||||
|
retry_queue: Dict[str, Any]
|
||||||
|
enabled_cameras: List[str]
|
||||||
|
|
||||||
|
|
||||||
class StorageStatsResponse(BaseModel):
|
class StorageStatsResponse(BaseModel):
|
||||||
"""Storage statistics response model"""
|
"""Storage statistics response model"""
|
||||||
|
|
||||||
|
|||||||
@@ -66,13 +66,14 @@ class WebSocketManager:
|
|||||||
class APIServer:
|
class APIServer:
|
||||||
"""FastAPI server for the USDA Vision Camera System"""
|
"""FastAPI server for the USDA Vision Camera System"""
|
||||||
|
|
||||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager, mqtt_client, storage_manager: StorageManager):
|
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager, mqtt_client, storage_manager: StorageManager, auto_recording_manager=None):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.state_manager = state_manager
|
self.state_manager = state_manager
|
||||||
self.event_system = event_system
|
self.event_system = event_system
|
||||||
self.camera_manager = camera_manager
|
self.camera_manager = camera_manager
|
||||||
self.mqtt_client = mqtt_client
|
self.mqtt_client = mqtt_client
|
||||||
self.storage_manager = storage_manager
|
self.storage_manager = storage_manager
|
||||||
|
self.auto_recording_manager = auto_recording_manager
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# FastAPI app
|
# FastAPI app
|
||||||
@@ -162,7 +163,21 @@ class APIServer:
|
|||||||
try:
|
try:
|
||||||
cameras = self.state_manager.get_all_cameras()
|
cameras = self.state_manager.get_all_cameras()
|
||||||
return {
|
return {
|
||||||
name: CameraStatusResponse(name=camera.name, status=camera.status.value, is_recording=camera.is_recording, last_checked=camera.last_checked.isoformat(), last_error=camera.last_error, device_info=camera.device_info, current_recording_file=camera.current_recording_file, recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None)
|
name: CameraStatusResponse(
|
||||||
|
name=camera.name,
|
||||||
|
status=camera.status.value,
|
||||||
|
is_recording=camera.is_recording,
|
||||||
|
last_checked=camera.last_checked.isoformat(),
|
||||||
|
last_error=camera.last_error,
|
||||||
|
device_info=camera.device_info,
|
||||||
|
current_recording_file=camera.current_recording_file,
|
||||||
|
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None,
|
||||||
|
auto_recording_enabled=camera.auto_recording_enabled,
|
||||||
|
auto_recording_active=camera.auto_recording_active,
|
||||||
|
auto_recording_failure_count=camera.auto_recording_failure_count,
|
||||||
|
auto_recording_last_attempt=camera.auto_recording_last_attempt.isoformat() if camera.auto_recording_last_attempt else None,
|
||||||
|
auto_recording_last_error=camera.auto_recording_last_error,
|
||||||
|
)
|
||||||
for name, camera in cameras.items()
|
for name, camera in cameras.items()
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -471,6 +486,74 @@ class APIServer:
|
|||||||
self.logger.error(f"Error reinitializing camera: {e}")
|
self.logger.error(f"Error reinitializing camera: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/cameras/{camera_name}/auto-recording/enable", response_model=AutoRecordingConfigResponse)
|
||||||
|
async def enable_auto_recording(camera_name: str):
|
||||||
|
"""Enable auto-recording for a camera"""
|
||||||
|
try:
|
||||||
|
if not self.auto_recording_manager:
|
||||||
|
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||||
|
|
||||||
|
# Update camera configuration
|
||||||
|
camera_config = self.config.get_camera_by_name(camera_name)
|
||||||
|
if not camera_config:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||||
|
|
||||||
|
camera_config.auto_start_recording_enabled = True
|
||||||
|
self.config.save_config()
|
||||||
|
|
||||||
|
# Update camera status in state manager
|
||||||
|
camera_info = self.state_manager.get_camera_info(camera_name)
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_enabled = True
|
||||||
|
|
||||||
|
return AutoRecordingConfigResponse(success=True, message=f"Auto-recording enabled for camera {camera_name}", camera_name=camera_name, enabled=True)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error enabling auto-recording for camera {camera_name}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/cameras/{camera_name}/auto-recording/disable", response_model=AutoRecordingConfigResponse)
|
||||||
|
async def disable_auto_recording(camera_name: str):
|
||||||
|
"""Disable auto-recording for a camera"""
|
||||||
|
try:
|
||||||
|
if not self.auto_recording_manager:
|
||||||
|
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||||
|
|
||||||
|
# Update camera configuration
|
||||||
|
camera_config = self.config.get_camera_by_name(camera_name)
|
||||||
|
if not camera_config:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
|
||||||
|
|
||||||
|
camera_config.auto_start_recording_enabled = False
|
||||||
|
self.config.save_config()
|
||||||
|
|
||||||
|
# Update camera status in state manager
|
||||||
|
camera_info = self.state_manager.get_camera_info(camera_name)
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_enabled = False
|
||||||
|
camera_info.auto_recording_active = False
|
||||||
|
|
||||||
|
return AutoRecordingConfigResponse(success=True, message=f"Auto-recording disabled for camera {camera_name}", camera_name=camera_name, enabled=False)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error disabling auto-recording for camera {camera_name}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/auto-recording/status", response_model=AutoRecordingStatusResponse)
|
||||||
|
async def get_auto_recording_status():
|
||||||
|
"""Get auto-recording manager status"""
|
||||||
|
try:
|
||||||
|
if not self.auto_recording_manager:
|
||||||
|
raise HTTPException(status_code=503, detail="Auto-recording manager not available")
|
||||||
|
|
||||||
|
status = self.auto_recording_manager.get_status()
|
||||||
|
return AutoRecordingStatusResponse(**status)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error getting auto-recording status: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@self.app.get("/recordings", response_model=Dict[str, RecordingInfoResponse])
|
@self.app.get("/recordings", response_model=Dict[str, RecordingInfoResponse])
|
||||||
async def get_recordings():
|
async def get_recordings():
|
||||||
"""Get all recording sessions"""
|
"""Get all recording sessions"""
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -40,6 +40,11 @@ class CameraConfig:
|
|||||||
target_fps: float = 3.0
|
target_fps: float = 3.0
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
|
|
||||||
|
# Auto-recording settings
|
||||||
|
auto_start_recording_enabled: bool = False # Enable automatic recording when machine turns on
|
||||||
|
auto_recording_max_retries: int = 3 # Maximum retry attempts for failed auto-recording starts
|
||||||
|
auto_recording_retry_delay_seconds: int = 5 # Delay between retry attempts
|
||||||
|
|
||||||
# Image Quality Settings
|
# Image Quality Settings
|
||||||
sharpness: int = 100 # 0-200, default 100 (no sharpening)
|
sharpness: int = 100 # 0-200, default 100 (no sharpening)
|
||||||
contrast: int = 100 # 0-200, default 100 (normal contrast)
|
contrast: int = 100 # 0-200, default 100 (normal contrast)
|
||||||
@@ -86,7 +91,10 @@ class SystemConfig:
|
|||||||
api_host: str = "0.0.0.0"
|
api_host: str = "0.0.0.0"
|
||||||
api_port: int = 8000
|
api_port: int = 8000
|
||||||
enable_api: bool = True
|
enable_api: bool = True
|
||||||
timezone: str = "America/New_York" # Atlanta, Georgia timezone
|
timezone: str = "America/New_York"
|
||||||
|
|
||||||
|
# Auto-recording system settings
|
||||||
|
auto_recording_enabled: bool = True # Global enable/disable for auto-recording feature # Atlanta, Georgia timezone
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ class CameraInfo:
|
|||||||
current_recording_file: Optional[str] = None
|
current_recording_file: Optional[str] = None
|
||||||
recording_start_time: Optional[datetime] = None
|
recording_start_time: Optional[datetime] = None
|
||||||
|
|
||||||
|
# Auto-recording status
|
||||||
|
auto_recording_enabled: bool = False
|
||||||
|
auto_recording_active: bool = False # Whether auto-recording is currently managing this camera
|
||||||
|
auto_recording_failure_count: int = 0
|
||||||
|
auto_recording_last_attempt: Optional[datetime] = None
|
||||||
|
auto_recording_last_error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RecordingInfo:
|
class RecordingInfo:
|
||||||
|
|||||||
@@ -19,58 +19,55 @@ from .core.timezone_utils import log_time_info, check_time_sync
|
|||||||
from .mqtt.client import MQTTClient
|
from .mqtt.client import MQTTClient
|
||||||
from .camera.manager import CameraManager
|
from .camera.manager import CameraManager
|
||||||
from .storage.manager import StorageManager
|
from .storage.manager import StorageManager
|
||||||
|
from .recording.auto_manager import AutoRecordingManager
|
||||||
from .api.server import APIServer
|
from .api.server import APIServer
|
||||||
|
|
||||||
|
|
||||||
class USDAVisionSystem:
|
class USDAVisionSystem:
|
||||||
"""Main application coordinator for the USDA Vision Camera System"""
|
"""Main application coordinator for the USDA Vision Camera System"""
|
||||||
|
|
||||||
def __init__(self, config_file: Optional[str] = None):
|
def __init__(self, config_file: Optional[str] = None):
|
||||||
# Load configuration first (basic logging will be used initially)
|
# Load configuration first (basic logging will be used initially)
|
||||||
self.config = Config(config_file)
|
self.config = Config(config_file)
|
||||||
|
|
||||||
# Setup comprehensive logging
|
# Setup comprehensive logging
|
||||||
self.logger_setup = setup_logging(
|
self.logger_setup = setup_logging(log_level=self.config.system.log_level, log_file=self.config.system.log_file)
|
||||||
log_level=self.config.system.log_level,
|
|
||||||
log_file=self.config.system.log_file
|
|
||||||
)
|
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Setup error tracking and performance monitoring
|
# Setup error tracking and performance monitoring
|
||||||
self.error_tracker = get_error_tracker("main_system")
|
self.error_tracker = get_error_tracker("main_system")
|
||||||
self.performance_logger = get_performance_logger("main_system")
|
self.performance_logger = get_performance_logger("main_system")
|
||||||
|
|
||||||
# Initialize core components
|
# Initialize core components
|
||||||
self.state_manager = StateManager()
|
self.state_manager = StateManager()
|
||||||
self.event_system = EventSystem()
|
self.event_system = EventSystem()
|
||||||
|
|
||||||
# Initialize system components
|
# Initialize system components
|
||||||
self.storage_manager = StorageManager(self.config, self.state_manager, self.event_system)
|
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.auto_recording_manager = AutoRecordingManager(self.config, self.state_manager, self.event_system, self.camera_manager)
|
||||||
self.config, self.state_manager, self.event_system,
|
self.api_server = APIServer(self.config, self.state_manager, self.event_system, self.camera_manager, self.mqtt_client, self.storage_manager, self.auto_recording_manager)
|
||||||
self.camera_manager, self.mqtt_client, self.storage_manager
|
|
||||||
)
|
|
||||||
|
|
||||||
# System state
|
# System state
|
||||||
self.running = False
|
self.running = False
|
||||||
self.start_time: Optional[datetime] = None
|
self.start_time: Optional[datetime] = None
|
||||||
|
|
||||||
# Setup signal handlers for graceful shutdown
|
# Setup signal handlers for graceful shutdown
|
||||||
self._setup_signal_handlers()
|
self._setup_signal_handlers()
|
||||||
|
|
||||||
self.logger.info("USDA Vision Camera System initialized")
|
self.logger.info("USDA Vision Camera System initialized")
|
||||||
|
|
||||||
def _setup_signal_handlers(self) -> None:
|
def _setup_signal_handlers(self) -> None:
|
||||||
"""Setup signal handlers for graceful shutdown"""
|
"""Setup signal handlers for graceful shutdown"""
|
||||||
|
|
||||||
def signal_handler(signum, frame):
|
def signal_handler(signum, frame):
|
||||||
self.logger.info(f"Received signal {signum}, initiating graceful shutdown...")
|
self.logger.info(f"Received signal {signum}, initiating graceful shutdown...")
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
def start(self) -> bool:
|
def start(self) -> bool:
|
||||||
"""Start the entire system"""
|
"""Start the entire system"""
|
||||||
if self.running:
|
if self.running:
|
||||||
@@ -86,10 +83,7 @@ class USDAVisionSystem:
|
|||||||
log_time_info(self.logger)
|
log_time_info(self.logger)
|
||||||
sync_info = check_time_sync()
|
sync_info = check_time_sync()
|
||||||
if sync_info["sync_status"] == "out_of_sync":
|
if sync_info["sync_status"] == "out_of_sync":
|
||||||
self.error_tracker.log_warning(
|
self.error_tracker.log_warning(f"System time may be out of sync (difference: {sync_info.get('time_diff_seconds', 'unknown')}s)", "time_sync_check")
|
||||||
f"System time may be out of sync (difference: {sync_info.get('time_diff_seconds', 'unknown')}s)",
|
|
||||||
"time_sync_check"
|
|
||||||
)
|
|
||||||
elif sync_info["sync_status"] == "synchronized":
|
elif sync_info["sync_status"] == "synchronized":
|
||||||
self.logger.info("✅ System time is synchronized")
|
self.logger.info("✅ System time is synchronized")
|
||||||
|
|
||||||
@@ -131,6 +125,17 @@ class USDAVisionSystem:
|
|||||||
self.mqtt_client.stop()
|
self.mqtt_client.stop()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Start auto-recording manager
|
||||||
|
self.logger.info("Starting auto-recording manager...")
|
||||||
|
try:
|
||||||
|
if not self.auto_recording_manager.start():
|
||||||
|
self.error_tracker.log_warning("Failed to start auto-recording manager", "auto_recording_startup")
|
||||||
|
else:
|
||||||
|
self.logger.info("Auto-recording manager started successfully")
|
||||||
|
except Exception as e:
|
||||||
|
self.error_tracker.log_error(e, "auto_recording_startup")
|
||||||
|
self.logger.warning("Auto-recording manager failed to start (continuing without auto-recording)")
|
||||||
|
|
||||||
# Start API server
|
# Start API server
|
||||||
self.logger.info("Starting API server...")
|
self.logger.info("Starting API server...")
|
||||||
try:
|
try:
|
||||||
@@ -147,11 +152,7 @@ class USDAVisionSystem:
|
|||||||
self.state_manager.set_system_started(True)
|
self.state_manager.set_system_started(True)
|
||||||
|
|
||||||
# Publish system started event
|
# Publish system started event
|
||||||
self.event_system.publish(
|
self.event_system.publish(EventType.SYSTEM_SHUTDOWN, "main_system", {"action": "started", "timestamp": self.start_time.isoformat()}) # We don't have SYSTEM_STARTED, using closest
|
||||||
EventType.SYSTEM_SHUTDOWN, # We don't have SYSTEM_STARTED, using closest
|
|
||||||
"main_system",
|
|
||||||
{"action": "started", "timestamp": self.start_time.isoformat()}
|
|
||||||
)
|
|
||||||
|
|
||||||
startup_time = self.performance_logger.end_timer("system_startup")
|
startup_time = self.performance_logger.end_timer("system_startup")
|
||||||
self.logger.info(f"USDA Vision Camera System started successfully in {startup_time:.2f}s")
|
self.logger.info(f"USDA Vision Camera System started successfully in {startup_time:.2f}s")
|
||||||
@@ -161,89 +162,77 @@ class USDAVisionSystem:
|
|||||||
self.error_tracker.log_error(e, "system_startup")
|
self.error_tracker.log_error(e, "system_startup")
|
||||||
self.stop()
|
self.stop()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""Stop the entire system gracefully"""
|
"""Stop the entire system gracefully"""
|
||||||
if not self.running:
|
if not self.running:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.info("Stopping USDA Vision Camera System...")
|
self.logger.info("Stopping USDA Vision Camera System...")
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Update system state
|
# Update system state
|
||||||
self.state_manager.set_system_started(False)
|
self.state_manager.set_system_started(False)
|
||||||
|
|
||||||
# Publish system shutdown event
|
# Publish system shutdown event
|
||||||
self.event_system.publish(
|
self.event_system.publish(EventType.SYSTEM_SHUTDOWN, "main_system", {"action": "stopping", "timestamp": datetime.now().isoformat()})
|
||||||
EventType.SYSTEM_SHUTDOWN,
|
|
||||||
"main_system",
|
|
||||||
{"action": "stopping", "timestamp": datetime.now().isoformat()}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Stop API server
|
# Stop API server
|
||||||
self.api_server.stop()
|
self.api_server.stop()
|
||||||
|
|
||||||
|
# Stop auto-recording manager
|
||||||
|
self.auto_recording_manager.stop()
|
||||||
|
|
||||||
# Stop camera manager (this will stop all recordings)
|
# Stop camera manager (this will stop all recordings)
|
||||||
self.camera_manager.stop()
|
self.camera_manager.stop()
|
||||||
|
|
||||||
# Stop MQTT client
|
# Stop MQTT client
|
||||||
self.mqtt_client.stop()
|
self.mqtt_client.stop()
|
||||||
|
|
||||||
# Final cleanup
|
# Final cleanup
|
||||||
if self.start_time:
|
if self.start_time:
|
||||||
uptime = (datetime.now() - self.start_time).total_seconds()
|
uptime = (datetime.now() - self.start_time).total_seconds()
|
||||||
self.logger.info(f"System uptime: {uptime:.1f} seconds")
|
self.logger.info(f"System uptime: {uptime:.1f} seconds")
|
||||||
|
|
||||||
self.logger.info("USDA Vision Camera System stopped")
|
self.logger.info("USDA Vision Camera System stopped")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error during system shutdown: {e}")
|
self.logger.error(f"Error during system shutdown: {e}")
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
"""Run the system (blocking call)"""
|
"""Run the system (blocking call)"""
|
||||||
if not self.start():
|
if not self.start():
|
||||||
self.logger.error("Failed to start system")
|
self.logger.error("Failed to start system")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.info("System running... Press Ctrl+C to stop")
|
self.logger.info("System running... Press Ctrl+C to stop")
|
||||||
|
|
||||||
# Main loop - just keep the system alive
|
# Main loop - just keep the system alive
|
||||||
while self.running:
|
while self.running:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# Periodic maintenance tasks could go here
|
# Periodic maintenance tasks could go here
|
||||||
# For example: cleanup old recordings, health checks, etc.
|
# For example: cleanup old recordings, health checks, etc.
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
self.logger.info("Keyboard interrupt received")
|
self.logger.info("Keyboard interrupt received")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Unexpected error in main loop: {e}")
|
self.logger.error(f"Unexpected error in main loop: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
def get_system_status(self) -> dict:
|
def get_system_status(self) -> dict:
|
||||||
"""Get comprehensive system status"""
|
"""Get comprehensive system status"""
|
||||||
return {
|
return {
|
||||||
"running": self.running,
|
"running": self.running,
|
||||||
"start_time": self.start_time.isoformat() if self.start_time else None,
|
"start_time": self.start_time.isoformat() if self.start_time else None,
|
||||||
"uptime_seconds": (datetime.now() - self.start_time).total_seconds() if self.start_time else 0,
|
"uptime_seconds": (datetime.now() - self.start_time).total_seconds() if self.start_time else 0,
|
||||||
"components": {
|
"components": {"mqtt_client": {"running": self.mqtt_client.is_running(), "connected": self.mqtt_client.is_connected()}, "camera_manager": {"running": self.camera_manager.is_running()}, "api_server": {"running": self.api_server.is_running()}},
|
||||||
"mqtt_client": {
|
"state_summary": self.state_manager.get_system_summary(),
|
||||||
"running": self.mqtt_client.is_running(),
|
|
||||||
"connected": self.mqtt_client.is_connected()
|
|
||||||
},
|
|
||||||
"camera_manager": {
|
|
||||||
"running": self.camera_manager.is_running()
|
|
||||||
},
|
|
||||||
"api_server": {
|
|
||||||
"running": self.api_server.is_running()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"state_summary": self.state_manager.get_system_summary()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
"""Check if system is running"""
|
"""Check if system is running"""
|
||||||
return self.running
|
return self.running
|
||||||
@@ -252,31 +241,20 @@ class USDAVisionSystem:
|
|||||||
def main():
|
def main():
|
||||||
"""Main entry point for the application"""
|
"""Main entry point for the application"""
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="USDA Vision Camera System")
|
parser = argparse.ArgumentParser(description="USDA Vision Camera System")
|
||||||
parser.add_argument(
|
parser.add_argument("--config", type=str, help="Path to configuration file", default="config.json")
|
||||||
"--config",
|
parser.add_argument("--log-level", type=str, choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Override log level", default=None)
|
||||||
type=str,
|
|
||||||
help="Path to configuration file",
|
|
||||||
default="config.json"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--log-level",
|
|
||||||
type=str,
|
|
||||||
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
||||||
help="Override log level",
|
|
||||||
default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Create and run system
|
# Create and run system
|
||||||
system = USDAVisionSystem(args.config)
|
system = USDAVisionSystem(args.config)
|
||||||
|
|
||||||
# Override log level if specified
|
# Override log level if specified
|
||||||
if args.log_level:
|
if args.log_level:
|
||||||
logging.getLogger().setLevel(getattr(logging, args.log_level))
|
logging.getLogger().setLevel(getattr(logging, args.log_level))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
system.run()
|
system.run()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
10
usda_vision_system/recording/__init__.py
Normal file
10
usda_vision_system/recording/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Recording module for the USDA Vision Camera System.
|
||||||
|
|
||||||
|
This module contains components for managing automatic recording
|
||||||
|
based on machine state changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .auto_manager import AutoRecordingManager
|
||||||
|
|
||||||
|
__all__ = ["AutoRecordingManager"]
|
||||||
352
usda_vision_system/recording/auto_manager.py
Normal file
352
usda_vision_system/recording/auto_manager.py
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
"""
|
||||||
|
Auto-Recording Manager for the USDA Vision Camera System.
|
||||||
|
|
||||||
|
This module manages automatic recording start/stop based on machine state changes
|
||||||
|
received via MQTT. It includes retry logic for failed recording attempts and
|
||||||
|
tracks auto-recording status for each camera.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Optional, Any
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from ..core.config import Config, CameraConfig
|
||||||
|
from ..core.state_manager import StateManager, MachineState
|
||||||
|
from ..core.events import EventSystem, EventType, Event
|
||||||
|
from ..core.timezone_utils import format_filename_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class AutoRecordingManager:
|
||||||
|
"""Manages automatic recording based on machine state changes"""
|
||||||
|
|
||||||
|
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager):
|
||||||
|
self.config = config
|
||||||
|
self.state_manager = state_manager
|
||||||
|
self.event_system = event_system
|
||||||
|
self.camera_manager = camera_manager
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Threading
|
||||||
|
self.running = False
|
||||||
|
self._retry_thread: Optional[threading.Thread] = None
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
# Track retry attempts for each camera
|
||||||
|
self._retry_queue: Dict[str, Dict[str, Any]] = {} # camera_name -> retry_info
|
||||||
|
self._retry_lock = threading.RLock()
|
||||||
|
|
||||||
|
# Subscribe to machine state change events
|
||||||
|
self.event_system.subscribe(EventType.MACHINE_STATE_CHANGED, self._on_machine_state_changed)
|
||||||
|
|
||||||
|
def start(self) -> bool:
|
||||||
|
"""Start the auto-recording manager"""
|
||||||
|
if self.running:
|
||||||
|
self.logger.warning("Auto-recording manager is already running")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.config.system.auto_recording_enabled:
|
||||||
|
self.logger.info("Auto-recording is disabled in system configuration")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.logger.info("Starting auto-recording manager...")
|
||||||
|
self.running = True
|
||||||
|
self._stop_event.clear()
|
||||||
|
|
||||||
|
# Initialize camera auto-recording status
|
||||||
|
self._initialize_camera_status()
|
||||||
|
|
||||||
|
# Start retry thread
|
||||||
|
self._retry_thread = threading.Thread(target=self._retry_loop, daemon=True)
|
||||||
|
self._retry_thread.start()
|
||||||
|
|
||||||
|
self.logger.info("Auto-recording manager started successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stop the auto-recording manager"""
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info("Stopping auto-recording manager...")
|
||||||
|
self.running = False
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
# Wait for retry thread to finish
|
||||||
|
if self._retry_thread and self._retry_thread.is_alive():
|
||||||
|
self._retry_thread.join(timeout=5)
|
||||||
|
|
||||||
|
self.logger.info("Auto-recording manager stopped")
|
||||||
|
|
||||||
|
def _initialize_camera_status(self) -> None:
|
||||||
|
"""Initialize auto-recording status for all cameras"""
|
||||||
|
for camera_config in self.config.cameras:
|
||||||
|
if camera_config.enabled and camera_config.auto_start_recording_enabled:
|
||||||
|
# Update camera status in state manager
|
||||||
|
camera_info = self.state_manager.get_camera_info(camera_config.name)
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_enabled = True
|
||||||
|
self.logger.info(f"Auto-recording enabled for camera {camera_config.name}")
|
||||||
|
|
||||||
|
def _on_machine_state_changed(self, event: Event) -> None:
|
||||||
|
"""Handle machine state change events"""
|
||||||
|
try:
|
||||||
|
machine_name = event.data.get("machine_name")
|
||||||
|
new_state = event.data.get("state")
|
||||||
|
|
||||||
|
if not machine_name or not new_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(f"Machine state changed: {machine_name} -> {new_state}")
|
||||||
|
|
||||||
|
# Find cameras associated with this machine
|
||||||
|
associated_cameras = self._get_cameras_for_machine(machine_name)
|
||||||
|
|
||||||
|
for camera_config in associated_cameras:
|
||||||
|
if not camera_config.enabled or not camera_config.auto_start_recording_enabled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if new_state.lower() == "on":
|
||||||
|
self._handle_machine_on(camera_config)
|
||||||
|
elif new_state.lower() == "off":
|
||||||
|
self._handle_machine_off(camera_config)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error handling machine state change: {e}")
|
||||||
|
|
||||||
|
def _get_cameras_for_machine(self, machine_name: str) -> list[CameraConfig]:
|
||||||
|
"""Get all cameras associated with a machine topic"""
|
||||||
|
associated_cameras = []
|
||||||
|
|
||||||
|
# Map machine names to topics
|
||||||
|
machine_topic_map = {
|
||||||
|
"vibratory_conveyor": "vibratory_conveyor",
|
||||||
|
"blower_separator": "blower_separator"
|
||||||
|
}
|
||||||
|
|
||||||
|
machine_topic = machine_topic_map.get(machine_name)
|
||||||
|
if not machine_topic:
|
||||||
|
return associated_cameras
|
||||||
|
|
||||||
|
for camera_config in self.config.cameras:
|
||||||
|
if camera_config.machine_topic == machine_topic:
|
||||||
|
associated_cameras.append(camera_config)
|
||||||
|
|
||||||
|
return associated_cameras
|
||||||
|
|
||||||
|
def _handle_machine_on(self, camera_config: CameraConfig) -> None:
|
||||||
|
"""Handle machine turning on - start recording"""
|
||||||
|
camera_name = camera_config.name
|
||||||
|
|
||||||
|
# Check if camera is already recording
|
||||||
|
camera_info = self.state_manager.get_camera_info(camera_name)
|
||||||
|
if camera_info and camera_info.is_recording:
|
||||||
|
self.logger.info(f"Camera {camera_name} is already recording, skipping auto-start")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(f"Machine turned ON - attempting to start recording for camera {camera_name}")
|
||||||
|
|
||||||
|
# Update auto-recording status
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_active = True
|
||||||
|
camera_info.auto_recording_last_attempt = datetime.now()
|
||||||
|
|
||||||
|
# Attempt to start recording
|
||||||
|
success = self._start_recording_for_camera(camera_config)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
# Add to retry queue
|
||||||
|
self._add_to_retry_queue(camera_config, "start")
|
||||||
|
|
||||||
|
def _handle_machine_off(self, camera_config: CameraConfig) -> None:
|
||||||
|
"""Handle machine turning off - stop recording"""
|
||||||
|
camera_name = camera_config.name
|
||||||
|
|
||||||
|
self.logger.info(f"Machine turned OFF - attempting to stop recording for camera {camera_name}")
|
||||||
|
|
||||||
|
# Update auto-recording status
|
||||||
|
camera_info = self.state_manager.get_camera_info(camera_name)
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_active = False
|
||||||
|
|
||||||
|
# Remove from retry queue if present
|
||||||
|
with self._retry_lock:
|
||||||
|
if camera_name in self._retry_queue:
|
||||||
|
del self._retry_queue[camera_name]
|
||||||
|
|
||||||
|
# Attempt to stop recording
|
||||||
|
self._stop_recording_for_camera(camera_config)
|
||||||
|
|
||||||
|
def _start_recording_for_camera(self, camera_config: CameraConfig) -> bool:
|
||||||
|
"""Start recording for a specific camera"""
|
||||||
|
try:
|
||||||
|
camera_name = camera_config.name
|
||||||
|
|
||||||
|
# Generate filename with timestamp and machine info
|
||||||
|
timestamp = format_filename_timestamp()
|
||||||
|
machine_name = camera_config.machine_topic.replace("_", "-")
|
||||||
|
filename = f"{camera_name}_auto_{machine_name}_{timestamp}.avi"
|
||||||
|
|
||||||
|
# Use camera manager to start recording
|
||||||
|
success = self.camera_manager.manual_start_recording(camera_name, filename)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.logger.info(f"Successfully started auto-recording for camera {camera_name}: {filename}")
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
camera_info = self.state_manager.get_camera_info(camera_name)
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_failure_count = 0
|
||||||
|
camera_info.auto_recording_last_error = None
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.logger.error(f"Failed to start auto-recording for camera {camera_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error starting auto-recording for camera {camera_config.name}: {e}")
|
||||||
|
|
||||||
|
# Update error status
|
||||||
|
camera_info = self.state_manager.get_camera_info(camera_config.name)
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_last_error = str(e)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _stop_recording_for_camera(self, camera_config: CameraConfig) -> bool:
|
||||||
|
"""Stop recording for a specific camera"""
|
||||||
|
try:
|
||||||
|
camera_name = camera_config.name
|
||||||
|
|
||||||
|
# Use camera manager to stop recording
|
||||||
|
success = self.camera_manager.manual_stop_recording(camera_name)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.logger.info(f"Successfully stopped auto-recording for camera {camera_name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Failed to stop auto-recording for camera {camera_name} (may not have been recording)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error stopping auto-recording for camera {camera_config.name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _add_to_retry_queue(self, camera_config: CameraConfig, action: str) -> None:
|
||||||
|
"""Add a camera to the retry queue"""
|
||||||
|
with self._retry_lock:
|
||||||
|
camera_name = camera_config.name
|
||||||
|
|
||||||
|
retry_info = {
|
||||||
|
"camera_config": camera_config,
|
||||||
|
"action": action,
|
||||||
|
"attempt_count": 0,
|
||||||
|
"next_retry_time": datetime.now() + timedelta(seconds=camera_config.auto_recording_retry_delay_seconds),
|
||||||
|
"max_retries": camera_config.auto_recording_max_retries
|
||||||
|
}
|
||||||
|
|
||||||
|
self._retry_queue[camera_name] = retry_info
|
||||||
|
self.logger.info(f"Added camera {camera_name} to retry queue for {action} (max retries: {retry_info['max_retries']})")
|
||||||
|
|
||||||
|
def _retry_loop(self) -> None:
|
||||||
|
"""Background thread to handle retry attempts"""
|
||||||
|
while self.running and not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
current_time = datetime.now()
|
||||||
|
cameras_to_retry = []
|
||||||
|
|
||||||
|
# Find cameras ready for retry
|
||||||
|
with self._retry_lock:
|
||||||
|
for camera_name, retry_info in list(self._retry_queue.items()):
|
||||||
|
if current_time >= retry_info["next_retry_time"]:
|
||||||
|
cameras_to_retry.append((camera_name, retry_info))
|
||||||
|
|
||||||
|
# Process retries
|
||||||
|
for camera_name, retry_info in cameras_to_retry:
|
||||||
|
self._process_retry(camera_name, retry_info)
|
||||||
|
|
||||||
|
# Sleep for a short interval
|
||||||
|
self._stop_event.wait(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error in retry loop: {e}")
|
||||||
|
self._stop_event.wait(5)
|
||||||
|
|
||||||
|
def _process_retry(self, camera_name: str, retry_info: Dict[str, Any]) -> None:
|
||||||
|
"""Process a retry attempt for a camera"""
|
||||||
|
try:
|
||||||
|
retry_info["attempt_count"] += 1
|
||||||
|
camera_config = retry_info["camera_config"]
|
||||||
|
action = retry_info["action"]
|
||||||
|
|
||||||
|
self.logger.info(f"Retry attempt {retry_info['attempt_count']}/{retry_info['max_retries']} for camera {camera_name} ({action})")
|
||||||
|
|
||||||
|
# Update camera status
|
||||||
|
camera_info = self.state_manager.get_camera_info(camera_name)
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_last_attempt = datetime.now()
|
||||||
|
camera_info.auto_recording_failure_count = retry_info["attempt_count"]
|
||||||
|
|
||||||
|
# Attempt the action
|
||||||
|
success = False
|
||||||
|
if action == "start":
|
||||||
|
success = self._start_recording_for_camera(camera_config)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Success - remove from retry queue
|
||||||
|
with self._retry_lock:
|
||||||
|
if camera_name in self._retry_queue:
|
||||||
|
del self._retry_queue[camera_name]
|
||||||
|
self.logger.info(f"Retry successful for camera {camera_name}")
|
||||||
|
else:
|
||||||
|
# Failed - check if we should retry again
|
||||||
|
if retry_info["attempt_count"] >= retry_info["max_retries"]:
|
||||||
|
# Max retries reached
|
||||||
|
with self._retry_lock:
|
||||||
|
if camera_name in self._retry_queue:
|
||||||
|
del self._retry_queue[camera_name]
|
||||||
|
|
||||||
|
error_msg = f"Max retry attempts ({retry_info['max_retries']}) reached for camera {camera_name}"
|
||||||
|
self.logger.error(error_msg)
|
||||||
|
|
||||||
|
# Update camera status
|
||||||
|
if camera_info:
|
||||||
|
camera_info.auto_recording_last_error = error_msg
|
||||||
|
camera_info.auto_recording_active = False
|
||||||
|
else:
|
||||||
|
# Schedule next retry
|
||||||
|
retry_info["next_retry_time"] = datetime.now() + timedelta(seconds=camera_config.auto_recording_retry_delay_seconds)
|
||||||
|
self.logger.info(f"Scheduling next retry for camera {camera_name} in {camera_config.auto_recording_retry_delay_seconds} seconds")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error processing retry for camera {camera_name}: {e}")
|
||||||
|
|
||||||
|
# Remove from retry queue on error
|
||||||
|
with self._retry_lock:
|
||||||
|
if camera_name in self._retry_queue:
|
||||||
|
del self._retry_queue[camera_name]
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get auto-recording manager status"""
|
||||||
|
with self._retry_lock:
|
||||||
|
retry_queue_status = {
|
||||||
|
camera_name: {
|
||||||
|
"action": info["action"],
|
||||||
|
"attempt_count": info["attempt_count"],
|
||||||
|
"max_retries": info["max_retries"],
|
||||||
|
"next_retry_time": info["next_retry_time"].isoformat()
|
||||||
|
}
|
||||||
|
for camera_name, info in self._retry_queue.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"running": self.running,
|
||||||
|
"auto_recording_enabled": self.config.system.auto_recording_enabled,
|
||||||
|
"retry_queue": retry_queue_status,
|
||||||
|
"enabled_cameras": [
|
||||||
|
camera.name for camera in self.config.cameras
|
||||||
|
if camera.enabled and camera.auto_start_recording_enabled
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user