action_service.py 71 KB

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