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