RobotCore 1.0
WPILib port to raspberry Pi
Loading...
Searching...
No Matches
Scheduler.java
1// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5package org.wpilib.command3;
6
7import static org.wpilib.units.Units.Milliseconds;
8import static org.wpilib.units.Units.Nanoseconds;
9
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashSet;
14import java.util.LinkedHashMap;
15import java.util.LinkedHashSet;
16import java.util.List;
17import java.util.Map;
18import java.util.SequencedMap;
19import java.util.SequencedSet;
20import java.util.Set;
21import java.util.Stack;
22import java.util.function.Consumer;
23import java.util.stream.Collectors;
24import org.wpilib.annotation.NoDiscard;
25import org.wpilib.command3.Scheduler.ScheduleResult.AlreadyRunning;
26import org.wpilib.command3.Scheduler.ScheduleResult.LowerPriorityThanQueuedCommand;
27import org.wpilib.command3.Scheduler.ScheduleResult.LowerPriorityThanRunningCommand;
28import org.wpilib.command3.Scheduler.ScheduleResult.RequiresUnsafeMechanisms;
29import org.wpilib.command3.Scheduler.ScheduleResult.Success;
30import org.wpilib.command3.button.CommandGenericHID;
31import org.wpilib.command3.proto.SchedulerProto;
32import org.wpilib.event.EventLoop;
33import org.wpilib.framework.TimedRobot;
34import org.wpilib.system.RobotController;
35import org.wpilib.util.ErrorMessages;
36import org.wpilib.util.protobuf.ProtobufSerializable;
37
106public final class Scheduler implements ProtobufSerializable {
112 private final Map<Mechanism, List<Binding>> m_defaultCommandBindings = new LinkedHashMap<>();
113
118 private final Collection<Binding> m_activeBindings = new ArrayList<>();
119
121 private final Set<Trigger> m_boundTriggers = new HashSet<>();
122
124 private final SequencedSet<CommandState> m_queuedToRun = new LinkedHashSet<>();
125
130 private final SequencedMap<Command, CommandState> m_runningCommands = new LinkedHashMap<>();
131
137 private final Stack<CommandState> m_currentCommandAncestry = new Stack<>();
138
140 private final List<PeriodicCallback> m_periodicCallbacks = new ArrayList<>();
141
143 private final EventLoop m_eventLoop = new EventLoop();
144
146 private final ContinuationScope m_scope = new ContinuationScope("coroutine commands");
147
149 private record PeriodicCallback(BindingScope scope, Coroutine coroutine) {}
150
151 // Telemetry
153 public static final SchedulerProto proto = new SchedulerProto();
154
155 private double m_lastRunTimeMs = -1;
156
157 private final Set<Consumer<? super SchedulerEvent>> m_eventListeners = new LinkedHashSet<>();
158
160 private static final Scheduler s_defaultScheduler = new Scheduler();
161
170 @NoDiscard
171 public static Scheduler getDefault() {
172 return s_defaultScheduler;
173 }
174
184 @NoDiscard
185 public static Scheduler createIndependentScheduler() {
186 return new Scheduler();
187 }
188
190 private Scheduler() {}
191
210 @SuppressWarnings("PMD.CompareObjectsWithEquals")
211 public void setDefaultCommand(Mechanism mechanism, Command defaultCommand) {
212 if (!defaultCommand.requires(mechanism)) {
213 throw new IllegalArgumentException(
214 "A mechanism's default command must require that mechanism");
215 }
216
217 if (defaultCommand.requirements().size() > 1) {
218 throw new IllegalArgumentException(
219 "A mechanism's default command cannot require other mechanisms");
220 }
221
224
225 var binding =
226 new Binding(
227 scope, BindingType.CONTINUOUSLY_SCHEDULE_WHILE_HIGH, defaultCommand, new Throwable());
228
229 var currentDefaultCommand = getDefaultCommandFor(mechanism);
230 m_defaultCommandBindings.computeIfAbsent(mechanism, k -> new ArrayList<>()).add(binding);
231
232 if (currentCommand != null && currentCommand != currentDefaultCommand) {
233 // User called `setDefaultCommand` inside another command.
234 // Immediately reprocess the default commands for this mechanism to ensure it's in sync with
235 // the rest of the commands in the scheduler. This is required because we normally schedule
236 // the default commands at the start of the scheduler `run()` method, so the new default
237 // command wouldn't be handled until the next run (ie, the previous default command would
238 // still be active for the current iteration)
239 //
240 // Note that we cannot do this if the current default command is the caller because commands
241 // cannot be canceled while mounted.
242 processDefaultCommands(mechanism);
243 }
244 }
245
253 var bindings = m_defaultCommandBindings.getOrDefault(mechanism, Collections.emptyList());
254 if (bindings.isEmpty()) {
255 return null;
256 }
257
258 return bindings.getLast().command();
259 }
260
261 // package-private helper for unit test access
262 List<Binding> getDefaultCommandBindingsFor(Mechanism mechanism) {
263 return m_defaultCommandBindings.getOrDefault(mechanism, Collections.emptyList());
264 }
265
285 public void sideload(Consumer<Coroutine> callback) {
286 var coroutine = new Coroutine(this, m_scope, callback);
287 // Sideloaded functions shouldn't be canceled if a command can't be forked. The default behavior
288 // should allow user code the opportunity to recover from a failure. This differs from
289 // coroutines issued to commands, which should fail by default to prevent unexpected robot state
290 coroutine.setCancelOnForkFailure(false);
291
292 var scope = BindingScope.createNarrowestScope(this);
293 m_periodicCallbacks.add(new PeriodicCallback(scope, coroutine));
294 }
295
330 public void addPeriodic(Runnable callback) {
331 sideload(
332 coroutine -> {
333 while (true) {
334 callback.run();
335 coroutine.yield();
336 }
337 });
338 }
339
345 public sealed interface ScheduleResult {
352
358 boolean successful();
359
361 sealed interface Successful extends ScheduleResult {
362 @Override
363 default boolean successful() {
364 return true;
365 }
366
377 int runId();
378 }
379
381 sealed interface Failure extends ScheduleResult {
382 @Override
383 default boolean successful() {
384 return false;
385 }
386 }
387
394 record Success(Command command, int runId) implements Successful {}
395
408 @Override
409 public int runId() {
410 return runId;
411 }
412 }
413
422 implements Failure {}
423
432 implements Failure {}
433
442 record RequiresUnsafeMechanisms(Command command, Collection<Mechanism> unsafeMechanisms)
443 implements Failure {}
444 }
445
466 @NoDiscard
468 ErrorMessages.requireNonNullParam(command, "command", "isSchedulable");
469
470 int id = runId(command);
471 if (id != 0) {
472 return new AlreadyRunning(command, id);
473 }
474
475 if (!RobotStateFetcher.getFetcher().isEnabled()) {
476 var uncontrollables =
477 command.requirements().stream()
478 .filter(mechanism -> !mechanism.controllableDuringDisabled())
479 .collect(Collectors.toSet());
480
481 if (!uncontrollables.isEmpty()) {
482 return new RequiresUnsafeMechanisms(command, uncontrollables);
483 }
484 }
485
486 Set<Command> ancestry = new HashSet<>();
487 for (var state = currentState(); state != null; state = m_runningCommands.get(state.parent())) {
488 ancestry.add(state.command());
489 }
490
491 return isSchedulableFromPriority(command, ancestry);
492 }
493
494 // similar to isSchedulable(Command), but used internally for processing bindings
495 // that may have command scopes.
496 private ScheduleResult isSchedulable(Binding binding) {
497 var command = binding.command();
498
499 int id = runId(command);
500 if (id != 0) {
501 return new AlreadyRunning(command, id);
502 }
503
504 if (!RobotStateFetcher.getFetcher().isEnabled()) {
505 var uncontrollables =
506 command.requirements().stream()
507 .filter(mechanism -> !mechanism.controllableDuringDisabled())
508 .collect(Collectors.toSet());
509
510 if (!uncontrollables.isEmpty()) {
511 return new RequiresUnsafeMechanisms(command, uncontrollables);
512 }
513 }
514
515 Set<Command> ancestry = new HashSet<>();
516 if (binding.scope() instanceof BindingScope.ForCommand(Scheduler _, Command parent)) {
517 for (var state = m_runningCommands.get(parent);
518 state != null;
519 state = m_runningCommands.get(state.parent())) {
520 ancestry.add(state.command());
521 }
522 } else {
523 for (var state = currentState();
524 state != null;
525 state = m_runningCommands.get(state.parent())) {
526 ancestry.add(state.command());
527 }
528 }
529
530 return isSchedulableFromPriority(command, ancestry);
531 }
532
533 private Scheduler.ScheduleResult isSchedulableFromPriority(
534 Command command, Set<Command> ancestry) {
535 var running = m_runningCommands.values();
536 Command conflict = lowerPriorityThanConflictingCommands(command, ancestry, running);
537 if (conflict != null) {
538 return new LowerPriorityThanRunningCommand(command, conflict);
539 }
540
541 conflict = lowerPriorityThanConflictingCommands(command, ancestry, m_queuedToRun);
542 if (conflict != null) {
543 return new LowerPriorityThanQueuedCommand(command, conflict);
544 }
545
546 return new Success(command, 0);
547 }
548
568 // Implementation detail: a child command will immediately start running when scheduled by a
569 // parent command, skipping the queue entirely. This avoids dead loop cycles where a parent
570 // schedules a child, appending it to the queue, then waits for the next cycle to pick it up and
571 // start it. With deeply nested commands, dead loops could quickly to add up and cause the
572 // innermost commands that actually _do_ something to start running hundreds of milliseconds after
573 // their root ancestor was scheduled.
575 // Get the narrowest binding scope.
576 // This prevents commands from outliving the opmodes that scheduled them, or from outliving
577 // their parents (eg if someone writes a command that manually calls schedule(Command) instead
578 // of using triggers to do so).
580
581 // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier
582 // stack frame filtering and modification.
583 var binding = new Binding(scope, BindingType.IMMEDIATE, command, new Throwable());
584
585 return schedule(binding);
586 }
587
588 // Package-private for use by Trigger
589 ScheduleResult schedule(Binding binding) {
590 var command = binding.command();
591
592 var result = isSchedulable(binding);
593 if (!(result instanceof Success)) {
594 // We check specifically for Success, instead of `successful()`, because only a Success
595 // indicates that the command can actually go through the scheduling process. AlreadyRunning
596 // means what it says on the tin, and it would be incorrect to run the command back through
597 // the scheduling process.
598 return result;
599 }
600
601 if (!(binding.scope() instanceof BindingScope.ForCommand)) {
602 // Track this binding so we can disable it when it's out of scope.
603 // Note that, even though triggers can clean themselves up, commands that are manually
604 // scheduled cannot do the same, so we have to track them in the scheduler.
605
606 // We don't bother tracking command-scoped bindings; the bound command will already be
607 // cleaned up in the same cycle that the parent command exits, so this would attempt to
608 // double-cancel a child command across two cycles (once when the parent exits, and then in
609 // the next cycle in cancelStaleBindings). That would cause problems if the command object
610 // is immediately rescheduled as cancelStaleBindings would immediately cancel the new run
611 m_activeBindings.add(binding);
612 }
613
614 // Evict conflicting on-deck commands
615 // We check above if the input command is lower priority than any of these,
616 // so at this point we're guaranteed to be >= priority than anything already on deck
617 evictConflictingOnDeckCommands(command);
618
619 // If the binding is scoped to a particular command, that command is the parent. If we're in the
620 // middle of a run cycle and running commands, the parent is whatever command is currently
621 // running. Otherwise, there is no parent command.
622 var parentCommand =
623 binding.scope() instanceof BindingScope.ForCommand scope
624 ? scope.command()
625 : currentCommand();
626 var state = new CommandState(command, parentCommand, buildCoroutine(command), binding);
627
628 emitScheduledEvent(command);
629
630 if (currentState() != null) {
631 // Scheduling a child command while running. Start it immediately instead of waiting a full
632 // cycle. This prevents issues with deeply nested command groups taking many scheduler cycles
633 // to start running the commands that actually _do_ things
634 evictConflictingRunningCommands(state);
635 m_runningCommands.put(command, state);
636 runCommand(state);
637 } else {
638 // Scheduling outside a command, add it to the pending set. If it's not overridden by another
639 // conflicting command being scheduled in the same scheduler loop, it'll be promoted and
640 // start to run when #runCommands() is called
641 m_queuedToRun.add(state);
642 }
643
644 return new Success(command, state.id());
645 }
646
653 private Command lowerPriorityThanConflictingCommands(
654 Command command, Collection<Command> ancestry, Collection<CommandState> checkAgainst) {
655 int maxAncestorPriority = command.priority();
656 for (Command ancestor : ancestry) {
657 maxAncestorPriority = Math.max(maxAncestorPriority, ancestor.priority());
658 }
659
660 // Check for conflicts with the commands that are already running
661 for (var state : checkAgainst) {
662 if (ancestry.contains(state.command())) {
663 // Can't conflict with an ancestor command
664 continue;
665 }
666
667 var c = state.command();
668 if (c.conflictsWith(command) && maxAncestorPriority < effectivePriority(state)) {
669 return c;
670 }
671 }
672
673 return null;
674 }
675
683 private int effectivePriority(CommandState state) {
684 int max = state.command().priority();
685 for (var parent = state.parent(); parent != null; parent = state.parent()) {
686 state = m_runningCommands.get(parent);
687 max = Math.max(max, parent.priority());
688 }
689 return max;
690 }
691
692 private void evictConflictingOnDeckCommands(Command command) {
693 for (var iterator = m_queuedToRun.iterator(); iterator.hasNext(); ) {
694 var scheduledState = iterator.next();
695 var scheduledCommand = scheduledState.command();
696 if (scheduledCommand.conflictsWith(command)) {
697 // Remove the lower priority conflicting command from the on deck commands.
698 // We don't need to call removeOrphanedChildren here because it hasn't started yet,
699 // meaning it hasn't had a chance to schedule any children
700 iterator.remove();
701 emitInterruptedEvent(scheduledCommand, command);
702 emitCanceledEvent(scheduledCommand);
703 }
704 }
705 }
706
711 @SuppressWarnings("PMD.CompareObjectsWithEquals")
712 private void evictConflictingRunningCommands(CommandState incomingState) {
713 // The set of root states with which the incoming state conflicts but does not inherit from
714 Set<CommandState> conflictingRootStates =
715 m_runningCommands.values().stream()
716 .filter(state -> incomingState.command().conflictsWith(state.command()))
717 .filter(state -> !isAncestorOf(state.command(), incomingState))
718 .map(
719 state -> {
720 // Find the highest level ancestor of the conflicting command from which the
721 // incoming state does _not_ inherit. If they're totally unrelated, this will
722 // get the very root ancestor; otherwise, it'll return a direct sibling of the
723 // incoming command
724 CommandState root = state;
725 while (root.parent() != null && root.parent() != incomingState.parent()) {
726 root = m_runningCommands.get(root.parent());
727 }
728 return root;
729 })
730 .collect(Collectors.toSet());
731
732 // Cancel the root commands
733 for (var conflictingState : conflictingRootStates) {
734 emitInterruptedEvent(conflictingState.command(), incomingState.command());
735 cancel(conflictingState.command());
736 }
737 }
738
746 @SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.SimplifyBooleanReturns"})
747 private boolean isAncestorOf(Command ancestor, CommandState state) {
748 if (state.parent() == null) {
749 // No parent, cannot inherit
750 return false;
751 }
752 if (!m_runningCommands.containsKey(ancestor)) {
753 // The given ancestor isn't running
754 return false;
755 }
756 if (state.parent() == ancestor) {
757 // Direct child
758 return true;
759 }
760 // Check if the command's parent inherits from the given ancestor
761 return m_runningCommands.values().stream()
762 .filter(s -> state.parent() == s.command())
763 .anyMatch(s -> isAncestorOf(ancestor, s));
764 }
765
775 @SuppressWarnings("PMD.CompareObjectsWithEquals")
776 public void cancel(Command command) {
777 var currentState = currentState();
778 if (currentState != null && command == currentState.command()) {
779 currentState.coroutine().requestCancellation(); // yields internally
780 return;
781 }
782
783 boolean running = isRunning(command);
784
785 // Evict the command. The next call to run() will schedule the default command for all its
786 // required mechanisms, unless another command requiring those mechanisms is scheduled between
787 // calling cancel() and calling run()
788 m_runningCommands.remove(command);
789 boolean queued = m_queuedToRun.removeIf(state -> state.command() == command);
790
791 if (running) {
792 // Only run the hooks if the command was running. If it was on deck or not
793 // even in the scheduler at the time, then there's nothing to do
794 // Always run onExit first, in case cancellation has special behavior that overrides
795 // standard exit logic.
796 command.onExit();
797 command.onCancel();
798 }
799
800 if (running || queued) {
801 // Emit a cancellation event only if the given command was in the scheduler
802 emitCanceledEvent(command);
803 }
804
805 // Clean up any orphaned child commands; their lifespan must not exceed the parent's
806 removeOrphanedChildren(command);
807 }
808
829 public void run() {
830 final long startNanos = RobotController.getTime();
831
832 // Cancel any commands with stale binding scopes
833 cancelStaleBindings();
834
835 // Unbind any triggers with stale creation scopes.
836 // This allows triggers that can never be used again to be garbage collected to reduce
837 // memory usage and avoid potential OOMs from poorly written user code.
838 unbindStaleTriggers();
839
840 // If the robot is disabled, cancel any commands that require uncontrollable mechanisms
841 cancelCommandsThatCannotRunInDisabled();
842
843 // Sideloads may change some state that affects triggers. Run them first.
844 runPeriodicSideloads();
845
846 // Poll triggers next to schedule and cancel commands
847 m_eventLoop.poll();
848
849 // Schedule default commands for any mechanisms that don't have a running command and didn't
850 // have a new command scheduled by a sideload function or a trigger
851 scheduleDefaultCommands();
852
853 // Move all scheduled commands to the running set
854 promoteScheduledCommands();
855
856 // Run every command in order until they call Coroutine.yield() or exit
857 runCommands();
858
859 final long endNanos = RobotController.getTime();
860 m_lastRunTimeMs = Milliseconds.convertFrom(endNanos - startNanos, Nanoseconds);
861 }
862
863 private void cancelStaleBindings() {
864 for (var iterator = m_activeBindings.iterator(); iterator.hasNext(); ) {
865 var binding = iterator.next();
866 if (binding.scope().active()) {
867 continue;
868 }
869 cancel(binding.command());
870 iterator.remove();
871 }
872 }
873
874 private void unbindStaleTriggers() {
875 // Remove strong references to any triggers that have gone stale. This allows triggers to be
876 // garbage collected if they're not referenced outside the scope that created them. We don't
877 // clear any command bindings or unbind it from the event loop; otherwise, cached triggers
878 // would stop being updated until new command bindings are added.
879 for (var iterator = m_boundTriggers.iterator(); iterator.hasNext(); ) {
880 var trigger = iterator.next();
881 if (!trigger.isScopeActive()) {
882 iterator.remove();
883 }
884 }
885 }
886
887 private void cancelCommandsThatCannotRunInDisabled() {
888 if (RobotStateFetcher.getFetcher().isEnabled()) {
889 // Nothing to do if the robot is enabled
890 return;
891 }
892
893 List<Command> commandsToCancel = new ArrayList<>();
894
895 for (var runningState : m_runningCommands.values()) {
896 var command = runningState.command();
897 boolean canRun = true;
898 for (var mechanism : command.requirements()) {
899 if (!mechanism.controllableDuringDisabled()) {
900 canRun = false;
901 break;
902 }
903 }
904
905 if (canRun) {
906 continue;
907 }
908
909 commandsToCancel.add(getRoot(command));
910 }
911
912 for (var queuedState : m_queuedToRun) {
913 var command = queuedState.command();
914
915 boolean canRun = true;
916 for (var mechanism : command.requirements()) {
917 if (!mechanism.controllableDuringDisabled()) {
918 canRun = false;
919 break;
920 }
921 }
922
923 if (canRun) {
924 continue;
925 }
926
927 commandsToCancel.add(command);
928 }
929
930 for (var command : commandsToCancel) {
931 cancel(command);
932 }
933 }
934
939 // package-private for Trigger
940 void addBoundTrigger(Trigger trigger) {
941 m_boundTriggers.add(trigger);
942 }
943
948 // package-private for Trigger
949 void removeBoundTrigger(Trigger trigger) {
950 m_boundTriggers.remove(trigger);
951 }
952
953 private void promoteScheduledCommands() {
954 // Clear any commands that conflict with the scheduled set
955 for (var queuedState : m_queuedToRun) {
956 evictConflictingRunningCommands(queuedState);
957 }
958
959 // Move any scheduled commands to the running set
960 for (var queuedState : m_queuedToRun) {
961 m_runningCommands.put(queuedState.command(), queuedState);
962 }
963
964 // Clear the set of on-deck commands,
965 // since we just put them all into the set of running commands
966 m_queuedToRun.clear();
967 }
968
969 private void runPeriodicSideloads() {
970 for (var iterator = m_periodicCallbacks.iterator(); iterator.hasNext(); ) {
971 PeriodicCallback callback = iterator.next();
972 if (!callback.scope().active()) {
973 // The callback's enclosing scope exited - remove without running it and move on
974 iterator.remove();
975 continue;
976 }
977
978 // Update periodic callbacks
979 Coroutine coroutine = callback.coroutine();
980 if (coroutine.inUnmountableState()) {
981 // Nothing to do
982 continue;
983 }
984
985 coroutine.mount();
986 try {
987 coroutine.runToYieldPoint();
988 } finally {
989 Continuation.mountContinuation(null);
990 }
991
992 if (coroutine.isDone()
993 || coroutine.isInterruptRequested()
994 || coroutine.isCancellationRequested()) {
995 // Callback finished or requested early termination - remove it from the list
996 iterator.remove();
997 }
998 }
999 }
1000
1001 private void runCommands() {
1002 // TODO: Track command roots and run the commands in each root in reverse order, but run the
1003 // roots in insertion order
1004 // Tick every command that hasn't been completed yet
1005 // Run in reverse so parent commands can resume in the same loop cycle an awaited child command
1006 // completes. Otherwise, parents could only resume on the next loop cycle, introducing a delay
1007 // at every layer of nesting.
1008 for (var state : List.copyOf(m_runningCommands.values()).reversed()) {
1009 runCommand(state);
1010 }
1011 }
1012
1018 @SuppressWarnings("PMD.AvoidCatchingGenericException")
1019 private void runCommand(CommandState state) {
1020 final var command = state.command();
1021 final var coroutine = state.coroutine();
1022
1023 if (coroutine.inUnmountableState()) {
1024 // Nothing to do
1025 return;
1026 }
1027
1028 if (!m_runningCommands.containsKey(command)) {
1029 // Probably canceled by an owning composition, do not run
1030 return;
1031 }
1032
1033 var previousState = currentState();
1034
1035 m_currentCommandAncestry.push(state);
1036 long startNanos = RobotController.getTime();
1037 emitMountedEvent(command);
1038 coroutine.mount();
1039 try {
1040 coroutine.runToYieldPoint();
1041 } catch (RuntimeException e) {
1042 // Command encountered an uncaught exception.
1043 handleCommandException(state, e);
1044 } finally {
1045 long endNanos = RobotController.getTime();
1046 double elapsedMs = Milliseconds.convertFrom(endNanos - startNanos, Nanoseconds);
1047 state.setLastRuntimeMs(elapsedMs);
1048
1049 if (state.equals(currentState())) {
1050 // Remove the command we just ran from the top of the stack
1051 m_currentCommandAncestry.pop();
1052 }
1053
1054 if (previousState != null) {
1055 // Remount the parent command, if there is one
1056 previousState.coroutine().mount();
1057 } else {
1058 Continuation.mountContinuation(null);
1059 }
1060 }
1061
1062 if (coroutine.isCancellationRequested()) {
1063 cancel(command);
1064 } else if (coroutine.isDone()) {
1065 handleCommandCompletion(command);
1066 } else if (coroutine.getForkResult() != null) {
1067 handleCoroutineIRQ(coroutine.getForkResult(), command);
1068 } else {
1069 // Yielded
1070 emitYieldedEvent(command);
1071 }
1072 }
1073
1074 private void handleCommandCompletion(Command command) {
1075 command.onExit();
1076 emitCompletedEvent(command);
1077 m_runningCommands.remove(command);
1078 removeOrphanedChildren(command);
1079 }
1080
1081 private void handleCoroutineIRQ(Coroutine.ForkResult failure, Command command) {
1082 Command root = getRoot(command); // capture the root command before modifying scheduler state
1083 m_currentCommandAncestry.clear();
1084 emitForkFailureEvent(command, failure.getFailedCommands());
1085 cancel(root);
1086 Continuation.mountContinuation(null);
1087 }
1088
1099 @SuppressWarnings("PMD.CompareObjectsWithEquals")
1100 private void handleCommandException(CommandState state, RuntimeException e) {
1101 var command = state.command();
1102
1103 // Fetch the root command
1104 // (needs to be done before removing the failed command from the running set)
1105 final Command root = getRoot(command);
1106
1107 // Remove it from the running set.
1108 m_runningCommands.remove(command);
1109
1110 // Intercept the exception, inject stack frames from the schedule site, and rethrow it
1111 var binding = state.binding();
1112 e.setStackTrace(
1113 CommandTraceHelper.modifyTrace(
1114 e.getStackTrace(), binding.stackTraceStore().getStackTrace()));
1115 emitCompletedWithErrorEvent(command, e);
1116
1117 // Clean up child commands after emitting the event so child Canceled events are emitted
1118 // after the parent's CompletedWithError
1119 removeOrphanedChildren(command);
1120
1121 // Bubble up to the root and cancel all commands between the root and this one
1122 // Note: Because we remove the command from the running set above, we still need to
1123 // clean up all the failed command's children
1124 if (root != null && root != command) {
1125 cancel(root);
1126 }
1127
1128 // Then rethrow the exception
1129 throw e;
1130 }
1131
1137 private CommandState currentState() {
1138 if (m_currentCommandAncestry.isEmpty()) {
1139 // Avoid EmptyStackException
1140 return null;
1141 }
1142
1143 return m_currentCommandAncestry.peek();
1144 }
1145
1152 var state = currentState();
1153 if (state == null) {
1154 return null;
1155 }
1156
1157 return state.command();
1158 }
1159
1160 private void scheduleDefaultCommands() {
1161 m_defaultCommandBindings.keySet().forEach(this::processDefaultCommands);
1162 }
1163
1164 private void processDefaultCommands(Mechanism mechanism) {
1165 var bindings = m_defaultCommandBindings.get(mechanism);
1166
1167 // Remove default command bindings that are no longer active.
1168 // If a default command is running when its scope goes inactive, also be sure to cancel it.
1169 bindings.removeIf(
1170 b -> {
1171 if (!b.scope().active()) {
1172 cancel(b.command());
1173 return true;
1174 }
1175 return false;
1176 });
1177
1178 if (bindings.isEmpty()) {
1179 // Nothing to do. No active bindings remain.
1180 return;
1181 }
1182
1183 // Cancel any default command except the narrowest-scoped one (the last binding in the list)
1184 for (int i = 0; i < bindings.size() - 1; i++) {
1185 Command widerScopeDefaultCommand = bindings.get(i).command();
1186 cancel(widerScopeDefaultCommand);
1187 }
1188
1189 if (!RobotStateFetcher.getFetcher().isEnabled() && !mechanism.controllableDuringDisabled()) {
1190 // Default commands can never be scheduled if the robot is disabled and the mechanism is not
1191 // controllable during disabled, so there's no point in attempting to queue the default
1192 // command
1193 return;
1194 }
1195
1196 // Check if the mechanism is currently in use. We can queue the default command if it's not.
1197 for (Command runningCommand : m_runningCommands.keySet()) {
1198 if (runningCommand.requires(mechanism)) {
1199 return;
1200 }
1201 }
1202 for (CommandState queuedState : m_queuedToRun) {
1203 if (queuedState.command().requires(mechanism)) {
1204 return;
1205 }
1206 }
1207
1208 // Nothing currently running or queued that needs this mechanism. Queue the default command.
1209 var defaultCommand = bindings.getLast();
1210 schedule(defaultCommand);
1211 }
1212
1219 @SuppressWarnings("PMD.CompareObjectsWithEquals")
1220 private void removeOrphanedChildren(Command parent) {
1221 m_runningCommands.entrySet().stream()
1222 .filter(e -> e.getValue().parent() == parent)
1223 .toList() // copy to an intermediate list to avoid concurrent modification
1224 .forEach(e -> cancel(e.getKey()));
1225 }
1226
1234 private Coroutine buildCoroutine(Command command) {
1235 return new Coroutine(this, m_scope, command::run);
1236 }
1237
1244 public boolean isRunning(Command command) {
1245 return m_runningCommands.containsKey(command);
1246 }
1247
1254 @SuppressWarnings("PMD.CompareObjectsWithEquals")
1255 public boolean isScheduled(Command command) {
1256 return m_queuedToRun.stream().anyMatch(state -> state.command() == command);
1257 }
1258
1265 public boolean isScheduledOrRunning(Command command) {
1266 return isScheduled(command) || isRunning(command);
1267 }
1268
1275 public Collection<Command> getRunningCommands() {
1276 return List.copyOf(m_runningCommands.keySet());
1277 }
1278
1286 public List<Command> getRunningCommandsFor(Mechanism mechanism) {
1287 return m_runningCommands.keySet().stream()
1288 .filter(command -> command.requires(mechanism))
1289 .toList();
1290 }
1291
1297 public void cancelAll() {
1298 for (var onDeckIter = m_queuedToRun.iterator(); onDeckIter.hasNext(); ) {
1299 var state = onDeckIter.next();
1300 onDeckIter.remove();
1301 emitCanceledEvent(state.command());
1302 }
1303
1304 for (var liveIter = m_runningCommands.entrySet().iterator(); liveIter.hasNext(); ) {
1305 var entry = liveIter.next();
1306 liveIter.remove();
1307 Command canceledCommand = entry.getKey();
1308 // Always run onExit first, in case cancellation has special behavior that overrides
1309 // standard exit logic.
1310 canceledCommand.onExit();
1311 canceledCommand.onCancel();
1312 emitCanceledEvent(canceledCommand);
1313 }
1314 }
1315
1324 @NoDiscard
1326 return m_eventLoop;
1327 }
1328
1334 @NoDiscard
1335 public Collection<Command> getQueuedCommands() {
1336 return m_queuedToRun.stream().map(CommandState::command).toList();
1337 }
1338
1339 private Command getRoot(Command command) {
1340 Command root = command;
1341 Command parent = getParentOf(command);
1342 while (parent != null) {
1343 root = parent;
1344 parent = getParentOf(parent);
1345 }
1346
1347 return root;
1348 }
1349
1357 public Command getParentOf(Command command) {
1358 var state = m_runningCommands.get(command);
1359 if (state == null) {
1360 return null;
1361 }
1362 return state.parent();
1363 }
1364
1372 public double lastCommandRuntimeMs(Command command) {
1373 if (m_runningCommands.containsKey(command)) {
1374 return m_runningCommands.get(command).lastRuntimeMs();
1375 } else {
1376 return -1;
1377 }
1378 }
1379
1387 public double totalRuntimeMs(Command command) {
1388 if (m_runningCommands.containsKey(command)) {
1389 return m_runningCommands.get(command).totalRuntimeMs();
1390 } else {
1391 // Not running; no data
1392 return -1;
1393 }
1394 }
1395
1403 @NoDiscard
1404 @SuppressWarnings("PMD.CompareObjectsWithEquals")
1405 public int runId(Command command) {
1406 if (m_runningCommands.containsKey(command)) {
1407 return m_runningCommands.get(command).id();
1408 }
1409
1410 // Check scheduled commands
1411 for (var scheduled : m_queuedToRun) {
1412 if (scheduled.command() == command) {
1413 return scheduled.id();
1414 }
1415 }
1416
1417 return 0;
1418 }
1419
1426 @NoDiscard
1427 public double lastRuntimeMs() {
1428 return m_lastRunTimeMs;
1429 }
1430
1431 // Event-base telemetry and helpers. The static factories are for convenience to automatically
1432 // set the timestamp instead of littering RobotController.getTime() everywhere.
1433
1434 private void emitScheduledEvent(Command command) {
1435 var event = new SchedulerEvent.Scheduled(command, RobotController.getTime());
1436 emitEvent(event);
1437 }
1438
1439 private void emitMountedEvent(Command command) {
1440 var event = new SchedulerEvent.Mounted(command, RobotController.getTime());
1441 emitEvent(event);
1442 }
1443
1444 private void emitYieldedEvent(Command command) {
1445 var event = new SchedulerEvent.Yielded(command, RobotController.getTime());
1446 emitEvent(event);
1447 }
1448
1449 private void emitCompletedEvent(Command command) {
1450 var event = new SchedulerEvent.Completed(command, RobotController.getTime());
1451 emitEvent(event);
1452 }
1453
1454 private void emitCompletedWithErrorEvent(Command command, Throwable error) {
1455 var event = new SchedulerEvent.CompletedWithError(command, error, RobotController.getTime());
1456 emitEvent(event);
1457 }
1458
1459 private void emitCanceledEvent(Command command) {
1460 var event = new SchedulerEvent.Canceled(command, RobotController.getTime());
1461 emitEvent(event);
1462 }
1463
1464 private void emitInterruptedEvent(Command command, Command interrupter) {
1465 var event = new SchedulerEvent.Interrupted(command, interrupter, RobotController.getTime());
1466 emitEvent(event);
1467 }
1468
1469 private void emitForkFailureEvent(Command command, List<ScheduleResult.Failure> failures) {
1470 var event = new SchedulerEvent.ForkFailure(command, failures, RobotController.getTime());
1471 emitEvent(event);
1472 }
1473
1490 public void addEventListener(Consumer<? super SchedulerEvent> listener) {
1491 ErrorMessages.requireNonNullParam(listener, "listener", "addEventListener");
1492
1493 m_eventListeners.add(listener);
1494 }
1495
1496 private void emitEvent(SchedulerEvent event) {
1497 // TODO: Prevent listeners from interacting with the scheduler.
1498 // Scheduling or canceling commands while the scheduler is processing will probably cause
1499 // bugs in user code or even a program crash.
1500 for (var listener : m_eventListeners) {
1501 listener.accept(event);
1502 }
1503 }
1504}
static Scheduler getDefault()
double lastCommandRuntimeMs(Command command)
ScheduleResult isSchedulable(Command command)
Collection< Command > getQueuedCommands()
void cancel(Command command)
Command getParentOf(Command command)
Command getDefaultCommandFor(Mechanism mechanism)
List< Command > getRunningCommandsFor(Mechanism mechanism)
boolean isScheduled(Command command)
void setDefaultCommand(Mechanism mechanism, Command defaultCommand)
boolean isScheduledOrRunning(Command command)
int runId(Command command)
double totalRuntimeMs(Command command)
void sideload(Consumer< Coroutine > callback)
Collection< Command > getRunningCommands()
void addPeriodic(Runnable callback)
static final SchedulerProto proto
boolean isRunning(Command command)
void addEventListener(Consumer<? super SchedulerEvent > listener)
static Scheduler createIndependentScheduler()
ScheduleResult schedule(Command command)
static< T > T requireNonNullParam(T obj, String paramName, String methodName)
static BindingScope createNarrowestScope(Scheduler scheduler)
Set< Mechanism > requirements()
default boolean controllableDuringDisabled()
record AlreadyRunning(Command command, int runId) implements Successful
record RequiresUnsafeMechanisms(Command command, Collection< Mechanism > unsafeMechanisms) implements Failure
record LowerPriorityThanRunningCommand(Command command, Command alreadyRunning) implements Failure
record LowerPriorityThanQueuedCommand(Command command, Command queuedCommand) implements Failure
record Success(Command command, int runId) implements Successful
record Scheduled(Command command, long timestampNanos) implements SchedulerEvent
record CompletedWithError(Command command, Throwable error, long timestampNanos) implements SchedulerEvent
record Canceled(Command command, long timestampNanos) implements SchedulerEvent
record Interrupted(Command command, Command interrupter, long timestampNanos) implements SchedulerEvent
record ForkFailure(Command command, List< ScheduleResult.Failure > failures, long timestampNanos) implements SchedulerEvent