218  State Machine Lab: ESP32 Implementation

218.1 Learning Objectives

By the end of this lab, you will be able to:

  • Implement FSM Patterns: Build state machine engines in C/C++ for ESP32
  • Use Guard Conditions: Implement conditional transitions based on runtime data
  • Handle Hierarchical States: Create parent/child state relationships
  • Persist State: Save and restore state across power cycles
  • Process Event Queues: Handle multiple events without race conditions
  • Debug State Logic: Trace and validate state machine execution in real-time

218.2 Prerequisites

Before starting this lab, you should be familiar with:

218.3 State Machine Lab: ESP32 Implementation

Time: ~45 min | Difficulty: Intermediate | Unit: P04.FSM.U02

218.3.1 What You Will Learn

In this hands-on lab, you will build and experiment with a comprehensive state machine implementation on ESP32. The simulation demonstrates:

  1. Basic FSM patterns: State definition, transitions, and event handling
  2. Guard conditions: Conditional transitions based on runtime data
  3. Hierarchical states: Parent/child state relationships
  4. State persistence: Saving and restoring state across power cycles
  5. Event queues: Handling multiple events without race conditions
  6. Timeout handling: Time-based state transitions
  7. State history: Returning to previous states

218.3.2 Lab Components

Component Purpose Simulation Role
ESP32 DevKit Main controller Runs state machine engine
Push Buttons (4) Event triggers Simulate external events
LEDs (4) State indicators Visual feedback for current state
Potentiometer Guard condition input Simulates sensor threshold
Serial Monitor Debug output Shows state transitions and logs

218.3.3 Wokwi Simulator Environment

NoteAbout Wokwi

Wokwi is a free online simulator for Arduino, ESP32, and other microcontrollers. It allows you to build and test IoT projects entirely in your browser without purchasing hardware. This lab demonstrates state machine concepts using standard components.

Launch the simulator below and copy the provided code to explore state machine concepts interactively.

TipSimulator Tips
  • Click the + button to add components (search for “Push Button”, “LED”, “Potentiometer”)
  • Use the Serial Monitor to see state transitions (115200 baud)
  • Press buttons to trigger events and observe state changes
  • Adjust the potentiometer to test guard conditions
  • The code demonstrates production-quality state machine patterns

218.3.4 Step-by-Step Instructions

218.3.4.1 Step 1: Set Up the Circuit

  1. Add 4 Push Buttons: Click + and search for “Push Button” - add 4 buttons
  2. Add 4 LEDs: Click + and search for “LED” - add Red, Yellow, Green, and Blue LEDs
  3. Add 1 Potentiometer: Click + and search for “Potentiometer”
  4. Wire the components to ESP32:
    • Button 1 (Event A) -> GPIO 12
    • Button 2 (Event B) -> GPIO 13
    • Button 3 (Event C) -> GPIO 14
    • Button 4 (Reset) -> GPIO 15
    • Red LED (State 1) -> GPIO 25
    • Yellow LED (State 2) -> GPIO 26
    • Green LED (State 3) -> GPIO 27
    • Blue LED (State 4) -> GPIO 32
    • Potentiometer signal -> GPIO 34 (ADC)
    • All button other pins -> GND
    • All LED cathodes -> GND (with resistors)

%% fig-alt: Circuit diagram showing ESP32 connected to four push buttons for triggering state machine events, four LEDs for indicating current state, and a potentiometer for testing guard conditions
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#7F8C8D'}}}%%
flowchart LR
    subgraph ESP32["ESP32 DevKit"]
        G12["GPIO 12<br/>(Event A)"]
        G13["GPIO 13<br/>(Event B)"]
        G14["GPIO 14<br/>(Event C)"]
        G15["GPIO 15<br/>(Reset)"]
        G25["GPIO 25<br/>(Red LED)"]
        G26["GPIO 26<br/>(Yellow LED)"]
        G27["GPIO 27<br/>(Green LED)"]
        G32["GPIO 32<br/>(Blue LED)"]
        G34["GPIO 34<br/>(ADC - Pot)"]
        GND["GND"]
    end

    subgraph Buttons["Event Triggers"]
        B1["Button 1<br/>Event A"]
        B2["Button 2<br/>Event B"]
        B3["Button 3<br/>Event C"]
        B4["Button 4<br/>Reset"]
    end

    subgraph LEDs["State Indicators"]
        LEDR["Red LED<br/>State 1"]
        LEDY["Yellow LED<br/>State 2"]
        LEDG["Green LED<br/>State 3"]
        LEDB["Blue LED<br/>State 4"]
    end

    subgraph Sensors["Guard Input"]
        POT["Potentiometer<br/>Threshold"]
    end

    G12 -.->|"Input"| B1
    G13 -.->|"Input"| B2
    G14 -.->|"Input"| B3
    G15 -.->|"Input"| B4
    G25 -.->|"Output"| LEDR
    G26 -.->|"Output"| LEDY
    G27 -.->|"Output"| LEDG
    G32 -.->|"Output"| LEDB
    G34 -.->|"Analog"| POT

    style ESP32 fill:#2C3E50,stroke:#16A085,stroke-width:2px,color:#fff
    style Buttons fill:#16A085,stroke:#2C3E50,stroke-width:2px,color:#fff
    style LEDs fill:#E67E22,stroke:#2C3E50,stroke-width:2px,color:#fff
    style Sensors fill:#7F8C8D,stroke:#2C3E50,stroke-width:2px,color:#fff

