Event-Based Programming With EventLoop
Many operations in robot code are driven by certain conditions; buttons are one common example. Conditions can be polled with an imperative programming style by using an if
statement in a periodic method. As an alternative, WPILib offers an event-driven programming style of API in the shape of the EventLoop
and BooleanEvent
classes.
EventLoop
The EventLoop
class is a « container » for pairs of conditions and actions, which can be polled using the poll()
/Poll()
method. When polled, every condition will be queried and if it returns true
the action associated with the condition will be executed.
private final EventLoop m_loop = new EventLoop();
private final Joystick m_joystick = new Joystick(0);
@Override
public void robotPeriodic() {
// poll all the bindings
m_loop.poll();
}
frc::EventLoop m_loop{};
void RobotPeriodic() override { m_loop.Poll(); }
Avertissement
The EventLoop
’s poll()
method should be called consistently in a *Periodic()
method. Failure to do this will result in unintended loop behavior.
BooleanEvent
The BooleanEvent
class represents a boolean condition: a BooleanSupplier
(Java) / std::function<bool()>
(C++).
To bind a callback action to the condition, use ifHigh()
/IfHigh()
:
BooleanEvent atTargetVelocity =
new BooleanEvent(m_loop, m_controller::atSetpoint)
// debounce for more stability
.debounce(0.2);
// if we're at the target velocity, kick the ball into the shooter wheel
atTargetVelocity.ifHigh(() -> m_kicker.set(0.7));
frc::BooleanEvent atTargetVelocity =
frc::BooleanEvent(
&m_loop,
[&controller = m_controller] { return controller.AtSetpoint(); })
// debounce for more stability
.Debounce(0.2_s);
// if we're at the target velocity, kick the ball into the shooter wheel
atTargetVelocity.IfHigh([&kicker = m_kicker] { kicker.Set(0.7); });
N’oubliez pas que la liaison des boutons est déclarative: les liaisons ne doivent être déclarées qu’une seule fois, idéalement pendant l’initialisation du robot. La librarie gère tout le reste.
Composing Conditions
BooleanEvent
objects can be composed to create composite conditions. In C++ this is done using operators when applicable, other cases and all compositions in Java are done using methods.
and() / &&
The and()
/&&
composes two BooleanEvent
conditions into a third condition that returns true
only when both of the conditions return true
.
// if the thumb button is held
intakeButton
// and there is not a ball at the kicker
.and(isBallAtKicker.negate())
// activate the intake
.ifHigh(() -> m_intake.set(0.5));
// if the thumb button is held
(intakeButton
// and there is not a ball at the kicker
&& !isBallAtKicker)
// activate the intake
.IfHigh([&intake = m_intake] { intake.Set(0.5); });
or() / ||
The or()
/||
composes two BooleanEvent
conditions into a third condition that returns true
only when either of the conditions return true
.
// if the thumb button is not held
intakeButton
.negate()
// or there is a ball in the kicker
.or(isBallAtKicker)
// stop the intake
.ifHigh(m_intake::stopMotor);
// if the thumb button is not held
(!intakeButton
// or there is a ball in the kicker
|| isBallAtKicker)
// stop the intake
.IfHigh([&intake = m_intake] { intake.Set(0.0); });
negate() / !
The negate()
/!
composes one BooleanEvent
condition into another condition that returns the opposite of what the original conditional did.
// and there is not a ball at the kicker
.and(isBallAtKicker.negate())
// and there is not a ball at the kicker
&& !isBallAtKicker)
debounce() / Debounce()
To avoid rapid repeated activation, conditions (especially those originating from digital inputs) can be debounced with the WPILib Debouncer class using the debounce method:
BooleanEvent atTargetVelocity =
new BooleanEvent(m_loop, m_controller::atSetpoint)
// debounce for more stability
.debounce(0.2);
frc::BooleanEvent atTargetVelocity =
frc::BooleanEvent(
&m_loop,
[&controller = m_controller] { return controller.AtSetpoint(); })
// debounce for more stability
.Debounce(0.2_s);
rising(), falling()
Often times it is desired to bind an action not to the current state of a condition, but instead to when that state changes. For example, binding an action to when a button is newly pressed as opposed to when it is held. This is what the rising()
and falling()
decorators do: rising()
will return a condition that is true
only when the original condition returned true
in the current polling and false
in the previous polling; falling()
returns a condition that returns true
only on a transition from true
to false
.
Avertissement
Due to the « memory » these conditions have, do not use the same instance in multiple places.
// when we stop being at the target velocity, it means the ball was shot
atTargetVelocity
.falling()
// so stop the kicker
.ifHigh(m_kicker::stopMotor);
// when we stop being at the target velocity, it means the ball was shot
atTargetVelocity
.Falling()
// so stop the kicker
.IfHigh([&kicker = m_kicker] { kicker.Set(0.0); });
Downcasting BooleanEvent
Objects
To convert BooleanEvent
objects to other types, most commonly the Trigger
subclass used for binding commands to conditions, the generic castTo()
/CastTo()
decorator exists:
Trigger trigger = booleanEvent.castTo(Trigger::new);
frc2::Trigger trigger = booleanEvent.CastTo<frc2::Trigger>();
Note
In Java, the parameter expects a method reference to a constructor accepting an EventLoop
instance and a BooleanSupplier
. Due to the lack of method references, this parameter is defaulted in C++ as long as a constructor of the form Type(frc::EventLoop*, std::function<bool()>)
exists.