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.command3; 006 007import static org.wpilib.units.Units.Microseconds; 008import static org.wpilib.units.Units.Milliseconds; 009 010import java.util.ArrayList; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.HashSet; 014import java.util.LinkedHashMap; 015import java.util.LinkedHashSet; 016import java.util.List; 017import java.util.Map; 018import java.util.SequencedMap; 019import java.util.SequencedSet; 020import java.util.Set; 021import java.util.Stack; 022import java.util.function.Consumer; 023import java.util.stream.Collectors; 024import org.wpilib.annotation.NoDiscard; 025import org.wpilib.command3.button.CommandGenericHID; 026import org.wpilib.command3.proto.SchedulerProto; 027import org.wpilib.event.EventLoop; 028import org.wpilib.framework.TimedRobot; 029import org.wpilib.system.RobotController; 030import org.wpilib.util.ErrorMessages; 031import org.wpilib.util.protobuf.ProtobufSerializable; 032 033/** 034 * Manages the lifecycles of {@link Coroutine}-based {@link Command Commands}. Commands may be 035 * scheduled directly using {@link #schedule(Command)}, or be bound to {@link Trigger Triggers} to 036 * automatically handle scheduling and cancellation based on internal or external events. User code 037 * is responsible for calling {@link #run()} periodically to update trigger conditions and execute 038 * scheduled commands. Most often, this is done by overriding {@link TimedRobot#robotPeriodic()} to 039 * include a call to {@code Scheduler.getDefault().run()}: 040 * 041 * <pre>{@code 042 * public class Robot extends TimedRobot { 043 * @Override 044 * public void robotPeriodic() { 045 * // Update the scheduler on every loop 046 * Scheduler.getDefault().run(); 047 * } 048 * } 049 * }</pre> 050 * 051 * <h2>Danger</h2> 052 * 053 * <p>The scheduler <i>must</i> be used in a single-threaded program. Commands must be scheduled and 054 * canceled by the same thread that runs the scheduler, and cannot be run in a virtual thread. 055 * 056 * <p><strong>Using the commands framework in a multithreaded environment can cause crashes in the 057 * Java virtual machine at any time, including on an official field during a match.</strong> The 058 * Java JIT compilers make assumptions that rely on coroutines being used on a single thread. 059 * Breaking those assumptions can cause incorrect JIT code to be generated with undefined behavior, 060 * potentially causing control issues or crashes deep in JIT-generated native code. 061 * 062 * <p><strong>Normal concurrency constructs like locks, atomic references, and synchronized blocks 063 * or methods cannot save you.</strong> 064 * 065 * <h2>Lifecycle</h2> 066 * 067 * <p>The {@link #run()} method runs five steps: 068 * 069 * <ol> 070 * <li>Call {@link #sideload(Consumer) periodic sideload functions} 071 * <li>Poll all registered triggers to queue and cancel commands 072 * <li>Queue default commands for any mechanisms without a running command. The queued commands 073 * can be superseded by any manual scheduling or commands scheduled by triggers in the next 074 * run. 075 * <li>Start all queued commands. This happens after all triggers are checked in case multiple 076 * commands with conflicting requirements are queued in the same update; the last command to 077 * be queued takes precedence over the rest. 078 * <li>Loop over all running commands, mounting and calling each in turn until they either exit or 079 * call {@link Coroutine#yield()}. Commands run in the order in which they were scheduled. 080 * </ol> 081 * 082 * <h2>Telemetry</h2> 083 * 084 * <p>There are two mechanisms for telemetry for a scheduler. A protobuf serializer can be used to 085 * take a snapshot of a scheduler instance, and report what commands are queued (scheduled but have 086 * not yet started to run), commands that are running (along with timing data for each command), and 087 * total time spent in the most recent {@link #run()} call. However, it cannot detect one-shot 088 * commands that are scheduled, run, and complete all in a single {@code run()} invocation - 089 * effectively, commands that never call {@link Coroutine#yield()} are invisible. 090 * 091 * <p>A second telemetry mechanism is provided by {@link #addEventListener(Consumer)}. The scheduler 092 * will issue events to all registered listeners when certain events occur (see {@link 093 * SchedulerEvent} for all event types). These events are emitted immediately and can be used to 094 * detect lifecycle events for all commands, including one-shots that would be invisible to the 095 * protobuf serializer. However, it is up to the user to log those events themselves. 096 */ 097public final class Scheduler implements ProtobufSerializable { 098 private final Map<Mechanism, Command> m_defaultCommands = new LinkedHashMap<>(); 099 100 /** The set of commands scheduled since the start of the previous run. */ 101 private final SequencedSet<CommandState> m_queuedToRun = new LinkedHashSet<>(); 102 103 /** 104 * The states of all running commands (does not include on deck commands). We preserve insertion 105 * order to guarantee that child commands run after their parents. 106 */ 107 private final SequencedMap<Command, CommandState> m_runningCommands = new LinkedHashMap<>(); 108 109 /** 110 * The stack of currently executing commands. Child commands are pushed onto the stack and popped 111 * when they complete. Use {@link #currentState()} and {@link #currentCommand()} to get the 112 * currently executing command or its state. 113 */ 114 private final Stack<CommandState> m_currentCommandAncestry = new Stack<>(); 115 116 /** The periodic callbacks to run, outside of the command structure. */ 117 private final List<Coroutine> m_periodicCallbacks = new ArrayList<>(); 118 119 /** Event loop for trigger bindings. */ 120 private final EventLoop m_eventLoop = new EventLoop(); 121 122 /** The scope for continuations to yield to. */ 123 private final ContinuationScope m_scope = new ContinuationScope("coroutine commands"); 124 125 // Telemetry 126 /** Protobuf serializer for a scheduler. */ 127 public static final SchedulerProto proto = new SchedulerProto(); 128 129 private double m_lastRunTimeMs = -1; 130 131 private final Set<Consumer<? super SchedulerEvent>> m_eventListeners = new LinkedHashSet<>(); 132 133 /** The default scheduler instance. */ 134 private static final Scheduler s_defaultScheduler = new Scheduler(); 135 136 /** 137 * Gets the default scheduler instance for use in a robot program. Unless otherwise specified, 138 * triggers and mechanisms will be registered with the default scheduler and require the default 139 * scheduler to run. However, triggers and mechanisms can be constructed to be registered with a 140 * specific scheduler instance, which may be useful for isolation for unit tests. 141 * 142 * @return the default scheduler instance. 143 */ 144 @NoDiscard 145 public static Scheduler getDefault() { 146 return s_defaultScheduler; 147 } 148 149 /** 150 * Creates a new scheduler object. Note that most built-in constructs like {@link Trigger} and 151 * {@link CommandGenericHID} will use the {@link #getDefault() default scheduler instance} unless 152 * they were explicitly constructed with a different scheduler instance. Teams should use the 153 * default instance for convenience; however, new scheduler instances can be useful for unit 154 * tests. 155 * 156 * @return a new scheduler instance that is independent of the default scheduler instance. 157 */ 158 @NoDiscard 159 public static Scheduler createIndependentScheduler() { 160 return new Scheduler(); 161 } 162 163 /** Private constructor. Use static factory methods or the default scheduler instance. */ 164 private Scheduler() {} 165 166 /** 167 * Sets the default command for a mechanism. The command must require that mechanism, and cannot 168 * require any other mechanisms. 169 * 170 * @param mechanism the mechanism for which to set the default command 171 * @param defaultCommand the default command to execute on the mechanism 172 * @throws IllegalArgumentException if the command does not meet the requirements for being a 173 * default command 174 */ 175 public void setDefaultCommand(Mechanism mechanism, Command defaultCommand) { 176 if (!defaultCommand.requires(mechanism)) { 177 throw new IllegalArgumentException( 178 "A mechanism's default command must require that mechanism"); 179 } 180 181 if (defaultCommand.requirements().size() > 1) { 182 throw new IllegalArgumentException( 183 "A mechanism's default command cannot require other mechanisms"); 184 } 185 186 m_defaultCommands.put(mechanism, defaultCommand); 187 } 188 189 /** 190 * Gets the default command set for a mechanism. 191 * 192 * @param mechanism The mechanism 193 * @return The default command, or null if no default command was ever set 194 */ 195 public Command getDefaultCommandFor(Mechanism mechanism) { 196 return m_defaultCommands.get(mechanism); 197 } 198 199 /** 200 * Adds a callback to run as part of the scheduler. The callback should not manipulate or control 201 * any mechanisms, but can be used to log information, update data (such as simulations or LED 202 * data buffers), or perform some other helpful task. The callback is responsible for managing its 203 * own control flow and end conditions. If you want to run a single task periodically for the 204 * entire lifespan of the scheduler, use {@link #addPeriodic(Runnable)}. 205 * 206 * <p><strong>Note:</strong> Like commands, any loops in the callback must appropriately yield 207 * control back to the scheduler with {@link Coroutine#yield} or risk stalling your program in an 208 * unrecoverable infinite loop! 209 * 210 * @param callback the callback to sideload 211 */ 212 public void sideload(Consumer<Coroutine> callback) { 213 var coroutine = new Coroutine(this, m_scope, callback); 214 m_periodicCallbacks.add(coroutine); 215 } 216 217 /** 218 * Adds a task to run repeatedly for as long as the scheduler runs. This internally handles the 219 * looping and control yielding necessary for proper function. The callback will run at the same 220 * periodic frequency as the scheduler. 221 * 222 * <p>For example: 223 * 224 * <pre>{@code 225 * scheduler.addPeriodic(() -> leds.setData(ledDataBuffer)); 226 * scheduler.addPeriodic(() -> { 227 * SmartDashboard.putNumber("X", getX()); 228 * SmartDashboard.putNumber("Y", getY()); 229 * }); 230 * }</pre> 231 * 232 * @param callback the periodic function to run 233 */ 234 public void addPeriodic(Runnable callback) { 235 sideload( 236 coroutine -> { 237 while (true) { 238 callback.run(); 239 coroutine.yield(); 240 } 241 }); 242 } 243 244 /** Represents possible results of a command scheduling attempt. */ 245 public enum ScheduleResult { 246 /** The command was successfully scheduled and added to the queue. */ 247 SUCCESS, 248 /** The command is already scheduled or running. */ 249 ALREADY_RUNNING, 250 /** The command is a lower priority and conflicts with a command that's already running. */ 251 LOWER_PRIORITY_THAN_RUNNING_COMMAND, 252 } 253 254 /** 255 * Schedules a command to run. If one command schedules another (a "parent" and "child"), the 256 * child command will be canceled when the parent command completes. It is not possible to fork a 257 * child command and have it live longer than its parent. 258 * 259 * <p>Does nothing if the command is already scheduled or running, or requires at least one 260 * mechanism already used by a higher priority command. 261 * 262 * @param command the command to schedule 263 * @return the result of the scheduling attempt. See {@link ScheduleResult} for details. 264 * @throws IllegalArgumentException if scheduled by a command composition that has already 265 * scheduled another command that shares at least one required mechanism 266 */ 267 // Implementation detail: a child command will immediately start running when scheduled by a 268 // parent command, skipping the queue entirely. This avoids dead loop cycles where a parent 269 // schedules a child, appending it to the queue, then waits for the next cycle to pick it up and 270 // start it. With deeply nested commands, dead loops could quickly to add up and cause the 271 // innermost commands that actually _do_ something to start running hundreds of milliseconds after 272 // their root ancestor was scheduled. 273 public ScheduleResult schedule(Command command) { 274 // Get the narrowest binding scope. 275 // This prevents commands from outliving the opmodes that scheduled them, or from outliving 276 // their parents (eg if someone writes a command that manually calls schedule(Command) instead 277 // of using triggers to do so). 278 Command currentCommand = currentCommand(); 279 long currentOpmode = OpModeFetcher.getFetcher().getOpModeId(); 280 281 BindingScope scope; 282 if (currentCommand != null) { 283 scope = BindingScope.forCommand(this, currentCommand); 284 } else if (currentOpmode != 0) { 285 scope = BindingScope.forOpmode(currentOpmode); 286 } else { 287 scope = BindingScope.global(); 288 } 289 290 // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier 291 // stack frame filtering and modification. 292 var binding = 293 new Binding(scope, BindingType.IMMEDIATE, command, new Throwable().getStackTrace()); 294 295 return schedule(binding); 296 } 297 298 // Package-private for use by Trigger 299 ScheduleResult schedule(Binding binding) { 300 var command = binding.command(); 301 302 if (isScheduledOrRunning(command)) { 303 return ScheduleResult.ALREADY_RUNNING; 304 } 305 306 if (lowerPriorityThanConflictingCommands(command)) { 307 return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND; 308 } 309 310 for (var scheduledState : m_queuedToRun) { 311 if (!command.conflictsWith(scheduledState.command())) { 312 // No shared requirements, skip 313 continue; 314 } 315 if (command.isLowerPriorityThan(scheduledState.command())) { 316 // Lower priority than an already-scheduled (but not yet running) command that requires at 317 // one of the same mechanism. Ignore it. 318 return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND; 319 } 320 } 321 322 // Evict conflicting on-deck commands 323 // We check above if the input command is lower priority than any of these, 324 // so at this point we're guaranteed to be >= priority than anything already on deck 325 evictConflictingOnDeckCommands(command); 326 327 // If the binding is scoped to a particular command, that command is the parent. If we're in the 328 // middle of a run cycle and running commands, the parent is whatever command is currently 329 // running. Otherwise, there is no parent command. 330 var parentCommand = 331 binding.scope() instanceof BindingScope.ForCommand scope 332 ? scope.command() 333 : currentCommand(); 334 var state = new CommandState(command, parentCommand, buildCoroutine(command), binding); 335 336 emitScheduledEvent(command); 337 338 if (currentState() != null) { 339 // Scheduling a child command while running. Start it immediately instead of waiting a full 340 // cycle. This prevents issues with deeply nested command groups taking many scheduler cycles 341 // to start running the commands that actually _do_ things 342 evictConflictingRunningCommands(state); 343 m_runningCommands.put(command, state); 344 runCommand(state); 345 } else { 346 // Scheduling outside a command, add it to the pending set. If it's not overridden by another 347 // conflicting command being scheduled in the same scheduler loop, it'll be promoted and 348 // start to run when #runCommands() is called 349 m_queuedToRun.add(state); 350 } 351 352 return ScheduleResult.SUCCESS; 353 } 354 355 /** 356 * Checks if a command conflicts with and is a lower priority than any running command. Used when 357 * determining if the command can be scheduled. 358 */ 359 private boolean lowerPriorityThanConflictingCommands(Command command) { 360 Set<CommandState> ancestors = new HashSet<>(); 361 for (var state = currentState(); state != null; state = m_runningCommands.get(state.parent())) { 362 ancestors.add(state); 363 } 364 365 // Check for conflicts with the commands that are already running 366 for (var state : m_runningCommands.values()) { 367 if (ancestors.contains(state)) { 368 // Can't conflict with an ancestor command 369 continue; 370 } 371 372 var c = state.command(); 373 if (c.conflictsWith(command) && command.isLowerPriorityThan(c)) { 374 return true; 375 } 376 } 377 378 return false; 379 } 380 381 private void evictConflictingOnDeckCommands(Command command) { 382 for (var iterator = m_queuedToRun.iterator(); iterator.hasNext(); ) { 383 var scheduledState = iterator.next(); 384 var scheduledCommand = scheduledState.command(); 385 if (scheduledCommand.conflictsWith(command)) { 386 // Remove the lower priority conflicting command from the on deck commands. 387 // We don't need to call removeOrphanedChildren here because it hasn't started yet, 388 // meaning it hasn't had a chance to schedule any children 389 iterator.remove(); 390 emitInterruptedEvent(scheduledCommand, command); 391 emitCanceledEvent(scheduledCommand); 392 } 393 } 394 } 395 396 /** 397 * Cancels all running commands with which an incoming state conflicts. Ancestor commands of the 398 * incoming state will not be canceled. 399 */ 400 @SuppressWarnings("PMD.CompareObjectsWithEquals") 401 private void evictConflictingRunningCommands(CommandState incomingState) { 402 // The set of root states with which the incoming state conflicts but does not inherit from 403 Set<CommandState> conflictingRootStates = 404 m_runningCommands.values().stream() 405 .filter(state -> incomingState.command().conflictsWith(state.command())) 406 .filter(state -> !isAncestorOf(state.command(), incomingState)) 407 .map( 408 state -> { 409 // Find the highest level ancestor of the conflicting command from which the 410 // incoming state does _not_ inherit. If they're totally unrelated, this will 411 // get the very root ancestor; otherwise, it'll return a direct sibling of the 412 // incoming command 413 CommandState root = state; 414 while (root.parent() != null && root.parent() != incomingState.parent()) { 415 root = m_runningCommands.get(root.parent()); 416 } 417 return root; 418 }) 419 .collect(Collectors.toSet()); 420 421 // Cancel the root commands 422 for (var conflictingState : conflictingRootStates) { 423 emitInterruptedEvent(conflictingState.command(), incomingState.command()); 424 cancel(conflictingState.command()); 425 } 426 } 427 428 /** 429 * Checks if a particular command is an ancestor of another. 430 * 431 * @param ancestor the potential ancestor for which to search 432 * @param state the state to check 433 * @return true if {@code ancestor} is the direct parent or indirect ancestor, false if not 434 */ 435 @SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.SimplifyBooleanReturns"}) 436 private boolean isAncestorOf(Command ancestor, CommandState state) { 437 if (state.parent() == null) { 438 // No parent, cannot inherit 439 return false; 440 } 441 if (!m_runningCommands.containsKey(ancestor)) { 442 // The given ancestor isn't running 443 return false; 444 } 445 if (state.parent() == ancestor) { 446 // Direct child 447 return true; 448 } 449 // Check if the command's parent inherits from the given ancestor 450 return m_runningCommands.values().stream() 451 .filter(s -> state.parent() == s.command()) 452 .anyMatch(s -> isAncestorOf(ancestor, s)); 453 } 454 455 /** 456 * Cancels a command and any other command scheduled by it. This occurs immediately and does not 457 * need to wait for a call to {@link #run()}. Any command that it scheduled will also be canceled 458 * to ensure commands within compositions do not continue to run. 459 * 460 * <p>This has no effect if the given command is not currently scheduled or running. 461 * 462 * @param command the command to cancel 463 */ 464 @SuppressWarnings("PMD.CompareObjectsWithEquals") 465 public void cancel(Command command) { 466 if (command == currentCommand()) { 467 throw new IllegalArgumentException( 468 "Command `" + command.name() + "` is mounted and cannot be canceled"); 469 } 470 471 boolean running = isRunning(command); 472 473 // Evict the command. The next call to run() will schedule the default command for all its 474 // required mechanisms, unless another command requiring those mechanisms is scheduled between 475 // calling cancel() and calling run() 476 m_runningCommands.remove(command); 477 m_queuedToRun.removeIf(state -> state.command() == command); 478 479 if (running) { 480 // Only run the hook if the command was running. If it was on deck or not 481 // even in the scheduler at the time, then there's nothing to do 482 command.onCancel(); 483 emitCanceledEvent(command); 484 } 485 486 // Clean up any orphaned child commands; their lifespan must not exceed the parent's 487 removeOrphanedChildren(command); 488 } 489 490 /** 491 * Updates the command scheduler. This will run operations in the following order: 492 * 493 * <ol> 494 * <li>Run sideloaded functions from {@link #sideload(Consumer)} and {@link 495 * #addPeriodic(Runnable)} 496 * <li>Update trigger bindings to queue and cancel bound commands 497 * <li>Queue default commands for mechanisms that do not have a queued or running command 498 * <li>Promote queued commands to the running set 499 * <li>For every command in the running set, mount and run that command until it calls {@link 500 * Coroutine#yield()} or exits 501 * </ol> 502 * 503 * <p>This method is intended to be called in a periodic loop like {@link 504 * TimedRobot#robotPeriodic()} 505 */ 506 public void run() { 507 final long startMicros = RobotController.getTime(); 508 509 // Sideloads may change some state that affects triggers. Run them first. 510 runPeriodicSideloads(); 511 512 // Poll triggers next to schedule and cancel commands 513 m_eventLoop.poll(); 514 515 // Schedule default commands for any mechanisms that don't have a running command and didn't 516 // have a new command scheduled by a sideload function or a trigger 517 scheduleDefaultCommands(); 518 519 // Move all scheduled commands to the running set 520 promoteScheduledCommands(); 521 522 // Run every command in order until they call Coroutine.yield() or exit 523 runCommands(); 524 525 final long endMicros = RobotController.getTime(); 526 m_lastRunTimeMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds); 527 } 528 529 private void promoteScheduledCommands() { 530 // Clear any commands that conflict with the scheduled set 531 for (var queuedState : m_queuedToRun) { 532 evictConflictingRunningCommands(queuedState); 533 } 534 535 // Move any scheduled commands to the running set 536 for (var queuedState : m_queuedToRun) { 537 m_runningCommands.put(queuedState.command(), queuedState); 538 } 539 540 // Clear the set of on-deck commands, 541 // since we just put them all into the set of running commands 542 m_queuedToRun.clear(); 543 } 544 545 private void runPeriodicSideloads() { 546 // Update periodic callbacks 547 for (Coroutine coroutine : m_periodicCallbacks) { 548 coroutine.mount(); 549 try { 550 coroutine.runToYieldPoint(); 551 } finally { 552 Continuation.mountContinuation(null); 553 } 554 } 555 556 // And remove any periodic callbacks that have completed 557 m_periodicCallbacks.removeIf(Coroutine::isDone); 558 } 559 560 private void runCommands() { 561 // Tick every command that hasn't been completed yet 562 // Run in reverse so parent commands can resume in the same loop cycle an awaited child command 563 // completes. Otherwise, parents could only resume on the next loop cycle, introducing a delay 564 // at every layer of nesting. 565 for (var state : List.copyOf(m_runningCommands.values()).reversed()) { 566 runCommand(state); 567 } 568 } 569 570 /** 571 * Mounts and runs a command until it yields or exits. 572 * 573 * @param state The command state to run 574 */ 575 @SuppressWarnings("PMD.AvoidCatchingGenericException") 576 private void runCommand(CommandState state) { 577 final var command = state.command(); 578 final var coroutine = state.coroutine(); 579 580 if (!m_runningCommands.containsKey(command)) { 581 // Probably canceled by an owning composition, do not run 582 return; 583 } 584 585 var previousState = currentState(); 586 587 m_currentCommandAncestry.push(state); 588 long startMicros = RobotController.getTime(); 589 emitMountedEvent(command); 590 coroutine.mount(); 591 try { 592 coroutine.runToYieldPoint(); 593 } catch (RuntimeException e) { 594 // Command encountered an uncaught exception. 595 handleCommandException(state, e); 596 } finally { 597 long endMicros = RobotController.getTime(); 598 double elapsedMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds); 599 state.setLastRuntimeMs(elapsedMs); 600 601 if (state.equals(currentState())) { 602 // Remove the command we just ran from the top of the stack 603 m_currentCommandAncestry.pop(); 604 } 605 606 if (previousState != null) { 607 // Remount the parent command, if there is one 608 previousState.coroutine().mount(); 609 } else { 610 Continuation.mountContinuation(null); 611 } 612 } 613 614 if (coroutine.isDone()) { 615 // Immediately check if the command has completed and remove any children commands. 616 // This prevents child commands from being executed one extra time in the run() loop 617 emitCompletedEvent(command); 618 m_runningCommands.remove(command); 619 removeOrphanedChildren(command); 620 } else { 621 // Yielded 622 emitYieldedEvent(command); 623 } 624 } 625 626 /** 627 * Handles uncaught runtime exceptions from a mounted and running command. The command's ancestor 628 * and child commands will all be canceled and the exception's backtrace will be modified to 629 * include the stack frames of the schedule call site. 630 * 631 * @param state The state of the command that encountered the exception. 632 * @param e The exception that was thrown. 633 * @throws RuntimeException rethrows the exception, with a modified backtrace pointing to the 634 * schedule site 635 */ 636 @SuppressWarnings("PMD.CompareObjectsWithEquals") 637 private void handleCommandException(CommandState state, RuntimeException e) { 638 var command = state.command(); 639 640 // Fetch the root command 641 // (needs to be done before removing the failed command from the running set) 642 Command root = command; 643 while (getParentOf(root) != null) { 644 root = getParentOf(root); 645 } 646 647 // Remove it from the running set. 648 m_runningCommands.remove(command); 649 650 // Intercept the exception, inject stack frames from the schedule site, and rethrow it 651 var binding = state.binding(); 652 e.setStackTrace(CommandTraceHelper.modifyTrace(e.getStackTrace(), binding.frames())); 653 emitCompletedWithErrorEvent(command, e); 654 655 // Clean up child commands after emitting the event so child Canceled events are emitted 656 // after the parent's CompletedWithError 657 removeOrphanedChildren(command); 658 659 // Bubble up to the root and cancel all commands between the root and this one 660 // Note: Because we remove the command from the running set above, we still need to 661 // clean up all the failed command's children 662 if (root != null && root != command) { 663 cancel(root); 664 } 665 666 // Then rethrow the exception 667 throw e; 668 } 669 670 /** 671 * Gets the currently executing command state, or null if no command is currently executing. 672 * 673 * @return the currently executing command state 674 */ 675 private CommandState currentState() { 676 if (m_currentCommandAncestry.isEmpty()) { 677 // Avoid EmptyStackException 678 return null; 679 } 680 681 return m_currentCommandAncestry.peek(); 682 } 683 684 /** 685 * Gets the currently executing command, or null if no command is currently executing. 686 * 687 * @return the currently executing command 688 */ 689 public Command currentCommand() { 690 var state = currentState(); 691 if (state == null) { 692 return null; 693 } 694 695 return state.command(); 696 } 697 698 private void scheduleDefaultCommands() { 699 // Schedule the default commands for every mechanism that doesn't currently have a running or 700 // scheduled command. 701 m_defaultCommands.forEach( 702 (mechanism, defaultCommand) -> { 703 if (m_runningCommands.keySet().stream().noneMatch(c -> c.requires(mechanism)) 704 && m_queuedToRun.stream().noneMatch(c -> c.command().requires(mechanism)) 705 && defaultCommand != null) { 706 // Nothing currently running or scheduled 707 // Schedule the mechanism's default command, if it has one 708 schedule(defaultCommand); 709 } 710 }); 711 } 712 713 /** 714 * Removes all commands descended from a parent command. This is used to ensure that any command 715 * scheduled within a composition or group cannot live longer than any ancestor. 716 * 717 * @param parent the root command whose descendants to remove from the scheduler 718 */ 719 @SuppressWarnings("PMD.CompareObjectsWithEquals") 720 private void removeOrphanedChildren(Command parent) { 721 m_runningCommands.entrySet().stream() 722 .filter(e -> e.getValue().parent() == parent) 723 .toList() // copy to an intermediate list to avoid concurrent modification 724 .forEach(e -> cancel(e.getKey())); 725 } 726 727 /** 728 * Builds a coroutine object that the command will be bound to. The coroutine will be scoped to 729 * this scheduler object and cannot be used by another scheduler instance. 730 * 731 * @param command the command for which to build a coroutine 732 * @return the binding coroutine 733 */ 734 private Coroutine buildCoroutine(Command command) { 735 return new Coroutine(this, m_scope, command::run); 736 } 737 738 /** 739 * Checks if a command is currently running. 740 * 741 * @param command the command to check 742 * @return true if the command is running, false if not 743 */ 744 public boolean isRunning(Command command) { 745 return m_runningCommands.containsKey(command); 746 } 747 748 /** 749 * Checks if a command is currently scheduled to run, but is not yet running. 750 * 751 * @param command the command to check 752 * @return true if the command is scheduled to run, false if not 753 */ 754 @SuppressWarnings("PMD.CompareObjectsWithEquals") 755 public boolean isScheduled(Command command) { 756 return m_queuedToRun.stream().anyMatch(state -> state.command() == command); 757 } 758 759 /** 760 * Checks if a command is currently scheduled to run, or is already running. 761 * 762 * @param command the command to check 763 * @return true if the command is scheduled to run or is already running, false if not 764 */ 765 public boolean isScheduledOrRunning(Command command) { 766 return isScheduled(command) || isRunning(command); 767 } 768 769 /** 770 * Gets the set of all currently running commands. Commands are returned in the order in which 771 * they were scheduled. The returned set is read-only. 772 * 773 * @return the currently running commands 774 */ 775 public Collection<Command> getRunningCommands() { 776 return Collections.unmodifiableSet(m_runningCommands.keySet()); 777 } 778 779 /** 780 * Gets all the currently running commands that require a particular mechanism. Commands are 781 * returned in the order in which they were scheduled. The returned list is read-only. 782 * 783 * @param mechanism the mechanism to get the commands for 784 * @return the currently running commands that require the mechanism. 785 */ 786 public List<Command> getRunningCommandsFor(Mechanism mechanism) { 787 return m_runningCommands.keySet().stream() 788 .filter(command -> command.requires(mechanism)) 789 .toList(); 790 } 791 792 /** 793 * Cancels all currently running and scheduled commands. All default commands will be scheduled on 794 * the next call to {@link #run()}, unless a higher priority command is scheduled or triggered 795 * after {@code cancelAll()} is used. 796 */ 797 public void cancelAll() { 798 for (var onDeckIter = m_queuedToRun.iterator(); onDeckIter.hasNext(); ) { 799 var state = onDeckIter.next(); 800 onDeckIter.remove(); 801 emitCanceledEvent(state.command()); 802 } 803 804 for (var liveIter = m_runningCommands.entrySet().iterator(); liveIter.hasNext(); ) { 805 var entry = liveIter.next(); 806 liveIter.remove(); 807 Command canceledCommand = entry.getKey(); 808 canceledCommand.onCancel(); 809 emitCanceledEvent(canceledCommand); 810 } 811 } 812 813 /** 814 * An event loop used by the scheduler to poll triggers that schedule or cancel commands. This 815 * event loop is always polled on every call to {@link #run()}. Custom event loops need to be 816 * bound to this one for synchronicity with the scheduler; however, they can always be polled 817 * manually before or after the call to {@link #run()}. 818 * 819 * @return the default event loop. 820 */ 821 @NoDiscard 822 public EventLoop getDefaultEventLoop() { 823 return m_eventLoop; 824 } 825 826 /** 827 * For internal use. 828 * 829 * @return The commands that have been scheduled but not yet started. 830 */ 831 @NoDiscard 832 public Collection<Command> getQueuedCommands() { 833 return m_queuedToRun.stream().map(CommandState::command).toList(); 834 } 835 836 /** 837 * For internal use. 838 * 839 * @param command The command to check 840 * @return The command that forked the provided command. Null if the command is not a child of 841 * another command. 842 */ 843 public Command getParentOf(Command command) { 844 var state = m_runningCommands.get(command); 845 if (state == null) { 846 return null; 847 } 848 return state.parent(); 849 } 850 851 /** 852 * Gets how long a command took to run in the previous cycle. If the command is not currently 853 * running, returns -1. 854 * 855 * @param command The command to check 856 * @return How long, in milliseconds, the command last took to execute. 857 */ 858 public double lastCommandRuntimeMs(Command command) { 859 if (m_runningCommands.containsKey(command)) { 860 return m_runningCommands.get(command).lastRuntimeMs(); 861 } else { 862 return -1; 863 } 864 } 865 866 /** 867 * Gets how long a command has taken to run, in aggregate, since it was most recently scheduled. 868 * If the command is not currently running, returns -1. 869 * 870 * @param command The command to check 871 * @return How long, in milliseconds, the command has taken to execute in total 872 */ 873 public double totalRuntimeMs(Command command) { 874 if (m_runningCommands.containsKey(command)) { 875 return m_runningCommands.get(command).totalRuntimeMs(); 876 } else { 877 // Not running; no data 878 return -1; 879 } 880 } 881 882 /** 883 * Gets the unique run id for a scheduled or running command. If the command is not currently 884 * scheduled or running, this function returns {@code 0}. 885 * 886 * @param command The command to get the run ID for 887 * @return The run of the command 888 */ 889 @NoDiscard 890 @SuppressWarnings("PMD.CompareObjectsWithEquals") 891 public int runId(Command command) { 892 if (m_runningCommands.containsKey(command)) { 893 return m_runningCommands.get(command).id(); 894 } 895 896 // Check scheduled commands 897 for (var scheduled : m_queuedToRun) { 898 if (scheduled.command() == command) { 899 return scheduled.id(); 900 } 901 } 902 903 return 0; 904 } 905 906 /** 907 * Gets how long the scheduler took to process its most recent {@link #run()} invocation, in 908 * milliseconds. Defaults to -1 if the scheduler has not yet run. 909 * 910 * @return How long, in milliseconds, the scheduler last took to execute. 911 */ 912 @NoDiscard 913 public double lastRuntimeMs() { 914 return m_lastRunTimeMs; 915 } 916 917 // Event-base telemetry and helpers. The static factories are for convenience to automatically 918 // set the timestamp instead of littering RobotController.getTime() everywhere. 919 920 private void emitScheduledEvent(Command command) { 921 var event = new SchedulerEvent.Scheduled(command, RobotController.getTime()); 922 emitEvent(event); 923 } 924 925 private void emitMountedEvent(Command command) { 926 var event = new SchedulerEvent.Mounted(command, RobotController.getTime()); 927 emitEvent(event); 928 } 929 930 private void emitYieldedEvent(Command command) { 931 var event = new SchedulerEvent.Yielded(command, RobotController.getTime()); 932 emitEvent(event); 933 } 934 935 private void emitCompletedEvent(Command command) { 936 var event = new SchedulerEvent.Completed(command, RobotController.getTime()); 937 emitEvent(event); 938 } 939 940 private void emitCompletedWithErrorEvent(Command command, Throwable error) { 941 var event = new SchedulerEvent.CompletedWithError(command, error, RobotController.getTime()); 942 emitEvent(event); 943 } 944 945 private void emitCanceledEvent(Command command) { 946 var event = new SchedulerEvent.Canceled(command, RobotController.getTime()); 947 emitEvent(event); 948 } 949 950 private void emitInterruptedEvent(Command command, Command interrupter) { 951 var event = new SchedulerEvent.Interrupted(command, interrupter, RobotController.getTime()); 952 emitEvent(event); 953 } 954 955 /** 956 * Adds a listener to handle events that are emitted by the scheduler. Events are emitted when 957 * certain actions are taken by user code or by internal processing logic in the scheduler. 958 * Listeners should take care to be quick, simple, and not schedule or cancel commands, as that 959 * may cause inconsistent scheduler behavior or even cause a program crash. 960 * 961 * <p>Listeners are primarily expected to be for data logging and telemetry. In particular, a 962 * one-shot command (one that never calls {@link Coroutine#yield()}) will never appear in the 963 * standard protobuf telemetry because it is scheduled, runs, and finishes all in a single 964 * scheduler cycle. However, {@link SchedulerEvent.Scheduled},{@link SchedulerEvent.Mounted}, and 965 * {@link SchedulerEvent.Completed} events will be emitted corresponding to those actions, and 966 * user code can listen for and log such events. 967 * 968 * @param listener The listener to add. Cannot be null. 969 * @throws NullPointerException if given a null listener 970 */ 971 public void addEventListener(Consumer<? super SchedulerEvent> listener) { 972 ErrorMessages.requireNonNullParam(listener, "listener", "addEventListener"); 973 974 m_eventListeners.add(listener); 975 } 976 977 private void emitEvent(SchedulerEvent event) { 978 // TODO: Prevent listeners from interacting with the scheduler. 979 // Scheduling or canceling commands while the scheduler is processing will probably cause 980 // bugs in user code or even a program crash. 981 for (var listener : m_eventListeners) { 982 listener.accept(event); 983 } 984 } 985}