factory_candidate_navigator.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/env python3
  2. """Try factory candidate poses in order until one move_base goal succeeds."""
  3. import math
  4. import sys
  5. import time
  6. import actionlib
  7. import rospy
  8. from actionlib_msgs.msg import GoalStatus
  9. from geometry_msgs.msg import PoseWithCovarianceStamped, Quaternion
  10. from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal
  11. from std_msgs.msg import String
  12. class FactoryCandidateNavigator:
  13. _FAILURE_STATES = {
  14. GoalStatus.PREEMPTED,
  15. GoalStatus.ABORTED,
  16. GoalStatus.REJECTED,
  17. GoalStatus.RECALLED,
  18. GoalStatus.LOST,
  19. }
  20. def __init__(self):
  21. rospy.init_node("factory_candidate_navigator")
  22. self._status_pub = rospy.Publisher(
  23. "/factory_candidate_navigator/status", String, queue_size=10, latch=True
  24. )
  25. self._initial_pose_pub = rospy.Publisher(
  26. "/initialpose", PoseWithCovarianceStamped, queue_size=1
  27. )
  28. self._frame_id = rospy.get_param("~frame_id", "map")
  29. self._start_pose = rospy.get_param("~start_pose")
  30. self._candidates = rospy.get_param("~candidate_goals")
  31. self._server_timeout = float(rospy.get_param("~move_base_server_timeout_seconds", 10.0))
  32. self._goal_timeout = float(rospy.get_param("~goal_timeout_seconds", 120.0))
  33. self._initial_pose_confirmation_timeout = float(
  34. rospy.get_param("~initial_pose_confirmation_timeout_seconds", 5.0)
  35. )
  36. self._initial_pose_position_tolerance = float(
  37. rospy.get_param("~initial_pose_position_tolerance_m", 0.3)
  38. )
  39. self._initial_pose_yaw_tolerance = float(
  40. rospy.get_param("~initial_pose_yaw_tolerance_rad", 0.2)
  41. )
  42. self._latest_amcl_pose = None
  43. self._latest_amcl_monotonic = 0.0
  44. self._validate_config()
  45. rospy.Subscriber("/amcl_pose", PoseWithCovarianceStamped, self._amcl_callback, queue_size=10)
  46. self._client = actionlib.SimpleActionClient("move_base", MoveBaseAction)
  47. rospy.on_shutdown(self._cancel_goal)
  48. def _publish(self, text):
  49. rospy.loginfo("factory candidate navigator: %s", text)
  50. self._status_pub.publish(String(data=text))
  51. def _validate_pose(self, pose, label):
  52. try:
  53. values = (float(pose["x"]), float(pose["y"]), float(pose["yaw"]))
  54. except (KeyError, TypeError, ValueError) as error:
  55. raise ValueError("%s pose is invalid: %s" % (label, error))
  56. if not all(math.isfinite(value) for value in values):
  57. raise ValueError("%s pose has non-finite values" % label)
  58. return values
  59. def _validate_config(self):
  60. if self._frame_id != "map":
  61. raise ValueError("factory candidate goals must use the map frame")
  62. self._validate_pose(self._start_pose, "start")
  63. if not isinstance(self._candidates, list) or len(self._candidates) != 3:
  64. raise ValueError("candidate_goals must contain exactly three poses")
  65. for index, candidate in enumerate(self._candidates, start=1):
  66. self._validate_pose(candidate, "candidate %d" % index)
  67. if self._goal_timeout <= 0.0 or self._server_timeout <= 0.0:
  68. raise ValueError("navigation timeouts must be positive")
  69. if self._initial_pose_confirmation_timeout <= 0.0:
  70. raise ValueError("initial pose confirmation timeout must be positive")
  71. if self._initial_pose_position_tolerance <= 0.0 or self._initial_pose_yaw_tolerance <= 0.0:
  72. raise ValueError("initial pose confirmation tolerances must be positive")
  73. def _amcl_callback(self, message):
  74. orientation = message.pose.pose.orientation
  75. yaw = math.atan2(
  76. 2.0 * (orientation.w * orientation.z + orientation.x * orientation.y),
  77. 1.0 - 2.0 * (orientation.y * orientation.y + orientation.z * orientation.z),
  78. )
  79. self._latest_amcl_pose = (
  80. message.pose.pose.position.x, message.pose.pose.position.y, yaw
  81. )
  82. self._latest_amcl_monotonic = time.monotonic()
  83. @staticmethod
  84. def _wrap_to_pi(angle):
  85. return math.atan2(math.sin(angle), math.cos(angle))
  86. def _goal_from_candidate(self, candidate):
  87. x, y, yaw = self._validate_pose(candidate, "candidate")
  88. goal = MoveBaseGoal()
  89. goal.target_pose.header.frame_id = self._frame_id
  90. goal.target_pose.header.stamp = rospy.Time.now()
  91. goal.target_pose.pose.position.x = x
  92. goal.target_pose.pose.position.y = y
  93. goal.target_pose.pose.orientation = Quaternion(
  94. z=math.sin(yaw / 2.0), w=math.cos(yaw / 2.0)
  95. )
  96. return goal, x, y, yaw
  97. def _publish_initial_pose(self):
  98. x, y, yaw = self._validate_pose(self._start_pose, "start")
  99. message = PoseWithCovarianceStamped()
  100. message.header.frame_id = self._frame_id
  101. message.header.stamp = rospy.Time.now()
  102. message.pose.pose.position.x = x
  103. message.pose.pose.position.y = y
  104. message.pose.pose.orientation.z = math.sin(yaw / 2.0)
  105. message.pose.pose.orientation.w = math.cos(yaw / 2.0)
  106. message.pose.covariance[0] = 0.25
  107. message.pose.covariance[7] = 0.25
  108. message.pose.covariance[35] = 0.0685
  109. self._publish("INITIALIZING: setting AMCL pose x=%.3f y=%.3f yaw=%.3f" % (x, y, yaw))
  110. for _ in range(3):
  111. message.header.stamp = rospy.Time.now()
  112. self._initial_pose_pub.publish(message)
  113. rospy.sleep(0.1)
  114. def _wait_for_initial_pose_confirmation(self):
  115. target_x, target_y, target_yaw = self._validate_pose(self._start_pose, "start")
  116. deadline = time.monotonic() + self._initial_pose_confirmation_timeout
  117. while not rospy.is_shutdown() and time.monotonic() < deadline:
  118. pose = self._latest_amcl_pose
  119. fresh = time.monotonic() - self._latest_amcl_monotonic <= 1.0
  120. if pose is not None and fresh:
  121. x, y, yaw = pose
  122. position_error = math.hypot(x - target_x, y - target_y)
  123. yaw_error = abs(self._wrap_to_pi(yaw - target_yaw))
  124. if (position_error <= self._initial_pose_position_tolerance
  125. and yaw_error <= self._initial_pose_yaw_tolerance):
  126. self._publish("INITIAL_POSE_CONFIRMED: position_error=%.3f yaw_error=%.3f" % (
  127. position_error, yaw_error
  128. ))
  129. return True
  130. rospy.sleep(0.05)
  131. self._publish("INITIAL_POSE_TIMEOUT: candidate navigation not started")
  132. return False
  133. def _cancel_goal(self):
  134. if hasattr(self, "_client"):
  135. self._client.cancel_goal()
  136. def run(self):
  137. sx, sy, syaw = self._validate_pose(self._start_pose, "start")
  138. self._publish_initial_pose()
  139. if not self._wait_for_initial_pose_confirmation():
  140. return 1
  141. self._publish("START: task2 start x=%.3f y=%.3f yaw=%.3f" % (sx, sy, syaw))
  142. if not self._client.wait_for_server(rospy.Duration(self._server_timeout)):
  143. self._publish("FAILED: move_base unavailable")
  144. return 1
  145. for index, candidate in enumerate(self._candidates, start=1):
  146. goal, x, y, yaw = self._goal_from_candidate(candidate)
  147. self._publish("NAVIGATING: candidate %d/3 x=%.3f y=%.3f yaw=%.3f" % (
  148. index, x, y, yaw
  149. ))
  150. self._client.send_goal(goal)
  151. if not self._client.wait_for_result(rospy.Duration(self._goal_timeout)):
  152. self._client.cancel_goal()
  153. self._publish("FAILED: candidate %d/3 timed out; trying next" % index)
  154. continue
  155. state = self._client.get_state()
  156. if state == GoalStatus.SUCCEEDED:
  157. self._publish("SUCCEEDED: candidate %d/3 reached; remaining candidates skipped" % index)
  158. return 0
  159. if state in self._FAILURE_STATES:
  160. self._publish("FAILED: candidate %d/3 move_base state=%d; trying next" % (
  161. index, state
  162. ))
  163. else:
  164. self._client.cancel_goal()
  165. self._publish("FAILED: candidate %d/3 unexpected state=%d; trying next" % (
  166. index, state
  167. ))
  168. self._publish("ALL_CANDIDATES_FAILED")
  169. return 1
  170. def main():
  171. try:
  172. navigator = FactoryCandidateNavigator()
  173. return navigator.run()
  174. except (ValueError, rospy.ROSException) as error:
  175. rospy.logerr("factory candidate navigator configuration error: %s", error)
  176. return 2
  177. if __name__ == "__main__":
  178. sys.exit(main())