218.3.4.2 Step 2: Understanding the State Machine Architecture

This lab implements a multi-layered state machine demonstrating real-world IoT patterns:

%% fig-alt: State machine architecture diagram showing the main states IDLE, ACTIVE, PROCESSING, and ERROR with hierarchical sub-states within ACTIVE including MONITORING and RESPONDING modes
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#7F8C8D'}}}%%
stateDiagram-v2
    [*] --> IDLE : Power on / Restore

    state IDLE {
        [*] --> Waiting
        Waiting --> LowPower : Timeout 30s
        LowPower --> Waiting : Any event
    }

    IDLE --> ACTIVE : Event A [threshold > 50%]
    IDLE --> ERROR : Event C [system fault]

    state ACTIVE {
        [*] --> Monitoring
        Monitoring --> Responding : Sensor trigger
        Responding --> Monitoring : Response complete
    }

    ACTIVE --> PROCESSING : Event B
    ACTIVE --> IDLE : Event A (toggle)
    ACTIVE --> ERROR : Critical fault

    state PROCESSING {
        [*] --> Acquiring
        Acquiring --> Analyzing : Data ready
        Analyzing --> Transmitting : Analysis done
        Transmitting --> Complete : Sent
        Complete --> [*]
    }

    PROCESSING --> ACTIVE : Complete
    PROCESSING --> ERROR : Timeout

    state ERROR {
        [*] --> Diagnostic
        Diagnostic --> Recovery : Recoverable
        Diagnostic --> Fatal : Not recoverable
    }

    ERROR --> IDLE : Reset button

218.3.4.3 Step 3: Copy the State Machine Lab Code

Copy the following code into the Wokwi code editor. This comprehensive implementation demonstrates professional state machine patterns used in production IoT systems.

/*
 * State Machine Lab - ESP32 Implementation
 *
 * Comprehensive demonstration of state machine concepts for IoT:
 * - Finite State Machine (FSM) implementation
 * - Guard conditions for conditional transitions
 * - Hierarchical State Machines (HSM) with parent/child states
 * - Event-driven architecture with event queue
 * - State persistence using EEPROM/Preferences
 * - Timeout handling and watchdog patterns
 * - State history for return-to-previous functionality
 *
 * Hardware Configuration:
 * - Button 1 (GPIO 12): Event A - Primary trigger
 * - Button 2 (GPIO 13): Event B - Secondary trigger
 * - Button 3 (GPIO 14): Event C - Tertiary trigger
 * - Button 4 (GPIO 15): Reset - Force return to IDLE
 * - Red LED (GPIO 25): IDLE state indicator
 * - Yellow LED (GPIO 26): ACTIVE state indicator
 * - Green LED (GPIO 27): PROCESSING state indicator
 * - Blue LED (GPIO 32): ERROR state indicator
 * - Potentiometer (GPIO 34): Guard condition threshold
 *
 * Serial Monitor: 115200 baud for state transition logs
 */

#include <Arduino.h>
#include <Preferences.h>

// ============================================================
// PIN DEFINITIONS
// ============================================================
#define BTN_EVENT_A     12    // Primary event trigger
#define BTN_EVENT_B     13    // Secondary event trigger
#define BTN_EVENT_C     14    // Tertiary event trigger
#define BTN_RESET       15    // Force reset to IDLE

#define LED_IDLE        25    // Red LED - IDLE state
#define LED_ACTIVE      26    // Yellow LED - ACTIVE state
#define LED_PROCESSING  27    // Green LED - PROCESSING state
#define LED_ERROR       32    // Blue LED - ERROR state

#define POT_THRESHOLD   34    // Potentiometer for guard conditions

// ============================================================
// STATE MACHINE TYPE DEFINITIONS
// ============================================================

// Main state enumeration
enum MainState {
    STATE_IDLE = 0,
    STATE_ACTIVE,
    STATE_PROCESSING,
    STATE_ERROR,
    STATE_COUNT  // Used for array sizing
};

// Sub-states for IDLE
enum IdleSubState {
    IDLE_WAITING = 0,
    IDLE_LOW_POWER
};

// Sub-states for ACTIVE
enum ActiveSubState {
    ACTIVE_MONITORING = 0,
    ACTIVE_RESPONDING
};

// Sub-states for PROCESSING
enum ProcessingSubState {
    PROC_ACQUIRING = 0,
    PROC_ANALYZING,
    PROC_TRANSMITTING,
    PROC_COMPLETE
};

// Sub-states for ERROR
enum ErrorSubState {
    ERROR_DIAGNOSTIC = 0,
    ERROR_RECOVERY,
    ERROR_FATAL
};

// Event enumeration
enum Event {
    EVENT_NONE = 0,
    EVENT_A,           // Button 1 pressed
    EVENT_B,           // Button 2 pressed
    EVENT_C,           // Button 3 pressed
    EVENT_RESET,       // Reset button pressed
    EVENT_TIMEOUT,     // Timer expired
    EVENT_SENSOR,      // Simulated sensor trigger
    EVENT_DATA_READY,  // Data acquisition complete
    EVENT_TX_COMPLETE, // Transmission complete
    EVENT_FAULT,       // System fault detected
    EVENT_COUNT
};

