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  private static void updateAlarm() {
212    if (m_watchdogs.isEmpty()) {
213      NotifierJNI.cancelNotifierAlarm(m_notifier);
214    } else {
215      NotifierJNI.updateNotifierAlarm(
216          m_notifier, (long) (m_watchdogs.peek().m_expirationTimeSeconds * 1e6));
217    }
218  }
219
220  private static Thread startDaemonThread(Runnable target) {
221    Thread inst = new Thread(target);
222    inst.setDaemon(true);
223    inst.start();
224    return inst;
225  }
226
227  private static void schedulerFunc() {
228    while (!Thread.currentThread().isInterrupted()) {
229      long curTime = NotifierJNI.waitForNotifierAlarm(m_notifier);
230      if (curTime == 0) {
231        break;
232      }
233
234      m_queueMutex.lock();
235      try {
236        if (m_watchdogs.isEmpty()) {
237          continue;
238        }
239
240        // If the condition variable timed out, that means a Watchdog timeout
241        // has occurred, so call its timeout function.
242        Watchdog watchdog = m_watchdogs.poll();
243
244        double now = curTime * 1e-6;
245        if (now - watchdog.m_lastTimeoutPrintSeconds > kMinPrintPeriodMicroS) {
246          watchdog.m_lastTimeoutPrintSeconds = now;
247          if (!watchdog.m_suppressTimeoutMessage) {
248            DriverStation.reportWarning(
249                String.format("Watchdog not fed within %.6fs\n", watchdog.m_timeoutSeconds), false);
250          }
251        }
252
253        // Set expiration flag before calling the callback so any
254        // manipulation of the flag in the callback (e.g., calling
255        // Disable()) isn't clobbered.
256        watchdog.m_isExpired = true;
257
258        m_queueMutex.unlock();
259        watchdog.m_callback.run();
260        m_queueMutex.lock();
261
262        updateAlarm();
263      } finally {
264        m_queueMutex.unlock();
265      }
266    }
267  }
268}