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 edu.wpi.first.hal.FRCNetComm.tInstances;
008import edu.wpi.first.hal.FRCNetComm.tResourceType;
009import edu.wpi.first.hal.HAL;
010import edu.wpi.first.networktables.NTSendable;
011import edu.wpi.first.networktables.NTSendableBuilder;
012import edu.wpi.first.networktables.NetworkTableEntry;
013import edu.wpi.first.util.sendable.SendableRegistry;
014import edu.wpi.first.wpilibj.RobotBase;
015import edu.wpi.first.wpilibj.RobotState;
016import edu.wpi.first.wpilibj.TimedRobot;
017import edu.wpi.first.wpilibj.Watchdog;
018import edu.wpi.first.wpilibj.livewindow.LiveWindow;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Iterator;
023import java.util.LinkedHashMap;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.function.Consumer;
029
030/**
031 * The scheduler responsible for running {@link Command}s. A Command-based robot should call {@link
032 * CommandScheduler#run()} on the singleton instance in its periodic block in order to run commands
033 * synchronously from the main loop. Subsystems should be registered with the scheduler using {@link
034 * CommandScheduler#registerSubsystem(Subsystem...)} in order for their {@link Subsystem#periodic()}
035 * methods to be called and for their default commands to be scheduled.
036 */
037public final class CommandScheduler implements NTSendable, AutoCloseable {
038  /** The Singleton Instance. */
039  private static CommandScheduler instance;
040
041  /**
042   * Returns the Scheduler instance.
043   *
044   * @return the instance
045   */
046  public static synchronized CommandScheduler getInstance() {
047    if (instance == null) {
048      instance = new CommandScheduler();
049    }
050    return instance;
051  }
052
053  // A map from commands to their scheduling state.  Also used as a set of the currently-running
054  // commands.
055  private final Map<Command, CommandState> m_scheduledCommands = new LinkedHashMap<>();
056
057  // A map from required subsystems to their requiring commands.  Also used as a set of the
058  // currently-required subsystems.
059  private final Map<Subsystem, Command> m_requirements = new LinkedHashMap<>();
060
061  // A map from subsystems registered with the scheduler to their default commands.  Also used
062  // as a list of currently-registered subsystems.
063  private final Map<Subsystem, Command> m_subsystems = new LinkedHashMap<>();
064
065  // The set of currently-registered buttons that will be polled every iteration.
066  private final Collection<Runnable> m_buttons = new LinkedHashSet<>();
067
068  private boolean m_disabled;
069
070  // Lists of user-supplied actions to be executed on scheduling events for every command.
071  private final List<Consumer<Command>> m_initActions = new ArrayList<>();
072  private final List<Consumer<Command>> m_executeActions = new ArrayList<>();
073  private final List<Consumer<Command>> m_interruptActions = new ArrayList<>();
074  private final List<Consumer<Command>> m_finishActions = new ArrayList<>();
075
076  // Flag and queues for avoiding ConcurrentModificationException if commands are
077  // scheduled/canceled during run
078  private boolean m_inRunLoop;
079  private final Map<Command, Boolean> m_toSchedule = new LinkedHashMap<>();
080  private final List<Command> m_toCancel = new ArrayList<>();
081
082  private final Watchdog m_watchdog = new Watchdog(TimedRobot.kDefaultPeriod, () -> {});
083
084  CommandScheduler() {
085    HAL.report(tResourceType.kResourceType_Command, tInstances.kCommand2_Scheduler);
086    SendableRegistry.addLW(this, "Scheduler");
087    LiveWindow.setEnabledListener(
088        () -> {
089          disable();
090          cancelAll();
091        });
092    LiveWindow.setDisabledListener(
093        () -> {
094          enable();
095        });
096  }
097
098  /**
099   * Changes the period of the loop overrun watchdog. This should be be kept in sync with the
100   * TimedRobot period.
101   *
102   * @param period Period in seconds.
103   */
104  public void setPeriod(double period) {
105    m_watchdog.setTimeout(period);
106  }
107
108  @Override
109  public void close() {
110    SendableRegistry.remove(this);
111    LiveWindow.setEnabledListener(null);
112    LiveWindow.setDisabledListener(null);
113  }
114
115  /**
116   * Adds a button binding to the scheduler, which will be polled to schedule commands.
117   *
118   * @param button The button to add
119   */
120  public void addButton(Runnable button) {
121    m_buttons.add(button);
122  }
123
124  /** Removes all button bindings from the scheduler. */
125  public void clearButtons() {
126    m_buttons.clear();
127  }
128
129  /**
130   * Initializes a given command, adds its requirements to the list, and performs the init actions.
131   *
132   * @param command The command to initialize
133   * @param interruptible Whether the command is interruptible
134   * @param requirements The command requirements
135   */
136  private void initCommand(Command command, boolean interruptible, Set<Subsystem> requirements) {
137    CommandState scheduledCommand = new CommandState(interruptible);
138    m_scheduledCommands.put(command, scheduledCommand);
139    command.initialize();
140    for (Subsystem requirement : requirements) {
141      m_requirements.put(requirement, command);
142    }
143    for (Consumer<Command> action : m_initActions) {
144      action.accept(command);
145    }
146
147    m_watchdog.addEpoch(command.getName() + ".initialize()");
148  }
149
150  /**
151   * Schedules a command for execution. Does nothing if the command is already scheduled. If a
152   * command's requirements are not available, it will only be started if all the commands currently
153   * using those requirements have been scheduled as interruptible. If this is the case, they will
154   * be interrupted and the command will be scheduled.
155   *
156   * @param interruptible whether this command can be interrupted
157   * @param command the command to schedule
158   */
159  private void schedule(boolean interruptible, Command command) {
160    if (m_inRunLoop) {
161      m_toSchedule.put(command, interruptible);
162      return;
163    }
164
165    if (CommandGroupBase.getGroupedCommands().contains(command)) {
166      throw new IllegalArgumentException(
167          "A command that is part of a command group cannot be independently scheduled");
168    }
169
170    // Do nothing if the scheduler is disabled, the robot is disabled and the command doesn't
171    // run when disabled, or the command is already scheduled.
172    if (m_disabled
173        || RobotState.isDisabled() && !command.runsWhenDisabled()
174        || m_scheduledCommands.containsKey(command)) {
175      return;
176    }
177
178    Set<Subsystem> requirements = command.getRequirements();
179
180    // Schedule the command if the requirements are not currently in-use.
181    if (Collections.disjoint(m_requirements.keySet(), requirements)) {
182      initCommand(command, interruptible, requirements);
183    } else {
184      // Else check if the requirements that are in use have all have interruptible commands,
185      // and if so, interrupt those commands and schedule the new command.
186      for (Subsystem requirement : requirements) {
187        if (m_requirements.containsKey(requirement)
188            && !m_scheduledCommands.get(m_requirements.get(requirement)).isInterruptible()) {
189          return;
190        }
191      }
192      for (Subsystem requirement : requirements) {
193        if (m_requirements.containsKey(requirement)) {
194          cancel(m_requirements.get(requirement));
195        }
196      }
197      initCommand(command, interruptible, requirements);
198    }
199  }
200
201  /**
202   * Schedules multiple commands for execution. Does nothing if the command is already scheduled. If
203   * a command's requirements are not available, it will only be started if all the commands
204   * currently using those requirements have been scheduled as interruptible. If this is the case,
205   * they will be interrupted and the command will be scheduled.
206   *
207   * @param interruptible whether the commands should be interruptible
208   * @param commands the commands to schedule
209   */
210  public void schedule(boolean interruptible, Command... commands) {
211    for (Command command : commands) {
212      schedule(interruptible, command);
213    }
214  }
215
216  /**
217   * Schedules multiple commands for execution, with interruptible defaulted to true. Does nothing
218   * if the command is already scheduled.
219   *
220   * @param commands the commands to schedule
221   */
222  public void schedule(Command... commands) {
223    schedule(true, commands);
224  }
225
226  /**
227   * Runs a single iteration of the scheduler. The execution occurs in the following order:
228   *
229   * <p>Subsystem periodic methods are called.
230   *
231   * <p>Button bindings are polled, and new commands are scheduled from them.
232   *
233   * <p>Currently-scheduled commands are executed.
234   *
235   * <p>End conditions are checked on currently-scheduled commands, and commands that are finished
236   * have their end methods called and are removed.
237   *
238   * <p>Any subsystems not being used as requirements have their default methods started.
239   */
240  public void run() {
241    if (m_disabled) {
242      return;
243    }
244    m_watchdog.reset();
245
246    // Run the periodic method of all registered subsystems.
247    for (Subsystem subsystem : m_subsystems.keySet()) {
248      subsystem.periodic();
249      if (RobotBase.isSimulation()) {
250        subsystem.simulationPeriodic();
251      }
252      m_watchdog.addEpoch(subsystem.getClass().getSimpleName() + ".periodic()");
253    }
254
255    // Poll buttons for new commands to add.
256    for (Runnable button : m_buttons) {
257      button.run();
258    }
259    m_watchdog.addEpoch("buttons.run()");
260
261    m_inRunLoop = true;
262    // Run scheduled commands, remove finished commands.
263    for (Iterator<Command> iterator = m_scheduledCommands.keySet().iterator();
264        iterator.hasNext(); ) {
265      Command command = iterator.next();
266
267      if (!command.runsWhenDisabled() && RobotState.isDisabled()) {
268        command.end(true);
269        for (Consumer<Command> action : m_interruptActions) {
270          action.accept(command);
271        }
272        m_requirements.keySet().removeAll(command.getRequirements());
273        iterator.remove();
274        m_watchdog.addEpoch(command.getName() + ".end(true)");
275        continue;
276      }
277
278      command.execute();
279      for (Consumer<Command> action : m_executeActions) {
280        action.accept(command);
281      }
282      m_watchdog.addEpoch(command.getName() + ".execute()");
283      if (command.isFinished()) {
284        command.end(false);
285        for (Consumer<Command> action : m_finishActions) {
286          action.accept(command);
287        }
288        iterator.remove();
289
290        m_requirements.keySet().removeAll(command.getRequirements());
291        m_watchdog.addEpoch(command.getName() + ".end(false)");
292      }
293    }
294    m_inRunLoop = false;
295
296    // Schedule/cancel commands from queues populated during loop
297    for (Map.Entry<Command, Boolean> commandInterruptible : m_toSchedule.entrySet()) {
298      schedule(commandInterruptible.getValue(), commandInterruptible.getKey());
299    }
300
301    for (Command command : m_toCancel) {
302      cancel(command);
303    }
304
305    m_toSchedule.clear();
306    m_toCancel.clear();
307
308    // Add default commands for un-required registered subsystems.
309    for (Map.Entry<Subsystem, Command> subsystemCommand : m_subsystems.entrySet()) {
310      if (!m_requirements.containsKey(subsystemCommand.getKey())
311          && subsystemCommand.getValue() != null) {
312        schedule(subsystemCommand.getValue());
313      }
314    }
315
316    m_watchdog.disable();
317    if (m_watchdog.isExpired()) {
318      System.out.println("CommandScheduler loop overrun");
319      m_watchdog.printEpochs();
320    }
321  }
322
323  /**
324   * Registers subsystems with the scheduler. This must be called for the subsystem's periodic block
325   * to run when the scheduler is run, and for the subsystem's default command to be scheduled. It
326   * is recommended to call this from the constructor of your subsystem implementations.
327   *
328   * @param subsystems the subsystem to register
329   */
330  public void registerSubsystem(Subsystem... subsystems) {
331    for (Subsystem subsystem : subsystems) {
332      m_subsystems.put(subsystem, null);
333    }
334  }
335
336  /**
337   * Un-registers subsystems with the scheduler. The subsystem will no longer have its periodic
338   * block called, and will not have its default command scheduled.
339   *
340   * @param subsystems the subsystem to un-register
341   */
342  public void unregisterSubsystem(Subsystem... subsystems) {
343    m_subsystems.keySet().removeAll(Set.of(subsystems));
344  }
345
346  /**
347   * Sets the default command for a subsystem. Registers that subsystem if it is not already
348   * registered. Default commands will run whenever there is no other command currently scheduled
349   * that requires the subsystem. Default commands should be written to never end (i.e. their {@link
350   * Command#isFinished()} method should return false), as they would simply be re-scheduled if they
351   * do. Default commands must also require their subsystem.
352   *
353   * @param subsystem the subsystem whose default command will be set
354   * @param defaultCommand the default command to associate with the subsystem
355   */
356  public void setDefaultCommand(Subsystem subsystem, Command defaultCommand) {
357    if (!defaultCommand.getRequirements().contains(subsystem)) {
358      throw new IllegalArgumentException("Default commands must require their subsystem!");
359    }
360
361    if (defaultCommand.isFinished()) {
362      throw new IllegalArgumentException("Default commands should not end!");
363    }
364
365    m_subsystems.put(subsystem, defaultCommand);
366  }
367
368  /**
369   * Gets the default command associated with this subsystem. Null if this subsystem has no default
370   * command associated with it.
371   *
372   * @param subsystem the subsystem to inquire about
373   * @return the default command associated with the subsystem
374   */
375  public Command getDefaultCommand(Subsystem subsystem) {
376    return m_subsystems.get(subsystem);
377  }
378
379  /**
380   * Cancels commands. The scheduler will only call {@link Command#end(boolean)} method of the
381   * canceled command with {@code true}, indicating they were canceled (as opposed to finishing
382   * normally).
383   *
384   * <p>Commands will be canceled even if they are not scheduled as interruptible.
385   *
386   * @param commands the commands to cancel
387   */
388  public void cancel(Command... commands) {
389    if (m_inRunLoop) {
390      m_toCancel.addAll(List.of(commands));
391      return;
392    }
393
394    for (Command command : commands) {
395      if (!m_scheduledCommands.containsKey(command)) {
396        continue;
397      }
398
399      command.end(true);
400      for (Consumer<Command> action : m_interruptActions) {
401        action.accept(command);
402      }
403      m_scheduledCommands.remove(command);
404      m_requirements.keySet().removeAll(command.getRequirements());
405      m_watchdog.addEpoch(command.getName() + ".end(true)");
406    }
407  }
408
409  /** Cancels all commands that are currently scheduled. */
410  public void cancelAll() {
411    for (Command command : m_scheduledCommands.keySet().toArray(new Command[0])) {
412      cancel(command);
413    }
414  }
415
416  /**
417   * Returns the time since a given command was scheduled. Note that this only works on commands
418   * that are directly scheduled by the scheduler; it will not work on commands inside of
419   * commandgroups, as the scheduler does not see them.
420   *
421   * @param command the command to query
422   * @return the time since the command was scheduled, in seconds
423   */
424  public double timeSinceScheduled(Command command) {
425    CommandState commandState = m_scheduledCommands.get(command);
426    if (commandState != null) {
427      return commandState.timeSinceInitialized();
428    } else {
429      return -1;
430    }
431  }
432
433  /**
434   * Whether the given commands are running. Note that this only works on commands that are directly
435   * scheduled by the scheduler; it will not work on commands inside of CommandGroups, as the
436   * scheduler does not see them.
437   *
438   * @param commands the command to query
439   * @return whether the command is currently scheduled
440   */
441  public boolean isScheduled(Command... commands) {
442    return m_scheduledCommands.keySet().containsAll(Set.of(commands));
443  }
444
445  /**
446   * Returns the command currently requiring a given subsystem. Null if no command is currently
447   * requiring the subsystem
448   *
449   * @param subsystem the subsystem to be inquired about
450   * @return the command currently requiring the subsystem, or null if no command is currently
451   *     scheduled
452   */
453  public Command requiring(Subsystem subsystem) {
454    return m_requirements.get(subsystem);
455  }
456
457  /** Disables the command scheduler. */
458  public void disable() {
459    m_disabled = true;
460  }
461
462  /** Enables the command scheduler. */
463  public void enable() {
464    m_disabled = false;
465  }
466
467  /**
468   * Adds an action to perform on the initialization of any command by the scheduler.
469   *
470   * @param action the action to perform
471   */
472  public void onCommandInitialize(Consumer<Command> action) {
473    m_initActions.add(action);
474  }
475
476  /**
477   * Adds an action to perform on the execution of any command by the scheduler.
478   *
479   * @param action the action to perform
480   */
481  public void onCommandExecute(Consumer<Command> action) {
482    m_executeActions.add(action);
483  }
484
485  /**
486   * Adds an action to perform on the interruption of any command by the scheduler.
487   *
488   * @param action the action to perform
489   */
490  public void onCommandInterrupt(Consumer<Command> action) {
491    m_interruptActions.add(action);
492  }
493
494  /**
495   * Adds an action to perform on the finishing of any command by the scheduler.
496   *
497   * @param action the action to perform
498   */
499  public void onCommandFinish(Consumer<Command> action) {
500    m_finishActions.add(action);
501  }
502
503  @Override
504  public void initSendable(NTSendableBuilder builder) {
505    builder.setSmartDashboardType("Scheduler");
506    final NetworkTableEntry namesEntry = builder.getEntry("Names");
507    final NetworkTableEntry idsEntry = builder.getEntry("Ids");
508    final NetworkTableEntry cancelEntry = builder.getEntry("Cancel");
509    builder.setUpdateTable(
510        () -> {
511          if (namesEntry == null || idsEntry == null || cancelEntry == null) {
512            return;
513          }
514
515          Map<Double, Command> ids = new LinkedHashMap<>();
516
517          for (Command command : m_scheduledCommands.keySet()) {
518            ids.put((double) command.hashCode(), command);
519          }
520
521          double[] toCancel = cancelEntry.getDoubleArray(new double[0]);
522          if (toCancel.length > 0) {
523            for (double hash : toCancel) {
524              cancel(ids.get(hash));
525              ids.remove(hash);
526            }
527            cancelEntry.setDoubleArray(new double[0]);
528          }
529
530          List<String> names = new ArrayList<>();
531
532          ids.values().forEach(command -> names.add(command.getName()));
533
534          namesEntry.setStringArray(names.toArray(new String[0]));
535          idsEntry.setNumberArray(ids.keySet().toArray(new Double[0]));
536        });
537  }
538}