// State machine context structure
struct StateMachineContext {
    MainState currentState;
    MainState previousState;
    uint8_t subState;
    uint8_t previousSubState;

    unsigned long stateEntryTime;
    unsigned long lastEventTime;

    uint32_t transitionCount;
    uint32_t eventCount;

    int guardThreshold;
    bool faultFlag;
    bool recoverable;

    // History for return-to-previous
    MainState historyState;
    uint8_t historySubState;
};

// Event queue for handling multiple events
#define EVENT_QUEUE_SIZE 16
struct EventQueue {
    Event events[EVENT_QUEUE_SIZE];
    uint8_t head;
    uint8_t tail;
    uint8_t count;
};

// ============================================================
// GLOBAL VARIABLES
// ============================================================

StateMachineContext fsm;
EventQueue eventQueue;
Preferences preferences;

// Debouncing
unsigned long lastButtonPress[4] = {0, 0, 0, 0};
const unsigned long DEBOUNCE_MS = 200;

// Timing constants (milliseconds)
const unsigned long IDLE_TIMEOUT = 30000;      // 30 seconds to low power
const unsigned long ACTIVE_TIMEOUT = 60000;    // 60 seconds max in active
const unsigned long PROCESSING_TIMEOUT = 10000; // 10 seconds max processing
const unsigned long RESPONSE_DURATION = 2000;   // 2 seconds response time

// Statistics
uint32_t totalEvents = 0;
uint32_t totalTransitions = 0;

// State names for logging
const char* stateNames[] = {
    "IDLE", "ACTIVE", "PROCESSING", "ERROR"
};

const char* idleSubStateNames[] = {"WAITING", "LOW_POWER"};
const char* activeSubStateNames[] = {"MONITORING", "RESPONDING"};
const char* procSubStateNames[] = {"ACQUIRING", "ANALYZING", "TRANSMITTING", "COMPLETE"};
const char* errorSubStateNames[] = {"DIAGNOSTIC", "RECOVERY", "FATAL"};

const char* eventNames[] = {
    "NONE", "EVENT_A", "EVENT_B", "EVENT_C", "RESET",
    "TIMEOUT", "SENSOR", "DATA_READY", "TX_COMPLETE", "FAULT"
};

// ============================================================
// UTILITY FUNCTIONS
// ============================================================

// Get current timestamp string
void getTimestamp(char* buffer, size_t len) {
    unsigned long ms = millis();
    unsigned long sec = ms / 1000;
    unsigned long min = sec / 60;
    snprintf(buffer, len, "[%02lu:%02lu.%03lu]",
             min % 60, sec % 60, ms % 1000);
}

// Log state transition
void logTransition(MainState from, MainState to, Event trigger) {
    char timestamp[16];
    getTimestamp(timestamp, sizeof(timestamp));

    Serial.printf("%s TRANSITION: %s -> %s (trigger: %s)\n",
                  timestamp, stateNames[from], stateNames[to],
                  eventNames[trigger]);
}

// Log sub-state transition
void logSubTransition(const char* stateName,
                      const char* fromSub, const char* toSub) {
    char timestamp[16];
    getTimestamp(timestamp, sizeof(timestamp));

    Serial.printf("%s SUB-TRANSITION [%s]: %s -> %s\n",
                  timestamp, stateName, fromSub, toSub);
}

// Log event
void logEvent(Event event) {
    char timestamp[16];
    getTimestamp(timestamp, sizeof(timestamp));

    Serial.printf("%s EVENT: %s received\n",
                  timestamp, eventNames[event]);
}

// ============================================================
// LED CONTROL FUNCTIONS
// ============================================================

void setAllLEDs(bool idle, bool active, bool processing, bool error) {
    digitalWrite(LED_IDLE, idle ? HIGH : LOW);
    digitalWrite(LED_ACTIVE, active ? HIGH : LOW);
    digitalWrite(LED_PROCESSING, processing ? HIGH : LOW);
    digitalWrite(LED_ERROR, error ? HIGH : LOW);
}

void blinkLED(int pin, int count, int onMs, int offMs) {
    for (int i = 0; i < count; i++) {
        digitalWrite(pin, HIGH);
        delay(onMs);
        digitalWrite(pin, LOW);
        if (i < count - 1) delay(offMs);
    }
}

// LED pattern for current state
void updateLEDsForState() {
    switch (fsm.currentState) {
        case STATE_IDLE:
            if (fsm.subState == IDLE_LOW_POWER) {
                // Dim blink for low power
                static bool toggle = false;
                toggle = !toggle;
                setAllLEDs(toggle, false, false, false);
            } else {
                setAllLEDs(true, false, false, false);
            }
            break;

        case STATE_ACTIVE:
            if (fsm.subState == ACTIVE_RESPONDING) {
                setAllLEDs(false, true, true, false); // Yellow + Green
            } else {
                setAllLEDs(false, true, false, false);
            }
            break;

        case STATE_PROCESSING:
            // Progressive LEDs based on sub-state
            switch (fsm.subState) {
                case PROC_ACQUIRING:
                    setAllLEDs(false, false, true, false);
                    break;
                case PROC_ANALYZING:
                    setAllLEDs(false, true, true, false);
                    break;
                case PROC_TRANSMITTING:
                    setAllLEDs(true, true, true, false);
                    break;
                case PROC_COMPLETE:
                    setAllLEDs(true, true, true, true);
                    break;
            }
            break;

        case STATE_ERROR:
            if (fsm.subState == ERROR_FATAL) {
                // Fast blink all
                static bool errorToggle = false;
                errorToggle = !errorToggle;
                setAllLEDs(errorToggle, errorToggle, errorToggle, true);
            } else {
                setAllLEDs(false, false, false, true);
            }
            break;
    }
}

