Action Clients#

Warning

This topic is under construction and this might not even be its final form. Please feel free to open an issue if you spot any typos or other problems.

Added in version Jazzy: Added this section.

An action client will be much like a service client, but more complicated. There are at least three callbacks and two futures involved.

The reason for that is that processing the Action requires multiple steps.

To simplify the action server, our example code will only call the Action once and do nothing else.

Remember that when deploying actions in real applications they will be part of a more complex Node that might include publishers, subscribers, service servers/clients, and other actions server/clients. This means that it is important to take this complexity in consideration when designing your packages to make sure that an Action is the best way to communicate.

Diagram#

This is the sequence diagram from the point of view of the action client. Note that because we are using async calls for the goal and the result, the node is free to do other tasks while those do not arrive.

Files#

The highlighted file below will be modified or created in this section.

File structure

python_package_that_uses_the_actions/
|-- package.xml
|-- python_package_that_uses_the_actions
|   |-- __init__.py
|   |-- move_straight_in_2d_action_client_node.py
|   `-- move_straight_in_2d_action_server_node.py
|-- resource
|   `-- python_package_that_uses_the_actions
|-- setup.cfg
|-- setup.py
`-- test
    |-- test_copyright.py
    |-- test_flake8.py
    `-- test_pep257.py

Action Client#

  1. Create the Node with an ActionClient.

  2. Update the setup.py so that ros2 run finds the node (if needed).

move_straight_in_2d_action_client_node.py

 1import rclpy
 2from rclpy.action import ActionClient
 3from rclpy.action.client import ClientGoalHandle
 4from rclpy.node import Node
 5from rclpy.task import Future
 6
 7from geometry_msgs.msg import Point
 8from package_with_interfaces.action import MoveStraightIn2D
 9
10class MoveStraightIn2DActionClientNode(Node):
11    """A ROS2 Node with an Action Client for MoveStraightIn2D."""
12
13    def __init__(self):
14        super().__init__('move_straight_in_2d_action_client')
15
16        self.action_client = ActionClient(self, MoveStraightIn2D, '/move_straight_in_2d')
17
18        self.send_goal_future = None # This will be used in `send_goal`
19        self.get_result_future = None # This will be used in 'goal_response_callback'
20
21    def send_goal_async(self, desired_position: Point) -> None:
22        goal_msg = MoveStraightIn2D.Goal()
23        goal_msg.desired_position = desired_position
24
25        while not self.action_client.wait_for_server(timeout_sec=1.0):
26            self.get_logger().info(f'action {self.action_client} not available, waiting...')
27
28        self.get_logger().info(f'Sending goal: {desired_position}.')
29
30        self.send_goal_future = self.action_client.send_goal_async(goal_msg, feedback_callback=self.action_feedback_callback)
31        self.send_goal_future.add_done_callback(self.goal_response_callback)
32
33    def goal_response_callback(self, future: Future) -> None:
34        goal: ClientGoalHandle = future.result()
35
36        if not goal.accepted:
37            self.get_logger().info('Goal was rejected by the server.')
38            return
39        self.get_logger().info('Goal was accepted by the server.')
40
41        self.get_result_future = goal.get_result_async()
42        self.get_result_future.add_done_callback(self.action_result_callback)
43
44    def action_result_callback(self, future: Future) -> None:
45        response: MoveStraightIn2D.Response = future.result()
46        self.get_logger().info(f'Final position was: {response.result.final_position}.')
47
48    def action_feedback_callback(self, feedback_msg: MoveStraightIn2D.Feedback) -> None:
49        feedback = feedback_msg.feedback
50        self.get_logger().info(f'Received feedback distance: {feedback.distance}.')
51
52
53def main(args=None):
54    """
55    The main function.
56    :param args: Not used directly by the user, but used by ROS2 to configure certain aspects of the Node.
57    """
58    try:
59        rclpy.init(args=args)
60
61        node = MoveStraightIn2DActionClientNode()
62
63        # Send the goal once and then do nothing until the user shuts this node down.
64        desired_position = Point()
65        desired_position.x = 1.0
66        desired_position.y = -1.0
67        node.send_goal_async(desired_position)
68
69        rclpy.spin(node)
70    except KeyboardInterrupt:
71        pass
72    except Exception as e:
73        print(e)
74
75
76if __name__ == '__main__':
77    main()

As always, our class must inherit from rclpy.node.Node, so that it can be used as argument of rclpy.spin().

