Chore: rename api->camera-management-api and web->management-dashboard-web-app; update compose, ignore, README references
This commit is contained in:
@@ -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"]
|
||||
@@ -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]}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user