Forráskód Böngészése

创建mqtt 模拟程序,控制程序增加mqtt event 回掉

hwt 4 napja
szülő
commit
4731677375

+ 506 - 0
brain/PlannerNode2/MQTT协议.md

@@ -0,0 +1,506 @@
+# MQTT 通信协议文档
+
+## 1. 概述
+
+PlannerNode2 机器人决策系统使用 MQTT 协议实现智能体与外部业务模块(屏幕、机械臂、人脸识别等)之间的通信。
+
+**依赖安装**:
+```bash
+pip install paho-mqtt
+```
+
+---
+
+## 2. Topic 定义
+
+### 2.1 发布 (Publish) Topic
+
+| Topic 名称 | 方向 | 说明 |
+|-----------|------|------|
+| `/ai/agent/command` | 智能体 → 外部模块 | 发布命令消息 |
+
+### 2.2 订阅 (Subscribe) Topic
+
+| Topic 名称 | 方向 | 说明 |
+|-----------|------|------|
+| `/ai/module/status` | 外部模块 → 智能体 | 接收外部模块状态更新 |
+| `/ai/module/event` | 外部模块 → 智能体 | 接收外部模块事件通知(如人脸识别结果) |
+
+---
+
+## 3. 消息格式
+
+### 3.1 命令消息结构 (Command Message)
+
+所有通过 MQTT 发送的命令消息统一使用以下 JSON 格式:
+
+```json
+{
+  "msg_id": "agent_<timestamp_ms>",
+  "source": "agent",
+  "target": "<目标模块>",
+  "module": "<模块类型>",
+  "action": "<动作名称>",
+  "page": "<页面标识>",
+  "payload": {
+    // 额外参数
+  },
+  "timestamp": <unix_timestamp>
+}
+```
+
+### 3.2 字段说明
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `msg_id` | string | 是 | 消息唯一 ID,格式:`agent_<毫秒时间戳>` |
+| `source` | string | 是 | 消息来源,固定为 `"agent"` |
+| `target` | string | 是 | 目标模块(见下方模块列表) |
+| `module` | string | 是 | 模块类型 |
+| `action` | string | 是 | 动作名称 |
+| `page` | string | 否 | 页面标识,用于 UI 相关操作 |
+| `payload` | object | 否 | 额外参数 |
+| `timestamp` | int | 是 | Unix 时间戳(秒) |
+
+---
+
+## 4. 目标模块 (target)
+
+| target 值 | 说明 | 接收方 |
+|-----------|------|--------|
+| `all` | 所有模块(广播) | 全系统 |
+| `screen` | 屏幕/UI 模块 | 屏幕控制系统 |
+| `face` | 人脸识别模块 | 人脸识别服务 |
+| `nav` | 导航模块 | 导航控制系统 |
+| `visitor` | 访客管理模块 | 访客登记系统 |
+| `customer_service` | 客服模块 | 客服呼叫系统 |
+| `advertisement` | 广告模块 | 广告播放系统 |
+| `arm` | 机械臂模块 | 机械臂控制系统 |
+
+---
+
+## 5. 已实现的业务函数
+
+### 5.1 global_pause - 全局暂停
+
+**功能**:通知所有外部模块暂停当前业务。
+
+**使用场景**:
+- 机器人被唤醒时
+- 进入语音交互前
+- 欢迎语播放前
+- 业务页面切换时
+
+**调用方式**:
+```python
+self.mqtt_client.publish_command(
+    target="all",
+    module="system",
+    action="pause",
+    page="",
+    payload={
+        "reason": "wakeup",
+        "source_behavior": "agent_wakeup"
+    }
+)
+```
+
+**示例消息**:
+```json
+{
+  "msg_id": "agent_1747734123000",
+  "source": "agent",
+  "target": "all",
+  "module": "system",
+  "action": "pause",
+  "page": "",
+  "payload": {
+    "reason": "wakeup",
+    "source_behavior": "agent_wakeup"
+  },
+  "timestamp": 1747734123
+}
+```
+
+---
+
+### 5.2 open_visitor_register_page - 访客登记页面
+
+**功能**:唤醒访客登记页面。
+
+**调用方式**:
+```python
+self.mqtt_client.publish_command(
+    target="screen",
+    module="visitor",
+    action="open_page",
+    page="visitor_register",
+    payload={
+        "title": "访客登记",
+        "input_mode": "touch_and_voice"
+    }
+)
+```
+
+**示例消息**:
+```json
+{
+  "msg_id": "agent_1747734123001",
+  "source": "agent",
+  "target": "screen",
+  "module": "visitor",
+  "action": "open_page",
+  "page": "visitor_register",
+  "payload": {
+    "title": "访客登记",
+    "input_mode": "touch_and_voice"
+  },
+  "timestamp": 1747734123
+}
+```
+
+---
+
+### 5.3 open_appointment_confirm_page - 预约确认页面
+
+**功能**:唤醒预约确认页面。
+
+**调用方式**:
+```python
+self.mqtt_client.publish_command(
+    target="screen",
+    module="visitor",
+    action="open_page",
+    page="appointment_confirm",
+    payload={
+        "title": "预约确认",
+        "input_mode": "touch"
+    }
+)
+```
+
+**示例消息**:
+```json
+{
+  "msg_id": "agent_1747734123002",
+  "source": "agent",
+  "target": "screen",
+  "module": "visitor",
+  "action": "open_page",
+  "page": "appointment_confirm",
+  "payload": {
+    "title": "预约确认",
+    "input_mode": "touch"
+  },
+  "timestamp": 1747734123
+}
+```
+
+---
+
+### 5.4 open_navigation_page - 导航页面
+
+**功能**:唤醒导航页面。
+
+**调用方式**:
+```python
+self.mqtt_client.publish_command(
+    target="screen",
+    module="nav",
+    action="open_page",
+    page="navigation",
+    payload={
+        "title": "导航服务",
+        "mode": "select_destination"
+    }
+)
+```
+
+**示例消息**:
+```json
+{
+  "msg_id": "agent_1747734123003",
+  "source": "agent",
+  "target": "screen",
+  "module": "nav",
+  "action": "open_page",
+  "page": "navigation",
+  "payload": {
+    "title": "导航服务",
+    "mode": "select_destination"
+  },
+  "timestamp": 1747734123
+}
+```
+
+---
+
+### 5.5 open_customer_service_page - 呼叫客服页面
+
+**功能**:唤醒呼叫客服页面。
+
+**调用方式**:
+```python
+self.mqtt_client.publish_command(
+    target="screen",
+    module="customer_service",
+    action="open_page",
+    page="customer_service",
+    payload={
+        "title": "呼叫客服",
+        "mode": "call_service"
+    }
+)
+```
+
+**示例消息**:
+```json
+{
+  "msg_id": "agent_1747734123004",
+  "source": "agent",
+  "target": "screen",
+  "module": "customer_service",
+  "action": "open_page",
+  "page": "customer_service",
+  "payload": {
+    "title": "呼叫客服",
+    "mode": "call_service"
+  },
+  "timestamp": 1747734123
+}
+```
+
+---
+
+### 5.6 open_face_recognition_page - 人脸识别页面
+
+**功能**:唤醒人脸识别页面并启动识别。此函数发送两条 MQTT 消息。
+
+**调用方式**:
+```python
+# 第一条 - 打开页面
+self.mqtt_client.publish_command(
+    target="screen",
+    module="face",
+    action="open_page",
+    page="face_recognition",
+    payload={
+        "title": "人脸识别",
+        "tips": "请面向摄像头",
+        "mode": "whitelist"
+    }
+)
+
+# 第二条 - 启动识别
+self.mqtt_client.publish_command(
+    target="face",
+    module="face",
+    action="start",
+    page="",
+    payload={
+        "mode": "whitelist",
+        "timeout_ms": 10000,
+        "threshold": 0.65
+    }
+)
+```
+
+**示例消息 1(打开页面)**:
+```json
+{
+  "msg_id": "agent_1747734123005",
+  "source": "agent",
+  "target": "screen",
+  "module": "face",
+  "action": "open_page",
+  "page": "face_recognition",
+  "payload": {
+    "title": "人脸识别",
+    "tips": "请面向摄像头",
+    "mode": "whitelist"
+  },
+  "timestamp": 1747734123
+}
+```
+
+**示例消息 2(启动识别)**:
+```json
+{
+  "msg_id": "agent_1747734123006",
+  "source": "agent",
+  "target": "face",
+  "module": "face",
+  "action": "start",
+  "page": "",
+  "payload": {
+    "mode": "whitelist",
+    "timeout_ms": 10000,
+    "threshold": 0.65
+  },
+  "timestamp": 1747734123
+}
+```
+
+---
+
+## 6. 订阅消息处理
+
+### 6.1 状态消息 (status)
+
+订阅 `/ai/module/status` 接收外部模块的状态更新。
+
+**回调注册**:
+```python
+mqtt_client.on_status_callback = self.on_status_received
+
+def on_status_received(self, data):
+    # 处理状态数据
+    pass
+```
+
+### 6.2 事件消息 (event)
+
+订阅 `/ai/module/event` 接收外部模块的事件通知(如人脸识别结果)。
+
+**回调注册**:
+```python
+mqtt_client.on_event_callback = self.on_event_received
+
+def on_event_received(self, data):
+    # 处理事件数据
+    pass
+```
+
+---
+
+## 7. MQTT 配置格式
+
+MQTT 配置来自 `/ai/config` 中的 `mqtt` 字段:
+
+```json
+{
+  "mqtt": {
+    "enable": true,
+    "broker_host": "192.168.0.30",
+    "broker_port": 1883,
+    "username": "",
+    "password": "",
+    "client_id": "rdk_agent",
+    "keepalive": 60,
+    "command_topic": "/ai/agent/command",
+    "status_topic": "/ai/module/status",
+    "event_topic": "/ai/module/event"
+  }
+}
+```
+
+### 7.1 配置参数说明
+
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `enable` | bool | `false` | 是否启用 MQTT |
+| `broker_host` | string | `"127.0.0.1"` | MQTT Broker 地址 |
+| `broker_port` | int | `1883` | MQTT Broker 端口 |
+| `username` | string | `""` | 用户名(可选) |
+| `password` | string | `""` | 密码(可选) |
+| `client_id` | string | `"rdk_agent"` | 客户端唯一标识 |
+| `keepalive` | int | `60` | 保活时间(秒) |
+| `command_topic` | string | `/ai/agent/command` | 命令发布话题 |
+| `status_topic` | string | `/ai/module/status` | 状态订阅话题 |
+| `event_topic` | string | `/ai/module/event` | 事件订阅话题 |
+
+---
+
+## 8. 使用示例
+
+### 8.1 MQTT 客户端初始化
+
+```python
+from utils.mqttclient import MQTTClient
+
+# 创建 MQTT 客户端
+self.mqtt_client = MQTTClient(logger=self.get_logger())
+
+# 从配置初始化
+mqtt_config = {
+    "enable": True,
+    "broker_host": "192.168.0.30",
+    "broker_port": 1883,
+    "client_id": "rdk_agent"
+}
+self.mqtt_client.init(mqtt_config)
+
+# 注册回调
+self.mqtt_client.on_status_callback = self.on_status_received
+self.mqtt_client.on_event_callback = self.on_event_received
+```
+
+### 8.2 发送命令
+
+```python
+# 使用 publish_command 发送命令
+ok = self.mqtt_client.publish_command(
+    target="screen",
+    module="nav",
+    action="open_page",
+    page="navigation",
+    payload={
+        "title": "导航服务",
+        "mode": "select_destination"
+    }
+)
+
+if ok:
+    self.get_logger().info("命令发送成功")
+else:
+    self.get_logger().warn("命令发送失败")
+```
+
+### 8.3 扩展新的 MQTT 业务函数
+
+在 `CustomActionServer` 类中新增成员函数即可被大模型调用(通过 `hasattr` 判断)。
+
+```python
+def open_xxx_page(self):
+    """
+    打开XXX页面
+    """
+    try:
+        ok = self.mqtt_client.publish_command(
+            target="screen",
+            module="xxx",
+            action="open_page",
+            page="xxx_page",
+            payload={
+                "title": "XXX",
+                "mode": "xxx"
+            }
+        )
+        self.get_logger().info(
+            f"[MQTT业务] open_xxx_page sent, ok={ok}"
+        )
+    except Exception as e:
+        self.get_logger().warn(
+            f"[MQTT业务] open_xxx_page failed: {e}"
+        )
+```
+
+---
+
+## 9. 注意事项
+
+1. **MQTT 初始化时机**:MQTT 客户端在收到 `/ai/config` 配置后初始化,仅初始化一次,后续配置更新不会重新初始化。
+
+2. **发布失败不影响主流程**:所有业务函数内部已做异常捕获,MQTT 发布失败只会打印 warning 日志,不影响原有 ROS2 逻辑。
+
+3. **日志前缀**:所有 MQTT 业务日志使用 `[MQTT业务]` 前缀,方便排查。
+
+4. **唤醒时自动暂停**:`wakeup_callback` 收到唤醒信号后会自动调用 `global_pause("wakeup")`,通知全车暂停。
+
+---
+
+## 10. 更新日志
+
+| 日期 | 版本 | 更新内容 |
+|------|------|---------|
+| 2026-05-20 | 1.0 | 初始版本,支持 MQTT 客户端模块及基础业务函数 |

