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