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}