+ 89 - 2
brain/PlannerNode2/README.md

@@ -11,6 +11,7 @@ PlannerNode2/
 ├── config_node/          # 配置管理节点
 ├── environment_node/     # 环境感知节点
 ├── nav_simulator/        # 导航模拟节点
+├── mqtt_simulator/      # MQTT 模拟器节点
 ├── largemodel/           # 大模型服务节点
 ├── README.md             # 本文档
 ```
@@ -199,7 +200,78 @@ B:
 
 ---
 
-### 4. largemodel (大模型服务节点)
+### 4. mqtt_simulator (MQTT 模拟器节点)
+
+**功能说明**
+
+- 订阅 `/ai/agent/command` 话题,接收大模型发出的 MQTT 命令
+- 模拟外部模块(屏幕、人脸识别、导航等)的响应行为
+- 向 `/ai/module/status` 和 `/ai/module/event` 话题发布模拟回复
+- 用于在没有真实外部模块时测试大模型的指令下发功能
+
+**订阅话题**
+
+| 话题名称 | 类型 | 说明 |
+|---------|------|------|
+| `/ai/agent/command` | `std_msgs/String` | 大模型发送的命令 (MQTT) |
+
+**发布话题**
+
+| 话题名称 | 类型 | 说明 |
+|---------|------|------|
+| `/ai/module/status` | `std_msgs/String` | 状态消息 (JSON 格式) |
+| `/ai/module/event` | `std_msgs/String` | 事件消息 (JSON 格式) |
+
+**模拟的命令类型**
+
+| action | 说明 | 延迟范围 | 回复类型 |
+|--------|------|---------|---------|
+| `open_page` | 打开页面 | 1-2 秒 | status: page_opened |
+| `start` (face) | 人脸识别 | 2-4 秒 | event: face_recognition_result |
+| `navigate` | 导航操作 | 1-2 秒 | status: navigation_completed |
+| `call` | 呼叫客服 | 1-2 秒 | status: call_connected |
+| `register` | 访客登记 | 2-3 秒 | status: registration_completed |
+| `pause` | 全局暂停 | 无延迟 | 仅记录日志 |
+
+**配置文件**
+
+`config/simulator.yaml` - MQTT 连接和模拟参数配置
+
+```yaml
+mqtt:
+  broker_host: "192.168.0.30"
+  broker_port: 1883
+  username: "gbd01"
+  password: "gbd2025!"
+
+topics:
+  command_topic: "/ai/agent/command"
+  status_topic: "/ai/module/status"
+  event_topic: "/ai/module/event"
+
+delay:
+  page_load: [1.0, 2.0]          # 页面加载延迟
+  face_recognition: [2.0, 4.0]   # 人脸识别延迟
+
+results:
+  always_success: true            # 始终返回成功
+```
+
+**启动方式**
+
+```bash
+ros2 launch mqtt_simulator mqtt_simulator.launch.py
+```
+
+**参数说明**
+
+| 参数名 | 类型 | 默认值 | 说明 |
+|-------|------|--------|------|
+| `config_file` | string | `config/simulator.yaml` | 配置文件路径 |
+
+---
+
+### 5. largemodel (大模型服务节点)
 
 **功能说明**
 
@@ -228,8 +300,11 @@ ros2 launch environment_node environment.launch.py
 # 启动 nav_simulator
 ros2 launch nav_simulator nav_simulator.launch.py
 
+# 启动 mqtt_simulator (MQTT 模拟器)
+ros2 launch mqtt_simulator mqtt_simulator.launch.py
+
 # 启动 largemodel
-ros2 launch largemodel largemodel_control.launch.py 
+ros2 launch largemodel largemodel_control.launch.py
 ```
 
 ### 测试环境数据发布
