competition_task1_manager.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. #!/usr/bin/env python3
  2. """Run competition task 1 after a valid voice command."""
  3. import json
  4. import math
  5. import subprocess
  6. import sys
  7. import threading
  8. import time
  9. from pathlib import Path
  10. import actionlib
  11. import rospy
  12. import rospkg
  13. from actionlib_msgs.msg import GoalStatus
  14. from geometry_msgs.msg import PoseWithCovarianceStamped, Quaternion, Twist
  15. from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal
  16. from std_msgs.msg import String
  17. from std_srvs.srv import SetBool, Trigger
  18. class TaskFailure(Exception):
  19. """A controlled task failure that must not use stale TTS output."""
  20. class CompetitionTask1Manager:
  21. WALLS = ("wall1", "wall2", "wall3")
  22. def __init__(self):
  23. self._lock = threading.Lock()
  24. self._status_pub = rospy.Publisher(
  25. "/competition_task1/status", String, queue_size=10, latch=True
  26. )
  27. self._orders_pub = rospy.Publisher(
  28. "/competition_task1/orders", String, queue_size=1, latch=True
  29. )
  30. self._initial_pose_pub = rospy.Publisher(
  31. "/initialpose", PoseWithCovarianceStamped, queue_size=1
  32. )
  33. self._client = actionlib.SimpleActionClient("move_base", MoveBaseAction)
  34. self._cmd_vel_pub = rospy.Publisher("/cmd_vel", Twist, queue_size=1)
  35. self._latest_amcl_yaw = None
  36. self._latest_amcl_x = None
  37. self._latest_amcl_y = None
  38. self._latest_amcl_time = 0.0
  39. self._amcl_sub = rospy.Subscriber(
  40. "/amcl_pose", PoseWithCovarianceStamped, self._amcl_callback, queue_size=10
  41. )
  42. self._qr_sub = rospy.Subscriber(
  43. "/qr_scan_result", String, self._qr_callback, queue_size=1
  44. )
  45. self._active_wall = None
  46. self._products = {wall: None for wall in self.WALLS}
  47. self._seen_products = set()
  48. self._product_order = []
  49. self._scan_deadline = None
  50. self._config = self._load_config()
  51. self._frame_id = self._config["frame_id"]
  52. self._start_pose = self._config["start_pose"]
  53. self._initial_pose_settle = float(self._config["initial_pose_settle_seconds"])
  54. self._initial_pose_position_tolerance = float(
  55. self._config["initial_pose_position_tolerance_m"]
  56. )
  57. self._initial_pose_yaw_tolerance = float(
  58. self._config["initial_pose_yaw_tolerance_rad"]
  59. )
  60. self._points = self._config["points"]
  61. self._scan_window = float(self._config["scan_window_seconds"])
  62. self._settle = float(self._config["scan_settle_seconds"])
  63. self._center_rotation_speed = abs(float(
  64. self._config["center_rotation_speed_rad_s"]
  65. ))
  66. self._center_rotation_tolerance = float(
  67. self._config["center_rotation_yaw_tolerance_rad"]
  68. )
  69. self._center_rotation_timeout = float(
  70. self._config["center_rotation_timeout_seconds"]
  71. )
  72. self._scan_timeout = float(self._config["scan_total_timeout_seconds"])
  73. self._initial_navigation_timeout = float(
  74. self._config["initial_navigation_timeout_seconds"]
  75. )
  76. self._server_timeout = float(
  77. self._config["move_base_server_timeout_seconds"]
  78. )
  79. rospy.on_shutdown(self._shutdown)
  80. @staticmethod
  81. def _load_config():
  82. package_path = Path(rospkg.RosPack().get_path("ucar_nav"))
  83. config_path = package_path / "config" / "competition" / "task1_scan_points.json"
  84. with config_path.open("r", encoding="utf-8") as stream:
  85. config = json.load(stream)
  86. if config.get("frame_id") != "map":
  87. raise TaskFailure("任务1扫码点必须使用 map 坐标系")
  88. return config
  89. def _publish(self, state, detail):
  90. message = "%s: %s" % (state, detail)
  91. rospy.loginfo("competition task1 %s", message)
  92. self._status_pub.publish(String(data=message))
  93. def _amcl_callback(self, message):
  94. orientation = message.pose.pose.orientation
  95. yaw = math.atan2(
  96. 2.0 * (orientation.w * orientation.z + orientation.x * orientation.y),
  97. 1.0 - 2.0 * (orientation.y * orientation.y + orientation.z * orientation.z),
  98. )
  99. with self._lock:
  100. self._latest_amcl_x = message.pose.pose.position.x
  101. self._latest_amcl_y = message.pose.pose.position.y
  102. self._latest_amcl_yaw = yaw
  103. self._latest_amcl_time = time.monotonic()
  104. def _set_initial_pose(self):
  105. try:
  106. x = float(self._start_pose["x"])
  107. y = float(self._start_pose["y"])
  108. yaw = float(self._start_pose["yaw"])
  109. except (KeyError, TypeError, ValueError) as error:
  110. raise TaskFailure("初始位姿配置无效:%s" % error)
  111. if not all(math.isfinite(value) for value in (x, y, yaw)):
  112. raise TaskFailure("初始位姿包含非有限数值")
  113. message = PoseWithCovarianceStamped()
  114. message.header.frame_id = self._frame_id
  115. message.pose.pose.position.x = x
  116. message.pose.pose.position.y = y
  117. message.pose.pose.orientation.z = math.sin(yaw / 2.0)
  118. message.pose.pose.orientation.w = math.cos(yaw / 2.0)
  119. message.pose.covariance[0] = 0.25
  120. message.pose.covariance[7] = 0.25
  121. message.pose.covariance[35] = 0.0685
  122. self._publish("INITIALIZING", "设置AMCL初始位姿 x=%.3f y=%.3f yaw=%.3f" % (x, y, yaw))
  123. for _ in range(3):
  124. message.header.stamp = rospy.Time.now()
  125. self._initial_pose_pub.publish(message)
  126. rospy.sleep(0.1)
  127. deadline = time.monotonic() + self._initial_pose_settle
  128. while not rospy.is_shutdown() and time.monotonic() < deadline:
  129. with self._lock:
  130. current_x = self._latest_amcl_x
  131. current_y = self._latest_amcl_y
  132. current_yaw = self._latest_amcl_yaw
  133. if current_x is not None and current_y is not None and current_yaw is not None:
  134. position_error = math.hypot(current_x - x, current_y - y)
  135. yaw_error = abs(self._wrap_to_pi(current_yaw - yaw))
  136. if (position_error <= self._initial_pose_position_tolerance
  137. and yaw_error <= self._initial_pose_yaw_tolerance):
  138. self._publish("INITIALIZED", "AMCL已确认初始位姿")
  139. return
  140. rospy.sleep(0.05)
  141. raise TaskFailure("AMCL未确认自动设置的初始位姿")
  142. def _shutdown(self):
  143. self._client.cancel_all_goals()
  144. self._stop_robot()
  145. self._set_scanner_mode(False, False, required=False)
  146. def _qr_callback(self, message):
  147. product = message.data.strip()
  148. if not product:
  149. return
  150. with self._lock:
  151. if product in self._seen_products:
  152. rospy.logwarn("task1 ignored duplicate product: %s", product)
  153. return
  154. self._seen_products.add(product)
  155. self._product_order.append(product)
  156. wall = self._active_wall
  157. if wall is not None and self._products[wall] is None:
  158. self._products[wall] = product
  159. self._active_wall = None
  160. location = wall if wall is not None else "scan_area"
  161. self._publish("QR_FOUND", "%s识别到%s(第%d个商品)" % (
  162. location, product, len(self._product_order)
  163. ))
  164. def _reset_qr_scanner(self):
  165. try:
  166. rospy.wait_for_service("/qr_scan/reset", timeout=3.0)
  167. response = rospy.ServiceProxy("/qr_scan/reset", Trigger)()
  168. except (rospy.ROSException, rospy.ServiceException) as error:
  169. raise TaskFailure("二维码节点未就绪或不支持重置:%s" % error)
  170. if not response.success:
  171. raise TaskFailure("二维码节点重置失败:%s" % response.message)
  172. @staticmethod
  173. def _set_bool_service(name, value, required):
  174. try:
  175. rospy.wait_for_service(name, timeout=3.0)
  176. response = rospy.ServiceProxy(name, SetBool)(value)
  177. except (rospy.ROSException, rospy.ServiceException) as error:
  178. if required:
  179. raise TaskFailure("二维码服务%s不可用:%s" % (name, error))
  180. rospy.logwarn("二维码服务%s关闭失败:%s", name, error)
  181. return
  182. if not response.success:
  183. if required:
  184. raise TaskFailure("二维码服务%s切换失败:%s" % (name, response.message))
  185. rospy.logwarn("二维码服务%s关闭失败:%s", name, response.message)
  186. def _set_scanner_mode(self, npu_enabled, decode_enabled, required=True):
  187. # Decode is toggled first so idle mode never fetches QR URLs.
  188. self._set_bool_service("/qr_scan/set_decode_enabled", decode_enabled, required)
  189. self._set_bool_service("/qr_scan/set_npu_enabled", npu_enabled, required)
  190. def _remaining_scan_time(self):
  191. if self._scan_deadline is None:
  192. return None
  193. return self._scan_deadline - time.monotonic()
  194. def _require_scan_time(self):
  195. remaining = self._remaining_scan_time()
  196. if remaining is not None and remaining <= 0.0:
  197. raise TaskFailure("扫码总超时(%.0f 秒)" % self._scan_timeout)
  198. return remaining
  199. def _goal_from_point(self, name):
  200. try:
  201. point = self._points[name]
  202. x = float(point["x"])
  203. y = float(point["y"])
  204. yaw = float(point["yaw"])
  205. except (KeyError, TypeError, ValueError) as error:
  206. raise TaskFailure("扫码点%s配置无效:%s" % (name, error))
  207. if not all(math.isfinite(value) for value in (x, y, yaw)):
  208. raise TaskFailure("扫码点%s包含非有限数值" % name)
  209. goal = MoveBaseGoal()
  210. goal.target_pose.header.frame_id = self._frame_id
  211. goal.target_pose.header.stamp = rospy.Time.now()
  212. goal.target_pose.pose.position.x = x
  213. goal.target_pose.pose.position.y = y
  214. goal.target_pose.pose.orientation = Quaternion(
  215. z=math.sin(yaw / 2.0), w=math.cos(yaw / 2.0)
  216. )
  217. return goal
  218. def _navigate(self, point_name, initial=False):
  219. remaining = None if initial else self._require_scan_time()
  220. timeout = self._initial_navigation_timeout if initial else remaining
  221. goal = self._goal_from_point(point_name)
  222. self._publish(
  223. "NAVIGATING",
  224. "%s x=%.3f y=%.3f yaw=%.3f" % (
  225. point_name,
  226. goal.target_pose.pose.position.x,
  227. goal.target_pose.pose.position.y,
  228. 2.0 * math.atan2(
  229. goal.target_pose.pose.orientation.z,
  230. goal.target_pose.pose.orientation.w,
  231. ),
  232. ),
  233. )
  234. self._client.send_goal(goal)
  235. deadline = time.monotonic() + timeout
  236. terminal_states = {
  237. GoalStatus.PREEMPTED, GoalStatus.ABORTED, GoalStatus.REJECTED,
  238. GoalStatus.RECALLED, GoalStatus.LOST,
  239. }
  240. while not rospy.is_shutdown() and time.monotonic() < deadline:
  241. state = self._client.get_state()
  242. if state == GoalStatus.SUCCEEDED:
  243. return
  244. if state in terminal_states:
  245. raise TaskFailure("未到达%s,move_base状态码%d" % (point_name, state))
  246. rospy.sleep(0.05)
  247. self._client.cancel_goal()
  248. if initial:
  249. raise TaskFailure("前往领取区初始点超时")
  250. raise TaskFailure("前往%s时扫码总超时" % point_name)
  251. def _wrap_to_pi(self, angle):
  252. return math.atan2(math.sin(angle), math.cos(angle))
  253. def _stop_robot(self):
  254. command = Twist()
  255. for _ in range(3):
  256. self._cmd_vel_pub.publish(command)
  257. rospy.sleep(0.03)
  258. def _turn_in_place(self, target_point):
  259. target = float(self._points[target_point]["yaw"])
  260. remaining = self._require_scan_time()
  261. timeout = min(self._center_rotation_timeout, remaining)
  262. deadline = time.monotonic() + timeout
  263. self._client.cancel_all_goals()
  264. self._stop_robot()
  265. self._publish(
  266. "ROTATING",
  267. "%s slow in-place turn to %.3f rad at %.2f rad/s" % (
  268. target_point, target, self._center_rotation_speed
  269. ),
  270. )
  271. try:
  272. while not rospy.is_shutdown() and time.monotonic() < deadline:
  273. with self._lock:
  274. yaw = self._latest_amcl_yaw
  275. if yaw is None:
  276. rospy.sleep(0.05)
  277. continue
  278. error = self._wrap_to_pi(target - yaw)
  279. if abs(error) <= self._center_rotation_tolerance:
  280. return
  281. command = Twist()
  282. command.angular.z = (
  283. self._center_rotation_speed if error > 0.0
  284. else -self._center_rotation_speed
  285. )
  286. self._cmd_vel_pub.publish(command)
  287. rospy.sleep(0.05)
  288. finally:
  289. self._stop_robot()
  290. raise TaskFailure("中央原地转向%s超时" % target_point)
  291. def _scan_wall(self, wall, point_name):
  292. if self._all_walls_found():
  293. return True
  294. if self._products[wall] is not None:
  295. self._publish("SKIPPED", "%s已经识别,跳过%s" % (wall, point_name))
  296. return True
  297. self._require_scan_time()
  298. self._set_scanner_mode(True, True)
  299. try:
  300. rospy.sleep(min(self._settle, self._require_scan_time()))
  301. with self._lock:
  302. self._active_wall = wall
  303. self._publish("SCANNING", "%s在%s等待二维码" % (wall, point_name))
  304. deadline = time.monotonic() + min(
  305. self._scan_window, self._require_scan_time()
  306. )
  307. while not rospy.is_shutdown() and time.monotonic() < deadline:
  308. with self._lock:
  309. if self._products[wall] is not None or self._all_walls_found():
  310. return True
  311. rospy.sleep(0.05)
  312. with self._lock:
  313. return self._products[wall] is not None or self._all_walls_found()
  314. finally:
  315. with self._lock:
  316. if self._active_wall == wall:
  317. self._active_wall = None
  318. self._set_scanner_mode(False, False, required=False)
  319. def _all_walls_found(self):
  320. return len(self._product_order) >= len(self.WALLS)
  321. def _clear_previous_files(self):
  322. spark_dir = self._spark_dir()
  323. for filename in ("tts_output.txt", "llm_result.json", "qr_products_input.json"):
  324. path = spark_dir / "audio" / filename
  325. if path.exists():
  326. path.unlink()
  327. @staticmethod
  328. def _spark_dir():
  329. speech_path = Path(rospkg.RosPack().get_path("speech_command"))
  330. return speech_path.parent / "SparkTalk"
  331. def _write_products_for_spark(self):
  332. products = list(self._product_order)
  333. if len(products) != len(self.WALLS):
  334. raise TaskFailure("未收集到三个不同商品")
  335. output = self._spark_dir() / "audio" / "qr_products_input.json"
  336. payload = {"products": products, "wall_products": self._products}
  337. with output.open("w", encoding="utf-8") as stream:
  338. json.dump(payload, stream, ensure_ascii=False, indent=2)
  339. self._publish("QR_COMPLETE", "已收集%s" % "、".join(products))
  340. @staticmethod
  341. def _factory_category(category):
  342. return {"食品": "食品", "日用品": "日用品", "电子产品": "电子"}.get(str(category).strip())
  343. @staticmethod
  344. def _warehouse_for(category):
  345. return {"食品": "食品加工车间", "日用品": "日用品加工车间",
  346. "电子产品": "电子产品生产车间"}.get(category)
  347. def _publish_orders(self, spark_dir):
  348. path = spark_dir / "audio" / "llm_result.json"
  349. try:
  350. with path.open("r", encoding="utf-8") as stream:
  351. result = json.load(stream)
  352. raw_tasks = [("real_task", result["real_task"]),
  353. ("simulation_task", result["simulation_task"])]
  354. orders = []
  355. for order_id, task in raw_tasks:
  356. if task.get("success") is not True:
  357. raise ValueError("%s未成功分类" % order_id)
  358. product = str(task["matched_product"]).strip()
  359. category = str(task["category"]).strip()
  360. factory_category = self._factory_category(category)
  361. warehouse = self._warehouse_for(category)
  362. if not product or factory_category is None or warehouse is None:
  363. raise ValueError("%s字段或类别无效" % order_id)
  364. orders.append({"order_id": order_id, "product": product,
  365. "category": category, "factory_category": factory_category,
  366. "warehouse": warehouse})
  367. if orders[0]["factory_category"] == orders[1]["factory_category"]:
  368. raise ValueError("两份订单类别相同")
  369. except (OSError, ValueError, TypeError, KeyError, json.JSONDecodeError) as error:
  370. raise TaskFailure("星火订单结果无效:%s" % error)
  371. self._orders_pub.publish(String(data=json.dumps({"orders": orders}, ensure_ascii=False, sort_keys=True)))
  372. self._publish("ORDERS_READY", "已发布真实与仿真两份订单")
  373. def _run_spark(self):
  374. spark_dir = self._spark_dir()
  375. result = subprocess.run([sys.executable, "SparkMain.py"], cwd=str(spark_dir))
  376. if result.returncode != 0:
  377. raise TaskFailure("星火分类或JSON校验失败,退出码%d" % result.returncode)
  378. self._publish_orders(spark_dir)
  379. self._publish("COMPLETE", "任务1完成,等待语音播报")
  380. def run(self):
  381. self._clear_previous_files()
  382. self._publish("WAITING_FOR_NAV", "等待move_base")
  383. if not self._client.wait_for_server(rospy.Duration(self._server_timeout)):
  384. raise TaskFailure("%s秒内未连接到move_base" % self._server_timeout)
  385. self._set_initial_pose()
  386. self._reset_qr_scanner()
  387. self._navigate("wall1_p1", initial=True)
  388. self._scan_deadline = time.monotonic() + self._scan_timeout
  389. self._scan_wall("wall1", "wall1_p1")
  390. # 在中央C仅导航一次;其余两个墙面朝向由低速原地旋转完成。
  391. self._navigate("center_wall1")
  392. self._turn_in_place("center_wall1")
  393. self._scan_wall("wall1", "center_wall1")
  394. if not self._all_walls_found():
  395. self._turn_in_place("center_wall2")
  396. self._scan_wall("wall2", "center_wall2")
  397. if not self._all_walls_found():
  398. self._turn_in_place("center_wall3")
  399. self._scan_wall("wall3", "center_wall3")
  400. for wall, point in (
  401. ("wall3", "wall3_p1"),
  402. ("wall2", "wall2_p3"),
  403. ("wall2", "wall2_p1"),
  404. ("wall1", "wall1_p3"),
  405. ):
  406. if self._all_walls_found():
  407. break
  408. if self._products[wall] is None:
  409. self._navigate(point)
  410. self._scan_wall(wall, point)
  411. if not self._all_walls_found():
  412. missing = [wall for wall in self.WALLS if self._products[wall] is None]
  413. raise TaskFailure("扫码结束,未识别%s" % "、".join(missing))
  414. self._write_products_for_spark()
  415. self._run_spark()
  416. def main():
  417. rospy.init_node("competition_task1_manager")
  418. manager = CompetitionTask1Manager()
  419. try:
  420. manager.run()
  421. return 0
  422. except TaskFailure as error:
  423. manager._publish("FAILED", str(error))
  424. return 1
  425. except Exception as error:
  426. rospy.logerr("competition task1 crashed: %s", error)
  427. manager._publish("FAILED", "任务执行器异常:%s" % error)
  428. return 1
  429. if __name__ == "__main__":
  430. sys.exit(main())