action_service.py 79 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858
  1. import cv2
  2. import re
  3. import rclpy
  4. import subprocess
  5. import json
  6. import signal
  7. import psutil
  8. from rclpy.action import ActionServer
  9. from rclpy.node import Node
  10. from geometry_msgs.msg import Twist
  11. from rclpy.action import ActionClient
  12. from geometry_msgs.msg import PoseStamped, PoseWithCovarianceStamped
  13. import time
  14. from cv_bridge import CvBridge
  15. from std_msgs.msg import String
  16. from sensor_msgs.msg import Image
  17. from nav2_msgs.action import NavigateToPose
  18. from std_msgs.msg import Int16MultiArray, Bool
  19. #from arm_msgs.msg import ArmJoints, ArmJoint
  20. from interfaces.action import Rot
  21. import math
  22. import pygame
  23. #from arm_interface.msg import CurJoints
  24. import yaml
  25. from concurrent.futures import Future
  26. import psutil
  27. from ament_index_python.packages import get_package_share_directory
  28. import os
  29. from threading import Thread
  30. from tf2_ros.buffer import Buffer
  31. from tf2_ros.transform_listener import TransformListener
  32. from utils import large_model_interface
  33. import threading
  34. from rclpy.executors import MultiThreadedExecutor
  35. import functools
  36. class CustomActionServer(Node):
  37. def __init__(self):
  38. super().__init__("action_service_ndoe")
  39. # 初始化参数配置 / Initialize parameter configuration
  40. self.init_param_config()
  41. # 初始化ROS通信 / Initialize ROS communication
  42. self.init_ros_comunication()
  43. # 加载地图映射文件 / Load map mapping file
  44. self.load_target_points()
  45. # 初始化机械臂抓取功能 / Initialize arm grasping function
  46. # self.arm_grasp_init()
  47. # 配置标志:等待 /ai/config 到达后再初始化声音和语言
  48. self.config_ready = False
  49. self.config_init_done = False # 确保初始化只执行一次
  50. self._config_debounce_timer = None # 防抖定时器
  51. self._config_debounce_delay = 0.5 # 防抖延迟(秒)
  52. self.get_logger().info("action service started, waiting for /ai/config...")
  53. # 注册 shutdown 回调,确保退出时清理子进程
  54. self.get_logger().info("[Shutdown] Registered shutdown handler")
  55. def destroy_node(self):
  56. """
  57. 重写 destroy_node,确保节点销毁前清理所有子进程
  58. """
  59. self._on_shutdown()
  60. super().destroy_node()
  61. def init_param_config(self):
  62. """
  63. 初始化参数配置 / Initialize parameter configuration
  64. """
  65. # 设置夹取启动文件路径 / Set the path for the grasping startup file
  66. pkg_share = get_package_share_directory("largemodel")
  67. self.map_mapping_config = os.path.join(pkg_share, "config", "map_mapping.yaml")
  68. # 声明参数 / Declare parameters
  69. self.declare_parameter("Speed_topic", "/cmd_vel")
  70. self.declare_parameter("use_double_llm", False)
  71. self.declare_parameter("text_chat_mode", False)
  72. self.declare_parameter("useolinetts", False)
  73. self.declare_parameter("language", "zh")
  74. self.declare_parameter("image_topic", "/camera/color/image_raw")
  75. self.declare_parameter("regional_setting", "China")
  76. # 获取参数值 / Get parameter values
  77. self.Speed_topic = (
  78. self.get_parameter("Speed_topic").get_parameter_value().string_value
  79. )
  80. self.use_double_llm = (
  81. self.get_parameter("use_double_llm").get_parameter_value().bool_value
  82. )
  83. self.text_chat_mode = (
  84. self.get_parameter("text_chat_mode").get_parameter_value().bool_value
  85. )
  86. self.useolinetts = (
  87. self.get_parameter("useolinetts").get_parameter_value().bool_value
  88. )
  89. self.language = (
  90. self.get_parameter("language").get_parameter_value().string_value
  91. )
  92. self.image_topic = (
  93. self.get_parameter("image_topic").get_parameter_value().string_value
  94. )
  95. self.regional_setting = (
  96. self.get_parameter("regional_setting").get_parameter_value().string_value
  97. )
  98. self.pkg_path = get_package_share_directory("largemodel")
  99. self.image_save_path = os.path.join(
  100. self.pkg_path, "resources_file", "image.png"
  101. )
  102. self.current_pose = PoseWithCovarianceStamped()
  103. self.record_pose = PoseStamped()
  104. self.combination_mode = False # 组合模式 / Combination mode
  105. self.interrupt_flag = False # 打断标志 / Interrupt flag
  106. self.action_runing = False # 动作执行状态 / Action execution status
  107. self.first_record = True # 首次记录位置 / First record
  108. self.is_recording = False # 录音状态 / Recording status
  109. self.IS_SAVING = False #是否正在保存图像
  110. self.welcome_mode = False # 迎宾模式标志 / Welcome mode flag
  111. self.process_map = {
  112. 'person_approach': {'pid': None, 'sub': None, 'running': False}
  113. } # 进程管理字典 / Process management map
  114. self.joint6 = (
  115. 140 # 默认机械臂六轴的初始角度 / Default angle of the six-axis arm
  116. )
  117. # 图像处理对象 / Image processing object
  118. self.image_msg = None
  119. self.bridge = CvBridge()
  120. # 创建模型接口客户端 / Create model interface client
  121. # 传入 logger 用于调试日志
  122. self.model_client = large_model_interface.model_interface(logger=self.get_logger())
  123. def init_ros_comunication(self):
  124. """
  125. 初始化创建ros通信对象、函数 / Initialize creation of ROS communication objects and functions
  126. """
  127. # 创建速度话题发布者 / Create velocity topic publisher
  128. self.publisher = self.create_publisher(Twist, self.Speed_topic, 10)
  129. # 创建导航功能客户端,请求导航动作服务器 / Create navigation function client, request navigation action server
  130. self.navclient = ActionClient(self, NavigateToPose, "navigate_to_pose")
  131. # 创建动作执行服务器,用于接受动作列表,并执行动作 / Create action execution server to accept action lists and execute actions
  132. self._action_server = ActionServer(
  133. self, Rot, "action_service", self.execute_callback
  134. )
  135. # 创建机械臂角度发布者,用于发布arm6_joints,控制机械臂 / Create arm angle publisher to publish arm6_joints and control the arm
  136. # self.TargetAngle_pub = self.create_publisher(ArmJoints, "arm6_joints", 100)
  137. # 创建关节角度发布者,用于发布arm_joint控制关节 / Create joint angle publisher to publish arm_joint and control joints
  138. # self.SingleJoint_pub = self.create_publisher(ArmJoint, "arm_joint", 100)
  139. # 创建执行动作状态发布者 / Create action execution status publisher
  140. self.actionstatus_pub = self.create_publisher(String, "actionstatus", 3)
  141. # 创建发布者,发布 seewhat_handle 话题 / Create publisher to publish seewhat_handle topic
  142. self.seewhat_handle_pub = self.create_publisher(String, "seewhat_handle", 1)
  143. # 创建物体位置发布者,发布待夹取物体的坐标 / Create object position publisher to publish coordinates of objects to be grasped
  144. self.object_position_pub = self.create_publisher(
  145. Int16MultiArray, "corner_xy", 1
  146. )
  147. # 创建JoyCb话题发布者,启动KCF_Tracker_ALM节点测距的功能 / Create JoyCb topic publisher to enable distance measurement functionality of KCF_Tracker_ALM node
  148. self.joy_pub = self.create_publisher(Bool, "JoyState", 1)
  149. # 创建当前机械臂关节角发布者 / Create current arm joint angle publisher
  150. # self.pub_cur_joints = self.create_publisher(CurJoints, "Curjoints", 1)
  151. # 创建KCF_Tracker_ALM重置发布者 / Create KCF_Tracker_ALM reset publisher
  152. self.reset_pub = self.create_publisher(Bool, "reset_flag", 1)
  153. # 创建机械臂抓取完成话题订阅者 / Create subscriber for arm grasping completion topic
  154. self.largemodel_arm_done_sub = self.create_subscription(
  155. String, "/largemodel_arm_done", self.largemodel_arm_done_callback, 1
  156. )
  157. # 创建发布者,发布 tts_topic 主题 / Create publisher to publish tts_topic topic
  158. self.TTS_publisher = self.create_publisher(String, "tts_topic", 5)
  159. # 创建tf监听者,监听坐标变换 / Create tf listener to monitor coordinate transformations
  160. self.tf_buffer = Buffer()
  161. self.tf_listener = TransformListener(self.tf_buffer, self)
  162. # 创建打断状态发布者 / Create interrupt status publisher
  163. self.interrupt_flag_pub = self.create_publisher(Bool, "interrupt_flag", 1)
  164. # wakeup话题订阅者 / Subscribe to wakeup topic
  165. self.wakeup_sub = self.create_subscription(
  166. Bool, "wakeup", self.wakeup_callback, 5
  167. )
  168. # 图像话题订阅者 / Image topic subscriber
  169. self.subscription = self.create_subscription(
  170. Image, self.image_topic, self.image_callback, 2
  171. )
  172. # 录音状态话题订阅者 / Record status topic subscriber
  173. self.record_status_sub = self.create_subscription(
  174. Bool, "record_status", self.record_status_callback, 5
  175. )
  176. # 环境数据订阅者,动态更新导航点 / Environment data subscriber for dynamic navigation point updates
  177. # 默认值,后续从配置动态更新
  178. self.environment_topic = "/ai/env"
  179. self.config_sub = self.create_subscription(
  180. String, "/ai/config", self.config_callback, 10
  181. )
  182. self.environment_sub = self.create_subscription(
  183. String, self.environment_topic, self.environment_callback, 10
  184. )
  185. # ASR 控制话题发布者,用于多轮对话追问 / ASR control topic publisher for multi-turn dialogue
  186. self.asr_control_pub = self.create_publisher(String, "/asr/control", 10)
  187. def config_callback(self, msg):
  188. """
  189. 配置订阅回调函数
  190. 从 /ai/config 解析所有配置项
  191. """
  192. try:
  193. config = json.loads(msg.data)
  194. config_root = config.get('config', {})
  195. # --- action_service 配置处理 ---
  196. action_service_cfg = config_root.get('action_service', {})
  197. if action_service_cfg:
  198. # Speed_topic
  199. new_speed_topic = action_service_cfg.get('Speed_topic', self.Speed_topic)
  200. if new_speed_topic != self.Speed_topic:
  201. self.Speed_topic = new_speed_topic
  202. self.get_logger().debug(f'[配置] Speed_topic 已更新: {self.Speed_topic}')
  203. # text_chat_mode
  204. self.text_chat_mode = action_service_cfg.get('text_chat_mode', self.text_chat_mode)
  205. self.get_logger().debug(f'[配置] text_chat_mode: {self.text_chat_mode}')
  206. # image_topic(重建订阅)
  207. new_image_topic = action_service_cfg.get('image_topic', self.image_topic)
  208. if new_image_topic != self.image_topic:
  209. self.image_topic = new_image_topic
  210. if hasattr(self, 'subscription') and self.subscription:
  211. self.destroy_subscription(self.subscription)
  212. self.subscription = self.create_subscription(
  213. Image, self.image_topic, self.image_callback, 2
  214. )
  215. self.get_logger().debug(f'[配置] image_topic 已更新并重建订阅: {self.image_topic}')
  216. # useolinetts(配置变化不触发初始化,由外部重启节点处理)
  217. new_useolinetts = action_service_cfg.get('useolinetts', self.useolinetts)
  218. if new_useolinetts != self.useolinetts:
  219. self.useolinetts = new_useolinetts
  220. self.get_logger().warn(f'[配置] useolinetts 已更新: {self.useolinetts},需要重启节点以生效')
  221. # language(配置变化不触发初始化)
  222. new_language = action_service_cfg.get('language', self.language)
  223. if new_language != self.language:
  224. self.language = new_language
  225. self.get_logger().warn(f'[配置] language 已更新: {self.language},需要重启节点以生效')
  226. # regional_setting(配置变化不触发初始化)
  227. new_regional_setting = action_service_cfg.get('regional_setting', self.regional_setting)
  228. if new_regional_setting != self.regional_setting:
  229. self.regional_setting = new_regional_setting
  230. self.get_logger().warn(f'[配置] regional_setting 已更新: {self.regional_setting},需要重启节点以生效')
  231. # --- large_model 配置处理 ---
  232. large_model_cfg = config_root.get('large_model', {})
  233. if large_model_cfg:
  234. # 调用 model_client 的 update_config 方法更新配置
  235. self.model_client.update_config(large_model_cfg)
  236. self.get_logger().debug(
  237. f'[配置] large_model 已更新: oline_tts_model={large_model_cfg.get("oline_tts_model")}, '
  238. f'voice_tone={large_model_cfg.get("voice_tone")}, tts_supplier={large_model_cfg.get("tts_supplier")}'
  239. )
  240. # --- 首次配置到达:防抖执行一次性初始化 ---
  241. if not self.config_ready:
  242. self.config_ready = True
  243. self._schedule_config_init() # 防抖延迟执行初始化
  244. # --- topics 配置处理 ---
  245. topics = config_root.get('topics', {})
  246. environment_node_config = topics.get('environment_node', {})
  247. if environment_node_config:
  248. new_topic = environment_node_config.get('environment_topic', '/ai/env')
  249. if new_topic != self.environment_topic:
  250. self.environment_topic = new_topic
  251. if self.environment_sub:
  252. self.destroy_subscription(self.environment_sub)
  253. self.environment_sub = self.create_subscription(
  254. String, self.environment_topic, self.environment_callback, 10
  255. )
  256. self.get_logger().debug(f'[配置] 环境数据订阅 Topic 已更新: {self.environment_topic}')
  257. except Exception as e:
  258. self.get_logger().warn(f'解析配置数据失败: {e}')
  259. def _on_config_ready(self):
  260. """
  261. 防抖处理:配置稳定后执行一次性初始化
  262. """
  263. if self.config_init_done:
  264. return # 已经初始化过,跳过
  265. self.config_init_done = True
  266. self.get_logger().info('[配置] 开始执行一次性初始化...')
  267. self.system_sound_init()
  268. self.init_language()
  269. self.get_logger().info('[配置] 一次性初始化完成')
  270. def _schedule_config_init(self):
  271. """
  272. 防抖:延迟执行初始化,确保短时间内多次配置只执行一次
  273. """
  274. if self._config_debounce_timer:
  275. self._config_debounce_timer.cancel()
  276. self._config_debounce_timer = threading.Timer(
  277. self._config_debounce_delay,
  278. self._on_config_ready
  279. )
  280. self._config_debounce_timer.start()
  281. def _init_tts_model(self):
  282. """
  283. 根据当前 useolinetts / regional_setting 初始化 TTS 模型
  284. """
  285. pkg_path = get_package_share_directory("largemodel")
  286. if self.regional_setting == "China":
  287. if self.useolinetts:
  288. model_type = "oline"
  289. # Qwen-TTS 只返回 WAV 格式,不支持 MP3
  290. self.tts_out_path = os.path.join(
  291. pkg_path, "resources_file", "tts_output.wav"
  292. )
  293. else:
  294. model_type = "local"
  295. self.tts_out_path = os.path.join(
  296. pkg_path, "resources_file", "tts_output.wav"
  297. )
  298. elif self.regional_setting == "international":
  299. model_type = "XUNFEI_FOR_INTERNATIONAL"
  300. self.tts_out_path = os.path.join(
  301. pkg_path, "resources_file", "XUNFEI_TTS.mp3"
  302. )
  303. else:
  304. self.get_logger().warn(
  305. 'Please check the regional_setting parameter, it should be either "China" or "international".'
  306. )
  307. return
  308. self.model_client.tts_model_init(model_type, self.language)
  309. self.get_logger().info(
  310. f'[TTS] 模型初始化完成: model_type={model_type}, tts_out_path={self.tts_out_path}'
  311. )
  312. def system_sound_init(
  313. self,
  314. ): # 初始化系统声音相关的功能 / Initialize system sound-related functions
  315. self._init_tts_model()
  316. # 初始化音频播放器 / Initialize audio player
  317. pygame.mixer.init()
  318. self.stop_event = threading.Event()
  319. def init_language(self):
  320. language_list = ["zh", "en"]
  321. if self.language not in language_list:
  322. while True:
  323. self.get_logger().info(
  324. "The language setting is incorrect. Please check the action_service'' language setting in the yahboom.yaml file"
  325. )
  326. self.get_logger().info(self.language)
  327. time.sleep(1)
  328. self.feedback_largemoel_dict = {
  329. "zh": { # 中文 / Chinese
  330. "navigation_1": "机器人反馈:导航目标{point_name}被拒绝",
  331. "navigation_2": "机器人反馈:执行navigation({point_name})完成",
  332. "navigation_3": "机器人反馈:执行navigation({point_name})失败,目标点不存在",
  333. "navigation_4": "机器人反馈:执行navigation({point_name})失败",
  334. "get_current_pose_success": "机器人反馈:get_current_pose({point_name})成功",
  335. "arm_up_done": "机器人反馈:执行arm_up()完成",
  336. "arm_down_done": "机器人反馈:执行arm_down()完成",
  337. "drift_done": "机器人反馈:执行drift()完成",
  338. "wait_done": "机器人反馈:执行wait({duration})完成",
  339. "arm_shake_done": "机器人反馈:执行arm_shake()完成",
  340. "arm_nod_done": "机器人反馈:执行arm_nod()完成",
  341. "arm_applaud_done": "机器人反馈:执行arm_applaud()完成",
  342. "grasp_obj_done": "机器人反馈:执行grasp_obj({x1},{y1},{x2},{y2})完成",
  343. "grasp_obj_failed": "机器人反馈:执行grasp_obj({x1},{y1},{x2},{y2})失败",
  344. "putdown_done": "机器人反馈:执行putdown()完成",
  345. "set_cmdvel_done": "机器人反馈:执行set_cmdvel({linear_x},{linear_y},{angular_z},{duration})完成",
  346. "move_left_done": "机器人反馈:执行move_left({angle},{angular_speed})完成",
  347. "move_right_done": "机器人反馈:执行move_right({angle},{angular_speed})完成",
  348. "turn_left_done": "机器人反馈:执行turn_left()完成",
  349. "turn_right_done": "机器人反馈:执行turn_right()完成",
  350. "dance_done": "机器人反馈:执行dance()完成",
  351. "apriltag_sort_done": "机器人反馈:执行apriltag_sort({target_id})完成",
  352. "apriltag_sort_failed": "机器人反馈:执行apriltag_sort({target_id})失败",
  353. "apriltag_follow_2D_done": "机器人反馈:执行apriltag_follow_2D({target_id})完成",
  354. "apriltag_follow_2D_failed": "机器人反馈:执行apriltag_follow_2D({target_id})失败",
  355. "apriltag_remove_higher_done": "机器人反馈:执行apriltag_remove_higher({target_high})完成",
  356. "apriltag_remove_higher_failed": "机器人反馈:执行apriltag_remove_higher({target_high})失败",
  357. "color_follow_2D_done": "机器人反馈:执行color_follow_2D({color})完成",
  358. "color_follow_2D_failed": "机器人反馈:执行color_follow_2D({color})失败",
  359. "color_remove_higher_done": "机器人反馈:执行color_remove_higher({color},{target_high})完成",
  360. "color_remove_higher_failed": "机器人反馈:执行color_remove_higher({color},{target_high})失败",
  361. "follw_line_clear_done": "机器人反馈:执行follw_line_clear()完成",
  362. "response_done": "机器人反馈:回复用户完成",
  363. "failure_execute_action_function_not_exists": "机器人反馈:动作函数不存在,无法执行",
  364. "finish": "finish",
  365. "multiple_done": "机器人反馈:执行{actions}完成",
  366. "putdown_failed": "机器人反馈:执行putdown()失败,输入参数错误",
  367. },
  368. "en": { # 英文 / English
  369. "navigation_1": "Robot feedback: Navigation target {point_name} rejected",
  370. "navigation_2": "Robot feedback: Execute navigation({point_name}) completed",
  371. "navigation_3": "Robot feedback: Execute navigation({point_name}) failed, target does not exist",
  372. "navigation_4": "Robot feedback: Execute navigation({point_name}) failed",
  373. "get_current_pose_success": "Robot feedback: get_current_pose({point_name}) succeeded",
  374. "arm_up_done": "Robot feedback: Execute arm_up() completed",
  375. "arm_down_done": "Robot feedback: Execute arm_down() completed",
  376. "drift_done": "Robot feedback: Execute drift() completed",
  377. "wait_done": "Robot feedback: Execute wait({duration}) completed",
  378. "arm_shake_done": "Robot feedback: Execute arm_shake() completed",
  379. "arm_nod_done": "Robot feedback: Execute arm_nod() completed",
  380. "arm_applaud_done": "Robot feedback: Execute arm_applaud() completed",
  381. "grasp_obj_done": "Robot feedback: Execute grasp_obj({x1},{y1},{x2},{y2}) completed",
  382. "grasp_obj_failed": "Robot feedback: Execute grasp_obj({x1},{y1},{x2},{y2}) failed",
  383. "putdown_done": "Robot feedback: Execute putdown() completed",
  384. "set_cmdvel_done": "Robot feedback: Execute set_cmdvel({linear_x},{linear_y},{angular_z},{duration}) completed",
  385. "move_left_done": "Robot feedback: Execute move_left({angle},{angular_speed}) completed",
  386. "move_right_done": "Robot feedback: Execute move_right({angle},{angular_speed}) completed",
  387. "turn_left_done": "Robot feedback: Execute turn_left() completed",
  388. "turn_right_done": "Robot feedback: Execute turn_right() completed",
  389. "dance_done": "Robot feedback: Execute dance() completed",
  390. "apriltag_sort_done": "Robot feedback: Execute apriltag_sort({target_id}) completed",
  391. "apriltag_sort_failed": "Robot feedback: Execute apriltag_sort({target_id}) failed",
  392. "apriltag_follow_2D_done": "Robot feedback: Execute apriltag_follow_2D({target_id}) completed",
  393. "apriltag_follow_2D_failed": "Robot feedback: Execute apriltag_follow_2D({target_id}) failed",
  394. "apriltag_remove_higher_done": "Robot feedback: Execute apriltag_remove_higher({target_high}) completed",
  395. "apriltag_remove_higher_failed": "Robot feedback: Execute apriltag_remove_higher({target_high}) failed",
  396. "color_follow_2D_done": "Robot feedback: Execute color_follow_2D({color}) completed",
  397. "color_follow_2D_failed": "Robot feedback: Execute color_follow_2D({color}) failed",
  398. "color_remove_higher_done": "Robot feedback: Execute color_remove_higher({color},{target_high}) completed",
  399. "color_remove_higher_failed": "Robot feedback: Execute color_remove_higher({color},{target_high}) failed",
  400. "follw_line_clear_done": "Robot feedback: Execute follw_line_clear() completed",
  401. "response_done": "Robot feedback: Reply to user completed",
  402. "failure_execute_action_function_not_exists": "Robot feedback: Execute action function not exists",
  403. "finish": "finish",
  404. "multiple_done": "Robot feedback: Execution {actions} completed",
  405. },
  406. }
  407. def load_target_points(self):
  408. """
  409. 加载地图映射文件 /Load map mapping file
  410. """
  411. with open(self.map_mapping_config, "r") as file:
  412. target_points = yaml.safe_load(file)
  413. self.navpose_dict = {}
  414. for name, data in target_points.items():
  415. pose = PoseStamped()
  416. pose.header.frame_id = "map"
  417. pose.pose.position.x = data["position"]["x"]
  418. pose.pose.position.y = data["position"]["y"]
  419. pose.pose.position.z = data["position"]["z"]
  420. pose.pose.orientation.x = data["orientation"]["x"]
  421. pose.pose.orientation.y = data["orientation"]["y"]
  422. pose.pose.orientation.z = data["orientation"]["z"]
  423. pose.pose.orientation.w = data["orientation"]["w"]
  424. self.navpose_dict[name] = pose
  425. def update_navpose_from_environment(self, points):
  426. """
  427. 从环境数据更新导航点字典
  428. / Update navigation point dictionary from environment data
  429. """
  430. if not points:
  431. return
  432. updated_count = 0
  433. for point in points:
  434. point_id = point.get('id', '')
  435. name = point.get('name', '')
  436. position = point.get('position', {})
  437. if not point_id or not position:
  438. continue
  439. pose = PoseStamped()
  440. pose.header.frame_id = "map"
  441. # 从 position 获取坐标
  442. pose.pose.position.x = position.get('x', 0.0)
  443. pose.pose.position.y = position.get('y', 0.0)
  444. pose.pose.position.z = position.get('z', 0.0)
  445. # 从 position 获取 orientation(如果有)
  446. orientation = position.get('orientation', {})
  447. if orientation:
  448. pose.pose.orientation.x = orientation.get('x', 0.0)
  449. pose.pose.orientation.y = orientation.get('y', 0.0)
  450. pose.pose.orientation.z = orientation.get('z', 0.0)
  451. pose.pose.orientation.w = orientation.get('w', 1.0)
  452. else:
  453. # 使用默认朝向(无旋转)
  454. pose.pose.orientation.x = 0.0
  455. pose.pose.orientation.y = 0.0
  456. pose.pose.orientation.z = 0.0
  457. pose.pose.orientation.w = 1.0
  458. self.navpose_dict[point_id] = pose
  459. updated_count += 1
  460. if updated_count > 0:
  461. self.get_logger().debug(f'[环境更新] 已更新 {updated_count} 个导航点,当前共 {len(self.navpose_dict)} 个点')
  462. def environment_callback(self, msg):
  463. """
  464. 环境数据订阅回调函数
  465. 从 /ai/env topic 接收环境数据,动态更新导航点
  466. """
  467. try:
  468. env_json = json.loads(msg.data)
  469. map_data = env_json.get('map', {})
  470. if map_data:
  471. points = map_data.get('points', [])
  472. if points:
  473. self.update_navpose_from_environment(points)
  474. else:
  475. self.get_logger().debug("[环境回调] map points 为空")
  476. else:
  477. self.get_logger().debug("[环境回调] map_data 为空")
  478. except json.JSONDecodeError as e:
  479. self.get_logger().warn(f'解析环境数据失败: {e}')
  480. except Exception as e:
  481. self.get_logger().warn(f'环境数据处理异常: {e}')
  482. # def arm_grasp_init(self):
  483. # """
  484. # 初始化机械臂抓取功能 /initialize the grasping function of the robotic arm
  485. # """
  486. # # 机械臂状态变量/Robotic arm status variable
  487. # self.up_joints = [90, 90, 90, 90, 90, 90]
  488. # self.down_joints = [90, 0, 90, 90, 90, 90]
  489. # self.detect_joints = [90, 120, 0, 0, 90, 90]
  490. # self.init_joints = [
  491. # 90,
  492. # 130,
  493. # 0,
  494. # 5,
  495. # 90,
  496. # 0,
  497. # ]
  498. # # 机械臂初始姿态/robot arm initial pose
  499. # self.putsown_joints = [
  500. # 90,
  501. # 10,
  502. # 50,
  503. # 50,
  504. # 90,
  505. # 135,
  506. # ] # 机械臂放下姿态/robot arm putdown pose
  507. # while not self.TargetAngle_pub.get_subscription_count():
  508. # self.pubSix_Arm(self.init_joints)
  509. # time.sleep(0.1)
  510. # self.pubSix_Arm(self.init_joints)
  511. # self.apriltag_sort_future = Future()
  512. # self.apriltag_follow_2D_future = Future()
  513. # self.apriltag_remove_higher_future = Future()
  514. # self.color_follow_2D_future = Future()
  515. # self.color_sort_future = Future()
  516. # self.color_remove_higher_future = Future()
  517. # self.grasp_obj_future = Future()
  518. # self.follw_line_clear_future = Future()
  519. def record_status_callback(self, msg):
  520. # self.get_logger().info(f"record_status_callback:{msg.data}")
  521. if msg.data:
  522. self.is_recording = True
  523. else:
  524. self.is_recording = False
  525. def largemodel_arm_done_callback(self, msg):
  526. """
  527. 机械臂抓取完成话题回调函数/robot arm done callback function
  528. 用于接受机械臂抓取完成话题,并设置Future对象完成 /used to receive the topic of the robotic arm grasping completion, and set the Future object to complete
  529. """
  530. if msg.data in ["apriltag_sort_done", "apriltag_sort_failed"]:
  531. if not self.apriltag_sort_future.done():
  532. self.apriltag_sort_future.set_result(msg)
  533. elif msg.data == "apriltag_follow_2D_done":
  534. if not self.apriltag_follow_2D_future.done():
  535. self.apriltag_follow_2D_future.set_result(msg)
  536. elif msg.data in [
  537. "apriltag_remove_higher_done",
  538. "apriltag_remove_higher_failed",
  539. ]:
  540. self.get_logger().info(f"msg.data:{msg.data}")
  541. if not self.apriltag_remove_higher_future.done():
  542. self.apriltag_remove_higher_future.set_result(msg)
  543. elif msg.data == "color_follow_2D_done":
  544. if not self.color_follow_2D_future.done():
  545. self.color_follow_2D_future.set_result(msg)
  546. elif msg.data == "color_sort_done":
  547. if not self.color_sort_future.done():
  548. self.color_sort_future.set_result(msg)
  549. elif msg.data == "grasp_obj_done":
  550. if not self.grasp_obj_future.done():
  551. self.grasp_obj_future.set_result(msg)
  552. elif msg.data == "color_remove_higher_done":
  553. if not self.color_remove_higher_future.done():
  554. self.color_remove_higher_future.set_result(msg)
  555. elif msg.data == "follw_line_clear_future_done":
  556. if not self.follw_line_clear_future.done():
  557. self.follw_line_clear_future.set_result(msg)
  558. def wakeup_callback(self, msg):
  559. """
  560. 唤醒打断回调函数/Wake-up interrupt callback function
  561. 用于接受唤醒信号,判断是否需要打断当前的动作、语音 /used to receive the wake-up signal, determine whether to interrupt the current action, voice
  562. """
  563. if msg.data:
  564. self.get_logger().info(f"wakeup_callback: welcome_mode={self.welcome_mode}, action_runing={self.action_runing}")
  565. # 迎宾模式打断处理
  566. if self.welcome_mode:
  567. ##self.stop_event.set() # 停止 TTS 播放
  568. # 杀掉所有管理的进程
  569. for process_name, process_info in self.process_map.items():
  570. if process_info['pid'] is not None:
  571. self.kill_process_tree(process_info['pid'])
  572. process_info['pid'] = None
  573. if process_info['sub'] is not None:
  574. self.destroy_subscription(process_info['sub'])
  575. process_info['sub'] = None
  576. process_info['running'] = False
  577. self.welcome_mode = False
  578. self.stop_event.clear() # 清除停止事件,避免影响后续播放
  579. self.get_logger().info("Welcome mode interrupted by wakeup")
  580. elif (
  581. pygame.mixer.get_init() is not None
  582. and pygame.mixer.music.get_busy() # 如果音乐正在播放/If the music is playing
  583. ):
  584. self.stop_event.set() # 停止正在播放的音乐/Stop the music currently playing
  585. self.stop_event.clear() # 清除事件,避免影响后续播放
  586. if (
  587. self.action_runing # 如果当前有动作正在执行/If there is an action currently being
  588. ):
  589. self.interrupt_flag = True # 置位中断标志位/Set the interruption flag
  590. self.stop()
  591. # self.check_all_process()
  592. def welcome(self):
  593. """
  594. 迎宾模式函数 / Welcome mode function
  595. 启动人物靠近检测节点,订阅检测事件,收到事件后播放欢迎语
  596. """
  597. if self.welcome_mode:
  598. self.get_logger().warn("Welcome mode already running")
  599. return
  600. self.welcome_mode = True
  601. # 启动 person_approach 节点
  602. process = subprocess.Popen(
  603. ["ros2", "launch", "person_approach_skill", "person_approach_node.launch.py"]
  604. )
  605. self.process_map['person_approach']['pid'] = process.pid
  606. self.process_map['person_approach']['running'] = True
  607. self.get_logger().info(f"Started person_approach node, PID: {process.pid}")
  608. # 创建事件订阅
  609. self.process_map['person_approach']['sub'] = self.create_subscription(
  610. String, "/skill/person_approach/event",
  611. self.person_approach_event_callback, 10
  612. )
  613. self.get_logger().info("Subscribed to /skill/person_approach/event")
  614. def person_approach_event_callback(self, msg):
  615. """
  616. 人物靠近事件回调函数 / Person approach event callback
  617. 收到事件后播放欢迎语,并停止人物靠近检测
  618. """
  619. if not self.welcome_mode:
  620. return
  621. try:
  622. data = json.loads(msg.data)
  623. if data.get('event') == 'person_approach':
  624. self.get_logger().info(f"Person approach detected: {data}")
  625. # 停止人物靠近检测进程 / Stop person_approach detection process
  626. if self.process_map['person_approach']['running']:
  627. pid = self.process_map['person_approach']['pid']
  628. try:
  629. # 使用 psutil 杀掉整个进程树(包括子进程)
  630. parent = psutil.Process(pid)
  631. children = parent.children(recursive=True)
  632. for child in children:
  633. try:
  634. child.terminate()
  635. except psutil.NoSuchProcess:
  636. pass
  637. parent.terminate()
  638. self.process_map['person_approach']['running'] = False
  639. self.get_logger().info(f"Stopped person_approach node, PID: {pid}, children: {len(children)}")
  640. except Exception as e:
  641. self.get_logger().warn(f"Failed to stop person_approach: {e}")
  642. # 停止上一个 TTS 播放
  643. self.stop_event.set()
  644. time.sleep(0.1)
  645. # 欢迎语内容(后续可修改)
  646. welcome_text = "欢迎光临,有什么可以帮助您的呢?"
  647. # TTS 合成
  648. self.model_client.voice_synthesis(
  649. welcome_text, self.tts_out_path
  650. )
  651. # 同步播放欢迎语,等待播放完成后启动 ASR 监听
  652. self.play_audio(self.tts_out_path)
  653. self.get_logger().info(f"Playing welcome TTS: {welcome_text}")
  654. # 播放完成,启动 ASR 监听用户对话
  655. # 将 welcome_mode 设置为 False,允许对话结束后重新启动迎宾模式
  656. self.welcome_mode = False
  657. self.asr_control_pub.publish(String(data="start_listen"))
  658. self.get_logger().info("Welcome TTS finished, started ASR listening")
  659. except json.JSONDecodeError:
  660. self.get_logger().error("Failed to parse person_approach event data")
  661. except Exception as e:
  662. self.get_logger().error(f"Error in person_approach_event_callback: {e}")
  663. def get_current_pose(self, point_name="zero"):
  664. """
  665. 获取当前在全局地图坐标系下的位置 /Get the current position in the global map coordinate system
  666. :param point_name: 点位名称,用于保存到 navpose_dict,默认为 "zero"
  667. """
  668. # 获取当前目标点坐标
  669. transform = self.tf_buffer.lookup_transform(
  670. "map", "base_footprint", rclpy.time.Time()
  671. )
  672. # 提取位置和姿态
  673. pose = PoseStamped()
  674. pose.header.frame_id = "map"
  675. pose.pose.position.x = transform.transform.translation.x
  676. pose.pose.position.y = transform.transform.translation.y
  677. pose.pose.position.z = 0.0
  678. pose.pose.orientation = transform.transform.rotation
  679. self.navpose_dict[point_name] = pose
  680. # 打印记录的坐标
  681. position = pose.pose.position
  682. orientation = pose.pose.orientation
  683. self.get_logger().info(
  684. f"Recorded Pose [{point_name}] - Position: x={position.x}, y={position.y},\
  685. z={position.z},Orientation: x={orientation.x}, y={orientation.y}, z={orientation.z}, w={orientation.w}"
  686. )
  687. if not self.combination_mode and not self.interrupt_flag:
  688. self.action_status_pub("get_current_pose_success", point_name=point_name)
  689. def action_status_pub(self, key, **kwargs):
  690. """
  691. 多语言版本的动作结果发布方法
  692. :param key: 文本标识
  693. :param**kwargs: 占位符参数
  694. """
  695. text_template = self.feedback_largemoel_dict[self.language].get(key)
  696. try:
  697. message = text_template.format(**kwargs)
  698. except KeyError as e:
  699. self.get_logger().error(f"Translation placeholder error: {e} (key: {key})")
  700. message = f"[Translation failed: {key}]"
  701. # 发布消息
  702. self.actionstatus_pub.publish(String(data=message))
  703. self.get_logger().info(f"Published message: {message}")
  704. def navigation(self, point_name):
  705. """
  706. 从navpose_dict字典中获取目标点坐标.并导航到目标点
  707. """
  708. self.get_logger().info(f"navigation called with point_name: {point_name}")
  709. self.navigation_finish_flag = False
  710. self.goal_handle = None
  711. self.result = None
  712. point_name = point_name.strip("'\"")
  713. if point_name not in self.navpose_dict:
  714. self.get_logger().error(
  715. f"Target point '{point_name}' does not exist in the navigation dictionary."
  716. )
  717. self.action_status_pub(
  718. "navigation_3", point_name=point_name
  719. ) # 目标点地图映射中不存在
  720. return
  721. if self.first_record:
  722. # 出发前记录当前在全局地图中的坐标(只有在每个任务周期的第一次执行时才会记录)/ before starting a new task, record the current pose in the global map
  723. transform = self.tf_buffer.lookup_transform(
  724. "map", "base_footprint", rclpy.time.Time()
  725. )
  726. pose = PoseStamped()
  727. pose.header.frame_id = "map"
  728. pose.pose.position.x = transform.transform.translation.x
  729. pose.pose.position.y = transform.transform.translation.y
  730. pose.pose.position.z = 0.0
  731. pose.pose.orientation = transform.transform.rotation
  732. self.navpose_dict["zero"] = pose
  733. self.first_record = False
  734. # 获取目标点坐标 /get_target_pose
  735. target_pose = self.navpose_dict.get(point_name)
  736. goal_msg = NavigateToPose.Goal()
  737. goal_msg.pose = target_pose
  738. send_goal_future = self.navclient.send_goal_async(goal_msg)
  739. def goal_response_callback(future):
  740. self.goal_handle = future.result()
  741. if not self.goal_handle or not self.goal_handle.accepted:
  742. self.get_logger().error("Goal was rejected!")
  743. self.action_status_pub("navigation_1", point_name=point_name)
  744. return
  745. get_result_future = self.goal_handle.get_result_async()
  746. def result_callback(future_result):
  747. self.result = future_result.result()
  748. self.navigation_finish_flag = True
  749. if self.result.status == 4:
  750. if not self.combination_mode:
  751. self.action_status_pub(
  752. "navigation_2", point_name=point_name
  753. ) # 执行导航成功 /execute navigation success
  754. elif self.result.status == 5:
  755. self.get_logger().info("Cancel navigation")
  756. else:
  757. self.get_logger().info(
  758. f"Navigation failed with status: {self.result.status}"
  759. )
  760. self.action_status_pub(
  761. "navigation_4", point_name=point_name
  762. ) # 执行导航失败 /execute_navigation_failed
  763. get_result_future.add_done_callback(result_callback)
  764. send_goal_future.add_done_callback(goal_response_callback)
  765. while not self.navigation_finish_flag:
  766. if self.interrupt_flag and self.goal_handle is not None:
  767. self.navclient._cancel_goal(self.goal_handle)
  768. break
  769. time.sleep(0.1)
  770. self.stop()
  771. # def pubSix_Arm(self, joints, id=6, angle=180.0, runtime=2000):
  772. # while not self.TargetAngle_pub.get_subscription_count():
  773. # self.get_logger().info("Waiting for arm_subscriber...")
  774. # time.sleep(0.1)
  775. #
  776. # arm_joint = ArmJoints()
  777. # arm_joint.joint1 = joints[0]
  778. # arm_joint.joint2 = joints[1]
  779. # arm_joint.joint3 = joints[2]
  780. # arm_joint.joint4 = joints[3]
  781. # arm_joint.joint5 = joints[4]
  782. # arm_joint.joint6 = joints[5]
  783. # arm_joint.time = runtime
  784. # self.TargetAngle_pub.publish(arm_joint)
  785. # def pubSingle_Arm(self, joint_id=6, joint_angle=180.0, runtime=800):
  786. # arm_joint = ArmJoint()
  787. # arm_joint.joint = int(joint_angle)
  788. # arm_joint.id = int(joint_id)
  789. # arm_joint.time = runtime
  790. # self.SingleJoint_pub.publish(arm_joint)
  791. # def pubCurrentJoints(self):
  792. # cur_joints = CurJoints()
  793. # cur_joints.joints = self.init_joints
  794. # self.pub_cur_joints.publish(cur_joints)
  795. # def arm_up(self): # 机械臂向上
  796. # self.done = False
  797. # self.pubSix_Arm(self.up_joints)
  798. # time.sleep(1.0)
  799. # self.done = True
  800. # if not self.combination_mode and not self.interrupt_flag:
  801. # self.action_status_pub("arm_up_done")
  802. # def arm_down(self): # 机械臂向下
  803. # self.done = False
  804. # self.pubSix_Arm(self.down_joints)
  805. # time.sleep(1.0)
  806. # self.done = True
  807. # if not self.combination_mode and not self.interrupt_flag:
  808. # self.action_status_pub("arm_down_done")
  809. # def arm_dance(self): # 机械臂跳舞
  810. # dance_moves = [
  811. # [90, 90, 90, 90, 90, 90],
  812. # [90, 60, 120, 60, 90, 90],
  813. # [90, 45, 135, 45, 90, 90],
  814. # [90, 60, 120, 60, 90, 90],
  815. # [90, 90, 90, 90, 90, 90],
  816. # [90, 100, 80, 80, 90, 90],
  817. # [90, 120, 60, 60, 90, 90],
  818. # [90, 135, 45, 45, 90, 90],
  819. # [90, 90, 90, 90, 90, 90],
  820. # [90, 90, 90, 20, 90, 150],
  821. # [90, 90, 90, 90, 90, 90],
  822. # [90, 90, 90, 20, 90, 150],
  823. # ]
  824. # for joints in dance_moves:
  825. # if self.interrupt_flag:
  826. # break
  827. # self.pubSix_Arm(joints)
  828. # time.sleep(1.0)
  829. # self.pubSix_Arm(self.init_joints)
  830. def drift(self):
  831. """
  832. 漂移动作
  833. """
  834. twist = Twist()
  835. twist.linear.x = 0.0
  836. twist.linear.y = 0.5
  837. twist.angular.z = 1.0
  838. self._execute_action(twist, durationtime=4.0)
  839. if not self.combination_mode and not self.interrupt_flag:
  840. self.action_status_pub("drift_done")
  841. def wait(self, duration):
  842. duration = float(duration)
  843. time.sleep(duration)
  844. if not self.combination_mode and not self.interrupt_flag:
  845. self.action_status_pub("wait_done", duration=duration)
  846. # def arm_shake(self): # 机械臂摇头
  847. # for i in range(3):
  848. # if self.interrupt_flag:
  849. # break
  850. # tar_arm_joint = [140, 130, 0, 5, 90, 0]
  851. # self.pubSix_Arm(tar_arm_joint)
  852. # time.sleep(1.0)
  853. # tar_arm_joint = [40, 130, 0, 5, 90, 0]
  854. # self.pubSix_Arm(tar_arm_joint)
  855. # time.sleep(1.0)
  856. #
  857. # self.pubSix_Arm(self.init_joints)
  858. # if not self.combination_mode and not self.interrupt_flag:
  859. # self.action_status_pub("arm_shake_done")
  860. # def arm_nod(self): # 机械臂点头
  861. # for i in range(3):
  862. # if self.interrupt_flag:
  863. # break
  864. # tar_arm_joint = [90, 130, 0, 95, 90, 0]
  865. # self.pubSix_Arm(tar_arm_joint)
  866. # time.sleep(1.0)
  867. # self.pubSix_Arm(self.init_joints)
  868. # time.sleep(1.0)
  869. # self.pubSix_Arm(self.init_joints)
  870. # if not self.combination_mode and not self.interrupt_flag:
  871. # self.action_status_pub("arm_nod_done")
  872. # def arm_applaud(self): # 机械臂鼓掌
  873. # for i in range(3):
  874. # if self.interrupt_flag:
  875. # break
  876. # tar_arm_joint = [90, 145, 0, 71, 90, 31]
  877. # self.pubSix_Arm(tar_arm_joint)
  878. # time.sleep(1.0)
  879. # tar_arm_joint = [90, 145, 0, 71, 90, 168]
  880. # self.pubSix_Arm(tar_arm_joint)
  881. # time.sleep(1.0)
  882. # self.pubSix_Arm(self.init_joints)
  883. # if not self.combination_mode and not self.interrupt_flag:
  884. # self.action_status_pub("arm_applaud_done")
  885. # def check_track(self):
  886. # """
  887. # 检查相关进程是否存活
  888. # """
  889. # pass
  890. # def track(self, x1, y1, x2, y2):
  891. # """
  892. # 追踪物体
  893. # x1,y1,x2,y2: 物体外边框坐标
  894. # """
  895. # self.check_track()
  896. # cmd1 = "ros2 run largemodel_arm KCF_track"
  897. # cmd2 = "ros2 run M3Pro_KCF ALM_KCF_Tracker_Node"
  898. #
  899. # subprocess.Popen(
  900. # [
  901. # "gnome-terminal",
  902. # "--title=KCF_track",
  903. # "--",
  904. # "bash",
  905. # "-c",
  906. # f"{cmd2}; exec bash",
  907. # ]
  908. # )
  909. # subprocess.Popen(
  910. # [
  911. # "gnome-terminal",
  912. # "--title=ALM_KCF_Tracker_Node",
  913. # "--",
  914. # "bash",
  915. # "-c",
  916. # f"{cmd1}; exec bash",
  917. # ]
  918. # )
  919. # time.sleep(5.0) #等待ALM_KCF_Tracker_Node启动完成
  920. #
  921. # x1 = int(x1)
  922. # y1 = int(y1)
  923. # x2 = int(x2)
  924. # y2 = int(y2)
  925. # self.object_position_pub.publish(Int16MultiArray(data=[x1, y1, x2, y2]))
  926. # while True:
  927. # if self.interrupt_flag:
  928. # self.check_track()
  929. # self.pubSix_Arm(self.init_joints)
  930. # return
  931. # time.sleep(0.1)
  932. # self.pubSix_Arm(self.init_joints)
  933. # def check_close_grasp_obj(self):
  934. # """
  935. # 检查相关进程是否存活
  936. # """
  937. # pass
  938. # def grasp_obj(self, x1, y1, x2, y2):
  939. # """
  940. # 抓取物体
  941. # x1,y1,x2,y2: 物体外边框坐标
  942. # """
  943. # self.check_close_grasp_obj()
  944. # cmd1 = "ros2 run largemodel_arm grasp_desktop"
  945. # cmd2 = "ros2 run largemodel_arm KCF_follow"
  946. # cmd3 = "ros2 run M3Pro_KCF ALM_KCF_Tracker_Node"
  947. # # cmd3 = "ros2 run --prefix 'gdb -ex run --args' M3Pro_KCF ALM_KCF_Tracker_Node"
  948. # subprocess.Popen(
  949. # [
  950. # "gnome-terminal",
  951. # "--title=ALM_KCF_Tracker",
  952. # "--",
  953. # "bash",
  954. # "-c",
  955. # f"{cmd3}; exec bash",
  956. # ]
  957. # )
  958. # time.sleep(5.0) #等待ALM_KCF_Tracker_Node启动完成
  959. # subprocess.Popen(
  960. # [
  961. # "gnome-terminal",
  962. # "--title=grasp_desktop",
  963. # "--",
  964. # "bash",
  965. # "-c",
  966. # f"{cmd1}; exec bash",
  967. # ]
  968. # )
  969. # subprocess.Popen(
  970. # [
  971. # "gnome-terminal",
  972. # "--title=KCF_follow",
  973. # "--",
  974. # "bash",
  975. # "-c",
  976. # f"{cmd2}; exec bash",
  977. # ]
  978. # )
  979. # time.sleep(1.0)
  980. # x1 = int(x1)
  981. # y1 = int(y1)
  982. # x2 = int(x2)
  983. # y2 = int(y2)
  984. # self.object_position_pub.publish(Int16MultiArray(data=[x1, y1, x2, y2]))
  985. #
  986. # while not self.grasp_obj_future.done():
  987. # if self.interrupt_flag:
  988. # self.check_close_grasp_obj()
  989. # self.pubSix_Arm(self.init_joints) # 机械臂收回
  990. # self.stop()
  991. # return
  992. # time.sleep(0.1)
  993. #
  994. # result = self.grasp_obj_future.result()
  995. # if not self.interrupt_flag:
  996. # if result.data == "grasp_obj_done":
  997. # self.action_status_pub("grasp_obj_done", x1=x1, y1=y1, x2=x2, y2=y2)
  998. # else:
  999. # self.action_status_pub("grasp_obj_failed", x1=x1, y1=y1, x2=x2, y2=y2)
  1000. #
  1001. # self.check_close_grasp_obj()
  1002. # self.grasp_obj_future = Future() # 复位Future对象
  1003. # if self.interrupt_flag:
  1004. # time.sleep(0.5)
  1005. # self.pubSix_Arm(self.init_joints) # 机械臂收回
  1006. # def putdown(self):
  1007. # self.pubSix_Arm(self.putsown_joints) # 机械臂下放
  1008. # time.sleep(4)
  1009. # self.pubSingle_Arm(6, 30, 1000) # 机械臂打开夹抓,放下物品
  1010. # time.sleep(3)
  1011. # self.pubSix_Arm(self.init_joints) # 机械臂收回
  1012. # if not self.interrupt_flag:
  1013. # self.action_status_pub("putdown_done")
  1014. def seewhat(self):
  1015. self.save_single_image()
  1016. msg = String(data="seewhat")
  1017. self.seewhat_handle_pub.publish(
  1018. msg
  1019. ) # 归一化,发布seewhat话题,由model_service调用大模型
  1020. def set_cmdvel(self, linear_x, linear_y, angular_z, duration): # 发布cmd_vel
  1021. # 将参数从字符串转换为浮点数
  1022. linear_x = float(linear_x)
  1023. linear_y = float(linear_y)
  1024. angular_z = float(angular_z)
  1025. duration = float(duration)
  1026. twist = Twist()
  1027. twist.linear.x = linear_x
  1028. twist.linear.y = linear_y
  1029. twist.angular.z = angular_z
  1030. self._execute_action(twist, durationtime=duration)
  1031. if not self.combination_mode and not self.interrupt_flag:
  1032. self.action_status_pub(
  1033. "set_cmdvel_done",
  1034. linear_x=linear_x,
  1035. linear_y=linear_y,
  1036. angular_z=angular_z,
  1037. duration=duration,
  1038. )
  1039. def move_left(self, angle, angular_speed): # 左转x度
  1040. angle = float(angle)
  1041. angular_speed = float(angular_speed)
  1042. angle_rad = math.radians(angle) # 将角度转换为弧度
  1043. duration = abs(angle_rad / angular_speed)
  1044. angular_speed = abs(angular_speed)
  1045. twist = Twist()
  1046. twist.linear.x = 0.0
  1047. twist.angular.z = angular_speed
  1048. self._execute_action(twist, 1, duration)
  1049. self.stop()
  1050. if not self.combination_mode and not self.interrupt_flag:
  1051. self.action_status_pub(
  1052. "move_left_done",
  1053. angle=angle,
  1054. angular_speed=angular_speed,
  1055. )
  1056. def move_right(self, angle, angular_speed): # 右转x度
  1057. angle = float(angle)
  1058. angular_speed = float(angular_speed)
  1059. angle_rad = math.radians(angle) # 将角度转换为弧度
  1060. duration = abs(angle_rad / angular_speed)
  1061. angular_speed = -abs(angular_speed)
  1062. twist = Twist()
  1063. twist.linear.x = 0.0
  1064. twist.angular.z = angular_speed
  1065. self._execute_action(twist, 1, duration)
  1066. self.stop()
  1067. if not self.combination_mode and not self.interrupt_flag:
  1068. self.action_status_pub(
  1069. "move_right_done",
  1070. angle=angle,
  1071. angular_speed=angular_speed,
  1072. )
  1073. def turn_left(self): # 左转弯
  1074. twist = Twist()
  1075. twist.linear.x = 0.4
  1076. twist.angular.z = 1.0
  1077. self._execute_action(twist)
  1078. self.stop()
  1079. if not self.combination_mode and not self.interrupt_flag:
  1080. self.action_status_pub("turn_left_done")
  1081. def turn_right(self): # 右转弯
  1082. twist = Twist()
  1083. twist.linear.x = 0.4
  1084. twist.angular.z = -1.0
  1085. self._execute_action(twist)
  1086. self.stop()
  1087. if not self.combination_mode and not self.interrupt_flag:
  1088. self.action_status_pub("turn_right_done")
  1089. def dance(self): # 跳舞
  1090. # thread = Thread(target=self.arm_dance)
  1091. # thread.start()
  1092. actions = [
  1093. {"linear_x": 0.6, "linear_y": 0.0, "angular_z": 0.0, "durationtime": 1.5},
  1094. {"linear_x": -0.4, "linear_y": 0.0, "angular_z": 0.0, "durationtime": 1.0},
  1095. {"linear_x": 0.0, "linear_y": 0.3, "angular_z": 0.0, "durationtime": 1.0},
  1096. {"linear_x": 0.0, "linear_y": -0.3, "angular_z": 0.0, "durationtime": 1.0},
  1097. {"linear_x": 0.0, "linear_y": 0.0, "angular_z": 0.6, "durationtime": 3.0},
  1098. {"linear_x": 0.0, "linear_y": 0.0, "angular_z": -0.6, "durationtime": 3.0},
  1099. ]
  1100. for action in actions:
  1101. if self.interrupt_flag:
  1102. break
  1103. twist = Twist()
  1104. twist.linear.x = action["linear_x"]
  1105. twist.linear.y = action["linear_y"]
  1106. twist.angular.z = action["angular_z"]
  1107. self._execute_action(twist, durationtime=action["durationtime"])
  1108. # thread.join(timeout=5.0)
  1109. self.stop()
  1110. # self.pubSix_Arm(self.init_joints) # 机械臂收回
  1111. if not self.combination_mode and not self.interrupt_flag:
  1112. self.action_status_pub("dance_done")
  1113. def stop(self): # 停止
  1114. try:
  1115. twist = Twist()
  1116. twist.linear.x = 0.0
  1117. twist.linear.y = 0.0
  1118. twist.angular.z = 0.0
  1119. self.publisher.publish(twist)
  1120. except Exception as e:
  1121. self.get_logger().warn(f"stop() failed to publish twist: {e}")
  1122. def _execute_action(self, twist, num=1, durationtime=3.0):
  1123. for _ in range(num):
  1124. start_time = time.time()
  1125. while (time.time() - start_time) < durationtime:
  1126. if self.interrupt_flag:
  1127. self.stop()
  1128. return
  1129. self.publisher.publish(twist)
  1130. time.sleep(0.1)
  1131. # def check_apriltag_sort(self):
  1132. # """
  1133. # 检查相关进程是否存活
  1134. # """
  1135. # pass
  1136. # def apriltag_sort(self, target_id): # 夹取机器码
  1137. # self.check_apriltag_sort()
  1138. # target_idf = float(target_id)
  1139. # cmd1 = "ros2 run largemodel_arm grasp_desktop_apritag"
  1140. # cmd2 = f"ros2 run largemodel_arm apriltag_sort --ros-args -p target_id:={target_idf:.1f}"
  1141. # subprocess.Popen(
  1142. # [
  1143. # "gnome-terminal",
  1144. # "--title=grasp_desktop_apritag",
  1145. # "--",
  1146. # "bash",
  1147. # "-c",
  1148. # f"{cmd1}; exec bash",
  1149. # ]
  1150. # )
  1151. # subprocess.Popen(
  1152. # [
  1153. # "gnome-terminal",
  1154. # "--title=apriltag_sort",
  1155. # "--",
  1156. # "bash",
  1157. # "-c",
  1158. # f"{cmd2}; exec bash",
  1159. # ]
  1160. # )
  1161. #
  1162. # while not self.apriltag_sort_future.done():
  1163. # if self.interrupt_flag:
  1164. # self.check_apriltag_sort()
  1165. # self.stop()
  1166. # self.pubSix_Arm(self.init_joints)
  1167. # return
  1168. # time.sleep(0.1)
  1169. #
  1170. # result = self.apriltag_sort_future.result()
  1171. # if not self.interrupt_flag:
  1172. # if result.data == "apriltag_sort_done":
  1173. # self.action_status_pub("apriltag_sort_done", target_id=target_id)
  1174. # elif result.data == "apriltag_sort_failed":
  1175. # self.action_status_pub("apriltag_sort_failed", target_id=target_id)
  1176. #
  1177. # self.check_apriltag_sort()
  1178. # self.apriltag_sort_future = Future() # 复位Future对象
  1179. # def check_apriltag_remove_higher(self):
  1180. # """
  1181. # 检查相关进程是否存活
  1182. # """
  1183. # pass
  1184. # def apriltag_remove_higher(self, target_high): # 移除指定高度的机器码
  1185. # self.check_apriltag_remove_higher()
  1186. # target_highf = float(target_high) / 100
  1187. # cmd1 = "ros2 run largemodel_arm grasp_desktop_remove"
  1188. # cmd2 = f"ros2 run largemodel_arm apriltag_remove_higher --ros-args -p target_high:={target_highf:.2f}"
  1189. # subprocess.Popen(
  1190. # [
  1191. # "gnome-terminal",
  1192. # "--title=grasp_desktop_remove",
  1193. # "--",
  1194. # "bash",
  1195. # "-c",
  1196. # f"{cmd1}; exec bash",
  1197. # ]
  1198. # )
  1199. # subprocess.Popen(
  1200. # [
  1201. # "gnome-terminal",
  1202. # "--title=apriltag_remove_higher",
  1203. # "--",
  1204. # "bash",
  1205. # "-c",
  1206. # f"{cmd2}; exec bash",
  1207. # ]
  1208. # )
  1209. #
  1210. # while not self.apriltag_remove_higher_future.done():
  1211. # if self.interrupt_flag:
  1212. # self.check_apriltag_remove_higher()
  1213. # self.stop()
  1214. # self.pubSix_Arm(self.init_joints)
  1215. # return
  1216. # time.sleep(0.1)
  1217. # result = self.apriltag_remove_higher_future.result()
  1218. #
  1219. # if not self.interrupt_flag:
  1220. # if result.data == "apriltag_remove_higher_done":
  1221. # self.action_status_pub(
  1222. # "apriltag_remove_higher_done", target_high=target_high
  1223. # )
  1224. # elif result.data == "apriltag_remove_higher_failed":
  1225. # self.action_status_pub(
  1226. # "apriltag_remove_higher_failed", target_high=target_high
  1227. # )
  1228. #
  1229. # self.check_apriltag_remove_higher()
  1230. # self.apriltag_remove_higher_future = Future() # 复位Future对象
  1231. # self.pubSix_Arm(self.init_joints)
  1232. # def check_color_remove_higher(self):
  1233. # """
  1234. # 检查相关进程是否存活
  1235. # """
  1236. # pass
  1237. # def color_remove_higher(self, color, target_high):
  1238. # self.check_color_remove_higher()
  1239. # arm_joints = [90, 110, 0, 0, 90, 0]
  1240. # self.pubSix_Arm(arm_joints)
  1241. # color = color.strip("'\"") # 去掉单引号和双引号
  1242. # target_highf = float(target_high) / 100
  1243. # if color == "red":
  1244. # target_color = float(1)
  1245. # elif color == "green":
  1246. # target_color = float(2)
  1247. # elif color == "blue":
  1248. # target_color = float(3)
  1249. # elif color == "yellow":
  1250. # target_color = float(4)
  1251. # else:
  1252. # self.get_logger().info(
  1253. # "Fatal ERROR:Incorrect color input,Does the AI output not meet expectations?"
  1254. # )
  1255. # self.action_status_pub(
  1256. # "color_remove_higher_failed", color=color, target_high=target_high
  1257. # )
  1258. # return
  1259. #
  1260. # cmd1 = "ros2 run largemodel_arm grasp_desktop_remove_color"
  1261. # cmd2 = f"ros2 run largemodel_arm color_remove_higher --ros-args -p target_high:={target_highf:.2f} -p target_color:={target_color:.1f}"
  1262. # subprocess.Popen(
  1263. # [
  1264. # "gnome-terminal",
  1265. # "--title=grasp_desktop_remove_color",
  1266. # "--",
  1267. # "bash",
  1268. # "-c",
  1269. # f"{cmd1}; exec bash",
  1270. # ]
  1271. # )
  1272. # subprocess.Popen(
  1273. # [
  1274. # "gnome-terminal",
  1275. # "--title=color_remove_higher",
  1276. # "--",
  1277. # "bash",
  1278. # "-c",
  1279. # f"{cmd2}; exec bash",
  1280. # ]
  1281. # )
  1282. #
  1283. # while not self.color_remove_higher_future.done():
  1284. # if self.interrupt_flag:
  1285. # self.check_color_remove_higher()
  1286. # self.stop()
  1287. # self.pubSix_Arm(self.init_joints)
  1288. # return
  1289. # time.sleep(0.1)
  1290. #
  1291. # result = self.color_remove_higher_future.result()
  1292. # if not self.interrupt_flag:
  1293. # if result.data == "color_remove_higher_done":
  1294. # self.action_status_pub(
  1295. # "color_remove_higher_done", color=color, target_high=target_high
  1296. # )
  1297. # else:
  1298. # self.action_status_pub(
  1299. # "color_remove_higher_failed", color=color, target_high=target_high
  1300. # )
  1301. #
  1302. # self.check_color_remove_higher()
  1303. # self.color_remove_higher_future = Future() # 复位Future对象
  1304. # self.pubSix_Arm(self.init_joints)
  1305. # def check_follw_line_clear(self):
  1306. # """
  1307. # 检查相关进程是否存活
  1308. # """
  1309. # pass
  1310. # def follw_line_clear(self) -> None:
  1311. # self.check_follw_line_clear()
  1312. # cmd1 = "ros2 run largemodel_arm grasp_desktop_remove"
  1313. # cmd2 = "ros2 run largemodel_arm follow_line --ros-args -p start_follow:=True"
  1314. # subprocess.Popen(
  1315. # [
  1316. # "gnome-terminal",
  1317. # "--title=grasp_desktop_remove",
  1318. # "--",
  1319. # "bash",
  1320. # "-c",
  1321. # f"{cmd1}; exec bash",
  1322. # ]
  1323. # )
  1324. # subprocess.Popen(
  1325. # [
  1326. # "gnome-terminal",
  1327. # "--title=follow_line",
  1328. # "--",
  1329. # "bash",
  1330. # "-c",
  1331. # f"{cmd2}; exec bash",
  1332. # ]
  1333. # )
  1334. #
  1335. # while not self.follw_line_clear_future.done():
  1336. # if self.interrupt_flag:
  1337. # self.check_follw_line_clear()
  1338. # self.stop()
  1339. # self.pubSix_Arm(self.init_joints)
  1340. # return
  1341. # time.sleep(0.1)
  1342. #
  1343. # if not self.interrupt_flag:
  1344. # if self.follw_line_clear_future.result() is not None:
  1345. # self.action_status_pub("follw_line_clear_done")
  1346. #
  1347. # self.check_follw_line_clear()
  1348. # self.follw_line_clear_future = Future() # 复位Future对象
  1349. # self.pubSix_Arm(self.init_joints)
  1350. # def check_all_process(self):
  1351. # """
  1352. # 检查所有相关进程是否存活
  1353. # """
  1354. # pass
  1355. # 核心程序,解析动作列表并执行 # Core program, parse and execute action list
  1356. def execute_callback(self, goal_handle):
  1357. """
  1358. 动作执行回调函数:分3种情况: # Action execution callback function: divided into 3 cases:
  1359. 1. 动作列表为空 # 1. Empty action list
  1360. 2. 动作列表长度为1 # 2. Action list length is 1
  1361. 3. 动作列表长度大于1 # 3. Action list length is greater than 1
  1362. 文字交互模式下,不进行语音合成和播放 # In text interaction mode, no voice synthesis or playback is performed
  1363. """
  1364. if self.is_recording:
  1365. goal_handle.succeed()
  1366. result = Rot.Result()
  1367. result.success = True
  1368. return result
  1369. feedback_msg = Rot.Feedback()
  1370. actions = goal_handle.request.actions
  1371. self.action_runing = True
  1372. if not actions: # 动作列表为空 # If the action list is empty
  1373. if not self.text_chat_mode and (
  1374. goal_handle.request.llm_response is not None
  1375. or goal_handle.request.text_response != ""
  1376. ): # 语音模式,播放对话 # Voice mode, play dialogue
  1377. self.get_logger().info(f"[TTS] 开始合成: {goal_handle.request.llm_response[:50]}...")
  1378. result = self.model_client.voice_synthesis(
  1379. goal_handle.request.llm_response, self.tts_out_path
  1380. )
  1381. self.get_logger().info(f"[TTS] 合成完成, result={result}, 路径={self.tts_out_path}")
  1382. self.play_audio(self.tts_out_path, feedback=True)
  1383. else:
  1384. self.action_status_pub("response_done")
  1385. elif len(actions) == 1: # 动作列表长度为1 # If the action list length is 1
  1386. action = actions[0]
  1387. if not self.text_chat_mode and (
  1388. goal_handle.request.llm_response is not None
  1389. or goal_handle.request.text_response != ""
  1390. ): # 语音模式,播放对话 # Voice mode, play dialogue
  1391. self.model_client.voice_synthesis(
  1392. goal_handle.request.llm_response, self.tts_out_path
  1393. )
  1394. self.play_audio(self.tts_out_path)
  1395. match = re.match(r"(\w+)\((.*)\)", action)
  1396. action_name, args_str = match.groups()
  1397. # ask_user() 特殊处理:播放已在上面执行,只需播放完成后发布 continue_listen
  1398. if action_name == "ask_user":
  1399. # 播放完成后发布 continue_listen(播放已在上面执行)
  1400. self.publish_continue_listen()
  1401. elif not hasattr(self, action_name):
  1402. self.get_logger().warning(
  1403. f"action_service: {action} is invalid action,skip execution"
  1404. )
  1405. self.action_status_pub(
  1406. "failure_execute_action_function_not_exists"
  1407. ) # Robot feedback: action function does not exist, cannot execute
  1408. else:
  1409. action_name, args_str = match.groups()
  1410. args = [arg.strip() for arg in args_str.split(",")] if args_str else []
  1411. self.get_logger().info(f"Executing action: {action_name} with args: {args}")
  1412. method = getattr(self, action_name)
  1413. method(*args)
  1414. if self.interrupt_flag:
  1415. self.interrupt_flag = False
  1416. else: # 动作列表长度大于1,使能组合模式 # If the action list length is greater than 1, enable combination mode
  1417. self.combination_mode = True
  1418. if not self.text_chat_mode and (
  1419. goal_handle.request.llm_response is not None
  1420. or goal_handle.request.text_response != ""
  1421. ): # 语音模式,播放对话 # Voice mode, play dialogue
  1422. self.model_client.voice_synthesis(
  1423. goal_handle.request.llm_response, self.tts_out_path
  1424. )
  1425. self.play_audio_async(self.tts_out_path)
  1426. for action in actions:
  1427. if self.interrupt_flag:
  1428. break
  1429. match = re.match(r"(\w+)\((.*)\)", action)
  1430. action_name, args_str = match.groups()
  1431. args = [arg.strip() for arg in args_str.split(",")] if args_str else []
  1432. if not hasattr(self, action_name):
  1433. self.get_logger().warning(
  1434. f"action_service: {action} is invalid action,skip execution" # action_service: {action} is an invalid action, skip execution
  1435. )
  1436. self.action_status_pub(
  1437. "failure_execute_action_function_not_exists"
  1438. ) # Robot feedback: action function does not exist, cannot execute
  1439. else:
  1440. method = getattr(self, action_name)
  1441. method(*args)
  1442. feedback_msg.status = f"action service execute {action} successed"
  1443. if not self.interrupt_flag:
  1444. self.action_status_pub(
  1445. "multiple_done", actions=actions
  1446. ) # Robot feedback: execution of {actions} completed
  1447. self.combination_mode = (
  1448. False # 重置组合模式标志位 # Reset combination mode flag
  1449. )
  1450. self.stop() # 执行完全部动作停止机器人 # Stop the robot after executing all actions
  1451. self.action_runing = False # 重置运行标志位 # Reset running flag
  1452. self.interrupt_flag = False
  1453. goal_handle.succeed()
  1454. result = Rot.Result()
  1455. result.success = True
  1456. return result
  1457. def finish_dialogue(self): # 发布AI模型结束当前流程标志
  1458. self.first_record = True # 重置导航记录标志位 # Reset navigation record flag
  1459. self.is_recording = False # 重置录音标志位 # Reset recording flag
  1460. # self.pubSix_Arm(self.init_joints) # 机械臂收回
  1461. self.action_status_pub("finish") # 结束当前任务
  1462. def finishtask(self):
  1463. """
  1464. 空操作,不反馈消息,用于结束反馈
  1465. """
  1466. return
  1467. def ask_user(self):
  1468. """
  1469. 多轮对话追问:播放 response 后发布 continue_listen
  1470. """
  1471. pass
  1472. def publish_continue_listen(self):
  1473. """
  1474. 发布 continue_listen 指令,让 ASR 继续监听
  1475. """
  1476. # 关闭人物靠近检测,避免在多轮对话期间重复触发
  1477. if self.process_map['person_approach']['running']:
  1478. pid = self.process_map['person_approach']['pid']
  1479. try:
  1480. import os
  1481. os.kill(pid, signal.SIGTERM)
  1482. self.process_map['person_approach']['running'] = False
  1483. self.get_logger().info(f"Stopped person_approach node for dialogue, PID: {pid}")
  1484. except Exception as e:
  1485. self.get_logger().warn(f"Failed to stop person_approach: {e}")
  1486. msg = String()
  1487. msg.data = "continue_listen"
  1488. self.asr_control_pub.publish(msg)
  1489. self.get_logger().info("[多轮对话] ask_user 播放完成,已发布 /asr/control: continue_listen")
  1490. def kill_process_tree(self, pid):
  1491. try:
  1492. parent = psutil.Process(pid)
  1493. children = parent.children(recursive=True)
  1494. # 先终止所有子进程
  1495. for child in children:
  1496. try:
  1497. child.terminate()
  1498. except psutil.NoSuchProcess:
  1499. pass
  1500. # 等待子进程终止
  1501. gone, alive = psutil.wait_procs(children, timeout=3)
  1502. # 强制杀死仍然存活的进程
  1503. for p in alive:
  1504. try:
  1505. p.kill()
  1506. except psutil.NoSuchProcess:
  1507. pass
  1508. # 最后终止父进程
  1509. try:
  1510. parent.terminate()
  1511. parent.wait(timeout=3)
  1512. except psutil.TimeoutExpired:
  1513. parent.kill()
  1514. except psutil.NoSuchProcess:
  1515. pass
  1516. except psutil.NoSuchProcess:
  1517. pass
  1518. def _on_shutdown(self):
  1519. """
  1520. ROS2 节点关闭时的回调,确保清理所有子进程
  1521. """
  1522. self.get_logger().info("[Shutdown] Cleaning up child processes...")
  1523. # 取消防抖定时器
  1524. if self._config_debounce_timer:
  1525. self._config_debounce_timer.cancel()
  1526. # 清理所有管理的子进程
  1527. for process_name, process_info in self.process_map.items():
  1528. if process_info['pid'] is not None:
  1529. self.get_logger().info(f"[Shutdown] Killing {process_name} (PID: {process_info['pid']})")
  1530. self.kill_process_tree(process_info['pid'])
  1531. process_info['pid'] = None
  1532. self.get_logger().info("[Shutdown] All child processes cleaned up")
  1533. def kill_process_tree(self, pid):
  1534. """
  1535. 彻底杀死进程及其所有子进程(修复版)
  1536. """
  1537. import os
  1538. import time
  1539. try:
  1540. parent = psutil.Process(pid)
  1541. children = parent.children(recursive=True)
  1542. self.get_logger().info(f"[Shutdown] Found {len(children)} children for PID {pid}")
  1543. # 1. 先向所有子进程发送 SIGTERM
  1544. for child in children:
  1545. try:
  1546. child.terminate()
  1547. self.get_logger().info(f"[Shutdown] Sent SIGTERM to child PID {child.pid}")
  1548. except psutil.NoSuchProcess:
  1549. pass
  1550. # 2. 同时向父进程发送 SIGTERM
  1551. try:
  1552. parent.terminate()
  1553. self.get_logger().info(f"[Shutdown] Sent SIGTERM to parent PID {pid}")
  1554. except psutil.NoSuchProcess:
  1555. pass
  1556. # 3. 等待一小段时间让进程响应
  1557. time.sleep(0.5)
  1558. # 4. 检查哪些进程还活着,发送 SIGKILL
  1559. alive = []
  1560. for child in children:
  1561. try:
  1562. if child.is_running():
  1563. alive.append(child)
  1564. except psutil.NoSuchProcess:
  1565. pass
  1566. try:
  1567. if parent.is_running():
  1568. alive.append(parent)
  1569. except psutil.NoSuchProcess:
  1570. pass
  1571. for p in alive:
  1572. try:
  1573. p.kill()
  1574. self.get_logger().info(f"[Shutdown] Sent SIGKILL to PID {p.pid}")
  1575. except psutil.NoSuchProcess:
  1576. pass
  1577. except psutil.NoSuchProcess:
  1578. self.get_logger().info(f"[Shutdown] PID {pid} already terminated")
  1579. except Exception as e:
  1580. self.get_logger().warn(f"[Shutdown] Error killing process tree: {e}")
  1581. # 5. 额外措施:尝试使用 killpg 杀死进程组(针对 ros2 launch)
  1582. try:
  1583. import os
  1584. pgid = os.getpgid(pid)
  1585. os.killpg(pgid, signal.SIGKILL)
  1586. self.get_logger().info(f"[Shutdown] Killed process group {pgid}")
  1587. except ProcessLookupError:
  1588. self.get_logger().info(f"[Shutdown] Process group already terminated")
  1589. except Exception as e:
  1590. self.get_logger().warn(f"[Shutdown] killpg failed: {e}")
  1591. # 6. 最后手段:使用 ros2 命令停止节点
  1592. try:
  1593. subprocess.run(["ros2", "node", "kill", "/person_approach_node"],
  1594. capture_output=True, timeout=2)
  1595. self.get_logger().info("[Shutdown] Sent ros2 node kill for person_approach_node")
  1596. except Exception:
  1597. pass
  1598. def play_audio(self, file_path: str, feedback: Bool = False) -> None:
  1599. """
  1600. 同步方式播放音频函数The function for playing audio in synchronous mode
  1601. """
  1602. self.get_logger().info(f"play_audio called: file={file_path}, is_recording={self.is_recording}")
  1603. if self.is_recording:
  1604. self.get_logger().warn("play_audio: is_recording=True, skip")
  1605. return
  1606. if pygame.mixer.music.get_busy():
  1607. pygame.mixer.music.stop()
  1608. self.stop_event.clear()
  1609. self.get_logger().info(f"play_audio: loading {file_path}")
  1610. pygame.mixer.music.load(file_path)
  1611. pygame.mixer.music.play()
  1612. self.get_logger().info(f"play_audio: started playing {file_path}")
  1613. while pygame.mixer.music.get_busy():
  1614. if self.stop_event.is_set() or self.is_recording:
  1615. pygame.mixer.music.stop()
  1616. self.stop_event.clear()
  1617. self.get_logger().info("play_audio: stopped by event")
  1618. return
  1619. pygame.time.Clock().tick(10)
  1620. self.get_logger().info("play_audio: playback finished")
  1621. if feedback:
  1622. self.action_status_pub("response_done")
  1623. def play_audio_async(self, file_path: str, feedback: Bool = False) -> None:
  1624. """
  1625. 异步方式播放音频函数The function for playing audio in asynchronous mode
  1626. """
  1627. if self.is_recording:
  1628. self.get_logger().warn("play_audio_async: is_recording=True, skip")
  1629. return
  1630. def target():
  1631. try:
  1632. if pygame.mixer.music.get_busy():
  1633. pygame.mixer.music.stop() # 只在播放中才停止
  1634. pygame.mixer.music.load(file_path)
  1635. self.stop_event.clear() # 清除停止事件,在播放前清除
  1636. pygame.mixer.music.play()
  1637. self.get_logger().info(f"play_audio_async: started playing {file_path}")
  1638. while pygame.mixer.music.get_busy():
  1639. if self.stop_event.is_set() or self.is_recording:
  1640. pygame.mixer.music.stop()
  1641. self.stop_event.clear()
  1642. self.get_logger().info("play_audio_async: stopped by event")
  1643. return
  1644. pygame.time.Clock().tick(5)
  1645. self.get_logger().info("play_audio_async: playback finished")
  1646. if feedback:
  1647. self.action_status_pub("response_done")
  1648. except Exception as e:
  1649. self.get_logger().error(f"play_audio_async error: {e}")
  1650. thread = threading.Thread(target=target)
  1651. thread.daemon = True
  1652. thread.start()
  1653. def save_single_image(self):
  1654. """
  1655. 保存一张图片 / Save a single image
  1656. """
  1657. if self.image_msg is None:
  1658. self.get_logger().warning("No image received yet.") # 尚未接收到图像...
  1659. return
  1660. try:
  1661. # 将ROS图像消息转换为OpenCV图像 / Convert ROS image message to OpenCV image
  1662. cv_image = self.bridge.imgmsg_to_cv2(self.image_msg, "bgr8")
  1663. # 保存图片 / Save the image
  1664. cv2.imwrite(self.image_save_path, cv_image)
  1665. self.get_logger().info(f"Image saved to {self.image_save_path}")
  1666. except Exception as e:
  1667. self.get_logger().error(f"Error saving image: {e}") # 保存图像时出错...
  1668. def display_saved_image(self):
  1669. """
  1670. 显示已保存的图片4秒后关闭窗口 / Display the saved image for 4 seconds before closing the window
  1671. Note: 在无头环境或服务器环境中,图像显示功能被禁用,仅保留图片保存功能
  1672. """
  1673. # Image display is disabled in headless/server environments
  1674. # The image has been saved in save_single_image() for downstream processing
  1675. pass
  1676. def image_callback(self, msg): # 图像回调函数 / Image callback function
  1677. self.image_msg = msg
  1678. def main(args=None):
  1679. rclpy.init(args=args)
  1680. custom_action_server = CustomActionServer()
  1681. executor = MultiThreadedExecutor(num_threads=6)
  1682. executor.add_node(custom_action_server)
  1683. try:
  1684. executor.spin()
  1685. except KeyboardInterrupt:
  1686. pass
  1687. finally:
  1688. # 先调用 destroy_node 清理子进程(这会触发 _on_shutdown)
  1689. custom_action_server.destroy_node()
  1690. executor.shutdown()
  1691. rclpy.shutdown()
  1692. if __name__ == "__main__":
  1693. main()