Skip to content

Actions

Tom Tzook edited this page Nov 20, 2020 · 11 revisions

Actions are tasks executed by FlashLib's Scheduler. These tasks are more than a simple run, they are executed by specific steps, and can be considered as a complex task.

Using Actions, we can instruct the robot and its system to perform complex tasks of various types. This makes Actions very crucial for robot control.

Execution Flow

Actions are built on a specific execution flow, which is provided by the Scheduler:

Execution Flow

Once started, the task enters the initialization step, which allows users to initialize data or reset data holders in preparation for running.

Next, the task enters the execution step. During this step, users should perform the main task logic. This step continues until the task is marked to be stop.

The action can be marked to stop in several ways:

  • The Action reports that it has finished.
  • An outside user calls cancel
  • The Scheduler interrupted the Action.

If the action stopped because it reported a finish, the flow enters the end phase. Otherwise it enters the interruption phase.

Implementation

An Action implementation is a class which implements Action interface. Although it's actually recommended to extend the ActionBase instead.

When implementing an Action, users need to implement what the action should be doing at each phase of it's execution flow.

This is done by implementing the following methods:

  • void initialize(): (optional) initialization step.
  • void execute(): (required) execution step.
  • boolean isFinished(): (optional) reports when the Action is finished. Should return true when it is, false otherwise.
  • void end(boolean wasInterrupted): (optional) during the ending/interruption steps. If in interruption step, wasInterrupted would be true. If in ending step, wasInterrupted would be false.

An empty Action class, would look as such:

public class SomeAction extends ActionBase {

    @Override
    public void initialize() {

    }

    @Override
    public void execute() {

    }

    @Override
    public boolean isFinished() {
        return false;
    }

    @Override
    public void end(boolean wasInterrupted) {

    }
}

The following example RotateAction is an Action which rotates a Rotatable at a constant speed forever (until canceled or interrupted).

We would need two instance variables: the Rotatable and the wanted speed. Then we would need to implement the execution behavior.

public class RotateAction extends ActionBase {

    private final Rotatable mRotatable;
    private final double mSpeed;

    public RotateAction(Rotatable rotatable, double speed) {
        mRotatable = rotatable;
        mSpeed = speed;
    }

    @Override
    public void initialize() {
        // nothing to do here, so leave empty.
    }

    @Override
    public void execute() {
        // here we actually rotate the rotatable
        mRotatable.rotate(mSpeed);
    }

    @Override
    public boolean isFinished() {
        // we want the action to run forever (until interrupted/canceled) so we will just return false.
        return false;
    }

    @Override
    public void end(boolean wasInterrupted) {
        // when finished, it's best to just stop the rotatable
        mRotatable.stop();
    }
}

Pretty straight-forward. This is actually pretty close to how RotateAction from FlashLib is actually implemented.

Now, to start the action, all we have to do is create an instance of it and call start:

Rotatable rotatable = ...
RotateAction action = new RotateAction(rotatable, 0.5);
action.start();

To stop it, we can use cancel:

action.cancel();

Requirements

When using Actions which operate a system on the robot, we must be careful not to run more than one Action at any given time. Doing so, might cause the system to go haywire, because multiple actions are commending it to do conflicting operations.

To avoid such occurrence, we must define each Action its Requirements: a series of systems it uses during execution. Using this definition, the Scheduler will make sure only one Action uses a system at any given time.

Using Action.requires, we may define the used systems:

Subsystem system1 = ...
Subsystem system2 = ...

Action action = ...
action.requires(system);

Action action2 = ...
action2.requires(system, system2);

Assuming action is running, if we start action2 now, a conflict of requirements will occur (both require system). The Scheduler will solve this by interrupting action and letting action2, the new action, to start.

Taking a look at the RotateAction from before, we now need to update it's usage:

Rotatable rotatable = ...
RotateAction action = new RotateAction(rotatable, 0.5)
            .requires(rotatable);

Note the usage of a sugar syntax when using requires.

However, it is usually preferable to define the requirements in the constructor of the action implementations. Let's modify RotateAction's constructor for example:

    public RotateAction(Rotatable rotatable, double speed) {
        mRotatable = rotatable;
        mSpeed = speed;
 
        requires(rotatable);
    }

So now, using the action is as simple as:

Rotatable rotatable = ...
RotateAction action = new RotateAction(rotatable, 0.5);
action.start();

This simplifies the work for users of the action. All action implementations in FlashLib guarantee such behavior.

Timeout

Actions can be set with a timeout, which defines the maximum run time of the action:

Action action = ...
action.withTimeout(Time.seconds(5));
action.start();

In this example, if the action doesn't finish in 5 seconds, the Scheduler will interrupt it.

Action Group

Sometimes, especially for complex tasks, it is necessary to perform several actions in a specific order. To do so, we will use ActionGroups. These groups allow us to organize execution order of several actions, and run them.

For example, to run actions in sequence:

Action action1 = ...;
Action action2 = ...;
Action group = Actions.sequential(action1, action2);

// using a sugar syntax:
Action group2 = new SomeAction()
          .andThen(new SomeOtherAction());

Or running in parallel

Action action1 = ...;
Action action2 = ...;
Action group = Actions.parallel(action1, action2);

// using a sugar syntax
Action group2 = new SomeAction()
          .alongWith(new SomeOtherAction());

It is also possible to combine them:

// using a sugar syntax
Action group2 = new SomeAction()
          .alongWith(new SomeOtherAction());
          .andThen(new LastAction());

This will cause SomeAction and SomeOtherAction to be executed in parallel, and when both are finished LastAction will start.

Requirements

When using ActionGroups, we must pay good attention to what requirements the action have. By default, action groups will copy the requirements of all the actions they contain, so there shouldn't be any problem as long as all the actions used, define the appropriate requirements. If not, simply add requires call like with any other action:

// using a sugar syntax
Action group2 = new SomeAction()
          .alongWith(new SomeOtherAction());
          .requires(system1, system2);

With parallel groups, be careful not to use several actions which have a joint requirement.

Specialized Actions

Some specialized action types are available, for better convenience of usage:

  • Actions.wait(Time time): a wait action. Does nothing, for a given amount of time. Useful in ActionGroups, when waiting is needed.
Action group = new DoSomething()
         .andThen(Actions.wait(Time.seconds(5)))
         .andThen(new DoAnotherThing());
  • Actions.instant(Runnable runnable): runs the given Runnable once, and stops. Useful for execution actions which don't need the repetition of execute.
Action action = Actions.instant(()-> System.out.println("hello world"));
  • Actions.fromRunnable(Runnable runnable): runs the given Runnable continuously in the execution phase.
Action action = Actions.fromRunnable(()-> System.out.println("endless hello world"));
  • Actions.periodic(Runnable runnable, Time period): runs the given Runnable once every period of time.
Action action = Actions.periodic(()-> System.out.println("periodic hello world"), Time.seconds(1));