feat: Add MQTT publisher and tester scripts for USDA Vision Camera System

- Implemented mqtt_publisher_test.py for manual MQTT message publishing
- Created mqtt_test.py to test MQTT message reception and display statistics
- Developed test_api_changes.py to verify API changes for camera settings and filename handling
- Added test_camera_recovery_api.py for testing camera recovery API endpoints
- Introduced test_max_fps.py to demonstrate maximum FPS capture functionality
- Implemented test_mqtt_events_api.py to test MQTT events API endpoint
- Created test_mqtt_logging.py for enhanced MQTT logging and API endpoint testing
- Added sdk_config.py for SDK initialization and configuration with error suppression
This commit is contained in:
Alireza Vaezi
2025-07-28 16:30:14 -04:00
parent e2acebc056
commit 9cb043ef5f
40 changed files with 4485 additions and 838 deletions

View File

@@ -7,7 +7,7 @@ This module provides MQTT connectivity and message handling for machine state up
import threading
import time
import logging
from typing import Dict, Optional, Callable, List
from typing import Dict, Optional, Any
import paho.mqtt.client as mqtt
from ..core.config import Config, MQTTConfig
@@ -18,207 +18,219 @@ from .handlers import MQTTMessageHandler
class MQTTClient:
"""MQTT client for receiving machine state updates"""
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem):
self.config = config
self.mqtt_config = config.mqtt
self.state_manager = state_manager
self.event_system = event_system
self.logger = logging.getLogger(__name__)
# MQTT client
self.client: Optional[mqtt.Client] = None
self.connected = False
self.running = False
# Threading
self._thread: Optional[threading.Thread] = None
self._stop_event = threading.Event()
# Message handler
self.message_handler = MQTTMessageHandler(state_manager, event_system)
# Connection retry settings
self.reconnect_delay = 5 # seconds
self.max_reconnect_attempts = 10
# Topic mapping (topic -> machine_name)
self.topic_to_machine = {
topic: machine_name
for machine_name, topic in self.mqtt_config.topics.items()
}
self.topic_to_machine = {topic: machine_name for machine_name, topic in self.mqtt_config.topics.items()}
# Status tracking
self.start_time = None
self.message_count = 0
self.error_count = 0
self.last_message_time = None
def start(self) -> bool:
"""Start the MQTT client in a separate thread"""
if self.running:
self.logger.warning("MQTT client is already running")
return True
self.logger.info("Starting MQTT client...")
self.running = True
self._stop_event.clear()
self.start_time = time.time()
# Start in separate thread
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
# Wait a moment to see if connection succeeds
time.sleep(2)
return self.connected
def stop(self) -> None:
"""Stop the MQTT client"""
if not self.running:
return
self.logger.info("Stopping MQTT client...")
self.running = False
self._stop_event.set()
if self.client and self.connected:
self.client.disconnect()
if self._thread and self._thread.is_alive():
self._thread.join(timeout=5)
self.logger.info("MQTT client stopped")
def _run_loop(self) -> None:
"""Main MQTT client loop"""
reconnect_attempts = 0
while self.running and not self._stop_event.is_set():
try:
if not self.connected:
if self._connect():
reconnect_attempts = 0
self._subscribe_to_topics()
else:
reconnect_attempts += 1
if reconnect_attempts >= self.max_reconnect_attempts:
self.logger.error(f"Max reconnection attempts ({self.max_reconnect_attempts}) reached")
break
self.logger.warning(f"Reconnection attempt {reconnect_attempts}/{self.max_reconnect_attempts} in {self.reconnect_delay}s")
if self._stop_event.wait(self.reconnect_delay):
break
continue
# Process MQTT messages
if self.client:
self.client.loop(timeout=1.0)
# Small delay to prevent busy waiting
if self._stop_event.wait(0.1):
break
except Exception as e:
self.logger.error(f"Error in MQTT loop: {e}")
self.connected = False
if self._stop_event.wait(self.reconnect_delay):
break
self.running = False
self.logger.info("MQTT client loop ended")
def _connect(self) -> bool:
"""Connect to MQTT broker"""
try:
# Create new client instance
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
# Set callbacks
self.client.on_connect = self._on_connect
self.client.on_disconnect = self._on_disconnect
self.client.on_message = self._on_message
# Set authentication if provided
if self.mqtt_config.username and self.mqtt_config.password:
self.client.username_pw_set(
self.mqtt_config.username,
self.mqtt_config.password
)
self.client.username_pw_set(self.mqtt_config.username, self.mqtt_config.password)
# Connect to broker
self.logger.info(f"Connecting to MQTT broker at {self.mqtt_config.broker_host}:{self.mqtt_config.broker_port}")
self.client.connect(
self.mqtt_config.broker_host,
self.mqtt_config.broker_port,
60
)
self.client.connect(self.mqtt_config.broker_host, self.mqtt_config.broker_port, 60)
return True
except Exception as e:
self.logger.error(f"Failed to connect to MQTT broker: {e}")
return False
def _subscribe_to_topics(self) -> None:
"""Subscribe to all configured topics"""
if not self.client or not self.connected:
return
for machine_name, topic in self.mqtt_config.topics.items():
try:
result, mid = self.client.subscribe(topic)
if result == mqtt.MQTT_ERR_SUCCESS:
self.logger.info(f"Subscribed to topic: {topic} (machine: {machine_name})")
self.logger.info(f"📋 MQTT SUBSCRIBED: {topic} (machine: {machine_name})")
print(f"📋 MQTT SUBSCRIBED: {machine_name}{topic}")
else:
self.logger.error(f"Failed to subscribe to topic: {topic}")
self.logger.error(f"❌ MQTT SUBSCRIPTION FAILED: {topic}")
print(f"❌ MQTT SUBSCRIPTION FAILED: {topic}")
except Exception as e:
self.logger.error(f"Error subscribing to topic {topic}: {e}")
def _on_connect(self, client, userdata, flags, rc) -> None:
"""Callback for when the client connects to the broker"""
if rc == 0:
self.connected = True
self.state_manager.set_mqtt_connected(True)
self.event_system.publish(EventType.MQTT_CONNECTED, "mqtt_client")
self.logger.info("Successfully connected to MQTT broker")
self.logger.info("🔗 MQTT CONNECTED to broker successfully")
print(f"🔗 MQTT CONNECTED: {self.mqtt_config.broker_host}:{self.mqtt_config.broker_port}")
# Subscribe to topics immediately after connection
self._subscribe_to_topics()
else:
self.connected = False
self.logger.error(f"Failed to connect to MQTT broker, return code {rc}")
self.logger.error(f"❌ MQTT CONNECTION FAILED with return code {rc}")
print(f"❌ MQTT CONNECTION FAILED: {self.mqtt_config.broker_host}:{self.mqtt_config.broker_port} (code: {rc})")
def _on_disconnect(self, client, userdata, rc) -> None:
"""Callback for when the client disconnects from the broker"""
self.connected = False
self.state_manager.set_mqtt_connected(False)
self.event_system.publish(EventType.MQTT_DISCONNECTED, "mqtt_client")
if rc != 0:
self.logger.warning(f"Unexpected MQTT disconnection (rc: {rc})")
self.logger.warning(f"⚠️ MQTT DISCONNECTED unexpectedly (rc: {rc})")
print(f"⚠️ MQTT DISCONNECTED: Unexpected disconnection (code: {rc})")
else:
self.logger.info("MQTT client disconnected")
self.logger.info("🔌 MQTT DISCONNECTED gracefully")
print("🔌 MQTT DISCONNECTED: Graceful disconnection")
def _on_message(self, client, userdata, msg) -> None:
"""Callback for when a message is received"""
try:
topic = msg.topic
payload = msg.payload.decode('utf-8').strip()
payload = msg.payload.decode("utf-8").strip()
self.logger.debug(f"MQTT message received - Topic: {topic}, Payload: {payload}")
# Update MQTT activity
# Update MQTT activity and tracking
self.state_manager.update_mqtt_activity()
self.message_count += 1
self.last_message_time = time.time()
# Get machine name from topic
machine_name = self.topic_to_machine.get(topic)
if not machine_name:
self.logger.warning(f"Unknown topic: {topic}")
self.logger.warning(f"❓ MQTT UNKNOWN TOPIC: {topic}")
print(f"❓ MQTT UNKNOWN TOPIC: {topic}")
return
# Show MQTT message on console
print(f"📡 MQTT MESSAGE: {machine_name}{payload}")
# Handle the message
self.message_handler.handle_message(machine_name, topic, payload)
except Exception as e:
self.error_count += 1
self.logger.error(f"Error processing MQTT message: {e}")
def publish_message(self, topic: str, payload: str, qos: int = 0, retain: bool = False) -> bool:
"""Publish a message to MQTT broker"""
if not self.client or not self.connected:
self.logger.warning("Cannot publish: MQTT client not connected")
return False
try:
result = self.client.publish(topic, payload, qos, retain)
if result.rc == mqtt.MQTT_ERR_SUCCESS:
@@ -230,22 +242,26 @@ class MQTTClient:
except Exception as e:
self.logger.error(f"Error publishing message: {e}")
return False
def get_status(self) -> Dict[str, any]:
def get_status(self) -> Dict[str, Any]:
"""Get MQTT client status"""
return {
"connected": self.connected,
"running": self.running,
"broker_host": self.mqtt_config.broker_host,
"broker_port": self.mqtt_config.broker_port,
"subscribed_topics": list(self.mqtt_config.topics.values()),
"topic_mappings": self.topic_to_machine
}
uptime_seconds = None
last_message_time_str = None
if self.start_time:
uptime_seconds = time.time() - self.start_time
if self.last_message_time:
from datetime import datetime
last_message_time_str = datetime.fromtimestamp(self.last_message_time).isoformat()
return {"connected": self.connected, "running": self.running, "broker_host": self.mqtt_config.broker_host, "broker_port": self.mqtt_config.broker_port, "subscribed_topics": list(self.mqtt_config.topics.values()), "topic_mappings": self.topic_to_machine, "message_count": self.message_count, "error_count": self.error_count, "last_message_time": last_message_time_str, "uptime_seconds": uptime_seconds}
def is_connected(self) -> bool:
"""Check if MQTT client is connected"""
return self.connected
def is_running(self) -> bool:
"""Check if MQTT client is running"""
return self.running

