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