An action client is an instance of rclpy.action.ActionClient. As input to the initializer of ActionClient, we need an action and a string that names the action server this client will interact with.

In the action client, we will have to manage two instances of Future. One for the goal and another for the result. It is important to keep them as attributes of the instance to guarantee they will not get out of scope before the result is obtained. This is done as follows.

    def __init__(self):
        super().__init__('move_straight_in_2d_action_client')

        self.action_client = ActionClient(self, MoveStraightIn2D, '/move_straight_in_2d')

        self.send_goal_future = None # This will be used in `send_goal`
        self.get_result_future = None # This will be used in 'goal_response_callback'

The following method can be used to send the action goal and trigger the entire process. Notably, suppose that a suitable action has already been instantiated and is sent as argument to this method. We wait for the action server to be available. Then, in something that should be familiar after working with services, we send the request asynchronously.

In the request, send_goal_async, we define a callback for the feedback. This is similar to a callback for subscribers. Whenever a feedback is sent, the method we assign as callback will be called.

After we send the goal request and assign a callback for the feedback, we assign another callback. This second callback is for the result of the goal request operation.

    def send_goal_async(self, desired_position: Point) -> None:
        goal_msg = MoveStraightIn2D.Goal()
        goal_msg.desired_position = desired_position

        while not self.action_client.wait_for_server(timeout_sec=1.0):
            self.get_logger().info(f'action {self.action_client} not available, waiting...')

        self.get_logger().info(f'Sending goal: {desired_position}.')

        self.send_goal_future = self.action_client.send_goal_async(goal_msg, feedback_callback=self.action_feedback_callback)
        self.send_goal_future.add_done_callback(self.goal_response_callback)

After the goal is processed, the goal-related callback will be automatically called for us given that it was added to the respective future. The goal can be rejected, therefore we address that case by doing nothing. If the goal is accepted, we are ready to ask for a result.

We ask for a result asynchronously. We then assign a third callback for when a response is sent.

    def goal_response_callback(self, future: Future) -> None:
        goal: ClientGoalHandle = future.result()

        if not goal.accepted:
            self.get_logger().info('Goal was rejected by the server.')
            return
        self.get_logger().info('Goal was accepted by the server.')

        self.get_result_future = goal.get_result_async()
        self.get_result_future.add_done_callback(self.action_result_callback)

The result callback will receive a future and process the result of the action somehow. In this toy example, we simply print the results to the screen.

    def action_result_callback(self, future: Future) -> None:
        response: MoveStraightIn2D.Response = future.result()
        self.get_logger().info(f'Final position was: {response.result.final_position}.')

Lastly, we see that the callback for the feedback is, again, very similar to that for a subscriber.

    def action_feedback_callback(self, feedback_msg: MoveStraightIn2D.Feedback) -> None:
        feedback = feedback_msg.feedback
        self.get_logger().info(f'Received feedback distance: {feedback.distance}.')

To simplify this example slightly, please notice that the action is instantiated in the main function, and the action is triggered only once.

When doing so, please remember to call the action after the node is instantiated and before rclpy.spin(). Otherwise, the object will not yet exist, or you will be locked in the spinner before sending the goal.

        node = MoveStraightIn2DActionClientNode()

        # Send the goal once and then do nothing until the user shuts this node down.
        desired_position = Point()
        desired_position.x = 1.0
        desired_position.y = -1.0
        node.send_goal_async(desired_position)

        rclpy.spin(node)

Build and source#

Before we proceed, let us build and source once.

cd ~/ros2_tutorial_workspace
colcon build
source install/setup.bash

Note

For additional explanation and troubleshooting tips, see Always source after you build.

Warning

colcon will not work properly if your terminal has an active venv.

Testing the Action Client#

We will be working with two terminal windows. The first will run the action server. The second, the client.

ros2 run python_package_that_uses_the_actions move_straight_in_2d_action_server_node
ros2 run python_package_that_uses_the_actions move_straight_in_2d_action_client_node

The output in each window should be something similar to

The action server will make the point move towards the goal. If it does not manage to reach the desired threshold within the allocated time, it will show, in this case, a warning that it was implicitly aborted.

[INFO] [1761753120.100902419] [move_straight_in_2d_action_server]: current_position is geometry_msgs.msg.Point(x=0.0, y=0.0, z=0.0).
[INFO] [1761753120.101090669] [move_straight_in_2d_action_server]: desired_position set to geometry_msgs.msg.Point(x=1.0, y=-1.0, z=0.0).
[WARN] [1761753121.235096837] [move_straight_in_2d_action_server.action_server]: Goal state not set, assuming aborted. Goal ID: [  9  49  65  27 156  53  65  61 134 216 224 156 147 240 198  62]

