30 Days Lost in Space → Help Center

Coding Concepts

State Machines

Your Arduino project needs to be in a mode, like locked vs unlocked or waiting vs entering a code. State machines are the pattern that keeps this organized instead of devolving into tangled if-statements and boolean flags.

Think About a Door Lock

Forget code for a second. Think about how a combination lock on a door actually works. At any moment, the lock is in one of a few situations:

  • Locked. Waiting for someone to start entering a code.
  • Entering code. Someone is pressing buttons. The lock is collecting digits.
  • Unlocked. The right code was entered. The door can open.
  • Alarm. The wrong code was entered too many times.

These situations are states. The lock can only be in one state at a time. Specific events cause it to change from one state to another: entering the last correct digit moves it from "entering code" to "unlocked." Entering the wrong code three times moves it from "entering code" to "alarm."

That is a state machine. Your Arduino programs need the same structure any time the system has modes or phases.

Why You Need This

Without a state machine, here is what happens. You start with a boolean: bool isLocked = true;. Then you need to track whether someone is entering a code, so you add bool isEnteringCode = false;. Then you need failed attempts, so int failCount = 0;. Then a timeout, so bool timedOut = false;.

Pretty soon your loop() function is a nest of if (isLocked && !isEnteringCode && !timedOut) checks. The booleans start contradicting each other. Bugs hide in combinations you never thought of.

A state machine replaces all of that with one variable: currentState. Your loop checks which state you are in and runs only the code for that state. Clean, debuggable, and impossible to be in two states at once.

The Pattern

Every state machine has three parts:

1. Define the states

enum State {
  STATE_LOCKED,
  STATE_ENTERING_CODE,
  STATE_UNLOCKED,
  STATE_ALARM
};

2. Track the current state

State currentState = STATE_LOCKED;

3. A switch/case in loop() that handles each state

void loop() {
  switch (currentState) {
    case STATE_LOCKED:
      handleLocked();
      break;
    case STATE_ENTERING_CODE:
      handleEnteringCode();
      break;
    case STATE_UNLOCKED:
      handleUnlocked();
      break;
    case STATE_ALARM:
      handleAlarm();
      break;
  }
}

Each handler function does the work for that state and changes currentState when a transition should happen.

State Transitions

A transition is when your machine moves from one state to another. Draw these out before you write code. Grab paper and draw circles for each state with arrows between them.

  +--------+     key pressed    +-----------------+
  | LOCKED | -----------------> | ENTERING_CODE   |
  +--------+                    +-----------------+
      ^                           |          |
      |                  correct  |          | 3 wrong
      |                  code     |          | attempts
      |                           v          v
      |    timeout         +-----------+  +-------+
      +------------------- | UNLOCKED  |  | ALARM |
                           +-----------+  +-------+

In code, a transition looks like this:

void handleEnteringCode() {
  char key = keypad.getKey();
  if (key) {
    enteredCode += key;
    if (enteredCode == SECRET_CODE) {
      currentState = STATE_UNLOCKED;  // transition!
      enteredCode = "";
    }
  }
}

You change currentState and then the next time through loop(), the switch/case runs the new state's code.

Adding Timeouts

States often need timeouts. If the system is unlocked, it should re-lock after 10 seconds. Combine the state machine with the millis() timing pattern:

unsigned long stateEnteredAt = 0;
const unsigned long UNLOCK_TIMEOUT = 10000;

void transitionTo(State newState) {
  currentState = newState;
  stateEnteredAt = millis();
}

void handleUnlocked() {
  if (millis() - stateEnteredAt >= UNLOCK_TIMEOUT) {
    transitionTo(STATE_LOCKED);
    return;
  }
  // ... other unlocked behavior
}

By using a transitionTo() helper, you automatically record the time of every state change.

Debugging State Machines

When something goes wrong, the first question is always: "What state was it in?" Add Serial prints to your transitions:

void transitionTo(State newState) {
  Serial.print("[");
  Serial.print(millis());
  Serial.print("] State: ");

  switch (newState) {
    case STATE_LOCKED:        Serial.println("LOCKED"); break;
    case STATE_ENTERING_CODE: Serial.println("ENTERING_CODE"); break;
    case STATE_UNLOCKED:      Serial.println("UNLOCKED"); break;
    case STATE_ALARM:         Serial.println("ALARM"); break;
  }

  currentState = newState;
  stateEnteredAt = millis();
}

Common Mistakes

| Mistake | What Happens | Fix | |---------|-------------|-----| | Transition happens every loop iteration | State keeps resetting, timeout never works | Only transition once when the trigger condition first becomes true | | No path back to the starting state | System gets stuck in ALARM or UNLOCKED forever | Every state needs at least one transition out | | Forgetting to handle a state in the switch | Code silently does nothing for that state | Include all enum values, add a default case | | Doing too much work in one handler | Inputs are missed, keypad presses lost | Keep handlers fast, use millis() instead of delay() | | Not resetting variables during transitions | Old data carries over and causes bugs | Put reset logic in transitionTo() |