From 559699ec1921f56941e6d143ca2c871e571dabb8 Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Mon, 22 Sep 2025 14:18:03 +0800 Subject: [PATCH] =?UTF-8?q?update:=E5=A2=9E=E5=8A=A0=E5=8D=95=E6=A8=A1?= =?UTF-8?q?=E5=9D=97mqtt-gateway=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 18 ++++ main/xiaozhi-server/core/api/ota_handler.py | 108 +++++++++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index a02de435..a946579c 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -41,6 +41,24 @@ server: # 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。 #allowed_devices: # - "24:0A:C4:1D:3B:F0" # MAC地址列表 + # MQTT网关配置,用于通过OTA下发到设备 + mqtt_gateway: + # MQTT服务器地址以及端口,根据mqtt_gateway的.env文件配置,为null或者不填写时使用websocket协议 + host: null + port: null + # group_id会从设备型号动态生成,格式为GID_设备型号 + # 此配置仅作为当获取设备型号失败时可以填写的回退值 + # group_id: + + # MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置 + mqtt_signature_key: null + + # UDP网关配置,根据mqtt_gateway的.env文件配置 + udp_gateway: + # UDP服务器地址 + host: null + # UDP服务器端口 + port: null log: # 设置控制台输出的日志格式,时间、日志级别、标签、消息 log_format: "{time:YYMMDD HH:mm:ss}[{version}_{selected_module}][{extra[tag]}]-{level}-{message}" diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index 763c9c49..f24c3f8f 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -1,5 +1,8 @@ import json import time +import base64 +import hashlib +import hmac from aiohttp import web from core.utils.util import get_local_ip from core.api.base_handler import BaseHandler @@ -10,6 +13,24 @@ TAG = __name__ class OTAHandler(BaseHandler): def __init__(self, config: dict): super().__init__(config) + + def generate_password_signature(self, content: str, secret_key: str) -> str: + """生成MQTT密码签名 + + Args: + content: 签名内容 (clientId + '|' + username) + secret_key: 密钥 + + Returns: + str: Base64编码的HMAC-SHA256签名 + """ + try: + hmac_obj = hmac.new(secret_key.encode('utf-8'), content.encode('utf-8'), hashlib.sha256) + signature = hmac_obj.digest() + return base64.b64encode(signature).decode('utf-8') + except Exception as e: + self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}") + return "" def _get_websocket_url(self, local_ip: str, port: int) -> str: """获取websocket地址 @@ -58,10 +79,91 @@ class OTAHandler(BaseHandler): "version": data_json["application"].get("version", "1.0.0"), "url": "", }, - "websocket": { - "url": self._get_websocket_url(local_ip, port), - }, } + + mqtt_gateway_config = server_config.get("mqtt_gateway", {}) + mqtt_gateway_host = mqtt_gateway_config.get("host", "") + mqtt_gateway_port = mqtt_gateway_config.get("port", "") + + + if mqtt_gateway_host: # 配置了mqtt_gateway,使用MQTT和UDP协议传输 + # 从设备型号获取group_id,与manager-api保持一致 + # 客户端ID格式:groupId@@@macAddress@@@macAddress + # 尝试从请求数据中获取设备型号 + device_model = "default" + try: + # 假设设备型号在request数据的某个字段中 + if "device" in data_json and isinstance(data_json["device"], dict): + device_model = data_json["device"].get("model", "default") + elif "model" in data_json: + device_model = data_json["model"] + # 为了保证格式一致性,进行与manager-api相同的处理 + group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_") + except Exception as e: + self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}") + # 如果获取失败,使用配置文件中的默认值 + group_id = mqtt_gateway_config.get("group_id", "GID_default").replace(":", "_") + + mac_address_safe = device_id.replace(":", "_") + mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}" + + # 构建用户数据(包含IP等信息) + user_data = { + "ip": "unknown" + } + + # 将用户数据编码为Base64 JSON + try: + user_data_json = json.dumps(user_data) + username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8') + except Exception as e: + self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}") + username = "" + + # 获取MQTT签名密钥 + password = "" + signature_key = server_config.get("mqtt_signature_key", "") + if signature_key: + # 使用签名密钥生成密码 + password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key) + if not password: + # 如果签名生成失败,使用配置文件中的密码 + password = mqtt_gateway_config.get("password", "") + else: + # 如果没有签名密钥,使用配置文件中的密码 + password = mqtt_gateway_config.get("password", "") + self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,使用配置文件中的密码") + + # 构建MQTT配置 + endpoint = f"{mqtt_gateway_host}:{mqtt_gateway_port}" + return_json["mqtt_gateway"] = { + "endpoint": endpoint, + "client_id": mqtt_client_id, + "username": username, + "password": password, + "publish_topic": "device-server", + "subscribe_topic": f"devices/p2p/{mac_address_safe}" + } + self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置") + + # 添加UDP网关配置 + if "udp_gateway" in server_config: + udp_config = server_config["udp_gateway"] + udp_host = udp_config.get("host", "") + udp_port = udp_config.get("port", "") + if udp_host: + return_json["udp_gateway"] = { + "host": udp_host, + "port": udp_port + } + self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发UDP网关配置") + + else: # 未配置mqtt_gateway,使用WebSocket协议传输 + return_json["websocket"] = { + "url": self._get_websocket_url(local_ip, port), + } + self.logger.bind(tag=TAG).info(f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置") + response = web.Response( text=json.dumps(return_json, separators=(",", ":")), content_type="application/json",