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.NotifierJNI; 010import edu.wpi.first.units.measure.Time; 011import java.io.Closeable; 012import java.util.PriorityQueue; 013import java.util.concurrent.locks.ReentrantLock; 014 015/** 016 * A class that's a wrapper around a watchdog timer. 017 * 018 * <p>When the timer expires, a message is printed to the console and an optional user-provided 019 * callback is invoked. 020 * 021 * <p>The watchdog is initialized disabled, so the user needs to call enable() before use. 022 */ 023public class Watchdog implements Closeable, Comparable<Watchdog> { 024 // Used for timeout print rate-limiting 025 private static final long kMinPrintPeriodMicroS = (long) 1e6; 026 027 private double m_startTimeSeconds; 028 private double m_timeoutSeconds; 029 private double m_expirationTimeSeconds; 030 private final Runnable m_callback; 031 private double m_lastTimeoutPrintSeconds; 032 033 boolean m_isExpired; 034 035 boolean m_suppressTimeoutMessage; 036 037 private final Tracer m_tracer; 038 039 private static final PriorityQueue<Watchdog> m_watchdogs = new PriorityQueue<>(); 040 private static ReentrantLock m_queueMutex = new ReentrantLock(); 041 private static int m_notifier; 042 043 static { 044 m_notifier = NotifierJNI.initializeNotifier(); 045 NotifierJNI.setNotifierName(m_notifier, "Watchdog"); 046 startDaemonThread(Watchdog::schedulerFunc); 047 } 048 049 /** 050 * Watchdog constructor. 051 * 052 * @param timeoutSeconds The watchdog's timeout in seconds with microsecond resolution. 053 * @param callback This function is called when the timeout expires. 054 */ 055 public Watchdog(double timeoutSeconds, Runnable callback) { 056 m_timeoutSeconds = timeoutSeconds; 057 m_callback = callback; 058 m_tracer = new Tracer(); 059 } 060 061 /** 062 * Watchdog constructor. 063 * 064 * @param timeout The watchdog's timeout with microsecond resolution. 065 * @param callback This function is called when the timeout expires. 066 */ 067 public Watchdog(Time timeout, Runnable callback) { 068 this(timeout.in(Seconds), callback); 069 } 070 071 @Override 072 public void close() { 073 disable(); 074 } 075 076 @Override 077 public boolean equals(Object obj) { 078 return obj instanceof Watchdog watchdog 079 && Double.compare(m_expirationTimeSeconds, watchdog.m_expirationTimeSeconds) == 0; 080 } 081 082 @Override 083 public int hashCode() { 084 return Double.hashCode(m_expirationTimeSeconds); 085 } 086 087 @Override 088 public int compareTo(Watchdog rhs) { 089 // Elements with sooner expiration times are sorted as lesser. The head of 090 // Java's PriorityQueue is the least element. 091 return Double.compare(m_expirationTimeSeconds, rhs.m_expirationTimeSeconds); 092 } 093 094 /** 095 * Returns the time in seconds since the watchdog was last fed. 096 * 097 * @return The time in seconds since the watchdog was last fed. 098 */ 099 public double getTime() { 100 return Timer.getFPGATimestamp() - m_startTimeSeconds; 101 } 102 103 /** 104 * Sets the watchdog's timeout. 105 * 106 * @param timeoutSeconds The watchdog's timeout in seconds with microsecond resolution. 107 */ 108 public void setTimeout(double timeoutSeconds) { 109 m_startTimeSeconds = Timer.getFPGATimestamp(); 110 m_tracer.clearEpochs(); 111 112 m_queueMutex.lock(); 113 try { 114 m_timeoutSeconds = timeoutSeconds; 115 m_isExpired = false; 116 117 m_watchdogs.remove(this); 118 m_expirationTimeSeconds = m_startTimeSeconds + m_timeoutSeconds; 119 m_watchdogs.add(this); 120 updateAlarm(); 121 } finally { 122 m_queueMutex.unlock(); 123 } 124 } 125 126 /** 127 * Returns the watchdog's timeout in seconds. 128 * 129 * @return The watchdog's timeout in seconds. 130 */ 131 public double getTimeout() { 132 m_queueMutex.lock(); 133 try { 134 return m_timeoutSeconds; 135 } finally { 136 m_queueMutex.unlock(); 137 } 138 } 139 140 /** 141 * Returns true if the watchdog timer has expired. 142 * 143 * @return True if the watchdog timer has expired. 144 */ 145 public boolean isExpired() { 146 m_queueMutex.lock(); 147 try { 148 return m_isExpired; 149 } finally { 150 m_queueMutex.unlock(); 151 } 152 } 153 154 /** 155 * Adds time since last epoch to the list printed by printEpochs(). 156 * 157 * @see Tracer#addEpoch(String) 158 * @param epochName The name to associate with the epoch. 159 */ 160 public void addEpoch(String epochName) { 161 m_tracer.addEpoch(epochName); 162 } 163 164 /** 165 * Prints list of epochs added so far and their times. 166 * 167 * @see Tracer#printEpochs() 168 */ 169 public void printEpochs() { 170 m_tracer.printEpochs(); 171 } 172 173 /** 174 * Resets the watchdog timer. 175 * 176 * <p>This also enables the timer if it was previously disabled. 177 */ 178 public void reset() { 179 enable(); 180 } 181 182 /** Enables the watchdog timer. */ 183 public void enable() { 184 m_startTimeSeconds = Timer.getFPGATimestamp(); 185 m_tracer.clearEpochs(); 186 187 m_queueMutex.lock(); 188 try { 189 m_isExpired = false; 190 191 m_watchdogs.remove(this); 192 m_expirationTimeSeconds = m_startTimeSeconds + m_timeoutSeconds; 193 m_watchdogs.add(this); 194 updateAlarm(); 195 } finally { 196 m_queueMutex.unlock(); 197 } 198 } 199 200 /** Disables the watchdog timer. */ 201 public void disable() { 202 m_queueMutex.lock(); 203 try { 204 m_watchdogs.remove(this); 205 updateAlarm(); 206 } finally { 207 m_queueMutex.unlock(); 208 } 209 } 210 211 /** 212 * Enable or disable suppression of the generic timeout message. 213 * 214 * <p>This may be desirable if the user-provided callback already prints a more specific message. 215 * 216 * @param suppress Whether to suppress generic timeout message. 217 */ 218 public void suppressTimeoutMessage(boolean suppress) { 219 m_suppressTimeoutMessage = suppress; 220 } 221 222 @SuppressWarnings("resource") 223 private static void updateAlarm() { 224 if (m_watchdogs.isEmpty()) { 225 NotifierJNI.cancelNotifierAlarm(m_notifier); 226 } else { 227 NotifierJNI.updateNotifierAlarm( 228 m_notifier, (long) (m_watchdogs.peek().m_expirationTimeSeconds * 1e6)); 229 } 230 } 231 232 private static Thread startDaemonThread(Runnable target) { 233 Thread inst = new Thread(target); 234 inst.setDaemon(true); 235 inst.start(); 236 return inst; 237 } 238 239 private static void schedulerFunc() { 240 while (!Thread.currentThread().isInterrupted()) { 241 long curTime = NotifierJNI.waitForNotifierAlarm(m_notifier); 242 if (curTime == 0) { 243 break; 244 } 245 246 m_queueMutex.lock(); 247 try { 248 if (m_watchdogs.isEmpty()) { 249 continue; 250 } 251 252 // If the condition variable timed out, that means a Watchdog timeout 253 // has occurred, so call its timeout function. 254 Watchdog watchdog = m_watchdogs.poll(); 255 256 double now = curTime * 1e-6; 257 if (now - watchdog.m_lastTimeoutPrintSeconds > kMinPrintPeriodMicroS) { 258 watchdog.m_lastTimeoutPrintSeconds = now; 259 if (!watchdog.m_suppressTimeoutMessage) { 260 DriverStation.reportWarning( 261 String.format("Watchdog not fed within %.6fs\n", watchdog.m_timeoutSeconds), false); 262 } 263 } 264 265 // Set expiration flag before calling the callback so any 266 // manipulation of the flag in the callback (e.g., calling 267 // Disable()) isn't clobbered. 268 watchdog.m_isExpired = true; 269 270 m_queueMutex.unlock(); 271 watchdog.m_callback.run(); 272 m_queueMutex.lock(); 273 274 updateAlarm(); 275 } finally { 276 m_queueMutex.unlock(); 277 } 278 } 279 } 280}