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}