Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Commit 35a9808

Browse files
author
fred-labs
authored
Update nav2 library
1 parent 855f612 commit 35a9808

File tree

12 files changed

+206
-128
lines changed

12 files changed

+206
-128
lines changed

.github/workflows/test_build.yml

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ jobs:
351351
export -n CYCLONEDDS_URI
352352
export ROS_DOMAIN_ID=2
353353
# shellcheck disable=SC1083
354-
scenario_batch_execution -i examples/example_external_method/scenarios -o test_example_external_method -- ros2 launch scenario_execution_ros scenario_launch.py scenario:={SCENARIO} output_dir:={OUTPUT_DIR}
354+
scenario_batch_execution -i examples/example_external_method/scenarios -o test_example_external_method -- ros2 launch scenario_execution_ros scenario_launch.py scenario:={SCENARIO} output_dir:={OUTPUT_DIR} headless:=True
355355
- name: Upload result
356356
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
357357
if: always()
@@ -384,13 +384,59 @@ jobs:
384384
export ROS_DOMAIN_ID=2
385385
export IGN_PARTITION=${HOSTNAME}:${GITHUB_RUN_ID}
386386
# shellcheck disable=SC1083
387-
scenario_batch_execution -i test/scenario_execution_gazebo_test/scenarios/ -o test_scenario_execution_gazebo -- ros2 launch tb4_sim_scenario sim_nav_scenario_launch.py scenario:={SCENARIO} output_dir:={OUTPUT_DIR}
387+
scenario_batch_execution -i test/scenario_execution_gazebo_test/scenarios/ -o test_scenario_execution_gazebo -- ros2 launch tb4_sim_scenario sim_nav_scenario_launch.py scenario:={SCENARIO} output_dir:={OUTPUT_DIR} headless:=True
388388
- name: Upload result
389389
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
390390
if: always()
391391
with:
392392
name: test-scenario-execution-gazebo
393393
path: test_scenario_execution_gazebo/test.xml
394+
test-scenario-execution-nav2:
395+
needs: [build]
396+
runs-on: intellabs-01
397+
timeout-minutes: 30
398+
container:
399+
image: ghcr.io/intellabs/scenario-execution:${{ github.event.pull_request.base.ref }}
400+
credentials:
401+
username: ${{ github.repository_owner }}
402+
password: ${{ secrets.GITHUB_TOKEN }}
403+
steps:
404+
- name: Restore cache
405+
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
406+
with:
407+
key: ${{ runner.os }}-build-${{ github.run_number }}
408+
path: .
409+
- name: Test Scenario Execution Nav2
410+
shell: bash
411+
run: |
412+
source /opt/ros/${{ github.event.pull_request.base.ref == 'main' && 'humble' || github.event.pull_request.base.ref }}/setup.bash
413+
source install/setup.bash
414+
Xvfb :1 -screen 0 800x600x16 &
415+
export DISPLAY=:1.0
416+
export -n CYCLONEDDS_URI
417+
export ROS_DOMAIN_ID=2
418+
export IGN_PARTITION=${HOSTNAME}:${GITHUB_RUN_ID}
419+
# shellcheck disable=SC1083
420+
scenario_batch_execution -i test/scenario_execution_nav2_test/scenarios/ -o test_scenario_execution_nav2 -- ros2 launch tb4_sim_scenario sim_nav_scenario_launch.py scenario:={SCENARIO} output_dir:={OUTPUT_DIR} headless:=True
421+
- name: Upload result
422+
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
423+
if: always()
424+
with:
425+
name: test-scenario-execution-nav2
426+
path: test_scenario_execution_nav2/test.xml
427+
- name: Convert video
428+
if: always()
429+
shell: bash
430+
run: |
431+
source /opt/ros/${{ github.event.pull_request.base.ref == 'main' && 'humble' || github.event.pull_request.base.ref }}/setup.bash
432+
source /rosbag2_to_video/install/setup.bash
433+
ros2 bag to_video -t /static_camera/image_raw -o test_scenario_execution_nav2/test_scenario_execution_nav2/example_nav2.mp4 test_scenario_execution_nav2/test_scenario_execution_nav2/rosbag2_* --fps 5 --codec mp4v
434+
- name: Upload video
435+
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
436+
if: always()
437+
with:
438+
name: test-scenario-execution-nav2-video
439+
path: test_scenario_execution_nav2/test_scenario_execution_nav2/example_nav2.mp4
394440
test-scenario-execution-pybullet:
395441
needs: [build]
396442
runs-on: intellabs-01
@@ -434,6 +480,7 @@ jobs:
434480
- test-example-multirobot
435481
- test-example-external-method
436482
- test-scenario-execution-gazebo
483+
- test-scenario-execution-nav2
437484
- test-scenario-execution-pybullet
438485
runs-on: intellabs-01
439486
if: ${{ always() }}
@@ -471,4 +518,5 @@ jobs:
471518
downloaded-artifacts/test-example-multirobot-result/test.xml
472519
downloaded-artifacts/test-example-external-method-result/test.xml
473520
downloaded-artifacts/test-scenario-execution-gazebo/test.xml
521+
downloaded-artifacts/test-scenario-execution-nav2/test.xml
474522
downloaded-artifacts/test-scenario-execution-pybullet/test.xml

