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