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