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}