View File

@@ -14,69 +14,63 @@ from ..core.events import EventSystem, publish_machine_state_changed
class MQTTMessageHandler:
"""Handles MQTT messages and triggers appropriate system actions"""
def __init__(self, state_manager: StateManager, event_system: EventSystem):
self.state_manager = state_manager
self.event_system = event_system
self.logger = logging.getLogger(__name__)
# Message processing statistics
self.message_count = 0
self.last_message_time: Optional[datetime] = None
self.error_count = 0
def handle_message(self, machine_name: str, topic: str, payload: str) -> None:
"""Handle an incoming MQTT message"""
try:
self.message_count += 1
self.last_message_time = datetime.now()
self.logger.info(f"Processing MQTT message - Machine: {machine_name}, Topic: {topic}, Payload: {payload}")
# Normalize payload
normalized_payload = self._normalize_payload(payload)
# Update machine state
state_changed = self.state_manager.update_machine_state(
name=machine_name,
state=normalized_payload,
message=payload,
topic=topic
)
state_changed = self.state_manager.update_machine_state(name=machine_name, state=normalized_payload, message=payload, topic=topic)
# Store MQTT event in history
self.state_manager.add_mqtt_event(machine_name=machine_name, topic=topic, payload=payload, normalized_state=normalized_payload)
# Publish state change event if state actually changed
if state_changed:
publish_machine_state_changed(
machine_name=machine_name,
state=normalized_payload,
source="mqtt_handler"
)
publish_machine_state_changed(machine_name=machine_name, state=normalized_payload, source="mqtt_handler")
self.logger.info(f"Machine {machine_name} state changed to: {normalized_payload}")
# Log the message for debugging
self._log_message_details(machine_name, topic, payload, normalized_payload)
except Exception as e:
self.error_count += 1
self.logger.error(f"Error handling MQTT message for {machine_name}: {e}")
def _normalize_payload(self, payload: str) -> str:
"""Normalize payload to standard machine states"""
payload_lower = payload.lower().strip()
# Map various possible payloads to standard states
if payload_lower in ['on', 'true', '1', 'start', 'running', 'active']:
return 'on'
elif payload_lower in ['off', 'false', '0', 'stop', 'stopped', 'inactive']:
return 'off'
elif payload_lower in ['error', 'fault', 'alarm']:
return 'error'
if payload_lower in ["on", "true", "1", "start", "running", "active"]:
return "on"
elif payload_lower in ["off", "false", "0", "stop", "stopped", "inactive"]:
return "off"
elif payload_lower in ["error", "fault", "alarm"]:
return "error"
else:
# For unknown payloads, log and return as-is
self.logger.warning(f"Unknown payload format: '{payload}', treating as raw state")
return payload_lower
def _log_message_details(self, machine_name: str, topic: str, original_payload: str, normalized_payload: str) -> None:
"""Log detailed message information"""
self.logger.debug(f"MQTT Message Details:")
@@ -86,16 +80,11 @@ class MQTTMessageHandler:
self.logger.debug(f" Normalized Payload: '{normalized_payload}'")
self.logger.debug(f" Timestamp: {self.last_message_time}")
self.logger.debug(f" Total Messages Processed: {self.message_count}")
def get_statistics(self) -> Dict[str, any]:
"""Get message processing statistics"""
return {
"total_messages": self.message_count,
"error_count": self.error_count,
"last_message_time": self.last_message_time.isoformat() if self.last_message_time else None,
"success_rate": (self.message_count - self.error_count) / max(self.message_count, 1) * 100
}
return {"total_messages": self.message_count, "error_count": self.error_count, "last_message_time": self.last_message_time.isoformat() if self.last_message_time else None, "success_rate": (self.message_count - self.error_count) / max(self.message_count, 1) * 100}
def reset_statistics(self) -> None:
"""Reset message processing statistics"""
self.message_count = 0
@@ -106,47 +95,47 @@ class MQTTMessageHandler:
class MachineStateProcessor:
"""Processes machine state changes and determines actions"""
def __init__(self, state_manager: StateManager, event_system: EventSystem):
self.state_manager = state_manager
self.event_system = event_system
self.logger = logging.getLogger(__name__)
def process_state_change(self, machine_name: str, old_state: str, new_state: str) -> None:
"""Process a machine state change and determine what actions to take"""
self.logger.info(f"Processing state change for {machine_name}: {old_state} -> {new_state}")
# Handle state transitions
if old_state != 'on' and new_state == 'on':
if old_state != "on" and new_state == "on":
self._handle_machine_turned_on(machine_name)
elif old_state == 'on' and new_state != 'on':
elif old_state == "on" and new_state != "on":
self._handle_machine_turned_off(machine_name)
elif new_state == 'error':
elif new_state == "error":
self._handle_machine_error(machine_name)
def _handle_machine_turned_on(self, machine_name: str) -> None:
"""Handle machine turning on - should start recording"""
self.logger.info(f"Machine {machine_name} turned ON - should start recording")
# The actual recording start will be handled by the camera manager
# which listens to the MACHINE_STATE_CHANGED event
# We could add additional logic here, such as:
# - Checking if camera is available
# - Pre-warming camera settings
# - Sending notifications
def _handle_machine_turned_off(self, machine_name: str) -> None:
"""Handle machine turning off - should stop recording"""
self.logger.info(f"Machine {machine_name} turned OFF - should stop recording")
# The actual recording stop will be handled by the camera manager
# which listens to the MACHINE_STATE_CHANGED event
def _handle_machine_error(self, machine_name: str) -> None:
"""Handle machine error state"""
self.logger.warning(f"Machine {machine_name} in ERROR state")
# Could implement error handling logic here:
# - Stop recording if active
# - Send alerts