[wpilib] Add EventLoop (#4104)

This is a generic expansion of the command-based Trigger framework.
This commit is contained in:
Starlight220
2022-06-09 08:16:51 +03:00
committed by GitHub
parent 16a4888c52
commit 45b7fc445b
27 changed files with 1265 additions and 428 deletions

View File

@@ -4,6 +4,8 @@
package edu.wpi.first.wpilibj2.command;
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.hal.FRCNetComm.tInstances;
import edu.wpi.first.hal.FRCNetComm.tResourceType;
import edu.wpi.first.hal.HAL;
@@ -15,13 +17,13 @@ import edu.wpi.first.wpilibj.RobotBase;
import edu.wpi.first.wpilibj.RobotState;
import edu.wpi.first.wpilibj.TimedRobot;
import edu.wpi.first.wpilibj.Watchdog;
import edu.wpi.first.wpilibj.event.EventLoop;
import edu.wpi.first.wpilibj.livewindow.LiveWindow;
import edu.wpi.first.wpilibj2.command.button.Trigger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -64,8 +66,9 @@ public final class CommandScheduler implements NTSendable, AutoCloseable {
// as a list of currently-registered subsystems.
private final Map<Subsystem, Command> m_subsystems = new LinkedHashMap<>();
private final EventLoop m_defaultButtonLoop = new EventLoop();
// The set of currently-registered buttons that will be polled every iteration.
private final Collection<Runnable> m_buttons = new LinkedHashSet<>();
private EventLoop m_activeButtonLoop = m_defaultButtonLoop;
private boolean m_disabled;
@@ -114,18 +117,53 @@ public final class CommandScheduler implements NTSendable, AutoCloseable {
LiveWindow.setDisabledListener(null);
}
/**
* Get the default button poll.
*
* @return a reference to the default {@link EventLoop} object polling buttons.
*/
public EventLoop getDefaultButtonLoop() {
return m_defaultButtonLoop;
}
/**
* Get the active button poll.
*
* @return a reference to the current {@link EventLoop} object polling buttons.
*/
public EventLoop getActiveButtonLoop() {
return m_activeButtonLoop;
}
/**
* Replace the button poll with another one.
*
* @param loop the new button polling loop object.
*/
public void setActiveButtonLoop(EventLoop loop) {
m_activeButtonLoop =
requireNonNullParam(loop, "loop", "CommandScheduler" + ".replaceButtonEventLoop");
}
/**
* Adds a button binding to the scheduler, which will be polled to schedule commands.
*
* @param button The button to add
* @deprecated Use {@link Trigger}
*/
@Deprecated(since = "2023")
public void addButton(Runnable button) {
m_buttons.add(button);
m_activeButtonLoop.bind(() -> true, button);
}
/** Removes all button bindings from the scheduler. */
/**
* Removes all button bindings from the scheduler.
*
* @deprecated call {@link EventLoop#clear()} on {@link #getActiveButtonLoop()} directly instead.
*/
@Deprecated(since = "2023")
public void clearButtons() {
m_buttons.clear();
m_activeButtonLoop.clear();
}
/**
@@ -254,10 +292,11 @@ public final class CommandScheduler implements NTSendable, AutoCloseable {
m_watchdog.addEpoch(subsystem.getClass().getSimpleName() + ".periodic()");
}
// Cache the active instance to avoid concurrency problems if setActiveLoop() is called from
// inside the button bindings.
EventLoop loopCache = m_activeButtonLoop;
// Poll buttons for new commands to add.
for (Runnable button : m_buttons) {
button.run();
}
loopCache.poll();
m_watchdog.addEpoch("buttons.run()");
m_inRunLoop = true;

View File

@@ -20,9 +20,11 @@ import java.util.function.BooleanSupplier;
*/
public class Button extends Trigger {
/**
* Default constructor; creates a button that is never pressed (unless {@link Button#get()} is
* overridden).
* Default constructor; creates a button that is never pressed.
*
* @deprecated Replace with {@code new Button(() -> false) }.
*/
@Deprecated(since = "2023")
public Button() {}
/**

View File

@@ -4,6 +4,8 @@
package edu.wpi.first.wpilibj2.command.button;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class is intended to be used within a program. The programmer can manually set its value.
* Also includes a setting for whether or not it should invert its value.
@@ -11,8 +13,9 @@ package edu.wpi.first.wpilibj2.command.button;
* <p>This class is provided by the NewCommands VendorDep
*/
public class InternalButton extends Button {
private boolean m_pressed;
private boolean m_inverted;
// need to be references, so they can be mutated after being captured in the constructor.
private final AtomicBoolean m_pressed;
private final AtomicBoolean m_inverted;
/** Creates an InternalButton that is not inverted. */
public InternalButton() {
@@ -26,19 +29,24 @@ public class InternalButton extends Button {
* when set to false.
*/
public InternalButton(boolean inverted) {
m_pressed = m_inverted = inverted;
this(new AtomicBoolean(), new AtomicBoolean(inverted));
}
/*
* Mock constructor so the AtomicBoolean objects can be constructed before the super
* constructor invocation.
*/
private InternalButton(AtomicBoolean state, AtomicBoolean inverted) {
super(() -> state.get() != inverted.get());
this.m_pressed = state;
this.m_inverted = inverted;
}
public void setInverted(boolean inverted) {
m_inverted = inverted;
m_inverted.set(inverted);
}
public void setPressed(boolean pressed) {
m_pressed = pressed;
}
@Override
public boolean get() {
return m_pressed ^ m_inverted;
m_pressed.set(pressed);
}
}

View File

@@ -14,9 +14,6 @@ import edu.wpi.first.wpilibj.GenericHID;
* <p>This class is provided by the NewCommands VendorDep
*/
public class JoystickButton extends Button {
private final GenericHID m_joystick;
private final int m_buttonNumber;
/**
* Creates a joystick button for triggering commands.
*
@@ -24,19 +21,7 @@ public class JoystickButton extends Button {
* @param buttonNumber The button number (see {@link GenericHID#getRawButton(int) }
*/
public JoystickButton(GenericHID joystick, int buttonNumber) {
super(() -> joystick.getRawButton(buttonNumber));
requireNonNullParam(joystick, "joystick", "JoystickButton");
m_joystick = joystick;
m_buttonNumber = buttonNumber;
}
/**
* Gets the value of the joystick button.
*
* @return The value of the joystick button
*/
@Override
public boolean get() {
return m_joystick.getRawButton(m_buttonNumber);
}
}

View File

@@ -4,6 +4,8 @@
package edu.wpi.first.wpilibj2.command.button;
import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableEntry;
import edu.wpi.first.networktables.NetworkTableInstance;
@@ -14,15 +16,14 @@ import edu.wpi.first.networktables.NetworkTableInstance;
* <p>This class is provided by the NewCommands VendorDep
*/
public class NetworkButton extends Button {
private final NetworkTableEntry m_entry;
/**
* Creates a NetworkButton that commands can be bound to.
*
* @param entry The entry that is the value.
*/
public NetworkButton(NetworkTableEntry entry) {
m_entry = entry;
super(() -> entry.getInstance().isConnected() && entry.getBoolean(false));
requireNonNullParam(entry, "entry", "NetworkButton");
}
/**
@@ -44,9 +45,4 @@ public class NetworkButton extends Button {
public NetworkButton(String table, String field) {
this(NetworkTableInstance.getDefault().getTable(table), field);
}
@Override
public boolean get() {
return m_entry.getInstance().isConnected() && m_entry.getBoolean(false);
}
}

View File

@@ -14,10 +14,6 @@ import edu.wpi.first.wpilibj.GenericHID;
* <p>This class is provided by the NewCommands VendorDep
*/
public class POVButton extends Button {
private final GenericHID m_joystick;
private final int m_angle;
private final int m_povNumber;
/**
* Creates a POV button for triggering commands.
*
@@ -26,11 +22,8 @@ public class POVButton extends Button {
* @param povNumber The POV number (see {@link GenericHID#getPOV(int)})
*/
public POVButton(GenericHID joystick, int angle, int povNumber) {
super(() -> joystick.getPOV(povNumber) == angle);
requireNonNullParam(joystick, "joystick", "POVButton");
m_joystick = joystick;
m_angle = angle;
m_povNumber = povNumber;
}
/**
@@ -42,14 +35,4 @@ public class POVButton extends Button {
public POVButton(GenericHID joystick, int angle) {
this(joystick, angle, 0);
}
/**
* Checks whether the current value of the POV is the target angle.
*
* @return Whether the value of the POV matches the target angle
*/
@Override
public boolean get() {
return m_joystick.getPOV(m_povNumber) == m_angle;
}
}

View File

@@ -7,43 +7,58 @@ package edu.wpi.first.wpilibj2.command.button;
import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.math.filter.Debouncer;
import edu.wpi.first.wpilibj.event.BooleanEvent;
import edu.wpi.first.wpilibj.event.EventLoop;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.CommandScheduler;
import edu.wpi.first.wpilibj2.command.InstantCommand;
import edu.wpi.first.wpilibj2.command.Subsystem;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
/**
* This class provides an easy way to link commands to inputs.
*
* <p>It is very easy to link a button to a command. For instance, you could link the trigger button
* of a joystick to a "score" command.
*
* <p>It is encouraged that teams write a subclass of Trigger if they want to have something unusual
* (for instance, if they want to react to the user holding a button while the robot is reading a
* certain sensor input). For this, they only have to write the {@link Trigger#get()} method to get
* the full functionality of the Trigger class.
* This class is a wrapper around {@link BooleanEvent}, providing an easy way to link commands to
* digital inputs.
*
* <p>This class is provided by the NewCommands VendorDep
*/
public class Trigger implements BooleanSupplier {
private final BooleanSupplier m_isActive;
public class Trigger extends BooleanEvent {
/**
* Creates a new trigger with the given condition determining whether it is active.
* Creates a new trigger with the given condition/digital signal.
*
* @param isActive returns whether or not the trigger should be active
* @param loop the loop that polls this trigger
* @param signal the digital signal represented.
*/
public Trigger(BooleanSupplier isActive) {
m_isActive = isActive;
public Trigger(EventLoop loop, BooleanSupplier signal) {
super(loop, signal);
}
/**
* Creates a new trigger that is always inactive. Useful only as a no-arg constructor for
* subclasses that will be overriding {@link Trigger#get()} anyway.
* Copies the BooleanEvent into a Trigger object.
*
* @param toCast the BooleanEvent
* @return a Trigger wrapping the given BooleanEvent
* @see BooleanEvent#castTo(BiFunction)
*/
public static Trigger cast(BooleanEvent toCast) {
return toCast.castTo(Trigger::new);
}
/**
* Creates a new trigger with the given condition/digital signal.
*
* <p>Polled by the {@link CommandScheduler#getDefaultButtonLoop() default scheduler button loop}.
*
* @param signal the digital signal represented.
*/
public Trigger(BooleanSupplier signal) {
this(CommandScheduler.getInstance().getDefaultButtonLoop(), signal);
}
/** Creates a new trigger that is always inactive. */
@Deprecated
public Trigger() {
m_isActive = () -> false;
this(() -> false);
}
/**
@@ -54,23 +69,11 @@ public class Trigger implements BooleanSupplier {
* <p>Functionally identical to {@link Trigger#getAsBoolean()}.
*
* @return whether or not the trigger condition is active.
* @deprecated use {@link #getAsBoolean()}
*/
public boolean get() {
return m_isActive.getAsBoolean();
}
/**
* Returns whether or not the trigger is active.
*
* <p>This method will be called repeatedly a command is linked to the Trigger.
*
* <p>Functionally identical to {@link Trigger#get()}.
*
* @return whether or not the trigger condition is active.
*/
@Override
public final boolean getAsBoolean() {
return this.get();
@Deprecated
public final boolean get() {
return getAsBoolean();
}
/**
@@ -83,23 +86,7 @@ public class Trigger implements BooleanSupplier {
public Trigger whenActive(final Command command, boolean interruptible) {
requireNonNullParam(command, "command", "whenActive");
CommandScheduler.getInstance()
.addButton(
new Runnable() {
private boolean m_pressedLast = get();
@Override
public void run() {
boolean pressed = get();
if (!m_pressedLast && pressed) {
command.schedule(interruptible);
}
m_pressedLast = pressed;
}
});
this.rising().ifHigh(() -> command.schedule(interruptible));
return this;
}
@@ -138,24 +125,9 @@ public class Trigger implements BooleanSupplier {
public Trigger whileActiveContinuous(final Command command, boolean interruptible) {
requireNonNullParam(command, "command", "whileActiveContinuous");
CommandScheduler.getInstance()
.addButton(
new Runnable() {
private boolean m_pressedLast = get();
this.ifHigh(() -> command.schedule(interruptible));
this.falling().ifHigh(command::cancel);
@Override
public void run() {
boolean pressed = get();
if (pressed) {
command.schedule(interruptible);
} else if (m_pressedLast) {
command.cancel();
}
m_pressedLast = pressed;
}
});
return this;
}
@@ -163,7 +135,7 @@ public class Trigger implements BooleanSupplier {
* Constantly starts the given command while the button is held.
*
* <p>{@link Command#schedule(boolean)} will be called repeatedly while the trigger is active, and
* will be canceled when the trigger becomes inactive. The command is set to be interruptible.
* will be canceled when the trigger becomes inactive.
*
* @param command the command to start
* @return this trigger, so calls can be chained
@@ -194,24 +166,9 @@ public class Trigger implements BooleanSupplier {
public Trigger whileActiveOnce(final Command command, boolean interruptible) {
requireNonNullParam(command, "command", "whileActiveOnce");
CommandScheduler.getInstance()
.addButton(
new Runnable() {
private boolean m_pressedLast = get();
this.rising().ifHigh(() -> command.schedule(interruptible));
this.falling().ifHigh(command::cancel);
@Override
public void run() {
boolean pressed = get();
if (!m_pressedLast && pressed) {
command.schedule(interruptible);
} else if (m_pressedLast && !pressed) {
command.cancel();
}
m_pressedLast = pressed;
}
});
return this;
}
@@ -236,22 +193,8 @@ public class Trigger implements BooleanSupplier {
public Trigger whenInactive(final Command command, boolean interruptible) {
requireNonNullParam(command, "command", "whenInactive");
CommandScheduler.getInstance()
.addButton(
new Runnable() {
private boolean m_pressedLast = get();
this.falling().ifHigh(() -> command.schedule(interruptible));
@Override
public void run() {
boolean pressed = get();
if (m_pressedLast && !pressed) {
command.schedule(interruptible);
}
m_pressedLast = pressed;
}
});
return this;
}
@@ -286,26 +229,16 @@ public class Trigger implements BooleanSupplier {
public Trigger toggleWhenActive(final Command command, boolean interruptible) {
requireNonNullParam(command, "command", "toggleWhenActive");
CommandScheduler.getInstance()
.addButton(
new Runnable() {
private boolean m_pressedLast = get();
@Override
public void run() {
boolean pressed = get();
if (!m_pressedLast && pressed) {
if (command.isScheduled()) {
command.cancel();
} else {
command.schedule(interruptible);
}
}
m_pressedLast = pressed;
this.rising()
.ifHigh(
() -> {
if (command.isScheduled()) {
command.cancel();
} else {
command.schedule(interruptible);
}
});
return this;
}
@@ -328,85 +261,45 @@ public class Trigger implements BooleanSupplier {
public Trigger cancelWhenActive(final Command command) {
requireNonNullParam(command, "command", "cancelWhenActive");
CommandScheduler.getInstance()
.addButton(
new Runnable() {
private boolean m_pressedLast = get();
this.rising().ifHigh(command::cancel);
@Override
public void run() {
boolean pressed = get();
if (!m_pressedLast && pressed) {
command.cancel();
}
m_pressedLast = pressed;
}
});
return this;
}
/**
* Composes this trigger with a boolean supplier, returning a new trigger that is active when both
* triggers are active.
*
* @param booleanSupplier the boolean supplier to compose with
* @return the trigger that is active when both triggers are active
*/
public Trigger and(BooleanSupplier booleanSupplier) {
return new Trigger(() -> get() && booleanSupplier.getAsBoolean());
/* ----------- Super method type redeclarations ----------------- */
@Override
public Trigger and(BooleanSupplier trigger) {
return cast(super.and(trigger));
}
/**
* Composes this trigger with a boolean supplier, returning a new trigger that is active when
* either trigger is active.
*
* @param booleanSupplier the boolean supplier to compose with
* @return the trigger that is active when either trigger is active
*/
public Trigger or(BooleanSupplier booleanSupplier) {
return new Trigger(() -> get() || booleanSupplier.getAsBoolean());
@Override
public Trigger or(BooleanSupplier trigger) {
return cast(super.or(trigger));
}
/**
* Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the
* negation of this trigger.
*
* @return the negated trigger
*/
@Override
public Trigger negate() {
return new Trigger(() -> !get());
return cast(super.negate());
}
/**
* Creates a new debounced trigger from this trigger - it will become active when this trigger has
* been active for longer than the specified period.
*
* @param seconds The debounce period.
* @return The debounced trigger (rising edges debounced only)
*/
@Override
public Trigger debounce(double seconds) {
return debounce(seconds, Debouncer.DebounceType.kRising);
}
/**
* Creates a new debounced trigger from this trigger - it will become active when this trigger has
* been active for longer than the specified period.
*
* @param seconds The debounce period.
* @param type The debounce type.
* @return The debounced trigger.
*/
@Override
public Trigger debounce(double seconds, Debouncer.DebounceType type) {
return new Trigger(
new BooleanSupplier() {
Debouncer m_debouncer = new Debouncer(seconds, type);
return cast(super.debounce(seconds, type));
}
@Override
public boolean getAsBoolean() {
return m_debouncer.calculate(get());
}
});
@Override
public Trigger rising() {
return cast(super.rising());
}
@Override
public Trigger falling() {
return cast(super.falling());
}
}

View File

@@ -38,9 +38,10 @@ class CommandScheduler::Impl {
// commands. Also used as a list of currently-registered subsystems.
wpi::DenseMap<Subsystem*, std::unique_ptr<Command>> subsystems;
frc::EventLoop defaultButtonLoop;
// The set of currently-registered buttons that will be polled every
// iteration.
wpi::SmallVector<wpi::unique_function<void()>, 4> buttons;
frc::EventLoop* activeButtonLoop{&defaultButtonLoop};
bool disabled{false};
@@ -95,12 +96,20 @@ void CommandScheduler::SetPeriod(units::second_t period) {
m_watchdog.SetTimeout(period);
}
void CommandScheduler::AddButton(wpi::unique_function<void()> button) {
m_impl->buttons.emplace_back(std::move(button));
frc::EventLoop* CommandScheduler::GetActiveButtonLoop() const {
return m_impl->activeButtonLoop;
}
void CommandScheduler::SetActiveButtonLoop(frc::EventLoop* loop) {
m_impl->activeButtonLoop = loop;
}
frc::EventLoop* CommandScheduler::GetDefaultButtonLoop() const {
return &(m_impl->defaultButtonLoop);
}
void CommandScheduler::ClearButtons() {
m_impl->buttons.clear();
m_impl->activeButtonLoop->Clear();
}
void CommandScheduler::Schedule(bool interruptible, Command* command) {
@@ -200,10 +209,11 @@ void CommandScheduler::Run() {
m_watchdog.AddEpoch("Subsystem Periodic()");
}
// Cache the active instance to avoid concurrency problems if SetActiveLoop()
// is called from inside the button bindings.
frc::EventLoop* loopCache = m_impl->activeButtonLoop;
// Poll buttons for new commands to add.
for (auto&& button : m_impl->buttons) {
button();
}
loopCache->Poll();
m_watchdog.AddEpoch("buttons.Run()");
m_impl->inRunLoop = true;

View File

@@ -8,22 +8,14 @@
#include "frc2/command/InstantCommand.h"
using namespace frc;
using namespace frc2;
Trigger::Trigger(const Trigger& other) = default;
Trigger Trigger::WhenActive(Command* command, bool interruptible) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this, command, interruptible]() mutable {
bool pressed = m_isActive();
if (!pressedLast && pressed) {
command->Schedule(interruptible);
}
pressedLast = pressed;
});
this->Rising().IfHigh(
[command, interruptible] { command->Schedule(interruptible); });
return *this;
}
@@ -39,18 +31,8 @@ Trigger Trigger::WhenActive(std::function<void()> toRun,
}
Trigger Trigger::WhileActiveContinous(Command* command, bool interruptible) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this, command, interruptible]() mutable {
bool pressed = m_isActive();
if (pressed) {
command->Schedule(interruptible);
} else if (pressedLast && !pressed) {
command->Cancel();
}
pressedLast = pressed;
});
this->IfHigh([command, interruptible] { command->Schedule(interruptible); });
this->Falling().IfHigh([command] { command->Cancel(); });
return *this;
}
@@ -67,32 +49,15 @@ Trigger Trigger::WhileActiveContinous(
}
Trigger Trigger::WhileActiveOnce(Command* command, bool interruptible) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this, command, interruptible]() mutable {
bool pressed = m_isActive();
if (!pressedLast && pressed) {
command->Schedule(interruptible);
} else if (pressedLast && !pressed) {
command->Cancel();
}
pressedLast = pressed;
});
this->Rising().IfHigh(
[command, interruptible] { command->Schedule(interruptible); });
this->Falling().IfHigh([command] { command->Cancel(); });
return *this;
}
Trigger Trigger::WhenInactive(Command* command, bool interruptible) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this, command, interruptible]() mutable {
bool pressed = m_isActive();
if (pressedLast && !pressed) {
command->Schedule(interruptible);
}
pressedLast = pressed;
});
this->Falling().IfHigh(
[command, interruptible] { command->Schedule(interruptible); });
return *this;
}
@@ -108,41 +73,17 @@ Trigger Trigger::WhenInactive(std::function<void()> toRun,
}
Trigger Trigger::ToggleWhenActive(Command* command, bool interruptible) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this, command, interruptible]() mutable {
bool pressed = m_isActive();
if (!pressedLast && pressed) {
if (command->IsScheduled()) {
command->Cancel();
} else {
command->Schedule(interruptible);
}
}
pressedLast = pressed;
});
this->Rising().IfHigh([command, interruptible] {
if (command->IsScheduled()) {
command->Cancel();
} else {
command->Schedule(interruptible);
}
});
return *this;
}
Trigger Trigger::CancelWhenActive(Command* command) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this, command]() mutable {
bool pressed = m_isActive();
if (!pressedLast && pressed) {
command->Cancel();
}
pressedLast = pressed;
});
this->Rising().IfHigh([command] { command->Cancel(); });
return *this;
}
Trigger Trigger::Debounce(units::second_t debounceTime,
frc::Debouncer::DebounceType type) {
return Trigger(
[debouncer = frc::Debouncer(debounceTime, type), *this]() mutable {
return debouncer.Calculate(m_isActive());
});
}