docs/libraries.rst

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -586,10 +586,10 @@ Use nav2 to navigate through poses.
586586
- ``string``
587587
- ``''``
588588
- If set, it's used as namespace (instead of the associated actor's namespace)
589-
* - ``monitor_progress``
590-
- ``bool``
591-
- ``true``
592-
- If yes, the action returns after the goal is reached or on failure. If no, the action returns after request.
589+
* - ``action_topic``
590+
- ``string``
591+
- ``navigate_through_poses``
592+
- Action name
593593

594594
``differential_drive_robot.nav_to_pose()``
595595
""""""""""""""""""""""""""""""""""""""""""
@@ -616,10 +616,6 @@ Use nav2 to navigate to goal pose.
616616
- ``string``
617617
- ``navigate_to_pose``
618618
- Action name
619-
* - ``monitor_progress``
620-
- ``bool``
621-
- ``true``
622-
- If yes, the action returns after the goal is reached or on failure. If no, the action returns after request.
623619

624620

625621
OS

libs/scenario_execution_nav2/scenario_execution_nav2/actions/nav_through_poses.py

Lines changed: 31 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -14,117 +14,47 @@
1414
#
1515
# SPDX-License-Identifier: Apache-2.0
1616

17-
from enum import Enum
18-
19-
from rclpy.node import Node
2017
from rclpy.duration import Duration
21-
22-
import py_trees
23-
24-
from nav2_simple_commander.robot_navigator import TaskResult # pylint: disable=import-error
25-
26-
from .nav2_common import NamespaceAwareBasicNavigator
18+
from nav2_msgs.action import NavigateThroughPoses
2719
from scenario_execution_ros.actions.common import get_pose_stamped
28-
from scenario_execution.actions.base_action import BaseAction
29-
30-
31-
class NavThroughPosesState(Enum):
32-
"""
33-
States for executing a nav-through-poses with nav2
34-
"""
35-
IDLE = 1
36-
NAV_TO_GOAL = 2
37-
DONE = 3
20+
from scenario_execution_ros.actions.ros_action_call import RosActionCall, ActionCallActionState
3821

3922

40-
class NavThroughPoses(BaseAction):
23+
class NavThroughPoses(RosActionCall):
4124
"""
4225
Class to navigate through poses
4326
"""
4427

45-
def __init__(self, associated_actor, goal_poses: list, monitor_progress: bool, namespace_override: str):
46-
super().__init__()
28+
def __init__(self, associated_actor, goal_poses: list, action_topic: str, namespace_override: str):
4729
self.namespace = associated_actor["namespace"]
48-
self.monitor_progress = monitor_progress
49-
self.node = None
50-
self.future = None
51-
self.current_state = NavThroughPosesState.IDLE
52-
self.nav = None
5330
if namespace_override:
5431
self.namespace = namespace_override
32+
self.goal_poses = None
33+
super().__init__(self.namespace + '/' + action_topic, "nav2_msgs.action.NavigateThroughPoses", "")
5534

