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.framework;
006
007import java.util.Optional;
008import java.util.concurrent.locks.ReentrantLock;
009import org.wpilib.driverstation.DriverStationErrors;
010import org.wpilib.driverstation.RobotState;
011import org.wpilib.driverstation.UserControls;
012import org.wpilib.driverstation.UserControlsInstance;
013import org.wpilib.driverstation.internal.DriverStationBackend;
014import org.wpilib.hardware.hal.HAL;
015import org.wpilib.hardware.hal.HALUtil;
016import org.wpilib.math.util.MathShared;
017import org.wpilib.math.util.MathSharedStore;
018import org.wpilib.networktables.MultiSubscriber;
019import org.wpilib.networktables.NetworkTableEvent;
020import org.wpilib.networktables.NetworkTableInstance;
021import org.wpilib.system.RuntimeType;
022import org.wpilib.system.Timer;
023import org.wpilib.system.WPILibVersion;
024import org.wpilib.util.ConstructorMatch;
025import org.wpilib.util.WPIUtilJNI;
026import org.wpilib.vision.stream.CameraServerShared;
027import org.wpilib.vision.stream.CameraServerSharedStore;
028
029/**
030 * Implement a Robot Program framework. The RobotBase class is intended to be subclassed to create a
031 * robot program. The user must implement {@link #startCompetition()}, which will be called once and
032 * is not expected to exit. The user must also implement {@link #endCompetition()}, which signals to
033 * the code in {@link #startCompetition()} that it should exit.
034 *
035 * <p>It is not recommended to subclass this class directly - instead subclass IterativeRobotBase or
036 * TimedRobot.
037 */
038public abstract class RobotBase implements AutoCloseable {
039  /** The ID of the main Java thread. */
040  // This is usually 1, but it is best to make sure
041  private static long m_threadId = -1;
042
043  private final MultiSubscriber m_suball;
044
045  private final int m_connListenerHandle;
046
047  private static void setupCameraServerShared() {
048    CameraServerShared shared =
049        new CameraServerShared() {
050          @Override
051          public void reportUsage(String resource, String data) {
052            HAL.reportUsage(resource, data);
053          }
054
055          @Override
056          public void reportDriverStationError(String error) {
057            DriverStationErrors.reportError(error, true);
058          }
059
060          @Override
061          public Long getRobotMainThreadId() {
062            return RobotBase.getMainThreadId();
063          }
064
065          @Override
066          public boolean isRoboRIO() {
067            return !RobotBase.isSimulation();
068          }
069        };
070
071    CameraServerSharedStore.setCameraServerShared(shared);
072  }
073
074  private static void setupMathShared() {
075    MathSharedStore.setMathShared(
076        new MathShared() {
077          @Override
078          public void reportError(String error, StackTraceElement[] stackTrace) {
079            DriverStationErrors.reportError(error, stackTrace);
080          }
081
082          @Override
083          public void reportUsage(String resource, String data) {
084            HAL.reportUsage(resource, data);
085          }
086
087          @Override
088          public double getTimestamp() {
089            return Timer.getTimestamp();
090          }
091        });
092  }
093
094  /**
095   * Constructor for a generic robot program. User code can be placed in the constructor that runs
096   * before the Autonomous or Operator Control period starts. The constructor will run to completion
097   * before Autonomous is entered.
098   *
099   * <p>This must be used to ensure that the communications code starts. In the future it would be
100   * nice to put this code into its own task that loads on boot so ensure that it runs.
101   */
102  protected RobotBase() {
103    final NetworkTableInstance inst = NetworkTableInstance.getDefault();
104    m_threadId = Thread.currentThread().threadId();
105    setupCameraServerShared();
106    setupMathShared();
107    // subscribe to "" to force persistent values to propagate to local
108    m_suball = new MultiSubscriber(inst, new String[] {""});
109    if (!isSimulation()) {
110      inst.startServer("/home/systemcore/networktables.json", "", "robot");
111    } else {
112      inst.startServer("networktables.json", "", "robot");
113    }
114
115    // wait for the NT server to actually start
116    try {
117      int count = 0;
118      while (inst.getNetworkMode().contains(NetworkTableInstance.NetworkMode.STARTING)) {
119        Thread.sleep(10);
120        count++;
121        if (count > 100) {
122          throw new InterruptedException();
123        }
124      }
125    } catch (InterruptedException ex) {
126      System.err.println("timed out while waiting for NT server to start");
127    }
128
129    m_connListenerHandle =
130        inst.addConnectionListener(
131            false,
132            event -> {
133              if (event.is(NetworkTableEvent.Kind.CONNECTED)) {
134                HAL.reportUsage("NT/" + event.connInfo.remoteId, "");
135              }
136            });
137  }
138
139  /**
140   * Returns the main thread ID.
141   *
142   * @return The main thread ID.
143   */
144  public static long getMainThreadId() {
145    return m_threadId;
146  }
147
148  @Override
149  public void close() {
150    m_suball.close();
151    NetworkTableInstance.getDefault().removeListener(m_connListenerHandle);
152  }
153
154  /**
155   * Get the current runtime type.
156   *
157   * @return Current runtime type.
158   */
159  public static RuntimeType getRuntimeType() {
160    return RuntimeType.getValue(HALUtil.getHALRuntimeType());
161  }
162
163  /**
164   * Get if the robot is a simulation.
165   *
166   * @return If the robot is running in simulation.
167   */
168  public static boolean isSimulation() {
169    return getRuntimeType() == RuntimeType.SIMULATION;
170  }
171
172  /**
173   * Get if the robot is real.
174   *
175   * @return If the robot is running in the real world.
176   */
177  public static boolean isReal() {
178    RuntimeType runtimeType = getRuntimeType();
179    return runtimeType == RuntimeType.SYSTEMCORE;
180  }
181
182  /**
183   * Determine if the Robot is currently disabled.
184   *
185   * @return True if the Robot is currently disabled by the Driver Station.
186   */
187  public static boolean isDisabled() {
188    return RobotState.isDisabled();
189  }
190
191  /**
192   * Determine if the Robot is currently enabled.
193   *
194   * @return True if the Robot is currently enabled by the Driver Station.
195   */
196  public static boolean isEnabled() {
197    return RobotState.isEnabled();
198  }
199
200  /**
201   * Determine if the robot is currently in Autonomous mode as determined by the Driver Station.
202   *
203   * @return True if the robot is currently operating Autonomously.
204   */
205  public static boolean isAutonomous() {
206    return RobotState.isAutonomous();
207  }
208
209  /**
210   * Determine if the robot is currently in Autonomous mode and enabled as determined by the Driver
211   * Station.
212   *
213   * @return True if the robot is currently operating autonomously while enabled.
214   */
215  public static boolean isAutonomousEnabled() {
216    return RobotState.isAutonomousEnabled();
217  }
218
219  /**
220   * Determine if the robot is currently in Utility mode as determined by the Driver Station.
221   *
222   * @return True if the robot is currently operating in Utility mode.
223   */
224  public static boolean isUtility() {
225    return RobotState.isUtility();
226  }
227
228  /**
229   * Determine if the robot is current in Utility mode and enabled as determined by the Driver
230   * Station.
231   *
232   * @return True if the robot is currently operating in Utility mode while enabled.
233   */
234  public static boolean isUtilityEnabled() {
235    return RobotState.isUtilityEnabled();
236  }
237
238  /**
239   * Determine if the robot is currently in Operator Control mode as determined by the Driver
240   * Station.
241   *
242   * @return True if the robot is currently operating in Tele-Op mode.
243   */
244  public static boolean isTeleop() {
245    return RobotState.isTeleop();
246  }
247
248  /**
249   * Determine if the robot is currently in Operator Control mode and enabled as determined by the
250   * Driver Station.
251   *
252   * @return True if the robot is currently operating in Tele-Op mode while enabled.
253   */
254  public static boolean isTeleopEnabled() {
255    return RobotState.isTeleopEnabled();
256  }
257
258  /**
259   * Gets the currently selected operating mode of the driver station. Note this does not mean the
260   * robot is enabled; use isEnabled() for that.
261   *
262   * @return the unique ID provided by the DriverStation.addOpMode() function; may return 0 or a
263   *     unique ID not added, so callers should be prepared to handle that case
264   */
265  public static long getOpModeId() {
266    return RobotState.getOpModeId();
267  }
268
269  /**
270   * Gets the currently selected operating mode of the driver station. Note this does not mean the
271   * robot is enabled; use isEnabled() for that.
272   *
273   * @return Operating mode string; may return a string not in the list of options, so callers
274   *     should be prepared to handle that case
275   */
276  public static String getOpMode() {
277    return RobotState.getOpMode();
278  }
279
280  /**
281   * Start the main robot code. This function will be called once and should not exit until
282   * signalled by {@link #endCompetition()}
283   */
284  public abstract void startCompetition();
285
286  /** Ends the main loop in {@link #startCompetition()}. */
287  public abstract void endCompetition();
288
289  private static final ReentrantLock m_runMutex = new ReentrantLock();
290  private static RobotBase m_robotCopy;
291  private static boolean m_suppressExitWarning;
292
293  private static <T extends RobotBase> T constructRobot(Class<T> robotClass) throws Throwable {
294    UserControlsInstance userControlsAttribute =
295        robotClass.getDeclaredAnnotation(UserControlsInstance.class);
296    UserControls userControlsInstance = null;
297    Optional<ConstructorMatch<T>> constructorMatch = Optional.empty();
298    if (userControlsAttribute != null) {
299      var userControlsClass = userControlsAttribute.value();
300      userControlsInstance = userControlsClass.getDeclaredConstructor().newInstance();
301      constructorMatch = ConstructorMatch.findBestConstructor(robotClass, userControlsClass);
302    }
303
304    if (constructorMatch.isEmpty()) {
305      // Try to find a constructor with no parameters if there is no UserControls constructor
306      constructorMatch = ConstructorMatch.findBestConstructor(robotClass);
307    }
308
309    if (constructorMatch.isEmpty()) {
310      throw new IllegalArgumentException(
311          "No valid constructor found in robot class " + robotClass.getName());
312    }
313
314    T robot = constructorMatch.get().newInstance(userControlsInstance);
315
316    if (userControlsInstance != null && robot instanceof OpModeRobot opModeRobot) {
317      // Insert the UserControls instance into the opModeRobot for use when constructing opmodes
318      opModeRobot.setUserControlsInstance(userControlsInstance);
319    }
320    return robot;
321  }
322
323  /** Run the robot main loop. */
324  @SuppressWarnings("PMD.AvoidCatchingGenericException")
325  private static <T extends RobotBase> void runRobot(Class<T> robotClass) {
326    System.out.println("********** Robot program starting **********");
327
328    T robot;
329    try {
330      robot = constructRobot(robotClass);
331    } catch (Throwable throwable) {
332      Throwable cause = throwable.getCause();
333      if (cause != null) {
334        throwable = cause;
335      }
336      String robotName = "Unknown";
337      StackTraceElement[] elements = throwable.getStackTrace();
338      if (elements.length > 0) {
339        robotName = elements[0].getClassName();
340      }
341      DriverStationErrors.reportError(
342          "Unhandled exception instantiating robot " + robotName + " " + throwable, elements);
343      DriverStationErrors.reportError(
344          "The robot program quit unexpectedly."
345              + " This is usually due to a code error.\n"
346              + "  The above stacktrace can help determine where the error occurred.\n"
347              + "  See https://wpilib.org/stacktrace for more information.\n",
348          false);
349      DriverStationErrors.reportError("Could not instantiate robot " + robotName + "!", false);
350      return;
351    }
352
353    m_runMutex.lock();
354    m_robotCopy = robot;
355    m_runMutex.unlock();
356
357    boolean errorOnExit = false;
358    try {
359      robot.startCompetition();
360    } catch (Throwable throwable) {
361      Throwable cause = throwable.getCause();
362      if (cause != null) {
363        throwable = cause;
364      }
365      DriverStationErrors.reportError(
366          "Unhandled exception: " + throwable, throwable.getStackTrace());
367      errorOnExit = true;
368    } finally {
369      m_runMutex.lock();
370      boolean suppressExitWarning = m_suppressExitWarning;
371      m_runMutex.unlock();
372      if (!suppressExitWarning) {
373        // startCompetition never returns unless exception occurs....
374        DriverStationErrors.reportWarning(
375            "The robot program quit unexpectedly."
376                + " This is usually due to a code error.\n"
377                + "  The above stacktrace can help determine where the error occurred.\n"
378                + "  See https://wpilib.org/stacktrace for more information.",
379            false);
380        if (errorOnExit) {
381          DriverStationErrors.reportError(
382              "The startCompetition() method (or methods called by it) should have "
383                  + "handled the exception above.",
384              false);
385        } else {
386          DriverStationErrors.reportError(
387              "Unexpected return from startCompetition() method.", false);
388        }
389      }
390    }
391  }
392
393  /**
394   * Suppress the "The robot program quit unexpectedly." message.
395   *
396   * @param value True if exit warning should be suppressed.
397   */
398  public static void suppressExitWarning(boolean value) {
399    m_runMutex.lock();
400    m_suppressExitWarning = value;
401    m_runMutex.unlock();
402  }
403
404  /**
405   * Starting point for the applications.
406   *
407   * @param <T> Robot subclass.
408   * @param robotClass Robot subclass type.
409   */
410  public static <T extends RobotBase> void startRobot(Class<T> robotClass) {
411    // Check that the MSVC runtime is valid.
412    WPIUtilJNI.checkMsvcRuntime();
413
414    if (!HAL.initialize(500, 0)) {
415      throw new IllegalStateException("Failed to initialize. Terminating");
416    }
417
418    // Force refresh DS data
419    DriverStationBackend.refreshData();
420
421    HAL.reportUsage("Language", "Java");
422    HAL.reportUsage("WPILibVersion", WPILibVersion.Version);
423
424    if (HAL.hasMain()) {
425      Thread thread =
426          new Thread(
427              () -> {
428                runRobot(robotClass);
429                HAL.exitMain();
430              },
431              "robot main");
432      thread.setDaemon(true);
433      thread.start();
434      HAL.runMain();
435      suppressExitWarning(true);
436      m_runMutex.lock();
437      RobotBase robot = m_robotCopy;
438      m_runMutex.unlock();
439      if (robot != null) {
440        robot.endCompetition();
441      }
442      try {
443        thread.join(1000);
444      } catch (InterruptedException ex) {
445        Thread.currentThread().interrupt();
446      }
447    } else {
448      runRobot(robotClass);
449    }
450
451    // On RIO, this will just terminate rather than shutting down cleanly (it's a no-op in sim).
452    // It's not worth the risk of hanging on shutdown when we want the code to restart as quickly
453    // as possible.
454    HAL.terminate();
455
456    HAL.shutdown();
457
458    System.exit(0);
459  }
460}