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.

Note

The example code here is taken from the EventLoop example project (Java/C++).

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(); }

Warning

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.

Warning

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.