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(); }
Advertencia
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); });
Remember that button binding is declarative: bindings only need to be declared once, ideally some time during robot initialization. The library handles everything else.
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
.
Advertencia
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>();
Nota
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.