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}