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    return obj instanceof Watchdog watchdog
066        && Double.compare(m_expirationTimeSeconds, watchdog.m_expirationTimeSeconds) == 0;
067  }
068
069  @Override
070  public int hashCode() {
071    return Double.hashCode(m_expirationTimeSeconds);
072  }
073
074  @Override
075  public int compareTo(Watchdog rhs) {
076    // Elements with sooner expiration times are sorted as lesser. The head of
077    // Java's PriorityQueue is the least element.
078    return Double.compare(m_expirationTimeSeconds, rhs.m_expirationTimeSeconds);
079  }
080
081  /**
082   * Returns the time in seconds since the watchdog was last fed.
083   *
084   * @return The time in seconds since the watchdog was last fed.
085   */
086  public double getTime() {
087    return Timer.getFPGATimestamp() - m_startTimeSeconds;
088  }
089
090  /**
091   * Sets the watchdog's timeout.
092   *
093   * @param timeoutSeconds The watchdog's timeout in seconds with microsecond resolution.
094   */
095  public void setTimeout(double timeoutSeconds) {
096    m_startTimeSeconds = Timer.getFPGATimestamp();
097    m_tracer.clearEpochs();
098
099    m_queueMutex.lock();
100    try {
101      m_timeoutSeconds = timeoutSeconds;
102      m_isExpired = false;
103
104      m_watchdogs.remove(this);
105      m_expirationTimeSeconds = m_startTimeSeconds + m_timeoutSeconds;
106      m_watchdogs.add(this);
107      updateAlarm();
108    } finally {
109      m_queueMutex.unlock();
110    }
111  }
112
113  /**
114   * Returns the watchdog's timeout in seconds.
115   *
116   * @return The watchdog's timeout in seconds.
117   */
118  public double getTimeout() {
119    m_queueMutex.lock();
120    try {
121      return m_timeoutSeconds;
122    } finally {
123      m_queueMutex.unlock();
124    }
125  }
126
127  /**
128   * Returns true if the watchdog timer has expired.
129   *
130   * @return True if the watchdog timer has expired.
131   */
132  public boolean isExpired() {
133    m_queueMutex.lock();
134    try {
135      return m_isExpired;
136    } finally {
137      m_queueMutex.unlock();
138    }
139  }
140
141  /**
142   * Adds time since last epoch to the list printed by printEpochs().
143   *
144   * @see Tracer#addEpoch(String)
145   * @param epochName The name to associate with the epoch.
146   */
147  public void addEpoch(String epochName) {
148    m_tracer.addEpoch(epochName);
149  }
150
151  /**
152   * Prints list of epochs added so far and their times.
153   *
154   * @see Tracer#printEpochs()
155   */
156  public void printEpochs() {
157    m_tracer.printEpochs();
158  }
159
160  /**
161   * Resets the watchdog timer.
162   *
163   * <p>This also enables the timer if it was previously disabled.
164   */
165  public void reset() {
166    enable();
167  }
168
169  /** Enables the watchdog timer. */
170  public void enable() {
171    m_startTimeSeconds = Timer.getFPGATimestamp();
172    m_tracer.clearEpochs();
173
174    m_queueMutex.lock();
175    try {
176      m_isExpired = false;
177
178      m_watchdogs.remove(this);
179      m_expirationTimeSeconds = m_startTimeSeconds + m_timeoutSeconds;
180      m_watchdogs.add(this);
181      updateAlarm();
182    } finally {
183      m_queueMutex.unlock();
184    }
185  }
186
187  /** Disables the watchdog timer. */
188  public void disable() {
189    m_queueMutex.lock();
190    try {
191      m_watchdogs.remove(this);
192      updateAlarm();
193    } finally {
194      m_queueMutex.unlock();
195    }
196  }
197
198  /**
199   * Enable or disable suppression of the generic timeout message.
200   *
201   * <p>This may be desirable if the user-provided callback already prints a more specific message.
202   *
203   * @param suppress Whether to suppress generic timeout message.
204   */
205  public void suppressTimeoutMessage(boolean suppress) {
206    m_suppressTimeoutMessage = suppress;
207  }
208
209  @SuppressWarnings("resource")
210  private static void updateAlarm() {
211    if (m_watchdogs.isEmpty()) {
212      NotifierJNI.cancelNotifierAlarm(m_notifier);
213    } else {
214      NotifierJNI.updateNotifierAlarm(
215          m_notifier, (long) (m_watchdogs.peek().m_expirationTimeSeconds * 1e6));
216    }
217  }
218
219  private static Thread startDaemonThread(Runnable target) {
220    Thread inst = new Thread(target);
221    inst.setDaemon(true);
222    inst.start();
223    return inst;
224  }
225
226  private static void schedulerFunc() {
227    while (!Thread.currentThread().isInterrupted()) {
228      long curTime = NotifierJNI.waitForNotifierAlarm(m_notifier);
229      if (curTime == 0) {
230        break;
231      }
232
233      m_queueMutex.lock();
234      try {
235        if (m_watchdogs.isEmpty()) {
236          continue;
237        }
238
239        // If the condition variable timed out, that means a Watchdog timeout
240        // has occurred, so call its timeout function.
241        Watchdog watchdog = m_watchdogs.poll();
242
243        double now = curTime * 1e-6;
244        if (now - watchdog.m_lastTimeoutPrintSeconds > kMinPrintPeriodMicroS) {
245          watchdog.m_lastTimeoutPrintSeconds = now;
246          if (!watchdog.m_suppressTimeoutMessage) {
247            DriverStation.reportWarning(
248                String.format("Watchdog not fed within %.6fs\n", watchdog.m_timeoutSeconds), false);
249          }
250        }
251
252        // Set expiration flag before calling the callback so any
253        // manipulation of the flag in the callback (e.g., calling
254        // Disable()) isn't clobbered.
255        watchdog.m_isExpired = true;
256
257        m_queueMutex.unlock();
258        watchdog.m_callback.run();
259        m_queueMutex.lock();
260
261        updateAlarm();
262      } finally {
263        m_queueMutex.unlock();
264      }
265    }
266  }
267}