diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SelfCancellableRunnable.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SelfCancellableRunnable.java new file mode 100644 index 00000000000..bbf3bb02224 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SelfCancellableRunnable.java @@ -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; + +import edu.wpi.first.units.measure.Time; +import edu.wpi.first.wpilibj.TimedRobot.Cancellable; + +public abstract class SelfCancellableRunnable implements Runnable { + + private Cancellable m_cancelThisCallback; + + /** + * Schedule this {@link SelfCancellableRunnable} to run every {@code periodSeconds} seconds on the + * specified {@code robot} + * + *
This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run + * synchronously. Interactions between them are thread-safe. + * + * @param robot The {@link TimedRobot} to run this {@link SelfCancellableRunnable} {@code robot} + * must not be {@code null}. + * @param periodSeconds The period at which to run the callback in seconds. + * @return a {@link TimedRobot.Cancellable} that allows the user to cancel periodic invocation. + * @see TimedRobot#addPeriodic(Runnable, double) + */ + public Cancellable schedulePeriodic(TimedRobot robot, double periodSeconds) { + return m_cancelThisCallback = robot.addPeriodic(this, periodSeconds); + } + + /** + * Schedule this {@link SelfCancellableRunnable} to run every {@code periodSeconds} on {@code + * robot}. Offset invocation by {@code offsetSeconds} seconds. + * + *
This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run + * synchronously. Interactions between them are thread-safe. + * + * @param robot The {@link TimedRobot} to run this {@link SelfCancellableRunnable} {@code robot} + * must not be {@code null}. + * @param periodSeconds The period at which to run the callback in seconds. + * @param offsetSeconds The offset from the common starting time in seconds. This is useful for + * scheduling a callback in a different timeslot relative to TimedRobot. + * @return a {@link TimedRobot.Cancellable} that allows the user to cancel periodic invocation. + * @see TimedRobot#addPeriodic(Runnable, double, double) + */ + public final Cancellable schedulePeriodic( + TimedRobot robot, double periodSeconds, double offsetSeconds) { + return m_cancelThisCallback = robot.addPeriodic(this, periodSeconds, offsetSeconds); + } + + /** + * Schedule this {@link SelfCancellableRunnable} to run periodically on the specified {@link + * TimedRobot} at the specified {@code period}. + * + *
This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run + * synchronously. Interactions between them are thread-safe. + * + * @param robot The {@link TimedRobot} to run this {@link SelfCancellableRunnable} {@code robot} + * must not be {@code null}. + * @param period The period at which to run the callback. + * @return A {@link Cancellable} that allows the user to cancel periodic invocation. + * @see TimedRobot#addPeriodic(Runnable, Time) + */ + public final Cancellable schedulePeriodic(TimedRobot robot, Time period) { + return m_cancelThisCallback = robot.addPeriodic(this, period); + } + + /** + * Schedule this {@link SelfCancellableRunnable} to run periodically at the specified {@code + * period} on the specified {@code robot}. Offset invocation by the specified {@code offset}. + * + *
This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run + * synchronously. Interactions between them are thread-safe. + * + * @param robot The {@link TimedRobot} to run this {@link SelfCancellableRunnable} {@code robot} + * must not be {@code null}. + * @param period The period at which to run the callback. + * @param offset The offset from the common starting time in seconds. This is useful for + * scheduling a callback in a different timeslot relative to TimedRobot. + * @return A {@link Cancellable} that allows the user to cancel periodic invocation. + * @see TimedRobot#addPeriodic(Runnable, Time, Time) + */ + public final Cancellable schedulePeriodic(TimedRobot robot, Time period, Time offset) { + return m_cancelThisCallback = robot.addPeriodic(this, period, offset); + } + + /** Cancel periodic invocation of this {@link SelfCancellableRunnable}. */ + protected void cancel() { + m_cancelThisCallback.cancel(); + } +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java index 7b59f528a6b..59c0c074e2c 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java @@ -20,24 +20,74 @@ *
The TimedRobot class is intended to be subclassed by a user creating a robot program. * *
periodic() functions from the base class are called on an interval by a Notifier instance. + * + *
The addOneShot() function enqueues an action to be run after a specified fixed delay.
*/
public class TimedRobot extends IterativeRobotBase {
+
+ /** Callback schedule types. */
+ enum CallbackScheduleType {
+ /** Run the callback indefinitely at a preset interval. */
+ PERIODIC {
+ @Override
+ boolean invoke(Runnable action) {
+ action.run();
+ return true;
+ }
+ },
+ /** Run the callback only once. */
+ ONE_SHOT {
+ @Override
+ boolean invoke(Runnable action) {
+ action.run();
+ return false;
+ }
+ },
+ /** Callback is cancelled. Don't run it at all. */
+ CANCELLED {
+ @Override
+ boolean invoke(Runnable action) {
+ return false;
+ }
+ };
+
+ /**
+ * Invoke the specified {@code action} if it should run.
+ *
+ * @param action action to invoke, provided that it should run.
+ * @return {@code true} if the action should be rescheduled, {@code false} otherwise.
+ */
+ abstract boolean invoke(Runnable action);
+ }
+
+ public interface Cancellable {
+ void cancel();
+ }
+
+ /** Holds and invokes the action (a {@link Runnable}) to invoke at a set time. */
@SuppressWarnings("MemberName")
- static class Callback implements Comparable TODO(emintz): move TimedRobot.MockRobot into its own file.
+ */
+class SelfCancellableRunnableTest {
+
+ static class MockRobot extends TimedRobot {
+ static final double kPeriod = 0.02;
+
+ public final AtomicInteger m_robotInitCount = new AtomicInteger(0);
+ public final AtomicInteger m_simulationInitCount = new AtomicInteger(0);
+ public final AtomicInteger m_disabledInitCount = new AtomicInteger(0);
+ public final AtomicInteger m_autonomousInitCount = new AtomicInteger(0);
+ public final AtomicInteger m_teleopInitCount = new AtomicInteger(0);
+ public final AtomicInteger m_testInitCount = new AtomicInteger(0);
+
+ public final AtomicInteger m_robotPeriodicCount = new AtomicInteger(0);
+ public final AtomicInteger m_simulationPeriodicCount = new AtomicInteger(0);
+ public final AtomicInteger m_disabledPeriodicCount = new AtomicInteger(0);
+ public final AtomicInteger m_autonomousPeriodicCount = new AtomicInteger(0);
+ public final AtomicInteger m_teleopPeriodicCount = new AtomicInteger(0);
+ public final AtomicInteger m_testPeriodicCount = new AtomicInteger(0);
+
+ public final AtomicInteger m_disabledExitCount = new AtomicInteger(0);
+ public final AtomicInteger m_autonomousExitCount = new AtomicInteger(0);
+ public final AtomicInteger m_teleopExitCount = new AtomicInteger(0);
+ public final AtomicInteger m_testExitCount = new AtomicInteger(0);
+
+ MockRobot() {
+ super(kPeriod);
+
+ m_robotInitCount.addAndGet(1);
+ }
+
+ @Override
+ public void simulationInit() {
+ m_simulationInitCount.addAndGet(1);
+ }
+
+ @Override
+ public void disabledInit() {
+ m_disabledInitCount.addAndGet(1);
+ }
+
+ @Override
+ public void autonomousInit() {
+ m_autonomousInitCount.addAndGet(1);
+ }
+
+ @Override
+ public void teleopInit() {
+ m_teleopInitCount.addAndGet(1);
+ }
+
+ @Override
+ public void testInit() {
+ m_testInitCount.addAndGet(1);
+ }
+
+ @Override
+ public void robotPeriodic() {
+ m_robotPeriodicCount.addAndGet(1);
+ }
+
+ @Override
+ public void simulationPeriodic() {
+ m_simulationPeriodicCount.addAndGet(1);
+ }
+
+ @Override
+ public void disabledPeriodic() {
+ m_disabledPeriodicCount.addAndGet(1);
+ }
+
+ @Override
+ public void autonomousPeriodic() {
+ m_autonomousPeriodicCount.addAndGet(1);
+ }
+
+ @Override
+ public void teleopPeriodic() {
+ m_teleopPeriodicCount.addAndGet(1);
+ }
+
+ @Override
+ public void testPeriodic() {
+ m_testPeriodicCount.addAndGet(1);
+ }
+
+ @Override
+ public void disabledExit() {
+ m_disabledExitCount.addAndGet(1);
+ }
+
+ @Override
+ public void autonomousExit() {
+ m_autonomousExitCount.addAndGet(1);
+ }
+
+ @Override
+ public void teleopExit() {
+ m_teleopExitCount.addAndGet(1);
+ }
+
+ @Override
+ public void testExit() {
+ m_testExitCount.addAndGet(1);
+ }
+
+ public static double period() {
+ return kPeriod;
+ }
+ }
+
+ private static final class DummySelfCancellingRunnable extends SelfCancellableRunnable {
+
+ private final AtomicInteger m_invocationCount;
+ private final int m_cancelAfter;
+
+ private DummySelfCancellingRunnable(int cancelAfter) {
+ m_cancelAfter = cancelAfter;
+ m_invocationCount = new AtomicInteger(0);
+ }
+
+ @Override
+ public void run() {
+ if (m_invocationCount.get() < m_cancelAfter) {
+ m_invocationCount.addAndGet(1);
+ } else {
+ cancel();
+ }
+ }
+
+ private int invocationCount() {
+ return m_invocationCount.get();
+ }
+ }
+
+ private MockRobot m_robot;
+ private Thread m_robotThread;
+
+ @BeforeEach
+ public void setup() {
+ SimHooks.pauseTiming();
+ DriverStationSim.resetData();
+ m_robot = new MockRobot();
+ }
+
+ @AfterEach
+ public void cleanup() {
+ // Note: we use try with resources to guarantee that the robot is always
+ // closed, which means that we need an "effectively final" resource,
+ // hence the final local variable.
+ try (var robot = m_robot) {
+ m_robot = null;
+ robot.endCompetition();
+ m_robotThread.interrupt();
+ m_robotThread.join();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ } finally {
+ SimHooks.resumeTiming();
+ }
+ }
+
+ private void startSimulation() {
+ m_robotThread = new Thread(m_robot::startCompetition);
+ m_robotThread.start();
+ DriverStationSim.setEnabled(false);
+ DriverStationSim.notifyNewData();
+ SimHooks.stepTiming(0.0); // Wait for Notifiers
+ }
+
+ @Test
+ @ResourceLock("timing")
+ void schedulePeriodicPeriodSecondsNoDelayOneIteration() {
+ double timeStep = MockRobot.period() / 2.0;
+
+ DummySelfCancellingRunnable callback = new DummySelfCancellingRunnable(1);
+ assertNotNull(callback.schedulePeriodic(m_robot, timeStep));
+
+ startSimulation();
+
+ assertEquals(0, m_robot.m_disabledInitCount.get());
+ assertEquals(0, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(0, callback.invocationCount());
+
+ SimHooks.stepTiming(timeStep);
+
+ assertEquals(0, m_robot.m_disabledInitCount.get());
+ assertEquals(0, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callback.invocationCount());
+
+ SimHooks.stepTiming(timeStep);
+
+ assertEquals(1, m_robot.m_disabledInitCount.get());
+ assertEquals(1, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callback.invocationCount());
+ }
+
+ @Test
+ @ResourceLock("timing")
+ void schedulePeriodicPeriodSecondsNoDelayTwoIterations() {
+ double timeStep = MockRobot.period() / 2.0;
+
+ DummySelfCancellingRunnable callback = new DummySelfCancellingRunnable(2);
+ callback.schedulePeriodic(m_robot, timeStep);
+
+ startSimulation();
+
+ assertEquals(0, m_robot.m_disabledInitCount.get());
+ assertEquals(0, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(0, callback.invocationCount());
+
+ SimHooks.stepTiming(timeStep);
+
+ assertEquals(0, m_robot.m_disabledInitCount.get());
+ assertEquals(0, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callback.invocationCount());
+
+ SimHooks.stepTiming(timeStep);
+
+ assertEquals(1, m_robot.m_disabledInitCount.get());
+ assertEquals(1, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(2, callback.invocationCount());
+
+ SimHooks.stepTiming(timeStep);
+
+ assertEquals(1, m_robot.m_disabledInitCount.get());
+ assertEquals(1, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(2, callback.invocationCount());
+
+ SimHooks.stepTiming(timeStep);
+
+ assertEquals(1, m_robot.m_disabledInitCount.get());
+ assertEquals(2, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(2, callback.invocationCount());
+ }
+
+ @Test
+ @ResourceLock("timing")
+ void testSchedulePeriodicSecondsWithOffsetTwoIterations() {
+ final double periodSeconds = MockRobot.period() / 2.0;
+ final double offsetSeconds = MockRobot.period() / 4.0;
+ final double threeEightsSecond = MockRobot.period() * 3.0 / 8.0;
+ final double oneQuarterSecond = MockRobot.period() / 4.0;
+
+ DummySelfCancellingRunnable callback = new DummySelfCancellingRunnable(2);
+ callback.schedulePeriodic(m_robot, periodSeconds, offsetSeconds);
+
+ // Expirations in this test (ms)
+ //
+ // Let p be period in ms.
+ //
+ // Robot | Callback
+ // ================
+ // p | 0.75p
+ // 2p | 1.25p
+
+ startSimulation();
+
+ assertEquals(0, m_robot.m_disabledInitCount.get());
+ assertEquals(0, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(0, callback.invocationCount());
+
+ SimHooks.stepTiming(threeEightsSecond);
+
+ assertEquals(0, m_robot.m_disabledInitCount.get());
+ assertEquals(0, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(0, callback.invocationCount());
+
+ SimHooks.stepTiming(threeEightsSecond);
+
+ assertEquals(0, m_robot.m_disabledInitCount.get());
+ assertEquals(0, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callback.invocationCount());
+
+ SimHooks.stepTiming(oneQuarterSecond);
+
+ assertEquals(1, m_robot.m_disabledInitCount.get());
+ assertEquals(1, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callback.invocationCount());
+
+ SimHooks.stepTiming(oneQuarterSecond);
+
+ assertEquals(1, m_robot.m_disabledInitCount.get());
+ assertEquals(1, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(2, callback.invocationCount());
+
+ SimHooks.stepTiming(oneQuarterSecond);
+
+ assertEquals(1, m_robot.m_disabledInitCount.get());
+ assertEquals(1, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(2, callback.invocationCount());
+
+ SimHooks.stepTiming(oneQuarterSecond);
+
+ assertEquals(1, m_robot.m_disabledInitCount.get());
+ assertEquals(1, m_robot.m_disabledPeriodicCount.get());
+ assertEquals(2, callback.invocationCount());
+ }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java
index da0bd92bdb3..fd14bf1a30e 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java
@@ -6,6 +6,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import edu.wpi.first.wpilibj.livewindow.LiveWindow;
@@ -632,7 +633,7 @@ void addPeriodicTest() {
MockRobot robot = new MockRobot();
final AtomicInteger callbackCount = new AtomicInteger(0);
- robot.addPeriodic(() -> callbackCount.addAndGet(1), kPeriod / 2.0);
+ assertNotNull(robot.addPeriodic(() -> callbackCount.addAndGet(1), kPeriod / 2.0));
Thread robotThread = new Thread(robot::startCompetition);
robotThread.start();
@@ -667,6 +668,50 @@ void addPeriodicTest() {
robot.close();
}
+ @Test
+ @ResourceLock("timing")
+ void cancelPeriodicTest() {
+ MockRobot robot = new MockRobot();
+
+ final AtomicInteger callbackCount = new AtomicInteger(0);
+ final TimedRobot.Cancellable periodicHandle =
+ robot.addPeriodic(() -> callbackCount.addAndGet(1), kPeriod / 2.0);
+
+ Thread robotThread = new Thread(robot::startCompetition);
+ robotThread.start();
+
+ DriverStationSim.setEnabled(false);
+ DriverStationSim.notifyNewData();
+ SimHooks.stepTiming(0.0); // Wait for Notifiers
+
+ assertEquals(0, robot.m_disabledInitCount.get());
+ assertEquals(0, robot.m_disabledPeriodicCount.get());
+ assertEquals(0, callbackCount.get());
+
+ SimHooks.stepTiming(kPeriod / 2.0);
+
+ assertEquals(0, robot.m_disabledInitCount.get());
+ assertEquals(0, robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callbackCount.get());
+
+ periodicHandle.cancel();
+
+ SimHooks.stepTiming(kPeriod / 2.0);
+
+ assertEquals(1, robot.m_disabledInitCount.get());
+ assertEquals(1, robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callbackCount.get());
+
+ robot.endCompetition();
+ try {
+ robotThread.interrupt();
+ robotThread.join();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ robot.close();
+ }
+
@Test
@ResourceLock("timing")
void addPeriodicWithOffsetTest() {
@@ -728,4 +773,45 @@ void addPeriodicWithOffsetTest() {
}
robot.close();
}
+
+ @Test
+ @ResourceLock("timing")
+ void addOneShotTest() {
+ MockRobot robot = new MockRobot();
+
+ final AtomicInteger callbackCount = new AtomicInteger(0);
+ robot.addOneShot(() -> callbackCount.addAndGet(1), kPeriod / 2.0);
+
+ Thread robotThread = new Thread(robot::startCompetition);
+ robotThread.start();
+
+ DriverStationSim.setEnabled(false);
+ DriverStationSim.notifyNewData();
+ SimHooks.stepTiming(0.0); // Wait for Notifiers
+
+ assertEquals(0, robot.m_disabledInitCount.get());
+ assertEquals(0, robot.m_disabledPeriodicCount.get());
+ assertEquals(0, callbackCount.get());
+
+ SimHooks.stepTiming(kPeriod / 2.0);
+
+ assertEquals(0, robot.m_disabledInitCount.get());
+ assertEquals(0, robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callbackCount.get());
+
+ SimHooks.stepTiming(kPeriod / 2.0);
+
+ assertEquals(1, robot.m_disabledInitCount.get());
+ assertEquals(1, robot.m_disabledPeriodicCount.get());
+ assertEquals(1, callbackCount.get());
+
+ robot.endCompetition();
+ try {
+ robotThread.interrupt();
+ robotThread.join();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ robot.close();
+ }
}