// ============================================================
// EVENT QUEUE FUNCTIONS
// ============================================================

void initEventQueue() {
    eventQueue.head = 0;
    eventQueue.tail = 0;
    eventQueue.count = 0;
    memset(eventQueue.events, EVENT_NONE, sizeof(eventQueue.events));
}

bool enqueueEvent(Event event) {
    if (eventQueue.count >= EVENT_QUEUE_SIZE) {
        Serial.println("WARNING: Event queue full, event dropped!");
        return false;
    }

    eventQueue.events[eventQueue.tail] = event;
    eventQueue.tail = (eventQueue.tail + 1) % EVENT_QUEUE_SIZE;
    eventQueue.count++;

    totalEvents++;
    return true;
}

Event dequeueEvent() {
    if (eventQueue.count == 0) {
        return EVENT_NONE;
    }

    Event event = eventQueue.events[eventQueue.head];
    eventQueue.head = (eventQueue.head + 1) % EVENT_QUEUE_SIZE;
    eventQueue.count--;

    return event;
}

bool hasEvents() {
    return eventQueue.count > 0;
}

// ============================================================
// STATE PERSISTENCE FUNCTIONS
// ============================================================

void saveState() {
    preferences.begin("fsm", false);
    preferences.putUChar("mainState", (uint8_t)fsm.currentState);
    preferences.putUChar("subState", fsm.subState);
    preferences.putULong("transCnt", fsm.transitionCount);
    preferences.putULong("eventCnt", fsm.eventCount);
    preferences.putUChar("histState", (uint8_t)fsm.historyState);
    preferences.putUChar("histSub", fsm.historySubState);
    preferences.end();

    Serial.println("State saved to flash");
}

bool loadState() {
    preferences.begin("fsm", true);

    if (!preferences.isKey("mainState")) {
        preferences.end();
        return false;
    }

    fsm.currentState = (MainState)preferences.getUChar("mainState", STATE_IDLE);
    fsm.subState = preferences.getUChar("subState", 0);
    fsm.transitionCount = preferences.getULong("transCnt", 0);
    fsm.eventCount = preferences.getULong("eventCnt", 0);
    fsm.historyState = (MainState)preferences.getUChar("histState", STATE_IDLE);
    fsm.historySubState = preferences.getUChar("histSub", 0);

    preferences.end();

    Serial.println("State restored from flash");
    Serial.printf("  Restored to: %s (sub: %d)\n",
                  stateNames[fsm.currentState], fsm.subState);
    Serial.printf("  Historical transitions: %lu, events: %lu\n",
                  fsm.transitionCount, fsm.eventCount);

    return true;
}

void clearSavedState() {
    preferences.begin("fsm", false);
    preferences.clear();
    preferences.end();
    Serial.println("Saved state cleared");
}

// ============================================================
// GUARD CONDITION FUNCTIONS
// ============================================================

// Read and normalize potentiometer value (0-100)
int readGuardThreshold() {
    int raw = analogRead(POT_THRESHOLD);
    return map(raw, 0, 4095, 0, 100);
}

// Guard: Check if threshold is above 50%
bool guardThresholdHigh() {
    fsm.guardThreshold = readGuardThreshold();
    bool result = fsm.guardThreshold > 50;

    if (!result) {
        Serial.printf("  GUARD FAILED: Threshold %d%% <= 50%%\n",
                      fsm.guardThreshold);
    }
    return result;
}

// Guard: Check if system is not in fault state
bool guardNoFault() {
    return !fsm.faultFlag;
}

// Guard: Check if error is recoverable
bool guardRecoverable() {
    return fsm.recoverable;
}

// Guard: Check if in specific sub-state
bool guardSubState(uint8_t required) {
    return fsm.subState == required;
}

// ============================================================
// STATE ENTRY/EXIT ACTIONS
// ============================================================

void onEnterIdle() {
    Serial.println("  [ENTER] IDLE state");
    fsm.subState = IDLE_WAITING;
    fsm.stateEntryTime = millis();
    setAllLEDs(true, false, false, false);
}

void onExitIdle() {
    Serial.println("  [EXIT] IDLE state");
}

void onEnterActive() {
    Serial.println("  [ENTER] ACTIVE state");
    fsm.subState = ACTIVE_MONITORING;
    fsm.stateEntryTime = millis();
    setAllLEDs(false, true, false, false);

    // Simulate starting sensor monitoring
    Serial.println("  Starting sensor monitoring...");
}

void onExitActive() {
    Serial.println("  [EXIT] ACTIVE state");
    Serial.println("  Stopping sensor monitoring...");
}

void onEnterProcessing() {
    Serial.println("  [ENTER] PROCESSING state");
    fsm.subState = PROC_ACQUIRING;
    fsm.stateEntryTime = millis();
    setAllLEDs(false, false, true, false);

    Serial.println("  Starting data acquisition...");
}

void onExitProcessing() {
    Serial.println("  [EXIT] PROCESSING state");
}

