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.internal;
006
007import java.util.Collection;
008import java.util.PriorityQueue;
009import org.wpilib.hardware.hal.NotifierJNI;
010import org.wpilib.system.RobotController;
011import org.wpilib.util.WPIUtilJNI;
012
013/**
014 * A priority queue for scheduling periodic callbacks based on their next execution time.
015 *
016 * <p>This class manages a collection of periodic callbacks that execute at specified intervals.
017 * Callbacks are scheduled using monotonic timestamps and automatically rescheduled after execution
018 * to maintain their periodic behavior. The queue uses a priority heap to efficiently determine the
019 * next callback to execute.
020 *
021 * <p>This is an internal scheduling primitive used by robot frameworks like TimedRobot.
022 */
023public class PeriodicPriorityQueue {
024  /** Internal priority queue ordered by callback expiration times. */
025  private final PriorityQueue<Callback> m_queue;
026
027  private long m_loopStartTimeMicros;
028
029  /** Constructs an empty callback queue. */
030  public PeriodicPriorityQueue() {
031    m_queue = new PriorityQueue<>();
032  }
033
034  /**
035   * Adds a periodic callback to the queue with a specified start time.
036   *
037   * @param func The function to call periodically.
038   * @param timestamp The common starting point for callback scheduling in monotonic timestamp
039   *     microseconds.
040   * @param periodSeconds The callback period in seconds.
041   * @param offsetSeconds The offset from the common starting time in seconds.
042   * @return the callback object
043   */
044  public Callback add(Runnable func, long timestamp, double periodSeconds, double offsetSeconds) {
045    Callback callback = new Callback(func, timestamp, periodSeconds, offsetSeconds);
046    add(callback);
047    return callback;
048  }
049
050  /**
051   * Adds a periodic callback to the queue with a specified start time.
052   *
053   * @param func The function to call periodically.
054   * @param timestamp The common starting point for callback scheduling in monotonic timestamp
055   *     microseconds.
056   * @param periodSeconds The callback period in seconds.
057   * @return the callback object
058   */
059  public Callback add(Runnable func, long timestamp, double periodSeconds) {
060    Callback callback = new Callback(func, timestamp, periodSeconds);
061    m_queue.add(callback);
062    return callback;
063  }
064
065  /**
066   * Adds a periodic callback to the queue, starting from the current monotonic time.
067   *
068   * @param func The function to call periodically.
069   * @param periodSeconds The callback period in seconds.
070   * @param offsetSeconds The offset from the current monotonic time in seconds.
071   * @return the callback object
072   */
073  public Callback add(Runnable func, double periodSeconds, double offsetSeconds) {
074    return add(func, RobotController.getMonotonicTime(), periodSeconds, offsetSeconds);
075  }
076
077  /**
078   * Adds a periodic callback to the queue, starting from the current monotonic time.
079   *
080   * @param func The function to call periodically.
081   * @param periodSeconds The callback period in seconds.
082   * @return the callback object
083   */
084  public Callback add(Runnable func, double periodSeconds) {
085    return add(func, RobotController.getMonotonicTime(), periodSeconds);
086  }
087
088  /**
089   * Adds a pre-constructed callback to the queue.
090   *
091   * @param callback The callback to add.
092   */
093  public void add(Callback callback) {
094    m_queue.add(callback);
095  }
096
097  /**
098   * Adds multiple callbacks to the queue.
099   *
100   * @param callbacks The collection of callbacks to add.
101   */
102  public void addAll(Collection<Callback> callbacks) {
103    m_queue.addAll(callbacks);
104  }
105
106  /**
107   * Removes all callbacks associated with the given function.
108   *
109   * @param func The function whose callbacks should be removed.
110   */
111  public void remove(Runnable func) {
112    m_queue.removeIf(callback -> callback.func.equals(func));
113  }
114
115  /**
116   * Removes a specific callback from the queue.
117   *
118   * @param callback The callback to remove.
119   */
120  public void remove(Callback callback) {
121    m_queue.remove(callback);
122  }
123
124  /**
125   * Removes multiple callbacks from the queue.
126   *
127   * @param callbacks The collection of callbacks to remove.
128   */
129  public void removeAll(Collection<Callback> callbacks) {
130    m_queue.removeAll(callbacks);
131  }
132
133  /** Removes all callbacks from the queue. */
134  public void clear() {
135    m_queue.clear();
136  }
137
138  /**
139   * Executes all callbacks that are due, then waits for the next callback's scheduled time.
140   *
141   * <p>This method performs the following steps:
142   *
143   * <ol>
144   *   <li>Retrieves the callback with the earliest expiration time from the queue
145   *   <li>Sets a hardware notifier alarm to wait until that callback's expiration time
146   *   <li>Blocks until the notifier signals or is interrupted
147   *   <li>Executes the callback and reschedules it for its next period
148   *   <li>Processes any additional callbacks that have become due during execution
149   * </ol>
150   *
151   * <p>When rescheduling callbacks, this method automatically compensates for execution delays by
152   * advancing the expiration time by the number of full periods that have elapsed, ensuring
153   * callbacks maintain their scheduled phase over time.
154   *
155   * @param notifier The HAL notifier handle to use for timing.
156   * @return whether the notifier was signaled before the timeout.
157   * @throws IllegalStateException if the queue is empty when this method is called.
158   */
159  public boolean runCallbacks(int notifier) {
160    var callback = m_queue.poll();
161    if (callback == null) {
162      throw new IllegalStateException(
163          "No callbacks to run! Did you make sure to call add() first?");
164    }
165
166    NotifierJNI.setNotifierAlarm(notifier, callback.expirationTime, 0, true, true);
167
168    try {
169      WPIUtilJNI.waitForObject(notifier);
170    } catch (InterruptedException ex) {
171      return false;
172    }
173
174    m_loopStartTimeMicros = RobotController.getMonotonicTime();
175
176    callback.func.run();
177
178    // Increment the expiration time by the number of full periods it's behind
179    // plus one to avoid rapid repeat fires from a large loop overrun. We
180    // assume m_loopStartTime ≥ expirationTime rather than checking for it since
181    // the callback wouldn't be running otherwise.
182    callback.expirationTime +=
183        callback.period
184            + (m_loopStartTimeMicros - callback.expirationTime) / callback.period * callback.period;
185    m_queue.add(callback);
186
187    // Process all other callbacks that are ready to run
188    while (m_queue.peek().expirationTime <= m_loopStartTimeMicros) {
189      callback = m_queue.poll();
190
191      callback.func.run();
192
193      callback.expirationTime +=
194          callback.period
195              + (m_loopStartTimeMicros - callback.expirationTime)
196                  / callback.period
197                  * callback.period;
198      m_queue.add(callback);
199    }
200
201    return true;
202  }
203
204  /**
205   * Return the system clock time in microseconds for the start of the current periodic loop. This
206   * is in the same time base as Timer.getMonotonicTimeStamp(), but is stable through a loop. It is
207   * updated at the beginning of every periodic callback (including the normal periodic loop).
208   *
209   * @return Robot running time in microseconds, as of the start of the current periodic function.
210   */
211  public long getLoopStartTime() {
212    return m_loopStartTimeMicros;
213  }
214
215  /**
216   * A periodic callback with scheduling metadata.
217   *
218   * <p>Each callback tracks its target function, period, and next expiration time. After execution,
219   * the expiration time is automatically advanced by full periods to maintain precise timing even
220   * when execution is delayed.
221   */
222  public static class Callback implements Comparable<Callback> {
223    /** The function to execute when the callback fires. */
224    public final Runnable func;
225
226    /** The period at which to run the callback in microseconds. */
227    public final long period;
228
229    /** The next scheduled execution time in monotonic timestamp microseconds. */
230    public long expirationTime;
231
232    /**
233     * Construct a callback container.
234     *
235     * @param func The callback to run.
236     * @param startTime The common starting point for all callback scheduling in microseconds.
237     * @param period The period at which to run the callback in microseconds.
238     * @param offset The offset from the common starting time in microseconds.
239     */
240    public Callback(Runnable func, long startTime, long period, long offset) {
241      this.func = func;
242      this.period = period;
243      this.expirationTime =
244          startTime
245              + offset
246              + (1 + (RobotController.getMonotonicTime() - startTime - offset) / this.period)
247                  * this.period;
248    }
249
250    /**
251     * Construct a callback container.
252     *
253     * @param func The callback to run.
254     * @param timestamp The common starting point for all callback scheduling in microseconds.
255     * @param periodSeconds The period at which to run the callback in seconds.
256     * @param offsetSeconds The offset from the common starting time in seconds.
257     */
258    public Callback(Runnable func, long timestamp, double periodSeconds, double offsetSeconds) {
259      this(func, timestamp, (long) (periodSeconds * 1e6), (long) (offsetSeconds * 1e6));
260    }
261
262    /**
263     * Construct a callback container.
264     *
265     * @param func The callback to run.
266     * @param timestamp The common starting point for all callback scheduling in microseconds.
267     * @param periodSeconds The period at which to run the callback in seconds.
268     */
269    public Callback(Runnable func, long timestamp, double periodSeconds) {
270      this(func, timestamp, (long) (periodSeconds * 1e6), 0);
271    }
272
273    /**
274     * Compares callbacks based on expiration time for equality.
275     *
276     * @param rhs The object to compare against.
277     * @return true if rhs is a Callback with the same expiration time.
278     */
279    @Override
280    public boolean equals(Object rhs) {
281      return rhs instanceof Callback callback && expirationTime == callback.expirationTime;
282    }
283
284    /**
285     * Returns a hash code based on the expiration time.
286     *
287     * @return hash code for this callback.
288     */
289    @Override
290    public int hashCode() {
291      return Long.hashCode(expirationTime);
292    }
293
294    /**
295     * Compares this callback to another based on expiration time.
296     *
297     * <p>Callbacks with earlier expiration times are considered "less than" those with later
298     * expiration times. This ordering is used by the priority queue to determine execution order.
299     *
300     * @param rhs The callback to compare to.
301     * @return negative if this expires before rhs, positive if after, zero if equal.
302     */
303    @Override
304    public int compareTo(Callback rhs) {
305      // Elements with sooner expiration times are sorted as lesser. The head of
306      // Java's PriorityQueue is the least element.
307      return Long.compare(expirationTime, rhs.expirationTime);
308    }
309  }
310}