View File

@@ -10,9 +10,11 @@
#include <frc/Errors.h>
#include <frc/Watchdog.h>
#include <frc/event/EventLoop.h>
#include <networktables/NTSendable.h>
#include <units/time.h>
#include <wpi/FunctionExtras.h>
#include <wpi/deprecated.h>
#include <wpi/sendable/SendableHelper.h>
#include <wpi/span.h>
@@ -52,16 +54,32 @@ class CommandScheduler final : public nt::NTSendable,
void SetPeriod(units::second_t period);
/**
* Adds a button binding to the scheduler, which will be polled to schedule
* commands.
* Get the active button poll.
*
* @param button The button to add
* @return a reference to the current {@link frc::EventLoop} object polling
* buttons.
*/
void AddButton(wpi::unique_function<void()> button);
frc::EventLoop* GetActiveButtonLoop() const;
/**
* Replace the button poll with another one.
*
* @param loop the new button polling loop object.
*/
void SetActiveButtonLoop(frc::EventLoop* loop);
/**
* Get the default button poll.
*
* @return a reference to the default {@link frc::EventLoop} object polling
* buttons.
*/
frc::EventLoop* GetDefaultButtonLoop() const;
/**
* Removes all button bindings from the scheduler.
*/
WPI_DEPRECATED("Call Clear on the EventLoop instance directly!")
void ClearButtons();
/**

View File

@@ -9,6 +9,8 @@
#include <memory>
#include <utility>
#include <frc/event/BooleanEvent.h>
#include <frc/event/EventLoop.h>
#include <frc/filter/Debouncer.h>
#include <units/time.h>
#include <wpi/span.h>
@@ -19,32 +21,41 @@
namespace frc2 {
class Command;
/**
* A class used to bind command scheduling to events. The
* Trigger class is a base for all command-event-binding classes, and so the
* methods are named fairly abstractly; for purpose-specific wrappers, see
* Button.
* This class is a command-based wrapper around {@link BooleanEvent}, providing
* an easy way to link commands to inputs.
*
* This class is provided by the NewCommands VendorDep
*
* @see Button
*/
class Trigger {
class Trigger : public frc::BooleanEvent {
public:
/**
* Creates a new trigger with the given condition determining whether it is
* active.
*
* <p>Polled by the default scheduler button loop.
*
* @param isActive returns whether or not the trigger should be active
*/
explicit Trigger(std::function<bool()> isActive)
: BooleanEvent{CommandScheduler::GetInstance().GetDefaultButtonLoop(),
std::move(isActive)} {}
/**
* Create a new trigger that is active when the given condition is true.
*
* @param loop The loop instance that polls this trigger.
* @param isActive Whether the trigger is active.
*/
Trigger(std::function<bool()> isActive) // NOLINT
: m_isActive{std::move(isActive)} {}
Trigger(frc::EventLoop* loop, std::function<bool()> isActive)
: BooleanEvent{loop, std::move(isActive)} {}
/**
* Create a new trigger that is never active (default constructor) - activity
* can be further determined by subclass code.
*/
Trigger() {
m_isActive = [] { return false; };
}
Trigger() : Trigger([] { return false; }) {}
Trigger(const Trigger& other);
@@ -72,19 +83,10 @@ class Trigger {
template <class T, typename = std::enable_if_t<std::is_base_of_v<
Command, std::remove_reference_t<T>>>>
Trigger WhenActive(T&& command, bool interruptible = true) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this,
command = std::make_unique<std::remove_reference_t<T>>(
this->Rising().IfHigh(
[command = std::make_unique<std::remove_reference_t<T>>(
std::forward<T>(command)),
interruptible]() mutable {
bool pressed = m_isActive();
if (!pressedLast && pressed) {
command->Schedule(interruptible);
}
pressedLast = pressed;
});
interruptible] { command->Schedule(interruptible); });
return *this;
}
@@ -131,21 +133,11 @@ class Trigger {
template <class T, typename = std::enable_if_t<std::is_base_of_v<
Command, std::remove_reference_t<T>>>>
Trigger WhileActiveContinous(T&& command, bool interruptible = true) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this,
command = std::make_unique<std::remove_reference_t<T>>(
std::forward<T>(command)),
interruptible]() mutable {
bool pressed = m_isActive();
std::shared_ptr<T> ptr =
std::make_shared<std::remove_reference_t<T>>(std::forward<T>(command));
this->IfHigh([ptr, interruptible] { ptr->Schedule(interruptible); });
this->Falling().IfHigh([ptr] { ptr->Cancel(); });
if (pressed) {
command->Schedule(interruptible);
} else if (pressedLast && !pressed) {
command->Cancel();
}
pressedLast = pressed;
});
return *this;
}
@@ -191,21 +183,13 @@ class Trigger {
template <class T, typename = std::enable_if_t<std::is_base_of_v<
Command, std::remove_reference_t<T>>>>
Trigger WhileActiveOnce(T&& command, bool interruptible = true) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this,
command = std::make_unique<std::remove_reference_t<T>>(
std::forward<T>(command)),
interruptible]() mutable {
bool pressed = m_isActive();
std::shared_ptr<T> ptr =
std::make_shared<std::remove_reference_t<T>>(std::forward<T>(command));
if (!pressedLast && pressed) {
command->Schedule(interruptible);
} else if (pressedLast && !pressed) {
command->Cancel();
}
this->Rising().IfHigh(
[ptr, interruptible] { ptr->Schedule(interruptible); });
this->Falling().IfHigh([ptr] { ptr->Cancel(); });
pressedLast = pressed;
});
return *this;
}
@@ -233,19 +217,11 @@ class Trigger {
template <class T, typename = std::enable_if_t<std::is_base_of_v<
Command, std::remove_reference_t<T>>>>
Trigger WhenInactive(T&& command, bool interruptible = true) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this,
command = std::make_unique<std::remove_reference_t<T>>(
this->Falling().IfHigh(
[command = std::make_unique<std::remove_reference_t<T>>(
std::forward<T>(command)),
interruptible]() mutable {
bool pressed = m_isActive();
interruptible] { command->Schedule(interruptible); });
if (pressedLast && !pressed) {
command->Schedule(interruptible);
}
pressedLast = pressed;
});
return *this;
}
@@ -291,23 +267,17 @@ class Trigger {
template <class T, typename = std::enable_if_t<std::is_base_of_v<
Command, std::remove_reference_t<T>>>>
Trigger ToggleWhenActive(T&& command, bool interruptible = true) {
CommandScheduler::GetInstance().AddButton(
[pressedLast = m_isActive(), *this,
command = std::make_unique<std::remove_reference_t<T>>(
this->Rising().IfHigh(
[command = std::make_unique<std::remove_reference_t<T>>(
std::forward<T>(command)),
interruptible]() mutable {
bool pressed = m_isActive();
if (!pressedLast && pressed) {
if (command->IsScheduled()) {
command->Cancel();
} else {
command->Schedule(interruptible);
}
interruptible] {
if (!command->IsScheduled()) {
command->Schedule(interruptible);
} else {
command->Cancel();
}
pressedLast = pressed;
});
return *this;
}
@@ -321,13 +291,27 @@ class Trigger {
*/
Trigger CancelWhenActive(Command* command);
/**
* Get a new event that events only when this one newly changes to true.
*
* @return a new event representing when this one newly changes to true.
*/
Trigger Rising() { return BooleanEvent::Rising().CastTo<Trigger>(); }
/**
* Get a new event that triggers only when this one newly changes to false.
*
* @return a new event representing when this one newly changes to false.
*/
Trigger Falling() { return BooleanEvent::Falling().CastTo<Trigger>(); }
/**
* Composes two triggers with logical AND.
*
* @return A trigger which is active when both component triggers are active.
*/
Trigger operator&&(Trigger rhs) {
return Trigger([*this, rhs] { return m_isActive() && rhs.m_isActive(); });
Trigger operator&&(std::function<bool()> rhs) {
return BooleanEvent::operator&&(rhs).CastTo<Trigger>();
}
/**
@@ -335,8 +319,8 @@ class Trigger {
*
* @return A trigger which is active when either component trigger is active.
*/
Trigger operator||(Trigger rhs) {
return Trigger([*this, rhs] { return m_isActive() || rhs.m_isActive(); });
Trigger operator||(std::function<bool()> rhs) {
return BooleanEvent::operator||(rhs).CastTo<Trigger>();
}
/**
@@ -345,9 +329,7 @@ class Trigger {
* @return A trigger which is active when the component trigger is inactive,
* and vice-versa.
*/
Trigger operator!() {
return Trigger([*this] { return !m_isActive(); });
}
Trigger operator!() { return BooleanEvent::operator!().CastTo<Trigger>(); }
/**
* Creates a new debounced trigger from this trigger - it will become active
@@ -359,9 +341,8 @@ class Trigger {
*/
Trigger Debounce(units::second_t debounceTime,
frc::Debouncer::DebounceType type =
frc::Debouncer::DebounceType::kRising);
private:
std::function<bool()> m_isActive;
frc::Debouncer::DebounceType::kRising) {
return BooleanEvent::Debounce(debounceTime, type).CastTo<Trigger>();
}
};
} // namespace frc2

View File

@@ -20,7 +20,7 @@ public class CommandTestBase {
void commandSetup() {
CommandScheduler.getInstance().cancelAll();
CommandScheduler.getInstance().enable();
CommandScheduler.getInstance().clearButtons();
CommandScheduler.getInstance().getActiveButtonLoop().clear();
CommandGroupBase.clearGroupedCommands();
setDSEnabled(true);

View File

@@ -170,10 +170,10 @@ class ButtonTest extends CommandTestBase {
button1.setPressed(true);
button2.setPressed(false);
assertFalse(button1.and(button2).get());
assertTrue(button1.or(button2).get());
assertFalse(button1.negate().get());
assertTrue(button1.and(button2.negate()).get());
assertFalse(button1.and(button2).getAsBoolean());
assertTrue(button1.or(button2).getAsBoolean());
assertFalse(button1.negate().getAsBoolean());
assertTrue(button1.and(button2.negate()).getAsBoolean());
}
@Test
@@ -183,8 +183,8 @@ class ButtonTest extends CommandTestBase {
button1.setPressed(true);
assertFalse(button1.and(booleanSupplier).get());
assertTrue(button1.or(booleanSupplier).get());
assertFalse(button1.and(booleanSupplier).getAsBoolean());
assertTrue(button1.or(booleanSupplier).getAsBoolean());
}
@Test

View File

@@ -10,7 +10,7 @@ CommandTestBase::CommandTestBase() {
auto& scheduler = CommandScheduler::GetInstance();
scheduler.CancelAll();
scheduler.Enable();
scheduler.ClearButtons();
scheduler.GetActiveButtonLoop()->Clear();
}
CommandScheduler CommandTestBase::GetScheduler() {
@@ -22,7 +22,7 @@ void CommandTestBase::SetUp() {
}
void CommandTestBase::TearDown() {
CommandScheduler::GetInstance().ClearButtons();
CommandScheduler::GetInstance().GetActiveButtonLoop()->Clear();
}
void CommandTestBase::SetDSEnabled(bool enabled) {

View File

@@ -0,0 +1,64 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "frc/event/BooleanEvent.h"
using namespace frc;
BooleanEvent::BooleanEvent(EventLoop* loop, std::function<bool()> condition)
: m_loop(loop), m_condition(std::move(condition)) {}
BooleanEvent::operator std::function<bool()>() {
return m_condition;
}
bool BooleanEvent::GetAsBoolean() const {
return m_condition();
}
void BooleanEvent::IfHigh(wpi::unique_function<void()> action) {
m_loop->Bind(m_condition, std::move(action));
}
BooleanEvent BooleanEvent::operator!() {
return BooleanEvent(this->m_loop, [lhs = m_condition] { return !lhs(); });
}
BooleanEvent BooleanEvent::operator&&(std::function<bool()> rhs) {
return BooleanEvent(this->m_loop,
[lhs = m_condition, rhs] { return lhs() && rhs(); });
}
BooleanEvent BooleanEvent::operator||(std::function<bool()> rhs) {
return BooleanEvent(this->m_loop,
[lhs = m_condition, rhs] { return lhs() || rhs(); });
}
BooleanEvent BooleanEvent::Rising() {
return BooleanEvent(
this->m_loop, [lhs = m_condition, m_previous = m_condition()]() mutable {
bool present = lhs();
bool past = m_previous;
m_previous = present;
return !past && present;
});
}
BooleanEvent BooleanEvent::Falling() {
return BooleanEvent(
this->m_loop, [lhs = m_condition, m_previous = m_condition()]() mutable {
bool present = lhs();
bool past = m_previous;
m_previous = present;
return past && !present;
});
}
BooleanEvent BooleanEvent::Debounce(units::second_t debounceTime,
frc::Debouncer::DebounceType type) {
return BooleanEvent(
this->m_loop,
[debouncer = frc::Debouncer(debounceTime, type),
lhs = m_condition]() mutable { return debouncer.Calculate(lhs()); });
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "frc/event/EventLoop.h"
using namespace frc;
EventLoop::EventLoop() {}
void EventLoop::Binding::Poll() {
if (condition()) {
action();
}
}
void EventLoop::Bind(std::function<bool()> condition,
wpi::unique_function<void()> action) {
m_bindings.emplace_back(Binding{condition, std::move(action)});
}
void EventLoop::Poll() {
for (Binding& binding : m_bindings) {
binding.Poll();
}
}
void EventLoop::Clear() {
m_bindings.clear();
}

View File

@@ -0,0 +1,136 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <frc/filter/Debouncer.h>
#include <functional>
#include <memory>
#include <units/time.h>
#include <wpi/FunctionExtras.h>
#include "EventLoop.h"
namespace frc {
/**
* This class provides an easy way to link actions to inputs. Each object
* represents a boolean condition to which callback actions can be bound using
* {@link #IfHigh(wpi::unique_function<void()>)}.
*
* <p>These events can easily be composed using factories such as {@link
* #operator!},
* {@link #operator||}, {@link #operator&&} etc.
*
* <p>To get an event that activates only when this one changes, see {@link
* #Falling()} and {@link #Rising()}.
*/
class BooleanEvent {
public:
/**
* Creates a new event with the given condition determining whether it is
* active.
*
* @param loop the loop that polls this event
* @param condition returns whether or not the event should be active
*/
BooleanEvent(EventLoop* loop, std::function<bool()> condition);
/**
* Check whether this event is active or not.
*
* @return true if active.
*/
bool GetAsBoolean() const;
/**
* Bind an action to this event.
*
* @param action the action to run if this event is active.
*/
void IfHigh(wpi::unique_function<void()> action);
operator std::function<bool()>(); // NOLINT
/**
* A method to "downcast" a BooleanEvent instance to a subclass (for example,
* to a command-based version of this class).
*
* @param ctor a method reference to the constructor of the subclass that
* accepts the loop as the first parameter and the condition/signal as the
* second.
* @return an instance of the subclass.
*/
template <class T,
typename = std::enable_if_t<std::is_base_of_v<BooleanEvent, T>>>
T CastTo(std::function<T(EventLoop*, std::function<bool()>)> ctor =
[](EventLoop* loop, std::function<bool()> condition) {
return T(loop, condition);
}) {
return ctor(m_loop, m_condition);
}
/**
* Creates a new event that is active when this event is inactive, i.e. that
* acts as the negation of this event.
*
* @return the negated event
*/
BooleanEvent operator!();
/**
* Composes this event with another event, returning a new event that is
* active when both events are active.
*
* <p>The new event will use this event's polling loop.
*
* @param rhs the event to compose with
* @return the event that is active when both events are active
*/
BooleanEvent operator&&(std::function<bool()> rhs);
/**
* Composes this event with another event, returning a new event that is
* active when either event is active.
*
* <p>The new event will use this event's polling loop.
*
* @param rhs the event to compose with
* @return the event that is active when either event is active
*/
BooleanEvent operator||(std::function<bool()> rhs);
/**
* Get a new event that events only when this one newly changes to true.
*
* @return a new event representing when this one newly changes to true.
*/
BooleanEvent Rising();
/**
* Get a new event that triggers only when this one newly changes to false.
*
* @return a new event representing when this one newly changes to false.
*/
BooleanEvent Falling();
/**
* Creates a new debounced event from this event - it will become active when
* this event has been active for longer than the specified period.
*
* @param debounceTime The debounce period.
* @param type The debounce type.
* @return The debounced event.
*/
BooleanEvent Debounce(units::second_t debounceTime,
frc::Debouncer::DebounceType type =
frc::Debouncer::DebounceType::kRising);
private:
EventLoop* m_loop;
std::function<bool()> m_condition;
};
} // namespace frc

View File

@@ -0,0 +1,48 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <functional>
#include <list>
#include <wpi/FunctionExtras.h>
#include <wpi/SmallVector.h>
namespace frc {
/** The loop polling BooleanEvent objects and executing the actions bound to
* them. */
class EventLoop {
public:
EventLoop();
/**
* Bind a new action to run whenever the condition is true.
*
* @param condition the condition to listen to.
* @param action the action to run.
*/
void Bind(std::function<bool()> condition,
wpi::unique_function<void()> action);
/**
* Poll all bindings.
*/
void Poll();
/**
* Clear all bindings.
*/
void Clear();
private:
struct Binding {
std::function<bool()> condition;
wpi::unique_function<void()> action;
void Poll();
};
std::list<Binding> m_bindings;
};
} // namespace frc

View File

@@ -0,0 +1,102 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include <frc/Encoder.h>
#include <frc/Joystick.h>
#include <frc/TimedRobot.h>
#include <frc/Ultrasonic.h>
#include <frc/controller/PIDController.h>
#include <frc/controller/SimpleMotorFeedforward.h>
#include <frc/event/BooleanEvent.h>
#include <frc/event/EventLoop.h>
#include <frc/motorcontrol/PWMSparkMax.h>
#include <units/angular_velocity.h>
#include <units/length.h>
#include <units/time.h>
#include <units/voltage.h>
static const auto SHOT_VELOCITY = 200_rpm;
static const auto TOLERANCE = 8_rpm;
static const auto KICKER_THRESHOLD = 15_mm;
class Robot : public frc::TimedRobot {
public:
void RobotInit() override {
m_controller.SetTolerance(TOLERANCE.value());
frc::BooleanEvent isBallAtKicker{&m_loop, [&kickerSensor = m_kickerSensor] {
return kickerSensor.GetRange() <
KICKER_THRESHOLD;
}};
frc::BooleanEvent intakeButton{
&m_loop, [&joystick = m_joystick] { return joystick.GetRawButton(2); }};
// 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); });
// 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); });
frc::BooleanEvent shootTrigger{
&m_loop, [&joystick = m_joystick] { return joystick.GetTrigger(); }};
// if the trigger is held
shootTrigger
// accelerate the shooter wheel
.IfHigh([&shooter = m_shooter, &controller = m_controller, &ff = m_ff,
&encoder = m_shooterEncoder] {
shooter.SetVoltage(units::volt_t(controller.Calculate(
encoder.GetRate(), SHOT_VELOCITY.value())) +
ff.Calculate(SHOT_VELOCITY));
});
// if not, stop
shootTrigger.IfHigh([&shooter = m_shooter] { shooter.Set(0.0); });
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); });
// 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); });
}
void RobotPeriodic() override { m_loop.Poll(); }
private:
frc::PWMSparkMax m_shooter{0};
frc::Encoder m_shooterEncoder{0, 1};
frc::PIDController m_controller{0.3, 0, 0};
frc::SimpleMotorFeedforward<units::radians> m_ff{0.1_V, 0.065_V / 1_rpm};
frc::PWMSparkMax m_kicker{1};
frc::Ultrasonic m_kickerSensor{2, 3};
frc::PWMSparkMax m_intake{3};
frc::EventLoop m_loop{};
frc::Joystick m_joystick{0};
};
#ifndef RUNNING_FRC_TESTS
int main() {
return frc::StartRobot<Robot>();
}
#endif

View File

@@ -84,6 +84,16 @@
"gradlebase": "cpp",
"commandversion": 2
},
{
"name": "EventLoop",
"description": "Demonstrate managing a ball system using EventLoop and BooleanEvent.",
"tags": [
"EventLoop"
],
"foldername": "EventLoop",
"gradlebase": "cpp",
"commandversion": 2
},
{
"name": "Arcade Drive",
"description": "An example program which demonstrates the use of Arcade Drive with the DifferentialDrive class",

View File

@@ -0,0 +1,184 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.event;
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.math.filter.Debouncer;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
/**
* This class provides an easy way to link actions to high-active logic signals. Each object
* represents a digital signal to which callback actions can be bound using {@link
* #ifHigh(Runnable)}.
*
* <p>These signals can easily be composed for advanced functionality using {@link
* #and(BooleanSupplier)}, {@link #or(BooleanSupplier)}, {@link #negate()} etc.
*
* <p>To get an event that activates only when this one changes, see {@link #falling()} and {@link
* #rising()}.
*/
public class BooleanEvent implements BooleanSupplier {
/** Poller loop. */
protected final EventLoop m_loop;
/** Condition. */
private final BooleanSupplier m_signal;
/**
* Creates a new event with the given signal determining whether it is active.
*
* @param loop the loop that polls this event
* @param signal the digital signal represented by this object.
*/
public BooleanEvent(EventLoop loop, BooleanSupplier signal) {
m_loop = requireNonNullParam(loop, "loop", "BooleanEvent");
m_signal = requireNonNullParam(signal, "signal", "BooleanEvent");
}
/**
* Check the state of this signal (high or low).
*
* @return true for the high state, false for the low state.
*/
@Override
public final boolean getAsBoolean() {
return m_signal.getAsBoolean();
}
/**
* Bind an action to this event.
*
* @param action the action to run if this event is active.
*/
public final void ifHigh(Runnable action) {
m_loop.bind(m_signal, action);
}
/**
* Get a new event that events only when this one newly changes to true.
*
* @return a new event representing when this one newly changes to true.
*/
public BooleanEvent rising() {
return new BooleanEvent(
m_loop,
new BooleanSupplier() {
private boolean m_previous = m_signal.getAsBoolean();
@Override
public boolean getAsBoolean() {
boolean present = m_signal.getAsBoolean();
boolean ret = !m_previous && present;
m_previous = present;
return ret;
}
});
}
/**
* Get a new event that triggers only when this one newly changes to false.
*
* @return a new event representing when this one newly changes to false.
*/
public BooleanEvent falling() {
return new BooleanEvent(
m_loop,
new BooleanSupplier() {
private boolean m_previous = m_signal.getAsBoolean();
@Override
public boolean getAsBoolean() {
boolean present = m_signal.getAsBoolean();
boolean ret = m_previous && !present;
m_previous = present;
return ret;
}
});
}
/**
* Creates a new debounced event from this event - it will become active when this event has been
* active for longer than the specified period.
*
* @param seconds The debounce period.
* @return The debounced event (rising edges debounced only)
*/
public BooleanEvent debounce(double seconds) {
return debounce(seconds, Debouncer.DebounceType.kRising);
}
/**
* Creates a new debounced event from this event - it will become active when this event has been
* active for longer than the specified period.
*
* @param seconds The debounce period.
* @param type The debounce type.
* @return The debounced event.
*/
public BooleanEvent debounce(double seconds, Debouncer.DebounceType type) {
return new BooleanEvent(
m_loop,
new BooleanSupplier() {
private final Debouncer m_debouncer = new Debouncer(seconds, type);
@Override
public boolean getAsBoolean() {
return m_debouncer.calculate(m_signal.getAsBoolean());
}
});
}
/**
* Creates a new event that is active when this event is inactive, i.e. that acts as the negation
* of this event.
*
* @return the negated event
*/
public BooleanEvent negate() {
return new BooleanEvent(m_loop, () -> !m_signal.getAsBoolean());
}
/**
* Composes this event with another event, returning a new signal that is in the high state when
* both signals are in the high state.
*
* <p>The new event will use this event's polling loop.
*
* @param other the event to compose with
* @return the event that is active when both events are active
*/
public BooleanEvent and(BooleanSupplier other) {
requireNonNullParam(other, "other", "and");
return new BooleanEvent(m_loop, () -> m_signal.getAsBoolean() && other.getAsBoolean());
}
/**
* Composes this event with another event, returning a new signal that is high when either signal
* is high.
*
* <p>The new event will use this event's polling loop.
*
* @param other the event to compose with
* @return a signal that is high when either signal is high.
*/
public BooleanEvent or(BooleanSupplier other) {
requireNonNullParam(other, "other", "or");
return new BooleanEvent(m_loop, () -> m_signal.getAsBoolean() || other.getAsBoolean());
}
/**
* A method to "downcast" a BooleanEvent instance to a subclass (for example, to a command-based
* version of this class).
*
* @param ctor a method reference to the constructor of the subclass that accepts the loop as the
* first parameter and the condition/signal as the second.
* @param <T> the subclass type
* @return an instance of the subclass.
*/
public <T extends BooleanEvent> T castTo(BiFunction<EventLoop, BooleanSupplier, T> ctor) {
return ctor.apply(m_loop, m_signal);
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.event;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.function.BooleanSupplier;
/** The loop polling {@link BooleanEvent} objects and executing the actions bound to them. */
public final class EventLoop {
private final Collection<Binding> m_bindings = new LinkedHashSet<>();
/**
* Bind a new action to run whenever the condition is true.
*
* @param condition the condition to listen to.
* @param action the action to run.
*/
public void bind(BooleanSupplier condition, Runnable action) {
m_bindings.add(new Binding(condition, action));
}
/** Poll all bindings. */
public void poll() {
m_bindings.forEach(Binding::poll);
}
/** Clear all bindings. */
public void clear() {
m_bindings.clear();
}
private static class Binding {
private final BooleanSupplier m_condition;
private final Runnable m_action;
private Binding(BooleanSupplier condition, Runnable action) {
this.m_condition = condition;
this.m_action = action;
}
void poll() {
if (m_condition.getAsBoolean()) {
m_action.run();
}
}
}
}

View File

@@ -0,0 +1,168 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.event;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
class BooleanEventTest {
@Test
void testBinaryCompositions() {
var loop = new EventLoop();
var andCounter = new AtomicInteger(0);
var orCounter = new AtomicInteger(0);
assertEquals(0, andCounter.get());
assertEquals(0, orCounter.get());
new BooleanEvent(loop, () -> true).and(() -> false).ifHigh(andCounter::incrementAndGet);
new BooleanEvent(loop, () -> true).or(() -> false).ifHigh(orCounter::incrementAndGet);
loop.poll();
assertEquals(0, andCounter.get());
assertEquals(1, orCounter.get());
}
@Test
void testBinaryCompositionLoopSemantics() {
var loop1 = new EventLoop();
var loop2 = new EventLoop();
var counter1 = new AtomicInteger(0);
var counter2 = new AtomicInteger(0);
new BooleanEvent(loop1, () -> true)
.and(new BooleanEvent(loop2, () -> true))
.ifHigh(counter1::incrementAndGet);
new BooleanEvent(loop2, () -> true)
.and(new BooleanEvent(loop1, () -> true))
.ifHigh(counter2::incrementAndGet);
assertEquals(0, counter1.get());
assertEquals(0, counter2.get());
loop1.poll();
assertEquals(1, counter1.get());
assertEquals(0, counter2.get());
loop2.poll();
assertEquals(1, counter1.get());
assertEquals(1, counter2.get());
}
@Test
void testEdgeDecorators() {
var bool = new AtomicBoolean(false);
var counter = new AtomicInteger(0);
var loop = new EventLoop();
new BooleanEvent(loop, bool::get).falling().ifHigh(counter::decrementAndGet);
new BooleanEvent(loop, bool::get).rising().ifHigh(counter::incrementAndGet);
assertEquals(0, counter.get());
bool.set(false);
loop.poll();
assertEquals(0, counter.get());
bool.set(true);
loop.poll();
assertEquals(1, counter.get());
bool.set(true);
loop.poll();
assertEquals(1, counter.get());
bool.set(false);
loop.poll();
assertEquals(0, counter.get());
}
@Test
void testEdgeReuse() {
var loop = new EventLoop();
var bool = new AtomicBoolean(false);
var counter = new AtomicInteger(0);
var event = new BooleanEvent(loop, bool::get).rising();
event.ifHigh(counter::incrementAndGet);
event.ifHigh(counter::incrementAndGet);
assertEquals(0, counter.get());
loop.poll();
assertEquals(0, counter.get());
bool.set(true);
loop.poll();
assertEquals(1, counter.get()); // FIXME?: natural sense dictates counter == 2!!
loop.poll();
assertEquals(1, counter.get());
bool.set(false);
loop.poll();
assertEquals(1, counter.get());
bool.set(true);
loop.poll();
assertEquals(2, counter.get());
}
@Test
void testEdgeReconstruct() {
var loop = new EventLoop();
var bool = new AtomicBoolean(false);
var counter = new AtomicInteger(0);
var event = new BooleanEvent(loop, bool::get);
event.rising().ifHigh(counter::incrementAndGet);
event.rising().ifHigh(counter::incrementAndGet);
assertEquals(0, counter.get());
loop.poll();
assertEquals(0, counter.get());
bool.set(true);
loop.poll();
// unlike the previous test ...
assertEquals(2, counter.get()); // as natural sense dictates, counter == 2
loop.poll();
assertEquals(2, counter.get());
bool.set(false);
loop.poll();
assertEquals(2, counter.get());
bool.set(true);
loop.poll();
assertEquals(4, counter.get());
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.event;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
class EventLoopTest {
@Test
void testConditions() {
var counterTrue = new AtomicInteger(0);
var counterFalse = new AtomicInteger(0);
var loop = new EventLoop();
loop.bind(() -> true, counterTrue::incrementAndGet);
loop.bind(() -> false, counterFalse::incrementAndGet);
assertEquals(0, counterTrue.get());
assertEquals(0, counterFalse.get());
loop.poll();
assertEquals(1, counterTrue.get());
assertEquals(0, counterFalse.get());
loop.poll();
assertEquals(2, counterTrue.get());
assertEquals(0, counterFalse.get());
}
@Test
void testClear() {
var condition = new AtomicBoolean(false);
var counter = new AtomicInteger(0);
var loop = new EventLoop();
// first ensure binding works
loop.bind(condition::get, counter::incrementAndGet);
condition.set(false);
loop.poll();
assertEquals(0, counter.get());
condition.set(true);
loop.poll();
assertEquals(1, counter.get());
// clear bindings
loop.clear();
condition.set(true);
loop.poll();
// shouldn't change
assertEquals(1, counter.get());
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.examples.eventloop;
import edu.wpi.first.wpilibj.RobotBase;
import edu.wpi.first.wpilibj.examples.encoder.Robot;
/**
* Do NOT add any static variables to this class, or any initialization at all. Unless you know what
* you are doing, do not modify this file except to change the parameter class to the startRobot
* call.
*/
public final class Main {
private Main() {}
/**
* Main initialization function. Do not perform any initialization here.
*
* <p>If you change your main robot class, change the parameter type.
*/
public static void main(String... args) {
RobotBase.startRobot(Robot::new);
}
}

View File

@@ -0,0 +1,91 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.examples.eventloop;
import edu.wpi.first.math.controller.PIDController;
import edu.wpi.first.math.controller.SimpleMotorFeedforward;
import edu.wpi.first.wpilibj.Encoder;
import edu.wpi.first.wpilibj.Joystick;
import edu.wpi.first.wpilibj.TimedRobot;
import edu.wpi.first.wpilibj.Ultrasonic;
import edu.wpi.first.wpilibj.event.BooleanEvent;
import edu.wpi.first.wpilibj.event.EventLoop;
import edu.wpi.first.wpilibj.motorcontrol.MotorController;
import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
public class Robot extends TimedRobot {
public static final int SHOT_VELOCITY = 200; // rpm
public static final int TOLERANCE = 8; // rpm
public static final int KICKER_THRESHOLD = 15; // mm
private final MotorController m_shooter = new PWMSparkMax(0);
private final Encoder m_shooterEncoder = new Encoder(0, 1);
private final PIDController m_controller = new PIDController(0.3, 0, 0);
private final SimpleMotorFeedforward m_ff = new SimpleMotorFeedforward(0.1, 0.065);
private final MotorController m_kicker = new PWMSparkMax(1);
private final Ultrasonic m_kickerSensor = new Ultrasonic(2, 3);
private final MotorController m_intake = new PWMSparkMax(2);
private final EventLoop m_loop = new EventLoop();
private final Joystick m_joystick = new Joystick(0);
@Override
public void robotInit() {
m_controller.setTolerance(TOLERANCE);
BooleanEvent isBallAtKicker =
new BooleanEvent(m_loop, () -> m_kickerSensor.getRangeMM() < KICKER_THRESHOLD);
BooleanEvent intakeButton = new BooleanEvent(m_loop, () -> m_joystick.getRawButton(2));
// 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 not held
intakeButton
// or there is a ball in the kicker
.or(isBallAtKicker)
// stop the intake
.ifHigh(m_intake::stopMotor);
BooleanEvent shootTrigger = new BooleanEvent(m_loop, m_joystick::getTrigger);
// if the trigger is held
shootTrigger
// accelerate the shooter wheel
.ifHigh(
() ->
m_shooter.setVoltage(
m_controller.calculate(m_shooterEncoder.getRate(), SHOT_VELOCITY)
+ m_ff.calculate(SHOT_VELOCITY)));
// if not, stop
shootTrigger.ifHigh(m_shooter::stopMotor);
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));
// when we stop being at the target velocity, it means the ball was shot
atTargetVelocity
.falling()
// so stop the kicker
.ifHigh(m_kicker::stopMotor);
}
@Override
public void robotPeriodic() {
// poll all the bindings
m_loop.poll();
}
}

View File

@@ -89,6 +89,17 @@
"mainclass": "Main",
"commandversion": 2
},
{
"name": "EventLoop",
"description": "Demonstrate managing a ball system using EventLoop and BooleanEvent.",
"tags": [
"EventLoop"
],
"foldername": "eventloop",
"gradlebase": "java",
"mainclass": "Main",
"commandversion": 2
},
{
"name": "Relay",
"description": "Demonstrate controlling a Relay from Joystick buttons.",