void onEnterError() {
    Serial.println("  [ENTER] ERROR state");
    fsm.subState = ERROR_DIAGNOSTIC;
    fsm.stateEntryTime = millis();
    setAllLEDs(false, false, false, true);

    Serial.println("  Running diagnostics...");

    // Determine if error is recoverable
    fsm.recoverable = (random(100) > 30);  // 70% chance recoverable
    Serial.printf("  Error recoverable: %s\n",
                  fsm.recoverable ? "YES" : "NO");
}

void onExitError() {
    Serial.println("  [EXIT] ERROR state");
    fsm.faultFlag = false;
}

// ============================================================
// STATE TRANSITION FUNCTION
// ============================================================

void transitionTo(MainState newState, Event trigger) {
    if (newState == fsm.currentState) {
        return;  // No transition needed
    }

    MainState oldState = fsm.currentState;

    // Save history before transition
    fsm.historyState = fsm.currentState;
    fsm.historySubState = fsm.subState;

    // Execute exit action for current state
    switch (fsm.currentState) {
        case STATE_IDLE:      onExitIdle();       break;
        case STATE_ACTIVE:    onExitActive();     break;
        case STATE_PROCESSING: onExitProcessing(); break;
        case STATE_ERROR:     onExitError();      break;
    }

    // Update state
    fsm.previousState = fsm.currentState;
    fsm.currentState = newState;
    fsm.transitionCount++;
    totalTransitions++;

    // Log the transition
    logTransition(oldState, newState, trigger);

    // Execute entry action for new state
    switch (fsm.currentState) {
        case STATE_IDLE:      onEnterIdle();       break;
        case STATE_ACTIVE:    onEnterActive();     break;
        case STATE_PROCESSING: onEnterProcessing(); break;
        case STATE_ERROR:     onEnterError();      break;
    }

    // Save state periodically (every 5 transitions)
    if (fsm.transitionCount % 5 == 0) {
        saveState();
    }

    updateLEDsForState();
}

// ============================================================
// SUB-STATE TRANSITION FUNCTION
// ============================================================

void transitionSubState(uint8_t newSubState) {
    if (newSubState == fsm.subState) {
        return;
    }

    uint8_t oldSub = fsm.subState;
    fsm.previousSubState = fsm.subState;
    fsm.subState = newSubState;

    // Get sub-state names based on current main state
    const char** subNames = NULL;
    switch (fsm.currentState) {
        case STATE_IDLE:      subNames = idleSubStateNames;   break;
        case STATE_ACTIVE:    subNames = activeSubStateNames; break;
        case STATE_PROCESSING: subNames = procSubStateNames;  break;
        case STATE_ERROR:     subNames = errorSubStateNames;  break;
    }

    if (subNames) {
        logSubTransition(stateNames[fsm.currentState],
                        subNames[oldSub], subNames[newSubState]);
    }

    updateLEDsForState();
}

// ============================================================
// STATE HANDLERS (Per-State Logic)
// ============================================================

void handleIdleState(Event event) {
    unsigned long elapsed = millis() - fsm.stateEntryTime;

    // Handle sub-states
    switch (fsm.subState) {
        case IDLE_WAITING:
            if (elapsed > IDLE_TIMEOUT) {
                transitionSubState(IDLE_LOW_POWER);
                Serial.println("  Entering low power mode...");
            }
            break;

        case IDLE_LOW_POWER:
            // Any event wakes us up
            if (event != EVENT_NONE && event != EVENT_TIMEOUT) {
                transitionSubState(IDLE_WAITING);
                fsm.stateEntryTime = millis();
                Serial.println("  Waking from low power...");
            }
            break;
    }

    // Handle events
    switch (event) {
        case EVENT_A:
            if (guardThresholdHigh()) {
                transitionTo(STATE_ACTIVE, event);
            } else {
                Serial.println("  Transition blocked by guard condition");
            }
            break;

        case EVENT_C:
            // Simulate fault trigger
            fsm.faultFlag = true;
            transitionTo(STATE_ERROR, event);
            break;

        default:
            break;
    }
}

void handleActiveState(Event event) {
    unsigned long elapsed = millis() - fsm.stateEntryTime;

    // Timeout check
    if (elapsed > ACTIVE_TIMEOUT) {
        Serial.println("  ACTIVE state timeout - returning to IDLE");
        transitionTo(STATE_IDLE, EVENT_TIMEOUT);
        return;
    }

    // Handle sub-states
    switch (fsm.subState) {
        case ACTIVE_MONITORING:
            // Simulate random sensor trigger (5% chance per loop)
            if (event == EVENT_SENSOR || random(100) < 5) {
                transitionSubState(ACTIVE_RESPONDING);
                fsm.stateEntryTime = millis();  // Reset for response timing
            }
            break;

        case ACTIVE_RESPONDING:
            if (millis() - fsm.stateEntryTime > RESPONSE_DURATION) {
                transitionSubState(ACTIVE_MONITORING);
                Serial.println("  Response complete, back to monitoring");
            }
            break;
    }

    // Handle events
    switch (event) {
        case EVENT_A:
            // Toggle back to IDLE
            transitionTo(STATE_IDLE, event);
            break;

        case EVENT_B:
            // Start processing
            transitionTo(STATE_PROCESSING, event);
            break;

        case EVENT_C:
            // Critical fault
            fsm.faultFlag = true;
            transitionTo(STATE_ERROR, event);
            break;

        default:
            break;
    }
}

