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.util.ErrorMessages.requireNonNullParam;
008
009import edu.wpi.first.hal.NotifierJNI;
010import java.util.concurrent.atomic.AtomicInteger;
011import java.util.concurrent.locks.ReentrantLock;
012
013/**
014 * Notifiers run a user-provided callback function on a separate thread.
015 *
016 * <p>If startSingle() is used, the callback will run once. If startPeriodic() is used, the callback
017 * will run repeatedly with the given period until stop() is called.
018 */
019public class Notifier implements AutoCloseable {
020  // The thread waiting on the HAL alarm.
021  private Thread m_thread;
022
023  // The lock held while updating process information.
024  private final ReentrantLock m_processLock = new ReentrantLock();
025
026  // HAL handle passed to the JNI bindings (atomic for proper destruction).
027  private final AtomicInteger m_notifier = new AtomicInteger();
028
029  // The user-provided callback.
030  private Runnable m_callback;
031
032  // The time, in seconds, at which the callback should be called. Has the same
033  // zero as RobotController.getFPGATime().
034  private double m_expirationTimeSeconds;
035
036  // If periodic, stores the callback period; if single, stores the time until
037  // the callback call.
038  private double m_periodSeconds;
039
040  // True if the callback is periodic
041  private boolean m_periodic;
042
043  @Override
044  public void close() {
045    int handle = m_notifier.getAndSet(0);
046    if (handle == 0) {
047      return;
048    }
049    NotifierJNI.stopNotifier(handle);
050    // Join the thread to ensure the callback has exited.
051    if (m_thread.isAlive()) {
052      try {
053        m_thread.interrupt();
054        m_thread.join();
055      } catch (InterruptedException ex) {
056        Thread.currentThread().interrupt();
057      }
058    }
059    NotifierJNI.cleanNotifier(handle);
060    m_thread = null;
061  }
062
063  /**
064   * Update the alarm hardware to reflect the next alarm.
065   *
066   * @param triggerTimeMicroS the time in microseconds at which the next alarm will be triggered
067   */
068  private void updateAlarm(long triggerTimeMicroS) {
069    int notifier = m_notifier.get();
070    if (notifier == 0) {
071      return;
072    }
073    NotifierJNI.updateNotifierAlarm(notifier, triggerTimeMicroS);
074  }
075
076  /** Update the alarm hardware to reflect the next alarm. */
077  private void updateAlarm() {
078    updateAlarm((long) (m_expirationTimeSeconds * 1e6));
079  }
080
081  /**
082   * Create a Notifier with the given callback.
083   *
084   * <p>Configure when the callback runs with startSingle() or startPeriodic().
085   *
086   * @param callback The callback to run.
087   */
088  public Notifier(Runnable callback) {
089    requireNonNullParam(callback, "callback", "Notifier");
090
091    m_callback = callback;
092    m_notifier.set(NotifierJNI.initializeNotifier());
093
094    m_thread =
095        new Thread(
096            () -> {
097              while (!Thread.interrupted()) {
098                int notifier = m_notifier.get();
099                if (notifier == 0) {
100                  break;
101                }
102                long curTime = NotifierJNI.waitForNotifierAlarm(notifier);
103                if (curTime == 0) {
104                  break;
105                }
106
107                Runnable threadHandler;
108                m_processLock.lock();
109                try {
110                  threadHandler = m_callback;
111                  if (m_periodic) {
112                    m_expirationTimeSeconds += m_periodSeconds;
113                    updateAlarm();
114                  } else {
115                    // Need to update the alarm to cause it to wait again
116                    updateAlarm(-1);
117                  }
118                } finally {
119                  m_processLock.unlock();
120                }
121
122                // Call callback
123                if (threadHandler != null) {
124                  threadHandler.run();
125                }
126              }
127            });
128    m_thread.setName("Notifier");
129    m_thread.setDaemon(true);
130    m_thread.setUncaughtExceptionHandler(
131        (thread, error) -> {
132          Throwable cause = error.getCause();
133          if (cause != null) {
134            error = cause;
135          }
136          DriverStation.reportError(
137              "Unhandled exception in Notifier thread: " + error, error.getStackTrace());
138          DriverStation.reportError(
139              "The Runnable for this Notifier (or methods called by it) should have handled "
140                  + "the exception above.\n"
141                  + "  The above stacktrace can help determine where the error occurred.\n"
142                  + "  See https://wpilib.org/stacktrace for more information.",
143              false);
144        });
145    m_thread.start();
146  }
147
148  /**
149   * Sets the name of the notifier. Used for debugging purposes only.
150   *
151   * @param name Name
152   */
153  public void setName(String name) {
154    m_thread.setName(name);
155    NotifierJNI.setNotifierName(m_notifier.get(), name);
156  }
157
158  /**
159   * Change the callback function.
160   *
161   * @param callback The callback function.
162   * @deprecated Use setCallback() instead.
163   */
164  @Deprecated(forRemoval = true, since = "2024")
165  public void setHandler(Runnable callback) {
166    m_processLock.lock();
167    try {
168      m_callback = callback;
169    } finally {
170      m_processLock.unlock();
171    }
172  }
173
174  /**
175   * Change the callback function.
176   *
177   * @param callback The callback function.
178   */
179  public void setCallback(Runnable callback) {
180    m_processLock.lock();
181    try {
182      m_callback = callback;
183    } finally {
184      m_processLock.unlock();
185    }
186  }
187
188  /**
189   * Run the callback once after the given delay.
190   *
191   * @param delaySeconds Time in seconds to wait before the callback is called.
192   */
193  public void startSingle(double delaySeconds) {
194    m_processLock.lock();
195    try {
196      m_periodic = false;
197      m_periodSeconds = delaySeconds;
198      m_expirationTimeSeconds = RobotController.getFPGATime() * 1e-6 + delaySeconds;
199      updateAlarm();
200    } finally {
201      m_processLock.unlock();
202    }
203  }
204
205  /**
206   * Run the callback periodically with the given period.
207   *
208   * <p>The user-provided callback should be written so that it completes before the next time it's
209   * scheduled to run.
210   *
211   * @param periodSeconds Period in seconds after which to to call the callback starting one period
212   *     after the call to this method.
213   */
214  public void startPeriodic(double periodSeconds) {
215    m_processLock.lock();
216    try {
217      m_periodic = true;
218      m_periodSeconds = periodSeconds;
219      m_expirationTimeSeconds = RobotController.getFPGATime() * 1e-6 + periodSeconds;
220      updateAlarm();
221    } finally {
222      m_processLock.unlock();
223    }
224  }
225
226  /**
227   * Stop further callback invocations.
228   *
229   * <p>No further periodic callbacks will occur. Single invocations will also be cancelled if they
230   * haven't yet occurred.
231   *
232   * <p>If a callback invocation is in progress, this function will block until the callback is
233   * complete.
234   */
235  public void stop() {
236    m_processLock.lock();
237    try {
238      m_periodic = false;
239      NotifierJNI.cancelNotifierAlarm(m_notifier.get());
240    } finally {
241      m_processLock.unlock();
242    }
243  }
244
245  /**
246   * Sets the HAL notifier thread priority.
247   *
248   * <p>The HAL notifier thread is responsible for managing the FPGA's notifier interrupt and waking
249   * up user's Notifiers when it's their time to run. Giving the HAL notifier thread real-time
250   * priority helps ensure the user's real-time Notifiers, if any, are notified to run in a timely
251   * manner.
252   *
253   * @param realTime Set to true to set a real-time priority, false for standard priority.
254   * @param priority Priority to set the thread to. For real-time, this is 1-99 with 99 being
255   *     highest. For non-real-time, this is forced to 0. See "man 7 sched" for more details.
256   * @return True on success.
257   */
258  public static boolean setHALThreadPriority(boolean realTime, int priority) {
259    return NotifierJNI.setHALThreadPriority(realTime, priority);
260  }
261}