competition_task2_executor.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/env python3
  2. """Execute one task-2 order through the configured factory observation route."""
  3. from __future__ import annotations
  4. import json
  5. import math
  6. import threading
  7. from pathlib import Path
  8. import rospy
  9. import rospkg
  10. from geometry_msgs.msg import PoseStamped, Quaternion
  11. from std_msgs.msg import String
  12. from std_srvs.srv import Trigger
  13. class Task2Executor:
  14. _NEXT_POINT_PREFIXES = ('ALL_SEARCH_CANDIDATES_EXHAUSTED', 'NO_SAFE_SEARCH_CANDIDATES',
  15. 'MOVE_BASE_UNAVAILABLE', 'OBSERVATION_MISMATCH')
  16. def __init__(self):
  17. rospy.init_node('competition_task2_executor')
  18. self._lock = threading.RLock()
  19. self._points = self._load_points()
  20. self._current_order = None
  21. self._active = False
  22. self._pending_start_order_id = None
  23. self._index = 0
  24. self._last_alignment_status = ''
  25. self._status_pub = rospy.Publisher('/competition_task2/status', String, queue_size=10, latch=True)
  26. self._result_pub = rospy.Publisher('/competition_task2/order_result', String, queue_size=10, latch=True)
  27. self._nominal_pub = rospy.Publisher('/factory_search_adapter/nominal_goal', PoseStamped, queue_size=1)
  28. self._reset = rospy.ServiceProxy('/factory_search_adapter/reset', Trigger)
  29. rospy.Subscriber('/competition_task2/current_order', String, self._order_cb, queue_size=1)
  30. rospy.Subscriber('/competition_task2/command', String, self._command_cb, queue_size=10)
  31. rospy.Subscriber('/factory_search_adapter/status', String, self._adapter_cb, queue_size=10)
  32. rospy.Subscriber('/factory_alignment_demo/status', String, self._alignment_cb, queue_size=10)
  33. self._publish('IDLE')
  34. @staticmethod
  35. def _load_points():
  36. path = Path(rospkg.RosPack().get_path('ucar_nav')) / 'config' / 'competition' / 'task2_observation_points.yaml'
  37. try:
  38. import yaml
  39. with path.open(encoding='utf-8') as stream:
  40. config = yaml.safe_load(stream)
  41. except (OSError, ImportError) as error:
  42. raise rospy.ROSException('任务二观察点配置不可读:%s' % error)
  43. if not isinstance(config, dict) or config.get('frame_id') != 'map':
  44. raise rospy.ROSException('任务二观察点必须使用map坐标系')
  45. points = config.get('observation_points')
  46. if not isinstance(points, list) or not points:
  47. raise rospy.ROSException('任务二观察点不能为空')
  48. out = []
  49. for item in points:
  50. values = tuple(float(item[key]) for key in ('x', 'y', 'yaw'))
  51. if not all(math.isfinite(value) for value in values):
  52. raise rospy.ROSException('任务二观察点包含非有限数值')
  53. out.append(values)
  54. return out
  55. def _publish(self, detail):
  56. rospy.loginfo('task2 executor: %s', detail)
  57. self._status_pub.publish(String(data=detail))
  58. def _order_cb(self, message):
  59. try:
  60. order = json.loads(message.data)
  61. if not all(str(order[key]).strip() for key in ('order_id', 'product', 'category', 'warehouse')):
  62. raise ValueError('订单字段不完整')
  63. except (ValueError, TypeError, KeyError, json.JSONDecodeError) as error:
  64. self._publish('ORDER_INVALID %s' % error)
  65. return
  66. with self._lock:
  67. self._current_order = order
  68. self._active = False
  69. self._publish('ORDER_READY order_id=%s' % order['order_id'])
  70. if self._pending_start_order_id == order['order_id']:
  71. self._pending_start_order_id = None
  72. self._start_order_locked(order['order_id'])
  73. def _start_order_locked(self, order_id):
  74. if self._active and self._current_order.get('order_id') == order_id:
  75. self._publish('START_DUPLICATE_IGNORED order_id=%s' % order_id)
  76. return
  77. self._active = True
  78. self._index = 0
  79. self._last_alignment_status = ''
  80. self._send_current_point('START_ORDER')
  81. def _command_cb(self, message):
  82. command = message.data.strip()
  83. with self._lock:
  84. if command == 'STOP':
  85. was_active = self._active
  86. self._pending_start_order_id = None
  87. self._active = False
  88. if was_active:
  89. self._reset_adapter(required=False)
  90. self._publish('STOPPED')
  91. return
  92. if not command.startswith('START_ORDER order_id='):
  93. return
  94. order_id = command.split('=', 1)[1].strip()
  95. if not order_id:
  96. return
  97. if self._current_order is None or self._current_order.get('order_id') != order_id:
  98. self._pending_start_order_id = order_id
  99. self._publish('START_WAITING_FOR_ORDER order_id=%s' % order_id)
  100. return
  101. self._pending_start_order_id = None
  102. self._start_order_locked(order_id)
  103. def _reset_adapter(self, required=True):
  104. try:
  105. rospy.wait_for_service('/factory_search_adapter/reset', timeout=1.5)
  106. response = self._reset()
  107. if not response.success:
  108. raise rospy.ServiceException(response.message)
  109. return True
  110. except (rospy.ROSException, rospy.ServiceException) as error:
  111. if required:
  112. self._fail('ADAPTER_RESET_FAILED %s' % error)
  113. else:
  114. rospy.logwarn('task2 executor could not reset adapter while stopping: %s', error)
  115. return False
  116. def _send_current_point(self, reason):
  117. if not self._active:
  118. return
  119. if self._index >= len(self._points):
  120. self._fail('SEARCH_ROUTE_EXHAUSTED')
  121. return
  122. if not self._reset_adapter():
  123. return
  124. x, y, yaw = self._points[self._index]
  125. pose = PoseStamped()
  126. pose.header.frame_id = 'map'
  127. pose.header.stamp = rospy.Time.now()
  128. pose.pose.position.x, pose.pose.position.y = x, y
  129. pose.pose.orientation = Quaternion(z=math.sin(yaw / 2.0), w=math.cos(yaw / 2.0))
  130. self._nominal_pub.publish(pose)
  131. self._publish('OBSERVATION_SENT %d/%d reason=%s x=%.3f y=%.3f yaw=%.3f' % (
  132. self._index + 1, len(self._points), reason, x, y, yaw))
  133. def _next_point(self, reason):
  134. if not self._active:
  135. return
  136. self._index += 1
  137. self._send_current_point(reason)
  138. def _adapter_cb(self, message):
  139. status = message.data.strip()
  140. with self._lock:
  141. if not self._active:
  142. return
  143. if status.startswith(self._NEXT_POINT_PREFIXES):
  144. self._next_point(status.split()[0])
  145. def _alignment_cb(self, message):
  146. status = message.data.strip()
  147. with self._lock:
  148. if not self._active or status == self._last_alignment_status:
  149. return
  150. self._last_alignment_status = status
  151. if status.startswith('FACTORY_ENTRY_COMPLETE'):
  152. self._complete_order()
  153. return
  154. # These are recoverable search-position failures. The adapter
  155. # retries another candidate and only route exhaustion is terminal.
  156. entry_goal_failed = (
  157. status.startswith('ENTRY_GOAL_')
  158. and not status.startswith('ENTRY_GOAL_SENT')
  159. )
  160. if status.startswith('FACTORY_ENTRY_FAILED') or entry_goal_failed:
  161. self._publish('ENTRY_RETRY_PENDING %s' % status)
  162. def _complete_order(self):
  163. if not self._active or self._current_order is None:
  164. return
  165. self._active = False
  166. result = dict(self._current_order)
  167. result['status'] = 'ORDER_PARKED'
  168. self._result_pub.publish(String(
  169. data=json.dumps(result, ensure_ascii=False, sort_keys=True)))
  170. self._publish('ORDER_PARKED order_id=%s' % result['order_id'])
  171. def _fail(self, reason):
  172. if not self._active:
  173. return
  174. self._active = False
  175. result = dict(self._current_order or {})
  176. result.update({'status': 'FAILED', 'reason': reason})
  177. self._result_pub.publish(String(
  178. data=json.dumps(result, ensure_ascii=False, sort_keys=True)))
  179. self._publish('FAILED %s' % reason)
  180. def main():
  181. Task2Executor()
  182. rospy.spin()
  183. if __name__ == '__main__':
  184. main()