-
Notifications
You must be signed in to change notification settings - Fork 0
Actions
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 Action
s, we can instruct the robot and its system to perform complex tasks of various types. This makes Action
s very crucial for robot control.
Actions are built on a specific execution flow, which is provided by the Scheduler
:
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 theAction
.
If the action stopped because it reported a finish, the flow enters the end phase. Otherwise it enters the interruption phase.
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 theAction
is finished. Should returntrue
when it is,false
otherwise. -
void end(boolean wasInterrupted)
: (optional) during the ending/interruption steps. If in interruption step,wasInterrupted
would betrue
. If in ending step,wasInterrupted
would befalse
.
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();
When using Action
s 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 Requirement
s: 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.
Action
s 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.
Sometimes, especially for complex tasks, it is necessary to perform several actions in a specific order.
To do so, we will use ActionGroup
s. 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.
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.
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 inActionGroup
s, when waiting is needed.
Action group = new DoSomething()
.andThen(Actions.wait(Time.seconds(5)))
.andThen(new DoAnotherThing());
-
Actions.instant(Runnable runnable)
: runs the givenRunnable
once, and stops. Useful for execution actions which don't need the repetition ofexecute
.
Action action = Actions.instant(()-> System.out.println("hello world"));
-
Actions.fromRunnable(Runnable runnable)
: runs the givenRunnable
continuously in the execution phase.
Action action = Actions.fromRunnable(()-> System.out.println("endless hello world"));
-
Actions.periodic(Runnable runnable, Time period)
: runs the givenRunnable
once every period of time.
Action action = Actions.periodic(()-> System.out.println("periodic hello world"), Time.seconds(1));