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:
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user