The action client will show the feedback as it happens and the final state of the action when the action server stops processing this goal.

[INFO] [1761753120.092259753] [move_straight_in_2d_action_client]: Sending goal: geometry_msgs.msg.Point(x=1.0, y=-1.0, z=0.0).
[INFO] [1761753120.093185086] [move_straight_in_2d_action_client]: Goal was accepted by the server.
[INFO] [1761753120.101412211] [move_straight_in_2d_action_client]: Received feedback distance: 1.4142135381698608.
[INFO] [1761753120.112173919] [move_straight_in_2d_action_client]: Received feedback distance: 1.404213547706604.
[INFO] [1761753120.123090294] [move_straight_in_2d_action_client]: Received feedback distance: 1.3942135572433472.
[INFO] [1761753120.134245086] [move_straight_in_2d_action_client]: Received feedback distance: 1.3842135667800903.
[INFO] [1761753120.145173920] [move_straight_in_2d_action_client]: Received feedback distance: 1.3742135763168335.
[INFO] [1761753120.156160961] [move_straight_in_2d_action_client]: Received feedback distance: 1.3642135858535767.
[INFO] [1761753120.167298045] [move_straight_in_2d_action_client]: Received feedback distance: 1.3542135953903198.
[INFO] [1761753120.178755836] [move_straight_in_2d_action_client]: Received feedback distance: 1.344213604927063.
[INFO] [1761753120.189400378] [move_straight_in_2d_action_client]: Received feedback distance: 1.3342136144638062.
[INFO] [1761753120.200484253] [move_straight_in_2d_action_client]: Received feedback distance: 1.3242135047912598.
[INFO] [1761753120.211245128] [move_straight_in_2d_action_client]: Received feedback distance: 1.314213514328003.
[INFO] [1761753120.222122295] [move_straight_in_2d_action_client]: Received feedback distance: 1.304213523864746.
[INFO] [1761753120.234860420] [move_straight_in_2d_action_client]: Received feedback distance: 1.2942135334014893.
[INFO] [1761753120.245268795] [move_straight_in_2d_action_client]: Received feedback distance: 1.2842135429382324.
[INFO] [1761753120.257182295] [move_straight_in_2d_action_client]: Received feedback distance: 1.2742135524749756.
[INFO] [1761753120.268218170] [move_straight_in_2d_action_client]: Received feedback distance: 1.2642135620117188.
[INFO] [1761753120.279651420] [move_straight_in_2d_action_client]: Received feedback distance: 1.254213571548462.
[INFO] [1761753120.290934753] [move_straight_in_2d_action_client]: Received feedback distance: 1.244213581085205.
[INFO] [1761753120.302183086] [move_straight_in_2d_action_client]: Received feedback distance: 1.2342135906219482.
[INFO] [1761753120.313913295] [move_straight_in_2d_action_client]: Received feedback distance: 1.2242136001586914.
[INFO] [1761753120.325966795] [move_straight_in_2d_action_client]: Received feedback distance: 1.2142136096954346.
[INFO] [1761753120.336757795] [move_straight_in_2d_action_client]: Received feedback distance: 1.2042136192321777.
[INFO] [1761753120.347904086] [move_straight_in_2d_action_client]: Received feedback distance: 1.1942135095596313.
[INFO] [1761753120.359854920] [move_straight_in_2d_action_client]: Received feedback distance: 1.1842135190963745.
[INFO] [1761753120.371660170] [move_straight_in_2d_action_client]: Received feedback distance: 1.1742135286331177.
[INFO] [1761753120.382972545] [move_straight_in_2d_action_client]: Received feedback distance: 1.1642135381698608.
[INFO] [1761753120.396173336] [move_straight_in_2d_action_client]: Received feedback distance: 1.154213547706604.
[INFO] [1761753120.406693836] [move_straight_in_2d_action_client]: Received feedback distance: 1.1442135572433472.
[INFO] [1761753120.418283586] [move_straight_in_2d_action_client]: Received feedback distance: 1.1342135667800903.
[INFO] [1761753120.430012878] [move_straight_in_2d_action_client]: Received feedback distance: 1.1242135763168335.
[INFO] [1761753120.440861920] [move_straight_in_2d_action_client]: Received feedback distance: 1.1142135858535767.
[INFO] [1761753120.452906586] [move_straight_in_2d_action_client]: Received feedback distance: 1.1042135953903198.
[INFO] [1761753120.464116961] [move_straight_in_2d_action_client]: Received feedback distance: 1.094213604927063.
[INFO] [1761753120.475175961] [move_straight_in_2d_action_client]: Received feedback distance: 1.0842136144638062.
[INFO] [1761753120.486437628] [move_straight_in_2d_action_client]: Received feedback distance: 1.0742135047912598.
[INFO] [1761753120.497304670] [move_straight_in_2d_action_client]: Received feedback distance: 1.064213514328003.
[INFO] [1761753120.508036878] [move_straight_in_2d_action_client]: Received feedback distance: 1.054213523864746.
[INFO] [1761753120.519005753] [move_straight_in_2d_action_client]: Received feedback distance: 1.0442135334014893.
[INFO] [1761753120.529962128] [move_straight_in_2d_action_client]: Received feedback distance: 1.0342135429382324.
[INFO] [1761753120.540856920] [move_straight_in_2d_action_client]: Received feedback distance: 1.0242135524749756.
[INFO] [1761753120.551795461] [move_straight_in_2d_action_client]: Received feedback distance: 1.0142135620117188.
[INFO] [1761753120.563487586] [move_straight_in_2d_action_client]: Received feedback distance: 1.004213571548462.
[INFO] [1761753120.575029170] [move_straight_in_2d_action_client]: Received feedback distance: 0.9942135810852051.
[INFO] [1761753120.585385753] [move_straight_in_2d_action_client]: Received feedback distance: 0.9842135906219482.
[INFO] [1761753120.596445128] [move_straight_in_2d_action_client]: Received feedback distance: 0.9742135405540466.
[INFO] [1761753120.607320045] [move_straight_in_2d_action_client]: Received feedback distance: 0.9642135500907898.
[INFO] [1761753120.617953295] [move_straight_in_2d_action_client]: Received feedback distance: 0.954213559627533.
[INFO] [1761753120.628741545] [move_straight_in_2d_action_client]: Received feedback distance: 0.9442135691642761.
[INFO] [1761753120.640129795] [move_straight_in_2d_action_client]: Received feedback distance: 0.9342135787010193.
[INFO] [1761753120.651860920] [move_straight_in_2d_action_client]: Received feedback distance: 0.9242135882377625.
[INFO] [1761753120.664198961] [move_straight_in_2d_action_client]: Received feedback distance: 0.9142135381698608.
[INFO] [1761753120.674831961] [move_straight_in_2d_action_client]: Received feedback distance: 0.904213547706604.
[INFO] [1761753120.686480336] [move_straight_in_2d_action_client]: Received feedback distance: 0.8942135572433472.
[INFO] [1761753120.696874711] [move_straight_in_2d_action_client]: Received feedback distance: 0.8842135667800903.
[INFO] [1761753120.708105420] [move_straight_in_2d_action_client]: Received feedback distance: 0.8742135763168335.
[INFO] [1761753120.720106128] [move_straight_in_2d_action_client]: Received feedback distance: 0.8642135858535767.
[INFO] [1761753120.731688795] [move_straight_in_2d_action_client]: Received feedback distance: 0.854213535785675.
[INFO] [1761753120.744141753] [move_straight_in_2d_action_client]: Received feedback distance: 0.8442135453224182.
[INFO] [1761753120.755073711] [move_straight_in_2d_action_client]: Received feedback distance: 0.8342135548591614.
[INFO] [1761753120.767188003] [move_straight_in_2d_action_client]: Received feedback distance: 0.8242135643959045.
[INFO] [1761753120.778392795] [move_straight_in_2d_action_client]: Received feedback distance: 0.8142135739326477.
[INFO] [1761753120.789267920] [move_straight_in_2d_action_client]: Received feedback distance: 0.8042135834693909.
[INFO] [1761753120.800152128] [move_straight_in_2d_action_client]: Received feedback distance: 0.7942135334014893.
[INFO] [1761753120.811231461] [move_straight_in_2d_action_client]: Received feedback distance: 0.7842135429382324.
[INFO] [1761753120.822597753] [move_straight_in_2d_action_client]: Received feedback distance: 0.7742135524749756.
[INFO] [1761753120.833374087] [move_straight_in_2d_action_client]: Received feedback distance: 0.7642135620117188.
[INFO] [1761753120.845155503] [move_straight_in_2d_action_client]: Received feedback distance: 0.7542135715484619.
[INFO] [1761753120.857314462] [move_straight_in_2d_action_client]: Received feedback distance: 0.7442135810852051.
[INFO] [1761753120.868129378] [move_straight_in_2d_action_client]: Received feedback distance: 0.7342135906219482.
[INFO] [1761753120.879274587] [move_straight_in_2d_action_client]: Received feedback distance: 0.7242135405540466.
[INFO] [1761753120.890035378] [move_straight_in_2d_action_client]: Received feedback distance: 0.7142135500907898.
[INFO] [1761753120.900985587] [move_straight_in_2d_action_client]: Received feedback distance: 0.704213559627533.
[INFO] [1761753120.911983378] [move_straight_in_2d_action_client]: Received feedback distance: 0.6942135691642761.
[INFO] [1761753120.923409587] [move_straight_in_2d_action_client]: Received feedback distance: 0.6842135787010193.
[INFO] [1761753120.934035170] [move_straight_in_2d_action_client]: Received feedback distance: 0.6742135882377625.
[INFO] [1761753120.945181837] [move_straight_in_2d_action_client]: Received feedback distance: 0.6642135381698608.
[INFO] [1761753120.957020503] [move_straight_in_2d_action_client]: Received feedback distance: 0.654213547706604.
[INFO] [1761753120.968711462] [move_straight_in_2d_action_client]: Received feedback distance: 0.6442135572433472.
[INFO] [1761753120.980621087] [move_straight_in_2d_action_client]: Received feedback distance: 0.6342135667800903.
[INFO] [1761753120.991584587] [move_straight_in_2d_action_client]: Received feedback distance: 0.6242135763168335.
[INFO] [1761753121.003069045] [move_straight_in_2d_action_client]: Received feedback distance: 0.6142135858535767.
[INFO] [1761753121.015031753] [move_straight_in_2d_action_client]: Received feedback distance: 0.604213535785675.
[INFO] [1761753121.027384962] [move_straight_in_2d_action_client]: Received feedback distance: 0.5942135453224182.
[INFO] [1761753121.037966962] [move_straight_in_2d_action_client]: Received feedback distance: 0.5842135548591614.
[INFO] [1761753121.048998128] [move_straight_in_2d_action_client]: Received feedback distance: 0.5742135643959045.
[INFO] [1761753121.061886295] [move_straight_in_2d_action_client]: Received feedback distance: 0.5642135739326477.
[INFO] [1761753121.072371337] [move_straight_in_2d_action_client]: Received feedback distance: 0.5542135834693909.
[INFO] [1761753121.084468795] [move_straight_in_2d_action_client]: Received feedback distance: 0.5442135334014893.
[INFO] [1761753121.096903712] [move_straight_in_2d_action_client]: Received feedback distance: 0.5342135429382324.
[INFO] [1761753121.107056378] [move_straight_in_2d_action_client]: Received feedback distance: 0.5242135524749756.
[INFO] [1761753121.118114628] [move_straight_in_2d_action_client]: Received feedback distance: 0.5142135620117188.
[INFO] [1761753121.128596212] [move_straight_in_2d_action_client]: Received feedback distance: 0.5042135715484619.
[INFO] [1761753121.140370212] [move_straight_in_2d_action_client]: Received feedback distance: 0.4942135512828827.
[INFO] [1761753121.152598753] [move_straight_in_2d_action_client]: Received feedback distance: 0.48421356081962585.
[INFO] [1761753121.165302628] [move_straight_in_2d_action_client]: Received feedback distance: 0.474213570356369.
[INFO] [1761753121.178417045] [move_straight_in_2d_action_client]: Received feedback distance: 0.4642135500907898.
[INFO] [1761753121.189332878] [move_straight_in_2d_action_client]: Received feedback distance: 0.45421355962753296.
[INFO] [1761753121.199914878] [move_straight_in_2d_action_client]: Received feedback distance: 0.4442135691642761.
[INFO] [1761753121.211306087] [move_straight_in_2d_action_client]: Received feedback distance: 0.4342135488986969.
[INFO] [1761753121.223468503] [move_straight_in_2d_action_client]: Received feedback distance: 0.42421355843544006.
[INFO] [1761753121.237240878] [move_straight_in_2d_action_client]: Final position was: geometry_msgs.msg.Point(x=0.7071067811865476, y=-0.7071067811865476, z=0.0).