Skip to main content

State Machine Architecture

The OSSM firmware uses a declarative state machine to manage device behavior. This ensures predictable transitions between operating modes and prevents invalid states that could cause unexpected behavior.

State Diagram

The following diagram shows all states and transitions in the OSSM state machine:

Design Overview

The state machine is implemented using Boost.SML (State Machine Language), a header-only C++14 library that provides a domain-specific language for defining state machines.

Why Boost.SML?

Boost.SML was chosen for OSSM because it offers:
  • Compile-time verification - Invalid transitions are caught at compile time, not runtime
  • Zero runtime overhead - Performance equivalent to hand-written switch/case
  • Declarative syntax - The transition table reads like documentation
  • Thread safety - Built-in support for concurrent access via policies
  • Small footprint - Single header, ~2000 lines of code, no dependencies

Transition Table Syntax

Each row in the transition table follows this pattern:
source_state + event [guard] / action = target_state
For example:
"menu.idle"_s + buttonPress[(isOption(Menu::SimplePenetration))] = "simplePenetration"_s
This reads as: “When in menu.idle state and a buttonPress event occurs, if the guard isOption(Menu::SimplePenetration) returns true, transition to simplePenetration state.”
For a complete tutorial on transition table syntax, see the Boost.SML Tutorial.

Events

Events trigger state transitions. They are simple structs defined in Events.h and can be dispatched from anywhere in the codebase.
EventDescriptionTypical Source
ButtonPressSingle button clickRotary encoder button
LongPressButton held for extended durationRotary encoder button (held)
DoublePressTwo rapid button clicksRotary encoder button
DoneAsync operation completedHoming task, preflight check
ErrorOperation failedHoming failure, stroke too short
EmergencyStopImmediate halt requiredSafety systems
HomeRequest homing sequenceExternal command

Dispatching Events

Events are dispatched using the global stateMachine instance:
#include "ossm/state/state.h"

// From anywhere in the codebase
stateMachine->process_event(ButtonPress{});
stateMachine->process_event(Done{});
The state machine is initialized at startup in state.cpp and exposed as a global pointer.
Events are defined as empty structs. The type itself carries the meaning - no data payload is needed.

Guards

Guards are conditional checks that determine whether a transition should occur. They return true to allow the transition or false to block it.
GuardDescriptionReturns true when…
isOnlineCheck WiFi connectionDevice is connected to WiFi
isUpdateAvailableCheck for firmware updatesServer reports newer version available
isStrokeTooShortValidate homing resultMeasured stroke is below minimum threshold
isOption(Menu)Check selected menu itemCurrent menu selection matches the specified option
isPreflightSafeValidate speed knob positionSpeed potentiometer is in the dead zone (safe to start)
isFirstHomedOne-time initial homing checkThis is the first successful homing since boot
isNotHomedCheck homing statusDevice has not been homed or homing was invalidated

Guard Implementation

Guards are defined as constexpr lambdas in the guards namespace. They call forward-declared implementation functions to keep the header lightweight:
// In guards.h - forward declaration
bool ossmIsPreflightSafe();

namespace guards {
    // Constexpr lambda wraps the implementation
    constexpr auto isPreflightSafe = []() { 
        return ossmIsPreflightSafe(); 
    };
    
    // Parameterized guard using nested lambda
    constexpr auto isOption = [](Menu option) {
        return [option]() { return ossmGetMenuOption() == option; };
    };
}
The actual logic lives in guards.cpp, keeping compile times fast and allowing implementation changes without recompiling the transition table.
For more on guard patterns, see the Boost.SML Guards documentation.

Actions

Actions are functions executed during state transitions. They perform side effects like updating the display, starting motors, or resetting settings.

Display Actions

ActionDescription
drawHelloShow welcome/boot screen
drawMenuRender the main menu
drawPlayControlsShow speed/stroke/depth controls
drawPatternControlsShow pattern selection UI
drawPreflightShow “reduce speed to start” warning
drawHelpDisplay help/support information
drawWiFiShow WiFi configuration screen
drawUpdateShow “checking for updates” screen
drawNoUpdateShow “firmware is up to date” message
drawUpdatingShow update progress
drawErrorDisplay error state with message

Motion Actions

ActionDescription
startHomingBegin the homing sequence
clearHomingReset homing state variables
startSimplePenetrationStart Simple Penetration mode task
startStrokeEngineStart Stroke Engine mode task
startStreamingStart Streaming mode task
emergencyStopForce stop motor and disable outputs

Settings Actions

ActionDescription
resetSettingsStrokeEngineInitialize defaults for Stroke Engine (speed=0, stroke=50, depth=10, sensation=50)
resetSettingsSimplePenInitialize defaults for Simple Penetration (speed=0, stroke=0, depth=50)
incrementControlCycle through control parameters (stroke → depth → sensation)
setHomedMark device as successfully homed
setNotHomedInvalidate homing (requires re-home before play)

System Actions

ActionDescription
restartRestart the ESP32
resetWiFiClear saved WiFi credentials
updateOSSMDownload and install firmware update

Action Implementation

Actions are defined as constexpr lambdas in the actions namespace. Like guards, they delegate to forward-declared implementation functions:
// In actions.h - forward declarations
void ossmDrawHello();
void ossmEmergencyStop();

namespace actions {
    constexpr auto drawHello = []() { ossmDrawHello(); };
    constexpr auto emergencyStop = []() { ossmEmergencyStop(); };
}
The implementation functions in actions.cpp call into the appropriate feature modules:
// In actions.cpp
void ossmEmergencyStop() {
    stepper->forceStop();
    stepper->disableOutputs();
}

void ossmDrawHello() {
    pages::drawHello();  // Delegates to pages namespace
}
This pattern keeps the state machine decoupled from specific implementations and allows feature modules to be developed independently.
Actions must not block the main thread. Long-running operations should spawn FreeRTOS tasks instead.
For more on action patterns, see the Boost.SML Actions documentation.

Thread Safety

The OSSM state machine is configured with thread-safe policies to handle events from multiple FreeRTOS tasks:
sml::sm<OSSMStateMachine, sml::thread_safe<ESP32RecursiveMutex>, sml::logger<StateLogger>>
This uses a recursive mutex to protect state transitions, allowing safe event dispatch from:
  • The main loop
  • Button interrupt handlers
  • BLE command handlers
  • Background tasks
The state machine is initialized in state.cpp:
// Global state machine instance
StateMachine* stateMachine = nullptr;

void initStateMachine() {
    stateMachine = new StateMachine{};
    stateMachine->process_event(Done{});  // Trigger initial transition
}
For more on thread safety policies, see the Boost.SML Policies documentation.

State Logging

The firmware includes a StateLogger that logs all state transitions for debugging:
sml::logger<StateLogger>
This outputs transition information via ESP-IDF logging, making it easy to trace state machine behavior during development.

Architecture Notes

The state machine follows a decoupled modular architecture:
  • State machine definition (machine.h) contains only the transition table
  • Actions and guards are constexpr lambdas that delegate to implementation functions
  • Feature modules (like pages::, simple_penetration::, stroke_engine::) contain the actual logic
  • Global state structs manage application state instead of class members
The OSSM class in ossm/OSSM.h is retained for backward compatibility with BLE command handling. New features should use stateless namespace functions that operate on global state.
For a complete overview of the source code organization, see Folder Structure.

Further Reading