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}