56-
if not isinstance(goal_poses, list):
57-
raise TypeError(f'goal_poses needs to be list of position_3d, got {type(goal_poses)}.')
58-
else:
59-
self.goal_poses = goal_poses
60-
61-
def setup(self, **kwargs):
62-
"""
63-
Setup ROS2 node and service client
64-
65-
"""
66-
try:
67-
self.node: Node = kwargs['node']
68-
except KeyError as e:
69-
error_message = "didn't find 'node' in setup's kwargs [{}][{}]".format(
70-
self.name, self.__class__.__name__)
71-
raise KeyError(error_message) from e
72-
73-
self.nav = NamespaceAwareBasicNavigator(
74-
node_name="basic_nav_nav_through_poses", namespace=self.namespace)
75-
76-
def update(self) -> py_trees.common.Status:
77-
"""
78-
Execute states
79-
"""
80-
self.logger.debug(f"Current State {self.current_state}")
81-
result = py_trees.common.Status.FAILURE
82-
if self.current_state == NavThroughPosesState.IDLE:
83-
self.current_state = NavThroughPosesState.NAV_TO_GOAL
84-
goal_poses = []
85-
for pose in self.goal_poses:
86-
goal_poses.append(get_pose_stamped(self.nav.get_clock().now().to_msg(), pose))
87-
self.feedback_message = "Execute navigation." # pylint: disable= attribute-defined-outside-init
88-
go_to_pose_result = self.nav.goThroughPoses(goal_poses)
89-
if go_to_pose_result:
90-
if self.monitor_progress:
91-
result = py_trees.common.Status.RUNNING
92-
self.current_state = NavThroughPosesState.NAV_TO_GOAL
93-
else:
94-
result = py_trees.common.Status.SUCCESS
95-
self.current_state = NavThroughPosesState.DONE
96-
else:
97-
self.current_state = NavThroughPosesState.DONE
98-
result = py_trees.common.Status.FAILURE
99-
elif self.current_state == NavThroughPosesState.NAV_TO_GOAL:
100-
if not self.nav.isTaskComplete():
101-
feedback = self.nav.getFeedback()
102-
if feedback:
103-
self.feedback_message = 'Estimated time of arrival: ' + '{0:.0f}'.format( # pylint: disable= attribute-defined-outside-init
104-
Duration.from_msg(feedback.estimated_time_remaining).nanoseconds / 1e9) + ' seconds.'
105-
106-
if Duration.from_msg(feedback.navigation_time) > Duration(seconds=600):
107-
self.nav.cancelTask()
108-
result = py_trees.common.Status.RUNNING
35+
def execute(self, associated_actor, goal_poses: list, action_topic: str, namespace_override: str) -> None: # pylint: disable=arguments-differ
36+
self.namespace = associated_actor["namespace"]
37+
if namespace_override:
38+
self.namespace = namespace_override
39+
self.goal_poses = goal_poses
40+
super().execute(self.namespace + '/' + action_topic, "nav2_msgs.action.NavigateThroughPoses", "")
41+
42+
def get_goal_msg(self):
43+
goal_msg = NavigateThroughPoses.Goal()
44+
for pose in self.goal_poses:
45+
goal_msg.poses.append(get_pose_stamped(self.node.get_clock().now().to_msg(), pose))
46+
return goal_msg
47+
48+
def get_feedback_message(self, current_state):
49+
feedback_message = super().get_feedback_message(current_state)
50+
51+
if self.current_state == ActionCallActionState.IDLE:
52+
feedback_message = "Waiting for navigation"
53+
elif self.current_state == ActionCallActionState.ACTION_CALLED:
54+
if self.received_feedback:
55+
feedback_message = f'Estimated time of arrival: {Duration.from_msg(self.received_feedback.estimated_time_remaining).nanoseconds / 1e9:0.0f}, poses left {self.received_feedback.number_of_poses_remaining}.'
10956
else:
110-
self.current_state = NavThroughPosesState.DONE
111-
result = self.nav.getResult()
112-
if result == TaskResult.SUCCEEDED:
113-
self.feedback_message = 'Goal succeeded!' # pylint: disable= attribute-defined-outside-init
114-
result = py_trees.common.Status.SUCCESS
115-
elif result == TaskResult.CANCELED:
116-
self.feedback_message = 'Goal was canceled!' # pylint: disable= attribute-defined-outside-init
117-
result = py_trees.common.Status.FAILURE
118-
elif result == TaskResult.FAILED:
119-
self.feedback_message = 'Goal failed!' # pylint: disable= attribute-defined-outside-init
120-
result = py_trees.common.Status.FAILURE
121-
elif self.current_state == NavThroughPosesState.DONE:
122-
self.logger.debug("Nothing to do!")
123-
else:
124-
self.logger.error(f"Invalid state {self.current_state}")
125-
126-
return result
127-
128-
def shutdown(self):
129-
self.nav.cancelTask()
130-
self.nav.destroy_node()
57+
feedback_message = f"Executing navigation to ({self.goal_poses})."
58+
elif current_state == ActionCallActionState.DONE:
59+
feedback_message = f"Goal reached."
60+
return feedback_message

