Skip to content

Commit c170282

Browse files
authored
Allow action servers without execute callback (#1219)
Signed-off-by: Tim Clephas <[email protected]>
1 parent d854e87 commit c170282

File tree

2 files changed

+79
-6
lines changed

2 files changed

+79
-6
lines changed

rclpy/rclpy/action/server.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,16 @@ def _update_state(self, event: _rclpy.GoalEvent) -> None:
161161
if not self._goal_handle.is_active():
162162
self._action_server.notify_goal_done()
163163

164+
def _set_result(self, response: Optional[ResultT]) -> None:
165+
# Set result
166+
result_response = self._action_server._action_type.Impl.GetResultService.Response()
167+
result_response.status = self.status
168+
if response is not None:
169+
result_response.result = response
170+
else:
171+
result_response.result = self._action_server._action_type.Result()
172+
self._action_server._result_futures[bytes(self.goal_id.uuid)].set_result(result_response)
173+
164174
def execute(
165175
self,
166176
execute_callback: Optional[Callable[['ServerGoalHandle[GoalT, ResultT, FeedbackT]'],
@@ -170,7 +180,7 @@ def execute(
170180
# In this case we want to avoid the illegal state transition to EXECUTING
171181
# but still call the users execute callback to let them handle canceling the goal.
172182
if not self.is_cancel_requested:
173-
self._update_state(_rclpy.GoalEvent.EXECUTE)
183+
self.executing()
174184
self._action_server.notify_execute(self, execute_callback)
175185

176186
def publish_feedback(self, feedback: FeedbackMessage[FeedbackT]) -> None:
@@ -191,14 +201,20 @@ def publish_feedback(self, feedback: FeedbackMessage[FeedbackT]) -> None:
191201
# Publish
192202
self._action_server._handle.publish_feedback(feedback_message)
193203

194-
def succeed(self) -> None:
204+
def executing(self) -> None:
205+
self._update_state(_rclpy.GoalEvent.EXECUTE)
206+
207+
def succeed(self, response: Optional[ResultT] = None) -> None:
195208
self._update_state(_rclpy.GoalEvent.SUCCEED)
209+
self._set_result(response)
196210

197-
def abort(self) -> None:
211+
def abort(self, response: Optional[ResultT] = None) -> None:
198212
self._update_state(_rclpy.GoalEvent.ABORT)
213+
self._set_result(response)
199214

200-
def canceled(self) -> None:
215+
def canceled(self, response: Optional[ResultT] = None) -> None:
201216
self._update_state(_rclpy.GoalEvent.CANCELED)
217+
self._set_result(response)
202218

203219
def destroy(self) -> None:
204220
with self._lock:
@@ -233,7 +249,8 @@ def __init__(
233249
node: 'Node',
234250
action_type: Type[Action],
235251
action_name: str,
236-
execute_callback: Callable[[ServerGoalHandle[GoalT, ResultT, FeedbackT]], ResultT],
252+
execute_callback: Optional[Callable[[ServerGoalHandle[GoalT, ResultT, FeedbackT]],
253+
ResultT]] = None,
237254
*,
238255
callback_group: 'Optional[CallbackGroup]' = None,
239256
goal_callback: Callable[[CancelGoal.Request], GoalResponse] = default_goal_callback,
@@ -283,7 +300,11 @@ def __init__(
283300
self.register_handle_accepted_callback(handle_accepted_callback)
284301
self.register_goal_callback(goal_callback)
285302
self.register_cancel_callback(cancel_callback)
286-
self.register_execute_callback(execute_callback)
303+
if execute_callback:
304+
self.register_execute_callback(execute_callback)
305+
elif handle_accepted_callback is default_handle_accepted_callback:
306+
self._logger.warning(
307+
'Not handling nor executing the goal, this server will do nothing')
287308

288309
# Import the typesupport for the action module if not already done
289310
check_for_type_support(action_type)

rclpy/test/test_action_server.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,58 @@ def execute_with_feedback(goal_handle: ServerGoalHandle[Any, Fibonacci.Result, A
766766
finally:
767767
action_server.destroy()
768768

769+
def test_without_execute_cb(self) -> None:
770+
# Just like test_execute_succeed, but without an execute callback
771+
# Goal handle is stored and succeeded from outside the execute callback
772+
stored_goal_handle = None
773+
774+
def handle_accepted_callback(goal_handle: ServerGoalHandle[Fibonacci.Goal,
775+
Fibonacci.Result,
776+
Fibonacci.Feedback]) -> None:
777+
goal_handle.executing()
778+
nonlocal stored_goal_handle
779+
stored_goal_handle = goal_handle
780+
781+
executor = MultiThreadedExecutor(context=self.context)
782+
783+
action_server = ActionServer(
784+
self.node,
785+
Fibonacci,
786+
'fibonacci',
787+
handle_accepted_callback=handle_accepted_callback,
788+
)
789+
790+
goal_uuid = UUID(uuid=list(uuid.uuid4().bytes))
791+
goal_msg = Fibonacci.Impl.SendGoalService.Request()
792+
goal_msg.goal_id = goal_uuid
793+
goal_future = self.mock_action_client.send_goal(goal_msg)
794+
rclpy.spin_until_future_complete(self.node, goal_future, executor)
795+
goal_handle = goal_future.result()
796+
assert goal_handle
797+
self.assertTrue(goal_handle.accepted)
798+
799+
get_result_future = self.mock_action_client.get_result(goal_uuid)
800+
self.assertFalse(get_result_future.done())
801+
802+
result = Fibonacci.Result()
803+
result.sequence.extend([1, 1, 2, 3, 5])
804+
assert stored_goal_handle
805+
stored_goal_handle.succeed(result)
806+
807+
# Handle all callbacks
808+
for _ in range(5):
809+
rclpy.spin_once(self.node, executor=self.executor, timeout_sec=0.01)
810+
if get_result_future.done():
811+
break
812+
self.assertTrue(get_result_future.done())
813+
814+
result_response = get_result_future.result()
815+
assert result_response
816+
817+
self.assertEqual(result_response.status, GoalStatus.STATUS_SUCCEEDED)
818+
self.assertEqual(result_response.result.sequence.tolist(), [1, 1, 2, 3, 5])
819+
action_server.destroy()
820+
769821
def test_action_introspection_default_status(self) -> None:
770822
goal_order = 10
771823

0 commit comments

Comments
 (0)