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