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}