VWED_server/services/online_script/device_handler_service.py

1898 lines
80 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
设备处理服务模块
基于VDA5050协议的设备管理框架支持MQTT通信
消息直接透传,不进行编解码处理
"""
import asyncio
import json
import time
import socket
import threading
from datetime import datetime
from typing import Dict, Any, Optional, Callable, List, Union
from dataclasses import dataclass, asdict
from enum import Enum
from utils.logger import get_logger
from .mqtt_service import MQTTService
from .async_mqtt_service import AsyncMQTTService
from config.tf_api_config import MQTT_TOPIC_CONFIG
logger = get_logger("services.device_handler")
class DeviceType(Enum):
"""设备类型枚举"""
VEHICLE = "vehicle" # 小车
DOOR = "door" # 门
CALLER = "caller" # 呼叫器
LIFT = "lift" # 电梯
CONVEYOR = "conveyor" # 输送带
SENSOR = "sensor" # 传感器
ROBOT = "robot" # 机器人
CAMERA = "camera" # 摄像头
SCANNER = "scanner" # 扫描器
CUSTOM = "custom" # 自定义设备
class VDA5050CommandType(Enum):
"""VDA5050协议指令类型"""
ORDER = "order" # 订单指令(仅小车)
STATE = "state" # 状态查询(仅小车)
FACTSHEET = "factsheet" # 设备信息(仅小车)
INSTANT_ACTIONS = "instantActions" # 即时动作(所有设备)
CONNECTION = "connection" # 连接管理
VISUALIZATION = "visualization" # 可视化
class ProtocolType(Enum):
"""协议类型"""
VDA5050 = "vda5050" # VDA5050协议默认
TCP = "tcp" # TCP协议
class DeviceBrand(Enum):
"""设备品牌枚举"""
HUARUI = "huarui" # 华睿
SEER = "seer" # 仙工
QUICKTRON = "quicktron" # 快仓
GEEK = "geek" # 极智嘉
MUSHINY = "mushiny" # 木蚁
FLASHHOLD = "flashhold" # 闪电匣
HIKROBOT = "hikrobot" # 海康机器人
STANDARD = "standard" # 标准协议
CUSTOM = "custom" # 自定义品牌
class DeviceProtocol:
"""设备协议定义基类"""
def __init__(self, brand: DeviceBrand, device_type: DeviceType,
protocol_type: ProtocolType = ProtocolType.VDA5050):
self.brand = brand
self.device_type = device_type
self.protocol_type = protocol_type
def encode_command(self, command_type: str, command: Dict[str, Any]) -> Dict[str, Any]:
"""将VDA5050标准指令编码为设备特定格式"""
raise NotImplementedError("子类必须实现encode_command方法")
def decode_response(self, message_type: str, response: Dict[str, Any]) -> Dict[str, Any]:
"""将设备响应解码为VDA5050标准格式"""
raise NotImplementedError("子类必须实现decode_response方法")
def validate_command(self, command_type: str, command: Dict[str, Any]) -> bool:
"""验证指令格式是否正确"""
return True
def get_supported_command_types(self) -> List[str]:
"""获取支持的VDA5050指令类型列表"""
if self.device_type == DeviceType.VEHICLE:
return [
VDA5050CommandType.ORDER.value,
VDA5050CommandType.STATE.value,
VDA5050CommandType.FACTSHEET.value,
VDA5050CommandType.INSTANT_ACTIONS.value
]
else:
# 其他设备只支持即时动作
return [VDA5050CommandType.INSTANT_ACTIONS.value]
class VDA5050Protocol(DeviceProtocol):
"""VDA5050协议实现标准协议"""
def __init__(self, brand: DeviceBrand = DeviceBrand.STANDARD, device_type: DeviceType = DeviceType.VEHICLE):
super().__init__(brand, device_type, ProtocolType.VDA5050)
def encode_command(self, command_type: str, command: Dict[str, Any]) -> Dict[str, Any]:
"""VDA5050标准指令编码"""
if command_type == VDA5050CommandType.ORDER.value:
return self._encode_order(command)
elif command_type == VDA5050CommandType.INSTANT_ACTIONS.value:
return self._encode_instant_actions(command)
elif command_type == VDA5050CommandType.STATE.value:
return self._encode_state_request(command)
elif command_type == VDA5050CommandType.FACTSHEET.value:
return self._encode_factsheet_request(command)
else:
return command
def decode_response(self, message_type: str, response: Dict[str, Any]) -> Dict[str, Any]:
"""VDA5050标准响应解码"""
if message_type == VDA5050CommandType.STATE.value:
return self._decode_state_response(response)
elif message_type == VDA5050CommandType.FACTSHEET.value:
return self._decode_factsheet_response(response)
else:
return response
def _encode_order(self, command: Dict[str, Any]) -> Dict[str, Any]:
"""编码VDA5050订单指令"""
return {
"headerId": command.get("header_id", f"order_{int(time.time())}"),
"timestamp": command.get("timestamp", datetime.now().isoformat()),
"version": command.get("version", "2.0.0"),
"manufacturer": self.brand.value,
"serialNumber": command.get("serial_number", ""),
"orderId": command.get("order_id", f"order_{int(time.time())}"),
"orderUpdateId": command.get("order_update_id", 0),
"zoneSetId": command.get("zone_set_id", ""),
"nodes": command.get("nodes", []),
"edges": command.get("edges", [])
}
def _encode_instant_actions(self, command: Dict[str, Any]) -> Dict[str, Any]:
"""编码VDA5050即时动作指令"""
actions = command.get("actions", [])
# 标准化动作格式
formatted_actions = []
for action in actions:
if isinstance(action, str):
# 简单字符串转换为标准VDA5050动作格式
formatted_actions.append({
"actionType": action,
"actionId": f"{action}_{int(time.time())}",
"actionDescription": f"{self.device_type.value} {action} action",
"blockingType": "SOFT"
})
else:
formatted_actions.append(action)
return {
"headerId": command.get("header_id", f"instant_{int(time.time())}"),
"timestamp": command.get("timestamp", datetime.now().isoformat()),
"version": command.get("version", "2.0.0"),
"manufacturer": self.brand.value,
"serialNumber": command.get("serial_number", ""),
"instantActions": formatted_actions
}
def _encode_state_request(self, command: Dict[str, Any]) -> Dict[str, Any]:
"""编码VDA5050状态请求"""
return {
"requestId": command.get("request_id", f"state_{int(time.time())}"),
"timestamp": command.get("timestamp", datetime.now().isoformat())
}
def _encode_factsheet_request(self, command: Dict[str, Any]) -> Dict[str, Any]:
"""编码VDA5050设备信息请求"""
return {
"requestId": command.get("request_id", f"factsheet_{int(time.time())}"),
"timestamp": command.get("timestamp", datetime.now().isoformat())
}
def _decode_state_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
"""解码VDA5050状态响应"""
return {
"command_type": "state_response",
"order_id": response.get("orderId", ""),
"order_update_id": response.get("orderUpdateId", 0),
"last_node_id": response.get("lastNodeId", ""),
"last_node_sequence_id": response.get("lastNodeSequenceId", 0),
"driving": response.get("driving", False),
"paused": response.get("paused", False),
"new_base_request": response.get("newBaseRequest", False),
"distance_since_last_node": response.get("distanceSinceLastNode", 0.0),
"operating_mode": response.get("operatingMode", ""),
"node_states": response.get("nodeStates", []),
"edge_states": response.get("edgeStates", []),
"agv_position": response.get("agvPosition", {}),
"velocity": response.get("velocity", {}),
"loads": response.get("loads", []),
"action_states": response.get("actionStates", []),
"battery_state": response.get("batteryState", {}),
"safety_state": response.get("safetyState", {}),
"information": response.get("information", []),
"errors": response.get("errors", []),
"timestamp": response.get("timestamp", "")
}
def _decode_factsheet_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
"""解码VDA5050设备信息响应"""
return {
"command_type": "factsheet_response",
"manufacturer": response.get("manufacturer", ""),
"serial_number": response.get("serialNumber", ""),
"type_specification": response.get("typeSpecification", {}),
"physical_parameters": response.get("physicalParameters", {}),
"protocol_limits": response.get("protocolLimits", {}),
"protocol_features": response.get("protocolFeatures", {}),
"agv_geometry": response.get("agvGeometry", {}),
"load_specification": response.get("loadSpecification", {}),
"timestamp": response.get("timestamp", "")
}
class TCPProtocol(DeviceProtocol):
"""TCP协议实现VDA5050通过TCP传输"""
def __init__(self, brand: DeviceBrand = DeviceBrand.STANDARD, device_type: DeviceType = DeviceType.VEHICLE):
super().__init__(brand, device_type, ProtocolType.TCP)
self.vda_protocol = VDA5050Protocol(brand, device_type)
def encode_command(self, command_type: str, command: Dict[str, Any]) -> Dict[str, Any]:
"""TCP协议编码基于VDA5050"""
# 先通过VDA5050协议编码
vda_data = self.vda_protocol.encode_command(command_type, command)
# 添加TCP传输包装
tcp_packet = {
"protocol": "vda5050",
"transport": "tcp",
"timestamp": datetime.now().isoformat(),
"data": vda_data
}
return tcp_packet
def decode_response(self, message_type: str, response: Dict[str, Any]) -> Dict[str, Any]:
"""TCP协议解码基于VDA5050"""
# 如果是TCP包装格式提取VDA5050数据
if "data" in response and response.get("protocol") == "vda5050":
vda_data = response["data"]
return self.vda_protocol.decode_response(message_type, vda_data)
else:
# 直接作为VDA5050数据处理
return self.vda_protocol.decode_response(message_type, response)
class DeviceProtocolRegistry:
"""设备协议注册中心"""
def __init__(self):
self.protocols: Dict[str, DeviceProtocol] = {}
self._register_builtin_protocols()
def _register_builtin_protocols(self):
"""注册内置协议"""
# 默认VDA5050协议适用于所有设备
default_vda5050 = VDA5050Protocol()
self.register_protocol("vda5050", default_vda5050)
# 各品牌VDA5050协议
for brand in [DeviceBrand.HUARUI, DeviceBrand.SEER, DeviceBrand.QUICKTRON,
DeviceBrand.GEEK, DeviceBrand.MUSHINY, DeviceBrand.FLASHHOLD,
DeviceBrand.HIKROBOT]:
# 小车VDA5050协议
vehicle_protocol = VDA5050Protocol(brand, DeviceType.VEHICLE)
self.register_protocol(f"{brand.value}_vda5050", vehicle_protocol)
# TCP协议基于VDA5050
tcp_protocol = TCPProtocol(brand, DeviceType.VEHICLE)
self.register_protocol(f"{brand.value}_tcp", tcp_protocol)
# 非小车设备的VDA5050协议即时动作
for device_type in [DeviceType.DOOR, DeviceType.CALLER, DeviceType.LIFT,
DeviceType.CONVEYOR, DeviceType.SENSOR, DeviceType.CAMERA,
DeviceType.SCANNER, DeviceType.ROBOT, DeviceType.CUSTOM]:
vda_protocol = VDA5050Protocol(DeviceBrand.STANDARD, device_type)
self.register_protocol(f"{device_type.value}_vda5050", vda_protocol)
tcp_protocol = TCPProtocol(DeviceBrand.STANDARD, device_type)
self.register_protocol(f"{device_type.value}_tcp", tcp_protocol)
def register_protocol(self, protocol_key: str, protocol: DeviceProtocol):
"""注册设备协议"""
self.protocols[protocol_key] = protocol
logger.info(f"注册设备协议: {protocol_key} ({protocol.brand.value}_{protocol.device_type.value})")
def get_protocol(self, protocol_key: str) -> Optional[DeviceProtocol]:
"""获取设备协议"""
return self.protocols.get(protocol_key)
def get_protocol_by_brand_type(self, brand: Union[str, DeviceBrand],
device_type: Union[str, DeviceType]) -> Optional[DeviceProtocol]:
"""根据品牌和设备类型获取协议"""
if isinstance(brand, str):
brand = DeviceBrand(brand)
if isinstance(device_type, str):
device_type = DeviceType(device_type)
for protocol in self.protocols.values():
if protocol.brand == brand and protocol.device_type == device_type:
return protocol
return None
def list_protocols(self) -> Dict[str, Dict[str, str]]:
"""列出所有协议"""
return {
key: {
"brand": protocol.brand.value,
"device_type": protocol.device_type.value,
"supported_commands": protocol.get_supported_commands()
}
for key, protocol in self.protocols.items()
}
def register_custom_protocol(self, protocol_key: str, brand: str, device_type: str,
encode_func: Callable = None, decode_func: Callable = None):
"""注册自定义协议基于VDA5050"""
# 解析设备类型
try:
device_enum = DeviceType(device_type)
except ValueError:
device_enum = DeviceType.CUSTOM
# 解析品牌
try:
brand_enum = DeviceBrand(brand)
except ValueError:
brand_enum = DeviceBrand.CUSTOM
# 创建基于VDA5050的自定义协议
custom_protocol = VDA5050Protocol(brand_enum, device_enum)
# 如果提供了自定义编解码函数,包装协议
if encode_func or decode_func:
original_encode = custom_protocol.encode_command
original_decode = custom_protocol.decode_response
def custom_encode(command_type: str, command: Dict[str, Any]) -> Dict[str, Any]:
vda_result = original_encode(command_type, command)
if encode_func:
return encode_func(command_type, vda_result)
return vda_result
def custom_decode(message_type: str, response: Dict[str, Any]) -> Dict[str, Any]:
if decode_func:
processed_response = decode_func(message_type, response)
return original_decode(message_type, processed_response)
return original_decode(message_type, response)
custom_protocol.encode_command = custom_encode
custom_protocol.decode_response = custom_decode
self.register_protocol(protocol_key, custom_protocol)
# 全局协议注册中心
_global_protocol_registry = DeviceProtocolRegistry()
def get_protocol_registry() -> DeviceProtocolRegistry:
"""获取全局协议注册中心"""
return _global_protocol_registry
class TopicManager:
"""MQTT Topic管理器"""
def __init__(self):
self.topic_config = MQTT_TOPIC_CONFIG
self.instance_id = self.topic_config["instance_id"]
self.supported_brands = self.topic_config["supported_brands"]
self.command_directions = self.topic_config["command_directions"]
# 扁平化品牌配置便于查找
self._flat_brands = {}
for category in ["vehicle", "other"]:
if category in self.supported_brands:
self._flat_brands.update(self.supported_brands[category])
def validate_brand(self, brand_name: str) -> bool:
"""验证品牌是否支持"""
return brand_name.lower() in self._flat_brands
def get_supported_brands(self, category: str = None) -> List[str]:
"""获取支持的品牌列表
Args:
category: 设备类型分类 ("vehicle", "other", 或 None 返回所有)
Returns:
品牌名称列表
"""
if category is None:
# 返回所有品牌
return list(self._flat_brands.keys())
elif category in self.supported_brands:
# 返回指定分类的品牌
return list(self.supported_brands[category].keys())
else:
logger.warning(f"未知的设备类型分类: {category}")
return []
def get_brand_config(self, brand_name: str) -> Optional[Dict[str, Any]]:
"""获取品牌配置
Args:
brand_name: 品牌名称
Returns:
品牌配置字典如果不存在返回None
"""
return self._flat_brands.get(brand_name.lower())
def generate_listen_topics(self, device_ids: List[str], brand_name: str,
command_types: List[str] = None, device_type: DeviceType = DeviceType.VEHICLE) -> List[str]:
"""生成监听topics
Args:
device_ids: 设备ID列表
brand_name: 品牌名称
command_types: 指令类型列表如果为None则使用默认指令
device_type: 设备类型
Returns:
监听topic列表
"""
if not self.validate_brand(brand_name):
raise ValueError(f"不支持的品牌: {brand_name},支持的品牌: {self.get_supported_brands()}")
brand_config = self.get_brand_config(brand_name)
if not brand_config:
raise ValueError(f"无法获取品牌配置: {brand_name}")
brand_suffix = brand_config["brand_suffix"]
oagv_version = brand_config["oagv_version"]
if command_types is None:
if device_type == DeviceType.VEHICLE:
command_types = self.topic_config["default_command_types"]["vehicle"]
else:
command_types = self.topic_config["default_command_types"]["other"]
listen_topics = []
for command_type in command_types:
if command_type not in self.command_directions:
logger.warning(f"未知的指令类型: {command_type}")
continue
direction_config = self.command_directions[command_type]
listen_queue = direction_config["listen"]
if listen_queue == "oagv":
# oagv格式: oagv/v2/{instanceId}_BRAND/{deviceId}/order
for device_id in device_ids:
topic = f"oagv/{oagv_version}/{self.instance_id}{brand_suffix}/{device_id}/{command_type}"
listen_topics.append(topic)
elif listen_queue == "uagv":
# uagv格式: uagv/v2.0.0/BRAND/{deviceId}/state
uagv_version = brand_config["uagv_version"]
brand_name_only = brand_suffix[1:] # 去掉前缀下划线
for device_id in device_ids:
topic = f"uagv/{uagv_version}/{brand_name_only}/{device_id}/{command_type}"
listen_topics.append(topic)
return listen_topics
def generate_forward_topics(self, device_ids: List[str], brand_name: str,
command_types: List[str] = None, device_type: DeviceType = DeviceType.VEHICLE) -> List[str]:
"""生成转发topics
Args:
device_ids: 设备ID列表
brand_name: 品牌名称
command_types: 指令类型列表如果为None则使用默认指令
device_type: 设备类型
Returns:
转发topic列表
"""
if not self.validate_brand(brand_name):
raise ValueError(f"不支持的品牌: {brand_name},支持的品牌: {self.get_supported_brands()}")
brand_config = self.get_brand_config(brand_name)
if not brand_config:
raise ValueError(f"无法获取品牌配置: {brand_name}")
brand_suffix = brand_config["brand_suffix"]
uagv_version = brand_config["uagv_version"]
oagv_version = brand_config["oagv_version"]
if command_types is None:
if device_type == DeviceType.VEHICLE:
command_types = self.topic_config["default_command_types"]["vehicle"]
else:
command_types = self.topic_config["default_command_types"]["other"]
forward_topics = []
for command_type in command_types:
if command_type not in self.command_directions:
logger.warning(f"未知的指令类型: {command_type}")
continue
direction_config = self.command_directions[command_type]
forward_queue = direction_config["forward"]
if forward_queue == "uagv":
# uagv格式: uagv/v2.0.0/BRAND/{deviceId}/order
brand_name_only = brand_suffix[1:] # 去掉前缀下划线
for device_id in device_ids:
topic = f"uagv/{uagv_version}/{brand_name_only}/{device_id}/{command_type}"
forward_topics.append(topic)
elif forward_queue == "oagv":
# oagv格式: oagv/v2/{instanceId}_BRAND/{deviceId}/state
for device_id in device_ids:
topic = f"oagv/{oagv_version}/{self.instance_id}{brand_suffix}/{device_id}/{command_type}"
forward_topics.append(topic)
return forward_topics
def extract_vehicle_id_from_topic(self, topic: str) -> Optional[str]:
"""从topic中提取车辆ID"""
parts = topic.split('/')
if len(parts) >= 4:
if parts[0] == "oagv":
# oagv格式: oagv/v2/{instanceId}_BRAND/{vehicleId}/command
return parts[3] if parts[3] != "+" else None
elif parts[0] == "uagv":
# uagv格式: uagv/v2.0.0/BRAND/{vehicleId}/command
return parts[3]
return None
class MessagePriority(Enum):
"""消息优先级"""
LOW = 1
NORMAL = 2
HIGH = 3
CRITICAL = 4
@dataclass
class DeviceMessage:
"""设备消息数据结构"""
device_id: str
device_type: DeviceType
topic: str
payload: Dict[str, Any]
timestamp: float
priority: MessagePriority = MessagePriority.NORMAL
source_topic: Optional[str] = None
target_topics: Optional[List[str]] = None
processed: bool = False
retry_count: int = 0
max_retries: int = 3
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
data = asdict(self)
data['device_type'] = self.device_type.value
data['priority'] = self.priority.value
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'DeviceMessage':
"""从字典创建消息对象"""
data['device_type'] = DeviceType(data['device_type'])
data['priority'] = MessagePriority(data['priority'])
return cls(**data)
@dataclass
class DeviceHandlerConfig:
"""设备处理器配置"""
device_id: str
device_type: DeviceType
listen_topics: List[str]
forward_topics: List[str]
handler_function: str
script_id: str
device_brand: Optional[DeviceBrand] = DeviceBrand.STANDARD # 设备品牌(默认标准)
protocol_type: ProtocolType = ProtocolType.VDA5050 # 协议类型默认VDA5050
# auto_encode: bool = True # 编解码功能已移除,消息直接透传
enabled: bool = True
auto_retry: bool = True
timeout_seconds: int = 30
batch_size: int = 1
description: str = ""
metadata: Dict[str, Any] = None
def __post_init__(self):
if self.metadata is None:
self.metadata = {}
# 已移除: protocol_key 方法,不再需要协议标识
class DeviceHandlerRegistry:
"""设备处理器注册中心"""
def __init__(self, use_async_mqtt: bool =True):
self.device_handlers: Dict[str, DeviceHandlerConfig] = {}
self.message_processors: Dict[str, Callable] = {}
self.topic_mappings: Dict[str, List[str]] = {} # topic -> device_ids
self.active_devices: Dict[str, Dict[str, Any]] = {}
self.message_queue: asyncio.Queue = asyncio.Queue()
self.processing_stats: Dict[str, Dict[str, Any]] = {}
# 选择MQTT服务类型
self.use_async_mqtt = use_async_mqtt
if use_async_mqtt:
self.mqtt_service = AsyncMQTTService()
logger.info("使用异步MQTT服务")
else:
self.mqtt_service = MQTTService()
logger.info("使用同步MQTT服务")
# self.protocol_registry = get_protocol_registry() # 已移除协议注册中心
# Topic管理器
self.topic_manager = TopicManager()
# 系统级监听状态管理
self.system_listen_topics: Dict[str, str] = {} # topic -> handler_id
self.custom_device_handlers: Dict[str, str] = {} # device_id -> handler_id
self.is_system_listening: bool = False
# 设备消息队列和处理协程 - 按设备ID分流
self.device_queues: Dict[str, asyncio.Queue] = {} # device_id -> queue
self.device_workers: Dict[str, asyncio.Task] = {} # device_id -> worker_task
self.max_queue_size_per_device = 1000 # 每个设备的队列容量(可配置)
self.device_queue_stats: Dict[str, Dict[str, Any]] = {} # 队列统计信息
def set_max_queue_size(self, max_size: int):
"""设置每个设备的最大队列容量
Args:
max_size: 最大队列容量建议500-2000
"""
if max_size < 10:
logger.warning("队列大小太小建议至少设置为10")
max_size = 10
elif max_size > 10000:
logger.warning("队列大小太大建议不超过10000")
max_size = 10000
self.max_queue_size_per_device = max_size
logger.info(f"设备队列最大容量已设置为: {max_size}")
def register_device_handler(self, config: DeviceHandlerConfig, processor: Callable):
"""注册并运行设备处理器"""
device_id = config.device_id
# 标记为自定义设备处理器(从系统监听中排除)
self.custom_device_handlers[device_id] = config.script_id
# 注册设备配置
self.device_handlers[device_id] = config
self.message_processors[device_id] = processor
# 建立topic映射
for topic in config.listen_topics:
if topic not in self.topic_mappings:
self.topic_mappings[topic] = []
if device_id not in self.topic_mappings[topic]:
self.topic_mappings[topic].append(device_id)
# 初始化统计信息
self.processing_stats[device_id] = {
"total_messages": 0,
"success_count": 0,
"error_count": 0,
"last_processed": None,
"average_processing_time_ms": None,
"registered_at": datetime.now().isoformat(),
"status": "running"
}
# 标记设备为活跃
self.active_devices[device_id] = {
"config": config,
"status": "running",
"last_heartbeat": time.time(),
"message_count": 0
}
brand_info = f" 品牌: {config.device_brand.value}" if config.device_brand else ""
# protocol_info = f" 协议: {config.protocol_key}" if config.protocol_key else "" # 已移除协议信息
logger.info(f"注册并运行设备处理器: {device_id} ({config.device_type.value}{brand_info})")
logger.info(f"监听topics: {config.listen_topics}")
logger.info(f"转发topics: {config.forward_topics}")
logger.info("消息透传模式: 已启用")
logger.info(f"设备 {device_id} 已从系统级监听中移除,由自定义处理器接管")
# 如果MQTT服务已连接立即订阅topics
if self.mqtt_service.connected:
# 过滤掉已经被系统级监听覆盖的topics避免重复订阅
topics_to_subscribe = self._filter_duplicate_topics(config.listen_topics, config.device_brand.value)
if topics_to_subscribe:
if self.use_async_mqtt:
# 异步MQTT需要在异步上下文中订阅
asyncio.create_task(self._subscribe_topics_async(topics_to_subscribe))
else:
self._subscribe_topics(topics_to_subscribe)
logger.info(f"设备 {device_id} 订阅了 {len(topics_to_subscribe)} 个新topics")
else:
logger.info(f"设备 {device_id} 的所有topics已被系统级监听覆盖无需重复订阅")
def unregister_device_handler(self, device_id: str):
"""停止并注销设备处理器"""
if device_id in self.device_handlers:
config = self.device_handlers[device_id]
# 只在MQTT连接时才尝试取消订阅topics_unsubscribe_topics已内置连接检查
self._unsubscribe_topics(config.listen_topics, device_id)
# 清理topic映射
for topic in config.listen_topics:
if topic in self.topic_mappings:
if device_id in self.topic_mappings[topic]:
self.topic_mappings[topic].remove(device_id)
if not self.topic_mappings[topic]:
del self.topic_mappings[topic]
# 从自定义设备列表中移除(恢复系统级监听)
if device_id in self.custom_device_handlers:
del self.custom_device_handlers[device_id]
logger.info(f"设备 {device_id} 已恢复系统级监听")
# 清理设备队列和worker协程
if device_id in self.device_workers:
worker_task = self.device_workers[device_id]
if not worker_task.done():
worker_task.cancel()
del self.device_workers[device_id]
logger.info(f"已取消设备 {device_id} 的worker协程")
if device_id in self.device_queues:
del self.device_queues[device_id]
logger.info(f"已清理设备 {device_id} 的消息队列")
if device_id in self.device_queue_stats:
del self.device_queue_stats[device_id]
# 清理注册信息
del self.device_handlers[device_id]
del self.message_processors[device_id]
if device_id in self.active_devices:
del self.active_devices[device_id]
if device_id in self.processing_stats:
del self.processing_stats[device_id]
logger.info(f"停止并注销设备处理器: {device_id}")
def _subscribe_topics(self, topics: List[str]):
"""订阅MQTT topics同步版本"""
for topic in topics:
# 检查是否已经为该topic添加了我们的消息处理器
topic_handlers = getattr(self.mqtt_service, 'message_handlers', {}).get(topic, [])
sync_handler = self._handle_mqtt_message
# 只有当处理器不在列表中时才添加
if sync_handler not in topic_handlers:
self.mqtt_service.add_message_handler(topic, sync_handler)
self.mqtt_service.subscribe(topic)
async def _subscribe_topics_async(self, topics: List[str]):
"""订阅MQTT topics异步版本"""
for topic in topics:
# 检查是否已经为该topic添加了我们的消息处理器
topic_handlers = getattr(self.mqtt_service, 'message_handlers', {}).get(topic, [])
async_handler = self._handle_mqtt_message_async
# 只有当处理器不在列表中时才添加
if async_handler not in topic_handlers:
self.mqtt_service.add_message_handler(topic, async_handler)
await self.mqtt_service.subscribe(topic)
def _filter_duplicate_topics(self, topics: List[str], brand_name: str) -> List[str]:
"""过滤掉已经被系统级监听覆盖的topics
Args:
topics: 要检查的topic列表
brand_name: 设备品牌名称
Returns:
需要订阅的topic列表排除已被系统级监听覆盖的
"""
if not self.is_system_listening:
# 如果系统级监听未启动则所有topics都需要订阅
return topics
filtered_topics = []
for topic in topics:
# 检查是否已被系统级监听覆盖
is_covered_by_system = False
# 检查当前topic是否被任何系统级通配符topic覆盖
for system_topic in self.system_listen_topics.keys():
if self._is_topic_covered_by_wildcard(topic, system_topic, brand_name):
is_covered_by_system = True
logger.debug(f"Topic {topic} 已被系统级监听 {system_topic} 覆盖")
break
if not is_covered_by_system:
filtered_topics.append(topic)
logger.debug(f"Topic {topic} 需要单独订阅")
return filtered_topics
def _is_topic_covered_by_wildcard(self, specific_topic: str, wildcard_topic: str, brand_name: str) -> bool:
"""检查特定topic是否被通配符topic覆盖
Args:
specific_topic: 具体的topic"oagv/v2/asbm2_IRAYPLE/AGV001/order"
wildcard_topic: 通配符topic"oagv/v2/asbm2_IRAYPLE/+/order"
brand_name: 品牌名称
Returns:
是否被覆盖
"""
specific_parts = specific_topic.split('/')
wildcard_parts = wildcard_topic.split('/')
# 长度必须相同
if len(specific_parts) != len(wildcard_parts):
return False
# 检查每个部分是否匹配
for i, (specific_part, wildcard_part) in enumerate(zip(specific_parts, wildcard_parts)):
if wildcard_part == '+':
# 通配符匹配任意值
continue
elif specific_part != wildcard_part:
# 非通配符部分必须完全匹配
return False
# 额外检查:确保品牌匹配
# 从topic中提取品牌信息进行验证
if len(specific_parts) >= 3:
topic_brand_instance = specific_parts[2]
# 检查品牌是否匹配(使用新的品牌配置获取方法)
brand_config = self.topic_manager.get_brand_config(brand_name)
if brand_config:
brand_suffix = brand_config["brand_suffix"]
# 对于oagv格式检查实例+品牌后缀是否匹配
if specific_parts[0] == "oagv":
expected_instance_brand = f"{self.topic_manager.instance_id}{brand_suffix}"
if topic_brand_instance == expected_instance_brand:
return True
# 对于uagv格式检查品牌名是否匹配
elif specific_parts[0] == "uagv":
brand_name_only = brand_suffix[1:] if brand_suffix.startswith('_') else brand_suffix
if topic_brand_instance == brand_name_only:
return True
return False
def _unsubscribe_topics(self, topics: List[str], device_id: str):
"""取消订阅MQTT topics"""
# 检查MQTT连接状态避免在断开连接时产生警告
if not self.mqtt_service.connected:
logger.debug(f"MQTT未连接跳过取消订阅: {topics}")
return
for topic in topics:
# 检查是否还有其他设备在监听这个topic
other_devices = [d for d in self.topic_mappings.get(topic, []) if d != device_id]
if not other_devices:
if self.use_async_mqtt:
# 异步MQTT需要创建任务来取消订阅
asyncio.create_task(self.mqtt_service.unsubscribe(topic))
else:
self.mqtt_service.unsubscribe(topic)
def get_handlers_for_topic(self, topic: str) -> List[str]:
"""获取处理指定topic的设备ID列表支持通配符匹配"""
handlers = []
# 首先尝试精确匹配
exact_handlers = self.topic_mappings.get(topic, [])
handlers.extend(exact_handlers)
# 然后尝试通配符匹配
topic_parts = topic.split('/')
for registered_topic, topic_handlers in self.topic_mappings.items():
# 跳过已经精确匹配的topic
if registered_topic == topic:
continue
registered_parts = registered_topic.split('/')
# 检查路径长度是否匹配
if len(topic_parts) == len(registered_parts):
# 检查每个部分是否匹配(支持+通配符)
match = True
for i, (topic_part, registered_part) in enumerate(zip(topic_parts, registered_parts)):
if registered_part != '+' and registered_part != topic_part:
match = False
break
if match:
handlers.extend(topic_handlers)
return handlers
def get_device_config(self, device_id: str) -> Optional[DeviceHandlerConfig]:
"""获取设备配置"""
return self.device_handlers.get(device_id)
def get_device_processor(self, device_id: str) -> Optional[Callable]:
"""获取设备处理器"""
return self.message_processors.get(device_id)
def _extract_device_id_from_topic(self, topic: str) -> Optional[str]:
"""快速从topic中提取设备ID优化版本"""
# oagv/v2/instance_BRAND/AGV001/state
# uagv/v2.0.0/BRAND/AGV001/state
parts = topic.split('/')
if len(parts) >= 5:
# 倒数第二个部分通常是设备ID
device_id = parts[-2]
# 过滤通配符
if device_id != "+":
return device_id
return None
async def _create_device_worker(self, device_id: str):
"""为设备创建独立的处理协程"""
if device_id in self.device_workers and not self.device_workers[device_id].done():
return # 已存在
# 创建队列
self.device_queues[device_id] = asyncio.Queue(maxsize=self.max_queue_size_per_device)
# 初始化统计信息
self.device_queue_stats[device_id] = {
"total_enqueued": 0,
"total_processed": 0,
"dropped_messages": 0,
"current_queue_size": 0,
"worker_started_at": datetime.now().isoformat()
}
# 创建处理协程
self.device_workers[device_id] = asyncio.create_task(
self._device_message_worker(device_id)
)
logger.info(f"为设备 {device_id} 创建了独立消息处理协程")
async def _device_message_worker(self, device_id: str):
"""设备消息处理协程 - 独立处理该设备的所有消息"""
logger.info(f"设备 {device_id} 消息处理协程已启动")
queue = self.device_queues[device_id]
while True:
try:
# 从队列取出消息
message_data = await queue.get()
try:
# 处理单条消息
await self._process_single_device_message(
device_id=device_id,
topic=message_data["topic"],
payload=message_data["payload"]
)
# 更新统计
if device_id in self.device_queue_stats:
self.device_queue_stats[device_id]["total_processed"] += 1
self.device_queue_stats[device_id]["current_queue_size"] = queue.qsize()
except Exception as e:
logger.error(f"处理设备 {device_id} 消息异常: {e}", exc_info=True)
finally:
queue.task_done()
except asyncio.CancelledError:
logger.info(f"设备 {device_id} 消息处理协程被取消")
break
except Exception as e:
logger.error(f"设备 {device_id} 处理协程异常: {e}", exc_info=True)
await asyncio.sleep(0.1) # 避免异常风暴
async def _process_single_device_message(self, device_id: str, topic: str,
payload: Union[str, bytes, Dict[str, Any]]):
"""处理单个设备的单条消息原来的handle_mqtt_message逻辑"""
try:
# 解析payload
if isinstance(payload, (str, bytes)):
payload_data = json.loads(payload)
else:
payload_data = payload
# 获取处理该topic的设备列表
handler_ids = self.get_handlers_for_topic(topic)
logger.debug(f"处理topic {topic} 的handler_ids: {handler_ids}")
# 分离自定义处理器和系统级处理器
custom_handlers = [hid for hid in handler_ids if not hid.startswith("system_")]
system_handlers = [hid for hid in handler_ids if hid.startswith("system_")]
# 优先处理自定义处理器
if custom_handlers:
logger.debug(f"使用自定义处理器: {custom_handlers}")
for handler_id in custom_handlers:
config = self.get_device_config(handler_id)
if config and config.enabled:
# 创建设备消息
message = DeviceMessage(
device_id=handler_id,
device_type=config.device_type,
topic=topic,
payload=payload_data,
timestamp=time.time(),
source_topic=topic,
target_topics=config.forward_topics
)
# 处理消息
await self._process_device_message(message)
# 如果没有自定义处理器,使用系统级处理器
elif system_handlers:
logger.debug(f"使用系统级处理器: {system_handlers}")
for handler_id in system_handlers:
# 系统级处理器从topic中提取设备ID
extracted_device_id = self.topic_manager.extract_vehicle_id_from_topic(topic)
if extracted_device_id:
# 根据收到的topic自动生成转发topic
forward_topic = self._generate_forward_topic_from_listen_topic(topic)
target_topics = [forward_topic] if forward_topic else []
# 创建系统级设备消息
message = DeviceMessage(
device_id=extracted_device_id,
device_type=DeviceType.VEHICLE, # 系统级默认为vehicle
topic=topic,
payload=payload_data,
timestamp=time.time(),
source_topic=topic,
target_topics=target_topics
)
# 使用系统级处理器处理
processor = self.message_processors.get(handler_id)
if processor:
await self._process_system_message(handler_id, message, processor)
except json.JSONDecodeError as e:
logger.error(f"MQTT消息JSON解析失败: {e}, topic: {topic}, payload: {payload}")
except Exception as e:
logger.error(f"处理设备消息异常: device_id={device_id}, topic={topic}, error={e}",
exc_info=True)
def _handle_mqtt_message(self, topic: str, payload: str):
"""处理MQTT消息"""
try:
# 获取当前事件循环,如果没有则创建新的
try:
loop = asyncio.get_running_loop()
# 在当前循环中创建任务
asyncio.create_task(self.handle_mqtt_message(topic, payload))
except RuntimeError:
# 没有运行中的事件循环,使用线程安全的方式
def run_async():
# 在新线程中运行异步处理
asyncio.run(self.handle_mqtt_message(topic, payload))
# 创建并启动线程
thread = threading.Thread(target=run_async, daemon=True)
thread.start()
except Exception as e:
logger.error(f"处理MQTT消息异常: {e}", exc_info=True)
async def _handle_mqtt_message_async(self, topic: str, payload: str):
"""处理MQTT消息异步版本"""
await self.handle_mqtt_message(topic, payload)
async def handle_mqtt_message(self, topic: str, payload: Union[str, bytes, Dict[str, Any]]):
"""处理MQTT消息 - 快速分发到设备队列"""
logger.debug(f"收到MQTT消息: topic={topic}")
try:
# 快速提取设备ID不做复杂处理
device_id = self._extract_device_id_from_topic(topic)
if not device_id:
logger.warning(f"无法从topic中提取设备ID: {topic}")
return
# 确保该设备有处理队列
if device_id not in self.device_queues:
await self._create_device_worker(device_id)
# 快速入队不阻塞使用put_nowait避免await
try:
message_data = {
"topic": topic,
"payload": payload,
"timestamp": time.time()
}
self.device_queues[device_id].put_nowait(message_data)
# 更新队列统计
if device_id in self.device_queue_stats:
self.device_queue_stats[device_id]["total_enqueued"] += 1
self.device_queue_stats[device_id]["current_queue_size"] = self.device_queues[device_id].qsize()
except asyncio.QueueFull:
logger.warning(f"设备 {device_id} 消息队列已满({self.max_queue_size_per_device}),丢弃消息")
# 记录丢弃统计
if device_id in self.device_queue_stats:
self.device_queue_stats[device_id]["dropped_messages"] += 1
except Exception as e:
logger.error(f"分发消息异常: topic={topic}, error={e}", exc_info=True)
async def _process_system_message(self, handler_id: str, message: DeviceMessage, processor: Callable):
"""处理系统级消息"""
try:
# 执行系统级处理器
if asyncio.iscoroutinefunction(processor):
result = await processor(message)
else:
result = processor(message)
# 处理转发逻辑
if result and isinstance(result, dict):
await self._handle_system_message_result(message, result)
except Exception as e:
logger.error(f"系统级消息处理失败: handler_id={handler_id}, device_id={message.device_id}, 错误: {e}", exc_info=True)
async def _handle_system_message_result(self, original_message: DeviceMessage, result: Dict[str, Any]):
"""处理系统级消息处理结果"""
try:
# 检查是否需要转发
if result.get("forward", True):
# 确定转发的payload
forward_payload = result.get("payload", original_message.payload)
# 确定转发的topic
response_topic = result.get("response_topic")
if response_topic:
self._publish_mqtt_message(response_topic, forward_payload)
logger.debug(f"系统级消息已转发: {original_message.device_id} -> {response_topic}")
except Exception as e:
logger.error(f"处理系统级消息结果失败: {e}", exc_info=True)
async def _process_device_message(self, message: DeviceMessage):
"""处理单个设备消息"""
device_id = message.device_id
config = self.device_handlers.get(device_id)
processor = self.message_processors.get(device_id)
if not config or not processor:
logger.warning(f"未找到设备处理器: {device_id}")
return
if not config.enabled:
logger.debug(f"设备处理器已禁用: {device_id}")
return
start_time = time.time()
try:
# 更新统计信息
stats = self.processing_stats[device_id]
stats["total_messages"] += 1
# 执行处理器
if asyncio.iscoroutinefunction(processor):
result = await processor(message)
else:
result = processor(message)
# 处理成功
stats["success_count"] += 1
stats["last_processed"] = datetime.now().isoformat()
message.processed = True
# 更新设备心跳
if device_id in self.active_devices:
self.active_devices[device_id]["last_heartbeat"] = time.time()
self.active_devices[device_id]["message_count"] += 1
# 计算平均处理时间
processing_time_ms = int((time.time() - start_time) * 1000)
if stats["average_processing_time_ms"] is None:
stats["average_processing_time_ms"] = processing_time_ms
else:
total_time = (stats["average_processing_time_ms"] *
(stats["success_count"] - 1) + processing_time_ms)
stats["average_processing_time_ms"] = int(total_time / stats["success_count"])
# 处理转发逻辑
if result and isinstance(result, dict):
await self._handle_message_result(message, result)
logger.debug(f"设备消息处理成功: {device_id}, 耗时: {processing_time_ms}ms")
except Exception as e:
# 处理失败
stats["error_count"] += 1
logger.error(f"设备消息处理失败: {device_id}, 错误: {e}", exc_info=True)
async def _handle_message_result(self, original_message: DeviceMessage, result: Dict[str, Any]):
"""处理消息处理结果
用户自定义处理函数返回值格式约束:
{
"forward": bool, # 是否转发消息可选默认True
"payload": dict, # 要转发的消息载荷(可选,默认使用原始消息)
"response_topic": str, # 自定义响应topic可选
"response": dict # 自定义响应消息载荷可选需配合response_topic
}
示例1 - 直接透传:
return {"forward": True} # 使用原始消息转发到默认topics
示例2 - 修改后转发:
return {
"forward": True,
"payload": {"status": "processed", "data": modified_data}
}
示例3 - 不转发:
return {"forward": False}
示例4 - 发送自定义响应:
return {
"forward": False,
"response_topic": "custom/response/topic",
"response": {"result": "success"}
}
"""
try:
config = self.device_handlers.get(original_message.device_id)
if not config:
logger.warning(f"未找到设备配置: {original_message.device_id}")
return
# 验证返回值格式
if not isinstance(result, dict):
logger.error(f"处理器返回值格式错误,必须是字典类型,实际类型: {type(result)}")
return
# 检查是否需要转发
should_forward = result.get("forward", True)
if should_forward:
# 确定转发的payload
forward_payload = result.get("payload", original_message.payload)
# 验证payload格式
if not isinstance(forward_payload, dict):
logger.error(f"转发payload格式错误必须是字典类型实际类型: {type(forward_payload)}")
return
# 确定转发的topics
forward_topics = config.forward_topics
if isinstance(forward_topics, str):
forward_topics = [forward_topics]
# 转发到目标topics
if forward_topics:
for topic in forward_topics:
self._publish_mqtt_message(topic, forward_payload)
logger.debug(f"消息已转发: {original_message.device_id} -> {topic}")
logger.debug(f"转发载荷: {forward_payload}")
else:
logger.warning(f"未配置转发topics设备: {original_message.device_id}")
# 处理自定义响应
if "response" in result:
response_topic = result.get("response_topic")
response_payload = result["response"]
# 验证响应格式
if not response_topic:
logger.error("提供了response但未指定response_topic")
return
if not isinstance(response_payload, dict):
logger.error(f"响应payload格式错误必须是字典类型实际类型: {type(response_payload)}")
return
self._publish_mqtt_message(response_topic, response_payload)
logger.debug(f"响应已发送: {original_message.device_id} -> {response_topic}")
logger.debug(f"响应载荷: {response_payload}")
except Exception as e:
logger.error(f"处理消息结果失败: {e}", exc_info=True)
def _publish_mqtt_message(self, topic: str, payload: Any):
"""发布MQTT消息"""
if self.use_async_mqtt:
# 对于异步MQTT创建异步任务
asyncio.create_task(self.mqtt_service.publish(topic, payload))
else:
self.mqtt_service.publish(topic, payload)
async def start_mqtt_service(self):
"""启动内部MQTT服务"""
try:
if self.use_async_mqtt:
success = await self.mqtt_service.connect()
else:
success = self.mqtt_service.connect()
if success:
logger.info("内部MQTT服务已启动")
# 启动系统级监听所有设备
await self.start_system_listening()
# 为已注册的设备订阅topics只订阅不重复添加消息处理器
all_topics = set()
for config in self.device_handlers.values():
all_topics.update(config.listen_topics)
if all_topics:
# 直接订阅topic不添加消息处理器消息处理器在注册时已添加
for topic in all_topics:
if self.use_async_mqtt:
await self.mqtt_service.subscribe(topic)
else:
self.mqtt_service.subscribe(topic)
return True
else:
logger.error("启动内部MQTT服务失败")
return False
except Exception as e:
logger.error(f"启动MQTT服务异常: {e}", exc_info=True)
return False
async def stop_mqtt_service(self):
"""停止内部MQTT服务"""
try:
# 先停止系统级监听
await self.stop_system_listening()
if self.use_async_mqtt:
await self.mqtt_service.disconnect()
else:
self.mqtt_service.disconnect()
logger.info("内部MQTT服务已停止")
except Exception as e:
logger.error(f"停止MQTT服务异常: {e}", exc_info=True)
def get_device_statistics(self, device_id: str = None) -> Dict[str, Any]:
"""获取设备统计信息"""
if device_id:
stats = self.processing_stats.get(device_id, {}).copy()
if device_id in self.active_devices:
stats.update({
"device_status": self.active_devices[device_id]["status"],
"last_heartbeat": self.active_devices[device_id]["last_heartbeat"],
"message_count": self.active_devices[device_id]["message_count"]
})
# 添加队列统计信息
if device_id in self.device_queue_stats:
stats["queue_stats"] = self.device_queue_stats[device_id].copy()
# 添加实时队列大小
if device_id in self.device_queues:
stats["queue_stats"]["current_queue_size"] = self.device_queues[device_id].qsize()
return stats
else:
# 计算总队列统计
total_enqueued = sum(s.get("total_enqueued", 0) for s in self.device_queue_stats.values())
total_processed = sum(s.get("total_processed", 0) for s in self.device_queue_stats.values())
total_dropped = sum(s.get("dropped_messages", 0) for s in self.device_queue_stats.values())
total_queue_size = sum(q.qsize() for q in self.device_queues.values())
return {
"device_count": len(self.device_handlers),
"running_devices": len([d for d in self.active_devices.values()
if d["status"] == "running"]),
"total_topics": len(self.topic_mappings),
"queue_summary": {
"total_workers": len(self.device_workers),
"total_queues": len(self.device_queues),
"total_enqueued": total_enqueued,
"total_processed": total_processed,
"total_dropped": total_dropped,
"current_total_queue_size": total_queue_size,
"max_queue_size_per_device": self.max_queue_size_per_device
},
"devices": {device_id: self.get_device_statistics(device_id)
for device_id in self.device_handlers.keys()}
}
def clear_script_device_registrations(self, script_id: str):
"""清理指定脚本的所有设备注册"""
devices_to_remove = []
for device_id, config in self.device_handlers.items():
if config.script_id == script_id:
devices_to_remove.append(device_id)
for device_id in devices_to_remove:
self.unregister_device_handler(device_id)
logger.info(f"已清理脚本 {script_id}{len(devices_to_remove)} 个设备注册")
async def start_system_listening(self):
"""启动系统级监听所有设备"""
if self.is_system_listening:
logger.info("系统监听已经在运行中")
return
logger.info("启动系统级设备监听...")
# 生成监听所有设备的topics使用通配符+
system_topics = self._generate_system_listen_topics()
# 创建系统级透传处理器
system_handler = self._create_system_forward_handler()
# 为每个topic创建系统级处理器
for topic in system_topics:
handler_id = f"system_{topic.replace('/', '_').replace('+', 'all')}"
# 注册系统级处理器
self.system_listen_topics[topic] = handler_id
self.message_processors[handler_id] = system_handler
# 添加topic映射
if topic not in self.topic_mappings:
self.topic_mappings[topic] = []
self.topic_mappings[topic].append(handler_id)
# 订阅topic只有在第一次添加时才添加消息处理器
if self.mqtt_service.connected:
# 检查是否已经为该topic添加了我们的消息处理器
topic_handlers = getattr(self.mqtt_service, 'message_handlers', {}).get(topic, [])
async_handler = self._handle_mqtt_message_async
sync_handler = self._handle_mqtt_message
if self.use_async_mqtt:
await self.mqtt_service.subscribe(topic)
# 只有当处理器不在列表中时才添加
if async_handler not in topic_handlers:
self.mqtt_service.add_message_handler(topic, async_handler)
else:
self.mqtt_service.subscribe(topic)
# 只有当处理器不在列表中时才添加
if sync_handler not in topic_handlers:
self.mqtt_service.add_message_handler(topic, sync_handler)
self.is_system_listening = True
logger.info(f"系统级监听已启动监听topics: {system_topics}")
def _generate_system_listen_topics(self) -> List[str]:
"""生成系统级监听所有设备的topics包括vehicle和other设备"""
topics = []
# 获取默认监听的指令类型
default_vehicle_commands = self.topic_manager.topic_config["default_command_types"]["vehicle"]
default_other_commands = self.topic_manager.topic_config["default_command_types"]["other"]
# 1. 为vehicle类型设备生成监听topics
logger.info("生成vehicle设备系统监听topics...")
vehicle_brands = self.topic_manager.get_supported_brands(category="vehicle")
for brand_name in vehicle_brands:
brand_config = self.topic_manager.get_brand_config(brand_name)
if not brand_config:
continue
brand_suffix = brand_config["brand_suffix"]
oagv_version = brand_config["oagv_version"]
uagv_version = brand_config["uagv_version"]
for command_type, direction_config in self.topic_manager.command_directions.items():
# 只生成vehicle默认监听列表中的指令类型
if command_type not in default_vehicle_commands:
logger.debug(f"[Vehicle] 跳过非默认监听指令类型: {command_type}")
continue
listen_queue = direction_config["listen"]
if listen_queue == "oagv":
# oagv格式: oagv/v2/{instanceId}_BRAND/+/command
topic = f"oagv/{oagv_version}/{self.topic_manager.instance_id}{brand_suffix}/+/{command_type}"
topics.append(topic)
logger.debug(f"[Vehicle] 添加监听topic: {topic}")
elif listen_queue == "uagv":
# uagv格式: uagv/v2.0.0/BRAND/+/command
brand_name_only = brand_suffix[1:] # 去掉前缀下划线
topic = f"uagv/{uagv_version}/{brand_name_only}/+/{command_type}"
topics.append(topic)
logger.debug(f"[Vehicle] 添加监听topic: {topic}")
# 2. 为other类型设备生成监听topics
logger.info("生成other设备系统监听topics...")
other_brands = self.topic_manager.get_supported_brands(category="other")
for brand_name in other_brands:
brand_config = self.topic_manager.get_brand_config(brand_name)
if not brand_config:
continue
brand_suffix = brand_config["brand_suffix"]
oagv_version = brand_config["oagv_version"]
uagv_version = brand_config["uagv_version"]
for command_type, direction_config in self.topic_manager.command_directions.items():
# 只生成other默认监听列表中的指令类型
if command_type not in default_other_commands:
logger.debug(f"[Other] 跳过非默认监听指令类型: {command_type}")
continue
listen_queue = direction_config["listen"]
if listen_queue == "oagv":
# oagv格式: oagv/v2/{instanceId}_BRAND/+/command
topic = f"oagv/{oagv_version}/{self.topic_manager.instance_id}{brand_suffix}/+/{command_type}"
topics.append(topic)
logger.debug(f"[Other] 添加监听topic: {topic}")
elif listen_queue == "uagv":
# uagv格式: uagv/v2.0.0/BRAND/+/command
brand_name_only = brand_suffix[1:] # 去掉前缀下划线
topic = f"uagv/{uagv_version}/{brand_name_only}/+/{command_type}"
topics.append(topic)
logger.debug(f"[Other] 添加监听topic: {topic}")
logger.info(f"系统监听topics生成完成{len(topics)}")
return topics
def _create_system_forward_handler(self) -> Callable:
"""创建系统级透传处理器"""
async def system_forward_handler(message: DeviceMessage) -> Dict[str, Any]:
"""系统级透传处理器:直接转发消息,不做任何处理"""
# 提取设备ID
device_id = self.topic_manager.extract_vehicle_id_from_topic(message.topic)
if not device_id:
logger.warning(f"无法从topic中提取设备ID: {message.topic}")
return {"forward": False}
# 检查是否为自定义处理的设备 - 关键修复:排除自定义设备
if device_id in self.custom_device_handlers:
logger.debug(f"设备 {device_id} 由自定义处理器处理,跳过系统转发")
return {"forward": False}
# 解析topic信息以确定转发目标
forward_topic = self._generate_forward_topic_from_listen_topic(message.topic)
if not forward_topic:
logger.warning(f"无法生成转发topic: {message.topic}")
return {"forward": False}
logger.info(f"系统透传转发: {message.topic} -> {forward_topic}, 设备: {device_id}")
# 直接透传消息
return {
"forward": True,
"payload": message.payload,
"response_topic": forward_topic
}
return system_forward_handler
def _generate_forward_topic_from_listen_topic(self, listen_topic: str) -> Optional[str]:
"""根据监听topic生成对应的转发topic"""
# logger.debug(f"开始解析转发topic: {listen_topic}")
parts = listen_topic.split('/')
if len(parts) < 5:
logger.warning(f"Topic格式不正确部分数量: {len(parts)}, topic: {listen_topic}")
return None
queue_type = parts[0] # oagv 或 uagv
version = parts[1]
brand_instance = parts[2]
device_id = parts[3]
command_type = parts[4]
# logger.debug(f"解析结果: queue_type={queue_type}, version={version}, brand_instance={brand_instance}, device_id={device_id}, command_type={command_type}")
# 如果device_id是通配符无法转发
if device_id == "+":
logger.debug("设备ID为通配符无法转发")
return None
# 查找指令的转发方向
if command_type not in self.topic_manager.command_directions:
logger.warning(f"未知的指令类型: {command_type}")
return None
direction_config = self.topic_manager.command_directions[command_type]
forward_queue = direction_config["forward"]
# logger.debug(f"指令 {command_type} 的转发方向: {forward_queue}")
# 解析品牌信息 - 改进品牌匹配逻辑(支持新的分类配置)
brand_name = None
# logger.debug(f"开始匹配品牌当前brand_instance: {brand_instance}")
# 遍历所有品牌(使用扁平化后的字典)
for brand, config in self.topic_manager._flat_brands.items():
brand_suffix = config["brand_suffix"]
# logger.debug(f"检查品牌 {brand}: suffix={brand_suffix}")
if queue_type == "oagv":
# oagv格式: oagv/v2/{instanceId}_BRAND/{deviceId}/command
# brand_instance 应该是 "asbm2_IRAYPLE" 这样的格式
if brand_instance.endswith(brand_suffix):
brand_name = brand
# logger.debug(f"成功匹配oagv品牌: {brand}")
break
elif queue_type == "uagv":
# uagv格式: uagv/v2/BRAND/{deviceId}/command
# brand_instance 应该是 "IRAYPLE" 这样的格式(不含下划线前缀)
brand_name_only = brand_suffix[1:] if brand_suffix.startswith('_') else brand_suffix
if brand_instance == brand_name_only:
brand_name = brand
# logger.debug(f"成功匹配uagv品牌: {brand}")
break
if not brand_name:
logger.warning(f"无法识别品牌信息: queue_type={queue_type}, brand_instance={brand_instance}")
logger.debug(f"可用的品牌配置: {list(self.topic_manager._flat_brands.keys())}")
return None
brand_config = self.topic_manager.get_brand_config(brand_name)
if not brand_config:
logger.error(f"无法获取品牌配置: {brand_name}")
return None
# logger.debug(f"使用品牌配置: {brand_name} -> {brand_config}")
# 生成转发topic
forward_topic = None
if forward_queue == "uagv":
# 转发到uagv
uagv_version = brand_config["uagv_version"]
brand_name_only = brand_config["brand_suffix"][1:] if brand_config["brand_suffix"].startswith('_') else brand_config["brand_suffix"]
forward_topic = f"uagv/{uagv_version}/{brand_name_only}/{device_id}/{command_type}"
# logger.debug(f"生成uagv转发topic: {forward_topic}")
elif forward_queue == "oagv":
# 转发到oagv
oagv_version = brand_config["oagv_version"]
brand_suffix = brand_config["brand_suffix"]
forward_topic = f"oagv/{oagv_version}/{self.topic_manager.instance_id}{brand_suffix}/{device_id}/{command_type}"
# logger.debug(f"生成oagv转发topic: {forward_topic}")
else:
logger.warning(f"未知的转发队列类型: {forward_queue}")
# logger.info(f"转发topic生成完成: {listen_topic} -> {forward_topic}")
return forward_topic
async def stop_system_listening(self):
"""停止系统级监听"""
if not self.is_system_listening:
return
logger.info("停止系统级设备监听...")
# 取消订阅所有系统级topics
for topic, handler_id in self.system_listen_topics.items():
if self.mqtt_service.connected:
if self.use_async_mqtt:
await self.mqtt_service.unsubscribe(topic)
else:
self.mqtt_service.unsubscribe(topic)
# 清理映射
if topic in self.topic_mappings:
if handler_id in self.topic_mappings[topic]:
self.topic_mappings[topic].remove(handler_id)
if not self.topic_mappings[topic]:
del self.topic_mappings[topic]
# 清理处理器
if handler_id in self.message_processors:
del self.message_processors[handler_id]
self.system_listen_topics.clear()
self.is_system_listening = False
logger.info("系统级监听已停止")
def get_system_listening_status(self) -> Dict[str, Any]:
"""获取系统级监听状态"""
return {
"is_listening": self.is_system_listening,
"system_topics": list(self.system_listen_topics.keys()),
"custom_devices": list(self.custom_device_handlers.keys()),
"excluded_devices_count": len(self.custom_device_handlers)
}
# 全局设备处理器注册中心
_global_device_registry = DeviceHandlerRegistry()
def get_device_registry() -> DeviceHandlerRegistry:
"""获取全局设备注册中心实例"""
return _global_device_registry
class DeviceHandlerService:
"""设备处理器服务"""
def __init__(self):
self.registry = get_device_registry()
async def start_service(self):
"""启动设备处理服务"""
if await self.registry.start_mqtt_service():
logger.info("设备处理服务已启动MQTT服务已连接")
else:
logger.warning("设备处理服务已启动但MQTT连接失败")
async def stop_service(self):
"""停止设备处理服务"""
# 先清理所有设备注册这会取消订阅topics
device_ids = list(self.registry.device_handlers.keys())
for device_id in device_ids:
self.registry.unregister_device_handler(device_id)
# 最后停止MQTT服务
await self.registry.stop_mqtt_service()
logger.info("设备处理服务已停止")
def stop_handler(self, device_id: str):
"""停止指定设备处理器"""
self.registry.unregister_device_handler(device_id)
def set_device_queue_size(self, max_size: int):
"""设置设备消息队列的最大容量
Args:
max_size: 最大队列容量建议500-2000
- 如果消息处理速度快,可以设置小一些(如500)
- 如果消息处理速度慢或消息频率高,可以设置大一些(如2000)
- 队列满了会自动丢弃新消息
Example:
device_service = get_device_service()
device_service.set_device_queue_size(1500) # 设置为1500
"""
self.registry.set_max_queue_size(max_size)
def get_running_handlers(self) -> Dict[str, Any]:
"""获取所有正在运行的设备处理器"""
return self.registry.get_device_statistics()
def get_handler_status(self, device_id: str) -> Dict[str, Any]:
"""获取设备处理器状态"""
return self.registry.get_device_statistics(device_id)
def get_system_listening_status(self) -> Dict[str, Any]:
"""获取系统级监听状态"""
return self.registry.get_system_listening_status()
async def publish_message(self, topic: str, payload: Any):
"""主动发布MQTT消息
Args:
topic: MQTT主题
payload: 消息载荷
"""
self.registry._publish_mqtt_message(topic, payload)
def send_order(self, device_id: str, order_data: Dict[str, Any], topic: str = None) -> None:
"""发送订单消息
Args:
device_id: 设备ID
order_data: 订单数据
topic: 发布主题可选默认使用vda5050格式
"""
import asyncio
if not topic:
topic = f"vda5050/{device_id}/order"
asyncio.create_task(
self.publish_message(topic, order_data)
)
def send_instant_action(self, device_id: str, actions: Union[List[str], List[Dict[str, Any]]],
topic: str = None) -> None:
"""发送即时动作消息
Args:
device_id: 设备ID
actions: 动作列表
topic: 发布主题可选默认使用vda5050格式
"""
import asyncio
if not topic:
topic = f"vda5050/{device_id}/instantActions"
action_data = {"actions": actions}
asyncio.create_task(
self.publish_message(topic, action_data)
)
def request_state(self, device_id: str, topic: str = None) -> None:
"""请求设备状态
Args:
device_id: 设备ID
topic: 发布主题可选默认使用vda5050格式
"""
import asyncio
if not topic:
topic = f"vda5050/{device_id}/state"
state_request = {"request_type": "state"}
asyncio.create_task(
self.publish_message(topic, state_request)
)
def request_factsheet(self, device_id: str, topic: str = None) -> None:
"""请求设备信息
Args:
device_id: 设备ID
topic: 发布主题可选默认使用vda5050格式
"""
import asyncio
if not topic:
topic = f"vda5050/{device_id}/factsheet"
factsheet_request = {"request_type": "factsheet"}
asyncio.create_task(
self.publish_message(topic, factsheet_request)
)
# 保持兼容性的别名方法
def send_vda5050_order(self, device_id: str, order_data: Dict[str, Any], topic: str = None) -> None:
"""发送VDA5050订单指令兼容性方法"""
self.send_order(device_id, order_data, topic)
def send_vda5050_instant_action(self, device_id: str, actions: List[Dict[str, Any]], topic: str = None) -> None:
"""发送VDA5050即时动作指令兼容性方法"""
self.send_instant_action(device_id, actions, topic)
def request_vda5050_state(self, device_id: str, topic: str = None) -> None:
"""请求VDA5050状态兼容性方法"""
self.request_state(device_id, topic)
@staticmethod
def create_default_forward_handler():
"""创建默认的透传处理器"""
def default_handler(message: DeviceMessage) -> Dict[str, Any]:
"""默认消息处理器:直接透传所有消息"""
logger.info(f"默认处理器收到消息: device_id={message.device_id}, "
f"topic={message.topic}, payload大小={len(str(message.payload))}")
# 透传所有消息到转发topics
return {
"forward": True,
"payload": message.payload,
"topics": message.target_topics
}
return default_handler
# 全局设备处理服务实例
_global_device_service = DeviceHandlerService()
def get_device_service() -> DeviceHandlerService:
"""获取全局设备处理服务实例"""
return _global_device_service