void handleProcessingState(Event event) {
    unsigned long elapsed = millis() - fsm.stateEntryTime;

    // Timeout check
    if (elapsed > PROCESSING_TIMEOUT) {
        Serial.println("  PROCESSING timeout - entering ERROR");
        fsm.faultFlag = true;
        fsm.recoverable = true;
        transitionTo(STATE_ERROR, EVENT_TIMEOUT);
        return;
    }

    // Automatic sub-state progression (simulated timing)
    switch (fsm.subState) {
        case PROC_ACQUIRING:
            if (elapsed > 2000) {
                transitionSubState(PROC_ANALYZING);
                Serial.println("  Data acquired, starting analysis...");
            }
            break;

        case PROC_ANALYZING:
            if (elapsed > 4000) {
                transitionSubState(PROC_TRANSMITTING);
                Serial.println("  Analysis complete, transmitting...");
            }
            break;

        case PROC_TRANSMITTING:
            if (elapsed > 6000) {
                transitionSubState(PROC_COMPLETE);
                Serial.println("  Transmission complete!");
            }
            break;

        case PROC_COMPLETE:
            if (elapsed > 7000) {
                transitionTo(STATE_ACTIVE, EVENT_TX_COMPLETE);
            }
            break;
    }

    // Handle events
    switch (event) {
        case EVENT_C:
            // Abort processing
            fsm.faultFlag = true;
            transitionTo(STATE_ERROR, event);
            break;

        case EVENT_RESET:
            transitionTo(STATE_IDLE, event);
            break;

        default:
            break;
    }
}

void handleErrorState(Event event) {
    unsigned long elapsed = millis() - fsm.stateEntryTime;

    // Handle sub-states
    switch (fsm.subState) {
        case ERROR_DIAGNOSTIC:
            if (elapsed > 2000) {
                if (fsm.recoverable) {
                    transitionSubState(ERROR_RECOVERY);
                    Serial.println("  Starting recovery procedure...");
                } else {
                    transitionSubState(ERROR_FATAL);
                    Serial.println("  FATAL ERROR - Manual reset required");
                }
            }
            break;

        case ERROR_RECOVERY:
            if (elapsed > 5000) {
                Serial.println("  Recovery successful!");
                // Return to history state
                transitionTo(fsm.historyState, EVENT_NONE);
            }
            break;

        case ERROR_FATAL:
            // Only reset button can exit
            break;
    }

    // Handle events
    switch (event) {
        case EVENT_RESET:
            Serial.println("  Manual reset triggered");
            fsm.faultFlag = false;
            transitionTo(STATE_IDLE, event);
            break;

        default:
            break;
    }
}

// ============================================================
// MAIN STATE MACHINE DISPATCHER
// ============================================================

void processStateMachine(Event event) {
    if (event != EVENT_NONE) {
        logEvent(event);
        fsm.eventCount++;
        fsm.lastEventTime = millis();
    }

    // Dispatch to current state handler
    switch (fsm.currentState) {
        case STATE_IDLE:
            handleIdleState(event);
            break;

        case STATE_ACTIVE:
            handleActiveState(event);
            break;

        case STATE_PROCESSING:
            handleProcessingState(event);
            break;

        case STATE_ERROR:
            handleErrorState(event);
            break;
    }
}

// ============================================================
// BUTTON HANDLING
// ============================================================

void checkButtons() {
    unsigned long now = millis();

    // Button 1 - Event A
    if (digitalRead(BTN_EVENT_A) == LOW) {
        if (now - lastButtonPress[0] > DEBOUNCE_MS) {
            enqueueEvent(EVENT_A);
            lastButtonPress[0] = now;
        }
    }

    // Button 2 - Event B
    if (digitalRead(BTN_EVENT_B) == LOW) {
        if (now - lastButtonPress[1] > DEBOUNCE_MS) {
            enqueueEvent(EVENT_B);
            lastButtonPress[1] = now;
        }
    }

    // Button 3 - Event C
    if (digitalRead(BTN_EVENT_C) == LOW) {
        if (now - lastButtonPress[2] > DEBOUNCE_MS) {
            enqueueEvent(EVENT_C);
            lastButtonPress[2] = now;
        }
    }

    // Button 4 - Reset
    if (digitalRead(BTN_RESET) == LOW) {
        if (now - lastButtonPress[3] > DEBOUNCE_MS) {
            enqueueEvent(EVENT_RESET);
            lastButtonPress[3] = now;
        }
    }
}

// ============================================================
// STATISTICS AND STATUS DISPLAY
// ============================================================

void printStatus() {
    Serial.println("\n========== STATE MACHINE STATUS ==========");
    Serial.printf("Current State: %s\n", stateNames[fsm.currentState]);

    const char** subNames = NULL;
    switch (fsm.currentState) {
        case STATE_IDLE:      subNames = idleSubStateNames;   break;
        case STATE_ACTIVE:    subNames = activeSubStateNames; break;
        case STATE_PROCESSING: subNames = procSubStateNames;  break;
        case STATE_ERROR:     subNames = errorSubStateNames;  break;
    }
    if (subNames) {
        Serial.printf("Sub-State: %s\n", subNames[fsm.subState]);
    }

    Serial.printf("Guard Threshold: %d%%\n", readGuardThreshold());
    Serial.printf("Time in State: %lu ms\n", millis() - fsm.stateEntryTime);
    Serial.printf("Total Transitions: %lu\n", totalTransitions);
    Serial.printf("Total Events: %lu\n", totalEvents);
    Serial.printf("Event Queue: %d pending\n", eventQueue.count);
    Serial.printf("Fault Flag: %s\n", fsm.faultFlag ? "SET" : "CLEAR");
    Serial.println("==========================================\n");
}

