21 from action_msgs.msg
import GoalStatus
23 from lifecycle_msgs.srv
import GetState
24 from nav2_msgs.action
import ComputeAndTrackRoute, ComputeRoute
25 from nav2_msgs.srv
import ManageLifecycleNodes
28 from rclpy.action
import ActionClient
29 from rclpy.client
import Client
30 from rclpy.node
import Node
31 from rclpy.qos
import QoSDurabilityPolicy, QoSHistoryPolicy, QoSProfile, QoSReliabilityPolicy
32 from std_srvs.srv
import Trigger
37 def __init__(self, initial_pose: Pose, goal_pose: Pose, namespace: str =
''):
38 super().__init__(node_name=
'nav2_tester', namespace=namespace)
40 PoseWithCovarianceStamped,
'initialpose', 10
43 pose_qos = QoSProfile(
44 durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
45 reliability=QoSReliabilityPolicy.RELIABLE,
46 history=QoSHistoryPolicy.KEEP_LAST,
51 PoseWithCovarianceStamped,
'amcl_pose', self.
poseCallbackposeCallback, pose_qos
56 self.compute_action_client: ActionClient[
60 ] = ActionClient(self, ComputeRoute,
'compute_route')
61 self.compute_track_action_client: ActionClient[
62 ComputeAndTrackRoute.Goal,
63 ComputeAndTrackRoute.Result,
64 ComputeAndTrackRoute.Feedback
66 self, ComputeAndTrackRoute,
'compute_and_track_route')
67 self.
feedback_msgsfeedback_msgs: list[ComputeAndTrackRoute.Feedback] = []
71 def runComputeRouteTest(self, use_poses: bool =
True) -> bool:
73 self.
info_msginfo_msg(
"Waiting for 'ComputeRoute' action server")
74 while not self.compute_action_client.wait_for_server(timeout_sec=1.0):
75 self.
info_msginfo_msg(
"'ComputeRoute' action server not available, waiting...")
77 route_msg = ComputeRoute.Goal()
81 route_msg.use_start =
True
82 route_msg.use_poses =
True
85 route_msg.start_id = 7
86 route_msg.goal_id = 13
87 route_msg.use_start =
False
88 route_msg.use_poses =
False
90 self.
info_msginfo_msg(
'Sending ComputeRoute goal request...')
91 send_goal_future = self.compute_action_client.send_goal_async(route_msg)
93 rclpy.spin_until_future_complete(self, send_goal_future)
94 goal_handle = send_goal_future.result()
96 if not goal_handle
or not goal_handle.accepted:
100 self.
info_msginfo_msg(
'Goal accepted')
101 get_result_future = goal_handle.get_result_async()
103 self.
info_msginfo_msg(
"Waiting for 'ComputeRoute' action to complete")
104 rclpy.spin_until_future_complete(self, get_result_future)
105 status = get_result_future.result().status
106 result = get_result_future.result().result
107 if status != GoalStatus.STATUS_SUCCEEDED:
108 self.
info_msginfo_msg(f
'Goal failed with status code: {status}')
111 self.
info_msginfo_msg(
'Action completed! Checking validity of results...')
114 self.
info_msginfo_msg(f
'Route path len(result.path.poses): {len(result.path.poses)}')
115 assert (len(result.path.poses) == 79)
116 assert (result.route.route_cost > 6)
117 assert (result.route.route_cost < 7)
118 assert (len(result.route.nodes) == 5)
119 assert (len(result.route.edges) == 4)
120 assert (result.error_code == 0)
121 assert (result.error_msg ==
'')
123 self.
info_msginfo_msg(
'Goal succeeded!')
126 def runComputeRouteSamePoseTest(self) -> bool:
128 self.
info_msginfo_msg(
"Waiting for 'ComputeRoute' action server")
129 while not self.compute_action_client.wait_for_server(timeout_sec=1.0):
130 self.
info_msginfo_msg(
"'ComputeRoute' action server not available, waiting...")
132 route_msg = ComputeRoute.Goal()
133 route_msg.start_id = 7
134 route_msg.goal_id = 7
135 route_msg.use_start =
False
136 route_msg.use_poses =
False
138 self.
info_msginfo_msg(
'Sending ComputeRoute goal request...')
139 send_goal_future = self.compute_action_client.send_goal_async(route_msg)
141 rclpy.spin_until_future_complete(self, send_goal_future)
142 goal_handle = send_goal_future.result()
144 if not goal_handle
or not goal_handle.accepted:
148 self.
info_msginfo_msg(
'Goal accepted')
149 get_result_future = goal_handle.get_result_async()
151 self.
info_msginfo_msg(
"Waiting for 'ComputeRoute' action to complete")
152 rclpy.spin_until_future_complete(self, get_result_future)
153 status = get_result_future.result().status
154 result = get_result_future.result().result
155 if status != GoalStatus.STATUS_SUCCEEDED:
156 self.
info_msginfo_msg(f
'Goal failed with status code: {status}')
159 self.
info_msginfo_msg(
'Action completed! Checking validity of results...')
162 assert (len(result.path.poses) == 1)
163 assert (len(result.route.nodes) == 1)
164 assert (len(result.route.edges) == 0)
165 assert (result.error_code == 0)
166 assert (result.error_msg ==
'')
168 self.
info_msginfo_msg(
'Goal succeeded!')
171 def runTrackRouteTest(self) -> bool:
173 self.
info_msginfo_msg(
"Waiting for 'ComputeAndTrackRoute' action server")
174 while not self.compute_track_action_client.wait_for_server(timeout_sec=1.0):
175 self.
info_msginfo_msg(
"'ComputeAndTrackRoute' action server not available, waiting...")
177 route_msg = ComputeAndTrackRoute.Goal()
179 route_msg.use_start =
False
180 route_msg.use_poses =
True
182 self.
info_msginfo_msg(
'Sending ComputeAndTrackRoute goal request...')
183 send_goal_future = self.compute_track_action_client.send_goal_async(
186 rclpy.spin_until_future_complete(self, send_goal_future)
187 goal_handle = send_goal_future.result()
189 if not goal_handle
or not goal_handle.accepted:
193 self.
info_msginfo_msg(
'Goal accepted')
194 get_result_future = goal_handle.get_result_async()
198 self.
info_msginfo_msg(
'Triggering a reroute')
199 srv_client: Client[Trigger.Request, Trigger.Response] = \
200 self.create_client(Trigger,
'route_server/ReroutingService/reroute')
201 while not srv_client.wait_for_service(timeout_sec=1.0):
202 self.
info_msginfo_msg(
'Reroute service not available, waiting...')
203 req = Trigger.Request()
204 future = srv_client.call_async(req)
205 rclpy.spin_until_future_complete(self, future)
206 if future.result()
is not None:
207 self.
info_msginfo_msg(
'Reroute triggered')
209 self.
error_msgerror_msg(
'Reroute failed')
215 cancel_future = goal_handle.cancel_goal_async()
216 rclpy.spin_until_future_complete(self, cancel_future)
217 status = cancel_future.result()
218 if status
is not None and len(status.goals_canceling) > 0:
219 self.
info_msginfo_msg(
'Action cancel completed!')
221 self.
info_msginfo_msg(
'Goal cancel failed')
225 self.
info_msginfo_msg(
'Sending ComputeAndTrackRoute goal request...')
226 send_goal_future = self.compute_track_action_client.send_goal_async(
229 rclpy.spin_until_future_complete(self, send_goal_future)
230 goal_handle = send_goal_future.result()
232 if not goal_handle
or not goal_handle.accepted:
236 self.
info_msginfo_msg(
'Goal accepted')
237 get_result_future = goal_handle.get_result_async()
243 route_msg.use_poses =
False
244 route_msg.start_id = 7
245 route_msg.goal_id = 13
246 send_goal_future = self.compute_track_action_client.send_goal_async(
249 rclpy.spin_until_future_complete(self, send_goal_future)
250 goal_handle = send_goal_future.result()
252 if not goal_handle
or not goal_handle.accepted:
256 self.
info_msginfo_msg(
'Goal accepted')
257 get_result_future = goal_handle.get_result_async()
260 self.
info_msginfo_msg(
"Waiting for 'ComputeAndTrackRoute' action to complete")
262 last_feedback_msg =
None
263 follow_path_task =
None
265 rclpy.spin_until_future_complete(self, get_result_future, timeout_sec=0.10)
266 if get_result_future.result()
is not None:
267 status = get_result_future.result().status
268 if status == GoalStatus.STATUS_SUCCEEDED:
270 elif status == GoalStatus.STATUS_CANCELED
or status == GoalStatus.STATUS_ABORTED:
271 self.
info_msginfo_msg(f
'Goal failed with status code: {status}')
279 if (last_feedback_msg
and feedback_msg.path != last_feedback_msg.path):
280 follow_path_task = self.
navigatornavigator.followPath(feedback_msg.path)
283 if last_feedback_msg
and \
284 last_feedback_msg.current_edge_id != feedback_msg.current_edge_id
and \
285 int(feedback_msg.current_edge_id) != 0:
286 if last_feedback_msg.next_node_id != feedback_msg.last_node_id:
287 self.
error_msgerror_msg(
'Feedback state is not tracking in order!')
290 last_feedback_msg = feedback_msg
293 if last_feedback_msg
is None:
294 self.
error_msgerror_msg(
'No feedback message received!')
297 if int(last_feedback_msg.next_node_id) != 0:
298 self.
error_msgerror_msg(
'Terminal feedback state of nodes is not correct!')
300 if int(last_feedback_msg.current_edge_id) != 0:
301 self.
error_msgerror_msg(
'Terminal feedback state of edges is not correct!')
303 if int(last_feedback_msg.route.nodes[-1].nodeid) != 13:
304 self.
error_msgerror_msg(
'Final route node is not correct!')
307 while not self.
navigatornavigator.isTaskComplete(task=follow_path_task):
310 self.
info_msginfo_msg(
'Action completed! Checking validity of terminal condition...')
314 self.
error_msgerror_msg(
'Did not make it to the goal pose!')
317 self.
info_msginfo_msg(
'Goal succeeded!')
320 def feedback_callback(
321 self, feedback_msg: ComputeAndTrackRoute.Impl.FeedbackMessage) ->
None:
322 self.
feedback_msgsfeedback_msgs.append(feedback_msg.feedback)
324 def distanceFromGoal(self) -> float:
327 distance = math.sqrt(d_x * d_x + d_y * d_y)
328 self.
info_msginfo_msg(f
'Distance from goal is: {distance}')
331 def info_msg(self, msg: str) ->
None:
332 self.get_logger().info(
'\033[1;37;44m' + msg +
'\033[0m')
334 def error_msg(self, msg: str) ->
None:
335 self.get_logger().error(
'\033[1;37;41m' + msg +
'\033[0m')
337 def setInitialPose(self) -> None:
338 msg = PoseWithCovarianceStamped()
340 msg.header.frame_id =
'map'
341 self.
info_msginfo_msg(
'Publishing Initial Pose')
345 def getStampedPoseMsg(self, pose: Pose) -> PoseStamped:
347 msg.header.frame_id =
'map'
351 def poseCallback(self, msg: PoseWithCovarianceStamped) ->
None:
352 self.
info_msginfo_msg(
'Received amcl_pose')
356 def wait_for_node_active(self, node_name: str) ->
None:
358 self.
info_msginfo_msg(f
'Waiting for {node_name} to become active')
359 node_service = f
'{node_name}/get_state'
360 state_client: Client[GetState.Request, GetState.Response] = \
361 self.create_client(GetState, node_service)
362 while not state_client.wait_for_service(timeout_sec=1.0):
363 self.
info_msginfo_msg(f
'{node_service} service not available, waiting...')
364 req = GetState.Request()
366 while state !=
'active':
367 self.
info_msginfo_msg(f
'Getting {node_name} state...')
368 future = state_client.call_async(req)
369 rclpy.spin_until_future_complete(self, future)
370 if future.result()
is not None:
371 state = future.result().current_state.label
372 self.
info_msginfo_msg(f
'Result of get_state: {state}')
375 f
'Exception while calling service: {future.exception()!r}'
379 def shutdown(self) -> None:
380 self.
info_msginfo_msg(
'Shutting down')
381 self.compute_action_client.destroy()
382 self.compute_track_action_client.destroy()
384 transition_service =
'lifecycle_manager_navigation/manage_nodes'
385 mgr_client: Client[ManageLifecycleNodes.Request, ManageLifecycleNodes.Response] = \
386 self.create_client(ManageLifecycleNodes, transition_service)
387 while not mgr_client.wait_for_service(timeout_sec=1.0):
388 self.
info_msginfo_msg(f
'{transition_service} service not available, waiting...')
390 req = ManageLifecycleNodes.Request()
391 req.command = ManageLifecycleNodes.Request.SHUTDOWN
392 future = mgr_client.call_async(req)
394 self.
info_msginfo_msg(
'Shutting down navigation lifecycle manager...')
395 rclpy.spin_until_future_complete(self, future)
397 self.
info_msginfo_msg(
'Shutting down navigation lifecycle manager complete.')
398 except Exception
as e:
399 self.
error_msgerror_msg(f
'Service call failed {e!r}')
400 transition_service =
'lifecycle_manager_localization/manage_nodes'
401 mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
402 while not mgr_client.wait_for_service(timeout_sec=1.0):
403 self.
info_msginfo_msg(f
'{transition_service} service not available, waiting...')
405 req = ManageLifecycleNodes.Request()
406 req.command = ManageLifecycleNodes.Request.SHUTDOWN
407 future = mgr_client.call_async(req)
409 self.
info_msginfo_msg(
'Shutting down localization lifecycle manager...')
410 rclpy.spin_until_future_complete(self, future)
412 self.
info_msginfo_msg(
'Shutting down localization lifecycle manager complete')
413 except Exception
as e:
414 self.
error_msgerror_msg(f
'Service call failed {e!r}')
416 def wait_for_initial_pose(self) -> None:
419 self.
info_msginfo_msg(
'Setting initial pose')
421 self.
info_msginfo_msg(
'Waiting for amcl_pose to be received')
422 rclpy.spin_once(self, timeout_sec=1)
425 def run_all_tests(robot_tester: RouteTester) -> bool:
427 robot_tester.wait_for_node_active(
'amcl')
428 robot_tester.wait_for_initial_pose()
429 robot_tester.wait_for_node_active(
'bt_navigator')
430 result_poses = robot_tester.runComputeRouteTest(use_poses=
True)
431 result_node_ids = robot_tester.runComputeRouteTest(use_poses=
False)
432 result_same = robot_tester.runComputeRouteSamePoseTest()
433 result = result_poses
and result_node_ids
and result_same
and robot_tester.runTrackRouteTest()
436 robot_tester.info_msg(
'Test PASSED')
438 robot_tester.error_msg(
'Test FAILED')
442 def fwd_pose(x: float = 0.0, y: float = 0.0, z: float = 0.01) -> Pose:
443 initial_pose = Pose()
444 initial_pose.position.x = x
445 initial_pose.position.y = y
446 initial_pose.position.z = z
447 initial_pose.orientation.x = 0.0
448 initial_pose.orientation.y = 0.0
449 initial_pose.orientation.z = 0.0
450 initial_pose.orientation.w = 1.0
454 def main(argv: list[str] = sys.argv[1:]):
456 parser = argparse.ArgumentParser(description=
'Route server tester node')
457 group = parser.add_mutually_exclusive_group(required=
True)
463 metavar=(
'init_x',
'init_y',
'final_x',
'final_y'),
464 help=
'The robot starting and final positions.',
466 args, unknown = parser.parse_known_args()
471 init_x, init_y, final_x, final_y = args.robot[0]
473 initial_pose=fwd_pose(float(init_x), float(init_y)),
474 goal_pose=fwd_pose(float(final_x), float(final_y)),
477 'Starting tester, robot going from '
485 +
' via route server.'
492 passed = run_all_tests(tester)
495 tester.info_msg(
'Done Shutting Down.')
498 tester.info_msg(
'Exiting failed')
501 tester.info_msg(
'Exiting passed')
505 if __name__ ==
'__main__':
None info_msg(self, str msg)
None poseCallback(self, PoseWithCovarianceStamped msg)
None error_msg(self, str msg)
None setInitialPose(self)
None feedback_callback(self, ComputeAndTrackRoute.Impl.FeedbackMessage feedback_msg)
PoseStamped getStampedPoseMsg(self, Pose pose)
float distanceFromGoal(self)