001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.opmode;
006
007import static org.wpilib.units.Units.Seconds;
008
009import java.util.PriorityQueue;
010import org.wpilib.driverstation.DriverStation;
011import org.wpilib.hardware.hal.ControlWord;
012import org.wpilib.hardware.hal.DriverStationJNI;
013import org.wpilib.hardware.hal.HAL;
014import org.wpilib.hardware.hal.NotifierJNI;
015import org.wpilib.networktables.NetworkTableInstance;
016import org.wpilib.smartdashboard.SmartDashboard;
017import org.wpilib.system.RobotController;
018import org.wpilib.system.Watchdog;
019import org.wpilib.units.measure.Time;
020import org.wpilib.util.WPIUtilJNI;
021
022/**
023 * An opmode structure for periodic operation. This base class implements a loop that runs one or
024 * more functions periodically (on a set time interval aka loop period). The primary periodic
025 * callback function is the abstract periodic() function; the time interval for this callback is 20
026 * ms by default, but may be changed via passing a different time interval to the constructor.
027 * Additional periodic callbacks with different intervals can be added using the addPeriodic() set
028 * of functions.
029 *
030 * <p>Lifecycle:
031 *
032 * <ul>
033 *   <li>constructed when opmode selected on driver station
034 *   <li>disabledPeriodic() called periodically as long as DS is disabled. Note this is not called
035 *       on a set time interval (it does not use the same time interval as periodic())
036 *   <li>when DS transitions from disabled to enabled, start() is called once
037 *   <li>while DS is enabled, periodic() is called periodically on the time interval set by the
038 *       constructor, and additional periodic callbacks added via addPeriodic() are called
039 *       periodically on their set time intervals
040 *   <li>when DS transitions from enabled to disabled, or a different opmode is selected on the
041 *       driver station when the DS is enabled, end() is called, followed by close(); the object is
042 *       not reused
043 *   <li>if a different opmode is selected on the driver station when the DS is disabled, only
044 *       close() is called; the object is not reused
045 * </ul>
046 */
047public abstract class PeriodicOpMode implements OpMode {
048  @SuppressWarnings("MemberName")
049  static class Callback implements Comparable<Callback> {
050    public Runnable func;
051    public long period;
052    public long expirationTime;
053
054    /**
055     * Construct a callback container.
056     *
057     * @param func The callback to run.
058     * @param startTime The common starting point for all callback scheduling in microseconds.
059     * @param period The period at which to run the callback in microseconds.
060     * @param offset The offset from the common starting time in microseconds.
061     */
062    Callback(Runnable func, long startTime, long period, long offset) {
063      this.func = func;
064      this.period = period;
065      this.expirationTime =
066          startTime
067              + offset
068              + this.period
069              + (RobotController.getFPGATime() - startTime) / this.period * this.period;
070    }
071
072    @Override
073    public boolean equals(Object rhs) {
074      return rhs instanceof Callback callback && expirationTime == callback.expirationTime;
075    }
076
077    @Override
078    public int hashCode() {
079      return Long.hashCode(expirationTime);
080    }
081
082    @Override
083    public int compareTo(Callback rhs) {
084      // Elements with sooner expiration times are sorted as lesser. The head of
085      // Java's PriorityQueue is the least element.
086      return Long.compare(expirationTime, rhs.expirationTime);
087    }
088  }
089
090  /** Default loop period. */
091  public static final double kDefaultPeriod = 0.02;
092
093  // The C pointer to the notifier object. We don't use it directly, it is
094  // just passed to the JNI bindings.
095  private int m_notifier = NotifierJNI.createNotifier();
096
097  private long m_startTimeUs;
098  private long m_loopStartTimeUs;
099
100  private final ControlWord m_word = new ControlWord();
101  private final double m_period;
102  private final Watchdog m_watchdog;
103
104  private long m_opModeId;
105  private boolean m_running = true;
106
107  private final PriorityQueue<Callback> m_callbacks = new PriorityQueue<>();
108
109  /**
110   * Constructor. Periodic opmodes may specify the period used for the periodic() function; the
111   * no-argument constructor uses a default period of 20 ms.
112   */
113  protected PeriodicOpMode() {
114    this(kDefaultPeriod);
115  }
116
117  /**
118   * Constructor. Periodic opmodes may specify the period used for the periodic() function.
119   *
120   * @param period period (in seconds) for callbacks to the periodic() function
121   */
122  protected PeriodicOpMode(double period) {
123    m_startTimeUs = RobotController.getFPGATime();
124    m_period = period;
125    m_watchdog = new Watchdog(period, this::printLoopOverrunMessage);
126
127    addPeriodic(this::loopFunc, period);
128    NotifierJNI.setNotifierName(m_notifier, "PeriodicOpMode");
129
130    HAL.reportUsage("OpMode", "PeriodicOpMode");
131  }
132
133  /** Called periodically while the opmode is selected on the DS (robot is disabled). */
134  @Override
135  public void disabledPeriodic() {}
136
137  /**
138   * Called when the opmode is de-selected on the DS. The object is not reused even if the same
139   * opmode is selected again (a new object will be created).
140   */
141  public void close() {}
142
143  /**
144   * Called a single time when the robot transitions from disabled to enabled. This is called prior
145   * to periodic() being called.
146   */
147  public void start() {}
148
149  /** Called periodically while the robot is enabled. */
150  public abstract void periodic();
151
152  /**
153   * Called a single time when the robot transitions from enabled to disabled, or just before
154   * close() is called if a different opmode is selected while the robot is enabled.
155   */
156  public void end() {}
157
158  /**
159   * Return the system clock time in micrseconds for the start of the current periodic loop. This is
160   * in the same time base as Timer.getFPGATimestamp(), but is stable through a loop. It is updated
161   * at the beginning of every periodic callback (including the normal periodic loop).
162   *
163   * @return Robot running time in microseconds, as of the start of the current periodic function.
164   */
165  public long getLoopStartTime() {
166    return m_loopStartTimeUs;
167  }
168
169  /**
170   * Add a callback to run at a specific period.
171   *
172   * <p>This is scheduled on the same Notifier as periodic(), so periodic() and the callback run
173   * synchronously. Interactions between them are thread-safe.
174   *
175   * @param callback The callback to run.
176   * @param period The period at which to run the callback in seconds.
177   */
178  public final void addPeriodic(Runnable callback, double period) {
179    m_callbacks.add(new Callback(callback, m_startTimeUs, (long) (period * 1e6), 0));
180  }
181
182  /**
183   * Add a callback to run at a specific period with a starting time offset.
184   *
185   * <p>This is scheduled on the same Notifier as periodic(), so periodic() and the callback run
186   * synchronously. Interactions between them are thread-safe.
187   *
188   * @param callback The callback to run.
189   * @param period The period at which to run the callback in seconds.
190   * @param offset The offset from the common starting time in seconds. This is useful for
191   *     scheduling a callback in a different timeslot relative to PeriodicOpMode.
192   */
193  public final void addPeriodic(Runnable callback, double period, double offset) {
194    m_callbacks.add(
195        new Callback(callback, m_startTimeUs, (long) (period * 1e6), (long) (offset * 1e6)));
196  }
197
198  /**
199   * Add a callback to run at a specific period.
200   *
201   * <p>This is scheduled on the same Notifier as periodic(), so periodic() and the callback run
202   * synchronously. Interactions between them are thread-safe.
203   *
204   * @param callback The callback to run.
205   * @param period The period at which to run the callback.
206   */
207  public final void addPeriodic(Runnable callback, Time period) {
208    addPeriodic(callback, period.in(Seconds));
209  }
210
211  /**
212   * Add a callback to run at a specific period with a starting time offset.
213   *
214   * <p>This is scheduled on the same Notifier as periodic(), so periodic() and the callback run
215   * synchronously. Interactions between them are thread-safe.
216   *
217   * @param callback The callback to run.
218   * @param period The period at which to run the callback.
219   * @param offset The offset from the common starting time. This is useful for scheduling a
220   *     callback in a different timeslot relative to PeriodicOpMode.
221   */
222  public final void addPeriodic(Runnable callback, Time period, Time offset) {
223    addPeriodic(callback, period.in(Seconds), offset.in(Seconds));
224  }
225
226  /**
227   * Gets time period between calls to Periodic() functions.
228   *
229   * @return The time period between calls to Periodic() functions.
230   */
231  public double getPeriod() {
232    return m_period;
233  }
234
235  /** Loop function. */
236  protected void loopFunc() {
237    DriverStation.refreshData();
238    DriverStation.refreshControlWordFromCache(m_word);
239    m_word.setOpModeId(m_opModeId);
240    DriverStationJNI.observeUserProgram(m_word.getNative());
241
242    if (!DriverStation.isEnabled() || DriverStation.getOpModeId() != m_opModeId) {
243      m_running = false;
244      return;
245    }
246
247    m_watchdog.reset();
248    periodic();
249    m_watchdog.addEpoch("periodic()");
250
251    SmartDashboard.updateValues();
252    m_watchdog.addEpoch("SmartDashboard.updateValues()");
253
254    // if (isSimulation()) {
255    //  HAL.simPeriodicBefore();
256    //  simulationPeriodic();
257    //  HAL.simPeriodicAfter();
258    //  m_watchdog.addEpoch("simulationPeriodic()");
259    // }
260
261    m_watchdog.disable();
262
263    // Flush NetworkTables
264    NetworkTableInstance.getDefault().flushLocal();
265
266    // Warn on loop time overruns
267    if (m_watchdog.isExpired()) {
268      m_watchdog.printEpochs();
269    }
270  }
271
272  // implements OpMode interface
273  @Override
274  public final void opModeRun(long opModeId) {
275    m_opModeId = opModeId;
276
277    start();
278
279    while (m_running) {
280      // We don't have to check there's an element in the queue first because
281      // there's always at least one (the constructor adds one). It's reenqueued
282      // at the end of the loop.
283      var callback = m_callbacks.poll();
284      NotifierJNI.setNotifierAlarm(m_notifier, callback.expirationTime, 0, true, true);
285
286      try {
287        WPIUtilJNI.waitForObject(m_notifier);
288      } catch (InterruptedException ex) {
289        Thread.currentThread().interrupt();
290        break;
291      }
292
293      long currentTime = RobotController.getFPGATime();
294      m_loopStartTimeUs = RobotController.getFPGATime();
295
296      callback.func.run();
297
298      // Increment the expiration time by the number of full periods it's behind
299      // plus one to avoid rapid repeat fires from a large loop overrun. We
300      // assume currentTime ≥ expirationTime rather than checking for it since
301      // the callback wouldn't be running otherwise.
302      callback.expirationTime +=
303          callback.period
304              + (currentTime - callback.expirationTime) / callback.period * callback.period;
305      m_callbacks.add(callback);
306
307      // Process all other callbacks that are ready to run
308      while (m_callbacks.peek().expirationTime <= currentTime) {
309        callback = m_callbacks.poll();
310
311        callback.func.run();
312
313        callback.expirationTime +=
314            callback.period
315                + (currentTime - callback.expirationTime) / callback.period * callback.period;
316        m_callbacks.add(callback);
317      }
318    }
319    end();
320  }
321
322  @Override
323  public final void opModeStop() {
324    NotifierJNI.destroyNotifier(m_notifier);
325    m_notifier = 0;
326  }
327
328  @Override
329  public final void opModeClose() {
330    if (m_notifier != 0) {
331      NotifierJNI.destroyNotifier(m_notifier);
332    }
333    close();
334  }
335
336  /** Prints list of epochs added so far and their times. */
337  public void printWatchdogEpochs() {
338    m_watchdog.printEpochs();
339  }
340
341  private void printLoopOverrunMessage() {
342    DriverStation.reportWarning("Loop time of " + m_period + "s overrun\n", false);
343  }
344}