112 private final Map<Mechanism, List<Binding>> m_defaultCommandBindings =
new LinkedHashMap<>();
118 private final Collection<Binding> m_activeBindings =
new ArrayList<>();
121 private final Set<Trigger> m_boundTriggers =
new HashSet<>();
124 private final SequencedSet<CommandState> m_queuedToRun =
new LinkedHashSet<>();
130 private final SequencedMap<Command, CommandState> m_runningCommands =
new LinkedHashMap<>();
137 private final Stack<CommandState> m_currentCommandAncestry =
new Stack<>();
140 private final List<PeriodicCallback> m_periodicCallbacks =
new ArrayList<>();
146 private final ContinuationScope m_scope =
new ContinuationScope(
"coroutine commands");
155 private double m_lastRunTimeMs = -1;
157 private final Set<Consumer<? super SchedulerEvent>> m_eventListeners =
new LinkedHashSet<>();
160 private static final Scheduler s_defaultScheduler =
new Scheduler();
172 return s_defaultScheduler;
186 return new Scheduler();
210 @SuppressWarnings(
"PMD.CompareObjectsWithEquals")
212 if (!defaultCommand.requires(mechanism)) {
213 throw new IllegalArgumentException(
214 "A mechanism's default command must require that mechanism");
217 if (defaultCommand.requirements().size() > 1) {
218 throw new IllegalArgumentException(
219 "A mechanism's default command cannot require other mechanisms");
230 m_defaultCommandBindings.computeIfAbsent(mechanism, k ->
new ArrayList<>()).add(binding);
242 processDefaultCommands(mechanism);
253 var bindings = m_defaultCommandBindings.getOrDefault(mechanism, Collections.emptyList());
254 if (bindings.isEmpty()) {
258 return bindings.getLast().command();
262 List<Binding> getDefaultCommandBindingsFor(
Mechanism mechanism) {
263 return m_defaultCommandBindings.getOrDefault(mechanism, Collections.emptyList());
285 public void sideload(Consumer<Coroutine> callback) {
286 var coroutine =
new Coroutine(
this, m_scope, callback);
290 coroutine.setCancelOnForkFailure(
false);
293 m_periodicCallbacks.add(
new PeriodicCallback(scope, coroutine));
470 int id =
runId(command);
472 return new AlreadyRunning(command,
id);
475 if (!RobotStateFetcher.getFetcher().isEnabled()) {
476 var uncontrollables =
479 .collect(Collectors.toSet());
481 if (!uncontrollables.isEmpty()) {
482 return new RequiresUnsafeMechanisms(command, uncontrollables);
486 Set<Command> ancestry =
new HashSet<>();
487 for (var state = currentState(); state !=
null; state = m_runningCommands.get(state.parent())) {
488 ancestry.add(state.command());
491 return isSchedulableFromPriority(command, ancestry);
497 var command = binding.
command();
499 int id =
runId(command);
501 return new AlreadyRunning(command,
id);
504 if (!RobotStateFetcher.getFetcher().isEnabled()) {
505 var uncontrollables =
506 command.requirements().stream()
508 .collect(Collectors.toSet());
510 if (!uncontrollables.isEmpty()) {
511 return new RequiresUnsafeMechanisms(command, uncontrollables);
515 Set<Command> ancestry =
new HashSet<>();
516 if (binding.scope() instanceof BindingScope.ForCommand(Scheduler _, Command parent)) {
517 for (var state = m_runningCommands.get(parent);
519 state = m_runningCommands.get(state.parent())) {
520 ancestry.add(state.command());
523 for (var state = currentState();
525 state = m_runningCommands.get(state.parent())) {
526 ancestry.add(state.command());
530 return isSchedulableFromPriority(command, ancestry);
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);
541 conflict = lowerPriorityThanConflictingCommands(command, ancestry, m_queuedToRun);
542 if (conflict !=
null) {
543 return new LowerPriorityThanQueuedCommand(command, conflict);
546 return new Success(command, 0);
590 var command = binding.
command();
593 if (!(result instanceof Success)) {
601 if (!(binding.scope() instanceof BindingScope.ForCommand)) {
611 m_activeBindings.add(binding);
617 evictConflictingOnDeckCommands(command);
623 binding.scope() instanceof BindingScope.ForCommand scope
626 var state =
new CommandState(command, parentCommand, buildCoroutine(command), binding);
628 emitScheduledEvent(command);
630 if (currentState() !=
null) {
634 evictConflictingRunningCommands(state);
635 m_runningCommands.put(command, state);
641 m_queuedToRun.add(state);
644 return new Success(command, state.id());
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());
661 for (var state : checkAgainst) {
662 if (ancestry.contains(state.command())) {
667 var c = state.command();
668 if (c.conflictsWith(command) && maxAncestorPriority < effectivePriority(state)) {
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());
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)) {
701 emitInterruptedEvent(scheduledCommand, command);
702 emitCanceledEvent(scheduledCommand);
711 @SuppressWarnings(
"PMD.CompareObjectsWithEquals")
712 private
void evictConflictingRunningCommands(CommandState incomingState) {
714 Set<CommandState> conflictingRootStates =
715 m_runningCommands.values().stream()
716 .filter(state -> incomingState.command().conflictsWith(state.command()))
717 .filter(state -> !isAncestorOf(state.command(), incomingState))
724 CommandState root = state;
725 while (root.parent() != null && root.parent() != incomingState.parent()) {
726 root = m_runningCommands.get(root.parent());
730 .collect(Collectors.toSet());
733 for (var conflictingState : conflictingRootStates) {
734 emitInterruptedEvent(conflictingState.command(), incomingState.command());
735 cancel(conflictingState.command());
746 @SuppressWarnings({
"PMD.CompareObjectsWithEquals",
"PMD.SimplifyBooleanReturns"})
747 private boolean isAncestorOf(
Command ancestor, CommandState state) {
748 if (state.parent() ==
null) {
752 if (!m_runningCommands.containsKey(ancestor)) {
756 if (state.parent() == ancestor) {
761 return m_runningCommands.values().stream()
762 .filter(s -> state.parent() == s.command())
763 .anyMatch(s -> isAncestorOf(ancestor, s));
775 @SuppressWarnings(
"PMD.CompareObjectsWithEquals")
777 var currentState = currentState();
778 if (currentState !=
null && command == currentState.command()) {
779 currentState.coroutine().requestCancellation();
788 m_runningCommands.remove(command);
789 boolean queued = m_queuedToRun.removeIf(state -> state.command() == command);
800 if (running || queued) {
802 emitCanceledEvent(command);
806 removeOrphanedChildren(command);
833 cancelStaleBindings();
838 unbindStaleTriggers();
841 cancelCommandsThatCannotRunInDisabled();
844 runPeriodicSideloads();
851 scheduleDefaultCommands();
854 promoteScheduledCommands();
860 m_lastRunTimeMs = Milliseconds.convertFrom(endNanos - startNanos, Nanoseconds);
863 private void cancelStaleBindings() {
864 for (var iterator = m_activeBindings.iterator(); iterator.hasNext(); ) {
865 var binding = iterator.next();
866 if (binding.scope().active()) {
869 cancel(binding.command());
874 private void unbindStaleTriggers() {
879 for (var iterator = m_boundTriggers.iterator(); iterator.hasNext(); ) {
880 var trigger = iterator.next();
881 if (!trigger.isScopeActive()) {
887 private void cancelCommandsThatCannotRunInDisabled() {
888 if (RobotStateFetcher.getFetcher().isEnabled()) {
893 List<Command> commandsToCancel =
new ArrayList<>();
895 for (var runningState : m_runningCommands.values()) {
896 var command = runningState.command();
897 boolean canRun =
true;
898 for (var mechanism : command.requirements()) {
909 commandsToCancel.add(getRoot(command));
912 for (var queuedState : m_queuedToRun) {
913 var command = queuedState.command();
915 boolean canRun =
true;
916 for (var mechanism : command.requirements()) {
927 commandsToCancel.add(command);
930 for (var command : commandsToCancel) {
940 void addBoundTrigger(
Trigger trigger) {
941 m_boundTriggers.add(trigger);
949 void removeBoundTrigger(
Trigger trigger) {
950 m_boundTriggers.remove(trigger);
953 private void promoteScheduledCommands() {
955 for (var queuedState : m_queuedToRun) {
956 evictConflictingRunningCommands(queuedState);
960 for (var queuedState : m_queuedToRun) {
961 m_runningCommands.put(queuedState.command(), queuedState);
966 m_queuedToRun.clear();
969 private void runPeriodicSideloads() {
970 for (var iterator = m_periodicCallbacks.iterator(); iterator.hasNext(); ) {
971 PeriodicCallback callback = iterator.next();
972 if (!callback.scope().active()) {
979 Coroutine coroutine = callback.coroutine();
980 if (coroutine.inUnmountableState()) {
987 coroutine.runToYieldPoint();
989 Continuation.mountContinuation(
null);
992 if (coroutine.isDone()
993 || coroutine.isInterruptRequested()
994 || coroutine.isCancellationRequested()) {
1001 private void runCommands() {
1008 for (var state : List.copyOf(m_runningCommands.values()).reversed()) {
1018 @SuppressWarnings(
"PMD.AvoidCatchingGenericException")
1019 private
void runCommand(CommandState state) {
1020 final var command = state.command();
1021 final var coroutine = state.coroutine();
1023 if (coroutine.inUnmountableState()) {
1028 if (!m_runningCommands.containsKey(command)) {
1033 var previousState = currentState();
1035 m_currentCommandAncestry.push(state);
1037 emitMountedEvent(command);
1040 coroutine.runToYieldPoint();
1041 }
catch (RuntimeException e) {
1043 handleCommandException(state, e);
1046 double elapsedMs = Milliseconds.convertFrom(endNanos - startNanos, Nanoseconds);
1047 state.setLastRuntimeMs(elapsedMs);
1049 if (state.equals(currentState())) {
1051 m_currentCommandAncestry.pop();
1054 if (previousState !=
null) {
1056 previousState.coroutine().mount();
1058 Continuation.mountContinuation(
null);
1062 if (coroutine.isCancellationRequested()) {
1064 }
else if (coroutine.isDone()) {
1065 handleCommandCompletion(command);
1066 }
else if (coroutine.getForkResult() !=
null) {
1067 handleCoroutineIRQ(coroutine.getForkResult(), command);
1070 emitYieldedEvent(command);
1074 private void handleCommandCompletion(
Command command) {
1076 emitCompletedEvent(command);
1077 m_runningCommands.remove(command);
1078 removeOrphanedChildren(command);
1082 Command root = getRoot(command);
1083 m_currentCommandAncestry.clear();
1084 emitForkFailureEvent(command, failure.getFailedCommands());
1086 Continuation.mountContinuation(
null);
1099 @SuppressWarnings(
"PMD.CompareObjectsWithEquals")
1100 private
void handleCommandException(CommandState state, RuntimeException e) {
1101 var command = state.command();
1105 final Command root = getRoot(command);
1108 m_runningCommands.remove(command);
1111 var binding = state.binding();
1113 CommandTraceHelper.modifyTrace(
1114 e.getStackTrace(), binding.stackTraceStore().getStackTrace()));
1115 emitCompletedWithErrorEvent(command, e);
1119 removeOrphanedChildren(command);
1124 if (root !=
null && root != command) {
1137 private CommandState currentState() {
1138 if (m_currentCommandAncestry.isEmpty()) {
1143 return m_currentCommandAncestry.peek();
1152 var state = currentState();
1153 if (state ==
null) {
1157 return state.command();
1160 private void scheduleDefaultCommands() {
1161 m_defaultCommandBindings.keySet().forEach(this::processDefaultCommands);
1164 private void processDefaultCommands(Mechanism mechanism) {
1165 var bindings = m_defaultCommandBindings.get(mechanism);
1171 if (!b.scope().active()) {
1172 cancel(b.command());
1178 if (bindings.isEmpty()) {
1184 for (
int i = 0; i < bindings.size() - 1; i++) {
1185 Command widerScopeDefaultCommand = bindings.get(i).command();
1186 cancel(widerScopeDefaultCommand);
1189 if (!RobotStateFetcher.getFetcher().isEnabled() && !mechanism.controllableDuringDisabled()) {
1197 for (
Command runningCommand : m_runningCommands.keySet()) {
1198 if (runningCommand.requires(mechanism)) {
1202 for (CommandState queuedState : m_queuedToRun) {
1203 if (queuedState.command().requires(mechanism)) {
1209 var defaultCommand = bindings.getLast();
1210 schedule(defaultCommand);
1219 @SuppressWarnings(
"PMD.CompareObjectsWithEquals")
1220 private
void removeOrphanedChildren(
Command parent) {
1221 m_runningCommands.entrySet().stream()
1222 .filter(e -> e.getValue().parent() == parent)
1224 .forEach(e -> cancel(e.getKey()));
1235 return new Coroutine(
this, m_scope, command::run);
1245 return m_runningCommands.containsKey(command);
1254 @SuppressWarnings(
"PMD.CompareObjectsWithEquals")
1256 return m_queuedToRun.stream().anyMatch(state -> state.command() == command);
1276 return List.copyOf(m_runningCommands.keySet());
1287 return m_runningCommands.keySet().stream()
1288 .filter(command -> command.requires(mechanism))
1298 for (var onDeckIter = m_queuedToRun.iterator(); onDeckIter.hasNext(); ) {
1299 var state = onDeckIter.next();
1300 onDeckIter.remove();
1301 emitCanceledEvent(state.command());
1304 for (var liveIter = m_runningCommands.entrySet().iterator(); liveIter.hasNext(); ) {
1305 var entry = liveIter.next();
1307 Command canceledCommand = entry.getKey();
1310 canceledCommand.
onExit();
1312 emitCanceledEvent(canceledCommand);
1336 return m_queuedToRun.stream().map(CommandState::command).toList();
1341 Command parent = getParentOf(command);
1342 while (parent !=
null) {
1344 parent = getParentOf(parent);
1358 var state = m_runningCommands.get(command);
1359 if (state ==
null) {
1362 return state.parent();
1373 if (m_runningCommands.containsKey(command)) {
1374 return m_runningCommands.get(command).lastRuntimeMs();
1388 if (m_runningCommands.containsKey(command)) {
1389 return m_runningCommands.get(command).totalRuntimeMs();
1404 @SuppressWarnings(
"PMD.CompareObjectsWithEquals")
1406 if (m_runningCommands.containsKey(command)) {
1407 return m_runningCommands.get(command).id();
1411 for (var scheduled : m_queuedToRun) {
1412 if (scheduled.command() == command) {
1413 return scheduled.id();
1428 return m_lastRunTimeMs;
1434 private void emitScheduledEvent(
Command command) {
1439 private void emitMountedEvent(Command command) {
1440 var
event =
new SchedulerEvent.Mounted(command, RobotController.getTime());
1444 private void emitYieldedEvent(Command command) {
1445 var
event =
new SchedulerEvent.Yielded(command, RobotController.getTime());
1449 private void emitCompletedEvent(Command command) {
1450 var
event =
new SchedulerEvent.Completed(command, RobotController.getTime());
1454 private void emitCompletedWithErrorEvent(
Command command, Throwable error) {
1459 private void emitCanceledEvent(
Command command) {
1464 private void emitInterruptedEvent(
Command command,
Command interrupter) {
1469 private void emitForkFailureEvent(
Command command, List<ScheduleResult.Failure> failures) {
1493 m_eventListeners.add(listener);
1500 for (var listener : m_eventListeners) {
1501 listener.accept(event);