// Copyright (c) FIRST and other WPILib contributors. // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. package edu.wpi.first.wpilibj2.command; import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; import edu.wpi.first.hal.FRCNetComm.tInstances; import edu.wpi.first.hal.FRCNetComm.tResourceType; import edu.wpi.first.hal.HAL; import edu.wpi.first.networktables.IntegerArrayEntry; import edu.wpi.first.networktables.IntegerArrayPublisher; import edu.wpi.first.networktables.IntegerArrayTopic; import edu.wpi.first.networktables.NTSendable; import edu.wpi.first.networktables.NTSendableBuilder; import edu.wpi.first.networktables.StringArrayPublisher; import edu.wpi.first.networktables.StringArrayTopic; import edu.wpi.first.util.sendable.SendableRegistry; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.RobotState; import edu.wpi.first.wpilibj.TimedRobot; import edu.wpi.first.wpilibj.Watchdog; import edu.wpi.first.wpilibj.event.EventLoop; import edu.wpi.first.wpilibj.livewindow.LiveWindow; import edu.wpi.first.wpilibj2.command.Command.InterruptionBehavior; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.function.Consumer; /** * The scheduler responsible for running {@link Command}s. A Command-based robot should call {@link * CommandScheduler#run()} on the singleton instance in its periodic block in order to run commands * synchronously from the main loop. Subsystems should be registered with the scheduler using {@link * CommandScheduler#registerSubsystem(Subsystem...)} in order for their {@link Subsystem#periodic()} * methods to be called and for their default commands to be scheduled. * *

