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}