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 * Initializes a given command, adds its requirements to the list, and performs the init actions. 156 * 157 * @param command The command to initialize 158 * @param requirements The command requirements 159 */ 160 private void initCommand(Command command, Set<Subsystem> requirements) { 161 m_scheduledCommands.add(command); 162 for (Subsystem requirement : requirements) { 163 m_requirements.put(requirement, command); 164 } 165 command.initialize(); 166 for (Consumer<Command> action : m_initActions) { 167 action.accept(command); 168 } 169 170 m_watchdog.addEpoch(command.getName() + ".initialize()"); 171 } 172 173 /** 174 * Schedules a command for execution. Does nothing if the command is already scheduled. If a 175 * command's requirements are not available, it will only be started if all the commands currently 176 * using those requirements have been scheduled as interruptible. If this is the case, they will 177 * be interrupted and the command will be scheduled. 178 * 179 * @param command the command to schedule. If null, no-op. 180 */ 181 private void schedule(Command command) { 182 if (command == null) { 183 DriverStation.reportWarning("Tried to schedule a null command", true); 184 return; 185 } 186 if (m_inRunLoop) { 187 m_toSchedule.add(command); 188 return; 189 } 190 191 requireNotComposed(command); 192 193 // Do nothing if the scheduler is disabled, the robot is disabled and the command doesn't 194 // run when disabled, or the command is already scheduled. 195 if (m_disabled 196 || isScheduled(command) 197 || RobotState.isDisabled() && !command.runsWhenDisabled()) { 198 return; 199 } 200 201 Set<Subsystem> requirements = command.getRequirements(); 202 203 // Schedule the command if the requirements are not currently in-use. 204 if (Collections.disjoint(m_requirements.keySet(), requirements)) { 205 initCommand(command, requirements); 206 } else { 207 // Else check if the requirements that are in use have all have interruptible commands, 208 // and if so, interrupt those commands and schedule the new command. 209 for (Subsystem requirement : requirements) { 210 Command requiring = requiring(requirement); 211 if (requiring != null 212 && requiring.getInterruptionBehavior() == InterruptionBehavior.kCancelIncoming) { 213 return; 214 } 215 } 216 for (Subsystem requirement : requirements) { 217 Command requiring = requiring(requirement); 218 if (requiring != null) { 219 cancel(requiring); 220 } 221 } 222 initCommand(command, requirements); 223 } 224 } 225 226 /** 227 * Schedules multiple commands for execution. Does nothing for commands already scheduled. 228 * 229 * @param commands the commands to schedule. No-op on null. 230 */ 231 public void schedule(Command... commands) { 232 for (Command command : commands) { 233 schedule(command); 234 } 235 } 236 237 /** 238 * Runs a single iteration of the scheduler. The execution occurs in the following order: 239 * 240 * <p>Subsystem periodic methods are called. 241 * 242 * <p>Button bindings are polled, and new commands are scheduled from them. 243 * 244 * <p>Currently-scheduled commands are executed. 245 * 246 * <p>End conditions are checked on currently-scheduled commands, and commands that are finished 247 * have their end methods called and are removed. 248 * 249 * <p>Any subsystems not being used as requirements have their default methods started. 250 */ 251 public void run() { 252 if (m_disabled) { 253 return; 254 } 255 m_watchdog.reset(); 256 257 // Run the periodic method of all registered subsystems. 258 for (Subsystem subsystem : m_subsystems.keySet()) { 259 subsystem.periodic(); 260 if (RobotBase.isSimulation()) { 261 subsystem.simulationPeriodic(); 262 } 263 m_watchdog.addEpoch(subsystem.getClass().getSimpleName() + ".periodic()"); 264 } 265 266 // Cache the active instance to avoid concurrency problems if setActiveLoop() is called from 267 // inside the button bindings. 268 EventLoop loopCache = m_activeButtonLoop; 269 // Poll buttons for new commands to add. 270 loopCache.poll(); 271 m_watchdog.addEpoch("buttons.run()"); 272 273 m_inRunLoop = true; 274 // Run scheduled commands, remove finished commands. 275 for (Iterator<Command> iterator = m_scheduledCommands.iterator(); iterator.hasNext(); ) { 276 Command command = iterator.next(); 277 278 if (!command.runsWhenDisabled() && RobotState.isDisabled()) { 279 command.end(true); 280 for (Consumer<Command> action : m_interruptActions) { 281 action.accept(command); 282 } 283 m_requirements.keySet().removeAll(command.getRequirements()); 284 iterator.remove(); 285 m_watchdog.addEpoch(command.getName() + ".end(true)"); 286 continue; 287 } 288 289 command.execute(); 290 for (Consumer<Command> action : m_executeActions) { 291 action.accept(command); 292 } 293 m_watchdog.addEpoch(command.getName() + ".execute()"); 294 if (command.isFinished()) { 295 command.end(false); 296 for (Consumer<Command> action : m_finishActions) { 297 action.accept(command); 298 } 299 iterator.remove(); 300 301 m_requirements.keySet().removeAll(command.getRequirements()); 302 m_watchdog.addEpoch(command.getName() + ".end(false)"); 303 } 304 } 305 m_inRunLoop = false; 306 307 // Schedule/cancel commands from queues populated during loop 308 for (Command command : m_toSchedule) { 309 schedule(command); 310 } 311 312 for (Command command : m_toCancel) { 313 cancel(command); 314 } 315 316 m_toSchedule.clear(); 317 m_toCancel.clear(); 318 319 // Add default commands for un-required registered subsystems. 320 for (Map.Entry<Subsystem, Command> subsystemCommand : m_subsystems.entrySet()) { 321 if (!m_requirements.containsKey(subsystemCommand.getKey()) 322 && subsystemCommand.getValue() != null) { 323 schedule(subsystemCommand.getValue()); 324 } 325 } 326 327 m_watchdog.disable(); 328 if (m_watchdog.isExpired()) { 329 System.out.println("CommandScheduler loop overrun"); 330 m_watchdog.printEpochs(); 331 } 332 } 333 334 /** 335 * Registers subsystems with the scheduler. This must be called for the subsystem's periodic block 336 * to run when the scheduler is run, and for the subsystem's default command to be scheduled. It 337 * is recommended to call this from the constructor of your subsystem implementations. 338 * 339 * @param subsystems the subsystem to register 340 */ 341 public void registerSubsystem(Subsystem... subsystems) { 342 for (Subsystem subsystem : subsystems) { 343 if (subsystem == null) { 344 DriverStation.reportWarning("Tried to register a null subsystem", true); 345 continue; 346 } 347 if (m_subsystems.containsKey(subsystem)) { 348 DriverStation.reportWarning("Tried to register an already-registered subsystem", true); 349 continue; 350 } 351 m_subsystems.put(subsystem, null); 352 } 353 } 354 355 /** 356 * Un-registers subsystems with the scheduler. The subsystem will no longer have its periodic 357 * block called, and will not have its default command scheduled. 358 * 359 * @param subsystems the subsystem to un-register 360 */ 361 public void unregisterSubsystem(Subsystem... subsystems) { 362 m_subsystems.keySet().removeAll(Set.of(subsystems)); 363 } 364 365 /** 366 * Sets the default command for a subsystem. Registers that subsystem if it is not already 367 * registered. Default commands will run whenever there is no other command currently scheduled 368 * that requires the subsystem. Default commands should be written to never end (i.e. their {@link 369 * Command#isFinished()} method should return false), as they would simply be re-scheduled if they 370 * do. Default commands must also require their subsystem. 371 * 372 * @param subsystem the subsystem whose default command will be set 373 * @param defaultCommand the default command to associate with the subsystem 374 */ 375 public void setDefaultCommand(Subsystem subsystem, Command defaultCommand) { 376 if (subsystem == null) { 377 DriverStation.reportWarning("Tried to set a default command for a null subsystem", true); 378 return; 379 } 380 if (defaultCommand == null) { 381 DriverStation.reportWarning("Tried to set a null default command", true); 382 return; 383 } 384 385 requireNotComposed(defaultCommand); 386 387 if (!defaultCommand.getRequirements().contains(subsystem)) { 388 throw new IllegalArgumentException("Default commands must require their subsystem!"); 389 } 390 391 if (defaultCommand.getInterruptionBehavior() == InterruptionBehavior.kCancelIncoming) { 392 DriverStation.reportWarning( 393 "Registering a non-interruptible default command!\n" 394 + "This will likely prevent any other commands from requiring this subsystem.", 395 true); 396 // Warn, but allow -- there might be a use case for this. 397 } 398 399 m_subsystems.put(subsystem, defaultCommand); 400 } 401 402 /** 403 * Removes the default command for a subsystem. The current default command will run until another 404 * command is scheduled that requires the subsystem, at which point the current default command 405 * will not be re-scheduled. 406 * 407 * @param subsystem the subsystem whose default command will be removed 408 */ 409 public void removeDefaultCommand(Subsystem subsystem) { 410 if (subsystem == null) { 411 DriverStation.reportWarning("Tried to remove a default command for a null subsystem", true); 412 return; 413 } 414 415 m_subsystems.put(subsystem, null); 416 } 417 418 /** 419 * Gets the default command associated with this subsystem. Null if this subsystem has no default 420 * command associated with it. 421 * 422 * @param subsystem the subsystem to inquire about 423 * @return the default command associated with the subsystem 424 */ 425 public Command getDefaultCommand(Subsystem subsystem) { 426 return m_subsystems.get(subsystem); 427 } 428 429 /** 430 * Cancels commands. The scheduler will only call {@link Command#end(boolean)} method of the 431 * canceled command with {@code true}, indicating they were canceled (as opposed to finishing 432 * normally). 433 * 434 * <p>Commands will be canceled regardless of {@link InterruptionBehavior interruption behavior}. 435 * 436 * @param commands the commands to cancel 437 */ 438 public void cancel(Command... commands) { 439 if (m_inRunLoop) { 440 m_toCancel.addAll(List.of(commands)); 441 return; 442 } 443 444 for (Command command : commands) { 445 if (command == null) { 446 DriverStation.reportWarning("Tried to cancel a null command", true); 447 continue; 448 } 449 if (!isScheduled(command)) { 450 continue; 451 } 452 453 m_scheduledCommands.remove(command); 454 m_requirements.keySet().removeAll(command.getRequirements()); 455 command.end(true); 456 for (Consumer<Command> action : m_interruptActions) { 457 action.accept(command); 458 } 459 m_watchdog.addEpoch(command.getName() + ".end(true)"); 460 } 461 } 462 463 /** Cancels all commands that are currently scheduled. */ 464 public void cancelAll() { 465 // Copy to array to avoid concurrent modification. 466 cancel(m_scheduledCommands.toArray(new Command[0])); 467 } 468 469 /** 470 * Whether the given commands are running. Note that this only works on commands that are directly 471 * scheduled by the scheduler; it will not work on commands inside compositions, as the scheduler 472 * does not see them. 473 * 474 * @param commands the command to query 475 * @return whether the command is currently scheduled 476 */ 477 public boolean isScheduled(Command... commands) { 478 return m_scheduledCommands.containsAll(Set.of(commands)); 479 } 480 481 /** 482 * Returns the command currently requiring a given subsystem. Null if no command is currently 483 * requiring the subsystem 484 * 485 * @param subsystem the subsystem to be inquired about 486 * @return the command currently requiring the subsystem, or null if no command is currently 487 * scheduled 488 */ 489 public Command requiring(Subsystem subsystem) { 490 return m_requirements.get(subsystem); 491 } 492 493 /** Disables the command scheduler. */ 494 public void disable() { 495 m_disabled = true; 496 } 497 498 /** Enables the command scheduler. */ 499 public void enable() { 500 m_disabled = false; 501 } 502 503 /** 504 * Adds an action to perform on the initialization of any command by the scheduler. 505 * 506 * @param action the action to perform 507 */ 508 public void onCommandInitialize(Consumer<Command> action) { 509 m_initActions.add(requireNonNullParam(action, "action", "onCommandInitialize")); 510 } 511 512 /** 513 * Adds an action to perform on the execution of any command by the scheduler. 514 * 515 * @param action the action to perform 516 */ 517 public void onCommandExecute(Consumer<Command> action) { 518 m_executeActions.add(requireNonNullParam(action, "action", "onCommandExecute")); 519 } 520 521 /** 522 * Adds an action to perform on the interruption of any command by the scheduler. 523 * 524 * @param action the action to perform 525 */ 526 public void onCommandInterrupt(Consumer<Command> action) { 527 m_interruptActions.add(requireNonNullParam(action, "action", "onCommandInterrupt")); 528 } 529 530 /** 531 * Adds an action to perform on the finishing of any command by the scheduler. 532 * 533 * @param action the action to perform 534 */ 535 public void onCommandFinish(Consumer<Command> action) { 536 m_finishActions.add(requireNonNullParam(action, "action", "onCommandFinish")); 537 } 538 539 /** 540 * Register commands as composed. An exception will be thrown if these commands are scheduled 541 * directly or added to a composition. 542 * 543 * @param commands the commands to register 544 * @throws IllegalArgumentException if the given commands have already been composed. 545 */ 546 public void registerComposedCommands(Command... commands) { 547 var commandSet = Set.of(commands); 548 requireNotComposed(commandSet); 549 m_composedCommands.addAll(commandSet); 550 } 551 552 /** 553 * Clears the list of composed commands, allowing all commands to be freely used again. 554 * 555 * <p>WARNING: Using this haphazardly can result in unexpected/undesirable behavior. Do not use 556 * this unless you fully understand what you are doing. 557 */ 558 public void clearComposedCommands() { 559 m_composedCommands.clear(); 560 } 561 562 /** 563 * Removes a single command from the list of composed commands, allowing it to be freely used 564 * again. 565 * 566 * <p>WARNING: Using this haphazardly can result in unexpected/undesirable behavior. Do not use 567 * this unless you fully understand what you are doing. 568 * 569 * @param command the command to remove from the list of grouped commands 570 */ 571 public void removeComposedCommand(Command command) { 572 m_composedCommands.remove(command); 573 } 574 575 /** 576 * Requires that the specified command hasn't been already added to a composition. 577 * 578 * @param command The command to check 579 * @throws IllegalArgumentException if the given commands have already been composed. 580 */ 581 public void requireNotComposed(Command command) { 582 if (m_composedCommands.contains(command)) { 583 throw new IllegalArgumentException( 584 "Commands that have been composed may not be added to another composition or scheduled " 585 + "individually!"); 586 } 587 } 588 589 /** 590 * Requires that the specified commands not have been already added to a composition. 591 * 592 * @param commands The commands to check 593 * @throws IllegalArgumentException if the given commands have already been composed. 594 */ 595 public void requireNotComposed(Collection<Command> commands) { 596 if (!Collections.disjoint(commands, getComposedCommands())) { 597 throw new IllegalArgumentException( 598 "Commands that have been composed may not be added to another composition or scheduled " 599 + "individually!"); 600 } 601 } 602 603 /** 604 * Check if the given command has been composed. 605 * 606 * @param command The command to check 607 * @return true if composed 608 */ 609 public boolean isComposed(Command command) { 610 return getComposedCommands().contains(command); 611 } 612 613 Set<Command> getComposedCommands() { 614 return m_composedCommands; 615 } 616 617 @Override 618 public void initSendable(NTSendableBuilder builder) { 619 builder.setSmartDashboardType("Scheduler"); 620 final StringArrayPublisher namesPub = new StringArrayTopic(builder.getTopic("Names")).publish(); 621 final IntegerArrayPublisher idsPub = new IntegerArrayTopic(builder.getTopic("Ids")).publish(); 622 final IntegerArrayEntry cancelEntry = 623 new IntegerArrayTopic(builder.getTopic("Cancel")).getEntry(new long[] {}); 624 builder.addCloseable(namesPub); 625 builder.addCloseable(idsPub); 626 builder.addCloseable(cancelEntry); 627 builder.setUpdateTable( 628 () -> { 629 if (namesPub == null || idsPub == null || cancelEntry == null) { 630 return; 631 } 632 633 Map<Long, Command> ids = new LinkedHashMap<>(); 634 List<String> names = new ArrayList<>(); 635 long[] ids2 = new long[m_scheduledCommands.size()]; 636 637 int i = 0; 638 for (Command command : m_scheduledCommands) { 639 long id = command.hashCode(); 640 ids.put(id, command); 641 names.add(command.getName()); 642 ids2[i] = id; 643 i++; 644 } 645 646 long[] toCancel = cancelEntry.get(); 647 if (toCancel.length > 0) { 648 for (long hash : toCancel) { 649 cancel(ids.get(hash)); 650 ids.remove(hash); 651 } 652 cancelEntry.set(new long[] {}); 653 } 654 655 namesPub.set(names.toArray(new String[] {})); 656 idsPub.set(ids2); 657 }); 658 } 659}