This class is provided by the NewCommands VendorDep */ public final class CommandScheduler implements NTSendable, AutoCloseable { /** The Singleton Instance. */ private static CommandScheduler instance; /** * Returns the Scheduler instance. * * @return the instance */ public static synchronized CommandScheduler getInstance() { if (instance == null) { instance = new CommandScheduler(); } return instance; } private final Set m_composedCommands = Collections.newSetFromMap(new WeakHashMap<>()); // A set of the currently-running commands. private final Set m_scheduledCommands = new LinkedHashSet<>(); // A map from required subsystems to their requiring commands. Also used as a set of the // currently-required subsystems. private final Map m_requirements = new LinkedHashMap<>(); // A map from subsystems registered with the scheduler to their default commands. Also used // as a list of currently-registered subsystems. private final Map m_subsystems = new LinkedHashMap<>(); private final EventLoop m_defaultButtonLoop = new EventLoop(); // The set of currently-registered buttons that will be polled every iteration. private EventLoop m_activeButtonLoop = m_defaultButtonLoop; private boolean m_disabled; // Lists of user-supplied actions to be executed on scheduling events for every command. private final List> m_initActions = new ArrayList<>(); private final List> m_executeActions = new ArrayList<>(); private final List> m_interruptActions = new ArrayList<>(); private final List> m_finishActions = new ArrayList<>(); // Flag and queues for avoiding ConcurrentModificationException if commands are // scheduled/canceled during run private boolean m_inRunLoop; private final Set m_toSchedule = new LinkedHashSet<>(); private final List m_toCancel = new ArrayList<>(); private final Watchdog m_watchdog = new Watchdog(TimedRobot.kDefaultPeriod, () -> {}); CommandScheduler() { HAL.report(tResourceType.kResourceType_Command, tInstances.kCommand2_Scheduler); SendableRegistry.addLW(this, "Scheduler"); LiveWindow.setEnabledListener( () -> { disable(); cancelAll(); }); LiveWindow.setDisabledListener(this::enable); } /** * Changes the period of the loop overrun watchdog. This should be kept in sync with the * TimedRobot period. * * @param period Period in seconds. */ public void setPeriod(double period) { m_watchdog.setTimeout(period); } @Override public void close() { SendableRegistry.remove(this); LiveWindow.setEnabledListener(null); LiveWindow.setDisabledListener(null); } /** * Get the default button poll. * * @return a reference to the default {@link EventLoop} object polling buttons. */ public EventLoop getDefaultButtonLoop() { return m_defaultButtonLoop; } /** * Get the active button poll. * * @return a reference to the current {@link EventLoop} object polling buttons. */ public EventLoop getActiveButtonLoop() { return m_activeButtonLoop; } /** * Replace the button poll with another one. * * @param loop the new button polling loop object. */ public void setActiveButtonLoop(EventLoop loop) { m_activeButtonLoop = requireNonNullParam(loop, "loop", "CommandScheduler" + ".replaceButtonEventLoop"); } /** * Adds a button binding to the scheduler, which will be polled to schedule commands. * * @param button The button to add * @deprecated Use {@link edu.wpi.first.wpilibj2.command.button.Trigger} */ @Deprecated(since = "2023") public void addButton(Runnable button) { m_activeButtonLoop.bind(requireNonNullParam(button, "button", "addButton")); } /** * Removes all button bindings from the scheduler. * * @deprecated call {@link EventLoop#clear()} on {@link #getActiveButtonLoop()} directly instead. */ @Deprecated(since = "2023") public void clearButtons() { m_activeButtonLoop.clear(); } /** * Initializes a given command, adds its requirements to the list, and performs the init actions. * * @param command The command to initialize * @param requirements The command requirements */ private void initCommand(Command command, Set requirements) { m_scheduledCommands.add(command); for (Subsystem requirement : requirements) { m_requirements.put(requirement, command); } command.initialize(); for (Consumer action : m_initActions) { action.accept(command); } m_watchdog.addEpoch(command.getName() + ".initialize()"); } /** * Schedules a command for execution. Does nothing if the command is already scheduled. If a * command's requirements are not available, it will only be started if all the commands currently * using those requirements have been scheduled as interruptible. If this is the case, they will * be interrupted and the command will be scheduled. * * @param command the command to schedule. If null, no-op. */ private void schedule(Command command) { if (command == null) { DriverStation.reportWarning("Tried to schedule a null command", true); return; } if (m_inRunLoop) { m_toSchedule.add(command); return; } requireNotComposed(command); // Do nothing if the scheduler is disabled, the robot is disabled and the command doesn't // run when disabled, or the command is already scheduled. if (m_disabled || isScheduled(command) || RobotState.isDisabled() && !command.runsWhenDisabled()) { return; } Set requirements = command.getRequirements(); // Schedule the command if the requirements are not currently in-use. if (Collections.disjoint(m_requirements.keySet(), requirements)) { initCommand(command, requirements); } else { // Else check if the requirements that are in use have all have interruptible commands, // and if so, interrupt those commands and schedule the new command. for (Subsystem requirement : requirements) { Command requiring = requiring(requirement); if (requiring != null && requiring.getInterruptionBehavior() == InterruptionBehavior.kCancelIncoming) { return; } } for (Subsystem requirement : requirements) { Command requiring = requiring(requirement); if (requiring != null) { cancel(requiring); } } initCommand(command, requirements); } } /** * Schedules multiple commands for execution. Does nothing for commands already scheduled. * * @param commands the commands to schedule. No-op on null. */ public void schedule(Command... commands) { for (Command command : commands) { schedule(command); } } /** * Runs a single iteration of the scheduler. The execution occurs in the following order: * *

Subsystem periodic methods are called. * *

Button bindings are polled, and new commands are scheduled from them. * *

Currently-scheduled commands are executed. * *

End conditions are checked on currently-scheduled commands, and commands that are finished * have their end methods called and are removed. * *

Any subsystems not being used as requirements have their default methods started. */ public void run() { if (m_disabled) { return; } m_watchdog.reset(); // Run the periodic method of all registered subsystems. for (Subsystem subsystem : m_subsystems.keySet()) { subsystem.periodic(); if (RobotBase.isSimulation()) { subsystem.simulationPeriodic(); } m_watchdog.addEpoch(subsystem.getClass().getSimpleName() + ".periodic()"); } // Cache the active instance to avoid concurrency problems if setActiveLoop() is called from // inside the button bindings. EventLoop loopCache = m_activeButtonLoop; // Poll buttons for new commands to add. loopCache.poll(); m_watchdog.addEpoch("buttons.run()"); m_inRunLoop = true; // Run scheduled commands, remove finished commands. for (Iterator iterator = m_scheduledCommands.iterator(); iterator.hasNext(); ) { Command command = iterator.next(); if (!command.runsWhenDisabled() && RobotState.isDisabled()) { command.end(true); for (Consumer action : m_interruptActions) { action.accept(command); } m_requirements.keySet().removeAll(command.getRequirements()); iterator.remove(); m_watchdog.addEpoch(command.getName() + ".end(true)"); continue; } command.execute(); for (Consumer action : m_executeActions) { action.accept(command); } m_watchdog.addEpoch(command.getName() + ".execute()"); if (command.isFinished()) { command.end(false); for (Consumer action : m_finishActions) { action.accept(command); } iterator.remove(); m_requirements.keySet().removeAll(command.getRequirements()); m_watchdog.addEpoch(command.getName() + ".end(false)"); } } m_inRunLoop = false; // Schedule/cancel commands from queues populated during loop for (Command command : m_toSchedule) { schedule(command); } for (Command command : m_toCancel) { cancel(command); } m_toSchedule.clear(); m_toCancel.clear(); // Add default commands for un-required registered subsystems. for (Map.Entry subsystemCommand : m_subsystems.entrySet()) { if (!m_requirements.containsKey(subsystemCommand.getKey()) && subsystemCommand.getValue() != null) { schedule(subsystemCommand.getValue()); } } m_watchdog.disable(); if (m_watchdog.isExpired()) { System.out.println("CommandScheduler loop overrun"); m_watchdog.printEpochs(); } } /** * Registers subsystems with the scheduler. This must be called for the subsystem's periodic block * to run when the scheduler is run, and for the subsystem's default command to be scheduled. It * is recommended to call this from the constructor of your subsystem implementations. * * @param subsystems the subsystem to register */ public void registerSubsystem(Subsystem... subsystems) { for (Subsystem subsystem : subsystems) { if (subsystem == null) { DriverStation.reportWarning("Tried to register a null subsystem", true); continue; } if (m_subsystems.containsKey(subsystem)) { DriverStation.reportWarning("Tried to register an already-registered subsystem", true); continue; } m_subsystems.put(subsystem, null); } } /** * Un-registers subsystems with the scheduler. The subsystem will no longer have its periodic * block called, and will not have its default command scheduled. * * @param subsystems the subsystem to un-register */ public void unregisterSubsystem(Subsystem... subsystems) { m_subsystems.keySet().removeAll(Set.of(subsystems)); } /** * Sets the default command for a subsystem. Registers that subsystem if it is not already * registered. Default commands will run whenever there is no other command currently scheduled * that requires the subsystem. Default commands should be written to never end (i.e. their {@link * Command#isFinished()} method should return false), as they would simply be re-scheduled if they * do. Default commands must also require their subsystem. * * @param subsystem the subsystem whose default command will be set * @param defaultCommand the default command to associate with the subsystem */ public void setDefaultCommand(Subsystem subsystem, Command defaultCommand) { if (subsystem == null) { DriverStation.reportWarning("Tried to set a default command for a null subsystem", true); return; } if (defaultCommand == null) { DriverStation.reportWarning("Tried to set a null default command", true); return; } requireNotComposed(defaultCommand); if (!defaultCommand.getRequirements().contains(subsystem)) { throw new IllegalArgumentException("Default commands must require their subsystem!"); } if (defaultCommand.getInterruptionBehavior() == InterruptionBehavior.kCancelIncoming) { DriverStation.reportWarning( "Registering a non-interruptible default command!\n" + "This will likely prevent any other commands from requiring this subsystem.", true); // Warn, but allow -- there might be a use case for this. } m_subsystems.put(subsystem, defaultCommand); } /** * Removes the default command for a subsystem. The current default command will run until another * command is scheduled that requires the subsystem, at which point the current default command * will not be re-scheduled. * * @param subsystem the subsystem whose default command will be removed */ public void removeDefaultCommand(Subsystem subsystem) { if (subsystem == null) { DriverStation.reportWarning("Tried to remove a default command for a null subsystem", true); return; } m_subsystems.put(subsystem, null); } /** * Gets the default command associated with this subsystem. Null if this subsystem has no default * command associated with it. * * @param subsystem the subsystem to inquire about * @return the default command associated with the subsystem */ public Command getDefaultCommand(Subsystem subsystem) { return m_subsystems.get(subsystem); } /** * Cancels commands. The scheduler will only call {@link Command#end(boolean)} method of the * canceled command with {@code true}, indicating they were canceled (as opposed to finishing * normally). * *

Commands will be canceled regardless of {@link InterruptionBehavior interruption behavior}. * * @param commands the commands to cancel */ public void cancel(Command... commands) { if (m_inRunLoop) { m_toCancel.addAll(List.of(commands)); return; } for (Command command : commands) { if (command == null) { DriverStation.reportWarning("Tried to cancel a null command", true); continue; } if (!isScheduled(command)) { continue; } m_scheduledCommands.remove(command); m_requirements.keySet().removeAll(command.getRequirements()); command.end(true); for (Consumer action : m_interruptActions) { action.accept(command); } m_watchdog.addEpoch(command.getName() + ".end(true)"); } } /** Cancels all commands that are currently scheduled. */ public void cancelAll() { // Copy to array to avoid concurrent modification. cancel(m_scheduledCommands.toArray(new Command[0])); } /** * Whether the given commands are running. Note that this only works on commands that are directly * scheduled by the scheduler; it will not work on commands inside compositions, as the scheduler * does not see them. * * @param commands the command to query * @return whether the command is currently scheduled */ public boolean isScheduled(Command... commands) { return m_scheduledCommands.containsAll(Set.of(commands)); } /** * Returns the command currently requiring a given subsystem. Null if no command is currently * requiring the subsystem * * @param subsystem the subsystem to be inquired about * @return the command currently requiring the subsystem, or null if no command is currently * scheduled */ public Command requiring(Subsystem subsystem) { return m_requirements.get(subsystem); } /** Disables the command scheduler. */ public void disable() { m_disabled = true; } /** Enables the command scheduler. */ public void enable() { m_disabled = false; } /** * Adds an action to perform on the initialization of any command by the scheduler. * * @param action the action to perform */ public void onCommandInitialize(Consumer action) { m_initActions.add(requireNonNullParam(action, "action", "onCommandInitialize")); } /** * Adds an action to perform on the execution of any command by the scheduler. * * @param action the action to perform */ public void onCommandExecute(Consumer action) { m_executeActions.add(requireNonNullParam(action, "action", "onCommandExecute")); } /** * Adds an action to perform on the interruption of any command by the scheduler. * * @param action the action to perform */ public void onCommandInterrupt(Consumer action) { m_interruptActions.add(requireNonNullParam(action, "action", "onCommandInterrupt")); } /** * Adds an action to perform on the finishing of any command by the scheduler. * * @param action the action to perform */ public void onCommandFinish(Consumer action) { m_finishActions.add(requireNonNullParam(action, "action", "onCommandFinish")); } /** * Register commands as composed. An exception will be thrown if these commands are scheduled * directly or added to a composition. * * @param commands the commands to register * @throws IllegalArgumentException if the given commands have already been composed. */ public void registerComposedCommands(Command... commands) { var commandSet = Set.of(commands); requireNotComposed(commandSet); m_composedCommands.addAll(commandSet); } /** * Clears the list of composed commands, allowing all commands to be freely used again. * *

WARNING: Using this haphazardly can result in unexpected/undesirable behavior. Do not use * this unless you fully understand what you are doing. */ public void clearComposedCommands() { m_composedCommands.clear(); } /** * Removes a single command from the list of composed commands, allowing it to be freely used * again. * *

WARNING: Using this haphazardly can result in unexpected/undesirable behavior. Do not use * this unless you fully understand what you are doing. * * @param command the command to remove from the list of grouped commands */ public void removeComposedCommand(Command command) { m_composedCommands.remove(command); } /** * Requires that the specified command hasn't been already added to a composition. * * @param command The command to check * @throws IllegalArgumentException if the given commands have already been composed. */ public void requireNotComposed(Command command) { if (m_composedCommands.contains(command)) { throw new IllegalArgumentException( "Commands that have been composed may not be added to another composition or scheduled " + "individually!"); } } /** * Requires that the specified commands not have been already added to a composition. * * @param commands The commands to check * @throws IllegalArgumentException if the given commands have already been composed. */ public void requireNotComposed(Collection commands) { if (!Collections.disjoint(commands, getComposedCommands())) { throw new IllegalArgumentException( "Commands that have been composed may not be added to another composition or scheduled " + "individually!"); } } /** * Check if the given command has been composed. * * @param command The command to check * @return true if composed */ public boolean isComposed(Command command) { return getComposedCommands().contains(command); } Set getComposedCommands() { return m_composedCommands; } @Override public void initSendable(NTSendableBuilder builder) { builder.setSmartDashboardType("Scheduler"); final StringArrayPublisher namesPub = new StringArrayTopic(builder.getTopic("Names")).publish(); final IntegerArrayPublisher idsPub = new IntegerArrayTopic(builder.getTopic("Ids")).publish(); final IntegerArrayEntry cancelEntry = new IntegerArrayTopic(builder.getTopic("Cancel")).getEntry(new long[] {}); builder.addCloseable(namesPub); builder.addCloseable(idsPub); builder.addCloseable(cancelEntry); builder.setUpdateTable( () -> { if (namesPub == null || idsPub == null || cancelEntry == null) { return; } Map ids = new LinkedHashMap<>(); List names = new ArrayList<>(); long[] ids2 = new long[m_scheduledCommands.size()]; int i = 0; for (Command command : m_scheduledCommands) { long id = command.hashCode(); ids.put(id, command); names.add(command.getName()); ids2[i] = id; i++; } long[] toCancel = cancelEntry.get(); if (toCancel.length > 0) { for (long hash : toCancel) { cancel(ids.get(hash)); ids.remove(hash); } cancelEntry.set(new long[] {}); } namesPub.set(names.toArray(new String[] {})); idsPub.set(ids2); }); } }