feat: 新增轮足机器人导航MCP服务器

添加 lzwcai_temi_mcp 包,实现基于MQTT的轮足机器人导航控制服务器。包含以下功能:
- 通过 MQTT 发布机器人控制命令
- 提供 goto、speak、reception、repose、patrol 等导航工具
- 支持通过 MCP 协议与AI助手集成
- 更新 lzwcai_lark_mcp 版本至 0.1.11
This commit is contained in:
bin
2026-03-15 18:36:34 +08:00
parent 7daa8e46c2
commit 3d04166314
9 changed files with 412 additions and 6 deletions

View File

View File

@@ -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()

View File

@@ -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

View File

@@ -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)}"

View File

@@ -0,0 +1,13 @@
{
"mcpServers": {
"lzwcai_temi_mcp": {
"disabled": false,
"type": "stdio",
"timeout": 30,
"command": "uvx",
"args": [
"lzwcai_temi_mcp"
]
}
}
}

View File

@@ -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"]