%% 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 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:
- State Machine Fundamentals: Core concepts of states, transitions, events, and guards
- Microcontroller Programming Essentials: Basic C/C++ programming and GPIO handling
218.3 State Machine Lab: ESP32 Implementation
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:
- Basic FSM patterns: State definition, transitions, and event handling
- Guard conditions: Conditional transitions based on runtime data
- Hierarchical states: Parent/child state relationships
- State persistence: Saving and restoring state across power cycles
- Event queues: Handling multiple events without race conditions
- Timeout handling: Time-based state transitions
- 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
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.
- 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
- Add 4 Push Buttons: Click + and search for “Push Button” - add 4 buttons
- Add 4 LEDs: Click + and search for “LED” - add Red, Yellow, Green, and Blue LEDs
- Add 1 Potentiometer: Click + and search for “Potentiometer”
- 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)
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
- Start the simulation: Click the green “Play” button
- Open Serial Monitor: Click on the Serial Monitor tab (set to 115200 baud)
- Observe initial state: The ESP32 starts in IDLE state (Red LED on)
- 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
Extend the state machine to include a deep sleep state:
- After 60 seconds in IDLE LOW_POWER, transition to SLEEP
- SLEEP should turn off all LEDs except brief periodic blinks
- Only Button 4 (Reset) should wake from SLEEP
- 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
Add a serial command interface to:
- Print a text-based state diagram showing current position
- List all valid transitions from current state
- Show transition history (last 10 transitions)
- Export state machine statistics in JSON format
Hints: - Add a handleSerialCommand() function - Use character commands: ‘d’ for diagram, ‘h’ for history, ‘s’ for stats
Modify the event queue to support priority:
- Reset events should always be processed first
- Error events should have high priority
- Normal events processed in FIFO order
- 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
Create a parallel watchdog state machine that:
- Monitors the main state machine for stuck states
- Triggers automatic recovery if no transition in 2 minutes
- Logs suspected stuck states with context
- 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%?
- The transition occurs but with a warning
- The transition is blocked and a message is logged
- The system enters an ERROR state
- 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?
- Reducing power consumption during sleep
- Resuming correct operation after power loss or reset
- Improving communication reliability
- 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?
- To save power by batching operations
- To prevent race conditions and ensure events are processed in order
- To make the code run faster
- 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:
- State Machine Design Patterns: Common patterns for connection, sampling, and safety
- Process Control and PID: Apply state machines to control systems
- Duty Cycling and Topology: State-based power management