A flexible and powerful event-driven State Machine library for Arduino with cross-platform support (ESP8266, ESP32, AVR, RP2040, and others).
Read this in Italian
- Cross-platform: Works on ESP8266, ESP32, AVR (Arduino Uno/Mega/Nano), RP2040 (Raspberry Pi Pico), and other Arduino-compatible boards
- Hardware timer support: Uses native hardware timers on ESP and RP2040 for reliable timeouts even when loop is blocked
- Advanced event handling: Multiple callbacks for each state event (enter, exit, during)
- Configurable timeouts: Set multiple timeouts per state with specific callbacks
- Global transition handlers: Customizable hooks before and after state changes
- Memory efficient: Uses static arrays on AVR to minimize RAM usage, dynamic vectors on platforms with more memory
- Easy integration with persistent storage: Example of saving and recovering state from flash memory
- Robust error handling: Verification of state validity and callbacks
| Platform | Timer Type | Timeout Reliability | STL Support |
|---|---|---|---|
| ESP8266 | Hardware (Ticker) | Guaranteed* | Yes |
| ESP32 | Hardware (Ticker) | Guaranteed* | Yes |
| RP2040 | Hardware (Pico SDK) | Guaranteed* | Yes |
| AVR | Software (millis) | Depends on update()** | No (static arrays) |
| Others | Software (millis) | Depends on update()** | Configurable |
* Timeouts fire even if the main loop is blocked
** Timeouts only fire when update() is called
- Arduino-compatible board (ESP8266, ESP32, AVR, RP2040, etc.)
- Arduino IDE 1.8.0+ or PlatformIO
- (Optional) LittleFS for state persistence example (ESP/RP2040 only)
- Open Arduino IDE
- Go to Sketch > Include Library > Manage Libraries...
- Search for "EventStateMachine"
- Select the library and click Install
Add to your platformio.ini:
lib_deps =
FantasyFactory/EventStateMachineOr use the PlatformIO Library Manager in VS Code.
- Download the library as a ZIP file from GitHub
- Arduino IDE: Sketch > Include Library > Add .ZIP Library...
- Select the downloaded ZIP file
EventStateMachine uses a callback system to handle state events:
- onEnter: Executed when entering a state
- onState: Executed continuously while in a state (in the
update()method) - onExit: Executed when exiting a state
- onTimeout: Executed when a configured timeout expires
Each state can have multiple timeouts with different durations and callbacks:
// Add a 5-second timeout to the RUNNING state
stateMachine.addTimeout(STATE_RUNNING, 5000, onRunningTimeout);You can register functions that will be called before and after every state transition:
// Add a handler to save every transition
stateMachine.addAfterStateChangeHandler(saveStateTransition);#include <EventStateMachine.h>
// Define states
enum States {
STATE_IDLE,
STATE_RUNNING,
STATE_ERROR,
NUM_STATES
};
// Create state machine
EventStateMachine stateMachine(NUM_STATES);
// Callback when entering RUNNING state
void onEnterRunning(uint8_t current, uint8_t previous) {
Serial.println("Entering RUNNING state");
digitalWrite(LED_BUILTIN, HIGH);
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
// Add callbacks
stateMachine.addOnEnter(STATE_RUNNING, onEnterRunning);
// Set initial state
stateMachine.setState(STATE_IDLE);
}
void loop() {
// Update state machine (required for onState callbacks and millis-based timeouts)
stateMachine.update();
// Change state based on external conditions
if (/* some condition */) {
stateMachine.setState(STATE_RUNNING);
}
}// Create a state machine with the specified number of states
EventStateMachine(uint8_t numberOfStates);// Configure a state in a single call
void configureState(
uint8_t state, // State to configure
unsigned long timeout = 0, // Optional timeout (ms)
StateCallback onEnter = nullptr, // Entry callback
StateFunction onState = nullptr, // During callback
StateCallback onExit = nullptr, // Exit callback
StateCallback onTimeout = nullptr // Timeout callback
);
// Methods to add individual callbacks
bool addTimeout(uint8_t state, unsigned long timeout, StateCallback onTimeout);
bool addOnEnter(uint8_t state, StateCallback onEnter);
bool addOnState(uint8_t state, StateFunction onState);
bool addOnExit(uint8_t state, StateCallback onExit);
// Methods to remove callbacks
bool removeTimeout(uint8_t state, unsigned long timeout);
bool removeOnEnter(uint8_t state, StateCallback onEnter);
bool removeOnState(uint8_t state, StateFunction onState);
bool removeOnExit(uint8_t state, StateCallback onExit);void addBeforeStateChangeHandler(GlobalStateCallback handler);
void addAfterStateChangeHandler(GlobalStateCallback handler);
bool removeBeforeStateChangeHandler(GlobalStateCallback handler);
bool removeAfterStateChangeHandler(GlobalStateCallback handler);// Change the current state
void setState(uint8_t newState);
// Perform an update cycle (call this in loop())
void update();
// Enable/disable debug messages on serial port
void setDebug(bool enable);
// Set singleton instance (auto-called in constructor, override for multiple instances)
void setInstance();// Get the current state
uint8_t getCurrentState() const;
// Get the previous state
uint8_t getPreviousState() const;
// Check if the state has just changed
bool isStateChanged() const;
// Get the time spent in the current state (in ms)
unsigned long timeInCurrentState() const;
// Get the total number of states
uint8_t getNumStates() const;
// Check if using hardware timer (ESP/RP2040) or software polling (AVR)
static bool hasHardwareTimer();On AVR platforms (Arduino Uno, Mega, Nano, etc.), the library uses static arrays instead of dynamic vectors to save memory. You can configure the maximum sizes before including the library:
// Optional: Override defaults before including the library
#define ESM_MAX_TIMEOUTS_PER_STATE 2 // Default: 4
#define ESM_MAX_CALLBACKS_PER_STATE 2 // Default: 4
#define ESM_MAX_GLOBAL_HANDLERS 2 // Default: 4
#include <EventStateMachine.h>The library includes three complete examples:
Demonstrates basic state machine usage with three states and essential callbacks. Controls the built-in LED based on state and responds to serial commands.
Illustrates how to use multiple callbacks for each type of state event:
- Multiple entry callbacks for a state
- Multiple during callbacks for a state
- Multiple exit callbacks for a state
- Multiple timeouts with different durations
Shows how to implement state persistence using LittleFS (ESP/RP2040 only). Allows you to:
- Save each state transition to a log file
- Recover the last saved state after a restart
- View the history of transitions
The library uses different timer implementations depending on the platform:
Hardware Timers (ESP8266, ESP32, RP2040)
- Timeouts use hardware interrupts via Ticker (ESP) or Pico SDK alarms (RP2040)
- Callbacks execute asynchronously, even if
loop()is blocked - Ideal for safety-critical timeouts (e.g., preventing a state from getting stuck)
Software Polling (AVR and others)
- Timeouts are checked during
update()usingmillis() - Accuracy depends on how frequently
update()is called - Blocking code (e.g.,
delay()) will delay timeout execution
You can check at runtime which timer type is being used:
if (EventStateMachine::hasHardwareTimer()) {
Serial.println("Using hardware timers - timeouts are reliable");
} else {
Serial.println("Using millis polling - call update() frequently!");
}- ESP/RP2040: Uses C++
std::vectorfor flexible, dynamic callback storage - AVR: Uses fixed-size arrays to minimize RAM usage on memory-constrained devices
On hardware timer platforms (ESP/RP2040):
- Timeouts should fire reliably even with blocking code
- Make sure the timeout callback is not null
- Verify the timeout duration is > 0
On millis-based platforms (AVR):
- Ensure
update()is called frequently inloop() - Avoid long
delay()calls that block execution - Consider using non-blocking patterns (e.g.,
millis()timing)
- Ensure the state is valid (< numStates)
- Verify callbacks have been properly registered
- Check that the state machine instance is not being recreated
- If you get "out of memory" errors, reduce
ESM_MAX_*values - The library disables STL features on AVR automatically
Contributions are welcome! Feel free to open issues or submit pull requests on GitHub.
This library is released under the MIT License. See the LICENSE file for details.
Created by Corrado Casoni (corrado.casoni@gmail.com), May 2025.