⚙️ Software · Architecture

Finite State Machine

A state machine is the upgrade from nested if-else spaghetti to clean, readable robot logic. Every mechanism has a set of states it can be in. Define the states, define what triggers transitions between them, and the code almost writes itself.

1
The Concept
2
Basic Example
3
Subsystem FSM
4
Auton FSM
// Section 01
What a State Machine Is
A finite state machine (FSM) is a model of a system that can be in exactly one state at a time. Events trigger transitions between states. Each state defines what the system does while it is in that state.
💡
You already think in state machines. “The intake is either IDLE, INTAKING, or EJECTING.” That is a 3-state FSM described in plain English. Writing it in code is just making that mental model explicit — giving each state a name, defining what the robot does in each state, and specifying what causes transitions.

States vs. Nested If-Else

Without a state machine, mechanism logic grows into nested conditionals that are hard to read and impossible to debug:

// Nested if-else — hard to read, easy to break if (master.get_digital(DIGITAL_R1)) { if (!jamDetected) { if (hasElement) { if (armIsUp) { intake.move(-127); } else { intake.move(127); } } else { intake.move(127); } } else { intake.move(-80); } // unjam } else { intake.move(0); }

With a state machine, the same logic becomes:

// State machine — each state is explicit and readable switch (intakeState) { case IDLE: intake.move(0); break; case INTAKING: intake.move(127); break; case EJECTING: intake.move(-127); break; case UNJAMMING: intake.move(-80); break; }

The transitions (what causes state changes) are handled separately. Each piece of logic has exactly one home.

