ESPStateMachine is a typed, flat finite-state machine helper for ESP32 firmware. It models runtime behavior after your modules are already initialized: device modes, connection flows, update phases, guarded recovery paths, and timeout-driven fault handling.
ESPLifecycle answers "what should initialize or deinitialize, and in what order?" ESPStateMachine answers "what runtime state is this feature in, and which event is allowed to move it next?"
- Typed enum states and events via
ESPStateMachine<State, Event>. - Flat finite-state machine with one active state.
- Guarded transitions evaluated in registration order.
- Entry, exit, transition, and rejection callbacks.
- Caller-owned
void*payload passthrough for event context. - Snapshot API for diagnostics.
- Reentrant dispatch protection with deterministic
Busyresult. - Header-only core with no mandatory ESPToolKit dependencies.
- Optional adapter headers for
ESPEventBus,ESPTimer, andESPLogger.
- PlatformIO: add
https://github.com/ESPToolKit/esp-state-machine.gittolib_deps. - Arduino IDE: install as ZIP from this repository.
Optional adapters require their matching libraries:
esp_state_machine/adapters/eventbus_bridge.hrequiresESPEventBus.esp_state_machine/adapters/timer_bridge.hrequiresESPTimer.esp_state_machine/adapters/logger_observer.hrequiresESPLogger.
#include <ESPStateMachine.h>#include <ESPStateMachine.h>
enum class DeviceState : uint8_t {
Idle,
Connecting,
Online,
Fault,
};
enum class DeviceEvent : uint8_t {
Start,
Connected,
Timeout,
Reset,
};
ESPStateMachine<DeviceState, DeviceEvent> machine;
void setup() {
Serial.begin(115200);
machine.addTransition(DeviceState::Idle, DeviceEvent::Start, DeviceState::Connecting);
machine.addTransition(DeviceState::Connecting, DeviceEvent::Connected, DeviceState::Online);
machine.addTransition(DeviceState::Connecting, DeviceEvent::Timeout, DeviceState::Fault);
machine.addTransition(DeviceState::Fault, DeviceEvent::Reset, DeviceState::Idle);
machine.onTransition([](const TransitionContext<DeviceState, DeviceEvent>& ctx) {
Serial.printf("transition seq=%lu\n", static_cast<unsigned long>(ctx.sequence));
});
machine.begin(DeviceState::Idle);
machine.dispatch(DeviceEvent::Start);
}
void loop() {}- Multiple transitions for the same
(state, event)are allowed. - Matching transitions are evaluated in registration order.
- The first transition with no guard or a passing guard is taken.
- If no transition matches the current state/event pair,
dispatch(...)returnsNoTransition. - If transitions match but all guards return false,
dispatch(...)returnsGuardRejected. - Dispatch from inside callbacks returns
Busy; queue follow-up work throughESPEventBusor your own scheduler.
For a successful dispatch:
- Exit callbacks for the source state.
- Internal current state updates to the target state.
- Transition action, if configured.
- Entry callbacks for the target state.
- Machine-level transition observers.
begin(initialState) invokes entry callbacks for the initial state with bootstrap=true. end() invokes exit callbacks for the current state with shutdown=true. If end() is requested from inside a callback, shutdown is deferred until the active callback chain completes.
- Dispatch scans registered transitions linearly, and callback/observer invocation scans the registered callback lists linearly.
- Guards, actions, state callbacks, transition observers, and rejection observers are stored as
std::function; prefer registering them during setup and keep captures small on memory-constrained targets.
#include <ESPEventBus.h>
#include <ESPStateMachine.h>
#include <esp_state_machine/adapters/eventbus_bridge.h>
ESPEventBus bus;
ESPStateMachine<DeviceState, DeviceEvent> machine;
ESPStateMachineEventBusBridge<DeviceState, DeviceEvent> bridge;
void setup() {
bus.init();
bridge.attach(bus, machine);
bridge.bind(42, DeviceEvent::Connected);
}#include <ESPTimer.h>
#include <ESPStateMachine.h>
#include <esp_state_machine/adapters/timer_bridge.h>
ESPTimer timer;
ESPStateMachineTimerBridge<DeviceState, DeviceEvent> timeouts;
void setup() {
timer.init();
timeouts.addTimeout(DeviceState::Connecting, 5000, DeviceEvent::Timeout);
timeouts.attach(machine, timer);
}#include <ESPLogger.h>
#include <ESPStateMachine.h>
#include <esp_state_machine/adapters/logger_observer.h>
const char* stateName(DeviceState state);
const char* eventName(DeviceEvent event);
ESPLogger logger;
ESPStateMachineLoggerObserver<DeviceState, DeviceEvent> trace;
void setup() {
logger.init();
trace.attach(machine, logger, {
"DEVICE_FSM",
stateName,
eventName,
true,
});
}- No hierarchical or nested states.
- No parallel regions.
- No built-in persistence.
- No runtime string state/event IDs.
- No worker task inside the core.
- No payload ownership or serialization.
examples/basic_toggleexamples/guarded_branchingexamples/network_timeoutexamples/eventbus_bridgeexamples/logger_trace
bool begin(State initialState)/void end()/bool isStarted() constbool addTransition(State from, Event event, State to, TransitionOptions<State, Event> options = {})StateMachineDispatchResult<State, Event> dispatch(Event event, void* payload = nullptr)State currentState() const/StateMachineSnapshot<State> snapshot() constbool hasTransition(Event event) constStateMachineCallbackId onEnter(State state, StateCallback callback)StateMachineCallbackId onExit(State state, StateCallback callback)StateMachineCallbackId onTransition(TransitionObserver callback)StateMachineCallbackId onRejected(RejectedObserver callback)bool offCallback(StateMachineCallbackId callbackId)
include(FetchContent)
FetchContent_Declare(
esp_state_machine
GIT_REPOSITORY https://github.com/ESPToolKit/esp-state-machine.git
GIT_TAG main
)
FetchContent_MakeAvailable(esp_state_machine)
target_link_libraries(your_target PRIVATE ESPStateMachine::esp_state_machine)If the source is vendored locally, add_subdirectory(path/to/esp-state-machine) exposes the same ESPStateMachine::esp_state_machine target.
- Host-side tests in
test/test_esp_state_machinecover the core and adapter compile behavior with small stubs. - CI also builds the Arduino examples through both PlatformIO and Arduino CLI on the standard ESP32 board matrix.
cmake -S . -B build
cmake --build build
ctest --test-dir buildThis repository follows the firmware formatting baseline from esptoolkit-template:
.clang-formatis the source of truth for C/C++/INO layout..editorconfigenforces tabs (tab_width = 4), LF endings, and final newline.- Format all tracked firmware sources with
bash scripts/format_cpp.sh.
MIT - see LICENSE.md.
- Check out other libraries: https://github.com/orgs/ESPToolKit/repositories
- Hang out on Discord: https://discord.gg/WG8sSqAy
- Support the project: https://ko-fi.com/esptoolkit
- Visit the website: https://www.esptoolkit.hu/