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 { - public Runnable func; - public long period; - public long expirationTime; + static class Callback implements Comparable, Cancellable { + private final Runnable func; // Scheduled action + public long period; // Time between invocations of a periodic callback + public long expirationTime; // The next time to invoke the callback + public volatile CallbackScheduleType m_scheduleType; /** * Construct a callback container. * * @param func The callback to run. - * @param startTimeSeconds The common starting point for all callback scheduling in - * microseconds. - * @param periodSeconds The period at which to run the callback in microseconds. - * @param offsetSeconds The offset from the common starting time in microseconds. + * @param startTimeUs The common starting point for all callback scheduling in microseconds. + * @param periodUs The period at which to run the callback in microseconds. + * @param offsetUs The offset from the common starting time in microseconds. + * @param scheduleType How to schedule the callback. Meaningful options are periodic or + * one-shot. */ - Callback(Runnable func, long startTimeUs, long periodUs, long offsetUs) { + Callback( + Runnable func, + long startTimeUs, + long periodUs, + long offsetUs, + CallbackScheduleType scheduleType) { this.func = func; this.period = periodUs; this.expirationTime = @@ -45,6 +95,16 @@ static class Callback implements Comparable { + offsetUs + this.period + (RobotController.getFPGATime() - startTimeUs) / this.period * this.period; + m_scheduleType = scheduleType; + } + + @Override + public void cancel() { + m_scheduleType = CallbackScheduleType.CANCELLED; + } + + boolean invoke() { + return m_scheduleType.invoke(func); } @Override @@ -72,7 +132,7 @@ public int compareTo(Callback rhs) { // just passed to the JNI bindings. private final int m_notifier = NotifierJNI.initializeNotifier(); - private long m_startTimeUs; + private final long m_startTimeUs; private final PriorityQueue m_callbacks = new PriorityQueue<>(); @@ -117,9 +177,9 @@ public void startCompetition() { // Loop forever, calling the appropriate mode-dependent function while (true) { // We don't have to check there's an element in the queue first because - // there's always at least one (the constructor adds one). It's reenqueued - // at the end of the loop. - var callback = m_callbacks.poll(); + // there's always at least one (the constructor adds one). If the action + // runs periodically, it is rescheduled immediately after it runs. + var callback = m_callbacks.peek(); NotifierJNI.updateNotifierAlarm(m_notifier, callback.expirationTime); @@ -128,27 +188,17 @@ public void startCompetition() { break; } - callback.func.run(); - - // Increment the expiration time by the number of full periods it's behind - // plus one to avoid rapid repeat fires from a large loop overrun. We - // assume currentTime ≥ expirationTime rather than checking for it since - // the callback wouldn't be running otherwise. - callback.expirationTime += - callback.period - + (currentTime - callback.expirationTime) / callback.period * callback.period; - m_callbacks.add(callback); - - // Process all other callbacks that are ready to run + // Process all callbacks that are ready to run while (m_callbacks.peek().expirationTime <= currentTime) { callback = m_callbacks.poll(); - callback.func.run(); + if (callback.invoke()) { - callback.expirationTime += - callback.period - + (currentTime - callback.expirationTime) / callback.period * callback.period; - m_callbacks.add(callback); + callback.expirationTime += + callback.period + + (currentTime - callback.expirationTime) / callback.period * callback.period; + m_callbacks.add(callback); + } } } } @@ -167,9 +217,18 @@ public void endCompetition() { * * @param callback The callback to run. * @param periodSeconds The period at which to run the callback in seconds. + * @return a {@link Cancellable} that allows the user to cancel periodic invocation. */ - public final void addPeriodic(Runnable callback, double periodSeconds) { - m_callbacks.add(new Callback(callback, m_startTimeUs, (long) (periodSeconds * 1e6), 0)); + public final Cancellable addPeriodic(Runnable callback, double periodSeconds) { + Callback scheduledCallback = + new Callback( + callback, + m_startTimeUs, + (long) (periodSeconds * 1e6), + 0, + CallbackScheduleType.PERIODIC); + m_callbacks.add(scheduledCallback); + return scheduledCallback; } /** @@ -182,11 +241,19 @@ public final void addPeriodic(Runnable callback, double periodSeconds) { * @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 Cancellable} that allows the user to cancel periodic invocation. */ - public final void addPeriodic(Runnable callback, double periodSeconds, double offsetSeconds) { - m_callbacks.add( + public final Cancellable addPeriodic( + Runnable callback, double periodSeconds, double offsetSeconds) { + Callback scheduledCallback = new Callback( - callback, m_startTimeUs, (long) (periodSeconds * 1e6), (long) (offsetSeconds * 1e6))); + callback, + m_startTimeUs, + (long) (periodSeconds * 1e6), + (long) (offsetSeconds * 1e6), + CallbackScheduleType.PERIODIC); + m_callbacks.add(scheduledCallback); + return scheduledCallback; } /** @@ -197,9 +264,10 @@ public final void addPeriodic(Runnable callback, double periodSeconds, double of * * @param callback The callback to run. * @param period The period at which to run the callback. + * @return a {@link Cancellable} that allows the user to cancel periodic invocation. */ - public final void addPeriodic(Runnable callback, Time period) { - addPeriodic(callback, period.in(Seconds)); + public final Cancellable addPeriodic(Runnable callback, Time period) { + return addPeriodic(callback, period.in(Seconds)); } /** @@ -212,8 +280,24 @@ public final void addPeriodic(Runnable callback, Time period) { * @param period The period at which to run the callback. * @param offset The offset from the common starting time. 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. */ - public final void addPeriodic(Runnable callback, Time period, Time offset) { - addPeriodic(callback, period.in(Seconds), offset.in(Seconds)); + public final Cancellable addPeriodic(Runnable callback, Time period, Time offset) { + return addPeriodic(callback, period.in(Seconds), offset.in(Seconds)); + } + + /** + * Add a one-shot that, unless cancelled, invokes an action exactly once after a specified delay. + * + * @param callback action to run + * @param delaySeconds the number of seconds to wait before running the action + * @return a {@link Cancellable} that allows the user to cancel the invocation + */ + public final Cancellable addOneShot(Runnable callback, double delaySeconds) { + Callback scheduledCallback = + new Callback( + callback, m_startTimeUs, (long) (delaySeconds * 1e6), 0, CallbackScheduleType.ONE_SHOT); + m_callbacks.add(scheduledCallback); + return scheduledCallback; } } diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/SelfCancellableRunnableTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SelfCancellableRunnableTest.java new file mode 100644 index 00000000000..cb9f3172827 --- /dev/null +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SelfCancellableRunnableTest.java @@ -0,0 +1,320 @@ +// 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import edu.wpi.first.wpilibj.simulation.DriverStationSim; +import edu.wpi.first.wpilibj.simulation.SimHooks; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceLock; + +/** + * Validates {@link SelfCancellableRunnable} + * + *

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