// Section 02
A Basic Toggle FSM
The simplest real-world FSM: a claw that toggles between OPEN and CLOSED on button press. Two states, one transition event.
OPEN
Button press
(rising edge)
CLOSED
src/claw.cpp — two-state FSM
#include "main.h" #include "claw.hpp" // Define the states using an enum — readable names, not magic numbers enum class ClawState { OPEN, CLOSED }; static ClawState clawState = ClawState::OPEN; // Called once per opcontrol() loop void clawUpdate() { // ── TRANSITION LOGIC ──────────────────────────────────────────────── if (master.get_digital_new_press(DIGITAL_A)) { // Toggle between states on rising edge only clawState = (clawState == ClawState::OPEN) ? ClawState::CLOSED : ClawState::OPEN; } // ── STATE ACTIONS ─────────────────────────────────────────────────── switch (clawState) { case ClawState::OPEN: claw.move_absolute(0, 80); // open position break; case ClawState::CLOSED: claw.move_absolute(500, 80); // closed position break; } }
Separate transitions from actions. The top half of clawUpdate() handles when to change state. The bottom half handles what to do in each state. Keep these two sections structurally separate in your code — it makes both easier to modify independently.
// Section 03
A Full Intake FSM with Four States
A real intake needs more than two states — it needs to handle idle, intaking, ejecting, and jammed conditions cleanly.
IDLE
INTAKING
LOADED
↳ stall detected → UNJAM → INTAKING
src/intake.cpp — 4-state FSM
enum class IntakeState { IDLE, INTAKING, EJECTING, UNJAMMING }; static IntakeState intakeState = IntakeState::IDLE; static uint32_t unjamEnd = 0; void intakeUpdate() { // ── TRANSITIONS ───────────────────────────────────────────────────── // Unjam timer expires → back to INTAKING if (intakeState == IntakeState::UNJAMMING && pros::millis() > unjamEnd) { intakeState = IntakeState::INTAKING; } // Stall detected while intaking → start unjam if (intakeState == IntakeState::INTAKING && intakeCheckStall()) { intakeState = IntakeState::UNJAMMING; unjamEnd = pros::millis() + 400; } // Driver input if (intakeState != IntakeState::UNJAMMING) { if (master.get_digital(DIGITAL_R1)) intakeState = IntakeState::INTAKING; else if (master.get_digital(DIGITAL_R2)) intakeState = IntakeState::EJECTING; else intakeState = IntakeState::IDLE; } // ── ACTIONS ───────────────────────────────────────────────────────── switch (intakeState) { case IntakeState::IDLE: intake.move(0); break; case IntakeState::INTAKING: intake.move(127); break; case IntakeState::EJECTING: intake.move(-127); break; case IntakeState::UNJAMMING: intake.move(-80); break; } }
ℹ️
Adding a new behavior is easy. Want a LOADED state that stops the intake when a sensor detects a game element? Add LOADED to the enum, add one transition (stall or optical → LOADED), and add one action (case LOADED: intake.move(0);). No other code changes needed. That is the power of FSM structure.
// Section 04
Using FSM in Autonomous
State machines shine in autonomous — they make complex multi-step sequences readable, and each step can respond to sensor feedback rather than blindly running on a timer.

Autonomous as a State Sequence

Instead of a linear list of drive commands, think of your autonomous as a sequence of states: DRIVE_TO_GOAL, SCORE, RETURN, AWP_TOUCH. Each state runs until a condition exits it.

src/autons.cpp — state-based autonomous
enum class AutonState { DRIVE_TO_GOAL, SCORE, RETURN_HOME, AWP_TOUCH, DONE }; void autonScoreAWP() { AutonState state = AutonState::DRIVE_TO_GOAL; bool initialized = false; while (state != AutonState::DONE) { switch (state) { case AutonState::DRIVE_TO_GOAL: if (!initialized) { chassis.pid_drive_set(30, 110); initialized = true; } if (!chassis.drive_pid_task_running()) { state = AutonState::SCORE; initialized = false; } break; case AutonState::SCORE: if (!initialized) { intake.move(-127); // eject into goal initialized = true; } // Sensor confirms score OR timer expires if (!intakeHasElement()) { intake.move(0); state = AutonState::RETURN_HOME; initialized = false; } break; case AutonState::RETURN_HOME: if (!initialized) { chassis.pid_drive_set(-20, 100); initialized = true; } if (!chassis.drive_pid_task_running()) { state = AutonState::AWP_TOUCH; initialized = false; } break; case AutonState::AWP_TOUCH: // AWP confirmed — done state = AutonState::DONE; break; default: state = AutonState::DONE; break; } pros::delay(10); } }
⚠️
For most VRC autonomous routines, EZ Template’s linear API (pid_drive_set + pid_wait) is simpler and adequate. FSM-based autonomous is most valuable when states need to respond to sensor feedback (did the game element actually score? is the robot stuck?) rather than pure dead-reckoning. Use FSM when you need the extra intelligence, not by default.

When to Use FSM

⚙ STEM Highlight Computer Science: Automata Theory & Formal State Models
Finite State Machines (FSMs) are a fundamental concept in theoretical computer science — originating with Alan Turing and formally described by Mealy and Moore in the 1950s. Every digital system that has modes is an FSM: traffic lights, vending machines, video games. In VRC, your robot’s intake (IDLE → COLLECTING → FULL → EJECTING) is a Mealy machine: transitions are triggered by inputs (sensor readings, button presses) and outputs (motor commands) are associated with transitions. The formal definition: FSM = (States, Inputs, Outputs, Transition function, Initial state).
🎤 Interview line: “Our robot’s intake logic is implemented as a Finite State Machine — a formal model from automata theory. We defined five states, the transitions between them, and the outputs in each state. This eliminates nested if-else logic and makes the behavior verifiable — we can prove the intake never enters an invalid state.”
🔬 Check for Understanding
Your FSM has an INTAKE_ON state. The intake button is pressed while already in INTAKE_ON. What should happen?
The FSM should crash because you cannot enter a state you are already in
The FSM should transition to INTAKE_OFF because pressing a toggle button while on should turn it off
It depends on how the transition function is defined — the FSM designer specifies the behavior for every state+input combination
FSMs cannot handle repeated inputs
Related Guides
⚡ Concurrent Actions →🚫 Stall Detection →🤖 Full Robot Code →
← ALL GUIDES