Chore: rename api->camera-management-api and web->management-dashboard-web-app; update compose, ignore, README references

This commit is contained in:
Alireza Vaezi
2025-08-07 22:07:25 -04:00
parent 28dab3a366
commit fc2da16728
281 changed files with 19 additions and 19 deletions

View 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"]

View File

@@ -0,0 +1,345 @@
"""
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_status(camera_config.name)
if camera_info:
camera_info.auto_recording_enabled = True
self.logger.info(f"Auto-recording enabled for camera {camera_config.name}")
else:
# Create camera info if it doesn't exist
self.state_manager.update_camera_status(camera_config.name, "unknown")
camera_info = self.state_manager.get_camera_status(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:
self.logger.warning(f"Invalid event data - machine_name: {machine_name}, state: {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:
self.logger.debug(f"Skipping camera {camera_config.name} - not enabled or auto recording disabled")
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_status(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()
else:
# Create camera info if it doesn't exist
self.state_manager.update_camera_status(camera_name, "unknown")
camera_info = self.state_manager.get_camera_status(camera_name)
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_status(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 using its default configuration"""
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}.{camera_config.video_format}"
# Use camera manager to start recording with the camera's default configuration
# Pass the camera's configured settings from config.json
success = self.camera_manager.manual_start_recording(camera_name=camera_name, filename=filename, exposure_ms=camera_config.exposure_ms, gain=camera_config.gain, fps=camera_config.target_fps)
if success:
self.logger.info(f"Successfully started auto-recording for camera {camera_name}: {filename}")
self.logger.info(f"Using camera settings - Exposure: {camera_config.exposure_ms}ms, Gain: {camera_config.gain}, FPS: {camera_config.target_fps}")
# Update status
camera_info = self.state_manager.get_camera_status(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_status(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_status(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]}

View File

@@ -0,0 +1,373 @@
#!/usr/bin/env python3
"""
Standalone Auto-Recording System for USDA Vision Cameras
This is a simplified, reliable auto-recording system that:
1. Monitors MQTT messages directly
2. Starts/stops camera recordings based on machine state
3. Works independently of the main system
4. Is easy to debug and maintain
Usage:
sudo python -m usda_vision_system.recording.standalone_auto_recorder
"""
import json
import logging
import signal
import sys
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional
import paho.mqtt.client as mqtt
# Add the project root to the path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from usda_vision_system.core.config import Config
from usda_vision_system.camera.recorder import CameraRecorder
from usda_vision_system.core.state_manager import StateManager
from usda_vision_system.core.events import EventSystem
class StandaloneAutoRecorder:
"""Standalone auto-recording system that monitors MQTT and controls cameras directly"""
def __init__(self, config_path: str = "config.json", config: Optional[Config] = None):
# Load configuration
if config:
self.config = config
else:
self.config = Config(config_path)
# Setup logging (only if not already configured)
if not logging.getLogger().handlers:
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[logging.FileHandler("standalone_auto_recorder.log"), logging.StreamHandler()])
self.logger = logging.getLogger(__name__)
# Initialize components
self.state_manager = StateManager()
self.event_system = EventSystem()
# MQTT client
self.mqtt_client: Optional[mqtt.Client] = None
# Camera recorders
self.camera_recorders: Dict[str, CameraRecorder] = {}
self.active_recordings: Dict[str, str] = {} # camera_name -> filename
# Machine to camera mapping
self.machine_camera_map = self._build_machine_camera_map()
# Threading
self.running = False
self._stop_event = threading.Event()
self.logger.info("Standalone Auto-Recorder initialized")
self.logger.info(f"Machine-Camera mapping: {self.machine_camera_map}")
def _build_machine_camera_map(self) -> Dict[str, str]:
"""Build mapping from machine topics to camera names"""
mapping = {}
for camera_config in self.config.cameras:
if camera_config.enabled and camera_config.auto_start_recording_enabled:
machine_name = camera_config.machine_topic
if machine_name:
mapping[machine_name] = camera_config.name
self.logger.info(f"Auto-recording enabled: {machine_name} -> {camera_config.name}")
return mapping
def _setup_mqtt(self) -> bool:
"""Setup MQTT client"""
try:
self.mqtt_client = mqtt.Client()
self.mqtt_client.on_connect = self._on_mqtt_connect
self.mqtt_client.on_message = self._on_mqtt_message
self.mqtt_client.on_disconnect = self._on_mqtt_disconnect
# Connect to MQTT broker
self.logger.info(f"Connecting to MQTT broker at {self.config.mqtt.broker_host}:{self.config.mqtt.broker_port}")
self.mqtt_client.connect(self.config.mqtt.broker_host, self.config.mqtt.broker_port, 60)
# Start MQTT loop in background
self.mqtt_client.loop_start()
return True
except Exception as e:
self.logger.error(f"Failed to setup MQTT: {e}")
return False
def _on_mqtt_connect(self, client, userdata, flags, rc):
"""MQTT connection callback"""
if rc == 0:
self.logger.info("Connected to MQTT broker")
# Subscribe to machine state topics
for machine_name in self.machine_camera_map.keys():
if hasattr(self.config.mqtt, "topics") and self.config.mqtt.topics:
topic = self.config.mqtt.topics.get(machine_name)
if topic:
client.subscribe(topic)
self.logger.info(f"Subscribed to: {topic}")
else:
self.logger.warning(f"No MQTT topic configured for machine: {machine_name}")
else:
# Fallback to default topic format
topic = f"vision/{machine_name}/state"
client.subscribe(topic)
self.logger.info(f"Subscribed to: {topic} (default format)")
else:
self.logger.error(f"Failed to connect to MQTT broker: {rc}")
def _on_mqtt_disconnect(self, client, userdata, rc):
"""MQTT disconnection callback"""
self.logger.warning(f"Disconnected from MQTT broker: {rc}")
def _on_mqtt_message(self, client, userdata, msg):
"""MQTT message callback"""
try:
topic = msg.topic
payload = msg.payload.decode("utf-8").strip().lower()
# Extract machine name from topic (vision/{machine_name}/state)
topic_parts = topic.split("/")
if len(topic_parts) >= 3 and topic_parts[0] == "vision" and topic_parts[2] == "state":
machine_name = topic_parts[1]
self.logger.info(f"MQTT: {machine_name} -> {payload}")
# Handle state change
self._handle_machine_state_change(machine_name, payload)
except Exception as e:
self.logger.error(f"Error processing MQTT message: {e}")
def _handle_machine_state_change(self, machine_name: str, state: str):
"""Handle machine state change"""
try:
# Check if we have a camera for this machine
camera_name = self.machine_camera_map.get(machine_name)
if not camera_name:
return
self.logger.info(f"Handling state change: {machine_name} ({camera_name}) -> {state}")
if state == "on":
self._start_recording(camera_name, machine_name)
elif state == "off":
self._stop_recording(camera_name, machine_name)
except Exception as e:
self.logger.error(f"Error handling machine state change: {e}")
def _start_recording(self, camera_name: str, machine_name: str):
"""Start recording for a camera"""
try:
# Check if already recording
if camera_name in self.active_recordings:
self.logger.warning(f"Camera {camera_name} is already recording")
return
# Get or create camera recorder
recorder = self._get_camera_recorder(camera_name)
if not recorder:
self.logger.error(f"Failed to get recorder for camera {camera_name}")
return
# Generate filename with timestamp and machine info
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
camera_config = self.config.get_camera_by_name(camera_name)
video_format = camera_config.video_format if camera_config else "mp4"
filename = f"{camera_name}_auto_{machine_name}_{timestamp}.{video_format}"
# Start recording
success = recorder.start_recording(filename)
if success:
self.active_recordings[camera_name] = filename
self.logger.info(f"✅ Started recording: {camera_name} -> {filename}")
else:
self.logger.error(f"❌ Failed to start recording for camera {camera_name}")
except Exception as e:
self.logger.error(f"Error starting recording for {camera_name}: {e}")
def _stop_recording(self, camera_name: str, machine_name: str):
"""Stop recording for a camera"""
try:
# Check if recording
if camera_name not in self.active_recordings:
self.logger.warning(f"Camera {camera_name} is not recording")
return
# Get recorder
recorder = self._get_camera_recorder(camera_name)
if not recorder:
self.logger.error(f"Failed to get recorder for camera {camera_name}")
return
# Stop recording
filename = self.active_recordings.pop(camera_name)
success = recorder.stop_recording()
if success:
self.logger.info(f"✅ Stopped recording: {camera_name} -> {filename}")
else:
self.logger.error(f"❌ Failed to stop recording for camera {camera_name}")
except Exception as e:
self.logger.error(f"Error stopping recording for {camera_name}: {e}")
def _get_camera_recorder(self, camera_name: str) -> Optional[CameraRecorder]:
"""Get or create camera recorder"""
try:
# Return existing recorder
if camera_name in self.camera_recorders:
return self.camera_recorders[camera_name]
# Find camera config
camera_config = None
for config in self.config.cameras:
if config.name == camera_name:
camera_config = config
break
if not camera_config:
self.logger.error(f"No configuration found for camera {camera_name}")
return None
# Find device info (simplified camera discovery)
device_info = self._find_camera_device(camera_name)
if not device_info:
self.logger.error(f"No device found for camera {camera_name}")
return None
# Create recorder
recorder = CameraRecorder(camera_config=camera_config, device_info=device_info, state_manager=self.state_manager, event_system=self.event_system)
self.camera_recorders[camera_name] = recorder
self.logger.info(f"Created recorder for camera {camera_name}")
return recorder
except Exception as e:
self.logger.error(f"Error creating recorder for {camera_name}: {e}")
return None
def _find_camera_device(self, camera_name: str):
"""Simplified camera device discovery"""
try:
# Import camera SDK
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "camera_sdk"))
import mvsdk
# Initialize SDK
mvsdk.CameraSdkInit(1)
# Enumerate cameras
device_list = mvsdk.CameraEnumerateDevice()
# For now, map by index (camera1 = index 0, camera2 = index 1)
camera_index = int(camera_name.replace("camera", "")) - 1
if 0 <= camera_index < len(device_list):
return device_list[camera_index]
else:
self.logger.error(f"Camera index {camera_index} not found (total: {len(device_list)})")
return None
except Exception as e:
self.logger.error(f"Error finding camera device: {e}")
return None
def start(self) -> bool:
"""Start the standalone auto-recorder"""
try:
self.logger.info("Starting Standalone Auto-Recorder...")
# Setup MQTT
if not self._setup_mqtt():
return False
# Wait for MQTT connection
time.sleep(2)
self.running = True
self.logger.info("✅ Standalone Auto-Recorder started successfully")
return True
except Exception as e:
self.logger.error(f"Failed to start auto-recorder: {e}")
return False
def stop(self) -> bool:
"""Stop the standalone auto-recorder"""
try:
self.logger.info("Stopping Standalone Auto-Recorder...")
self.running = False
self._stop_event.set()
# Stop all active recordings
for camera_name in list(self.active_recordings.keys()):
self._stop_recording(camera_name, "system_shutdown")
# Cleanup camera recorders
for recorder in self.camera_recorders.values():
try:
recorder.cleanup()
except:
pass
# Stop MQTT
if self.mqtt_client:
self.mqtt_client.loop_stop()
self.mqtt_client.disconnect()
self.logger.info("✅ Standalone Auto-Recorder stopped")
return True
except Exception as e:
self.logger.error(f"Error stopping auto-recorder: {e}")
return False
def run(self):
"""Run the auto-recorder (blocking)"""
if not self.start():
return False
try:
# Setup signal handlers
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
self.logger.info("Auto-recorder running... Press Ctrl+C to stop")
# Main loop
while self.running and not self._stop_event.is_set():
time.sleep(1)
except KeyboardInterrupt:
self.logger.info("Received keyboard interrupt")
finally:
self.stop()
def _signal_handler(self, signum, frame):
"""Handle shutdown signals"""
self.logger.info(f"Received signal {signum}, shutting down...")
self.running = False
self._stop_event.set()
def main():
"""Main entry point"""
recorder = StandaloneAutoRecorder()
recorder.run()
if __name__ == "__main__":
main()