|
|
@@ -30,11 +30,26 @@ from threading import Thread
|
|
|
from tf2_ros.buffer import Buffer
|
|
|
from tf2_ros.transform_listener import TransformListener
|
|
|
from utils import large_model_interface
|
|
|
+from utils.mqttclient import MQTTClient
|
|
|
import threading
|
|
|
from rclpy.executors import MultiThreadedExecutor
|
|
|
import functools
|
|
|
|
|
|
|
|
|
+# ==================== 机器人模式定义 ====================
|
|
|
+class mode:
|
|
|
+ """
|
|
|
+ 机器人模式枚举命名空间
|
|
|
+
|
|
|
+ 用于标识机器人当前处于什么工作模式
|
|
|
+ """
|
|
|
+ NONE = 0 # 无模式(默认)
|
|
|
+ WELCOME = 1 # 迎宾模式
|
|
|
+ DOOR_GUARD = 2 # 门岗模式
|
|
|
+ PATROL = 3 # 巡检模式
|
|
|
+# ==================== 模式定义结束 ====================
|
|
|
+
|
|
|
+
|
|
|
class CustomActionServer(Node):
|
|
|
def __init__(self):
|
|
|
super().__init__("action_service_ndoe")
|
|
|
@@ -58,9 +73,10 @@ class CustomActionServer(Node):
|
|
|
|
|
|
def destroy_node(self):
|
|
|
"""
|
|
|
- 重写 destroy_node,确保节点销毁前清理所有子进程
|
|
|
+ 重写 destroy_node,确保节点销毁前清理所有子进程和 MQTT 连接
|
|
|
"""
|
|
|
self._on_shutdown()
|
|
|
+ self.mqtt_client.shutdown()
|
|
|
super().destroy_node()
|
|
|
|
|
|
def init_param_config(self):
|
|
|
@@ -112,7 +128,7 @@ class CustomActionServer(Node):
|
|
|
self.first_record = True # 首次记录位置 / First record
|
|
|
self.is_recording = False # 录音状态 / Recording status
|
|
|
self.IS_SAVING = False #是否正在保存图像
|
|
|
- self.welcome_mode = False # 迎宾模式标志 / Welcome mode flag
|
|
|
+ self.current_robot_mode = mode.NONE # 机器人当前模式 / Robot current mode
|
|
|
self.process_map = {
|
|
|
'person_approach': {'pid': None, 'sub': None, 'running': False}
|
|
|
} # 进程管理字典 / Process management map
|
|
|
@@ -127,6 +143,9 @@ class CustomActionServer(Node):
|
|
|
# 传入 logger 用于调试日志
|
|
|
self.model_client = large_model_interface.model_interface(logger=self.get_logger())
|
|
|
|
|
|
+ # 创建 MQTT 客户端 / Create MQTT client
|
|
|
+ self.mqtt_client = MQTTClient(logger=self.get_logger())
|
|
|
+
|
|
|
def init_ros_comunication(self):
|
|
|
"""
|
|
|
初始化创建ros通信对象、函数 / Initialize creation of ROS communication objects and functions
|
|
|
@@ -272,6 +291,13 @@ class CustomActionServer(Node):
|
|
|
String, self.environment_topic, self.environment_callback, 10
|
|
|
)
|
|
|
self.get_logger().debug(f'[配置] 环境数据订阅 Topic 已更新: {self.environment_topic}')
|
|
|
+
|
|
|
+ # --- MQTT 配置处理 ---
|
|
|
+ mqtt_cfg = config_root.get('mqtt', {})
|
|
|
+ if mqtt_cfg:
|
|
|
+ # 初始化或更新 MQTT 客户端
|
|
|
+ self.mqtt_client.init(mqtt_cfg)
|
|
|
+ self.get_logger().debug('[MQTT] 配置已加载')
|
|
|
except Exception as e:
|
|
|
self.get_logger().warn(f'解析配置数据失败: {e}')
|
|
|
|
|
|
@@ -604,9 +630,10 @@ class CustomActionServer(Node):
|
|
|
"""
|
|
|
|
|
|
if msg.data:
|
|
|
- self.get_logger().info(f"wakeup_callback: welcome_mode={self.welcome_mode}, action_runing={self.action_runing}")
|
|
|
+ self.global_pause(reason="wakeup")
|
|
|
+ self.get_logger().info(f"wakeup_callback: 当前模式={self.current_robot_mode}, action_runing={self.action_runing}")
|
|
|
# 迎宾模式打断处理
|
|
|
- if self.welcome_mode:
|
|
|
+ if self.current_robot_mode > 0:
|
|
|
##self.stop_event.set() # 停止 TTS 播放
|
|
|
# 杀掉所有管理的进程
|
|
|
for process_name, process_info in self.process_map.items():
|
|
|
@@ -617,7 +644,6 @@ class CustomActionServer(Node):
|
|
|
self.destroy_subscription(process_info['sub'])
|
|
|
process_info['sub'] = None
|
|
|
process_info['running'] = False
|
|
|
- self.welcome_mode = False
|
|
|
self.stop_event.clear() # 清除停止事件,避免影响后续播放
|
|
|
self.get_logger().info("Welcome mode interrupted by wakeup")
|
|
|
|
|
|
@@ -639,11 +665,11 @@ class CustomActionServer(Node):
|
|
|
迎宾模式函数 / Welcome mode function
|
|
|
启动人物靠近检测节点,订阅检测事件,收到事件后播放欢迎语
|
|
|
"""
|
|
|
- if self.welcome_mode:
|
|
|
+ if self.current_robot_mode != mode.WELCOME:
|
|
|
self.get_logger().warn("Welcome mode already running")
|
|
|
return
|
|
|
|
|
|
- self.welcome_mode = True
|
|
|
+ self.set_robot_mode(mode.WELCOME)
|
|
|
|
|
|
# 启动 person_approach 节点
|
|
|
process = subprocess.Popen(
|
|
|
@@ -665,56 +691,67 @@ class CustomActionServer(Node):
|
|
|
人物靠近事件回调函数 / Person approach event callback
|
|
|
收到事件后播放欢迎语,并停止人物靠近检测
|
|
|
"""
|
|
|
- if not self.welcome_mode:
|
|
|
- return
|
|
|
+ if self.current_robot_mode == mode.WELCOME:
|
|
|
+ # ========== 迎宾模式 ==========
|
|
|
+ try:
|
|
|
+ data = json.loads(msg.data)
|
|
|
+ if data.get('event') == 'person_approach':
|
|
|
+ self.get_logger().info(f"Person approach detected: {data}")
|
|
|
+
|
|
|
+ # 停止人物靠近检测进程 / Stop person_approach detection process
|
|
|
+ if self.process_map['person_approach']['running']:
|
|
|
+ pid = self.process_map['person_approach']['pid']
|
|
|
+ try:
|
|
|
+ # 使用 psutil 杀掉整个进程树(包括子进程)
|
|
|
+ parent = psutil.Process(pid)
|
|
|
+ children = parent.children(recursive=True)
|
|
|
+ for child in children:
|
|
|
+ try:
|
|
|
+ child.terminate()
|
|
|
+ except psutil.NoSuchProcess:
|
|
|
+ pass
|
|
|
+ parent.terminate()
|
|
|
+ self.process_map['person_approach']['running'] = False
|
|
|
+ self.get_logger().info(f"Stopped person_approach node, PID: {pid}, children: {len(children)}")
|
|
|
+ except Exception as e:
|
|
|
+ self.get_logger().warn(f"Failed to stop person_approach: {e}")
|
|
|
+
|
|
|
+ # 停止上一个 TTS 播放
|
|
|
+ self.stop_event.set()
|
|
|
+ time.sleep(0.1)
|
|
|
+
|
|
|
+ # 欢迎语内容(后续可修改)
|
|
|
+ welcome_text = "欢迎光临,有什么可以帮助您的呢?"
|
|
|
+
|
|
|
+ # TTS 合成
|
|
|
+ self.model_client.voice_synthesis(
|
|
|
+ welcome_text, self.tts_out_path
|
|
|
+ )
|
|
|
+ # 同步播放欢迎语,等待播放完成后启动 ASR 监听
|
|
|
+ self.play_audio(self.tts_out_path)
|
|
|
+ self.get_logger().info(f"Playing welcome TTS: {welcome_text}")
|
|
|
+ # 播放完成,启动 ASR 监听用户对话
|
|
|
+ # 将模式设置为 NONE,允许对话结束后重新启动迎宾模式
|
|
|
+ self.set_robot_mode(mode.NONE)
|
|
|
+ self.asr_control_pub.publish(String(data="start_listen"))
|
|
|
+ self.get_logger().info("Welcome TTS finished, started ASR listening")
|
|
|
+
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ self.get_logger().error("Failed to parse person_approach event data")
|
|
|
+ except Exception as e:
|
|
|
+ self.get_logger().error(f"Error in person_approach_event_callback: {e}")
|
|
|
|
|
|
- try:
|
|
|
- data = json.loads(msg.data)
|
|
|
- if data.get('event') == 'person_approach':
|
|
|
- self.get_logger().info(f"Person approach detected: {data}")
|
|
|
-
|
|
|
- # 停止人物靠近检测进程 / Stop person_approach detection process
|
|
|
- if self.process_map['person_approach']['running']:
|
|
|
- pid = self.process_map['person_approach']['pid']
|
|
|
- try:
|
|
|
- # 使用 psutil 杀掉整个进程树(包括子进程)
|
|
|
- parent = psutil.Process(pid)
|
|
|
- children = parent.children(recursive=True)
|
|
|
- for child in children:
|
|
|
- try:
|
|
|
- child.terminate()
|
|
|
- except psutil.NoSuchProcess:
|
|
|
- pass
|
|
|
- parent.terminate()
|
|
|
- self.process_map['person_approach']['running'] = False
|
|
|
- self.get_logger().info(f"Stopped person_approach node, PID: {pid}, children: {len(children)}")
|
|
|
- except Exception as e:
|
|
|
- self.get_logger().warn(f"Failed to stop person_approach: {e}")
|
|
|
-
|
|
|
- # 停止上一个 TTS 播放
|
|
|
- self.stop_event.set()
|
|
|
- time.sleep(0.1)
|
|
|
+ elif self.current_robot_mode == mode.DOOR_GUARD:
|
|
|
+ # ========== 门岗模式 ==========
|
|
|
+ pass
|
|
|
|
|
|
- # 欢迎语内容(后续可修改)
|
|
|
- welcome_text = "欢迎光临,有什么可以帮助您的呢?"
|
|
|
+ elif self.current_robot_mode == mode.PATROL:
|
|
|
+ # ========== 巡检模式 ==========
|
|
|
+ pass
|
|
|
|
|
|
- # TTS 合成
|
|
|
- self.model_client.voice_synthesis(
|
|
|
- welcome_text, self.tts_out_path
|
|
|
- )
|
|
|
- # 同步播放欢迎语,等待播放完成后启动 ASR 监听
|
|
|
- self.play_audio(self.tts_out_path)
|
|
|
- self.get_logger().info(f"Playing welcome TTS: {welcome_text}")
|
|
|
- # 播放完成,启动 ASR 监听用户对话
|
|
|
- # 将 welcome_mode 设置为 False,允许对话结束后重新启动迎宾模式
|
|
|
- self.welcome_mode = False
|
|
|
- self.asr_control_pub.publish(String(data="start_listen"))
|
|
|
- self.get_logger().info("Welcome TTS finished, started ASR listening")
|
|
|
-
|
|
|
- except json.JSONDecodeError:
|
|
|
- self.get_logger().error("Failed to parse person_approach event data")
|
|
|
- except Exception as e:
|
|
|
- self.get_logger().error(f"Error in person_approach_event_callback: {e}")
|
|
|
+ else:
|
|
|
+ # NONE 或其他模式 → 忽略事件
|
|
|
+ return
|
|
|
|
|
|
def get_current_pose(self, point_name="zero"):
|
|
|
"""
|
|
|
@@ -1597,6 +1634,177 @@ class CustomActionServer(Node):
|
|
|
"""
|
|
|
pass
|
|
|
|
|
|
+ # ==================== 模式管理函数 ====================
|
|
|
+
|
|
|
+ def set_robot_mode(self, new_mode):
|
|
|
+ """
|
|
|
+ 设置机器人当前模式
|
|
|
+
|
|
|
+ :param new_mode: 目标模式,应使用 mode.NONE / mode.WELCOME / mode.DOOR_GUARD / mode.PATROL
|
|
|
+ """
|
|
|
+ old_mode = self.current_robot_mode
|
|
|
+ self.current_robot_mode = new_mode
|
|
|
+ self.get_logger().info(
|
|
|
+ f"[模式切换] 机器人模式变更: {old_mode} -> {new_mode}"
|
|
|
+ )
|
|
|
+
|
|
|
+ # ==================== MQTT 业务函数 ====================
|
|
|
+
|
|
|
+ def global_pause(self, reason="agent_wakeup"):
|
|
|
+ """
|
|
|
+ 全局暂停广播:通知所有外部模块暂停当前业务
|
|
|
+
|
|
|
+ 用于机器人被唤醒、准备进入语音交互、欢迎语、业务页面切换等场景
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ ok = self.mqtt_client.publish_command(
|
|
|
+ target="all",
|
|
|
+ module="system",
|
|
|
+ action="pause",
|
|
|
+ page="",
|
|
|
+ payload={
|
|
|
+ "reason": reason,
|
|
|
+ "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}")
|
|
|
+
|
|
|
+ def open_visitor_register_page(self):
|
|
|
+ """
|
|
|
+ 打开访客登记页面
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ ok = self.mqtt_client.publish_command(
|
|
|
+ target="screen",
|
|
|
+ module="visitor",
|
|
|
+ action="open_page",
|
|
|
+ page="visitor_register",
|
|
|
+ payload={
|
|
|
+ "title": "访客登记",
|
|
|
+ "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}"
|
|
|
+ )
|
|
|
+
|
|
|
+ def open_appointment_confirm_page(self):
|
|
|
+ """
|
|
|
+ 打开预约确认页面
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ ok = self.mqtt_client.publish_command(
|
|
|
+ target="screen",
|
|
|
+ module="visitor",
|
|
|
+ action="open_page",
|
|
|
+ page="appointment_confirm",
|
|
|
+ payload={
|
|
|
+ "title": "预约确认",
|
|
|
+ "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}"
|
|
|
+ )
|
|
|
+
|
|
|
+ def open_navigation_page(self):
|
|
|
+ """
|
|
|
+ 打开导航页面
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ ok = self.mqtt_client.publish_command(
|
|
|
+ target="screen",
|
|
|
+ module="nav",
|
|
|
+ action="open_page",
|
|
|
+ page="navigation",
|
|
|
+ payload={
|
|
|
+ "title": "导航服务",
|
|
|
+ "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}"
|
|
|
+ )
|
|
|
+
|
|
|
+ def open_customer_service_page(self):
|
|
|
+ """
|
|
|
+ 打开呼叫客服页面
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ ok = self.mqtt_client.publish_command(
|
|
|
+ target="screen",
|
|
|
+ module="customer_service",
|
|
|
+ action="open_page",
|
|
|
+ page="customer_service",
|
|
|
+ payload={
|
|
|
+ "title": "呼叫客服",
|
|
|
+ "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}"
|
|
|
+ )
|
|
|
+
|
|
|
+ def open_face_recognition_page(self):
|
|
|
+ """
|
|
|
+ 打开人脸识别页面,并启动人脸识别模块
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ ok_page = self.mqtt_client.publish_command(
|
|
|
+ target="screen",
|
|
|
+ module="face",
|
|
|
+ action="open_page",
|
|
|
+ page="face_recognition",
|
|
|
+ payload={
|
|
|
+ "title": "人脸识别",
|
|
|
+ "tips": "请面向摄像头",
|
|
|
+ "mode": "whitelist"
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ ok_start = self.mqtt_client.publish_command(
|
|
|
+ target="face",
|
|
|
+ module="face",
|
|
|
+ action="start",
|
|
|
+ page="",
|
|
|
+ payload={
|
|
|
+ "mode": "whitelist",
|
|
|
+ "timeout_ms": 10000,
|
|
|
+ "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}"
|
|
|
+ )
|
|
|
+
|
|
|
+ # ==================== MQTT 业务函数结束 ====================
|
|
|
+
|
|
|
def publish_continue_listen(self):
|
|
|
"""
|
|
|
发布 continue_listen 指令,让 ASR 继续监听
|