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