// ============================================================
// INITIALIZATION
// ============================================================

void initPins() {
    // Configure button pins with internal pull-up
    pinMode(BTN_EVENT_A, INPUT_PULLUP);
    pinMode(BTN_EVENT_B, INPUT_PULLUP);
    pinMode(BTN_EVENT_C, INPUT_PULLUP);
    pinMode(BTN_RESET, INPUT_PULLUP);

    // Configure LED pins as output
    pinMode(LED_IDLE, OUTPUT);
    pinMode(LED_ACTIVE, OUTPUT);
    pinMode(LED_PROCESSING, OUTPUT);
    pinMode(LED_ERROR, OUTPUT);

    // Initialize LEDs off
    setAllLEDs(false, false, false, false);

    // Potentiometer is analog input (no pinMode needed)
}

void initStateMachine() {
    // Initialize FSM context
    fsm.currentState = STATE_IDLE;
    fsm.previousState = STATE_IDLE;
    fsm.subState = IDLE_WAITING;
    fsm.previousSubState = IDLE_WAITING;
    fsm.stateEntryTime = millis();
    fsm.lastEventTime = millis();
    fsm.transitionCount = 0;
    fsm.eventCount = 0;
    fsm.guardThreshold = 50;
    fsm.faultFlag = false;
    fsm.recoverable = true;
    fsm.historyState = STATE_IDLE;
    fsm.historySubState = 0;

    // Initialize event queue
    initEventQueue();

    // Try to restore previous state
    if (!loadState()) {
        Serial.println("No saved state found, starting fresh");
    }

    // Execute entry action for initial state
    switch (fsm.currentState) {
        case STATE_IDLE:      onEnterIdle();       break;
        case STATE_ACTIVE:    onEnterActive();     break;
        case STATE_PROCESSING: onEnterProcessing(); break;
        case STATE_ERROR:     onEnterError();      break;
    }
}

// ============================================================
// SETUP AND MAIN LOOP
// ============================================================

void setup() {
    Serial.begin(115200);
    delay(1000);  // Wait for serial monitor

    Serial.println("\n");
    Serial.println("==============================================");
    Serial.println("  STATE MACHINE LAB - ESP32 Implementation");
    Serial.println("==============================================");
    Serial.println("Demonstrating FSM concepts for IoT devices:");
    Serial.println("  - Finite State Machine patterns");
    Serial.println("  - Guard conditions");
    Serial.println("  - Hierarchical sub-states");
    Serial.println("  - State persistence");
    Serial.println("  - Event queue processing");
    Serial.println("----------------------------------------------");
    Serial.println("CONTROLS:");
    Serial.println("  Button 1 (GPIO 12): Event A - IDLE<->ACTIVE");
    Serial.println("  Button 2 (GPIO 13): Event B - Start Processing");
    Serial.println("  Button 3 (GPIO 14): Event C - Trigger Error");
    Serial.println("  Button 4 (GPIO 15): Reset to IDLE");
    Serial.println("  Potentiometer: Guard threshold (needs >50%)");
    Serial.println("==============================================\n");

    // Initialize random seed
    randomSeed(analogRead(0));

    // Initialize hardware
    initPins();

    // Initialize state machine
    initStateMachine();

    // Print initial status
    printStatus();

    Serial.println("State machine running. Press buttons to trigger events.\n");
}

// LED update timing
unsigned long lastLEDUpdate = 0;
const unsigned long LED_UPDATE_INTERVAL = 100;

// Status print timing
unsigned long lastStatusPrint = 0;
const unsigned long STATUS_INTERVAL = 15000;  // Every 15 seconds

void loop() {
    unsigned long now = millis();

    // Check for button presses
    checkButtons();

    // Process any pending events
    while (hasEvents()) {
        Event event = dequeueEvent();
        processStateMachine(event);
    }

    // Run state machine with no event (for timeout processing)
    processStateMachine(EVENT_NONE);

    // Update LEDs periodically
    if (now - lastLEDUpdate > LED_UPDATE_INTERVAL) {
        updateLEDsForState();
        lastLEDUpdate = now;
    }

    // Print status periodically
    if (now - lastStatusPrint > STATUS_INTERVAL) {
        printStatus();
        lastStatusPrint = now;
    }

    // Small delay to prevent CPU hogging
    delay(10);
}

218.3.4.4 Step 4: Run the Simulation

  1. Start the simulation: Click the green “Play” button
  2. Open Serial Monitor: Click on the Serial Monitor tab (set to 115200 baud)
  3. Observe initial state: The ESP32 starts in IDLE state (Red LED on)
  4. Adjust the potentiometer: Turn it above 50% to enable the guard condition

218.3.4.5 Step 5: Experiment with State Transitions

Try these scenarios to understand state machine behavior:

