Skip to content

Commit e54af07

Browse files
authored
Merge pull request #143 from RobotWebTools/updates-ros2-support
Updates to ROS2 support
2 parents 1d549b7 + 7c52a8d commit e54af07

16 files changed

Lines changed: 492 additions & 69 deletions

File tree

.github/workflows/test-ros2.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
- name: Set up docker containers
5151
run: |
5252
docker build -t gramaziokohler/rosbridge:integration_tests_ros2 ./docker/ros2
53-
docker run -d -p 9090:9090 --name rosbridge gramaziokohler/rosbridge:integration_tests_ros2 /bin/bash -c "ros2 launch /integration-tests.launch"
53+
docker run -d -p 9090:9090 --name rosbridge gramaziokohler/rosbridge:integration_tests_ros2 /bin/bash -c "ros2 launch /integration-tests-launch.py"
5454
docker ps -a
5555
- name: Run linter
5656
run: |

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@ Unreleased
1111
----------
1212

1313
**Added**
14+
1415
* Added ROS 2 action client support.
16+
* Added ``wait_goal`` method to ``ActionClient`` to block until a goal reaches a terminal state.
17+
* Moved ``actionlib`` module to ``roslibpy.ros1`` namespace.
1518

1619
**Changed**
1720

1821
**Fixed**
1922

23+
* Fixed KeyError in ROS 2 ActionClient when receiving action results with different message formats from rosbridge.
24+
2025
**Deprecated**
2126

2227
**Removed**

docker/ros2/Dockerfile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ RUN apt-get update && apt-get install -y \
88
ros-${ROS_DISTRO}-rosbridge-suite \
99
# ros-${ROS_DISTRO}-tf2-web-republisher \
1010
# ros-${ROS_DISTRO}-ros-tutorials \
11-
# ros-${ROS_DISTRO}-actionlib-tutorials \
11+
ros-${ROS_DISTRO}-demo-nodes-py \
12+
ros-${ROS_DISTRO}-example-interfaces \
1213
--no-install-recommends \
1314
# Clear apt-cache to reduce image size
1415
&& rm -rf /var/lib/apt/lists/*
1516

16-
# Copy launch
17-
COPY ./integration-tests.launch /
17+
# Copy launch and example action server
18+
COPY ./integration-tests-launch.py /
19+
COPY ./fibonacci_server.py /
1820

1921
EXPOSE 9090
2022

docker/ros2/fibonacci_server.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Copyright 2019 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import threading
16+
import time
17+
18+
from example_interfaces.action import Fibonacci
19+
20+
import rclpy
21+
from rclpy.action import ActionServer, CancelResponse, GoalResponse
22+
from rclpy.callback_groups import ReentrantCallbackGroup
23+
from rclpy.executors import ExternalShutdownException
24+
from rclpy.executors import MultiThreadedExecutor
25+
from rclpy.node import Node
26+
27+
28+
class MinimalActionServer(Node):
29+
"""Minimal action server that processes one goal at a time."""
30+
31+
def __init__(self):
32+
super().__init__('minimal_action_server')
33+
self._goal_handle = None
34+
self._goal_lock = threading.Lock()
35+
self._action_server = ActionServer(
36+
self,
37+
Fibonacci,
38+
'fibonacci',
39+
execute_callback=self.execute_callback,
40+
goal_callback=self.goal_callback,
41+
handle_accepted_callback=self.handle_accepted_callback,
42+
cancel_callback=self.cancel_callback,
43+
callback_group=ReentrantCallbackGroup())
44+
self.get_logger().info('Starting fibonacci action server..')
45+
46+
def destroy(self):
47+
self._action_server.destroy()
48+
super().destroy_node()
49+
50+
def goal_callback(self, goal_request):
51+
"""Accept or reject a client request to begin an action."""
52+
self.get_logger().info('Received goal request')
53+
return GoalResponse.ACCEPT
54+
55+
def handle_accepted_callback(self, goal_handle):
56+
with self._goal_lock:
57+
# This server only allows one goal at a time
58+
if self._goal_handle is not None and self._goal_handle.is_active:
59+
self.get_logger().info('Aborting previous goal')
60+
# Abort the existing goal
61+
self._goal_handle.abort()
62+
self._goal_handle = goal_handle
63+
64+
goal_handle.execute()
65+
66+
def cancel_callback(self, goal):
67+
"""Accept or reject a client request to cancel an action."""
68+
self.get_logger().info('Received cancel request')
69+
return CancelResponse.ACCEPT
70+
71+
def execute_callback(self, goal_handle):
72+
"""Execute the goal."""
73+
self.get_logger().info('Executing goal...')
74+
75+
# Append the seeds for the Fibonacci sequence
76+
feedback_msg = Fibonacci.Feedback()
77+
feedback_msg.sequence = [0, 1]
78+
79+
# Start executing the action
80+
for i in range(1, goal_handle.request.order):
81+
# If goal is flagged as no longer active (ie. another goal was accepted),
82+
# then stop executing
83+
if not goal_handle.is_active:
84+
self.get_logger().info('Goal aborted')
85+
return Fibonacci.Result()
86+
87+
if goal_handle.is_cancel_requested:
88+
goal_handle.canceled()
89+
self.get_logger().info('Goal canceled')
90+
return Fibonacci.Result()
91+
92+
# Update Fibonacci sequence
93+
feedback_msg.sequence.append(feedback_msg.sequence[i] + feedback_msg.sequence[i-1])
94+
95+
self.get_logger().info('Publishing feedback: {0}'.format(feedback_msg.sequence))
96+
97+
# Publish the feedback
98+
goal_handle.publish_feedback(feedback_msg)
99+
100+
# Sleep for demonstration purposes
101+
time.sleep(1)
102+
103+
with self._goal_lock:
104+
if not goal_handle.is_active:
105+
self.get_logger().info('Goal aborted')
106+
return Fibonacci.Result()
107+
108+
goal_handle.succeed()
109+
110+
# Populate result message
111+
result = Fibonacci.Result()
112+
result.sequence = feedback_msg.sequence
113+
114+
self.get_logger().info('Returning result: {0}'.format(result.sequence))
115+
116+
return result
117+
118+
119+
def main(args=None):
120+
try:
121+
rclpy.init(args=args)
122+
action_server = MinimalActionServer()
123+
124+
# We use a MultiThreadedExecutor to handle incoming goal requests concurrently
125+
executor = MultiThreadedExecutor()
126+
rclpy.spin(action_server, executor=executor)
127+
except (KeyboardInterrupt, ExternalShutdownException):
128+
pass
129+
130+
131+
if __name__ == '__main__':
132+
main()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from launch import LaunchDescription
2+
from launch.substitutions import PathJoinSubstitution
3+
from launch_ros.substitutions import FindPackageShare
4+
from launch.actions import ExecuteProcess, IncludeLaunchDescription
5+
6+
def generate_launch_description():
7+
return LaunchDescription([
8+
# Start rosbridge_websocket
9+
IncludeLaunchDescription(
10+
PathJoinSubstitution([
11+
FindPackageShare('rosbridge_server'),
12+
'launch',
13+
'rosbridge_websocket_launch.xml'
14+
])
15+
),
16+
17+
# Start fibonacci_server.py with python3
18+
ExecuteProcess(
19+
cmd=['python3', "/fibonacci_server.py"],
20+
output='screen'
21+
)
22+
])

docker/ros2/integration-tests.launch

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/files/ros2-action-client.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def result_callback(msg):
1111
print("Action result:", msg)
1212

1313
def feedback_callback(msg):
14-
print(f"Action feedback: {msg['partial_sequence']}")
14+
print(f"Action feedback: {msg['sequence']}")
1515

1616
def fail_callback(msg):
1717
print(f"Action failed: {msg}")
@@ -23,7 +23,8 @@ def test_action_success(action_client):
2323
global result
2424
result = None
2525

26-
action_client.send_goal(roslibpy.ActionGoal({"order": 8}),
26+
goal = roslibpy.Goal({"order": 8})
27+
action_client.send_goal(goal,
2728
result_callback,
2829
feedback_callback,
2930
fail_callback)
@@ -44,7 +45,8 @@ def test_action_cancel(action_client):
4445
global result
4546
result = None
4647

47-
goal_id = action_client.send_goal(roslibpy.ActionGoal({"order": 8}),
48+
goal = roslibpy.Goal({"order": 8})
49+
goal_id = action_client.send_goal(goal,
4850
result_callback,
4951
feedback_callback,
5052
fail_callback)
@@ -66,7 +68,7 @@ def test_action_cancel(action_client):
6668

6769
action_client = roslibpy.ActionClient(client,
6870
"/fibonacci",
69-
"custom_action_interfaces/action/Fibonacci")
71+
"example_interfaces/action/Fibonacci")
7072
print("\n** Starting action client test **")
7173
test_action_success(action_client)
7274

docs/reference/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ API Reference
88
from roslibpy import *
99

1010
.. automodule:: roslibpy
11-
.. automodule:: roslibpy.actionlib
1211
.. automodule:: roslibpy.tf
12+
.. automodule:: roslibpy.ros1.actionlib
1313
.. automodule:: roslibpy.ros2

src/roslibpy/__init__.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
=================
4343
4444
ROS 1 vs ROS 2
45-
------------
45+
--------------
4646
4747
This library has been tested to work with ROS 1. ROS 2 should work, but it is still
4848
in the works.
@@ -82,19 +82,21 @@ class and are passed around via :class:`Topics <Topic>` using a **publish/subscr
8282
.. autoclass:: ServiceResponse
8383
:members:
8484
85-
Actions
86-
--------
85+
Actions (ROS 2)
86+
---------------
8787
8888
An Action client for ROS 2 Actions can be used by managing goal/feedback/result
8989
messages via :class:`ActionClient <ActionClient>`.
9090
9191
.. autoclass:: ActionClient
9292
:members:
93-
.. autoclass:: ActionGoal
93+
.. autoclass:: Goal
94+
:members:
95+
.. autoclass:: GoalStatus
9496
:members:
95-
.. autoclass:: ActionFeedback
97+
.. autoclass:: Feedback
9698
:members:
97-
.. autoclass:: ActionResult
99+
.. autoclass:: Result
98100
:members:
99101
100102
Parameter server
@@ -129,13 +131,13 @@ class and are passed around via :class:`Topics <Topic>` using a **publish/subscr
129131
)
130132
from .core import (
131133
ActionClient,
132-
ActionFeedback,
133-
ActionGoal,
134-
ActionGoalStatus,
135-
ActionResult,
134+
Feedback,
135+
Goal,
136+
GoalStatus,
136137
Header,
137138
Message,
138139
Param,
140+
Result,
139141
Service,
140142
ServiceRequest,
141143
ServiceResponse,
@@ -153,19 +155,19 @@ class and are passed around via :class:`Topics <Topic>` using a **publish/subscr
153155
"__title__",
154156
"__url__",
155157
"__version__",
158+
"ActionClient",
159+
"Feedback",
160+
"Goal",
161+
"GoalStatus",
156162
"Header",
157163
"Message",
158164
"Param",
165+
"Result",
166+
"Ros",
159167
"Service",
160168
"ServiceRequest",
161169
"ServiceResponse",
162-
"ActionClient",
163-
"ActionGoal",
164-
"ActionGoalStatus",
165-
"ActionFeedback",
166-
"ActionResult",
170+
"set_rosapi_timeout",
167171
"Time",
168172
"Topic",
169-
"set_rosapi_timeout",
170-
"Ros",
171173
]

0 commit comments

Comments
 (0)