From 3d04166314eded71d202fcf58bc9302992aee3ff Mon Sep 17 00:00:00 2001 From: bin <632190820@qq.com> Date: Sun, 15 Mar 2026 18:36:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=BD=AE=E8=B6=B3?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=E5=AF=BC=E8=88=AAMCP=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 lzwcai_temi_mcp 包,实现基于MQTT的轮足机器人导航控制服务器。包含以下功能: - 通过 MQTT 发布机器人控制命令 - 提供 goto、speak、reception、repose、patrol 等导航工具 - 支持通过 MCP 协议与AI助手集成 - 更新 lzwcai_lark_mcp 版本至 0.1.11 --- lzwcai_lark_mcp/pyproject.toml | 2 +- lzwcai_lark_mcp/test.py | 11 +- lzwcai_temi_mcp/__init__.py | 0 lzwcai_temi_mcp/lzwcai_temi_mcp/__init__.py | 0 lzwcai_temi_mcp/lzwcai_temi_mcp/main.py | 151 ++++++++++++++++++ lzwcai_temi_mcp/lzwcai_temi_mcp/mcp_mqtt.py | 113 +++++++++++++ lzwcai_temi_mcp/lzwcai_temi_mcp/nav_server.py | 102 ++++++++++++ lzwcai_temi_mcp/mcp-server.json | 13 ++ lzwcai_temi_mcp/pyproject.toml | 26 +++ 9 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 lzwcai_temi_mcp/__init__.py create mode 100644 lzwcai_temi_mcp/lzwcai_temi_mcp/__init__.py create mode 100644 lzwcai_temi_mcp/lzwcai_temi_mcp/main.py create mode 100644 lzwcai_temi_mcp/lzwcai_temi_mcp/mcp_mqtt.py create mode 100644 lzwcai_temi_mcp/lzwcai_temi_mcp/nav_server.py create mode 100644 lzwcai_temi_mcp/mcp-server.json create mode 100644 lzwcai_temi_mcp/pyproject.toml diff --git a/lzwcai_lark_mcp/pyproject.toml b/lzwcai_lark_mcp/pyproject.toml index 2685f32..62d6b48 100644 --- a/lzwcai_lark_mcp/pyproject.toml +++ b/lzwcai_lark_mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "lzwcai-lark-mcp" -version = "0.1.10" +version = "0.1.11" description = "Lark MCP server" requires-python = ">=3.10" dependencies = [ diff --git a/lzwcai_lark_mcp/test.py b/lzwcai_lark_mcp/test.py index a00ea41..95acb7e 100644 --- a/lzwcai_lark_mcp/test.py +++ b/lzwcai_lark_mcp/test.py @@ -35,7 +35,7 @@ def main() -> None: mcp_module.types = types_module sys.modules["mcp"] = mcp_module sys.modules["mcp.types"] = types_module - from lzwcai_lark_mcp.tools import send_asset_confirmation_card + from lzwcai_lark_mcp.tools import send_stranger_card app_id = os.getenv("app_id", "") app_secret = os.getenv("app_secret", "") auth_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" @@ -53,17 +53,18 @@ def main() -> None: if not token: raise RuntimeError(f"lark auth response missing token: {data}") user_id = "gegg1d78" - result = send_asset_confirmation_card( + result = send_stranger_card( token, user_id, "CONF-20260301-001", "2026-03-01 10:30:00", [ {"华为手机": "huawei_phone"}, - {"红米手机": "redmi_phone"}, - "MacBook Pro" + {"红米手机": "redmi_phone"} ], - "如有误报请点击反馈" + face_cap="img_v3_02vj_b25b040f-b6c1-49f4-a29d-a02c99a13a9g", + user_ids=["347f5e71", "gegg1d78"], + remark="如有误报请点击反馈" ) print(result) diff --git a/lzwcai_temi_mcp/__init__.py b/lzwcai_temi_mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_temi_mcp/lzwcai_temi_mcp/__init__.py b/lzwcai_temi_mcp/lzwcai_temi_mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_temi_mcp/lzwcai_temi_mcp/main.py b/lzwcai_temi_mcp/lzwcai_temi_mcp/main.py new file mode 100644 index 0000000..5428724 --- /dev/null +++ b/lzwcai_temi_mcp/lzwcai_temi_mcp/main.py @@ -0,0 +1,151 @@ +from typing import Sequence +import logging +import asyncio +from mcp.server import Server +from mcp.server.stdio import stdio_server +from mcp.types import Tool, TextContent +from .mcp_mqtt import get_mcpmqtt_handler +from .nav_server import NavServer + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +async def serve() -> None: + server = Server("terminal_temi_mcp") + mmhandler = get_mcpmqtt_handler() + nav_server = NavServer(mmhandler) + + @server.list_tools() + async def list_tools() -> list[Tool]: + """列出所有工具""" + return [ + Tool( + name="goto", + description="轮足机器人导航到指定地点为用户引路。触发关键词:带我去、导航、引路、带路、怎么走、在哪里。", + inputSchema={ + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "目标地点名称", + "minLength": 1 + } + }, + "required": ["location"] + } + ), + Tool( + name="speak", + description="轮足机器人进行语音播报:告诉、提醒、告知、提示、通知", + inputSchema={ + "type": "object", + "properties": { + "speech": { + "type": "string", + "description": "要播报的语音内容", + "minLength": 1 + } + }, + "required": ["speech"] + } + ), + Tool( + name="reception", + description="轮足机器人去接待客人:去接人、请迎接客人、去接待、迎接一下、带人过来", + inputSchema={ + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "引导接待客人到这个位置", + "minLength": 1 + }, + "name": { + "type": "string", + "description": "客人姓名", + "minLength": 1 + } + }, + "required": ["location", "name"] + } + ), + Tool( + name="repose", + description="轮足机器人、助手、机器人去重新定位", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="patrol", + description="轮足机器人、助手、机器人去巡逻:巡逻、巡查、去检查一下、去看看、去巡视", + inputSchema={ + "type": "object", + "properties": { + "locations": { + "type": "array", + "description": "机器人巡逻经过的地点列表", + "items": { + "type": "string" + } + } + }, + "required": ["locations"] + } + ) + ] + + @server.call_tool() + async def call_tool(name: str, arguments: dict) -> Sequence[TextContent]: + """处理工具调用""" + try: + result = "" + if name == "goto": + if "location" not in arguments: + raise ValueError("缺少必要参数: location") + result = await nav_server.goto( + location=arguments["location"] + ) + elif name == "speak": + if "speech" not in arguments: + raise ValueError("缺少必要参数: speech") + result = await nav_server.speak( + speech=arguments["speech"] + ) + elif name == "reception": + if "location" not in arguments: + raise ValueError("缺少必要参数: location") + result = await nav_server.reception( + location=arguments["location"], + name=arguments.get("name", "贵宾") + ) + elif name == "repose": + result = await nav_server.repose() + elif name == "patrol": + if "locations" not in arguments: + raise ValueError("缺少必要参数: locations") + result = await nav_server.patrol( + locations=arguments["locations"] + ) + else: + raise ValueError(f"未知工具: {name}") + return [TextContent(type="text", text=result)] + except Exception as e: + logger.error(f"工具调用失败: {str(e)}") + raise ValueError(f"执行失败: {str(e)}") + + options = server.create_initialization_options() + async with stdio_server() as (read_stream, write_stream): + await server.run(read_stream, write_stream, options) + +def main(): + asyncio.run(serve()) + +if __name__ == "__main__": + main() diff --git a/lzwcai_temi_mcp/lzwcai_temi_mcp/mcp_mqtt.py b/lzwcai_temi_mcp/lzwcai_temi_mcp/mcp_mqtt.py new file mode 100644 index 0000000..8febfee --- /dev/null +++ b/lzwcai_temi_mcp/lzwcai_temi_mcp/mcp_mqtt.py @@ -0,0 +1,113 @@ +import paho.mqtt.client as mqtt +import json +import logging +import threading +from typing import Optional +import uuid +import threading +import requests +from os import getenv +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +MQTT_CLIENT_ID = f"MCPMQTT-{uuid.uuid4().hex[:8]}" + +def getConfig_url(array): + url = "http://lzwcai-demp-corp-manager:8086/system/config/getConfig" + data = [array] + try: + response = requests.post(url, json=data, timeout=5) + response.raise_for_status() + data = response.json()['data'] + return data[0]['configValue'] + except Exception as e: + print(f"Error fetching config for {array}: {e}") + return None + +class MQTTHandler: + def __init__(self): + self.client = mqtt.Client() + self.tasks = {} # {task_id: task_data} + self._lock = threading.Lock() + + # 获取MQTT配置,提供默认值 + self.mqtt_username = getenv('MQTT_USERNAME') or 'lzwc' + self.mqtt_password = getenv('MQTT_PASSWORD') or 'Lzwc@4187.' + mqtt_broker_raw = getenv('MQTT_BROKER') or 'emqx' + # 移除协议前缀,只保留主机名 + self.mqtt_broker = mqtt_broker_raw.replace('tcp://', '').replace('mqtt://', '') + mqtt_port_str = getenv('MQTT_PORT') + self.mqtt_port = int(mqtt_port_str) if mqtt_port_str else 1883 + + # 记录配置信息 + logger.info(f"MQTT配置 - Broker: {self.mqtt_broker}, Port: {self.mqtt_port}, Username: {self.mqtt_username}") + if not getenv('MQTT_BROKER'): + logger.warning("MQTT_BROKER环境变量未设置,使用默认值") + if not mqtt_port_str: + logger.warning("MQTT_PORT环境变量未设置,使用默认端口1883") + + self.client.username_pw_set(self.mqtt_username, self.mqtt_password) + self.client.on_connect = self._on_connect + self.client.on_message = self._on_message + self.client.on_disconnect = self._on_disconnect + try: + logger.info(f"正在连接 MQTT 代理: {self.mqtt_broker}:{self.mqtt_port}") + self.client.connect(self.mqtt_broker, self.mqtt_port, 60) + self.client.loop_start() + except Exception as e: + logger.error(f"连接 MQTT 失败: {e}") + + + def _on_connect(self, client, userdata, flags, rc): + """MQTT 连接回调""" + if rc == 0: + logger.info("已成功连接到 MQTT 代理服务器") + else: + logger.error(f"连接 MQTT 代理服务器失败,返回码: {rc}") + + def get_status(self, task_id: str = None): + """获取任务状态 + Args: + task_id: 可选参数,指定任务ID。如果为None则返回所有任务状态 + Returns: + 如果指定task_id则返回该任务的状态,否则返回所有任务状态 + """ + with self._lock: + if task_id: + return self.tasks.get(task_id, {}).copy() + return {k: v.copy() for k, v in self.tasks.items()} + + def _on_message(self, client, userdata, msg): + try: + payload = json.loads(msg.payload.decode()) + logger.info(f"[MQTT] topic={msg.topic} msg: {payload}") + + # task_id = payload.get('task_id') + # if task_id and task_id in self.tasks: + # with self._lock: + # self.tasks[task_id].update({ + # 'status': payload.get('status', 'UNKNOWN'), + # 'description': payload.get('description', '') + # }) + + except Exception as e: + logger.error(f"[错误] 处理消息失败: {e}") + + def _on_disconnect(self, client, userdata, rc): + """MQTT 断开连接回调""" + if rc != 0: + logger.warning("意外断开与 MQTT 代理服务器的连接。") + try: + logger.info("尝试重新连接 MQTT...") + client.reconnect() + except Exception as e: + logger.error(f"重新连接 MQTT 失败: {e}") + +# 单例模式 +_instance: Optional[MQTTHandler] = None + +def get_mcpmqtt_handler() -> MQTTHandler: + """获取单例""" + global _instance + if _instance is None: + _instance = MQTTHandler() + return _instance diff --git a/lzwcai_temi_mcp/lzwcai_temi_mcp/nav_server.py b/lzwcai_temi_mcp/lzwcai_temi_mcp/nav_server.py new file mode 100644 index 0000000..5c08c11 --- /dev/null +++ b/lzwcai_temi_mcp/lzwcai_temi_mcp/nav_server.py @@ -0,0 +1,102 @@ +from typing import Optional, Sequence, List, Dict, Any +import logging +import json +from .mcp_mqtt import get_mcpmqtt_handler + +logger = logging.getLogger(__name__) + +class NavServer: + def __init__(self, mmhandler=None): + self.mmhandler = mmhandler or get_mcpmqtt_handler() + + async def pub_cmd(self, device_id: str, action: str, params: Dict[str, Any]): + """ + 发送MQTT命令 + :param device_id: 设备ID + :param action: 动作指令 (对应 Kotlin 中的 action/cmd/type) + :param params: 其他参数 (将被合并到根 JSON 对象中) + """ + try: + payload = { + "device_id": device_id, + "action": action + } + if params: + payload.update(params) + + logger.info(f"Publishing command: {action}, payload: {payload}") + self.mmhandler.client.publish("robot/cmd", json.dumps(payload), qos=2) + return f"{action} 任务已经下达完成" + except Exception as e: + logger.error(f"Failed to publish command: {str(e)}", exc_info=True) + return f"Failed to publish command: {str(e)}" + + async def goto(self, location: str, flag: bool = False): + """轮足导航到指定位置""" + try: + if not location: + return "Location is not specified." + + # Kotlin 端对应 action: "goto" + # Kotlin 端参数: location (or target) + params = { + "location": location + } + return await self.pub_cmd("temi-test", "goto", params) + except Exception as e: + logger.error(f"Failed to call navigation mcp-tool: {str(e)} ", exc_info=True) + return f"Failed to call navigation mcp-tool: {str(e)}" + + async def speak(self, speech: str): + """轮足机器人语音播报""" + try: + if not speech: + return "Speech content is not specified." + + params = { + "text": speech, + "lang": "zh" + } + return await self.pub_cmd("temi-test", "speak", params) + except Exception as e: + logger.error(f"Failed to call speak mcp-tool: {str(e)} ", exc_info=True) + return f"Failed to call speak mcp-tool: {str(e)}" + + async def reception(self, location: str, name: str = "贵宾"): + """轮足机器人移动到指定位置迎宾""" + try: + if not location: + return "Location is not specified." + + params = { + "location": location, + "text": f"您好,{name},我是接待机器人。", + } + return await self.pub_cmd("temi-test", "reception", params) + except Exception as e: + logger.error(f"Failed to call reception mcp-tool: {str(e)} ", exc_info=True) + return f"Failed to call reception mcp-tool: {str(e)}" + + async def repose(self): + """轮足机器人重新定位""" + try: + params = {} + return await self.pub_cmd("temi-test", "repose", params) + except Exception as e: + logger.error(f"Failed to call repose mcp-tool: {str(e)} ", exc_info=True) + return f"Failed to call repose mcp-tool: {str(e)}" + + async def patrol(self, locations: list): + """轮足机器人巡逻""" + try: + if not locations: + return "locations is not specified." + params = { + "flag": False, + "locations": locations + } + return await self.pub_cmd("temi-test", "patrol", params) + except Exception as e: + logger.error(f"Failed to call patrol mcp-tool: {str(e)} ", exc_info=True) + return f"Failed to call patrol mcp-tool: {str(e)}" + diff --git a/lzwcai_temi_mcp/mcp-server.json b/lzwcai_temi_mcp/mcp-server.json new file mode 100644 index 0000000..a1116c3 --- /dev/null +++ b/lzwcai_temi_mcp/mcp-server.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "lzwcai_temi_mcp": { + "disabled": false, + "type": "stdio", + "timeout": 30, + "command": "uvx", + "args": [ + "lzwcai_temi_mcp" + ] + } + } +} diff --git a/lzwcai_temi_mcp/pyproject.toml b/lzwcai_temi_mcp/pyproject.toml new file mode 100644 index 0000000..1fce1f7 --- /dev/null +++ b/lzwcai_temi_mcp/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "lzwcai_temi_mcp" +version = "0.1.1" +description = "MQTT-based navigation server for robot" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.95.0", + "uvicorn>=0.21.0", + "paho-mqtt>=2.0.0", + "pydantic>=1.10.0", + "python-dotenv>=0.21.0", + "mcp[cli]>=1.6.0", + "requests>=2.25.0" +] + +[project.scripts] +lzwcai_temi_mcp = "lzwcai_temi_mcp.main:main" + + +[tool.hatch.build.targets.wheel] +packages = ["lzwcai_temi_mcp"] +