1. Today's topic
A finite-state machine, or FSM, describes a subsystem as a set of permitted states, events and transitions:
state + event + guard -> next_state + actionImportant concepts:
- state — a subsystem's operating mode: WAIT_AT, ONLINE, RECOVERY_BACKOFF.
- event — something that has already happened: AT_OK, TIMEOUT, POWER_WARN.
- guard — a transition condition: does transaction_id match, and is the modem generation current?
- action — work to perform outside the machine: send an AT command, start a timer, reset EC25.
- entry/exit actions — actions performed once on entering or leaving a state.
- invariant — a condition that must be true in a state.
The main idea: one owner must change a subsystem's state. ISRs, the UART parser, CLI and timers must not write s_modem.state directly; they must send events to the owner's queue.
2. Why this matters in your projects
For EC25, state quickly turns into a collection of incompatible flags:
bool modem_ready;
bool network_registered;
bool pdp_active;
bool mqtt_connected;
bool reset_in_progress;This model allows impossible combinations. An FSM makes the permitted states explicit:
OFF
WAIT_BOOT
WAIT_AT
WAIT_REGISTRATION
WAIT_PDP
WAIT_MQTT
ONLINE
RECOVERY_BACKOFF
DEGRADEDFor traffic-light phases, an FSM helps describe transitions:
UNKNOWN -> CANDIDATE_RED -> STABLE_RED -> CANDIDATE_GREEN -> STABLE_GREEN
UNKNOWN -> CONFLICT
STABLE_* -> INPUT_LOSTFor OTA, an FSM separates different classes of failure:
IDLE -> PRECHECK -> MANIFEST -> DOWNLOAD -> VERIFY -> SET_BOOT -> REBOOT_PENDING
PENDING_CONFIRMATION -> CONFIRMED
PENDING_CONFIRMATION -> ROLLBACKA DOWNLOAD failure leaves the current firmware in place, whereas a PENDING_CONFIRMATION failure must lead to rollback.
3. Theory
A state is a mode of behaviour
Poor state names:
MODEM_HAS_RESPONSE
TIMEOUT_OCCURRED
MQTT_ERRORThese are events or facts rather than operating modes. Good state names:
MODEM_WAIT_BOOT
MODEM_WAIT_AT
MODEM_WAIT_REGISTRATION
MODEM_ONLINE
MODEM_RECOVERY_BACKOFFA state answers the question: which events are currently expected, and which actions are permitted?
An event is a fact
Good events:
MODEM_EVT_START
MODEM_EVT_AT_OK
MODEM_EVT_AT_ERROR
MODEM_EVT_TIMEOUT
MODEM_EVT_REGISTERED
MODEM_EVT_MQTT_CONNECTED
MODEM_EVT_POWER_WARNPoor events:
TRY_MQTT
DO_RECOVERY
CHECK_MODEMThese look more like actions.
The FSM core must not perform side effects
The FSM must return actions rather than operate UART/GPIO/NVS directly:
typedef enum {
MODEM_ACT_NONE = 0,
MODEM_ACT_POWER_ON,
MODEM_ACT_SEND_AT,
MODEM_ACT_QUERY_REGISTRATION,
MODEM_ACT_CONNECT_MQTT,
MODEM_ACT_RESET_HARDWARE,
MODEM_ACT_START_DEADLINE,
MODEM_ACT_REPORT_ONLINE,
} modem_action_type_t;
typedef struct {
modem_action_type_t type;
uint32_t argument;
} modem_action_t;The service layer then performs these actions through the actual drivers.
Guards protect against late events
After an EC25 reset, an old response may arrive late. An event must therefore carry generation/transaction context:
typedef struct {
modem_event_type_t type;
uint32_t transaction_id;
uint32_t modem_generation;
int32_t error;
uint64_t timestamp_us;
} modem_event_t;An event from another generation must not change the new state.
A timeout is an ordinary event
Do not call vTaskDelay() inside a state transition. Set a deadline when entering the state; the timer later sends MODEM_EVT_TIMEOUT.
WAIT_AT entry -> SEND_AT + START_DEADLINE(2000 ms)
TIMEOUT event -> RECOVERY_BACKOFFThis allows the FSM to keep receiving POWER_WARN, STOP, RESET and other events.
Invariants
The following facts must be true for ONLINE:
powered=true
at_ready=true
registered=true
pdp_active=true
mqtt_connected=trueCheck invariants after every transition. In debug builds this may be an assert; in production, a fault event and recovery.
4. Common mistakes
- Several tasks change the same state directly.
- The FSM executes a blocking AT command.
- A timeout is implemented using vTaskDelay().
- state, event and action share the same name, for example MODEM_RECONNECT.
- Dozens of independent bool values create impossible states.
- A side effect runs before the transition is committed.
- A late OK from an old AT command completes a new transaction.
- Unknown events are silently ignored.
- Recovery has no limit or backoff.
- An entry action runs on every cycle, sending AT commands hundreds of times.
5. A practical task for 30–60 minutes
Create STATE_MACHINE_POLICY.md:
# State machine policy
1. Every subsystem state has one owner task.
2. Hardware callbacks only post events.
3. State transitions never block.
4. Timeouts are events based on monotonic deadlines.
5. External side effects are returned as actions.
6. Every transaction has an ID or generation.
7. Invalid transitions are diagnosed.
8. Recovery has retry limits and backoff.
9. State invariants are checked after every transition.
10. FSM core is host-testable without RTOS/HAL.A minimal state list for EC25:
typedef enum {
MODEM_STATE_OFF = 0,
MODEM_STATE_WAIT_BOOT,
MODEM_STATE_WAIT_AT,
MODEM_STATE_WAIT_REGISTRATION,
MODEM_STATE_WAIT_PDP,
MODEM_STATE_WAIT_MQTT,
MODEM_STATE_ONLINE,
MODEM_STATE_RECOVERY_BACKOFF,
MODEM_STATE_DEGRADED,
} modem_state_t;Write unit tests:
1. The normal path to ONLINE.
2. WAIT_AT timeout -> RECOVERY_BACKOFF.
3. A late AT_OK after reset is ignored based on generation.
4. An AT_OK with another transaction_id does not complete the current command.
5. MQTT_DISCONNECTED in ONLINE returns to WAIT_MQTT, but does not reset the MCU.
6. Loss of registration clears PDP/MQTT facts.
7. Repeated timeout events beyond the limit move to DEGRADED.
8. A duplicate MQTT_CONNECTED in ONLINE does not create another transition.6. What to read or try next
- ESP-IDF Event Loop Library: custom event loops and handler profiling.
- FreeRTOS queues and task notifications.
- CMSIS-RTOS2 Message Queue and Thread Flags.
- Host unit tests for an FSM without HAL/RTOS.
Criteria: Check both event context and the one-owner rule.
Exercise
Describe a non-blocking WAIT_AT transition: what happens on entry, who performs the UART operation, and how does the timeout reach the FSM?
Self-check criteria: Identify the owner, returned actions, service-layer side effects and timeout event. Keep blocking work out of the FSM core.
Show the supplied answer
On entry, the FSM returns SEND_AT and START_DEADLINE actions. The service layer performs them. The timer posts MODEM_EVT_TIMEOUT to the owner queue when the monotonic deadline expires; the transition itself does not call vTaskDelay().