diff --git a/AI_AGENT_INSTRUCTIONS.md b/AI_AGENT_INSTRUCTIONS.md new file mode 100644 index 0000000..dedd89e --- /dev/null +++ b/AI_AGENT_INSTRUCTIONS.md @@ -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; + 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. diff --git a/AUTO_RECORDING_FEATURE_GUIDE.md b/AUTO_RECORDING_FEATURE_GUIDE.md new file mode 100644 index 0000000..fbdb14c --- /dev/null +++ b/AUTO_RECORDING_FEATURE_GUIDE.md @@ -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; + 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 && ( + + Auto-Recording {camera.auto_recording_active ? "Active" : "Enabled"} + +)} + +// Failure indicator +{camera.auto_recording_failure_count > 0 && ( + + Auto-recording failures: {camera.auto_recording_failure_count} + +)} +``` + +### 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 ( + + ); +}; +``` + +### 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! diff --git a/api-endpoints.http b/api-endpoints.http index 85c00ca..545fe39 100644 --- a/api-endpoints.http +++ b/api-endpoints.http @@ -291,6 +291,48 @@ POST http://localhost:8000/cameras/camera2/stop-recording # "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 ############################################################################### diff --git a/config.json b/config.json index 862e6fb..eaf518c 100644 --- a/config.json +++ b/config.json @@ -22,24 +22,28 @@ "api_host": "0.0.0.0", "api_port": 8000, "enable_api": true, - "timezone": "America/New_York" + "timezone": "America/New_York", + "auto_recording_enabled": true }, "cameras": [ { "name": "camera1", "machine_topic": "vibratory_conveyor", "storage_path": "/storage/camera1", - "exposure_ms": 1.0, - "gain": 3.5, + "exposure_ms": 0.5, + "gain": 0.5, "target_fps": 0, "enabled": true, + "auto_start_recording_enabled": true, + "auto_recording_max_retries": 3, + "auto_recording_retry_delay_seconds": 5, "sharpness": 100, "contrast": 100, "saturation": 100, - "gamma": 85, + "gamma": 110, "noise_filter_enabled": false, "denoise_3d_enabled": false, - "auto_white_balance": false, + "auto_white_balance": true, "color_temperature_preset": 0, "anti_flicker_enabled": false, "light_frequency": 1, @@ -51,14 +55,17 @@ "name": "camera2", "machine_topic": "blower_separator", "storage_path": "/storage/camera2", - "exposure_ms": 1.0, - "gain": 3.5, + "exposure_ms": 0.5, + "gain": 0.3, "target_fps": 0, "enabled": true, + "auto_start_recording_enabled": true, + "auto_recording_max_retries": 3, + "auto_recording_retry_delay_seconds": 5, "sharpness": 100, "contrast": 100, - "saturation": 100, - "gamma": 100, + "saturation": 75, + "gamma": 110, "noise_filter_enabled": false, "denoise_3d_enabled": false, "auto_white_balance": true, diff --git a/test_auto_recording_simple.py b/test_auto_recording_simple.py new file mode 100644 index 0000000..32cf89c --- /dev/null +++ b/test_auto_recording_simple.py @@ -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) diff --git a/tests/test_auto_recording.py b/tests/test_auto_recording.py new file mode 100644 index 0000000..02732b3 --- /dev/null +++ b/tests/test_auto_recording.py @@ -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) diff --git a/usda_vision_system/__pycache__/main.cpython-311.pyc b/usda_vision_system/__pycache__/main.cpython-311.pyc index f19dd0f..ad5fbd3 100644 Binary files a/usda_vision_system/__pycache__/main.cpython-311.pyc and b/usda_vision_system/__pycache__/main.cpython-311.pyc differ diff --git a/usda_vision_system/api/__pycache__/models.cpython-311.pyc b/usda_vision_system/api/__pycache__/models.cpython-311.pyc index e7d53f0..666a852 100644 Binary files a/usda_vision_system/api/__pycache__/models.cpython-311.pyc and b/usda_vision_system/api/__pycache__/models.cpython-311.pyc differ diff --git a/usda_vision_system/api/__pycache__/server.cpython-311.pyc b/usda_vision_system/api/__pycache__/server.cpython-311.pyc index dd12bf2..89af9de 100644 Binary files a/usda_vision_system/api/__pycache__/server.cpython-311.pyc and b/usda_vision_system/api/__pycache__/server.cpython-311.pyc differ diff --git a/usda_vision_system/api/models.py b/usda_vision_system/api/models.py index 02b95ea..6217214 100644 --- a/usda_vision_system/api/models.py +++ b/usda_vision_system/api/models.py @@ -57,6 +57,13 @@ class CameraStatusResponse(BaseModel): current_recording_file: 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): """Recording information response model""" @@ -120,6 +127,11 @@ class CameraConfigResponse(BaseModel): storage_path: str enabled: bool + # Auto-recording settings + auto_start_recording_enabled: bool + auto_recording_max_retries: int + auto_recording_retry_delay_seconds: int + # Basic settings exposure_ms: float gain: float @@ -173,6 +185,30 @@ class StopRecordingResponse(BaseModel): 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): """Storage statistics response model""" diff --git a/usda_vision_system/api/server.py b/usda_vision_system/api/server.py index 3fcd136..7cdb0ac 100644 --- a/usda_vision_system/api/server.py +++ b/usda_vision_system/api/server.py @@ -66,13 +66,14 @@ class WebSocketManager: class APIServer: """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.state_manager = state_manager self.event_system = event_system self.camera_manager = camera_manager self.mqtt_client = mqtt_client self.storage_manager = storage_manager + self.auto_recording_manager = auto_recording_manager self.logger = logging.getLogger(__name__) # FastAPI app @@ -162,7 +163,21 @@ class APIServer: try: cameras = self.state_manager.get_all_cameras() 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() } except Exception as e: @@ -471,6 +486,74 @@ class APIServer: self.logger.error(f"Error reinitializing camera: {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]) async def get_recordings(): """Get all recording sessions""" diff --git a/usda_vision_system/core/__pycache__/config.cpython-311.pyc b/usda_vision_system/core/__pycache__/config.cpython-311.pyc index 2770314..7529dcb 100644 Binary files a/usda_vision_system/core/__pycache__/config.cpython-311.pyc and b/usda_vision_system/core/__pycache__/config.cpython-311.pyc differ diff --git a/usda_vision_system/core/__pycache__/state_manager.cpython-311.pyc b/usda_vision_system/core/__pycache__/state_manager.cpython-311.pyc index 822dd62..8b2ce4c 100644 Binary files a/usda_vision_system/core/__pycache__/state_manager.cpython-311.pyc and b/usda_vision_system/core/__pycache__/state_manager.cpython-311.pyc differ diff --git a/usda_vision_system/core/config.py b/usda_vision_system/core/config.py index bbde668..32d1639 100644 --- a/usda_vision_system/core/config.py +++ b/usda_vision_system/core/config.py @@ -40,6 +40,11 @@ class CameraConfig: target_fps: float = 3.0 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 sharpness: int = 100 # 0-200, default 100 (no sharpening) contrast: int = 100 # 0-200, default 100 (normal contrast) @@ -86,7 +91,10 @@ class SystemConfig: api_host: str = "0.0.0.0" api_port: int = 8000 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: diff --git a/usda_vision_system/core/state_manager.py b/usda_vision_system/core/state_manager.py index 9f3732d..5683d22 100644 --- a/usda_vision_system/core/state_manager.py +++ b/usda_vision_system/core/state_manager.py @@ -77,6 +77,13 @@ class CameraInfo: current_recording_file: Optional[str] = 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 class RecordingInfo: diff --git a/usda_vision_system/main.py b/usda_vision_system/main.py index 1c3d2e6..b50427f 100644 --- a/usda_vision_system/main.py +++ b/usda_vision_system/main.py @@ -19,58 +19,55 @@ from .core.timezone_utils import log_time_info, check_time_sync from .mqtt.client import MQTTClient from .camera.manager import CameraManager from .storage.manager import StorageManager +from .recording.auto_manager import AutoRecordingManager from .api.server import APIServer class USDAVisionSystem: """Main application coordinator for the USDA Vision Camera System""" - + def __init__(self, config_file: Optional[str] = None): # Load configuration first (basic logging will be used initially) self.config = Config(config_file) # Setup comprehensive logging - self.logger_setup = setup_logging( - log_level=self.config.system.log_level, - log_file=self.config.system.log_file - ) + self.logger_setup = setup_logging(log_level=self.config.system.log_level, log_file=self.config.system.log_file) self.logger = logging.getLogger(__name__) # Setup error tracking and performance monitoring self.error_tracker = get_error_tracker("main_system") self.performance_logger = get_performance_logger("main_system") - + # Initialize core components self.state_manager = StateManager() self.event_system = EventSystem() - + # Initialize system components 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.camera_manager = CameraManager(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 = AutoRecordingManager(self.config, self.state_manager, self.event_system, self.camera_manager) + 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) + # System state self.running = False self.start_time: Optional[datetime] = None - + # Setup signal handlers for graceful shutdown self._setup_signal_handlers() - + self.logger.info("USDA Vision Camera System initialized") - + def _setup_signal_handlers(self) -> None: """Setup signal handlers for graceful shutdown""" + def signal_handler(signum, frame): self.logger.info(f"Received signal {signum}, initiating graceful shutdown...") self.stop() - + signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - + def start(self) -> bool: """Start the entire system""" if self.running: @@ -86,10 +83,7 @@ class USDAVisionSystem: log_time_info(self.logger) sync_info = check_time_sync() if sync_info["sync_status"] == "out_of_sync": - 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" - ) + 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") elif sync_info["sync_status"] == "synchronized": self.logger.info("โœ… System time is synchronized") @@ -131,6 +125,17 @@ class USDAVisionSystem: self.mqtt_client.stop() 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 self.logger.info("Starting API server...") try: @@ -147,11 +152,7 @@ class USDAVisionSystem: self.state_manager.set_system_started(True) # Publish system started event - self.event_system.publish( - EventType.SYSTEM_SHUTDOWN, # We don't have SYSTEM_STARTED, using closest - "main_system", - {"action": "started", "timestamp": self.start_time.isoformat()} - ) + self.event_system.publish(EventType.SYSTEM_SHUTDOWN, "main_system", {"action": "started", "timestamp": self.start_time.isoformat()}) # We don't have SYSTEM_STARTED, using closest startup_time = self.performance_logger.end_timer("system_startup") 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.stop() return False - + def stop(self) -> None: """Stop the entire system gracefully""" if not self.running: return - + self.logger.info("Stopping USDA Vision Camera System...") self.running = False - + try: # Update system state self.state_manager.set_system_started(False) - + # Publish system shutdown event - self.event_system.publish( - EventType.SYSTEM_SHUTDOWN, - "main_system", - {"action": "stopping", "timestamp": datetime.now().isoformat()} - ) - + self.event_system.publish(EventType.SYSTEM_SHUTDOWN, "main_system", {"action": "stopping", "timestamp": datetime.now().isoformat()}) + # Stop API server self.api_server.stop() - + + # Stop auto-recording manager + self.auto_recording_manager.stop() + # Stop camera manager (this will stop all recordings) self.camera_manager.stop() - + # Stop MQTT client self.mqtt_client.stop() - + # Final cleanup if self.start_time: uptime = (datetime.now() - self.start_time).total_seconds() self.logger.info(f"System uptime: {uptime:.1f} seconds") - + self.logger.info("USDA Vision Camera System stopped") - + except Exception as e: self.logger.error(f"Error during system shutdown: {e}") - + def run(self) -> None: """Run the system (blocking call)""" if not self.start(): self.logger.error("Failed to start system") return - + try: self.logger.info("System running... Press Ctrl+C to stop") - + # Main loop - just keep the system alive while self.running: time.sleep(1) - + # Periodic maintenance tasks could go here # For example: cleanup old recordings, health checks, etc. - + except KeyboardInterrupt: self.logger.info("Keyboard interrupt received") except Exception as e: self.logger.error(f"Unexpected error in main loop: {e}") finally: self.stop() - + def get_system_status(self) -> dict: """Get comprehensive system status""" return { "running": self.running, "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, - "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() - } - }, - "state_summary": self.state_manager.get_system_summary() + "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()}}, + "state_summary": self.state_manager.get_system_summary(), } - + def is_running(self) -> bool: """Check if system is running""" return self.running @@ -252,31 +241,20 @@ class USDAVisionSystem: def main(): """Main entry point for the application""" import argparse - + parser = argparse.ArgumentParser(description="USDA Vision Camera System") - parser.add_argument( - "--config", - 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 - ) - + parser.add_argument("--config", 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() - + # Create and run system system = USDAVisionSystem(args.config) - + # Override log level if specified if args.log_level: logging.getLogger().setLevel(getattr(logging, args.log_level)) - + try: system.run() except Exception as e: diff --git a/usda_vision_system/recording/__init__.py b/usda_vision_system/recording/__init__.py new file mode 100644 index 0000000..fee9c42 --- /dev/null +++ b/usda_vision_system/recording/__init__.py @@ -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"] diff --git a/usda_vision_system/recording/auto_manager.py b/usda_vision_system/recording/auto_manager.py new file mode 100644 index 0000000..b0bb1ea --- /dev/null +++ b/usda_vision_system/recording/auto_manager.py @@ -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 + ] + }