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.io.File;
008import java.io.IOException;
009import java.lang.reflect.Constructor;
010import java.lang.reflect.Modifier;
011import java.net.URL;
012import java.util.Enumeration;
013import java.util.HashMap;
014import java.util.Map;
015import java.util.concurrent.atomic.AtomicReference;
016import java.util.function.Supplier;
017import java.util.jar.JarEntry;
018import java.util.jar.JarFile;
019import org.wpilib.driverstation.DriverStation;
020import org.wpilib.hardware.hal.ControlWord;
021import org.wpilib.hardware.hal.DriverStationJNI;
022import org.wpilib.hardware.hal.HAL;
023import org.wpilib.hardware.hal.NotifierJNI;
024import org.wpilib.hardware.hal.RobotMode;
025import org.wpilib.opmode.Autonomous;
026import org.wpilib.opmode.OpMode;
027import org.wpilib.opmode.Teleop;
028import org.wpilib.opmode.TestOpMode;
029import org.wpilib.util.Color;
030import org.wpilib.util.WPIUtilJNI;
031
032/**
033 * OpModeRobot implements the opmode-based robot program framework.
034 *
035 * <p>The OpModeRobot class is intended to be subclassed by a user creating a robot program.
036 *
037 * <p>Classes annotated with {@link Autonomous}, {@link Teleop}, and {@link TestOpMode} in the same
038 * package or subpackages as the user's subclass will be automatically registered as autonomous,
039 * teleop, and test opmodes respectively.
040 *
041 * <p>Opmodes are constructed when selected on the driver station, and closed/no longer used when
042 * the robot is disabled after being enabled or a different opmode is selected. When no opmode is
043 * selected, nonePeriodic() is called. The driverStationConnected() function is called the first
044 * time the driver station connects to the robot.
045 */
046public abstract class OpModeRobot extends RobotBase {
047  private final ControlWord m_word = new ControlWord();
048
049  private record OpModeFactory(String name, Supplier<OpMode> supplier) {}
050
051  private final Map<Long, OpModeFactory> m_opModes = new HashMap<>();
052  private final AtomicReference<OpMode> m_activeOpMode = new AtomicReference<>(null);
053  private volatile int m_notifier;
054
055  private static void reportAddOpModeError(Class<?> cls, String message) {
056    DriverStation.reportError("Error adding OpMode " + cls.getSimpleName() + ": " + message, false);
057  }
058
059  /**
060   * Find a public constructor to instantiate the opmode. Prefer a single-arg public constructor
061   * whose parameter type is assignable from this.getClass() (if multiple, pick the most specific
062   * parameter type). Otherwise return the public no-arg constructor. Return null if neither exists.
063   */
064  private Constructor<?> findOpModeConstructor(Class<?> cls) {
065    Constructor<?> bestCtor = null;
066    Class<?> bestParam = null;
067    for (Constructor<?> ctor : cls.getConstructors()) {
068      Class<?>[] params = ctor.getParameterTypes();
069      if (params.length != 1) {
070        continue;
071      }
072      Class<?> param = params[0];
073      if (!param.isAssignableFrom(getClass())) {
074        continue;
075      }
076      if (bestCtor == null || bestParam.isAssignableFrom(param)) {
077        bestCtor = ctor;
078        bestParam = param;
079      }
080    }
081    if (bestCtor != null) {
082      return bestCtor;
083    }
084    try {
085      return cls.getConstructor();
086    } catch (NoSuchMethodException e) {
087      return null;
088    }
089  }
090
091  private OpMode constructOpModeClass(Class<?> cls) {
092    Constructor<?> constructor = findOpModeConstructor(cls);
093    if (constructor == null) {
094      DriverStation.reportError(
095          "No suitable constructor to instantiate OpMode " + cls.getSimpleName(), true);
096      return null;
097    }
098    try {
099      if (constructor.getParameterCount() == 1) {
100        return (OpMode) constructor.newInstance(this);
101      } else {
102        return (OpMode) constructor.newInstance();
103      }
104    } catch (ReflectiveOperationException e) {
105      DriverStation.reportError(
106          "Could not instantiate OpMode " + cls.getSimpleName(), e.getStackTrace());
107      return null;
108    }
109  }
110
111  private void checkOpModeClass(Class<?> cls) {
112    // the class must be a subclass of OpMode
113    if (!OpMode.class.isAssignableFrom(cls)) {
114      throw new IllegalArgumentException("not a subclass of OpMode");
115    }
116    int modifiers = cls.getModifiers();
117    // it cannot be abstract
118    if (Modifier.isAbstract(modifiers)) {
119      throw new IllegalArgumentException("is abstract");
120    }
121    // it must be public
122    if (!Modifier.isPublic(modifiers)) {
123      throw new IllegalArgumentException("not public");
124    }
125    // it must not be a non-static inner class
126    if (cls.getEnclosingClass() != null && !Modifier.isStatic(modifiers)) {
127      throw new IllegalArgumentException("is a non-static inner class");
128    }
129    // it must have a public no-arg constructor or a public constructor that accepts this class
130    // (or a superclass/interface) as an argument
131    if (findOpModeConstructor(cls) == null) {
132      throw new IllegalArgumentException(
133          "missing public no-arg constructor or constructor accepting "
134              + getClass().getSimpleName());
135    }
136  }
137
138  /**
139   * Adds an opmode using a factory function that creates the opmode. It's necessary to call
140   * publishOpModes() to make the added mode visible to the driver station.
141   *
142   * @param factory factory function to create the opmode
143   * @param mode robot mode
144   * @param name name of the operating mode
145   * @param group group of the operating mode
146   * @param description description of the operating mode
147   * @param textColor text color, or null for default
148   * @param backgroundColor background color, or null for default
149   * @throws IllegalArgumentException if class does not meet criteria
150   */
151  public void addOpModeFactory(
152      Supplier<OpMode> factory,
153      RobotMode mode,
154      String name,
155      String group,
156      String description,
157      Color textColor,
158      Color backgroundColor) {
159    long id = DriverStation.addOpMode(mode, name, group, description, textColor, backgroundColor);
160    m_opModes.put(id, new OpModeFactory(name, factory));
161  }
162
163  /**
164   * Adds an opmode using a factory function that creates the opmode. It's necessary to call
165   * publishOpModes() to make the added mode visible to the driver station.
166   *
167   * @param factory factory function to create the opmode
168   * @param mode robot mode
169   * @param name name of the operating mode
170   * @param group group of the operating mode
171   * @param description description of the operating mode
172   * @throws IllegalArgumentException if class does not meet criteria
173   */
174  public void addOpModeFactory(
175      Supplier<OpMode> factory, RobotMode mode, String name, String group, String description) {
176    addOpModeFactory(factory, mode, name, group, description, null, null);
177  }
178
179  /**
180   * Adds an opmode using a factory function that creates the opmode. It's necessary to call
181   * publishOpModes() to make the added mode visible to the driver station.
182   *
183   * @param factory factory function to create the opmode
184   * @param mode robot mode
185   * @param name name of the operating mode
186   * @param group group of the operating mode
187   * @throws IllegalArgumentException if class does not meet criteria
188   */
189  public void addOpModeFactory(
190      Supplier<OpMode> factory, RobotMode mode, String name, String group) {
191    addOpModeFactory(factory, mode, name, group, "");
192  }
193
194  /**
195   * Adds an opmode using a factory function that creates the opmode. It's necessary to call
196   * publishOpModes() to make the added mode visible to the driver station.
197   *
198   * @param factory factory function to create the opmode
199   * @param mode robot mode
200   * @param name name of the operating mode
201   * @throws IllegalArgumentException if class does not meet criteria
202   */
203  public void addOpModeFactory(Supplier<OpMode> factory, RobotMode mode, String name) {
204    addOpModeFactory(factory, mode, name, "");
205  }
206
207  /**
208   * Adds an opmode for an opmode class. The class must be a public, non-abstract subclass of OpMode
209   * with a public constructor that either takes no arguments or accepts a single argument of this
210   * class's type (the latter is preferred). It's necessary to call publishOpModes() to make the
211   * added mode visible to the driver station.
212   *
213   * @param cls class to add
214   * @param mode robot mode
215   * @param name name of the operating mode
216   * @param group group of the operating mode
217   * @param description description of the operating mode
218   * @param textColor text color, or null for default
219   * @param backgroundColor background color, or null for default
220   * @throws IllegalArgumentException if class does not meet criteria
221   */
222  public void addOpMode(
223      Class<? extends OpMode> cls,
224      RobotMode mode,
225      String name,
226      String group,
227      String description,
228      Color textColor,
229      Color backgroundColor) {
230    checkOpModeClass(cls);
231    addOpModeFactory(
232        () -> constructOpModeClass(cls),
233        mode,
234        name,
235        group,
236        description,
237        textColor,
238        backgroundColor);
239  }
240
241  /**
242   * Adds an opmode for an opmode class. The class must be a public, non-abstract subclass of OpMode
243   * with a public constructor that either takes no arguments or accepts a single argument of this
244   * class's type (the latter is preferred). It's necessary to call publishOpModes() to make the
245   * added mode visible to the driver station.
246   *
247   * @param cls class to add
248   * @param mode robot mode
249   * @param name name of the operating mode
250   * @param group group of the operating mode
251   * @param description description of the operating mode
252   * @throws IllegalArgumentException if class does not meet criteria
253   */
254  public void addOpMode(
255      Class<? extends OpMode> cls, RobotMode mode, String name, String group, String description) {
256    addOpMode(cls, mode, name, group, description, null, null);
257  }
258
259  /**
260   * Adds an opmode for an opmode class. The class must be a public, non-abstract subclass of OpMode
261   * with a public constructor that either takes no arguments or accepts a single argument of this
262   * class's type (the latter is preferred). It's necessary to call publishOpModes() to make the
263   * added mode visible to the driver station.
264   *
265   * @param cls class to add
266   * @param mode robot mode
267   * @param name name of the operating mode
268   * @param group group of the operating mode
269   * @throws IllegalArgumentException if class does not meet criteria
270   */
271  public void addOpMode(Class<? extends OpMode> cls, RobotMode mode, String name, String group) {
272    addOpMode(cls, mode, name, group, "");
273  }
274
275  /**
276   * Adds an opmode for an opmode class. The class must be a public, non-abstract subclass of OpMode
277   * with a public constructor that either takes no arguments or accepts a single argument of this
278   * class's type (the latter is preferred). It's necessary to call publishOpModes() to make the
279   * added mode visible to the driver station.
280   *
281   * @param cls class to add
282   * @param mode robot mode
283   * @param name name of the operating mode
284   * @throws IllegalArgumentException if class does not meet criteria
285   */
286  public void addOpMode(Class<? extends OpMode> cls, RobotMode mode, String name) {
287    addOpMode(cls, mode, name, "");
288  }
289
290  private void addOpModeClassImpl(
291      Class<?> cls,
292      RobotMode mode,
293      String name,
294      String group,
295      String description,
296      String textColor,
297      String backgroundColor) {
298    if (name == null || name.isBlank()) {
299      name = cls.getSimpleName();
300    }
301    Color tColor = textColor.isBlank() ? null : Color.fromString(textColor);
302    Color bColor = backgroundColor.isBlank() ? null : Color.fromString(backgroundColor);
303    long id = DriverStation.addOpMode(mode, name, group, description, tColor, bColor);
304    m_opModes.put(id, new OpModeFactory(name, () -> constructOpModeClass(cls)));
305  }
306
307  private void addAnnotatedOpModeImpl(
308      Class<?> cls, Autonomous auto, Teleop teleop, TestOpMode test) {
309    checkOpModeClass(cls);
310
311    // add an opmode for each annotation
312    if (auto != null) {
313      addOpModeClassImpl(
314          cls,
315          RobotMode.AUTONOMOUS,
316          auto.name(),
317          auto.group(),
318          auto.description(),
319          auto.textColor(),
320          auto.backgroundColor());
321    }
322    if (teleop != null) {
323      addOpModeClassImpl(
324          cls,
325          RobotMode.TELEOPERATED,
326          teleop.name(),
327          teleop.group(),
328          teleop.description(),
329          teleop.textColor(),
330          teleop.backgroundColor());
331    }
332    if (test != null) {
333      addOpModeClassImpl(
334          cls,
335          RobotMode.TEST,
336          test.name(),
337          test.group(),
338          test.description(),
339          test.textColor(),
340          test.backgroundColor());
341    }
342  }
343
344  /**
345   * Adds an opmode for an opmode class annotated with {@link Autonomous}, {@link Teleop}, or {@link
346   * TestOpMode}. The class must be a public, non-abstract subclass of OpMode with a public
347   * constructor that either takes no arguments or accepts a single argument of this class's type.
348   * It's necessary to call publishOpModes() to make the added mode visible to the driver station.
349   *
350   * @param cls class to add
351   * @throws IllegalArgumentException if class does not meet criteria
352   */
353  public void addAnnotatedOpMode(Class<? extends OpMode> cls) {
354    Autonomous auto = cls.getAnnotation(Autonomous.class);
355    Teleop teleop = cls.getAnnotation(Teleop.class);
356    TestOpMode test = cls.getAnnotation(TestOpMode.class);
357    if (auto == null && teleop == null && test == null) {
358      throw new IllegalArgumentException(
359          "must be annotated with Autonomous, Teleop, or TestOpMode");
360    }
361    addAnnotatedOpModeImpl(cls, auto, teleop, test);
362  }
363
364  private void addAnnotatedOpModeClass(String name) {
365    // trim ".class" from end
366    String className = name.replace('/', '.').substring(0, name.length() - 6);
367    Class<?> cls;
368    try {
369      cls = Class.forName(className);
370    } catch (ClassNotFoundException e) {
371      return;
372    }
373    Autonomous auto = cls.getAnnotation(Autonomous.class);
374    Teleop teleop = cls.getAnnotation(Teleop.class);
375    TestOpMode test = cls.getAnnotation(TestOpMode.class);
376    if (auto == null && teleop == null && test == null) {
377      return;
378    }
379    try {
380      addAnnotatedOpModeImpl(cls, auto, teleop, test);
381    } catch (IllegalArgumentException e) {
382      reportAddOpModeError(cls, e.getMessage());
383    }
384  }
385
386  private void addAnnotatedOpModeClassesDir(File root, File dir, String packagePath) {
387    File[] files = dir.listFiles();
388    if (files == null) {
389      return;
390    }
391    for (File file : files) {
392      if (file.isDirectory()) {
393        addAnnotatedOpModeClassesDir(root, file, packagePath);
394      } else if (file.getName().endsWith(".class")) {
395        String relPath = root.toPath().relativize(file.toPath()).toString().replace('\\', '/');
396        addAnnotatedOpModeClass(packagePath + "." + relPath);
397      }
398    }
399  }
400
401  /**
402   * Scans for classes in the specified package and all nested packages that are annotated with
403   * {@link Autonomous}, {@link Teleop}, or {@link TestOpMode} and registers them. It's necessary to
404   * call publishOpModes() to make the added modes visible to the driver station.
405   *
406   * @param pkg package to scan
407   */
408  public void addAnnotatedOpModeClasses(Package pkg) {
409    String packageName = pkg.getName();
410    String packagePath = packageName.replace('.', '/');
411    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
412
413    try {
414      Enumeration<URL> resources = classLoader.getResources(packagePath);
415      while (resources.hasMoreElements()) {
416        URL resource = resources.nextElement();
417        if ("jar".equals(resource.getProtocol())) {
418          // Get path of JAR file from URL path (format "file:<path_to_jar_file>!/path_to_entry")
419          String jarPath = resource.getPath().substring(5, resource.getPath().indexOf('!'));
420          try (JarFile jar = new JarFile(jarPath)) {
421            Enumeration<JarEntry> entries = jar.entries();
422            while (entries.hasMoreElements()) {
423              String name = entries.nextElement().getName();
424              if (!name.startsWith(packagePath) || !name.endsWith(".class")) {
425                continue;
426              }
427              addAnnotatedOpModeClass(name);
428            }
429          }
430        } else if ("file".equals(resource.getProtocol())) {
431          // Handle .class files in directories
432          File dir = new File(resource.getPath());
433          if (dir.exists() && dir.isDirectory()) {
434            addAnnotatedOpModeClassesDir(dir, dir, packagePath);
435          }
436        }
437      }
438    } catch (IOException e) {
439      e.printStackTrace();
440    }
441  }
442
443  /**
444   * Removes an operating mode option. It's necessary to call publishOpModes() to make the removed
445   * mode no longer visible to the driver station.
446   *
447   * @param mode robot mode
448   * @param name name of the operating mode
449   */
450  public void removeOpMode(RobotMode mode, String name) {
451    long id = DriverStation.removeOpMode(mode, name);
452    if (id != 0) {
453      m_opModes.remove(id);
454    }
455  }
456
457  /** Publishes the operating mode options to the driver station. */
458  public void publishOpModes() {
459    DriverStation.publishOpModes();
460  }
461
462  /** Clears all operating mode options and publishes an empty list to the driver station. */
463  public void clearOpModes() {
464    DriverStation.clearOpModes();
465    m_opModes.clear();
466  }
467
468  /** Constructor. */
469  @SuppressWarnings("this-escape")
470  public OpModeRobot() {
471    // Scan for annotated opmode classes within the derived class's package and subpackages
472    addAnnotatedOpModeClasses(getClass().getPackage());
473    DriverStation.publishOpModes();
474  }
475
476  /**
477   * Function called exactly once after the DS is connected.
478   *
479   * <p>Code that needs to know the DS state should go here.
480   *
481   * <p>Users should override this method for initialization that needs to occur after the DS is
482   * connected, such as needing the alliance information.
483   */
484  public void driverStationConnected() {}
485
486  /**
487   * Function called periodically anytime when no opmode is selected, including when the Driver
488   * Station is disconnected.
489   */
490  public void nonePeriodic() {}
491
492  /**
493   * Background monitor thread. On mode/opmode change, this checks to see if the change is actually
494   * reflected in this class within a reasonable amount of time. If not, that means that the user
495   * code is stuck and we need to take action to try to get it to exit (up to and including program
496   * termination).
497   */
498  private void monitorThreadMain(Thread thr, long opmode, int event, int endEvent) {
499    ControlWord word = new ControlWord();
500    int[] events = {event, endEvent};
501    while (true) {
502      try {
503        int[] signaled = WPIUtilJNI.waitForObjects(events);
504        for (int val : signaled) {
505          if (val < 0) {
506            return; // handle destroyed
507          }
508        }
509      } catch (InterruptedException e) {
510        Thread.currentThread().interrupt();
511        return;
512      }
513
514      // did the opmode or enable state change?
515      DriverStationJNI.getUncachedControlWord(word);
516      if (!word.isEnabled() || word.getOpModeId() != opmode) {
517        break;
518      }
519    }
520
521    // call opmode stop
522    OpMode opMode = m_activeOpMode.get();
523    if (opMode != null) {
524      opMode.opModeStop();
525    }
526
527    events[0] = m_notifier;
528    NotifierJNI.setNotifierAlarm(m_notifier, 200000, 0, false, true); // 200 ms
529    try {
530      int[] signaled = WPIUtilJNI.waitForObjects(events);
531      for (int val : signaled) {
532        if (val < 0 || val == endEvent) {
533          return; // transitioned, or handle destroyed
534        }
535      }
536    } catch (InterruptedException e) {
537      Thread.currentThread().interrupt();
538      return;
539    }
540
541    // if it hasn't transitioned after 200 ms, call thread.interrupt()
542    DriverStation.reportError("OpMode did not exit, interrupting thread", false);
543    thr.interrupt();
544
545    NotifierJNI.setNotifierAlarm(m_notifier, 800000, 0, false, true); // 800 ms
546    try {
547      int[] signaled = WPIUtilJNI.waitForObjects(events);
548      for (int val : signaled) {
549        if (val < 0 || val == endEvent) {
550          return; // transitioned, or handle destroyed
551        }
552      }
553    } catch (InterruptedException e) {
554      Thread.currentThread().interrupt();
555      return;
556    }
557
558    // if it hasn't transitioned after 1 second, terminate the program
559    DriverStation.reportError("OpMode did not exit, terminating program", false);
560    HAL.terminate();
561    HAL.shutdown();
562    System.exit(0);
563  }
564
565  /** Provide an alternate "main loop" via startCompetition(). */
566  @Override
567  public final void startCompetition() {
568    System.out.println("********** Robot program startup complete **********");
569
570    int event = WPIUtilJNI.createEvent(false, false);
571    DriverStationJNI.provideNewDataEventHandle(event);
572
573    m_notifier = NotifierJNI.createNotifier();
574    NotifierJNI.setNotifierName(m_notifier, "OpModeRobot");
575
576    try {
577      // Implement the opmode lifecycle
578      long lastModeId = -1;
579      boolean calledObserveUserProgramStarting = false;
580      boolean calledDriverStationConnected = false;
581      int[] events = {event, m_notifier};
582      while (true) {
583        // Wait for new data from the driver station, with 50 ms timeout
584        NotifierJNI.setNotifierAlarm(m_notifier, 50000, 0, false, true);
585
586        // Call observeUserProgramStarting() here as a one-shot to ensure it is called after the
587        // notifier alarm is set.  The notifier alarm is set using relative time, so tests that
588        // wait on the user program to start and then step time won't work correctly if we call
589        // this before setting the alarm.
590        if (!calledObserveUserProgramStarting) {
591          calledObserveUserProgramStarting = true;
592          DriverStationJNI.observeUserProgramStarting();
593        }
594
595        try {
596          int[] signaled = WPIUtilJNI.waitForObjects(events);
597          for (int val : signaled) {
598            if (val < 0) {
599              return; // handle destroyed
600            }
601          }
602        } catch (InterruptedException ex) {
603          Thread.currentThread().interrupt();
604          break;
605        }
606
607        // Get the latest control word and opmode
608        DriverStation.refreshData();
609        DriverStation.refreshControlWordFromCache(m_word);
610
611        if (!calledDriverStationConnected && m_word.isDSAttached()) {
612          calledDriverStationConnected = true;
613          driverStationConnected();
614        }
615
616        long modeId;
617        if (!m_word.isDSAttached()) {
618          modeId = 0;
619        } else {
620          modeId = m_word.getOpModeId();
621        }
622
623        OpMode opMode = m_activeOpMode.get();
624        if (opMode == null || modeId != lastModeId) {
625          if (opMode != null) {
626            // no or different opmode selected
627            m_activeOpMode.set(null);
628            opMode.opModeClose();
629          }
630
631          if (modeId == 0) {
632            // no opmode selected
633            nonePeriodic();
634            DriverStationJNI.observeUserProgram(m_word.getNative());
635            continue;
636          }
637
638          OpModeFactory factory = m_opModes.get(modeId);
639          if (factory == null) {
640            DriverStation.reportError("No OpMode found for mode " + modeId, false);
641            m_word.setOpModeId(0);
642            DriverStationJNI.observeUserProgram(m_word.getNative());
643            continue;
644          }
645
646          // Instantiate the opmode
647          System.out.println("********** Starting OpMode " + factory.name() + " **********");
648          opMode = factory.supplier().get();
649          if (opMode == null) {
650            // could not construct
651            m_word.setOpModeId(0);
652            DriverStationJNI.observeUserProgram(m_word.getNative());
653            continue;
654          }
655          m_activeOpMode.set(opMode);
656          lastModeId = modeId;
657          // Ensure disabledPeriodic is always called at least once
658          opMode.disabledPeriodic();
659        }
660
661        DriverStationJNI.observeUserProgram(m_word.getNative());
662
663        if (m_word.isEnabled()) {
664          // When enabled, call the opmode run function, then close and clear
665          int endMonitor = WPIUtilJNI.createEvent(true, false);
666          Thread curThread = Thread.currentThread();
667          Thread monitor =
668              new Thread(
669                  () -> {
670                    monitorThreadMain(curThread, modeId, event, endMonitor);
671                  });
672          monitor.start();
673          try {
674            opMode.opModeRun(modeId);
675          } catch (InterruptedException e) {
676            // ignored
677          } finally {
678            Thread.interrupted();
679            WPIUtilJNI.destroyEvent(endMonitor);
680            try {
681              monitor.join();
682            } catch (InterruptedException e) {
683              Thread.currentThread().interrupt();
684            }
685          }
686          opMode = m_activeOpMode.getAndSet(null);
687          if (opMode != null) {
688            opMode.opModeClose();
689          }
690        } else {
691          // When disabled, call the disabledPeriodic function
692          opMode.disabledPeriodic();
693        }
694      }
695    } finally {
696      DriverStationJNI.removeNewDataEventHandle(event);
697      WPIUtilJNI.destroyEvent(event);
698      NotifierJNI.destroyNotifier(m_notifier);
699    }
700  }
701
702  /** Ends the main loop in startCompetition(). */
703  @Override
704  public final void endCompetition() {
705    NotifierJNI.destroyNotifier(m_notifier);
706    OpMode opMode = m_activeOpMode.get();
707    if (opMode != null) {
708      opMode.opModeStop();
709    }
710  }
711}