Overview

Imagine a traffic light. It is always in one of three states: RED, YELLOW, or GREEN. It cannot be in two states at once. Each state has a set of rules for when to switch to the next state.

A state machine works exactly the same way for your robot. Your lift, intake, or any mechanism is always in one defined state, and button presses (or sensor readings) trigger transitions between states.

Why does this matter?

Without a state machine, mechanism control code quickly turns into a mess of if/else statements that get tangled up and cause unpredictable behavior. State machines make complex behavior predictable, easy to debug, and easy to expand.

The Problem State Machines Solve

Suppose you want a lift with three positions: down, middle, and up. Your first instinct might be:

if controller.buttonLUp.pressing():
    lift_motor.spin(FORWARD, 80, PERCENT)
else:
    lift_motor.stop()

This works for a simple case — but what about:

  • Holding the lift at exactly the right height?
  • Making sure it does not over-extend?
  • Triggering the intake automatically when the lift reaches a position?
  • Preventing the driver from accidentally driving the lift into the ground?

All of these become very hard to manage without structure. A state machine gives you that structure.

Core Concept: States and Transitions

A state machine has two key parts:

  1. States — the possible conditions your mechanism can be in (e.g., RETRACTED, LOW, HIGH)
  2. Transitions — the rules for moving from one state to another (e.g., “when the driver presses L1, go from LOW to HIGH”)

At any moment, the mechanism is in exactly one state. The code for that state runs until a transition moves it to the next state.

A Simple Lift State Machine

Step 1 — Define your states

# VEX IQ Python — define states as constants
RETRACTED = 0
LOW = 1
HIGH = 2
MANUAL = 3
// VEX V5 C++ — use an enum for cleaner code
enum LiftState {
    RETRACTED,
    LOW,
    HIGH,
    MANUAL
};

Step 2 — Track the current state

# VEX IQ Python
current_state = RETRACTED
// VEX V5 C++
LiftState currentState = RETRACTED;

Step 3 — Write the control loop

Each pass through the loop does two things:

  1. Check for transitions (button presses)
  2. Act based on the current state (run motors)
# VEX IQ Python — lift state machine
current_state = RETRACTED
last_l1 = False
last_l2 = False

while True:
    l1 = controller.buttonLUp.pressing()
    l2 = controller.buttonLDown.pressing()

    # --- TRANSITIONS (detect button press rising edge) ---
    if l1 and not last_l1:
        # L1 cycles up through states
        if current_state == RETRACTED:
            current_state = LOW
        elif current_state == LOW:
            current_state = HIGH

    if l2 and not last_l2:
        # L2 cycles down through states
        if current_state == HIGH:
            current_state = LOW
        elif current_state == LOW:
            current_state = RETRACTED

    # --- ACTIONS (run motors based on current state) ---
    if current_state == RETRACTED:
        lift_motor.spin_to_position(0, DEGREES, 60, PERCENT)

    elif current_state == LOW:
        lift_motor.spin_to_position(90, DEGREES, 60, PERCENT)

    elif current_state == HIGH:
        lift_motor.spin_to_position(200, DEGREES, 60, PERCENT)

    # Save button history for rising edge detection
    last_l1 = l1
    last_l2 = l2
    wait(20, MSEC)
// VEX V5 C++ — lift state machine
LiftState currentState = RETRACTED;
bool lastL1 = false;
bool lastL2 = false;

while (true) {
    bool l1 = Controller1.ButtonL1.pressing();
    bool l2 = Controller1.ButtonL2.pressing();

    // Transitions
    if (l1 && !lastL1) {
        if (currentState == RETRACTED) currentState = LOW;
        else if (currentState == LOW)  currentState = HIGH;
    }
    if (l2 && !lastL2) {
        if (currentState == HIGH) currentState = LOW;
        else if (currentState == LOW) currentState = RETRACTED;
    }

    // Actions
    switch (currentState) {
        case RETRACTED:
            LiftMotor.spinToPosition(0, degrees, 60, percent, false);
            break;
        case LOW:
            LiftMotor.spinToPosition(90, degrees, 60, percent, false);
            break;
        case HIGH:
            LiftMotor.spinToPosition(200, degrees, 60, percent, false);
            break;
    }

    lastL1 = l1;
    lastL2 = l2;
    wait(20, msec);
}

State Diagrams

Before writing code, draw a state diagram. It is just a picture of your states with arrows showing transitions.

         [ L1 pressed ]              [ L1 pressed ]
RETRACTED ──────────────▶ LOW ──────────────────▶ HIGH
          ◀──────────────     ◀──────────────────
         [ L2 pressed ]              [ L2 pressed ]

Drawing this first helps you catch missing transitions and impossible states before you write a single line of code. Put your state diagram in your engineering notebook!

Adding Sensor-Triggered Transitions

States do not have to be triggered only by buttons — sensors work too. This is called a sensor-triggered transition.

# VEX IQ Python — automatically raise the lift when an object is detected
if distance_sensor.object_distance(MM) < 80:
    current_state = LOW  # object is close — raise lift automatically

This is how you can build “smart” robots that react to the game field automatically.

Gravity Compensation

When a lift arm is extended, gravity pulls it down. The motor has to fight gravity to hold the arm in place. You can help by adding a small constant power term when the lift is not retracted.

# VEX IQ Python — add a small "hold" power to fight gravity
GRAVITY_HOLD = 5  # tune this value — the minimum power that keeps arm still

if current_state == HIGH:
    lift_motor.spin_to_position(200, DEGREES, 60, PERCENT)
    # After reaching position, apply hold power
    lift_motor.spin(FORWARD, GRAVITY_HOLD, PERCENT)

Start with a small value (5–10%) and increase it until the arm holds its position without drifting down.

Common Mistakes

  • No rising-edge detection — Without checking last_button, the state machine transitions 50 times per second while the button is held, cycling through all states instantly.
  • Skipping the state diagram — Always draw it first. You will almost always find a missing transition you had not thought of.
  • One too many states — Keep it simple. Add states only when you actually need them.
  • Mixing state machine logic with drive code — Keep your lift state machine separate from your drive code. Running them in the same big block of code creates hard-to-find bugs.