1036 lines
41 KiB
Python
1036 lines
41 KiB
Python
#!/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
|
||
|
||
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 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() # 已移除协议注册中心
|
||
|
||
def register_device_handler(self, config: DeviceHandlerConfig, processor: Callable):
|
||
"""注册并运行设备处理器"""
|
||
device_id = config.device_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("消息透传模式: 已启用")
|
||
|
||
# 如果MQTT服务已连接,立即订阅topics
|
||
if self.mqtt_service.connected:
|
||
if self.use_async_mqtt:
|
||
# 异步MQTT需要在异步上下文中订阅
|
||
asyncio.create_task(self._subscribe_topics_async(config.listen_topics))
|
||
else:
|
||
self._subscribe_topics(config.listen_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]
|
||
|
||
# 清理注册信息
|
||
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添加消息处理器
|
||
self.mqtt_service.add_message_handler(topic, self._handle_mqtt_message)
|
||
self.mqtt_service.subscribe(topic)
|
||
|
||
async def _subscribe_topics_async(self, topics: List[str]):
|
||
"""订阅MQTT topics(异步版本)"""
|
||
for topic in topics:
|
||
# 为每个topic添加消息处理器
|
||
self.mqtt_service.add_message_handler(topic, self._handle_mqtt_message_async)
|
||
await self.mqtt_service.subscribe(topic)
|
||
|
||
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列表"""
|
||
return self.topic_mappings.get(topic, [])
|
||
|
||
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 _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消息"""
|
||
try:
|
||
# 解析payload
|
||
if isinstance(payload, (str, bytes)):
|
||
payload_data = json.loads(payload)
|
||
else:
|
||
payload_data = payload
|
||
|
||
# 获取处理该topic的设备列表
|
||
device_ids = self.get_handlers_for_topic(topic)
|
||
|
||
for device_id in device_ids:
|
||
config = self.get_device_config(device_id)
|
||
if config and config.enabled:
|
||
decoded_payload = payload_data
|
||
|
||
# 创建设备消息
|
||
message = DeviceMessage(
|
||
device_id=device_id,
|
||
device_type=config.device_type,
|
||
topic=topic,
|
||
payload=decoded_payload,
|
||
timestamp=time.time(),
|
||
source_topic=topic,
|
||
target_topics=config.forward_topics
|
||
)
|
||
|
||
# 立即处理消息
|
||
await self._process_device_message(message)
|
||
|
||
except json.JSONDecodeError as e:
|
||
logger.error(f"MQTT消息JSON解析失败: {e}, topic: {topic}, payload: {payload}")
|
||
except Exception as e:
|
||
logger.error(f"处理MQTT消息失败: {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]):
|
||
"""处理消息处理结果"""
|
||
try:
|
||
config = self.device_handlers.get(original_message.device_id)
|
||
if not config:
|
||
return
|
||
|
||
# 检查是否需要转发
|
||
if result.get("forward", True):
|
||
# 确定转发的payload
|
||
forward_payload = result.get("payload", original_message.payload)
|
||
|
||
# 直接使用转发payload,不进行编码
|
||
encoded_payload = forward_payload
|
||
|
||
# 确定转发的topics
|
||
forward_topics = result.get("topics", original_message.target_topics)
|
||
if isinstance(forward_topics, str):
|
||
forward_topics = [forward_topics]
|
||
|
||
# 转发到所有目标topics
|
||
if forward_topics:
|
||
for topic in forward_topics:
|
||
self._publish_mqtt_message(topic, encoded_payload)
|
||
logger.debug(f"消息已转发: {original_message.device_id} -> {topic}")
|
||
|
||
# 处理自定义响应
|
||
if "response" in result:
|
||
response_topic = result.get("response_topic")
|
||
response_payload = result["response"]
|
||
|
||
# 直接使用响应payload,不进行编码
|
||
|
||
if response_topic:
|
||
self._publish_mqtt_message(response_topic, response_payload)
|
||
logger.debug(f"响应已发送: {original_message.device_id} -> {response_topic}")
|
||
|
||
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服务已启动")
|
||
|
||
# 为已注册的设备订阅topics
|
||
all_topics = set()
|
||
for config in self.device_handlers.values():
|
||
all_topics.update(config.listen_topics)
|
||
|
||
if all_topics:
|
||
if self.use_async_mqtt:
|
||
await self._subscribe_topics_async(list(all_topics))
|
||
else:
|
||
self._subscribe_topics(list(all_topics))
|
||
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:
|
||
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"]
|
||
})
|
||
return stats
|
||
else:
|
||
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),
|
||
"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)} 个设备注册")
|
||
|
||
|
||
# 全局设备处理器注册中心
|
||
_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 register_and_run(self, device_id: str, device_type: Union[str, DeviceType],
|
||
listen_topics: List[str], forward_topics: List[str],
|
||
handler: Callable, script_id: str, description: str = "",
|
||
device_brand: Union[str, DeviceBrand] = DeviceBrand.STANDARD,
|
||
protocol_type: Union[str, ProtocolType] = ProtocolType.VDA5050,
|
||
protocol_key: str = "vda5050", # 兼容性参数,默认值
|
||
**kwargs):
|
||
"""注册并运行设备处理器(消息直接透传)
|
||
|
||
Args:
|
||
device_id: 设备ID
|
||
device_type: 设备类型
|
||
listen_topics: 监听的MQTT topics
|
||
forward_topics: 转发的MQTT topics
|
||
handler: 消息处理器函数
|
||
script_id: 脚本ID
|
||
description: 描述信息
|
||
device_brand: 设备品牌(默认standard)
|
||
protocol_type: 协议类型(vda5050或tcp,默认vda5050)
|
||
protocol_key: 协议标识(已弃用,兼容性参数)
|
||
**kwargs: 其他配置参数
|
||
"""
|
||
|
||
# 确保device_type是DeviceType枚举
|
||
if isinstance(device_type, str):
|
||
device_type = DeviceType(device_type)
|
||
|
||
# 处理设备品牌
|
||
if isinstance(device_brand, str):
|
||
try:
|
||
device_brand = DeviceBrand(device_brand)
|
||
except ValueError:
|
||
logger.warning(f"未知的设备品牌: {device_brand}, 使用STANDARD品牌")
|
||
device_brand = DeviceBrand.STANDARD
|
||
|
||
# 处理协议类型
|
||
if isinstance(protocol_type, str):
|
||
try:
|
||
protocol_type = ProtocolType(protocol_type)
|
||
except ValueError:
|
||
logger.warning(f"未知的协议类型: {protocol_type}, 使用VDA5050协议")
|
||
protocol_type = ProtocolType.VDA5050
|
||
|
||
# 过滤掉已弃用的兼容性参数
|
||
if protocol_key != "vda5050" and protocol_key:
|
||
logger.warning(f"protocol_key参数已弃用,使用默认VDA5050协议: {protocol_key}")
|
||
|
||
# 过滤kwargs中的兼容性参数(包括可能传入的auto_encode)
|
||
filtered_kwargs = {k: v for k, v in kwargs.items()
|
||
if k not in ['protocol_key', 'auto_encode']}
|
||
|
||
config = DeviceHandlerConfig(
|
||
device_id=device_id,
|
||
device_type=device_type,
|
||
listen_topics=listen_topics,
|
||
forward_topics=forward_topics,
|
||
handler_function=handler.__name__,
|
||
script_id=script_id,
|
||
description=description,
|
||
device_brand=device_brand,
|
||
protocol_type=protocol_type,
|
||
**filtered_kwargs
|
||
)
|
||
|
||
self.registry.register_device_handler(config, handler)
|
||
|
||
def stop_handler(self, device_id: str):
|
||
"""停止指定设备处理器"""
|
||
self.registry.unregister_device_handler(device_id)
|
||
|
||
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)
|
||
|
||
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)
|
||
|
||
|
||
# 全局设备处理服务实例
|
||
_global_device_service = DeviceHandlerService()
|
||
|
||
|
||
def get_device_service() -> DeviceHandlerService:
|
||
"""获取全局设备处理服务实例"""
|
||
return _global_device_service |