@@ -253,6 +328,16 @@ ros2 topic echo /ai/config
 ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "{pose: {header: {frame_id: 'map'}, pose: {position: {x: 1.633, y: 3.490, z: 0.0}, orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}}}}"
 ```
 
+### 测试 MQTT 模拟器
+
+```bash
+# 查看模拟器发布的状态消息
+ros2 topic echo /ai/module/status
+
+# 查看模拟器发布的事件消息
+ros2 topic echo /ai/module/event
+```
+
 ---
 
 ## 依赖说明
@@ -265,6 +350,7 @@ ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "{pose:
 - geometry_msgs
 - tf2_ros
 - ament_index_python
+- paho-mqtt (MQTT 客户端库)
 
 ---
 
@@ -280,6 +366,7 @@ sunrise
 |------|------|---------|
 | 2026-05-12 | 1.0.0 | 初始版本,包含 config_node、environment_node、nav_simulator |
 | 2026-05-20 | 1.1.0 | 新增 MQTT 客户端模块及业务函数,支持与外部模块通信 |
+| 2026-05-22 | 1.2.0 | 新增 mqtt_simulator 节点,模拟外部模块响应大模型的 MQTT 命令 |
 
 ---
 

+ 44 - 21
brain/PlannerNode2/largemodel/largemodel/action_service.py

@@ -297,7 +297,10 @@ class CustomActionServer(Node):
             if mqtt_cfg:
                 # 初始化或更新 MQTT 客户端
                 self.mqtt_client.init(mqtt_cfg)
-                self.get_logger().debug('[MQTT] 配置已加载')
+                # 绑定 MQTT 消息回调
+                self.mqtt_client.on_status_callback = self._on_mqtt_status_received
+                self.mqtt_client.on_event_callback = self._on_mqtt_event_received
+                self.get_logger().debug('[MQTT] 配置已加载,回调已绑定')
         except Exception as e:
             self.get_logger().warn(f'解析配置数据失败: {e}')
 
@@ -314,6 +317,34 @@ class CustomActionServer(Node):
         self.init_language()
         self.get_logger().info('[配置] 一次性初始化完成')
 
+    def _on_mqtt_status_received(self, payload):
+        """
+        MQTT 状态消息回调
+
+        收到外部模块的状态更新后打印日志
+        """
+        self.get_logger().info(f"[MQTT状态] 收到状态消息: {payload}")
+
+        # 根据 status 字段打印成功日志
+        status = payload.get('status', '')
+        if status == 'page_opened':
+            page = payload.get('page', '')
+            self.get_logger().info(f"[MQTT业务] {page} 页面打开成功")
+
+    def _on_mqtt_event_received(self, payload):
+        """
+        MQTT 事件消息回调
+
+        收到外部模块的事件通知后打印日志
+        """
+        self.get_logger().info(f"[MQTT事件] 收到事件消息: {payload}")
+
+        # 根据 event 字段打印成功日志
+        event = payload.get('event', '')
+        if event == 'face_recognition_result':
+            success = payload.get('data', {}).get('success', False)
+            self.get_logger().info(f"[MQTT业务] 人脸识别完成, success={success}")
+
     def _schedule_config_init(self):
         """
         防抖:延迟执行初始化,确保短时间内多次配置只执行一次
@@ -1667,9 +1698,6 @@ class CustomActionServer(Node):
                     "source_behavior": "agent_wakeup"
                 }
             )
-            self.get_logger().info(
-                f"[MQTT业务] global_pause sent, reason={reason}, ok={ok}"
-            )
         except Exception as e:
             self.get_logger().warn(f"[MQTT业务] global_pause failed: {e}")
 
@@ -1688,13 +1716,13 @@ class CustomActionServer(Node):
                     "input_mode": "touch_and_voice"
                 }
             )
-            self.get_logger().info(
-                f"[MQTT业务] open_visitor_register_page sent, ok={ok}"
-            )
         except Exception as e:
             self.get_logger().warn(
                 f"[MQTT业务] open_visitor_register_page failed: {e}"
             )
+            self.get_logger().warn(
+                f"[MQTT业务] open_visitor_register_page failed: {e}"
+            )
 
     def open_appointment_confirm_page(self):
         """
@@ -1711,13 +1739,13 @@ class CustomActionServer(Node):
                     "input_mode": "touch"
                 }
             )
-            self.get_logger().info(
-                f"[MQTT业务] open_appointment_confirm_page sent, ok={ok}"
-            )
         except Exception as e:
             self.get_logger().warn(
                 f"[MQTT业务] open_appointment_confirm_page failed: {e}"
             )
+            self.get_logger().warn(
+                f"[MQTT业务] open_appointment_confirm_page failed: {e}"
+            )
 
     def open_navigation_page(self):
         """
@@ -1734,13 +1762,13 @@ class CustomActionServer(Node):
                     "mode": "select_destination"
                 }
             )
-            self.get_logger().info(
-                f"[MQTT业务] open_navigation_page sent, ok={ok}"
-            )
         except Exception as e:
             self.get_logger().warn(
                 f"[MQTT业务] open_navigation_page failed: {e}"
             )
+            self.get_logger().warn(
+                f"[MQTT业务] open_navigation_page failed: {e}"
+            )
 
     def open_customer_service_page(self):
         """
@@ -1757,13 +1785,13 @@ class CustomActionServer(Node):
                     "mode": "call_service"
                 }
             )
-            self.get_logger().info(
-                f"[MQTT业务] open_customer_service_page sent, ok={ok}"
-            )
         except Exception as e:
             self.get_logger().warn(
                 f"[MQTT业务] open_customer_service_page failed: {e}"
             )
+            self.get_logger().warn(
+                f"[MQTT业务] open_customer_service_page failed: {e}"
+            )
 
     def open_face_recognition_page(self):
         """
@@ -1793,11 +1821,6 @@ class CustomActionServer(Node):
                     "threshold": 0.65
                 }
             )
-
-            self.get_logger().info(
-                f"[MQTT业务] open_face_recognition_page sent, ok_page={ok_page}, ok_start={ok_start}"
-            )
-
         except Exception as e:
             self.get_logger().warn(
                 f"[MQTT业务] open_face_recognition_page failed: {e}"

+ 13 - 0
brain/PlannerNode2/largemodel/utils/promot.py

@@ -235,6 +235,19 @@ action_function_library='''
 - **取消模式**:`set_robot_mode(mode.NONE)`
   - 说明:取消所有正在执行的任务模式。
 ## 业务函数
+- **呼出导航页面**:`open_navigation_page()`
+  - 说明:当用户意图导航但目标点不明确时(如说"带我去"但没说去哪),呼出导航页面让用户手动选择目的地。
+  - 相近语义:导航去、带我去、我想去、帮我导航。
+- **呼出人脸识别页面**:`open_face_recognition_page()`
+  - 说明:打开人脸识别页面并启动人脸识别模块,用于门岗模式下人员身份核验。
+  - 使用场景:门岗模式下人员靠近时。
+- **呼出访客登记页面**:`open_visitor_register_page()`
+  - 说明:打开访客登记页面,供未识别身份的访客进行信息登记。
+  - 使用场景:人脸识别失败,用户选择"访客登记"。
+- **呼出预约确认页面**:`open_appointment_confirm_page()`
+  - 说明:打开预约确认页面,供有预约的访客确认预约信息。
+  - 使用场景:人脸识别失败,用户选择"有预约"。
+
 
 - **结束当前任务周期**:`finish_dialogue()`  
   - 说明:清空上下文,结束任务(如用户指令“退下”“休息”)。  

+ 50 - 0
brain/PlannerNode2/mqtt_simulator/config/simulator.yaml

@@ -0,0 +1,50 @@
+# MQTT 模拟器配置文件
+# 此文件用于配置模拟器的 MQTT 连接和模拟行为
+
+mqtt:
+  # Broker 地址
+  broker_host: "192.168.0.30"
+  # Broker 端口
+  broker_port: 1883
+  # 客户端 ID
+  client_id: "mqtt_simulator"
+  # 用户名(可选,留空表示不需要认证)
+  username: "gbd01"
+  # 密码(可选)
+  password: "gbd2025!"
+  # 保活时间(秒)
+  keepalive: 60
+
+topics:
+  # 订阅的命令话题(大模型发送命令到此话题)
+  command_topic: "/ai/agent/command"
+  # 发布状态回复的话题
+  status_topic: "/ai/module/status"
+  # 发布事件回复的话题
+  event_topic: "/ai/module/event"
+
+simulator:
+  # 是否启用模拟器
+  enable: true
+  # 是否启用日志输出
+  log_all_messages: true
+  # 是否启用随机延迟(替代固定延迟)
+  enable_random_delay: true
+
+delay:
+  # 页面加载延迟范围 [最小, 最大]
+  page_load: [1.0, 2.0]
+  # 人脸识别延迟范围 [最小, 最大]
+  face_recognition: [2.0, 4.0]
+  # 导航操作延迟范围 [最小, 最大]
+  navigation: [1.0, 2.0]
+  # 客服呼叫延迟范围 [最小, 最大]
+  customer_service: [1.0, 2.0]
+  # 访客登记延迟范围 [最小, 最大]
+  visitor_register: [2.0, 3.0]
+
+results:
+  # 人脸识别成功率 (0.0 - 1.0),后期可修改为随机失败
+  face_recognition_success_rate: 1.0
+  # 是否始终返回成功(当前配置为始终成功)
+  always_success: true

+ 37 - 0
brain/PlannerNode2/mqtt_simulator/launch/mqtt_simulator.launch.py

@@ -0,0 +1,37 @@
+"""
+MQTT 模拟器节点启动文件
+
+使用方法:
+    ros2 launch mqtt_simulator mqtt_simulator.launch.py
+"""
+
+from launch import LaunchDescription
+from launch_ros.actions import Node
+from ament_index_python.packages import get_package_share_directory
+import os
+
+
+def generate_launch_description():
+    """
+    生成启动描述
+    """
+    # 获取包目录和配置文件路径
+    pkg_share = get_package_share_directory('mqtt_simulator')
+    config_file = os.path.join(pkg_share, 'config', 'simulator.yaml')
+
+    # 创建节点
+    mqtt_simulator_node = Node(
+        package='mqtt_simulator',
+        executable='mqtt_simulator',
+        name='mqtt_simulator_node',
+        output='screen',
+        parameters=[{
+            'config_file': config_file,
+        }],
+        remappings=[],
+    )
+
+    # 返回启动描述
+    return LaunchDescription([
+        mqtt_simulator_node
+    ])

+ 5 - 0
brain/PlannerNode2/mqtt_simulator/mqtt_simulator/__init__.py

@@ -0,0 +1,5 @@
+"""
+MQTT Simulator 模拟器模块
+"""
+
+__version__ = '1.0.0'

+ 476 - 0
brain/PlannerNode2/mqtt_simulator/mqtt_simulator/mqtt_simulator_node.py

@@ -0,0 +1,476 @@
+"""
+MQTT 模拟器节点
+模拟外部模块响应大模型发出的 MQTT 命令
+
+功能:
+- 订阅 /ai/agent/command 话题(MQTT),接收大模型的命令
+- 根据命令类型模拟处理延迟
+- 向 /ai/module/status 和 /ai/module/event 话题(MQTT)发布模拟回复
+
+Author: sunrise
+"""
+
+import rclpy
+from rclpy.node import Node
+import paho.mqtt.client as mqtt
+import json
+import time
+import random
+import threading
+from ament_index_python.packages import get_package_share_directory
+import os
+import yaml
+
+
+class MQTTSimulatorNode(Node):
+    """
+    MQTT 模拟器节点
+
+    订阅大模型发出的 MQTT 命令,模拟外部模块的响应行为
+    """
+
+    def __init__(self):
+        super().__init__('mqtt_simulator_node')
+
+        # ========== 声明参数 ==========
+        self.declare_parameter('config_file', '')
+        self.declare_parameter('mqtt.broker_host', '127.0.0.1')
+        self.declare_parameter('mqtt.broker_port', 1883)
+        self.declare_parameter('mqtt.client_id', 'mqtt_simulator')
+        self.declare_parameter('mqtt.username', '')
+        self.declare_parameter('mqtt.password', '')
+        self.declare_parameter('mqtt.keepalive', 60)
+        self.declare_parameter('topics.command_topic', '/ai/agent/command')
+        self.declare_parameter('topics.status_topic', '/ai/module/status')
+        self.declare_parameter('topics.event_topic', '/ai/module/event')
+        self.declare_parameter('simulator.enable', True)
+        self.declare_parameter('simulator.log_all_messages', True)
+        self.declare_parameter('simulator.enable_random_delay', True)
+        self.declare_parameter('delay.page_load', [1.0, 2.0])
+        self.declare_parameter('delay.face_recognition', [2.0, 4.0])
+        self.declare_parameter('delay.navigation', [1.0, 2.0])
+        self.declare_parameter('delay.customer_service', [1.0, 2.0])
+        self.declare_parameter('delay.visitor_register', [2.0, 3.0])
+        self.declare_parameter('results.face_recognition_success_rate', 1.0)
+        self.declare_parameter('results.always_success', True)
+
+        # ========== 加载配置文件 ==========
+        self._load_config()
+
+        # MQTT 客户端
+        self.mqtt_client = None
+        self.mqtt_connected = False
+
+        # 日志输出
+        self.get_logger().info('MQTT Simulator Node 初始化完成')
+        self.get_logger().info(f'Broker: {self.broker_host}:{self.broker_port}')
+        self.get_logger().info(f'订阅话题: {self.command_topic}')
+        self.get_logger().info(f'发布话题: {self.status_topic}, {self.event_topic}')
+
+        # 如果启用,初始化 MQTT 连接
+        if self.enable:
+            self.init_mqtt()
+        else:
+            self.get_logger().warn('模拟器已禁用')
+
+    def _load_config(self):
+        """
+        从 YAML 配置文件加载参数
+        """
+        config_file = self.get_parameter('config_file').value
+        if not config_file:
+            pkg_share = get_package_share_directory('mqtt_simulator')
+            config_file = os.path.join(pkg_share, 'config', 'simulator.yaml')
+
+        self.get_logger().info(f'加载配置文件: {config_file}')
+
+        if os.path.exists(config_file):
+            try:
+                with open(config_file, 'r') as f:
+                    yaml_config = yaml.safe_load(f)
+
+                mqtt_cfg = yaml_config.get('mqtt', {})
+                topics_cfg = yaml_config.get('topics', {})
+                sim_cfg = yaml_config.get('simulator', {})
+                delay_cfg = yaml_config.get('delay', {})
+                results_cfg = yaml_config.get('results', {})
+
+                self.broker_host = mqtt_cfg.get('broker_host', '127.0.0.1')
+                self.broker_port = mqtt_cfg.get('broker_port', 1883)
+                self.client_id = mqtt_cfg.get('client_id', 'mqtt_simulator')
+                self.username = mqtt_cfg.get('username', '')
+                self.password = mqtt_cfg.get('password', '')
+                self.keepalive = mqtt_cfg.get('keepalive', 60)
+                self.command_topic = topics_cfg.get('command_topic', '/ai/agent/command')
+                self.status_topic = topics_cfg.get('status_topic', '/ai/module/status')
+                self.event_topic = topics_cfg.get('event_topic', '/ai/module/event')
+                self.enable = sim_cfg.get('enable', True)
+                self.log_all_messages = sim_cfg.get('log_all_messages', True)
+                self.enable_random_delay = sim_cfg.get('enable_random_delay', True)
+                self.page_load_delay = delay_cfg.get('page_load', [1.0, 2.0])
+                self.face_recognition_delay = delay_cfg.get('face_recognition', [2.0, 4.0])
+                self.navigation_delay = delay_cfg.get('navigation', [1.0, 2.0])
+                self.customer_service_delay = delay_cfg.get('customer_service', [1.0, 2.0])
+                self.visitor_register_delay = delay_cfg.get('visitor_register', [2.0, 3.0])
+                self.face_success_rate = results_cfg.get('face_recognition_success_rate', 1.0)
+                self.always_success = results_cfg.get('always_success', True)
+
+                self.get_logger().info('配置文件加载成功')
+
+            except Exception as e:
+                self.get_logger().warn(f'配置文件加载失败: {e},使用默认值')
+                self._use_default_config()
+        else:
+            self.get_logger().warn(f'配置文件不存在: {config_file},使用默认值')
+            self._use_default_config()
+
+    def _use_default_config(self):
+        """
+        使用默认配置
+        """
+        self.broker_host = self.get_parameter('mqtt.broker_host').value
+        self.broker_port = self.get_parameter('mqtt.broker_port').value
+        self.client_id = self.get_parameter('mqtt.client_id').value
+        self.username = self.get_parameter('mqtt.username').value
+        self.password = self.get_parameter('mqtt.password').value
+        self.keepalive = self.get_parameter('mqtt.keepalive').value
+        self.command_topic = self.get_parameter('topics.command_topic').value
+        self.status_topic = self.get_parameter('topics.status_topic').value
+        self.event_topic = self.get_parameter('topics.event_topic').value
+        self.enable = self.get_parameter('simulator.enable').value
+        self.log_all_messages = self.get_parameter('simulator.log_all_messages').value
+        self.enable_random_delay = self.get_parameter('simulator.enable_random_delay').value
+        self.page_load_delay = self.get_parameter('delay.page_load').value
+        self.face_recognition_delay = self.get_parameter('delay.face_recognition').value
+        self.navigation_delay = self.get_parameter('delay.navigation').value
+        self.customer_service_delay = self.get_parameter('delay.customer_service').value
+        self.visitor_register_delay = self.get_parameter('delay.visitor_register').value
+        self.face_success_rate = self.get_parameter('results.face_recognition_success_rate').value
+        self.always_success = self.get_parameter('results.always_success').value
+
+    def init_mqtt(self):
+        """
+        初始化 MQTT 连接
+        """
+        try:
+            self.mqtt_client = mqtt.Client(client_id=self.client_id)
+
+            if self.username:
+                self.mqtt_client.username_pw_set(self.username, self.password)
+
+            self.mqtt_client.on_connect = self._on_connect
+            self.mqtt_client.on_disconnect = self._on_disconnect
+            self.mqtt_client.on_message = self._on_mqtt_message
+
+            self.get_logger().info(f'正在连接到 MQTT Broker: {self.broker_host}:{self.broker_port}')
+            self.mqtt_client.connect(self.broker_host, self.broker_port, self.keepalive)
+            self.mqtt_client.loop_start()
+
+        except Exception as e:
+            self.get_logger().error(f'MQTT 初始化失败: {e}')
+
+    def _on_connect(self, client, userdata, flags, rc):
+        """
+        MQTT 连接回调
+        """
+        if rc == 0:
+            self.mqtt_connected = True
+            self.get_logger().info('MQTT 连接成功')
+
+            try:
+                client.subscribe(self.command_topic)
+                self.get_logger().info(f'已订阅话题: {self.command_topic}')
+            except Exception as e:
+                self.get_logger().error(f'订阅话题失败: {e}')
+        else:
+            self.mqtt_connected = False
+            self.get_logger().error(f'MQTT 连接失败, rc={rc}')
+
+    def _on_disconnect(self, client, userdata, rc):
+        """
+        MQTT 断开连接回调
+        """
+        self.mqtt_connected = False
+        if rc != 0:
+            self.get_logger().warn(f'MQTT 意外断开, rc={rc}')
+        else:
+            self.get_logger().info('MQTT 连接已断开')
+
+    def _on_mqtt_message(self, client, userdata, msg):
+        """
+        MQTT 消息回调
+        """
+        try:
+            topic = msg.topic
+            payload_str = msg.payload.decode('utf-8')
+
+            if self.log_all_messages:
+                self.get_logger().info(f'收到 MQTT 消息 [{topic}]: {payload_str}')
+
+            data = json.loads(payload_str)
+
+            thread = threading.Thread(target=self._handle_command, args=(data,))
+            thread.daemon = True
+            thread.start()
+
+        except json.JSONDecodeError as e:
+            self.get_logger().warn(f'JSON 解析失败: {e}')
+        except Exception as e:
+            self.get_logger().error(f'处理消息异常: {e}')
+
+    def _get_delay(self, delay_config):
+        """
+        获取延迟时间
+
+        Args:
+            delay_config: 延迟配置 [min, max]
+
+        Returns:
+            float: 延迟时间(秒)
+        """
+        if self.enable_random_delay and isinstance(delay_config, list) and len(delay_config) == 2:
+            return random.uniform(delay_config[0], delay_config[1])
+        elif isinstance(delay_config, list) and len(delay_config) == 2:
+            return (delay_config[0] + delay_config[1]) / 2
+        else:
+            return float(delay_config)
+
+    def _handle_command(self, data):
+        """
+        处理命令(在新线程中执行)
+
+        Args:
+            data: 命令数据字典
+        """
+        try:
+            target = data.get('target', '')
+            module = data.get('module', '')
+            action = data.get('action', '')
+            page = data.get('page', '')
+            original_msg_id = data.get('msg_id', '')
+
+            self.get_logger().info(f'处理命令: target={target}, module={module}, action={action}, page={page}')
+
+            if action == 'open_page':
+                self._handle_open_page(target, module, page, original_msg_id)
+            elif action == 'start':
+                self._handle_start(target, module, original_msg_id, data.get('payload', {}))
+            elif action == 'pause':
+                self._handle_pause(original_msg_id)
+            elif action == 'navigate':
+                self._handle_navigate(target, original_msg_id, data.get('payload', {}))
+            elif action == 'call':
+                self._handle_call_service(original_msg_id)
+            elif action == 'register':
+                self._handle_visitor_register(original_msg_id)
+            else:
+                self.get_logger().warn(f'未知动作类型: {action}')
+
+        except Exception as e:
+            self.get_logger().error(f'处理命令异常: {e}')
+
+    def _handle_open_page(self, target, module, page, original_msg_id):
+        """
+        处理打开页面命令
+        """
+        delay = self._get_delay(self.page_load_delay)
+        self.get_logger().info(f'模拟页面加载,延迟 {delay:.2f} 秒')
+        time.sleep(delay)
+
+        status_msg = {
+            'msg_id': f'sim_{int(time.time() * 1000)}',
+            'source': 'simulator',
+            'target': 'agent',
+            'module': module,
+            'status': 'page_opened',
+            'page': page,
+            'original_msg_id': original_msg_id,
+            'timestamp': int(time.time())
+        }
+
+        self._publish_mqtt(self.status_topic, status_msg)
+        self.get_logger().info(f'已回复页面打开状态: {page}')
+
+    def _handle_start(self, target, module, original_msg_id, payload):
+        """
+        处理启动命令(主要用于人脸识别)
+        """
+        if target == 'face':
+            self._handle_face_recognition(original_msg_id, payload)
+        else:
+            self.get_logger().warn(f'未知启动目标: {target}')
+
+    def _handle_face_recognition(self, original_msg_id, payload):
+        """
+        处理人脸识别
+        """
+        delay = self._get_delay(self.face_recognition_delay)
+        self.get_logger().info(f'模拟人脸识别,延迟 {delay:.2f} 秒')
+        time.sleep(delay)
+
+        if self.always_success:
+            success = True
+        else:
+            success = random.random() < self.face_success_rate
+
+        if success:
+            event_data = {
+                'msg_id': f'sim_{int(time.time() * 1000)}',
+                'source': 'simulator',
+                'event': 'face_recognition_result',
+                'original_msg_id': original_msg_id,
+                'data': {
+                    'success': True,
+                    'person_id': 'visitor_001',
+                    'person_name': '测试用户',
+                    'confidence': round(random.uniform(0.85, 0.99), 2)
+                },
+                'timestamp': int(time.time())
+            }
+        else:
+            event_data = {
+                'msg_id': f'sim_{int(time.time() * 1000)}',
+                'source': 'simulator',
+                'event': 'face_recognition_result',
+                'original_msg_id': original_msg_id,
+                'data': {
+                    'success': False,
+                    'reason': 'not_found',
+                    'confidence': 0.0
+                },
+                'timestamp': int(time.time())
+            }
+
+        self._publish_mqtt(self.event_topic, event_data)
+        self.get_logger().info(f'已回复人脸识别结果: success={success}')
+
+    def _handle_pause(self, original_msg_id):
+        """
+        处理暂停命令
+        """
+        self.get_logger().info('收到全局暂停命令,已记录')
+
+    def _handle_navigate(self, target, original_msg_id, payload):
+        """
+        处理导航命令
+        """
+        delay = self._get_delay(self.navigation_delay)
+        self.get_logger().info(f'模拟导航操作,延迟 {delay:.2f} 秒')
+        time.sleep(delay)
+
+        status_msg = {
+            'msg_id': f'sim_{int(time.time() * 1000)}',
+            'source': 'simulator',
+            'target': 'agent',
+            'module': 'nav',
+            'status': 'navigation_completed',
+            'original_msg_id': original_msg_id,
+            'timestamp': int(time.time())
+        }
+
+        self._publish_mqtt(self.status_topic, status_msg)
+        self.get_logger().info('已回复导航完成状态')
+
+    def _handle_call_service(self, original_msg_id):
+        """
+        处理呼叫客服命令
+        """
+        delay = self._get_delay(self.customer_service_delay)
+        self.get_logger().info(f'模拟呼叫客服,延迟 {delay:.2f} 秒')
+        time.sleep(delay)
+
+        status_msg = {
+            'msg_id': f'sim_{int(time.time() * 1000)}',
+            'source': 'simulator',
+            'target': 'agent',
+            'module': 'customer_service',
+            'status': 'call_connected',
+            'original_msg_id': original_msg_id,
+            'timestamp': int(time.time())
+        }
+
+        self._publish_mqtt(self.status_topic, status_msg)
+        self.get_logger().info('已回复客服呼叫成功')
+
+    def _handle_visitor_register(self, original_msg_id):
+        """
+        处理访客登记命令
+        """
+        delay = self._get_delay(self.visitor_register_delay)
+        self.get_logger().info(f'模拟访客登记,延迟 {delay:.2f} 秒')
+        time.sleep(delay)
+
+        status_msg = {
+            'msg_id': f'sim_{int(time.time() * 1000)}',
+            'source': 'simulator',
+            'target': 'agent',
+            'module': 'visitor',
+            'status': 'registration_completed',
+            'original_msg_id': original_msg_id,
+            'data': {
+                'visitor_id': f'V{int(time.time())}',
+                'visitor_name': '访客'
+            },
+            'timestamp': int(time.time())
+        }
+
+        self._publish_mqtt(self.status_topic, status_msg)
+        self.get_logger().info('已回复访客登记完成')
+
+    def _publish_mqtt(self, topic, data):
+        """
+        通过 MQTT 发布消息
+
+        Args:
+            topic: MQTT topic
+            data: 要发布的数据字典
+        """
+        if self.mqtt_client is None or not self.mqtt_connected:
+            self.get_logger().warn(f'MQTT 未连接,跳过发布: topic={topic}')
+            return
+
+        try:
+            payload = json.dumps(data, ensure_ascii=False)
+            result = self.mqtt_client.publish(topic, payload)
+
+            if result.rc == mqtt.MQTT_ERR_SUCCESS:
+                if self.log_all_messages:
+                    self.get_logger().info(f'[MQTT发布] topic={topic}, payload={payload}')
+            else:
+                self.get_logger().error(f'[MQTT发布] 失败, rc={result.rc}')
+
+        except Exception as e:
+            self.get_logger().error(f'[MQTT发布] 异常: {e}')
+
+    def destroy_node(self):
+        """
+        销毁节点
+        """
+        if self.mqtt_client is not None:
+            try:
+                self.mqtt_client.loop_stop()
+                self.mqtt_client.disconnect()
+                self.get_logger().info('MQTT 连接已关闭')
+            except Exception as e:
+                self.get_logger().warn(f'关闭 MQTT 连接时出错: {e}')
+
+        super().destroy_node()
+
+
+def main(args=None):
+    """
+    入口函数
+    """
+    rclpy.init(args=args)
+
+    try:
+        simulator_node = MQTTSimulatorNode()
+        rclpy.spin(simulator_node)
+    except KeyboardInterrupt:
+        pass
+    finally:
+        rclpy.shutdown()
+
+
+if __name__ == '__main__':
+    main()

+ 24 - 0
brain/PlannerNode2/mqtt_simulator/package.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
+<package format="3">
+  <name>mqtt_simulator</name>
+  <version>1.0.0</version>
+  <description>MQTT 模拟器节点 - 模拟外部模块响应大模型发出的 MQTT 命令</description>
+  <maintainer email="sunrise@todo.todo">sunrise</maintainer>
+  <license>TODO: License declaration</license>
+
+  <build_depend>ament_python</build_depend>
+
+  <exec_depend>rclpy</exec_depend>
+  <exec_depend>std_msgs</exec_depend>
+  <exec_depend>paho-mqtt</exec_depend>
+
+  <test_depend>ament_copyright</test_depend>
+  <test_depend>ament_flake8</test_depend>
+  <test_depend>ament_pep257</test_depend>
+  <test_depend>python3-pytest</test_depend>
+
+  <export>
+    <build_type>ament_python</build_type>
+  </export>
+</package>

+ 0 - 0
brain/PlannerNode2/mqtt_simulator/resource/mqtt_simulator


+ 5 - 0
brain/PlannerNode2/mqtt_simulator/setup.cfg

@@ -0,0 +1,5 @@
+[develop]
+script_dir=$base/lib/mqtt_simulator
+
+[install]
+install_scripts=$base/lib/mqtt_simulator

+ 28 - 0
brain/PlannerNode2/mqtt_simulator/setup.py

@@ -0,0 +1,28 @@
+from setuptools import setup
+
+package_name = 'mqtt_simulator'
+
+setup(
+    name=package_name,
+    version='1.0.0',
+    packages=[package_name],
+    data_files=[
+        ('share/ament_index/resource_index/packages',
+            ['resource/' + package_name]),
+        ('share/' + package_name, ['package.xml']),
+        ('share/' + package_name + '/launch', ['launch/mqtt_simulator.launch.py']),
+        ('share/' + package_name + '/config', ['config/simulator.yaml']),
+    ],
+    install_requires=['setuptools'],
+    zip_safe=True,
+    maintainer='sunrise',
+    maintainer_email='sunrise@todo.todo',
+    description='MQTT 模拟器节点 - 模拟外部模块响应大模型发出的 MQTT 命令',
+    license='TODO: License declaration',
+    tests_require=['pytest'],
+    entry_points={
+        'console_scripts': [
+            'mqtt_simulator = mqtt_simulator.mqtt_simulator_node:main',
+        ],
+    },
+)