Scenario 1: Basic Transitions 1. Ensure potentiometer is above 50% 2. Press Button 1 (Event A) - Observe transition from IDLE to ACTIVE 3. Press Button 1 again - Return to IDLE

Scenario 2: Guard Conditions 1. Turn potentiometer below 50% 2. Press Button 1 - Observe guard failure message 3. Turn potentiometer above 50% 4. Press Button 1 - Now transition succeeds

Scenario 3: Processing Flow 1. Start in ACTIVE state 2. Press Button 2 (Event B) - Enter PROCESSING 3. Watch automatic sub-state progression (Acquiring -> Analyzing -> Transmitting -> Complete) 4. Observe automatic return to ACTIVE when complete

Scenario 4: Error Handling 1. Press Button 3 (Event C) from any state - Enter ERROR 2. Watch diagnostic sub-state determine recoverability 3. If recoverable, watch automatic recovery 4. If fatal, use Button 4 (Reset) to return to IDLE

Scenario 5: State Persistence 1. Make several state transitions 2. Stop and restart the simulation 3. Observe state restoration from saved data

218.3.5 Challenge Exercises

NoteChallenge 1: Add a SLEEP State

Extend the state machine to include a deep sleep state:

  1. After 60 seconds in IDLE LOW_POWER, transition to SLEEP
  2. SLEEP should turn off all LEDs except brief periodic blinks
  3. Only Button 4 (Reset) should wake from SLEEP
  4. Track sleep duration and report on wake

Hints: - Add STATE_SLEEP to the MainState enum - Implement onEnterSleep() and onExitSleep() actions - Use ESP32’s esp_deep_sleep() for real hardware

NoteChallenge 2: Implement State Machine Visualization

Add a serial command interface to:

  1. Print a text-based state diagram showing current position
  2. List all valid transitions from current state
  3. Show transition history (last 10 transitions)
  4. Export state machine statistics in JSON format

Hints: - Add a handleSerialCommand() function - Use character commands: ‘d’ for diagram, ‘h’ for history, ‘s’ for stats

NoteChallenge 3: Add Priority Event Queue

Modify the event queue to support priority:

  1. Reset events should always be processed first
  2. Error events should have high priority
  3. Normal events processed in FIFO order
  4. Add queue statistics (dropped events, max depth)

Hints: - Create a priority field in the event structure - Use a heap or multiple queues for different priorities

NoteChallenge 4: Implement Watchdog State Machine

Create a parallel watchdog state machine that:

  1. Monitors the main state machine for stuck states
  2. Triggers automatic recovery if no transition in 2 minutes
  3. Logs suspected stuck states with context
  4. Implements a separate ERROR_WATCHDOG sub-state

Hints: - Track lastTransitionTime in the FSM context - Check in each loop iteration - Consider using a hardware watchdog timer

218.3.6 Expected Outcomes

After completing this lab, you should be able to:

Skill Demonstration
Define states clearly Each state has a single responsibility and clear LED indication
Implement guard conditions Potentiometer threshold blocks/allows transitions
Handle hierarchical states Sub-states within IDLE, ACTIVE, PROCESSING, ERROR
Persist state across resets State restored from flash on power-up
Process events safely Event queue prevents race conditions
Debug state machines Serial output shows all transitions and timing

218.4 Knowledge Check

In the lab code, what happens when you try to transition from IDLE to ACTIVE with the potentiometer set below 50%?

  1. The transition occurs but with a warning
  2. The transition is blocked and a message is logged
  3. The system enters an ERROR state
  4. The potentiometer value is automatically adjusted
Click for answer

Answer: B) The transition is blocked and a message is logged

Guard conditions act as filters on transitions. The guardThresholdHigh() function checks if the potentiometer reading is above 50%. If not, the transition is blocked and the message “Transition blocked by guard condition” is printed. The device remains in the current state.

What problem does state persistence solve in IoT devices?

  1. Reducing power consumption during sleep
  2. Resuming correct operation after power loss or reset
  3. Improving communication reliability
  4. Reducing code complexity
Click for answer

Answer: B) Resuming correct operation after power loss or reset

IoT devices may lose power unexpectedly or be reset remotely. State persistence saves the current state to non-volatile storage (EEPROM/flash) so the device can resume from where it left off rather than starting from scratch. This is critical for devices that maintain long-running operations or track accumulated data.

Why does the lab implementation use an event queue instead of processing button presses immediately?

  1. To save power by batching operations
  2. To prevent race conditions and ensure events are processed in order
  3. To make the code run faster
  4. Because ESP32 requires queued interrupt handling
Click for answer

Answer: B) To prevent race conditions and ensure events are processed in order

Event queues decouple event detection from event processing. This prevents race conditions where multiple buttons pressed simultaneously could cause undefined behavior. The queue ensures events are processed one at a time in the order they occurred, making the state machine behavior deterministic and predictable.

218.5 Summary

This lab demonstrated professional-grade state machine implementation for ESP32:

  • State definitions with clear responsibilities and visual indicators
  • Guard conditions using potentiometer as threshold sensor
  • Hierarchical states with parent/child relationships
  • State persistence using ESP32 Preferences API
  • Event queues for safe, ordered event processing
  • Entry/exit actions for state initialization and cleanup

The patterns learned here apply to everything from simple sensor nodes to complex industrial controllers.

218.6 What’s Next

Continue your state machine learning with: