Skip to content

Commit 57b1467

Browse files
committed
Implement action feedback capabilities
1 parent f857c5d commit 57b1467

File tree

4 files changed

+132
-8
lines changed

4 files changed

+132
-8
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Software License Agreement (BSD License)
2+
#
3+
# Copyright (c) 2023, PickNik Inc.
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions
8+
# are met:
9+
#
10+
# * Redistributions of source code must retain the above copyright
11+
# notice, this list of conditions and the following disclaimer.
12+
# * Redistributions in binary form must reproduce the above
13+
# copyright notice, this list of conditions and the following
14+
# disclaimer in the documentation and/or other materials provided
15+
# with the distribution.
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived
18+
# from this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23+
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24+
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30+
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31+
# POSSIBILITY OF SUCH DAMAGE.
32+
33+
from rosbridge_library.capability import Capability
34+
from rosbridge_library.internal import message_conversion, ros_loader
35+
36+
37+
class ActionFeedback(Capability):
38+
39+
action_feedback_msg_fields = [
40+
(True, "action", str),
41+
(False, "id", str),
42+
(False, "values", dict),
43+
]
44+
45+
def __init__(self, protocol):
46+
# Call superclass constructor
47+
Capability.__init__(self, protocol)
48+
49+
# Register the operations that this capability provides
50+
protocol.register_operation("action_feedback", self.action_feedback)
51+
52+
def action_feedback(self, message):
53+
# Typecheck the args
54+
self.basic_type_check(message, self.action_feedback_msg_fields)
55+
56+
# check for the action
57+
action_name = message["action"]
58+
if action_name in self.protocol.external_action_list:
59+
action_handler = self.protocol.external_action_list[action_name]
60+
# parse the message
61+
goal_id = message["id"]
62+
values = message["values"]
63+
# create a message instance
64+
feedback = ros_loader.get_action_feedback_instance(action_handler.action_type)
65+
message_conversion.populate_instance(values, feedback)
66+
# pass along the feedback
67+
action_handler.handle_feedback(goal_id, feedback)
68+
else:
69+
self.protocol.log(
70+
"error",
71+
f"Action {action_name} has not been advertised via rosbridge.",
72+
)

rosbridge_library/src/rosbridge_library/capabilities/advertise_action.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class AdvertisedActionHandler:
4747

4848
def __init__(self, action_name, action_type, protocol, sleep_time=0.001):
4949
self.goal_futures = {}
50+
self.goal_handles = {}
51+
5052
self.action_name = action_name
5153
self.action_type = action_type
5254
self.protocol = protocol
@@ -70,6 +72,7 @@ def execute_callback(self, goal):
7072
goal_id = f"action_goal:{self.action_name}:{self.next_id()}"
7173

7274
future = rclpy.task.Future()
75+
self.goal_handles[goal_id] = goal
7376
self.goal_futures[goal_id] = future
7477

7578
# build a request to send to the external client
@@ -88,8 +91,20 @@ def execute_callback(self, goal):
8891
result = future.result()
8992
goal.succeed()
9093
del self.goal_futures[goal_id]
94+
del self.goal_handles[goal_id]
9195
return result
9296

97+
98+
def handle_feedback(self, goal_id, feedback):
99+
"""
100+
Called by the ActionFeedback capability to handle action feedback from the external client.
101+
"""
102+
if goal_id in self.goal_handles:
103+
self.goal_handles[goal_id].publish_feedback(feedback)
104+
else:
105+
self.protocol.log("warning", f"Received action feedback for unrecognized id: {goal_id}")
106+
107+
93108
def handle_result(self, goal_id, res):
94109
"""
95110
Called by the ActionResult capability to handle an action result from the external client.

rosbridge_library/src/rosbridge_library/capabilities/send_action_goal.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ def send_action_goal(self, message):
111111
# Create the callbacks
112112
s_cb = partial(self._success, cid, action, fragment_size, compression)
113113
e_cb = partial(self._failure, cid, action)
114-
feedback = True # TODO: Implement
115-
if feedback:
114+
if message.get("feedback", False):
116115
f_cb = partial(self._feedback, cid, action)
117116
else:
118117
f_cb = None
@@ -156,7 +155,6 @@ def _feedback(self, cid, action, message):
156155
if cid is not None:
157156
outgoing_message["id"] = cid
158157
# TODO: fragmentation, compression
159-
print(outgoing_message)
160158
self.protocol.send(outgoing_message)
161159

162160

rosbridge_library/test/capabilities/test_action_capabilities.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
from json import dumps, loads
55
from threading import Thread
66

7+
from example_interfaces.action._fibonacci import Fibonacci_FeedbackMessage
8+
79
import rclpy
8-
from rclpy.executors import SingleThreadedExecutor
10+
from rclpy.executors import MultiThreadedExecutor
911
from rclpy.node import Node
12+
from rclpy.qos import DurabilityPolicy, ReliabilityPolicy, QoSProfile
13+
from rosbridge_library.capabilities.action_feedback import ActionFeedback
1014
from rosbridge_library.capabilities.action_result import ActionResult
1115
from rosbridge_library.capabilities.advertise_action import AdvertiseAction
1216
from rosbridge_library.capabilities.send_action_goal import SendActionGoal
@@ -20,7 +24,7 @@
2024
class TestActionCapabilities(unittest.TestCase):
2125
def setUp(self):
2226
rclpy.init()
23-
self.executor = SingleThreadedExecutor()
27+
self.executor = MultiThreadedExecutor(num_threads=2)
2428
self.node = Node("test_action_capabilities")
2529
self.executor.add_node(self.node)
2630

@@ -36,6 +40,7 @@ def setUp(self):
3640
# self.unadvertise = UnadvertiseService(self.proto)
3741
self.result = ActionResult(self.proto)
3842
self.send_goal = SendActionGoal(self.proto)
43+
self.feedback = ActionFeedback(self.proto)
3944
self.received_message = None
4045
self.log_entries = []
4146

@@ -48,9 +53,11 @@ def tearDown(self):
4853
rclpy.shutdown()
4954

5055
def local_send_cb(self, msg):
51-
print(f"Received: {msg}")
5256
self.received_message = msg
5357

58+
def feedback_subscriber_cb(self, msg):
59+
self.latest_feedback = msg
60+
5461
def mock_log(self, loglevel, message, _=None):
5562
self.log_entries.append((loglevel, message))
5663

@@ -102,7 +109,6 @@ def test_execute_advertised_action(self):
102109
)
103110
self.received_message = None
104111
self.advertise.advertise_action(advertise_msg)
105-
# rclpy.spin_once(self.node, timeout_sec=0.1)
106112

107113
# Send a goal to the advertised action using rosbridge
108114
self.received_message = None
@@ -131,7 +137,40 @@ def test_execute_advertised_action(self):
131137
self.assertTrue(self.received_message["op"] == "send_action_goal")
132138
self.assertTrue("id" in self.received_message)
133139

134-
# TODO: Send feedback
140+
# Send feedback message
141+
self.latest_feedback = None
142+
sub_qos_profile = QoSProfile(
143+
depth=10,
144+
durability=DurabilityPolicy.VOLATILE,
145+
reliability=ReliabilityPolicy.RELIABLE,
146+
)
147+
self.subscription = self.node.create_subscription(
148+
Fibonacci_FeedbackMessage,
149+
f"{action_path}/_action/feedback",
150+
self.feedback_subscriber_cb,
151+
sub_qos_profile)
152+
time.sleep(0.1)
153+
154+
feedback_msg = loads(
155+
dumps(
156+
{
157+
"op": "action_feedback",
158+
"action": action_path,
159+
"id": self.received_message["id"],
160+
"values": {"sequence": [1, 1, 2]},
161+
}
162+
)
163+
)
164+
self.feedback.action_feedback(feedback_msg)
165+
loop_iterations = 0
166+
while self.latest_feedback is None:
167+
time.sleep(0.5)
168+
loop_iterations += 1
169+
if loop_iterations > 3:
170+
self.fail("Timed out waiting for action feedback message.")
171+
172+
self.assertIsNotNone(self.latest_feedback)
173+
self.assertEqual(list(self.latest_feedback.feedback.sequence), [1, 1, 2])
135174

136175
# Now send the result
137176
result_msg = loads(

0 commit comments

Comments
 (0)