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.wpilibj2.command;
006
007import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
008
009import edu.wpi.first.hal.FRCNetComm.tInstances;
010import edu.wpi.first.hal.FRCNetComm.tResourceType;
011import edu.wpi.first.hal.HAL;
012import edu.wpi.first.networktables.IntegerArrayEntry;
013import edu.wpi.first.networktables.IntegerArrayPublisher;
014import edu.wpi.first.networktables.IntegerArrayTopic;
015import edu.wpi.first.networktables.NTSendable;
016import edu.wpi.first.networktables.NTSendableBuilder;
017import edu.wpi.first.networktables.StringArrayPublisher;
018import edu.wpi.first.networktables.StringArrayTopic;
019import edu.wpi.first.util.sendable.SendableRegistry;
020import edu.wpi.first.wpilibj.DriverStation;
021import edu.wpi.first.wpilibj.RobotBase;
022import edu.wpi.first.wpilibj.RobotState;
023import edu.wpi.first.wpilibj.TimedRobot;
024import edu.wpi.first.wpilibj.Watchdog;
025import edu.wpi.first.wpilibj.event.EventLoop;
026import edu.wpi.first.wpilibj.livewindow.LiveWindow;
027import edu.wpi.first.wpilibj2.command.Command.InterruptionBehavior;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.Iterator;
032import java.util.LinkedHashMap;
033import java.util.LinkedHashSet;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037import java.util.WeakHashMap;
038import java.util.function.Consumer;
039
040/**
041 * The scheduler responsible for running {@link Command}s. A Command-based robot should call {@link
042 * CommandScheduler#run()} on the singleton instance in its periodic block in order to run commands
043 * synchronously from the main loop. Subsystems should be registered with the scheduler using {@link
044 * CommandScheduler#registerSubsystem(Subsystem...)} in order for their {@link Subsystem#periodic()}
045 * methods to be called and for their default commands to be scheduled.
046 *
047 * <p>This class is provided by the NewCommands VendorDep
048 */
049public final class CommandScheduler implements NTSendable, AutoCloseable {
050  /** The Singleton Instance. */
051  private static CommandScheduler instance;
052
053  /**
054   * Returns the Scheduler instance.
055   *
056   * @return the instance
057   */
058  public static synchronized CommandScheduler getInstance() {
059    if (instance == null) {
060      instance = new CommandScheduler();
061    }
062    return instance;
063  }
064
065  private final Set<Command> m_composedCommands = Collections.newSetFromMap(new WeakHashMap<>());
066
067  // A set of the currently-running commands.
068  private final Set<Command> m_scheduledCommands = new LinkedHashSet<>();
069
070  // A map from required subsystems to their requiring commands. Also used as a set of the
071  // currently-required subsystems.
072  private final Map<Subsystem, Command> m_requirements = new LinkedHashMap<>();
073
074  // A map from subsystems registered with the scheduler to their default commands.  Also used
075  // as a list of currently-registered subsystems.
076  private final Map<Subsystem, Command> m_subsystems = new LinkedHashMap<>();
077
078  private final EventLoop m_defaultButtonLoop = new EventLoop();
079  // The set of currently-registered buttons that will be polled every iteration.
080  private EventLoop m_activeButtonLoop = m_defaultButtonLoop;
081
082  private boolean m_disabled;
083
084  // Lists of user-supplied actions to be executed on scheduling events for every command.
085  private final List<Consumer<Command>> m_initActions = new ArrayList<>();
086  private final List<Consumer<Command>> m_executeActions = new ArrayList<>();
087  private final List<Consumer<Command>> m_interruptActions = new ArrayList<>();
088  private final List<Consumer<Command>> m_finishActions = new ArrayList<>();
089
090  // Flag and queues for avoiding ConcurrentModificationException if commands are
091  // scheduled/canceled during run
092  private boolean m_inRunLoop;
093  private final Set<Command> m_toSchedule = new LinkedHashSet<>();
094  private final List<Command> m_toCancel = new ArrayList<>();
095
096  private final Watchdog m_watchdog = new Watchdog(TimedRobot.kDefaultPeriod, () -> {});
097
098  CommandScheduler() {
099    HAL.report(tResourceType.kResourceType_Command, tInstances.kCommand2_Scheduler);
100    SendableRegistry.addLW(this, "Scheduler");
101    LiveWindow.setEnabledListener(
102        () -> {
103          disable();
104          cancelAll();
105        });
106    LiveWindow.setDisabledListener(this::enable);
107  }
108
109  /**
110   * Changes the period of the loop overrun watchdog. This should be kept in sync with the
111   * TimedRobot period.
112   *
113   * @param period Period in seconds.
114   */
115  public void setPeriod(double period) {
116    m_watchdog.setTimeout(period);
117  }
118
119  @Override
120  public void close() {
121    SendableRegistry.remove(this);
122    LiveWindow.setEnabledListener(null);
123    LiveWindow.setDisabledListener(null);
124  }
125
126  /**
127   * Get the default button poll.
128   *
129   * @return a reference to the default {@link EventLoop} object polling buttons.
130   */
131  public EventLoop getDefaultButtonLoop() {
132    return m_defaultButtonLoop;
133  }
134
135  /**
136   * Get the active button poll.
137   *
138   * @return a reference to the current {@link EventLoop} object polling buttons.
139   */
140  public EventLoop getActiveButtonLoop() {
141    return m_activeButtonLoop;
142  }
143
144  /**
145   * Replace the button poll with another one.
146   *
147   * @param loop the new button polling loop object.
148   */
149  public void setActiveButtonLoop(EventLoop loop) {
150    m_activeButtonLoop =
151        requireNonNullParam(loop, "loop", "CommandScheduler" + ".replaceButtonEventLoop");
152  }
153
154  /**
155   * Adds a button binding to the scheduler, which will be polled to schedule commands.
156   *
157   * @param button The button to add
158   * @deprecated Use {@link edu.wpi.first.wpilibj2.command.button.Trigger}
159   */
160  @Deprecated(since = "2023")
161  public void addButton(Runnable button) {
162    m_activeButtonLoop.bind(requireNonNullParam(button, "button", "addButton"));
163  }
164
165  /**
166   * Removes all button bindings from the scheduler.
167   *
168   * @deprecated call {@link EventLoop#clear()} on {@link #getActiveButtonLoop()} directly instead.
169   */
170  @Deprecated(since = "2023")
171  public void clearButtons() {
172    m_activeButtonLoop.clear();
173  }
174
175  /**
176   * Initializes a given command, adds its requirements to the list, and performs the init actions.
177   *
178   * @param command The command to initialize
179   * @param requirements The command requirements
180   */
181  private void initCommand(Command command, Set<Subsystem> requirements) {
182    m_scheduledCommands.add(command);
183    for (Subsystem requirement : requirements) {
184      m_requirements.put(requirement, command);
185    }
186    command.initialize();
187    for (Consumer<Command> action : m_initActions) {
188      action.accept(command);
189    }
190
191    m_watchdog.addEpoch(command.getName() + ".initialize()");
192  }
193
194  /**
195   * Schedules a command for execution. Does nothing if the command is already scheduled. If a
196   * command's requirements are not available, it will only be started if all the commands currently
197   * using those requirements have been scheduled as interruptible. If this is the case, they will
198   * be interrupted and the command will be scheduled.
199   *
200   * @param command the command to schedule. If null, no-op.
201   */
202  private void schedule(Command command) {
203    if (command == null) {
204      DriverStation.reportWarning("Tried to schedule a null command", true);
205      return;
206    }
207    if (m_inRunLoop) {
208      m_toSchedule.add(command);
209      return;
210    }
211
212    requireNotComposed(command);
213
214    // Do nothing if the scheduler is disabled, the robot is disabled and the command doesn't
215    // run when disabled, or the command is already scheduled.
216    if (m_disabled
217        || isScheduled(command)
218        || RobotState.isDisabled() && !command.runsWhenDisabled()) {
219      return;
220    }
221
222    Set<Subsystem> requirements = command.getRequirements();
223
224    // Schedule the command if the requirements are not currently in-use.
225    if (Collections.disjoint(m_requirements.keySet(), requirements)) {
226      initCommand(command, requirements);
227    } else {
228      // Else check if the requirements that are in use have all have interruptible commands,
229      // and if so, interrupt those commands and schedule the new command.
230      for (Subsystem requirement : requirements) {
231        Command requiring = requiring(requirement);
232        if (requiring != null
233            && requiring.getInterruptionBehavior() == InterruptionBehavior.kCancelIncoming) {
234          return;
235        }
236      }
237      for (Subsystem requirement : requirements) {
238        Command requiring = requiring(requirement);
239        if (requiring != null) {
240          cancel(requiring);
241        }
242      }
243      initCommand(command, requirements);
244    }
245  }
246
247  /**
248   * Schedules multiple commands for execution. Does nothing for commands already scheduled.
249   *
250   * @param commands the commands to schedule. No-op on null.
251   */
252  public void schedule(Command... commands) {
253    for (Command command : commands) {
254      schedule(command);
255    }
256  }
257
258  /**
259   * Runs a single iteration of the scheduler. The execution occurs in the following order:
260   *
261   * <p>Subsystem periodic methods are called.
262   *
263   * <p>Button bindings are polled, and new commands are scheduled from them.
264   *
265   * <p>Currently-scheduled commands are executed.
266   *
267   * <p>End conditions are checked on currently-scheduled commands, and commands that are finished
268   * have their end methods called and are removed.
269   *
270   * <p>Any subsystems not being used as requirements have their default methods started.
271   */
272  public void run() {
273    if (m_disabled) {
274      return;
275    }
276    m_watchdog.reset();
277
278    // Run the periodic method of all registered subsystems.
279    for (Subsystem subsystem : m_subsystems.keySet()) {
280      subsystem.periodic();
281      if (RobotBase.isSimulation()) {
282        subsystem.simulationPeriodic();
283      }
284      m_watchdog.addEpoch(subsystem.getClass().getSimpleName() + ".periodic()");
285    }
286
287    // Cache the active instance to avoid concurrency problems if setActiveLoop() is called from
288    // inside the button bindings.
289    EventLoop loopCache = m_activeButtonLoop;
290    // Poll buttons for new commands to add.
291    loopCache.poll();
292    m_watchdog.addEpoch("buttons.run()");
293
294    m_inRunLoop = true;
295    // Run scheduled commands, remove finished commands.
296    for (Iterator<Command> iterator = m_scheduledCommands.iterator(); iterator.hasNext(); ) {
297      Command command = iterator.next();
298
299      if (!command.runsWhenDisabled() && RobotState.isDisabled()) {
300        command.end(true);
301        for (Consumer<Command> action : m_interruptActions) {
302          action.accept(command);
303        }
304        m_requirements.keySet().removeAll(command.getRequirements());
305        iterator.remove();
306        m_watchdog.addEpoch(command.getName() + ".end(true)");
307        continue;
308      }
309
310      command.execute();
311      for (Consumer<Command> action : m_executeActions) {
312        action.accept(command);
313      }
314      m_watchdog.addEpoch(command.getName() + ".execute()");
315      if (command.isFinished()) {
316        command.end(false);
317        for (Consumer<Command> action : m_finishActions) {
318          action.accept(command);
319        }
320        iterator.remove();
321
322        m_requirements.keySet().removeAll(command.getRequirements());
323        m_watchdog.addEpoch(command.getName() + ".end(false)");
324      }
325    }
326    m_inRunLoop = false;
327
328    // Schedule/cancel commands from queues populated during loop
329    for (Command command : m_toSchedule) {
330      schedule(command);
331    }
332
333    for (Command command : m_toCancel) {
334      cancel(command);
335    }
336
337    m_toSchedule.clear();
338    m_toCancel.clear();
339
340    // Add default commands for un-required registered subsystems.
341    for (Map.Entry<Subsystem, Command> subsystemCommand : m_subsystems.entrySet()) {
342      if (!m_requirements.containsKey(subsystemCommand.getKey())
343          && subsystemCommand.getValue() != null) {
344        schedule(subsystemCommand.getValue());
345      }
346    }
347
348    m_watchdog.disable();
349    if (m_watchdog.isExpired()) {
350      System.out.println("CommandScheduler loop overrun");
351      m_watchdog.printEpochs();
352    }
353  }
354
355  /**
356   * Registers subsystems with the scheduler. This must be called for the subsystem's periodic block
357   * to run when the scheduler is run, and for the subsystem's default command to be scheduled. It
358   * is recommended to call this from the constructor of your subsystem implementations.
359   *
360   * @param subsystems the subsystem to register
361   */
362  public void registerSubsystem(Subsystem... subsystems) {
363    for (Subsystem subsystem : subsystems) {
364      if (subsystem == null) {
365        DriverStation.reportWarning("Tried to register a null subsystem", true);
366        continue;
367      }
368      if (m_subsystems.containsKey(subsystem)) {
369        DriverStation.reportWarning("Tried to register an already-registered subsystem", true);
370        continue;
371      }
372      m_subsystems.put(subsystem, null);
373    }
374  }
375
376  /**
377   * Un-registers subsystems with the scheduler. The subsystem will no longer have its periodic
378   * block called, and will not have its default command scheduled.
379   *
380   * @param subsystems the subsystem to un-register
381   */
382  public void unregisterSubsystem(Subsystem... subsystems) {
383    m_subsystems.keySet().removeAll(Set.of(subsystems));
384  }
385
386  /**
387   * Sets the default command for a subsystem. Registers that subsystem if it is not already
388   * registered. Default commands will run whenever there is no other command currently scheduled
389   * that requires the subsystem. Default commands should be written to never end (i.e. their {@link
390   * Command#isFinished()} method should return false), as they would simply be re-scheduled if they
391   * do. Default commands must also require their subsystem.
392   *
393   * @param subsystem the subsystem whose default command will be set
394   * @param defaultCommand the default command to associate with the subsystem
395   */
396  public void setDefaultCommand(Subsystem subsystem, Command defaultCommand) {
397    if (subsystem == null) {
398      DriverStation.reportWarning("Tried to set a default command for a null subsystem", true);
399      return;
400    }
401    if (defaultCommand == null) {
402      DriverStation.reportWarning("Tried to set a null default command", true);
403      return;
404    }
405
406    requireNotComposed(defaultCommand);
407
408    if (!defaultCommand.getRequirements().contains(subsystem)) {
409      throw new IllegalArgumentException("Default commands must require their subsystem!");
410    }
411
412    if (defaultCommand.getInterruptionBehavior() == InterruptionBehavior.kCancelIncoming) {
413      DriverStation.reportWarning(
414          "Registering a non-interruptible default command!\n"
415              + "This will likely prevent any other commands from requiring this subsystem.",
416          true);
417      // Warn, but allow -- there might be a use case for this.
418    }
419
420    m_subsystems.put(subsystem, defaultCommand);
421  }
422
423  /**
424   * Removes the default command for a subsystem. The current default command will run until another
425   * command is scheduled that requires the subsystem, at which point the current default command
426   * will not be re-scheduled.
427   *
428   * @param subsystem the subsystem whose default command will be removed
429   */
430  public void removeDefaultCommand(Subsystem subsystem) {
431    if (subsystem == null) {
432      DriverStation.reportWarning("Tried to remove a default command for a null subsystem", true);
433      return;
434    }
435
436    m_subsystems.put(subsystem, null);
437  }
438
439  /**
440   * Gets the default command associated with this subsystem. Null if this subsystem has no default
441   * command associated with it.
442   *
443   * @param subsystem the subsystem to inquire about
444   * @return the default command associated with the subsystem
445   */
446  public Command getDefaultCommand(Subsystem subsystem) {
447    return m_subsystems.get(subsystem);
448  }
449
450  /**
451   * Cancels commands. The scheduler will only call {@link Command#end(boolean)} method of the
452   * canceled command with {@code true}, indicating they were canceled (as opposed to finishing
453   * normally).
454   *
455   * <p>Commands will be canceled regardless of {@link InterruptionBehavior interruption behavior}.
456   *
457   * @param commands the commands to cancel
458   */
459  public void cancel(Command... commands) {
460    if (m_inRunLoop) {
461      m_toCancel.addAll(List.of(commands));
462      return;
463    }
464
465    for (Command command : commands) {
466      if (command == null) {
467        DriverStation.reportWarning("Tried to cancel a null command", true);
468        continue;
469      }
470      if (!isScheduled(command)) {
471        continue;
472      }
473
474      m_scheduledCommands.remove(command);
475      m_requirements.keySet().removeAll(command.getRequirements());
476      command.end(true);
477      for (Consumer<Command> action : m_interruptActions) {
478        action.accept(command);
479      }
480      m_watchdog.addEpoch(command.getName() + ".end(true)");
481    }
482  }
483
484  /** Cancels all commands that are currently scheduled. */
485  public void cancelAll() {
486    // Copy to array to avoid concurrent modification.
487    cancel(m_scheduledCommands.toArray(new Command[0]));
488  }
489
490  /**
491   * Whether the given commands are running. Note that this only works on commands that are directly
492   * scheduled by the scheduler; it will not work on commands inside compositions, as the scheduler
493   * does not see them.
494   *
495   * @param commands the command to query
496   * @return whether the command is currently scheduled
497   */
498  public boolean isScheduled(Command... commands) {
499    return m_scheduledCommands.containsAll(Set.of(commands));
500  }
501
502  /**
503   * Returns the command currently requiring a given subsystem. Null if no command is currently
504   * requiring the subsystem
505   *
506   * @param subsystem the subsystem to be inquired about
507   * @return the command currently requiring the subsystem, or null if no command is currently
508   *     scheduled
509   */
510  public Command requiring(Subsystem subsystem) {
511    return m_requirements.get(subsystem);
512  }
513
514  /** Disables the command scheduler. */
515  public void disable() {
516    m_disabled = true;
517  }
518
519  /** Enables the command scheduler. */
520  public void enable() {
521    m_disabled = false;
522  }
523
524  /**
525   * Adds an action to perform on the initialization of any command by the scheduler.
526   *
527   * @param action the action to perform
528   */
529  public void onCommandInitialize(Consumer<Command> action) {
530    m_initActions.add(requireNonNullParam(action, "action", "onCommandInitialize"));
531  }
532
533  /**
534   * Adds an action to perform on the execution of any command by the scheduler.
535   *
536   * @param action the action to perform
537   */
538  public void onCommandExecute(Consumer<Command> action) {
539    m_executeActions.add(requireNonNullParam(action, "action", "onCommandExecute"));
540  }
541
542  /**
543   * Adds an action to perform on the interruption of any command by the scheduler.
544   *
545   * @param action the action to perform
546   */
547  public void onCommandInterrupt(Consumer<Command> action) {
548    m_interruptActions.add(requireNonNullParam(action, "action", "onCommandInterrupt"));
549  }
550
551  /**
552   * Adds an action to perform on the finishing of any command by the scheduler.
553   *
554   * @param action the action to perform
555   */
556  public void onCommandFinish(Consumer<Command> action) {
557    m_finishActions.add(requireNonNullParam(action, "action", "onCommandFinish"));
558  }
559
560  /**
561   * Register commands as composed. An exception will be thrown if these commands are scheduled
562   * directly or added to a composition.
563   *
564   * @param commands the commands to register
565   * @throws IllegalArgumentException if the given commands have already been composed.
566   */
567  public void registerComposedCommands(Command... commands) {
568    var commandSet = Set.of(commands);
569    requireNotComposed(commandSet);
570    m_composedCommands.addAll(commandSet);
571  }
572
573  /**
574   * Clears the list of composed commands, allowing all commands to be freely used again.
575   *
576   * <p>WARNING: Using this haphazardly can result in unexpected/undesirable behavior. Do not use
577   * this unless you fully understand what you are doing.
578   */
579  public void clearComposedCommands() {
580    m_composedCommands.clear();
581  }
582
583  /**
584   * Removes a single command from the list of composed commands, allowing it to be freely used
585   * again.
586   *
587   * <p>WARNING: Using this haphazardly can result in unexpected/undesirable behavior. Do not use
588   * this unless you fully understand what you are doing.
589   *
590   * @param command the command to remove from the list of grouped commands
591   */
592  public void removeComposedCommand(Command command) {
593    m_composedCommands.remove(command);
594  }
595
596  /**
597   * Requires that the specified command hasn't been already added to a composition.
598   *
599   * @param command The command to check
600   * @throws IllegalArgumentException if the given commands have already been composed.
601   */
602  public void requireNotComposed(Command command) {
603    if (m_composedCommands.contains(command)) {
604      throw new IllegalArgumentException(
605          "Commands that have been composed may not be added to another composition or scheduled "
606              + "individually!");
607    }
608  }
609
610  /**
611   * Requires that the specified commands not have been already added to a composition.
612   *
613   * @param commands The commands to check
614   * @throws IllegalArgumentException if the given commands have already been composed.
615   */
616  public void requireNotComposed(Collection<Command> commands) {
617    if (!Collections.disjoint(commands, getComposedCommands())) {
618      throw new IllegalArgumentException(
619          "Commands that have been composed may not be added to another composition or scheduled "
620              + "individually!");
621    }
622  }
623
624  /**
625   * Check if the given command has been composed.
626   *
627   * @param command The command to check
628   * @return true if composed
629   */
630  public boolean isComposed(Command command) {
631    return getComposedCommands().contains(command);
632  }
633
634  Set<Command> getComposedCommands() {
635    return m_composedCommands;
636  }
637
638  @Override
639  public void initSendable(NTSendableBuilder builder) {
640    builder.setSmartDashboardType("Scheduler");
641    final StringArrayPublisher namesPub = new StringArrayTopic(builder.getTopic("Names")).publish();
642    final IntegerArrayPublisher idsPub = new IntegerArrayTopic(builder.getTopic("Ids")).publish();
643    final IntegerArrayEntry cancelEntry =
644        new IntegerArrayTopic(builder.getTopic("Cancel")).getEntry(new long[] {});
645    builder.addCloseable(namesPub);
646    builder.addCloseable(idsPub);
647    builder.addCloseable(cancelEntry);
648    builder.setUpdateTable(
649        () -> {
650          if (namesPub == null || idsPub == null || cancelEntry == null) {
651            return;
652          }
653
654          Map<Long, Command> ids = new LinkedHashMap<>();
655          List<String> names = new ArrayList<>();
656          long[] ids2 = new long[m_scheduledCommands.size()];
657
658          int i = 0;
659          for (Command command : m_scheduledCommands) {
660            long id = command.hashCode();
661            ids.put(id, command);
662            names.add(command.getName());
663            ids2[i] = id;
664            i++;
665          }
666
667          long[] toCancel = cancelEntry.get();
668          if (toCancel.length > 0) {
669            for (long hash : toCancel) {
670              cancel(ids.get(hash));
671              ids.remove(hash);
672            }
673            cancelEntry.set(new long[] {});
674          }
675
676          namesPub.set(names.toArray(new String[] {}));
677          idsPub.set(ids2);
678        });
679  }
680}