From 984a6a25912fab19cdc5bb3b7ca3294115103344 Mon Sep 17 00:00:00 2001 From: Louis Fink <uutyc@student.kit.edu> Date: Fri, 14 Mar 2025 16:56:12 +0100 Subject: [PATCH] fix: add turn action cancel logic --- .../controller/hardware_controller.py | 1 + .../jetracerros2/jetracer_controller.py | 31 +++++++++-- .../jetracerros2/turn_controller.py | 55 ++++++++++++++----- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/uppaal2jetracer/controller/hardware_controller.py b/uppaal2jetracer/controller/hardware_controller.py index 5b12ba1..856a9ac 100644 --- a/uppaal2jetracer/controller/hardware_controller.py +++ b/uppaal2jetracer/controller/hardware_controller.py @@ -122,6 +122,7 @@ class JRHardwareController(HardwareController): """ self.set_speed(0.0) + self._turn_controller.stop() def turn(self, theta: float) -> HardwareResult[None]: """ diff --git a/uppaal2jetracer/jetracerros2/jetracerros2/jetracer_controller.py b/uppaal2jetracer/jetracerros2/jetracerros2/jetracer_controller.py index 234bd99..97db157 100644 --- a/uppaal2jetracer/jetracerros2/jetracerros2/jetracer_controller.py +++ b/uppaal2jetracer/jetracerros2/jetracerros2/jetracer_controller.py @@ -15,7 +15,7 @@ import rclpy from rclpy.node import Node from rclpy.publisher import Publisher from rclpy.service import Service -from rclpy.action import ActionServer +from rclpy.action import ActionServer, CancelResponse, GoalResponse from rclpy.executors import MultiThreadedExecutor from geometry_msgs.msg import Twist @@ -52,7 +52,10 @@ class JetRacerController(Node): Speed, "set_speed", self._speed_service_callback) self._turn_action_server: ActionServer = ActionServer( - self, Turn, "turn", self._execute_turn_callback) + self, Turn, "turn", + execute_callback = self._execute_turn_callback, + goal_callback = self._goal_turn_callback, + cancel_callback = self._cancel_turn_callback) def _speed_service_callback(self, request, response): self._speed = request.speed @@ -68,15 +71,14 @@ class JetRacerController(Node): goal_handle.succeed() return Turn.Result() - self._execute_turn(request.theta) + self._execute_turn(request.theta, goal_handle) result = Turn.Result() result.turned = request.theta - goal_handle.succeed() return result - def _execute_turn(self, theta: float): + def _execute_turn(self, theta: float, goal_handle): old_speed = self._speed if self._speed == 0: @@ -95,7 +97,15 @@ class JetRacerController(Node): self._publish_to_cmd_vel() - time.sleep(turn_time) + start_time = time.time() + + while time.time() - start_time < turn_time: + if goal_handle.is_cancel_requested: + self._speed = 0.0 + self._steering_angle = 0.0 + self._publish_to_cmd_vel() + goal_handle.canceled() + return self._steering_angle = 0.0 if self._speed != 0.0: @@ -103,6 +113,15 @@ class JetRacerController(Node): self._publish_to_cmd_vel() + goal_handle.succeed() + + + def _cancel_turn_callback(self, goal_handle): + return CancelResponse.ACCEPT + + def _goal_turn_callback(self, goal_handle): + return GoalResponse.ACCEPT + def _publish_timer_callback(self): self._publish_to_cmd_vel() diff --git a/uppaal2jetracer/jetracerros2/jetracerros2/turn_controller.py b/uppaal2jetracer/jetracerros2/jetracerros2/turn_controller.py index 6b0ccff..8ebc4e5 100644 --- a/uppaal2jetracer/jetracerros2/jetracerros2/turn_controller.py +++ b/uppaal2jetracer/jetracerros2/jetracerros2/turn_controller.py @@ -35,13 +35,15 @@ class TurnActionClientAsync(Node): :vartype _status: GoalStatus """ - __slots__ = ["_turn_action_client_async", "_result", "_status"] + __slots__ = ["_turn_action_client_async", "_result", "_status", + "_goal_handle"] def __init__(self): super().__init__("turn_action_client", namespace = "/jetracerros2") self._turn_action_client = ActionClient(self, Turn, "turn") self._result = Turn.Result() self._status = GoalStatus.STATUS_UNKNOWN + self._goal_handle = None @property def result(self) -> Turn.Result: @@ -72,9 +74,7 @@ class TurnActionClientAsync(Node): This method should only be called after the turn action has finished! """ - if self._status == GoalStatus.STATUS_SUCCEEDED: - self._status = GoalStatus.STATUS_UNKNOWN - logger.debug("Cleanup complete.") + self.status = GoalStatus.STATUS_UNKNOWN def send_turn_goal_async(self, theta: float): """ @@ -92,9 +92,29 @@ class TurnActionClientAsync(Node): self._turn_action_client.wait_for_server() logger.debug("Sending %f turn request async!", theta) send_goal_future = self._turn_action_client.send_goal_async(goal) - + self._status = GoalStatus.STATUS_UNKNOWN send_goal_future.add_done_callback(self._goal_response_callback) + def cancel_turn_goal_async(self): + """ + Cancel the turn goal. + """ + if not self._goal_handle: + logger.warning("No goal handle to cancel!") + return + + cancel_future = self._goal_handle.cancel_goal_async() + cancel_future.add_done_callback(self._cancel_response_callback) + + def is_turn_goal_active(self) -> bool: + """ + Check if the turn goal is active. + + :return: True if the turn goal is active, False otherwise. + """ + return (self._status != GoalStatus.STATUS_SUCCEDDED + and self._status != GoalStatus.STATUS_ABORTED) + def _goal_response_callback(self, future): goal_handle = future.result() if not goal_handle.accepted: @@ -103,6 +123,7 @@ class TurnActionClientAsync(Node): self._status = GoalStatus.STATUS_ACCEPTED logger.debug("Turn request accepted!") + self._goal_handle = goal_handle self._get_result_future = goal_handle.get_result_async() self._get_result_future.add_done_callback(self._get_result_callback) @@ -111,19 +132,31 @@ class TurnActionClientAsync(Node): self._status = GoalStatus.STATUS_SUCCEEDED logger.debug("Turn request marked as succeeded!") + def _cancel_response_callback(self, future): + if future.result(): + logger.debug("Turn request canceled!") + self._status = GoalStatus.STATUS_ABORTED + class TurnController: """ This class is used to send a goal to the turn action server and wait for the result. """ - __slots__ = ["_turn_action_client"] + __slots__ = ["_turn_action_client",] def __init__(self): if not rclpy.ok(): rclpy.init() self._turn_action_client = TurnActionClientAsync() + def stop(self): + """ + Stop the JetRacer. + """ + self._turn_action_client.cancel_turn_goal_async() + self._turn_action_client.cleanup() + def shutdown(self): """ Shutdown the TurnController and destroy the node. @@ -144,12 +177,8 @@ class TurnController: self._turn_action_client.send_turn_goal_async(theta) - try: - while self._turn_action_client.status != GoalStatus.STATUS_SUCCEEDED and rclpy.ok(): - logger.debug("Waiting for Turn action request %f to succeed.", theta) - rclpy.spin_once(self._turn_action_client) - except KeyboardInterrupt: - logger.warning("Turn action request %f was interrupted!", theta) - raise + while (self._turn_action_client.is_turn_goal_active() and rclpy.ok()): + logger.debug("Waiting for Turn action request %f to succeed.", theta) + rclpy.spin_once(self._turn_action_client) self._turn_action_client.cleanup() -- GitLab