Action Methods

So far, we know of one way to create actions, by importing a whole profile in an action interface using the use_profile statement. This is rather static: the only parametrization is by passing arguments to the toplevel composition of the network, which may trickle down to lower levels.

This section will present a second way, which allows to actually run code to "implement" the action. It essentially runs a method on the action interface, whose job is to create a small "plan" later executed by Syskit.

This section will go through the general syntax for such actions, and will list a few use-cases for such actions.

Definition

Action methods are methods that are defined on an action interface. To "export" them as actions, one must do a declaration before the method definition using the describe statement:

describe("a simple action")
    .returns(Tasks::ASimpleAction)
def a_simple_action
    Tasks::ASimpleAction.new
end

The name of the method becomes the name of the action. The string given to describe is documentation only.

If the action requires arguments, you must both declare them and allow them to be passed to the method as a keyword argument:

describe("a simple action")
    .required_arg("arg0", "some documentation")
    .returns(Tasks::ASimpleAction)
def a_simple_action(arg0:)
    Tasks::ASimpleAction.new(arg0: arg0)
end

Optional arguments may be defined the same way, and require a default value.

describe("a simple action")
    .optional_arg("arg1", "some documentation", 42)
    .returns(Tasks::ASimpleAction)
def a_simple_action(arg1: 42)
    Tasks::ASimpleAction.new(arg1: arg1)
end

Return Value

As with all actions, action methods are represented by a single "root" task. The action's return value is this very task.

You MUST specify the type of the task that is being returned, the way we've done it in the examples above. It can be either its exact type, or a parent class. For instance, all actions could have a returns(Roby::Task), but this would lead to a very confusing system (a.k.a. don't do that).

Specifying task return types becomes even more important when creating action state machines, as the return type is used to determine which events are available for the state machine to transition on.

For historical reasons, if there is no return type specified, Syskit auto-generates a task model whose name is based on the action name (e.g. ASimpleAction for our action above). Do not rely on this behavior, which ended up being a lot more confusing than useful.

Exception Handling

The failure of an action method does not change the currently executed system. The changes are being made in a transaction that is rolled back if the action method raises an exception. In addition, the action's planning taks will fail in this case, which should allow your reporting subsystem or higher-level control to react.

Tests

You can run an action through the action instanciation codepath using the roby_run_planner statement, e.g.

task = roby_run_planner MyInterface.my_action
# Look for properties on `task`

arguments are naturally passed to the action:

task = roby_run_planner MyInterface.my_action(arg1: 10)
# Look for properties on `task`

Use Cases

Generation of complex parameters from simpler action arguments

Let's consider a drone. The drone will usually have a trajectory following definition, which allows it to move along a curve, following some parameters such as for instance speed. Let's assume that the definition that implements this trajectory follower is called trajectory_follower in a Navigation profile. This definition's toplevel composition is Compositions::TrajectoryFollowing.

Now, there are a lot of use-cases where generating a full trajectory is a rather complicated endeavour. We might want to design the system to provide higher-level behaviors such as survey, that would do a sweep of an area, or straight_line which does a line.

One way to implement these would be to create separate compositions (possibly subclasses of the same base composition) that generate a trajectory based on their parameters, and write them to the trajectory follower.

Another way is to keep the single trajectory following definition, but create one action per behavior, which generates the trajectory and returns the properly parametrized trajectory following definition:

class Navigation < Roby::Actions::Interface
    use_profile Profiles::Navigation

    describe("goes on a straight line")
        .required_arg("start_p", "starting point, as an Eigen::Vector3")
        .required_arg("end_p", "end point, as an Eigen::Vector3")
        .required_arg("speed", "speed at which to execute the trajectory")
        .returns(Compositions::TrajectoryFollowing)
    def straight_line(start_p:, end_p:, speed:)
        self.model.trajectory_follower_def(
            trajectory: straight_line_trajectory(start_p, end_p, speed)
        )
    end

    # @api private
    #
    # Helper that converts two points into a straight line trajectory
    def straight_line_trajectory(start_p, end_p, speed)
        ...
    end
end

Dynamic Dependency Injection

Thanks to action methods, it is possible to dynamically inject dependencies Until now, the only mean we have seen was to pre-define the injections in the profiles and export them as action. Actions methods will allow to make the injection dependent on e.g. an action argument.

For instance, in a 4-camera teleoperated system, we could choose a main and auxiliary camera using a ui_camera_streamer action:

class UI < Roby::Actions::Interface
    use_profile Profiles::UI

    describe("runs the UI while selecting the current camera")
        .required_arg("main_camera", "the camera to use as main")
        .returns(Compositions::CameraStreamer)
    def ui_camera_streamer(main_camera:)
        # `resolve_cameras` is a helper that verifies the parameters,
        # and returns the camera devices from Profiles::Base in the right
        # order for injection
        main_camera, left_thumb, center_thumb, right_thumb =
            resolve_cameras(main_camera)
        self.model.camera_streamer_def
            .use('main_camera' => main_camera,
                 'left_thumb' => left_thumb,
                 'center_thumb' => center_thumb,
                 'right_thumb' => right_thumb)
    end
end

And many other things

In fine, the action methods give direct access to Syskit's execution data structures. We will see how this can be leveraged for other uses in further sections of this chapter, as for instance the creation of higher level controllers or with the event scheduling.