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   */
163  public void setCallback(Runnable callback) {
164    m_processLock.lock();
165    try {
166      m_callback = callback;
167    } finally {
168      m_processLock.unlock();
169    }
170  }
171
172  /**
173   * Run the callback once after the given delay.
174   *
175   * @param delaySeconds Time in seconds to wait before the callback is called.
176   */
177  public void startSingle(double delaySeconds) {
178    m_processLock.lock();
179    try {
180      m_periodic = false;
181      m_periodSeconds = delaySeconds;
182      m_expirationTimeSeconds = RobotController.getFPGATime() * 1e-6 + delaySeconds;
183      updateAlarm();
184    } finally {
185      m_processLock.unlock();
186    }
187  }
188
189  /**
190   * Run the callback periodically with the given period.
191   *
192   * <p>The user-provided callback should be written so that it completes before the next time it's
193   * scheduled to run.
194   *
195   * @param periodSeconds Period in seconds after which to to call the callback starting one period
196   *     after the call to this method.
197   */
198  public void startPeriodic(double periodSeconds) {
199    m_processLock.lock();
200    try {
201      m_periodic = true;
202      m_periodSeconds = periodSeconds;
203      m_expirationTimeSeconds = RobotController.getFPGATime() * 1e-6 + periodSeconds;
204      updateAlarm();
205    } finally {
206      m_processLock.unlock();
207    }
208  }
209
210  /**
211   * Stop further callback invocations.
212   *
213   * <p>No further periodic callbacks will occur. Single invocations will also be cancelled if they
214   * haven't yet occurred.
215   *
216   * <p>If a callback invocation is in progress, this function will block until the callback is
217   * complete.
218   */
219  public void stop() {
220    m_processLock.lock();
221    try {
222      m_periodic = false;
223      NotifierJNI.cancelNotifierAlarm(m_notifier.get());
224    } finally {
225      m_processLock.unlock();
226    }
227  }
228
229  /**
230   * Sets the HAL notifier thread priority.
231   *
232   * <p>The HAL notifier thread is responsible for managing the FPGA's notifier interrupt and waking
233   * up user's Notifiers when it's their time to run. Giving the HAL notifier thread real-time
234   * priority helps ensure the user's real-time Notifiers, if any, are notified to run in a timely
235   * manner.
236   *
237   * @param realTime Set to true to set a real-time priority, false for standard priority.
238   * @param priority Priority to set the thread to. For real-time, this is 1-99 with 99 being
239   *     highest. For non-real-time, this is forced to 0. See "man 7 sched" for more details.
240   * @return True on success.
241   */
242  public static boolean setHALThreadPriority(boolean realTime, int priority) {
243    return NotifierJNI.setHALThreadPriority(realTime, priority);
244  }
245}