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