Nav2 Navigation Stack - jazzy  jazzy
ROS 2 Navigation Stack
nav_to_pose_tester_node.py
1 #! /usr/bin/env python3
2 # Copyright 2018 Intel Corporation.
3 # Copyright 2020 Florian Gramss
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 
17 import argparse
18 import json
19 import math
20 import os
21 import struct
22 import sys
23 import time
24 
25 from typing import Optional
26 
27 from action_msgs.msg import GoalStatus
28 from geometry_msgs.msg import Pose
29 from geometry_msgs.msg import PoseStamped
30 from geometry_msgs.msg import PoseWithCovarianceStamped
31 from lifecycle_msgs.srv import GetState
32 from nav2_msgs.action import NavigateToPose
33 from nav2_msgs.srv import ManageLifecycleNodes
34 
35 import rclpy
36 
37 from rclpy.action import ActionClient
38 from rclpy.node import Node
39 from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSReliabilityPolicy
40 from rclpy.qos import QoSProfile
41 import zmq
42 
43 
44 class NavTester(Node):
45 
46  def __init__(self, initial_pose: Pose, goal_pose: Pose, namespace: str = ''):
47  super().__init__(node_name='nav2_tester', namespace=namespace)
48  self.initial_pose_pubinitial_pose_pub = self.create_publisher(
49  PoseWithCovarianceStamped, 'initialpose', 10
50  )
51  self.goal_pubgoal_pub = self.create_publisher(PoseStamped, 'goal_pose', 10)
52 
53  pose_qos = QoSProfile(
54  durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
55  reliability=QoSReliabilityPolicy.RELIABLE,
56  history=QoSHistoryPolicy.KEEP_LAST,
57  depth=1,
58  )
59 
60  self.model_pose_submodel_pose_sub = self.create_subscription(
61  PoseWithCovarianceStamped, 'amcl_pose', self.poseCallbackposeCallback, pose_qos
62  )
63  self.initial_pose_receivedinitial_pose_received = False
64  self.initial_poseinitial_pose = initial_pose
65  self.goal_posegoal_pose = goal_pose
66  self.set_initial_pose_timeoutset_initial_pose_timeout = 15
67  self.action_clientaction_client = ActionClient(self, NavigateToPose, 'navigate_to_pose')
68 
69  def info_msg(self, msg: str):
70  self.get_logger().info('\033[1;37;44m' + msg + '\033[0m')
71 
72  def warn_msg(self, msg: str):
73  self.get_logger().warn('\033[1;37;43m' + msg + '\033[0m')
74 
75  def error_msg(self, msg: str):
76  self.get_logger().error('\033[1;37;41m' + msg + '\033[0m')
77 
78  def setInitialPose(self):
79  msg = PoseWithCovarianceStamped()
80  msg.pose.pose = self.initial_poseinitial_pose
81  msg.header.frame_id = 'map'
82  self.info_msginfo_msg('Publishing Initial Pose')
83  self.initial_pose_pubinitial_pose_pub.publish(msg)
84  self.currentPosecurrentPose = self.initial_poseinitial_pose
85 
86  def getStampedPoseMsg(self, pose: Pose):
87  msg = PoseStamped()
88  msg.header.frame_id = 'map'
89  msg.pose = pose
90  return msg
91 
92  def publishGoalPose(self, goal_pose: Optional[Pose] = None):
93  self.goal_posegoal_pose = goal_pose if goal_pose is not None else self.goal_posegoal_pose
94  self.goal_pubgoal_pub.publish(self.getStampedPoseMsggetStampedPoseMsg(self.goal_posegoal_pose))
95 
96  def runNavigateAction(self, goal_pose: Optional[Pose] = None):
97  # Sends a `NavToPose` action request and waits for completion
98  self.info_msginfo_msg("Waiting for 'NavigateToPose' action server")
99  while not self.action_clientaction_client.wait_for_server(timeout_sec=1.0):
100  self.info_msginfo_msg("'NavigateToPose' action server not available, waiting...")
101 
102  if os.getenv('GROOT_MONITORING') == 'True':
103  if not self.grootMonitoringGetStatusgrootMonitoringGetStatus():
104  self.error_msgerror_msg('Behavior Tree must not be running already!')
105  self.error_msgerror_msg('Are you running multiple goals/bts..?')
106  return False
107 
108  self.goal_posegoal_pose = goal_pose if goal_pose is not None else self.goal_posegoal_pose
109  goal_msg = NavigateToPose.Goal()
110  goal_msg.pose = self.getStampedPoseMsggetStampedPoseMsg(self.goal_posegoal_pose)
111 
112  self.info_msginfo_msg('Sending goal request...')
113  send_goal_future = self.action_clientaction_client.send_goal_async(goal_msg)
114 
115  rclpy.spin_until_future_complete(self, send_goal_future)
116  goal_handle = send_goal_future.result()
117 
118  if not goal_handle.accepted:
119  self.error_msgerror_msg('Goal rejected')
120  return False
121 
122  self.info_msginfo_msg('Goal accepted')
123  get_result_future = goal_handle.get_result_async()
124 
125  future_return = True
126  if os.getenv('GROOT_MONITORING') == 'True':
127  try:
128  if not self.grootMonitoringReloadTreegrootMonitoringReloadTree():
129  self.error_msgerror_msg('Failed GROOT_BT - Reload Tree from ZMQ Server')
130  future_return = False
131  if not self.grootMonitoringSetBreakpointgrootMonitoringSetBreakpoint():
132  self.error_msgerror_msg('Failed GROOT_BT - Set Breakpoint from ZMQ Publisher')
133  future_return = False
134  except Exception as e: # noqa: B902
135  self.error_msgerror_msg(f'Failed GROOT_BT - ZMQ Tests: {e}')
136  future_return = False
137 
138  self.info_msginfo_msg("Waiting for 'NavigateToPose' action to complete")
139  rclpy.spin_until_future_complete(self, get_result_future)
140  status = get_result_future.result().status
141  if status != GoalStatus.STATUS_SUCCEEDED:
142  self.info_msginfo_msg(f'Goal failed with status code: {status}')
143  return False
144 
145  if not future_return:
146  return False
147 
148  self.info_msginfo_msg('Goal succeeded!')
149  return True
150 
151  def grootMonitoringReloadTree(self) -> bool:
152  # ZeroMQ Context
153  context = zmq.Context()
154 
155  sock = context.socket(zmq.REQ)
156  port = 1667 # default server port for groot monitoring
157  # # Set a Timeout so we do not spin till infinity
158  sock.setsockopt(zmq.RCVTIMEO, 1000)
159  # sock.setsockopt(zmq.LINGER, 0)
160 
161  sock.connect(f'tcp://localhost:{port}')
162  self.info_msginfo_msg(f'ZMQ Server Port:{port}')
163 
164  # this should fail
165  try:
166  sock.recv()
167  self.error_msgerror_msg('ZMQ Reload Tree Test 1/3 - This should have failed!')
168  # Only works when ZMQ server receives a request first
169  sock.close()
170  return False
171  except zmq.error.ZMQError:
172  self.info_msginfo_msg('ZMQ Reload Tree Test 1/3: Check')
173  try:
174  # request tree from server
175  request_header = struct.pack('!BBI', 2, ord('T'), 12345)
176  sock.send(request_header)
177  # receive tree from server as flat_buffer
178  sock.recv_multipart()
179  self.info_msginfo_msg('ZMQ Reload Tree Test 2/3: Check')
180  except zmq.error.Again:
181  self.info_msginfo_msg('ZMQ Reload Tree Test 2/3 - Failed to load tree')
182  sock.close()
183  return False
184 
185  # this should fail
186  try:
187  sock.recv()
188  self.error_msgerror_msg('ZMQ Reload Tree Test 3/3 - This should have failed!')
189  # Tree should only be loadable ONCE after ZMQ server received a request
190  sock.close()
191  return False
192  except zmq.error.ZMQError:
193  self.info_msginfo_msg('ZMQ Reload Tree Test 3/3: Check')
194 
195  return True
196 
197  def grootMonitoringSetBreakpoint(self) -> bool:
198  # ZeroMQ Context
199  context = zmq.Context()
200  # Define the socket using the 'Context'
201  sock = context.socket(zmq.REQ)
202  # Set a Timeout so we do not spin till infinity
203  sock.setsockopt(zmq.RCVTIMEO, 2000)
204  # sock.setsockopt(zmq.LINGER, 0)
205 
206  port = 1667 # default publishing port for groot monitoring
207  sock.connect(f'tcp://127.0.0.1:{port}')
208  self.info_msginfo_msg(f'ZMQ Publisher Port:{port}')
209 
210  # Create header for the request
211  request_header = struct.pack('!BBI', 2, ord('I'), 12345) # HOOK_INSERT
212  # Create JSON for the hook
213  hook_data = {
214  'enabled': True,
215  'uid': 9, # Node ID
216  'mode': 0, # 0 = BREAKPOINT, 1 = REPLACE
217  'once': False,
218  'desired_status': 'SUCCESS', # Desired status
219  'position': 0, # 0 = PRE, 1 = POST
220  }
221  hook_json = json.dumps(hook_data)
222 
223  # Send the request
224  try:
225  sock.send_multipart([request_header, hook_json.encode('utf-8')])
226  reply = sock.recv_multipart()
227  if len(reply[0]) < 2:
228  self.error_msgerror_msg('ZMQ - Incomplete reply received')
229  sock.close()
230  return False
231  except Exception as e:
232  self.error_msgerror_msg(f'ZMQ - Error during request: {e}')
233  sock.close()
234  return False
235  self.info_msginfo_msg('ZMQ - HOOK_INSERT request sent')
236  return True
237 
238  def grootMonitoringGetStatus(self) -> bool:
239  # ZeroMQ Context
240  context = zmq.Context()
241 
242  sock = context.socket(zmq.REQ)
243  port = 1667 # default server port for groot monitoring
244  # # Set a Timeout so we do not spin till infinity
245  sock.setsockopt(zmq.RCVTIMEO, 1000)
246  # sock.setsockopt(zmq.LINGER, 0)
247 
248  sock.connect(f'tcp://localhost:{port}')
249  self.info_msginfo_msg(f'ZMQ Server Port:{port}')
250 
251  for request in range(3):
252  try:
253  # request tree from server
254  request_header = struct.pack('!BBI', 2, ord('S'), 12345)
255  sock.send(request_header)
256  # receive tree from server as flat_buffer
257  reply = sock.recv_multipart()
258  if len(reply[0]) < 6:
259  self.error_msgerror_msg('ZMQ - Incomplete reply received')
260  sock.close()
261  return False
262  # Decoding payload
263  payload = reply[1]
264  node_states = []
265  offset = 0
266  while offset < len(payload):
267  node_uid, node_status = struct.unpack_from('!HB', payload, offset)
268  offset += 3 # 2 bytes for UID, 1 byte for status
269  node_states.append((node_uid, node_status))
270  # Get the status of the first node
271  node_uid, node_status = node_states[0]
272  if node_status != 0:
273  self.error_msgerror_msg('ZMQ - BT Not running')
274  return False
275  except zmq.error.Again:
276  self.error_msgerror_msg('ZMQ - Did not receive any status')
277  sock.close()
278  return False
279  self.info_msginfo_msg('ZMQ - Did receive status')
280  return True
281 
282  def poseCallback(self, msg: PoseWithCovarianceStamped) -> None:
283  self.info_msginfo_msg('Received amcl_pose')
284  self.current_posecurrent_pose = msg.pose.pose
285  self.initial_pose_receivedinitial_pose_received = True
286 
287  def reachesGoal(self, timeout, distance):
288  goalReached = False
289  start_time = time.time()
290 
291  while not goalReached:
292  rclpy.spin_once(self, timeout_sec=1)
293  if self.distanceFromGoaldistanceFromGoal() < distance:
294  goalReached = True
295  self.info_msginfo_msg('*** GOAL REACHED ***')
296  return True
297  elif timeout is not None:
298  if (time.time() - start_time) > timeout:
299  self.error_msgerror_msg('Robot timed out reaching its goal!')
300  return False
301 
302  def distanceFromGoal(self):
303  d_x = self.current_posecurrent_pose.position.x - self.goal_posegoal_pose.position.x
304  d_y = self.current_posecurrent_pose.position.y - self.goal_posegoal_pose.position.y
305  distance = math.sqrt(d_x * d_x + d_y * d_y)
306  self.info_msginfo_msg(f'Distance from goal is: {distance}')
307  return distance
308 
309  def wait_for_node_active(self, node_name: str):
310  # Waits for the node within the tester namespace to become active
311  self.info_msginfo_msg(f'Waiting for {node_name} to become active')
312  node_service = f'{node_name}/get_state'
313  state_client = self.create_client(GetState, node_service)
314  while not state_client.wait_for_service(timeout_sec=1.0):
315  self.info_msginfo_msg(f'{node_service} service not available, waiting...')
316  req = GetState.Request() # empty request
317  state = 'UNKNOWN'
318  while state != 'active':
319  self.info_msginfo_msg(f'Getting {node_name} state...')
320  future = state_client.call_async(req)
321  rclpy.spin_until_future_complete(self, future)
322  if future.result() is not None:
323  state = future.result().current_state.label
324  self.info_msginfo_msg(f'Result of get_state: {state}')
325  else:
326  self.error_msgerror_msg(
327  f'Exception while calling service: {future.exception()!r}'
328  )
329  time.sleep(5)
330 
331  def shutdown(self):
332  self.info_msginfo_msg('Shutting down')
333  self.action_clientaction_client.destroy()
334 
335  transition_service = 'lifecycle_manager_navigation/manage_nodes'
336  mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
337  while not mgr_client.wait_for_service(timeout_sec=1.0):
338  self.info_msginfo_msg(f'{transition_service} service not available, waiting...')
339 
340  req = ManageLifecycleNodes.Request()
341  req.command = ManageLifecycleNodes.Request().SHUTDOWN
342  future = mgr_client.call_async(req)
343  try:
344  self.info_msginfo_msg('Shutting down navigation lifecycle manager...')
345  rclpy.spin_until_future_complete(self, future)
346  future.result()
347  self.info_msginfo_msg('Shutting down navigation lifecycle manager complete.')
348  except Exception as e: # noqa: B902
349  self.error_msgerror_msg(f'Service call failed {e!r}')
350  transition_service = 'lifecycle_manager_localization/manage_nodes'
351  mgr_client = self.create_client(ManageLifecycleNodes, transition_service)
352  while not mgr_client.wait_for_service(timeout_sec=1.0):
353  self.info_msginfo_msg(f'{transition_service} service not available, waiting...')
354 
355  req = ManageLifecycleNodes.Request()
356  req.command = ManageLifecycleNodes.Request().SHUTDOWN
357  future = mgr_client.call_async(req)
358  try:
359  self.info_msginfo_msg('Shutting down localization lifecycle manager...')
360  rclpy.spin_until_future_complete(self, future)
361  future.result()
362  self.info_msginfo_msg('Shutting down localization lifecycle manager complete')
363  except Exception as e: # noqa: B902
364  self.error_msgerror_msg(f'Service call failed {e!r}')
365 
366  def wait_for_initial_pose(self):
367  self.initial_pose_receivedinitial_pose_received = False
368  # If the initial pose is not received within 100 seconds, return False
369  # this is because when setting a wrong initial pose, amcl_pose is not received
370  # and the test will hang indefinitely
371  start_time = time.time()
372  duration = 0
373  while not self.initial_pose_receivedinitial_pose_received:
374  self.info_msginfo_msg('Setting initial pose')
375  self.setInitialPosesetInitialPose()
376  self.info_msginfo_msg('Waiting for amcl_pose to be received')
377  duration = time.time() - start_time
378  if duration > self.set_initial_pose_timeoutset_initial_pose_timeout:
379  self.error_msgerror_msg('Timeout waiting for initial pose to be set')
380  return False
381  rclpy.spin_once(self, timeout_sec=1)
382  return True
383 
384 
385 def test_RobotMovesToGoal(robot_tester):
386  robot_tester.info_msg('Setting goal pose')
387  robot_tester.publishGoalPose()
388  robot_tester.info_msg('Waiting 60 seconds for robot to reach goal')
389  return robot_tester.reachesGoal(timeout=60, distance=0.5)
390 
391 
392 def run_all_tests(robot_tester):
393  # set transforms to use_sim_time
394  result = True
395  if result:
396  robot_tester.wait_for_node_active('amcl')
397  result = robot_tester.wait_for_initial_pose()
398  if result:
399  robot_tester.wait_for_node_active('bt_navigator')
400  result = robot_tester.runNavigateAction()
401 
402  if result:
403  result = test_RobotMovesToGoal(robot_tester)
404 
405  # Add more tests here if desired
406 
407  if result:
408  robot_tester.info_msg('Test PASSED')
409  else:
410  robot_tester.error_msg('Test FAILED')
411 
412  return result
413 
414 
415 def fwd_pose(x=0.0, y=0.0, z=0.01):
416  initial_pose = Pose()
417  initial_pose.position.x = x
418  initial_pose.position.y = y
419  initial_pose.position.z = z
420  initial_pose.orientation.x = 0.0
421  initial_pose.orientation.y = 0.0
422  initial_pose.orientation.z = 0.0
423  initial_pose.orientation.w = 1.0
424  return initial_pose
425 
426 
427 def get_testers(args):
428  testers = []
429 
430  if args.robot:
431  # Requested tester for one robot
432  init_x, init_y, final_x, final_y = args.robot[0]
433  tester = NavTester(
434  initial_pose=fwd_pose(float(init_x), float(init_y)),
435  goal_pose=fwd_pose(float(final_x), float(final_y)),
436  )
437  tester.info_msg(
438  'Starting tester, robot going from '
439  + init_x
440  + ', '
441  + init_y
442  + ' to '
443  + final_x
444  + ', '
445  + final_y
446  + '.'
447  )
448  testers.append(tester)
449  return testers
450 
451  # Requested tester for multiple robots
452  for robot in args.robots:
453  namespace, init_x, init_y, final_x, final_y = robot
454  tester = NavTester(
455  namespace=namespace,
456  initial_pose=fwd_pose(float(init_x), float(init_y)),
457  goal_pose=fwd_pose(float(final_x), float(final_y)),
458  )
459  tester.info_msg(
460  'Starting tester for '
461  + namespace
462  + ' going from '
463  + init_x
464  + ', '
465  + init_y
466  + ' to '
467  + final_x
468  + ', '
469  + final_y
470  )
471  testers.append(tester)
472  return testers
473 
474 
475 def check_args(expect_failure: str):
476  # Check if --expect_failure is True or False
477  if expect_failure != 'True' and expect_failure != 'False':
478  print(
479  '\033[1;37;41m' + ' -e flag must be set to True or False only. ' + '\033[0m'
480  )
481  exit(1)
482  else:
483  return eval(expect_failure)
484 
485 
486 def main(argv=sys.argv[1:]):
487  # The robot(s) positions from the input arguments
488  parser = argparse.ArgumentParser(description='System-level navigation tester node')
489  parser.add_argument('-e', '--expect_failure')
490  group = parser.add_mutually_exclusive_group(required=True)
491  group.add_argument(
492  '-r',
493  '--robot',
494  action='append',
495  nargs=4,
496  metavar=('init_x', 'init_y', 'final_x', 'final_y'),
497  help='The robot starting and final positions.',
498  )
499  group.add_argument(
500  '-rs',
501  '--robots',
502  action='append',
503  nargs=5,
504  metavar=('name', 'init_x', 'init_y', 'final_x', 'final_y'),
505  help="The robot's namespace and starting and final positions. "
506  + 'Repeating the argument for multiple robots is supported.',
507  )
508 
509  args, unknown = parser.parse_known_args()
510 
511  expect_failure = check_args(args.expect_failure)
512 
513  rclpy.init()
514 
515  # Create testers for each robot
516  testers = get_testers(args)
517 
518  # wait a few seconds to make sure entire stacks are up
519  time.sleep(10)
520 
521  for tester in testers:
522  passed = run_all_tests(tester)
523  if passed != expect_failure:
524  break
525 
526  for tester in testers:
527  # stop and shutdown the nav stack to exit cleanly
528  tester.shutdown()
529 
530  testers[0].info_msg('Done Shutting Down.')
531 
532  if passed != expect_failure:
533  testers[0].info_msg('Exiting failed')
534  exit(1)
535  else:
536  testers[0].info_msg('Exiting passed')
537  exit(0)
538 
539 
540 if __name__ == '__main__':
541  main()
None poseCallback(self, PoseWithCovarianceStamped msg)