libs/scenario_execution_nav2/scenario_execution_nav2/actions/nav_to_pose.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,20 @@ class NavToPose(RosActionCall):
2626
Class to navigate to a pose
2727
"""
2828

29-
def __init__(self, associated_actor, goal_pose: list, monitor_progress: bool, action_topic: str, namespace_override: str) -> None:
29+
def __init__(self, associated_actor, goal_pose: list, action_topic: str, namespace_override: str) -> None:
3030
self.namespace = associated_actor["namespace"]
3131
if namespace_override:
3232
self.namespace = namespace_override
33-
self.goal_pose = goal_pose
33+
self.goal_pose = None
3434
super().__init__(self.namespace + '/' + action_topic, "nav2_msgs.action.NavigateToPose", "")
3535

36+
def execute(self, associated_actor, goal_pose: list, action_topic: str, namespace_override: str) -> None: # pylint: disable=arguments-differ
37+
self.namespace = associated_actor["namespace"]
38+
if namespace_override:
39+
self.namespace = namespace_override
40+
self.goal_pose = goal_pose
41+
super().execute(self.namespace + '/' + action_topic, "nav2_msgs.action.NavigateToPose", "")
42+
3643
def get_goal_msg(self):
3744
goal_msg = NavigateToPose.Goal()
3845
goal_msg.pose = get_pose_stamped(self.node.get_clock().now().to_msg(), self.goal_pose)
@@ -45,11 +52,9 @@ def get_feedback_message(self, current_state):
4552
feedback_message = "Waiting for navigation"
4653
elif self.current_state == ActionCallActionState.ACTION_CALLED:
4754
if self.received_feedback:
48-
feedback_message = 'Estimated time of arrival: ' + \
49-
'{0:.0f}'.format(Duration.from_msg(self.received_feedback.estimated_time_remaining).nanoseconds / 1e9) + ' seconds.'
55+
feedback_message = f'Estimated time of arrival: {Duration.from_msg(self.received_feedback.estimated_time_remaining).nanoseconds / 1e9:0.0f}.'
5056
else:
51-
goal_msg = self.get_goal_msg()
52-
feedback_message = f"Executing navigation to ({goal_msg.pose.pose.position.x}, {goal_msg.pose.pose.position.y})."
57+
feedback_message = f"Executing navigation to ({self.goal_pose})."
5358
elif current_state == ActionCallActionState.DONE:
5459
feedback_message = f"Goal reached."
5560
return feedback_message

libs/scenario_execution_nav2/scenario_execution_nav2/lib_osc/nav2.osc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ action differential_drive_robot.nav_through_poses:
1313
# Use nav2 to navigate through poses.
1414
goal_poses: list of pose_3d # goal poses to navigate through
1515
namespace_override: string = '' # if set, it's used as namespace (instead of the associated actor's name)
16-
monitor_progress: bool = true # if true, the action returns after the goal is reached or on failure. If no, the action returns after request.
16+
action_topic: string = 'navigate_through_poses' # Name of action
1717

1818
action differential_drive_robot.nav_to_pose:
1919
# Nav2 to navigate to goal pose.
2020
goal_pose: pose_3d # goal pose to navigate to
2121
namespace_override: string = '' # if set, it's used as namespace (instead of the associated actor's name)
22-
action_topic: string = 'navigate_to_pose' # Name of action
23-
monitor_progress: bool = true # if yes, the action returns after the goal is reached or on failure. If no, the action returns after request.
22+
action_topic: string = 'navigate_to_pose' # Name of action

scenario_execution_ros/scenario_execution_ros/actions/ros_action_call.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,12 @@ def __init__(self, action_name: str, action_type: str, data: str):
4848
self.client = None
4949
self.send_goal_future = None
5050
self.goal_handle = None
51-
self.action_type = action_type
51+
self.action_type_string = action_type
52+
self.action_type = None
5253
self.action_name = action_name
5354
self.received_feedback = None
54-
if data:
55-
try:
56-
trimmed_data = data.encode('utf-8').decode('unicode_escape')
57-
self.data = literal_eval(trimmed_data)
58-
except Exception as e: # pylint: disable=broad-except
59-
raise ValueError(f"Error while parsing sevice call data:") from e
55+
self.data = None
56+
self.parse_data(data)
6057
self.current_state = ActionCallActionState.IDLE
6158
self.cb_group = ReentrantCallbackGroup()
6259

@@ -72,7 +69,7 @@ def setup(self, **kwargs):
7269
self.name, self.__class__.__name__)
7370
raise KeyError(error_message) from e
7471

75-
datatype_in_list = self.action_type.split(".")
72+
datatype_in_list = self.action_type_string.split(".")
7673
try:
7774
self.action_type = getattr(
7875
importlib.import_module(".".join(datatype_in_list[0:-1])),
@@ -82,6 +79,21 @@ def setup(self, **kwargs):
8279

8380
self.client = ActionClient(self.node, self.action_type, self.action_name, callback_group=self.cb_group)
8481

82+
def execute(self, action_name: str, action_type: str, data: str):
83+
if self.action_name != action_name or self.action_type_string != action_type:
84+
raise ValueError(f"Updating action_name or action_type_string not supported.")
85+
86+
self.parse_data(data)
87+
self.current_state = ActionCallActionState.IDLE
88+
89+
def parse_data(self, data):
90+
if data:
91+
try:
92+
trimmed_data = data.encode('utf-8').decode('unicode_escape')
93+
self.data = literal_eval(trimmed_data)
94+
except Exception as e: # pylint: disable=broad-except
95+
raise ValueError(f"Error while parsing sevice call data:") from e
96+
8597
def update(self) -> py_trees.common.Status:
8698
"""
8799
Execute states
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Tests of Scenario Execution Library for Nav2
2+
3+
The `scenario_execution_nav2_test` package provides test scenarios to test `scenario_execution_nav2` within the `tb4_sim_scenario` setup.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>scenario_execution_nav2_test</name>
5+
<version>1.2.0</version>
6+
<description>Tests for Scenario Execution library for Nav2</description>
7+
<author email="[email protected]">Intel Labs</author>
8+
<maintainer email="[email protected]">Intel Labs</maintainer>
9+
<license file="../../LICENSE">Apache-2.0</license>
10+
11+
<exec_depend>rclpy</exec_depend>
12+
<exec_depend>scenario_execution_nav2</exec_depend>
13+
<exec_depend>tb4_sim_scenario</exec_depend>
14+
<exec_depend>gazebo_static_camera</exec_depend>
15+
16+
<test_depend>ament_copyright</test_depend>
17+
<test_depend>ament_flake8</test_depend>
18+
<test_depend>ament_pep257</test_depend>
19+
<test_depend>python3-pytest</test_depend>
20+
21+
<export>
22+
<build_type>ament_python</build_type>
23+
</export>
24+
</package>

test/scenario_execution_nav2_test/resource/scenario_execution_nav2_test

Whitespace-only changes.

0 commit comments

Comments
 (0)