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 static org.wpilib.units.Units.Seconds;
008
009import java.io.File;
010import java.io.IOException;
011import java.lang.reflect.Modifier;
012import java.net.JarURLConnection;
013import java.net.URISyntaxException;
014import java.net.URL;
015import java.util.Enumeration;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.Map;
019import java.util.Optional;
020import java.util.Set;
021import java.util.function.Supplier;
022import java.util.jar.JarEntry;
023import java.util.jar.JarFile;
024import org.wpilib.driverstation.Alert;
025import org.wpilib.driverstation.DriverStationErrors;
026import org.wpilib.driverstation.RobotState;
027import org.wpilib.driverstation.UserControls;
028import org.wpilib.driverstation.UserControlsInstance;
029import org.wpilib.driverstation.internal.DriverStationBackend;
030import org.wpilib.hardware.hal.ControlWord;
031import org.wpilib.hardware.hal.DriverStationJNI;
032import org.wpilib.hardware.hal.HAL;
033import org.wpilib.hardware.hal.NotifierJNI;
034import org.wpilib.hardware.hal.RobotMode;
035import org.wpilib.internal.PeriodicPriorityQueue;
036import org.wpilib.internal.PeriodicPriorityQueue.Callback;
037import org.wpilib.networktables.NetworkTableInstance;
038import org.wpilib.opmode.Autonomous;
039import org.wpilib.opmode.OpMode;
040import org.wpilib.opmode.PeriodicOpMode;
041import org.wpilib.opmode.Teleop;
042import org.wpilib.opmode.Utility;
043import org.wpilib.smartdashboard.SmartDashboard;
044import org.wpilib.system.RobotController;
045import org.wpilib.system.Watchdog;
046import org.wpilib.util.Color;
047import org.wpilib.util.ConstructorMatch;
048
049/**
050 * OpModeRobot implements the opmode-based robot program framework.
051 *
052 * <p>The OpModeRobot class is intended to be subclassed by a user creating a robot program.
053 *
054 * <p>Classes annotated with {@link Autonomous}, {@link Teleop}, and {@link Utility} in the same
055 * package or subpackages as the user's subclass are automatically registered as autonomous, teleop,
056 * and utility opmodes respectively.
057 *
058 * <p>Opmodes are constructed when selected on the driver station. While selected and disabled,
059 * {@link PeriodicOpMode#disabledPeriodic()} is called. When enabled, {@link PeriodicOpMode#start()}
060 * is called once and {@link PeriodicOpMode#periodic()} runs at the rate from {@link #getPeriod()}.
061 * On disable or mode switch while enabled, {@link PeriodicOpMode#end()} is called asynchronously
062 * and the opmode is then closed and discarded. When no opmode is selected, {@link #nonePeriodic()}
063 * is called. {@link #driverStationConnected()} is called once when the DS first connects.
064 */
065public abstract class OpModeRobot extends RobotBase {
066  private final ControlWord m_word = new ControlWord();
067
068  private record OpModeFactory(String name, Supplier<OpMode> supplier) {}
069
070  private final Map<Long, OpModeFactory> m_opModes = new HashMap<>();
071
072  // Callback system fields (match C++ architecture)
073  private final PeriodicPriorityQueue m_callbacks = new PeriodicPriorityQueue();
074  private int m_notifier;
075  private final double m_period;
076  private final long m_startTimeUs;
077
078  // OpMode lifecycle state
079  private long m_lastModeId = -1;
080  private boolean m_calledDriverStationConnected = false;
081  private boolean m_lastEnabledState = false;
082  private OpMode m_currentOpMode;
083  private Callback m_currentOpModePeriodic;
084  private final Set<Callback> m_activeOpModeCallbacks = new HashSet<>();
085  private final Watchdog m_watchdog;
086  private final Alert m_loopOverrunAlert;
087
088  private static void reportAddOpModeError(Class<?> cls, String message) {
089    DriverStationErrors.reportError(
090        "Error adding OpMode " + cls.getSimpleName() + ": " + message, false);
091  }
092
093  private final Optional<Class<? extends UserControls>> m_userControlsBaseClass;
094  private UserControls m_userControlsInstance;
095
096  void setUserControlsInstance(UserControls userControlsInstance) {
097    if (m_userControlsBaseClass.isEmpty()) {
098      throw new IllegalStateException("No UserControls class specified");
099    }
100
101    if (!m_userControlsBaseClass.get().isAssignableFrom(userControlsInstance.getClass())) {
102      throw new IllegalArgumentException(
103          userControlsInstance.getClass().getSimpleName()
104              + " is not assignable to "
105              + m_userControlsBaseClass.get().getSimpleName());
106    }
107    m_userControlsInstance = userControlsInstance;
108  }
109
110  /**
111   * Find a public constructor to instantiate the opmode. This constructor can have up to 2
112   * parameters. The first parameter (if present) must be assignable from this.getClass(). The
113   * second parameter (if present) must be assignable from DriverStationBase. If multiple, first
114   * sort by most parameters, then by most specific first, then by most specific second.
115   */
116  private <T> Optional<ConstructorMatch<T>> findOpModeConstructor(Class<T> cls) {
117    Optional<ConstructorMatch<T>> ctor;
118
119    // try 2-parameter constructor
120    if (m_userControlsBaseClass.isPresent()) {
121      ctor = ConstructorMatch.findBestConstructor(cls, getClass(), m_userControlsBaseClass.get());
122      if (ctor.isPresent()) {
123        return ctor;
124      }
125    }
126
127    // try 1-parameter constructor with RobotBase parameter
128    ctor = ConstructorMatch.findBestConstructor(cls, getClass());
129    if (ctor.isPresent()) {
130      return ctor;
131    }
132
133    // try 1-parameter constructor with UserControls parameter
134    if (m_userControlsBaseClass.isPresent()) {
135      ctor = ConstructorMatch.findBestConstructor(cls, m_userControlsBaseClass.get());
136      if (ctor.isPresent()) {
137        return ctor;
138      }
139    }
140
141    // try no-parameter constructor
142    ctor = ConstructorMatch.findBestConstructor(cls);
143    return ctor;
144  }
145
146  private <T extends OpMode> T constructOpModeClass(Class<T> cls) {
147    Optional<ConstructorMatch<T>> constructor = findOpModeConstructor(cls);
148    if (constructor.isEmpty()) {
149      DriverStationErrors.reportError(
150          "No suitable constructor to instantiate OpMode " + cls.getSimpleName(), true);
151      return null;
152    }
153    try {
154      if (m_userControlsInstance != null) {
155        return constructor.get().newInstance(this, m_userControlsInstance);
156      } else {
157        return constructor.get().newInstance(this);
158      }
159    } catch (ReflectiveOperationException e) {
160      DriverStationErrors.reportError(
161          "Could not instantiate OpMode " + cls.getSimpleName(), e.getStackTrace());
162      return null;
163    }
164  }
165
166  private void checkOpModeClass(Class<?> cls) {
167    // the class must be a subclass of OpMode
168    if (!OpMode.class.isAssignableFrom(cls)) {
169      throw new IllegalArgumentException("not a subclass of OpMode");
170    }
171    int modifiers = cls.getModifiers();
172    // it cannot be abstract
173    if (Modifier.isAbstract(modifiers)) {
174      throw new IllegalArgumentException("is abstract");
175    }
176    // it must be public
177    if (!Modifier.isPublic(modifiers)) {
178      throw new IllegalArgumentException("not public");
179    }
180    // it must not be a non-static inner class
181    if (cls.getEnclosingClass() != null && !Modifier.isStatic(modifiers)) {
182      throw new IllegalArgumentException("is a non-static inner class");
183    }
184    // it must have a public no-arg constructor or a public constructor that accepts this class
185    // (or a superclass/interface) as an argument
186    if (findOpModeConstructor(cls).isEmpty()) {
187      throw new IllegalArgumentException(
188          "missing public no-arg constructor or constructor accepting "
189              + getClass().getSimpleName());
190    }
191  }
192
193  /**
194   * Adds an opmode using a factory function that creates the opmode. It's necessary to call
195   * publishOpModes() to make the added mode visible to the driver station.
196   *
197   * @param factory factory function to create the opmode
198   * @param mode robot mode
199   * @param name name of the operating mode
200   * @param group group of the operating mode
201   * @param description description of the operating mode
202   * @param textColor text color, or null for default
203   * @param backgroundColor background color, or null for default
204   * @throws IllegalArgumentException if class does not meet criteria
205   */
206  public void addOpModeFactory(
207      Supplier<OpMode> factory,
208      RobotMode mode,
209      String name,
210      String group,
211      String description,
212      Color textColor,
213      Color backgroundColor) {
214    long id = RobotState.addOpMode(mode, name, group, description, textColor, backgroundColor);
215    m_opModes.put(id, new OpModeFactory(name, factory));
216  }
217
218  /**
219   * Adds an opmode using a factory function that creates the opmode. It's necessary to call
220   * publishOpModes() to make the added mode visible to the driver station.
221   *
222   * @param factory factory function to create the opmode
223   * @param mode robot mode
224   * @param name name of the operating mode
225   * @param group group of the operating mode
226   * @param description description of the operating mode
227   * @throws IllegalArgumentException if class does not meet criteria
228   */
229  public void addOpModeFactory(
230      Supplier<OpMode> factory, RobotMode mode, String name, String group, String description) {
231    addOpModeFactory(factory, mode, name, group, description, null, null);
232  }
233
234  /**
235   * Adds an opmode using a factory function that creates the opmode. It's necessary to call
236   * publishOpModes() to make the added mode visible to the driver station.
237   *
238   * @param factory factory function to create the opmode
239   * @param mode robot mode
240   * @param name name of the operating mode
241   * @param group group of the operating mode
242   * @throws IllegalArgumentException if class does not meet criteria
243   */
244  public void addOpModeFactory(
245      Supplier<OpMode> factory, RobotMode mode, String name, String group) {
246    addOpModeFactory(factory, mode, name, group, "");
247  }
248
249  /**
250   * Adds an opmode using a factory function that creates the opmode. It's necessary to call
251   * publishOpModes() to make the added mode visible to the driver station.
252   *
253   * @param factory factory function to create the opmode
254   * @param mode robot mode
255   * @param name name of the operating mode
256   * @throws IllegalArgumentException if class does not meet criteria
257   */
258  public void addOpModeFactory(Supplier<OpMode> factory, RobotMode mode, String name) {
259    addOpModeFactory(factory, mode, name, "");
260  }
261
262  /**
263   * Adds an opmode for an opmode class. The class must be a public, non-abstract subclass of OpMode
264   * with a public constructor that either takes no arguments or accepts a single argument
265   * assignable from this robot class type (the latter is preferred; if multiple match, the most
266   * specific parameter type is used). It's necessary to call publishOpModes() to make the added
267   * mode visible to the driver station.
268   *
269   * @param cls class to add
270   * @param mode robot mode
271   * @param name name of the operating mode
272   * @param group group of the operating mode
273   * @param description description of the operating mode
274   * @param textColor text color, or null for default
275   * @param backgroundColor background color, or null for default
276   * @throws IllegalArgumentException if class does not meet criteria
277   */
278  public void addOpMode(
279      Class<? extends OpMode> cls,
280      RobotMode mode,
281      String name,
282      String group,
283      String description,
284      Color textColor,
285      Color backgroundColor) {
286    checkOpModeClass(cls);
287    addOpModeFactory(
288        () -> constructOpModeClass(cls),
289        mode,
290        name,
291        group,
292        description,
293        textColor,
294        backgroundColor);
295  }
296
297  /**
298   * Adds an opmode for an opmode class. The class must be a public, non-abstract subclass of OpMode
299   * with a public constructor that either takes no arguments or accepts a single argument
300   * assignable from this robot class type (the latter is preferred; if multiple match, the most
301   * specific parameter type is used). It's necessary to call publishOpModes() to make the added
302   * mode visible to the driver station.
303   *
304   * @param cls class to add
305   * @param mode robot mode
306   * @param name name of the operating mode
307   * @param group group of the operating mode
308   * @param description description of the operating mode
309   * @throws IllegalArgumentException if class does not meet criteria
310   */
311  public void addOpMode(
312      Class<? extends OpMode> cls, RobotMode mode, String name, String group, String description) {
313    addOpMode(cls, mode, name, group, description, null, null);
314  }
315
316  /**
317   * Adds an opmode for an opmode class. The class must be a public, non-abstract subclass of OpMode
318   * with a public constructor that either takes no arguments or accepts a single argument
319   * assignable from this robot class type (the latter is preferred; if multiple match, the most
320   * specific parameter type is used). It's necessary to call publishOpModes() to make the added
321   * mode visible to the driver station.
322   *
323   * @param cls class to add
324   * @param mode robot mode
325   * @param name name of the operating mode
326   * @param group group of the operating mode
327   * @throws IllegalArgumentException if class does not meet criteria
328   */
329  public void addOpMode(Class<? extends OpMode> cls, RobotMode mode, String name, String group) {
330    addOpMode(cls, mode, name, group, "");
331  }
332
333  /**
334   * Adds an opmode for an opmode class. The class must be a public, non-abstract subclass of OpMode
335   * with a public constructor that either takes no arguments or accepts a single argument
336   * assignable from this robot class type (the latter is preferred; if multiple match, the most
337   * specific parameter type is used). It's necessary to call publishOpModes() to make the added
338   * mode visible to the driver station.
339   *
340   * @param cls class to add
341   * @param mode robot mode
342   * @param name name of the operating mode
343   * @throws IllegalArgumentException if class does not meet criteria
344   */
345  public void addOpMode(Class<? extends OpMode> cls, RobotMode mode, String name) {
346    addOpMode(cls, mode, name, "");
347  }
348
349  private void addOpModeClassImpl(
350      Class<? extends OpMode> cls,
351      RobotMode mode,
352      String name,
353      String group,
354      String description,
355      String textColor,
356      String backgroundColor) {
357    if (name == null || name.isBlank()) {
358      name = cls.getSimpleName();
359    }
360    Color tColor = textColor.isBlank() ? null : Color.fromString(textColor);
361    Color bColor = backgroundColor.isBlank() ? null : Color.fromString(backgroundColor);
362    long id = RobotState.addOpMode(mode, name, group, description, tColor, bColor);
363    m_opModes.put(id, new OpModeFactory(name, () -> constructOpModeClass(cls)));
364  }
365
366  private void addAnnotatedOpModeImpl(
367      Class<? extends OpMode> cls, Autonomous auto, Teleop teleop, Utility utility) {
368    checkOpModeClass(cls);
369
370    // add an opmode for each annotation
371    if (auto != null) {
372      addOpModeClassImpl(
373          cls,
374          RobotMode.AUTONOMOUS,
375          auto.name(),
376          auto.group(),
377          auto.description(),
378          auto.textColor(),
379          auto.backgroundColor());
380    }
381    if (teleop != null) {
382      addOpModeClassImpl(
383          cls,
384          RobotMode.TELEOPERATED,
385          teleop.name(),
386          teleop.group(),
387          teleop.description(),
388          teleop.textColor(),
389          teleop.backgroundColor());
390    }
391    if (utility != null) {
392      addOpModeClassImpl(
393          cls,
394          RobotMode.UTILITY,
395          utility.name(),
396          utility.group(),
397          utility.description(),
398          utility.textColor(),
399          utility.backgroundColor());
400    }
401  }
402
403  /**
404   * Adds an opmode for an opmode class annotated with {@link Autonomous}, {@link Teleop}, or {@link
405   * Utility}. The class must be a public, non-abstract subclass of OpMode with a public constructor
406   * that either takes no arguments or accepts a single argument assignable from this robot class
407   * type (if multiple match, the most specific parameter type is used). It's necessary to call
408   * publishOpModes() to make the added mode visible to the driver station.
409   *
410   * @param cls class to add
411   * @throws IllegalArgumentException if class does not meet criteria
412   */
413  public void addAnnotatedOpMode(Class<? extends OpMode> cls) {
414    Autonomous auto = cls.getAnnotation(Autonomous.class);
415    Teleop teleop = cls.getAnnotation(Teleop.class);
416    Utility utility = cls.getAnnotation(Utility.class);
417    if (auto == null && teleop == null && utility == null) {
418      throw new IllegalArgumentException("must be annotated with Autonomous, Teleop, or Utility");
419    }
420    addAnnotatedOpModeImpl(cls, auto, teleop, utility);
421  }
422
423  private void addAnnotatedOpModeClass(String name) {
424    // trim ".class" from end
425    String className = name.replace('/', '.').substring(0, name.length() - 6);
426    Class<? extends OpMode> cls;
427    try {
428      cls = Class.forName(className).asSubclass(OpMode.class);
429    } catch (ClassNotFoundException | ClassCastException e) {
430      return;
431    }
432    Autonomous auto = cls.getAnnotation(Autonomous.class);
433    Teleop teleop = cls.getAnnotation(Teleop.class);
434    Utility utility = cls.getAnnotation(Utility.class);
435    if (auto == null && teleop == null && utility == null) {
436      return;
437    }
438    try {
439      addAnnotatedOpModeImpl(cls, auto, teleop, utility);
440    } catch (IllegalArgumentException e) {
441      reportAddOpModeError(cls, e.getMessage());
442    }
443  }
444
445  private void addAnnotatedOpModeClassesDir(File root, File dir, String packagePath) {
446    File[] files = dir.listFiles();
447    if (files == null) {
448      return;
449    }
450    for (File file : files) {
451      if (file.isDirectory()) {
452        addAnnotatedOpModeClassesDir(root, file, packagePath);
453      } else if (file.getName().endsWith(".class")) {
454        String relPath = root.toPath().relativize(file.toPath()).toString().replace('\\', '/');
455        addAnnotatedOpModeClass(packagePath + "." + relPath);
456      }
457    }
458  }
459
460  /**
461   * Scans for classes in the specified package and all nested packages that are annotated with
462   * {@link Autonomous}, {@link Teleop}, or {@link Utility} and registers them. It's necessary to
463   * call publishOpModes() to make the added modes visible to the driver station.
464   *
465   * @param pkg package to scan
466   */
467  public void addAnnotatedOpModeClasses(Package pkg) {
468    String packageName = pkg.getName();
469    String packagePath = packageName.replace('.', '/');
470    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
471
472    try {
473      Enumeration<URL> resources = classLoader.getResources(packagePath);
474      while (resources.hasMoreElements()) {
475        URL resource = resources.nextElement();
476        if ("jar".equals(resource.getProtocol())) {
477          var connection = resource.openConnection();
478          if (!(connection instanceof JarURLConnection jarConnection)) {
479            DriverStationErrors.reportError(
480                "Error scanning OpModes from "
481                    + resource
482                    + ": expected JarURLConnection, got "
483                    + connection.getClass().getSimpleName(),
484                false);
485            continue;
486          }
487          jarConnection.setUseCaches(false);
488          try (JarFile jar = jarConnection.getJarFile()) {
489            Enumeration<JarEntry> entries = jar.entries();
490            while (entries.hasMoreElements()) {
491              String name = entries.nextElement().getName();
492              if (!name.startsWith(packagePath) || !name.endsWith(".class")) {
493                continue;
494              }
495              addAnnotatedOpModeClass(name);
496            }
497          }
498        } else if ("file".equals(resource.getProtocol())) {
499          // Handle .class files in directories
500          File dir = new File(resource.toURI());
501          if (dir.exists() && dir.isDirectory()) {
502            addAnnotatedOpModeClassesDir(dir, dir, packagePath);
503          }
504        }
505      }
506    } catch (IOException | URISyntaxException e) {
507      e.printStackTrace();
508    }
509  }
510
511  /**
512   * Removes an operating mode option. It's necessary to call publishOpModes() to make the removed
513   * mode no longer visible to the driver station.
514   *
515   * @param mode robot mode
516   * @param name name of the operating mode
517   */
518  public void removeOpMode(RobotMode mode, String name) {
519    long id = RobotState.removeOpMode(mode, name);
520    if (id != 0) {
521      m_opModes.remove(id);
522    }
523  }
524
525  /** Publishes the operating mode options to the driver station. */
526  public void publishOpModes() {
527    RobotState.publishOpModes();
528  }
529
530  /** Clears all operating mode options and publishes an empty list to the driver station. */
531  public void clearOpModes() {
532    RobotState.clearOpModes();
533    m_opModes.clear();
534  }
535
536  /** Default loop period. */
537  public static final double DEFAULT_PERIOD = 0.02;
538
539  /** Constructor with default period. */
540  @SuppressWarnings("this-escape")
541  public OpModeRobot() {
542    this(DEFAULT_PERIOD);
543  }
544
545  /**
546   * Constructor with specified period.
547   *
548   * @param period the period at which to run the robot and opmode periodic callbacks.
549   */
550  @SuppressWarnings("this-escape")
551  public OpModeRobot(double period) {
552    m_period = period;
553
554    // Create our own notifier and callback queue (match C++)
555    m_notifier = NotifierJNI.createNotifier();
556    NotifierJNI.setNotifierName(m_notifier, "OpModeRobot");
557
558    m_startTimeUs = RobotController.getMonotonicTime();
559
560    m_loopOverrunAlert =
561        new Alert("Loop time of \"" + m_period + "\"s overrun", Alert.Level.MEDIUM);
562    m_watchdog = new Watchdog(Seconds.of(m_period), () -> m_loopOverrunAlert.set(true));
563
564    // Add LoopFunc as periodic callback (match C++)
565    addPeriodic(this::loopFunc, period);
566
567    // Check to see if we have a DS annotation
568    UserControlsInstance userControlsAnnotation =
569        getClass().getAnnotation(UserControlsInstance.class);
570    if (userControlsAnnotation != null) {
571      m_userControlsBaseClass = Optional.of(userControlsAnnotation.value());
572    } else {
573      m_userControlsBaseClass = Optional.empty();
574    }
575    // Scan for annotated opmode classes within the derived class's package and subpackages
576    addAnnotatedOpModeClasses(getClass().getPackage());
577    RobotState.publishOpModes();
578
579    HAL.reportUsage("Framework", "OpModeRobot");
580  }
581
582  /**
583   * Add a callback to run at a specific period.
584   *
585   * @param callback The callback to run.
586   * @param period The period at which to run the callback.
587   */
588  public void addPeriodic(Runnable callback, double period) {
589    m_callbacks.add(callback, m_startTimeUs, period);
590  }
591
592  /**
593   * Get the period at which robot and opmode periodic callbacks are run.
594   *
595   * @return The period at which robot and opmode periodic callbacks are run.
596   */
597  public double getPeriod() {
598    return m_period;
599  }
600
601  /**
602   * Code that needs to know the DS state should go here.
603   *
604   * <p>Users should override this method for initialization that needs to occur after the DS is
605   * connected, such as needing the alliance information.
606   */
607  public void driverStationConnected() {}
608
609  /** Function called periodically every loop, regardless of enabled state or OpMode selection. */
610  public void robotPeriodic() {}
611
612  /** Function called once during robot initialization in simulation. */
613  public void simulationInit() {}
614
615  /** Function called periodically in simulation. */
616  public void simulationPeriodic() {}
617
618  /** Function called once when the robot becomes disabled. */
619  public void disabledInit() {}
620
621  /** Function called periodically while the robot is disabled. */
622  public void disabledPeriodic() {}
623
624  /** Function called once when the robot exits disabled state. */
625  public void disabledExit() {}
626
627  /**
628   * Function called periodically anytime when no opmode is selected, including when the Driver
629   * Station is disconnected.
630   */
631  public void nonePeriodic() {}
632
633  /**
634   * Return the system clock time in microseconds for the start of the current periodic loop. This
635   * is in the same time base as Timer.getMonotonicTimestamp(), but is stable through a loop. It is
636   * updated at the beginning of every periodic callback (including the normal periodic loop).
637   *
638   * @return Robot running time in microseconds, as of the start of the current periodic function.
639   */
640  public long getLoopStartTime() {
641    return m_callbacks.getLoopStartTime();
642  }
643
644  /** Main robot loop function. Handles disabled state logic and opmode management. */
645  private void loopFunc() {
646    DriverStationBackend.refreshData();
647
648    // Get current enabled state and opmode
649    DriverStationBackend.refreshControlWordFromCache(m_word);
650    m_watchdog.reset();
651    boolean enabled = m_word.isEnabled();
652    long modeId = m_word.isDSAttached() ? m_word.getOpModeId() : 0;
653
654    if (!m_calledDriverStationConnected && m_word.isDSAttached()) {
655      m_calledDriverStationConnected = true;
656      driverStationConnected();
657      m_watchdog.addEpoch("driverStationConnected()");
658    }
659
660    // Handle opmode changes
661    if (modeId != m_lastModeId) {
662      // Clean up current opmode
663      if (m_currentOpMode != null) {
664        // Remove opmode callbacks
665        m_callbacks.remove(m_currentOpModePeriodic);
666        m_callbacks.removeAll(m_activeOpModeCallbacks);
667        m_activeOpModeCallbacks.clear();
668        m_currentOpMode.end();
669        m_currentOpMode.close();
670        m_currentOpMode = null;
671      }
672
673      // Set up new opmode
674      if (modeId != 0) {
675        OpModeFactory factory = m_opModes.get(modeId);
676        if (factory != null) {
677          // Instantiate the new opmode
678          System.out.println("********** Starting OpMode " + factory.name() + " **********");
679          m_currentOpMode = factory.supplier().get();
680          if (m_currentOpMode != null) {
681            // Ensure disabledPeriodic is called at least once
682            m_currentOpMode.disabledPeriodic();
683            m_watchdog.addEpoch("opMode.disabledPeriodic()");
684            // Register the opmode's periodic callbacks
685            m_currentOpModePeriodic =
686                m_callbacks.add(m_currentOpMode::periodic, m_startTimeUs, m_period);
687            m_activeOpModeCallbacks.addAll(m_currentOpMode.getCallbacks());
688            m_callbacks.addAll(m_activeOpModeCallbacks);
689          }
690        } else {
691          DriverStationErrors.reportError("No OpMode found for mode " + modeId, false);
692        }
693      }
694      m_lastModeId = modeId;
695    }
696
697    // Handle enabled state changes
698    boolean justCalledDisabledInit = false;
699    if (m_lastEnabledState != enabled) {
700      if (enabled) {
701        // Transitioning to enabled
702        disabledExit();
703        m_watchdog.addEpoch("disabledExit()");
704        if (m_currentOpMode != null) {
705          m_currentOpMode.start();
706          m_watchdog.addEpoch("opMode.start()");
707        }
708      } else {
709        // Transitioning to disabled
710        if (m_currentOpMode != null && m_lastEnabledState) {
711          // Was enabled, now disabled
712          m_currentOpMode.end();
713          m_watchdog.addEpoch("opMode.end()");
714        }
715        disabledInit();
716        m_watchdog.addEpoch("disabledInit()");
717        justCalledDisabledInit = true;
718      }
719      m_lastEnabledState = enabled;
720    }
721
722    // Call periodic functions based on current state
723    if (!enabled) {
724      // Only call disabledPeriodic if we didn't just call disabledInit
725      if (!justCalledDisabledInit) {
726        disabledPeriodic();
727        m_watchdog.addEpoch("disabledPeriodic()");
728      }
729
730      // Call opmode disabledPeriodic if we have one
731      if (m_currentOpMode != null) {
732        m_currentOpMode.disabledPeriodic();
733        m_watchdog.addEpoch("opMode.disabledPeriodic()");
734      }
735    }
736
737    // Call nonePeriodic when no opmode is selected
738    if (RobotState.getOpModeId() == 0) {
739      nonePeriodic();
740      m_watchdog.addEpoch("nonePeriodic()");
741    }
742
743    // Always call robotPeriodic
744    robotPeriodic();
745    m_watchdog.addEpoch("robotPeriodic()");
746
747    // Always observe user program state
748    DriverStationJNI.observeUserProgram(m_word.getNative());
749
750    SmartDashboard.updateValues();
751    m_watchdog.addEpoch("SmartDashboard.updateValues()");
752
753    // Call simulationPeriodic if in simulation
754    if (isSimulation()) {
755      HAL.simPeriodicBefore();
756      simulationPeriodic();
757      HAL.simPeriodicAfter();
758      m_watchdog.addEpoch("simulationPeriodic()");
759    }
760
761    m_watchdog.disable();
762
763    // Flush NetworkTables
764    NetworkTableInstance.getDefault().flushLocal();
765
766    // Warn on loop time overruns
767    if (m_watchdog.isExpired()) {
768      m_watchdog.printEpochs();
769    }
770  }
771
772  /** Provide an alternate "main loop" via startCompetition(). */
773  @Override
774  public final void startCompetition() {
775    System.out.println("********** Robot program startup complete **********");
776
777    if (isSimulation()) {
778      simulationInit();
779    }
780
781    // Tell the DS that the robot is ready to be enabled
782    DriverStationBackend.observeUserProgramStarting();
783
784    // Loop forever, calling the callback system which handles periodic functions
785    while (true) {
786      if (!m_callbacks.runCallbacks(m_notifier)) {
787        break;
788      }
789    }
790  }
791
792  @Override
793  public void close() {
794    NotifierJNI.destroyNotifier(m_notifier);
795  }
796
797  /** Ends the main loop in startCompetition(). */
798  @Override
799  public final void endCompetition() {
800    NotifierJNI.destroyNotifier(m_notifier);
801  }
802
803  /** Prints list of epochs added so far and their times. */
804  public void printWatchdogEpochs() {
805    m_watchdog.printEpochs();
806  }
807}