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 callback function on a separate thread at a specified period.
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  // The lock for the process information.
023  private final ReentrantLock m_processLock = new ReentrantLock();
024  // The C pointer to the notifier object. We don't use it directly, it is
025  // just passed to the JNI bindings.
026  private final AtomicInteger m_notifier = new AtomicInteger();
027  // The time, in seconds, at which the corresponding handler should be
028  // called. Has the same zero as RobotController.getFPGATime().
029  private double m_expirationTimeSeconds;
030  // The handler passed in by the user which should be called at the
031  // appropriate interval.
032  private Runnable m_handler;
033  // Whether we are calling the handler just once or periodically.
034  private boolean m_periodic;
035  // If periodic, the period of the calling; if just once, stores how long it
036  // is until we call the handler.
037  private double m_periodSeconds;
038
039  @Override
040  public void close() {
041    int handle = m_notifier.getAndSet(0);
042    if (handle == 0) {
043      return;
044    }
045    NotifierJNI.stopNotifier(handle);
046    // Join the thread to ensure the handler has exited.
047    if (m_thread.isAlive()) {
048      try {
049        m_thread.interrupt();
050        m_thread.join();
051      } catch (InterruptedException ex) {
052        Thread.currentThread().interrupt();
053      }
054    }
055    NotifierJNI.cleanNotifier(handle);
056    m_thread = null;
057  }
058
059  /**
060   * Update the alarm hardware to reflect the next alarm.
061   *
062   * @param triggerTimeMicroS the time in microseconds at which the next alarm will be triggered
063   */
064  private void updateAlarm(long triggerTimeMicroS) {
065    int notifier = m_notifier.get();
066    if (notifier == 0) {
067      return;
068    }
069    NotifierJNI.updateNotifierAlarm(notifier, triggerTimeMicroS);
070  }
071
072  /** Update the alarm hardware to reflect the next alarm. */
073  private void updateAlarm() {
074    updateAlarm((long) (m_expirationTimeSeconds * 1e6));
075  }
076
077  /**
078   * Create a Notifier for timer event notification.
079   *
080   * @param run The handler that is called at the notification time which is set using StartSingle
081   *     or StartPeriodic.
082   */
083  public Notifier(Runnable run) {
084    requireNonNullParam(run, "run", "Notifier");
085
086    m_handler = run;
087    m_notifier.set(NotifierJNI.initializeNotifier());
088
089    m_thread =
090        new Thread(
091            () -> {
092              while (!Thread.interrupted()) {
093                int notifier = m_notifier.get();
094                if (notifier == 0) {
095                  break;
096                }
097                long curTime = NotifierJNI.waitForNotifierAlarm(notifier);
098                if (curTime == 0) {
099                  break;
100                }
101
102                Runnable handler;
103                m_processLock.lock();
104                try {
105                  handler = m_handler;
106                  if (m_periodic) {
107                    m_expirationTimeSeconds += m_periodSeconds;
108                    updateAlarm();
109                  } else {
110                    // need to update the alarm to cause it to wait again
111                    updateAlarm((long) -1);
112                  }
113                } finally {
114                  m_processLock.unlock();
115                }
116
117                if (handler != null) {
118                  handler.run();
119                }
120              }
121            });
122    m_thread.setName("Notifier");
123    m_thread.setDaemon(true);
124    m_thread.setUncaughtExceptionHandler(
125        (thread, error) -> {
126          Throwable cause = error.getCause();
127          if (cause != null) {
128            error = cause;
129          }
130          DriverStation.reportError(
131              "Unhandled exception in Notifier thread: " + error.toString(), error.getStackTrace());
132          DriverStation.reportError(
133              "The Runnable for this Notifier (or methods called by it) should have handled "
134                  + "the exception above.\n"
135                  + "  The above stacktrace can help determine where the error occurred.\n"
136                  + "  See https://wpilib.org/stacktrace for more information.",
137              false);
138        });
139    m_thread.start();
140  }
141
142  /**
143   * Sets the name of the notifier. Used for debugging purposes only.
144   *
145   * @param name Name
146   */
147  public void setName(String name) {
148    m_thread.setName(name);
149    NotifierJNI.setNotifierName(m_notifier.get(), name);
150  }
151
152  /**
153   * Change the handler function.
154   *
155   * @param handler Handler
156   */
157  public void setHandler(Runnable handler) {
158    m_processLock.lock();
159    try {
160      m_handler = handler;
161    } finally {
162      m_processLock.unlock();
163    }
164  }
165
166  /**
167   * Register for single event notification. A timer event is queued for a single event after the
168   * specified delay.
169   *
170   * @param delaySeconds Seconds to wait before the handler is called.
171   */
172  public void startSingle(double delaySeconds) {
173    m_processLock.lock();
174    try {
175      m_periodic = false;
176      m_periodSeconds = delaySeconds;
177      m_expirationTimeSeconds = RobotController.getFPGATime() * 1e-6 + delaySeconds;
178      updateAlarm();
179    } finally {
180      m_processLock.unlock();
181    }
182  }
183
184  /**
185   * Register for periodic event notification. A timer event is queued for periodic event
186   * notification. Each time the interrupt occurs, the event will be immediately requeued for the
187   * same time interval.
188   *
189   * <p>The user-provided callback should be written in a nonblocking manner so the callback can be
190   * recalled at the next periodic event notification.
191   *
192   * @param periodSeconds Period in seconds to call the handler starting one period after the call
193   *     to this method.
194   */
195  public void startPeriodic(double periodSeconds) {
196    m_processLock.lock();
197    try {
198      m_periodic = true;
199      m_periodSeconds = periodSeconds;
200      m_expirationTimeSeconds = RobotController.getFPGATime() * 1e-6 + periodSeconds;
201      updateAlarm();
202    } finally {
203      m_processLock.unlock();
204    }
205  }
206
207  /**
208   * Stop timer events from occurring. Stop any repeating timer events from occurring. This will
209   * also remove any single notification events from the queue. If a timer-based call to the
210   * registered handler is in progress, this function will block until the handler call is complete.
211   */
212  public void stop() {
213    m_processLock.lock();
214    try {
215      m_periodic = false;
216      NotifierJNI.cancelNotifierAlarm(m_notifier.get());
217    } finally {
218      m_processLock.unlock();
219    }
220  }
221
222  /**
223   * Sets the HAL notifier thread priority.
224   *
225   * <p>The HAL notifier thread is responsible for managing the FPGA's notifier interrupt and waking
226   * up user's Notifiers when it's their time to run. Giving the HAL notifier thread real-time
227   * priority helps ensure the user's real-time Notifiers, if any, are notified to run in a timely
228   * manner.
229   *
230   * @param realTime Set to true to set a real-time priority, false for standard priority.
231   * @param priority Priority to set the thread to. For real-time, this is 1-99 with 99 being
232   *     highest. For non-real-time, this is forced to 0. See "man 7 sched" for more details.
233   * @return True on success.
234   */
235  public static boolean setHALThreadPriority(boolean realTime, int priority) {
236    return NotifierJNI.setHALThreadPriority(realTime, priority);
237  }
238}