State Machines with Commands

The StateMachine class provides a way to define complex behavior as a series of states and transitions. Each state in a state machine runs a single Command, and transitions define when the state machine should move from one state to another.

State machines are commands themselves, so they have a name and can be scheduled just like any other command. They can even be used as states in other state machines. However, state machines do not have any requirements of their own: the active state command owns the mechanisms it requires. This means a state machine can move between states that use different mechanisms without owning all of them for the entire lifetime of the machine, but also means that a state machine cannot be used as a default command.

Defining a State Machine

To define a state machine, create an instance of StateMachine, add all of its states, choose an initial state, and then add transitions. Defining states first makes global transitions easier to reason about because switchFromAny() without arguments only applies to states that already exist.

import org.wpilib.command3.StateMachine;

StateMachine sm = new StateMachine("Example State Machine");

// 1. Define all states
var idleState = sm.addState(arm.idle());
var upState = sm.addState(arm.up());
var downState = sm.addState(arm.down());

// 2. Define transitions
idleState.switchTo(upState).when(driverController.y());
idleState.switchTo(downState).when(driverController.a());

upState.switchTo(idleState).whenComplete();
downState.switchTo(idleState).whenComplete();

// 3. Set the initial state
sm.setInitialState(idleState);

Note

Calling setInitialState() is required - otherwise the state machine wouldn’t know where to start. If you forget to set an initial state, the WPILib compiler plugin will detect it and issue an error.

State Machine States

Each state is a wrapper around a Command. When the state machine enters a state, it schedules the associated command. When the state machine transitions away from a state, the command is canceled.

If an external command is scheduled that conflicts with any mechanisms owned by the currently running state command, the state machine command will be interrupted. The parent-child relationships section in Creating Commands goes into more detail on how interruptions work.

If a state’s command finishes and no completion transition is configured, the state machine exits. Use whenComplete() when a finished state should automatically move to another state. Use whenCompleteAnd(condition) when a finished state should choose a next state only if some condition is also true.

You can also add enter and exit callbacks to states:

upState.onEnter(() -> System.out.println("Entering UP state"));
upState.onExit(() -> System.out.println("Exiting UP state"));

Enter callbacks run immediately after the state’s command is scheduled. Exit callbacks run immediately before the state’s command is canceled during a transition, or immediately after it completes naturally. If an enter callback schedules commands, those commands are scoped to the lifetime of the state machine, not to the lifetime of just that state.

Transitions

Transitions define the movement between states. They are checked every loop cycle while the current state’s command is running.

Conditional transitions are treated as rising-edge conditions to prevent a state from repeatedly transitioning to itself in the same scheduler cycle. If multiple transitions from the same state become true in the same loop, the first transition that was declared wins and the rest are ignored. Transitions created with when(condition) do not fire for one-shot states that complete without yielding; use whenComplete() or whenCompleteAnd(...) for those states.

You can also define transitions for multiple states at once, which can help make your code more readable.

sm.switchFromAny(upState, downState).to(idleState).when(driverController.b());

Global Transitions with switchFromAny()

If you call switchFromAny() without any arguments, it creates a transition that applies to all states in the state machine. This is useful for “global” transitions, such as returning to an initial or home state from anywhere in the state graph.

// Any state will transition to idle if the X button is pressed
sm.switchFromAny().to(idleState).when(driverController.x());

// Any state will exit the state machine if a safety sensor is tripped
sm.switchFromAny().toExitStateMachine().when(safetySensor::get);

Warning

switchFromAny() with no arguments only applies to the states that have already been defined on the state machine at the time the method is called. Any states added with addState() after the call to switchFromAny() will not have this transition applied to them.

For this reason, it is recommended to add all of your states first, and then define transitions after all states have been added.