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 edu.wpi.first.wpilibj;
006
007import static edu.wpi.first.units.Units.Seconds;
008
009import edu.wpi.first.hal.DriverStationJNI;
010import edu.wpi.first.hal.FRCNetComm.tInstances;
011import edu.wpi.first.hal.FRCNetComm.tResourceType;
012import edu.wpi.first.hal.HAL;
013import edu.wpi.first.hal.NotifierJNI;
014import edu.wpi.first.units.measure.Frequency;
015import edu.wpi.first.units.measure.Time;
016import java.util.PriorityQueue;
017
018/**
019 * TimedRobot implements the IterativeRobotBase robot program framework.
020 *
021 * <p>The TimedRobot class is intended to be subclassed by a user creating a robot program.
022 *
023 * <p>periodic() functions from the base class are called on an interval by a Notifier instance.
024 */
025public class TimedRobot extends IterativeRobotBase {
026  @SuppressWarnings("MemberName")
027  static class Callback implements Comparable<Callback> {
028    public Runnable func;
029    public long period;
030    public long expirationTime;
031
032    /**
033     * Construct a callback container.
034     *
035     * @param func The callback to run.
036     * @param startTimeUs The common starting point for all callback scheduling in microseconds.
037     * @param periodUs The period at which to run the callback in microseconds.
038     * @param offsetUs The offset from the common starting time in microseconds.
039     */
040    Callback(Runnable func, long startTimeUs, long periodUs, long offsetUs) {
041      this.func = func;
042      this.period = periodUs;
043      this.expirationTime =
044          startTimeUs
045              + offsetUs
046              + this.period
047              + (RobotController.getFPGATime() - startTimeUs) / this.period * this.period;
048    }
049
050    @Override
051    public boolean equals(Object rhs) {
052      return rhs instanceof Callback callback && expirationTime == callback.expirationTime;
053    }
054
055    @Override
056    public int hashCode() {
057      return Long.hashCode(expirationTime);
058    }
059
060    @Override
061    public int compareTo(Callback rhs) {
062      // Elements with sooner expiration times are sorted as lesser. The head of
063      // Java's PriorityQueue is the least element.
064      return Long.compare(expirationTime, rhs.expirationTime);
065    }
066  }
067
068  /** Default loop period. */
069  public static final double kDefaultPeriod = 0.02;
070
071  // The C pointer to the notifier object. We don't use it directly, it is
072  // just passed to the JNI bindings.
073  private final int m_notifier = NotifierJNI.initializeNotifier();
074
075  private long m_startTimeUs;
076  private long m_loopStartTimeUs;
077
078  private final PriorityQueue<Callback> m_callbacks = new PriorityQueue<>();
079
080  /** Constructor for TimedRobot. */
081  protected TimedRobot() {
082    this(kDefaultPeriod);
083  }
084
085  /**
086   * Constructor for TimedRobot.
087   *
088   * @param period The period of the robot loop function.
089   */
090  protected TimedRobot(double period) {
091    super(period);
092    m_startTimeUs = RobotController.getFPGATime();
093    addPeriodic(this::loopFunc, period);
094    NotifierJNI.setNotifierName(m_notifier, "TimedRobot");
095
096    HAL.report(tResourceType.kResourceType_Framework, tInstances.kFramework_Timed);
097  }
098
099  /**
100   * Constructor for TimedRobot.
101   *
102   * @param period The period of the robot loop function.
103   */
104  protected TimedRobot(Time period) {
105    this(period.in(Seconds));
106  }
107
108  /**
109   * Constructor for TimedRobot.
110   *
111   * @param frequency The frequency of the robot loop function.
112   */
113  protected TimedRobot(Frequency frequency) {
114    this(frequency.asPeriod());
115  }
116
117  @Override
118  public void close() {
119    NotifierJNI.stopNotifier(m_notifier);
120    NotifierJNI.cleanNotifier(m_notifier);
121  }
122
123  /** Provide an alternate "main loop" via startCompetition(). */
124  @Override
125  public void startCompetition() {
126    robotInit();
127
128    if (isSimulation()) {
129      simulationInit();
130    }
131
132    // Tell the DS that the robot is ready to be enabled
133    System.out.println("********** Robot program startup complete **********");
134    DriverStationJNI.observeUserProgramStarting();
135
136    // Loop forever, calling the appropriate mode-dependent function
137    while (true) {
138      // We don't have to check there's an element in the queue first because
139      // there's always at least one (the constructor adds one). It's reenqueued
140      // at the end of the loop.
141      var callback = m_callbacks.poll();
142
143      NotifierJNI.updateNotifierAlarm(m_notifier, callback.expirationTime);
144
145      long currentTime = NotifierJNI.waitForNotifierAlarm(m_notifier);
146      if (currentTime == 0) {
147        break;
148      }
149
150      m_loopStartTimeUs = RobotController.getFPGATime();
151
152      callback.func.run();
153
154      // Increment the expiration time by the number of full periods it's behind
155      // plus one to avoid rapid repeat fires from a large loop overrun. We
156      // assume currentTime ≥ expirationTime rather than checking for it since
157      // the callback wouldn't be running otherwise.
158      callback.expirationTime +=
159          callback.period
160              + (currentTime - callback.expirationTime) / callback.period * callback.period;
161      m_callbacks.add(callback);
162
163      // Process all other callbacks that are ready to run
164      while (m_callbacks.peek().expirationTime <= currentTime) {
165        callback = m_callbacks.poll();
166
167        callback.func.run();
168
169        callback.expirationTime +=
170            callback.period
171                + (currentTime - callback.expirationTime) / callback.period * callback.period;
172        m_callbacks.add(callback);
173      }
174    }
175  }
176
177  /** Ends the main loop in startCompetition(). */
178  @Override
179  public void endCompetition() {
180    NotifierJNI.stopNotifier(m_notifier);
181  }
182
183  /**
184   * Return the system clock time in micrseconds for the start of the current periodic loop. This is
185   * in the same time base as Timer.getFPGATimestamp(), but is stable through a loop. It is updated
186   * at the beginning of every periodic callback (including the normal periodic loop).
187   *
188   * @return Robot running time in microseconds, as of the start of the current periodic function.
189   */
190  public long getLoopStartTime() {
191    return m_loopStartTimeUs;
192  }
193
194  /**
195   * Add a callback to run at a specific period.
196   *
197   * <p>This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run
198   * synchronously. Interactions between them are thread-safe.
199   *
200   * @param callback The callback to run.
201   * @param periodSeconds The period at which to run the callback in seconds.
202   */
203  public final void addPeriodic(Runnable callback, double periodSeconds) {
204    m_callbacks.add(new Callback(callback, m_startTimeUs, (long) (periodSeconds * 1e6), 0));
205  }
206
207  /**
208   * Add a callback to run at a specific period with a starting time offset.
209   *
210   * <p>This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run
211   * synchronously. Interactions between them are thread-safe.
212   *
213   * @param callback The callback to run.
214   * @param periodSeconds The period at which to run the callback in seconds.
215   * @param offsetSeconds The offset from the common starting time in seconds. This is useful for
216   *     scheduling a callback in a different timeslot relative to TimedRobot.
217   */
218  public final void addPeriodic(Runnable callback, double periodSeconds, double offsetSeconds) {
219    m_callbacks.add(
220        new Callback(
221            callback, m_startTimeUs, (long) (periodSeconds * 1e6), (long) (offsetSeconds * 1e6)));
222  }
223
224  /**
225   * Add a callback to run at a specific period.
226   *
227   * <p>This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run
228   * synchronously. Interactions between them are thread-safe.
229   *
230   * @param callback The callback to run.
231   * @param period The period at which to run the callback.
232   */
233  public final void addPeriodic(Runnable callback, Time period) {
234    addPeriodic(callback, period.in(Seconds));
235  }
236
237  /**
238   * Add a callback to run at a specific period with a starting time offset.
239   *
240   * <p>This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run
241   * synchronously. Interactions between them are thread-safe.
242   *
243   * @param callback The callback to run.
244   * @param period The period at which to run the callback.
245   * @param offset The offset from the common starting time. This is useful for scheduling a
246   *     callback in a different timeslot relative to TimedRobot.
247   */
248  public final void addPeriodic(Runnable callback, Time period, Time offset) {
249    addPeriodic(callback, period.in(Seconds), offset.in(Seconds));
250  }
251}