RobotCore 1.0
WPILib port to raspberry Pi
Loading...
Searching...
No Matches
StateMachine.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.util.ErrorMessages.requireNonNullParam;
8
9import java.util.ArrayList;
10import java.util.List;
11import java.util.Set;
12import java.util.function.BooleanSupplier;
13import java.util.function.Supplier;
14import org.wpilib.annotation.NoDiscard;
15import org.wpilib.annotation.PostConstructionInitializer;
16
58public final class StateMachine implements Command {
59 private final String m_name;
60 private int m_priority = DEFAULT_PRIORITY;
61 private State m_initialState = null;
62 private final List<State> m_states = new ArrayList<>();
63
64 // Marks the state machine as no longer accepting new states. Used when a no-arg switchFromAny()
65 // is called to prevent surprising behavior if users try to define new states afterward
66 private boolean m_closedForNewStates = false;
67
74 public StateMachine(String name) {
75 requireNonNullParam(name, "name", "StateMachine");
76 m_name = name;
77 }
78
91 public void setPriority(int priority) {
92 m_priority = priority;
93 }
94
104 @Override
105 public int priority() {
106 return m_priority;
107 }
108
109 @Override
110 public String name() {
111 return m_name;
112 }
113
120 @Override
121 public Set<Mechanism> requirements() {
122 // The machine itself doesn't have any requirements. Commands bound to the various states that
123 // the machine moves through may have requirements, however.
124 return Set.of();
125 }
126
136 @NoDiscard
137 public State addState(Command command) {
138 requireNonNullParam(command, "command", "StateMachine.addState");
139 if (m_closedForNewStates) {
140 throw new IllegalStateException(
141 "Cannot add new states to a state machine after switchFromAny() has been called with no"
142 + " arguments");
143 }
144 var state = new State(this, command);
145 m_states.add(state);
146 return state;
147 }
148
176 public TransitionNeedsTargetStage switchFromAny(State... states) {
177 if (states.length == 0) {
178 m_closedForNewStates = true;
179 return new TransitionNeedsTargetStage(List.copyOf(m_states));
180 } else {
181 return new TransitionNeedsTargetStage(List.of(states));
182 }
183 }
184
195 public void setInitialState(State initialState) {
196 requireNonNullParam(initialState, "initialState", "StateMachine.setInitialState");
197 if (!this.equals(initialState.m_stateMachine)) {
198 throw new IllegalArgumentException("Cannot set initial state in a different state machine");
199 }
200 m_initialState = initialState;
201 }
202
203 @Override
204 public void run(Coroutine coroutine) {
205 if (m_initialState == null) {
206 throw new IllegalStateException(
207 m_name + " does not have an initial state. Use .setInitialState() to provide one.");
208 }
209
210 var currentState = m_initialState;
211
212 outer_loop:
213 while (currentState != null) {
214 final var currentCommand = currentState.command();
215 // NOTE: The state machine will exit if the child command fails to be forked. This happens
216 // if the command shares requirements with a higher-priority command.
217 coroutine.fork(currentCommand);
218 currentState.runEnterCallbacks();
219 boolean didYield = false;
220
221 while (coroutine.scheduler().isRunning(currentCommand)) {
222 for (var transition : currentState.transitions()) {
223 if (transition.shouldTransition()) {
224 // Cancel the current state's command and move to the next state specified by the
225 // transition. Break the state loop early to avoid an unnecessary yield() call and
226 // allow the next state's command to start in the same loop iteration that the
227 // previous state completed. If the next state is null, the state machine will exit
228 // immediately.
229 // Note: to prevent infinite loops when states transition to themselves, we require
230 // the transition signal to be a rising edge on the user-supplied condition to ensure
231 // that the transition is only triggered once per loop iteration.
232 currentState.runExitCallbacks();
233 coroutine.scheduler().cancel(currentCommand);
234 currentState = verifyState(transition.nextState());
235 continue outer_loop;
236 }
237 }
238
239 // Yield after checking all transitions.
240 // Note: this will be skipped if a transition is triggered.
241 coroutine.yield();
242 didYield = true;
243 }
244
245 // Move to the next configured state if no transition was hit before the command completed.
246 // We need to be careful about states with oneshot commands; they will complete immediately
247 // in the `fork()` call above and never enter the `while` loop and thus never yield.
248 // Therefore, we inject a yield call at the end here to ensure that the state machine will
249 // always yield once per state. This has a downside of adding extra loop cycles to states that
250 // may not need them (and has slightly different behavior to SequentialCommandGroup, which
251 // runs commands as fast as possible).
252 currentState.runExitCallbacks();
253 currentState = verifyState(currentState.nextState());
254 if (!didYield && currentState != null) {
255 // No need to yield if we're exiting the state machine
256 coroutine.yield();
257 }
258 }
259 }
260
261 private State verifyState(State next) {
262 if (next == null || this.equals(next.m_stateMachine)) {
263 // OK
264 return next;
265 }
266
267 // Bad user setup
268 throw new IllegalStateException(
269 "The next state does not belong to this state machine. Check the state for "
270 + next.command().name());
271 }
272
281 public static final class State {
283 private final StateMachine m_stateMachine;
284
286 private final Command m_command;
287
289 private final List<Completion> m_completions = new ArrayList<>();
290
292 private Supplier<State> m_defaultNextState = () -> null;
293
298 private final List<Transition> m_transitions = new ArrayList<>();
299
300 private final List<Runnable> m_enterCallbacks = new ArrayList<>();
301 private final List<Runnable> m_exitCallbacks = new ArrayList<>();
302
303 private State(StateMachine stateMachine, Command command) {
304 m_stateMachine = stateMachine;
305 m_command = command;
306 }
307
308 private Command command() {
309 return m_command;
310 }
311
312 private List<Transition> transitions() {
313 return m_transitions;
314 }
315
316 private void addTransition(Transition transition) {
317 m_transitions.add(transition);
318 }
319
327 private void setNextState(Supplier<State> nextState) {
328 m_defaultNextState = nextState;
329 }
330
331 // Custom boolean supplier classes may override .equals to do boolean value comparisons,
332 // particularly in Kotlin code. Check reference equality instead to just remove bindings to
333 // the same condition object.
334 @SuppressWarnings("PMD.CompareObjectsWithEquals")
335 private void addCompletion(BooleanSupplier condition, Supplier<State> next) {
336 // Remove any preexisting completion with the same condition
337 m_completions.removeIf(c -> c.getCondition() == condition);
338 m_completions.add(new Completion(next, condition));
339 }
340
341 private State nextState() {
342 for (var completion : m_completions) {
343 if (completion.shouldTransition()) {
344 return completion.nextState();
345 }
346 }
347
348 // No conditional transition has been met, use the default next state.
349 // If this was never set or was set to be null, the state machine will exit.
350 return m_defaultNextState.get();
351 }
352
353 private void runEnterCallbacks() {
354 m_enterCallbacks.forEach(Runnable::run);
355 }
356
357 private void runExitCallbacks() {
358 m_exitCallbacks.forEach(Runnable::run);
359 }
360
370 public void onEnter(Runnable callback) {
371 requireNonNullParam(callback, "callback", "State.onEnter");
372 m_enterCallbacks.add(callback);
373 }
374
383 public void onExit(Runnable callback) {
384 requireNonNullParam(callback, "callback", "State.onExit");
385 m_exitCallbacks.add(callback);
386 }
387
394 public TransitionNeedsConditionStage switchTo(State to) {
395 requireNonNullParam(to, "to", "State.switchTo");
396 if (!m_stateMachine.equals(to.m_stateMachine)) {
397 throw new IllegalArgumentException(
398 "Cannot transition to a state in a different state machine");
399 }
400 return new TransitionNeedsTargetStage(List.of(this)).to(to);
401 }
402
410 public TransitionNeedsConditionStage switchTo(Supplier<State> dynamic) {
411 requireNonNullParam(dynamic, "dynamic", "State.switchTo");
412 // Unfortunately, we can't check up front that the supplier will always return a state for
413 // this state machine. The output will need to be checked when the supplier is called
414 return new TransitionNeedsTargetStage(List.of(this)).to(dynamic);
415 }
416
423 public TransitionNeedsConditionStage exitStateMachine() {
424 return new TransitionNeedsConditionStage(List.of(this), () -> null);
425 }
426 }
427
432 @NoDiscard("Use .to() to specify the target state")
433 public static final class TransitionNeedsTargetStage {
434 private final List<State> m_from;
435
436 private TransitionNeedsTargetStage(List<State> from) {
437 m_from = from;
438 }
439
446 public TransitionNeedsConditionStage to(State to) {
447 requireNonNullParam(to, "to", "NeedsTargetTransitionBuilder.to");
448 for (var state : m_from) {
449 if (!state.m_stateMachine.equals(to.m_stateMachine)) {
450 throw new IllegalArgumentException(
451 "Cannot transition to a state in a different state machine");
452 }
453 }
454 return new TransitionNeedsConditionStage(m_from, () -> to);
455 }
456
464 public TransitionNeedsConditionStage to(Supplier<State> dynamic) {
465 requireNonNullParam(dynamic, "dynamic", "NeedsTargetTransitionBuilder.to");
466 return new TransitionNeedsConditionStage(m_from, dynamic);
467 }
468
475 public TransitionNeedsConditionStage toExitStateMachine() {
476 return new TransitionNeedsConditionStage(m_from, () -> null);
477 }
478 }
479
486 @NoDiscard("Use .when() or .whenComplete() to specify the transition condition")
487 public static final class TransitionNeedsConditionStage {
488 private final List<State> m_originatingStates;
489
490 // Note: A null result from the supplier indicates that the transition will cause the state
491 // machine to exit
492 private final Supplier<State> m_targetStateSupplier;
493
494 private TransitionNeedsConditionStage(List<State> from, Supplier<State> to) {
495 requireNonNullParam(from, "from", "TransitionNeedsConditionStage");
496 requireNonNullParam(to, "to", "TransitionNeedsConditionStage");
497
498 m_originatingStates = from;
499 m_targetStateSupplier = to;
500 }
501
525 public void when(BooleanSupplier condition) {
526 requireNonNullParam(condition, "condition", "NeedsConditionTransitionBuilder.when");
527 var transition = new Transition(m_targetStateSupplier, condition);
528 m_originatingStates.forEach(originatingState -> originatingState.addTransition(transition));
529 }
530
549 public void whenComplete() {
550 m_originatingStates.forEach(state -> state.setNextState(m_targetStateSupplier));
551 }
552
572 public void whenCompleteAnd(BooleanSupplier condition) {
573 requireNonNullParam(condition, "condition", "NeedsConditionTransitionBuilder.whenComplete");
574 m_originatingStates.forEach(state -> state.addCompletion(condition, m_targetStateSupplier));
575 }
576 }
577
582 private static final class Completion {
583 private final Supplier<State> m_nextSupplier;
584 private final BooleanSupplier m_condition;
585
592 private Completion(Supplier<State> next, BooleanSupplier condition) {
593 m_nextSupplier = next;
594 m_condition = condition;
595 }
596
597 private boolean shouldTransition() {
598 return m_condition.getAsBoolean();
599 }
600
601 public State nextState() {
602 return m_nextSupplier.get();
603 }
604
605 public BooleanSupplier getCondition() {
606 return m_condition;
607 }
608 }
609
614 private static final class Transition {
616 private final Supplier<State> m_nextSupplier;
617
619 private final BooleanSupplier m_condition;
620
621 private boolean m_previousSignal = false;
622
623 private Transition(Supplier<State> next, BooleanSupplier condition) {
624 m_nextSupplier = next;
625 m_condition = condition;
626 }
627
629 private boolean shouldTransition() {
630 // Wrap the condition in a rising edge detector so that it will only trigger a single time per
631 // loop iteration. This prevents issues with a state transitioning to itself like so:
632 // state1.switchTo(state1).when(() -> foo == true);
633 // If the condition is itself a rising edge detector, this wrapping is redundant but harmless.
634 boolean currentValue = m_condition.getAsBoolean();
635 boolean isRisingEdge = currentValue && !m_previousSignal;
636 m_previousSignal = currentValue;
637 return isRisingEdge;
638 }
639
640 private State nextState() {
641 return m_nextSupplier.get();
642 }
643 }
644}
ForkResult fork(Command... commands)
void cancel(Command command)
boolean isRunning(Command command)
State addState(Command command)
void run(Coroutine coroutine)
void setInitialState(State initialState)
TransitionNeedsTargetStage switchFromAny(State... states)