mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-24 08:03:53 +08:00
280 lines
9.8 KiB
Python
280 lines
9.8 KiB
Python
import asyncio
|
|
import json
|
|
import time
|
|
import uuid
|
|
from typing import Dict, Any, Optional, Callable
|
|
from config.logger import setup_logging
|
|
|
|
logger = setup_logging()
|
|
|
|
|
|
class MQTTConnection:
|
|
"""
|
|
MQTT连接处理类:管理单个MQTT客户端连接
|
|
处理MQTT协议消息和会话管理
|
|
"""
|
|
|
|
def __init__(self, socket, connection_id: int, mqtt_server):
|
|
self.socket = socket
|
|
self.connection_id = connection_id
|
|
self.mqtt_server = mqtt_server
|
|
|
|
# 连接信息
|
|
self.client_id = None
|
|
self.device_id = None
|
|
self.username = None
|
|
self.password = None
|
|
self.session_id = None
|
|
|
|
# 协议状态
|
|
self.is_connected_flag = False
|
|
self.keep_alive_interval = 0
|
|
self.last_activity = time.time()
|
|
|
|
# 消息处理
|
|
self.message_callback = None
|
|
self.reply_topic = None
|
|
|
|
# UDP相关
|
|
self.udp_config = None
|
|
|
|
# 任务管理
|
|
self.keep_alive_task = None
|
|
self._closed = False
|
|
|
|
# 创建MQTT协议处理器
|
|
from core.protocols.mqtt_protocol import MQTTProtocol
|
|
self.protocol = MQTTProtocol(socket)
|
|
self._setup_protocol_handlers()
|
|
|
|
def _setup_protocol_handlers(self):
|
|
"""设置协议事件处理"""
|
|
self.protocol.on('connect', self._handle_connect)
|
|
self.protocol.on('publish', self._handle_publish)
|
|
self.protocol.on('subscribe', self._handle_subscribe)
|
|
self.protocol.on('disconnect', self._handle_disconnect)
|
|
self.protocol.on('close', self._handle_close)
|
|
self.protocol.on('error', self._handle_error)
|
|
|
|
async def _handle_connect(self, connect_data: Dict[str, Any]):
|
|
"""处理CONNECT消息"""
|
|
try:
|
|
self.client_id = connect_data['clientId']
|
|
self.username = connect_data.get('username')
|
|
self.password = connect_data.get('password')
|
|
self.keep_alive_interval = connect_data.get('keepAlive', 0) * 1000 # 转换为毫秒
|
|
|
|
logger.info(f"MQTT客户端连接: {self.client_id}")
|
|
|
|
# 解析客户端ID获取设备信息
|
|
if not self._parse_client_id():
|
|
await self.protocol.send_connack(1) # 连接被拒绝
|
|
await self.close()
|
|
return
|
|
|
|
# 生成会话ID
|
|
self.session_id = str(uuid.uuid4())
|
|
|
|
# 设置回复主题
|
|
self.reply_topic = f"devices/p2p/{self.device_id.replace(':', '_')}"
|
|
|
|
# 发送连接确认
|
|
await self.protocol.send_connack(0) # 连接接受
|
|
self.is_connected_flag = True
|
|
|
|
# 启动心跳检查
|
|
if self.keep_alive_interval > 0:
|
|
self.keep_alive_task = asyncio.create_task(self._keep_alive_check())
|
|
|
|
# 通知服务器新连接
|
|
await self.mqtt_server.on_client_connected(self)
|
|
|
|
except Exception as e:
|
|
logger.error(f"处理CONNECT消息失败: {e}")
|
|
await self.close()
|
|
|
|
def _parse_client_id(self) -> bool:
|
|
"""解析客户端ID获取设备信息"""
|
|
try:
|
|
# 支持格式: GID_test@@@mac_address@@@uuid 或 GID_test@@@mac_address
|
|
parts = self.client_id.split('@@@')
|
|
|
|
if len(parts) >= 2:
|
|
self.group_id = parts[0]
|
|
# 将下划线替换为冒号格式的MAC地址
|
|
self.device_id = parts[1].replace('_', ':')
|
|
|
|
if len(parts) >= 3:
|
|
self.uuid = parts[2]
|
|
|
|
return True
|
|
else:
|
|
logger.error(f"无效的客户端ID格式: {self.client_id}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"解析客户端ID失败: {e}")
|
|
return False
|
|
|
|
async def _handle_publish(self, publish_data: Dict[str, Any]):
|
|
"""处理PUBLISH消息"""
|
|
try:
|
|
topic = publish_data['topic']
|
|
payload = publish_data['payload']
|
|
|
|
logger.debug(f"收到MQTT发布消息: topic={topic}, payload={payload}")
|
|
|
|
# 更新活动时间
|
|
self.last_activity = time.time()
|
|
|
|
# 解析JSON消息
|
|
try:
|
|
message_data = json.loads(payload)
|
|
|
|
# 处理不同类型的消息
|
|
if message_data.get('type') == 'hello':
|
|
await self._handle_hello_message(message_data)
|
|
else:
|
|
# 其他消息通过回调处理
|
|
if self.message_callback:
|
|
self.message_callback(topic, payload)
|
|
|
|
except json.JSONDecodeError:
|
|
logger.error(f"MQTT消息JSON解析失败: {payload}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"处理PUBLISH消息失败: {e}")
|
|
|
|
async def _handle_hello_message(self, message_data: Dict[str, Any]):
|
|
"""处理hello消息,初始化UDP配置"""
|
|
try:
|
|
# 生成UDP加密配置
|
|
import os
|
|
|
|
self.udp_config = {
|
|
'key': os.urandom(16),
|
|
'encryption': 'aes-128-ctr',
|
|
'server': self.mqtt_server.public_ip,
|
|
'port': self.mqtt_server.udp_port
|
|
}
|
|
|
|
# 构造hello回复
|
|
hello_reply = {
|
|
'type': 'hello',
|
|
'version': message_data.get('version', 3),
|
|
'session_id': self.session_id,
|
|
'transport': 'udp',
|
|
'udp': {
|
|
'server': self.udp_config['server'],
|
|
'port': self.udp_config['port'],
|
|
'encryption': self.udp_config['encryption'],
|
|
'key': self.udp_config['key'].hex(),
|
|
'nonce': '00' * 16 # 临时nonce
|
|
},
|
|
'audio_params': message_data.get('audio_params', {})
|
|
}
|
|
|
|
# 发送回复
|
|
await self.send_message(self.reply_topic, json.dumps(hello_reply))
|
|
|
|
logger.info(f"MQTT Hello消息处理完成: {self.client_id}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"处理hello消息失败: {e}")
|
|
|
|
async def _handle_subscribe(self, subscribe_data: Dict[str, Any]):
|
|
"""处理SUBSCRIBE消息"""
|
|
try:
|
|
topic = subscribe_data['topic']
|
|
packet_id = subscribe_data['packetId']
|
|
|
|
logger.debug(f"客户端订阅主题: {topic}")
|
|
|
|
# 发送订阅确认
|
|
await self.protocol.send_suback(packet_id, 0)
|
|
|
|
except Exception as e:
|
|
logger.error(f"处理SUBSCRIBE消息失败: {e}")
|
|
|
|
async def _handle_disconnect(self):
|
|
"""处理DISCONNECT消息"""
|
|
logger.info(f"客户端主动断开连接: {self.client_id}")
|
|
await self.close()
|
|
|
|
async def _handle_close(self):
|
|
"""处理连接关闭"""
|
|
logger.info(f"MQTT连接关闭: {self.client_id}")
|
|
await self.close()
|
|
|
|
async def _handle_error(self, error):
|
|
"""处理连接错误"""
|
|
logger.error(f"MQTT连接错误: {self.client_id}, error: {error}")
|
|
await self.close()
|
|
|
|
async def _keep_alive_check(self):
|
|
"""心跳检查任务"""
|
|
try:
|
|
while self.is_connected_flag and not self._closed:
|
|
await asyncio.sleep(self.keep_alive_interval / 1000 / 2) # 检查间隔为心跳间隔的一半
|
|
|
|
current_time = time.time()
|
|
if current_time - self.last_activity > self.keep_alive_interval / 1000 * 1.5:
|
|
logger.info(f"MQTT客户端心跳超时: {self.client_id}")
|
|
await self.close()
|
|
break
|
|
|
|
except asyncio.CancelledError:
|
|
pass
|
|
except Exception as e:
|
|
logger.error(f"心跳检查任务出错: {e}")
|
|
|
|
def set_message_callback(self, callback: Callable[[str, str], None]):
|
|
"""设置消息接收回调"""
|
|
self.message_callback = callback
|
|
|
|
async def send_message(self, topic: str, payload: str):
|
|
"""发送MQTT消息"""
|
|
if self._closed or not self.is_connected_flag:
|
|
return
|
|
|
|
try:
|
|
await self.protocol.send_publish(topic, payload, qos=0)
|
|
logger.debug(f"发送MQTT消息: topic={topic}, payload={payload}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"发送MQTT消息失败: {e}")
|
|
|
|
def is_connected(self) -> bool:
|
|
"""检查连接状态"""
|
|
return self.is_connected_flag and not self._closed
|
|
|
|
async def close(self):
|
|
"""关闭连接"""
|
|
if self._closed:
|
|
return
|
|
|
|
self._closed = True
|
|
self.is_connected_flag = False
|
|
|
|
# 取消心跳检查任务
|
|
if self.keep_alive_task and not self.keep_alive_task.done():
|
|
self.keep_alive_task.cancel()
|
|
try:
|
|
await self.keep_alive_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
# 通知服务器连接关闭
|
|
try:
|
|
await self.mqtt_server.on_client_disconnected(self)
|
|
except Exception as e:
|
|
logger.error(f"通知服务器连接关闭失败: {e}")
|
|
|
|
# 关闭协议处理器
|
|
try:
|
|
await self.protocol.close()
|
|
except Exception as e:
|
|
logger.error(f"关闭MQTT协议处理器失败: {e}")
|
|
|
|
logger.info(f"MQTT连接已关闭: {self.client_id}")
|