7  State Machines for Device Behavior

design-patterns
state
machine

7.1 Start With the Behavior That Must Not Be Ambiguous

A state-machine pattern earns its place when a device has behavior that prose cannot keep straight. Sleep, wake, sample, transmit, retry, fault, and recover are simple words, but a deployed device needs exact rules for moving between them.

The practical story is to make modes visible before adding cleverness. Start with the confusing behavior, turn it into named states and events, then choose the pattern that keeps illegal transitions out of the system.

In 60 Seconds

State machines make device and service behavior explicit. Instead of scattering flags through nested conditionals, a state machine defines named states, accepted events, guard conditions, transition actions, target states, timeout paths, and observable logs. The most useful IoT patterns are connection management, sampling and power management, actuator safety, and cloud-shadow reconciliation. The quality test is simple: every waiting state must have a timeout, every unsafe state must have a safe exit, and every field failure should leave a readable transition trail.

Minimum Viable Understanding
  • A state is a mode with rules. It defines what the device or service may do now and which events are valid.
  • A transition is a contract. It should name the current state, event, guard, action, target state, timeout, and log record.
  • Unexpected events are design inputs. Ignoring an event is a decision; so is moving to error, retry, safe off, or lockout.
  • Power, connection, and safety behavior are good state-machine candidates. They have clear modes, events, and failure paths.
  • A state machine is not automatically safe. It becomes safe only when transitions, timeouts, watchdogs, and recovery rules are reviewed and tested.
Chapter Roadmap

This chapter moves from messy behavior to reviewable behavior:

  • First you decide when named modes are better than flags and nested branches.
  • Then you build a transition table with states, events, guards, actions, timeouts, and logs.
  • Next you compare the reusable IoT patterns: connection, sampling and power, actuator safety, and cloud-shadow reconciliation.
  • After that you keep growing models reviewable with hierarchy, parallel regions, rejected-event handling, and testable implementation.
  • Finally you practice the review checklist, quizzes, and design record.

Checkpoints recap the contract as you go; deeper implementation details are useful but can wait on a first read.

7.2 Modes Become Safer When They Are Named

A state machine turns scattered conditions into a small set of legal modes. That matters for IoT because connection, power, command, actuator, and cloud-shadow behavior all depend on what the device or service is allowed to do right now. A device that is commissioning over BLE, reconnecting over LTE, applying a firmware update, or holding an actuator in lockout should not be interpreted through the same loose set of flags. The mode is part of the product contract.

The design quality test is whether the model rejects impossible combinations before they escape into firmware and operations. A gateway should not be both locked out and active. A sensor should not transmit forever after a missed acknowledgement. A shadow should not report success for a command the device rejected locally. These are not cosmetic diagram problems; they are the conditions that create dead batteries, duplicate commands, stale dashboards, and unsafe recovery paths.

State machines are useful when behavior depends on history. A temperature node may accept a calibration command only after it has completed warmup. A lock may accept an unlock command only after it has verified authorization, door position, and local safety state. A Matter or Bluetooth LE pairing flow may allow different events during advertising, credential exchange, verification, and operational modes. Naming those modes lets engineers ask precise questions: which events are valid here, which ones are rejected, and what trace record will explain the decision later?

  • States name legal modes such as offline, connecting, online, sampling, buffering, safe off, active, lockout, applying, and rejected.
  • Events move the model: timer expired, MQTT connected, packet acknowledged, command received, watchdog warning, battery low, or sensor fault.
  • Guards prevent unsafe transitions unless conditions such as authorization valid, battery above threshold, door closed, retry budget remaining, or command sequence accepted are true.

Good state-machine work also separates independent concerns. Connection lifecycle, sampling cycle, actuator safety, and cloud synchronization often deserve separate machines or hierarchical regions rather than one giant list of combined states. That keeps the model reviewable. It also makes the design easier to test: replay an MQTT disconnect, a sensor fault, a stale desired-property update, or a watchdog warning and confirm that the same transition contract appears in the diagram, code, tests, and logs.

The practical value is shared language. Firmware engineers can implement table-driven dispatch in C, Rust, or MicroPython. Backend engineers can model command lifecycles in services and device shadows. QA can write tests for rejected events and timeout paths. Operations can read transition logs when a field unit enters lockout. The diagram is only the start; the real product improvement is making legal behavior and failure behavior explicit enough to review.

7.3 Build The Transition Table First

Before writing firmware branches, write the transition table for one workflow. For a battery sensor, list sleep, wake, sample, transmit, buffer, and error paths, then force every waiting state to show a timeout transition. The table should include source state, event, guard, action, target state, timeout, and the log fields needed for diagnosis. If a row cannot be reviewed by firmware, cloud, QA, and support together, the model is probably still too informal.

Work from a real scenario rather than a generic drawing. For an ESP32 or nRF52 environmental sensor, the sampling cycle might start in SLEEP, wake from an RTC or GPIO interrupt, warm the sensor, read a value, validate range and checksum, publish through MQTT, buffer locally when the network is unavailable, then return to low power. The transition table should say what happens when the sensor never becomes ready, the broker acknowledgement times out, flash storage is full, battery is below threshold, or the device receives an OTA update request mid-cycle.

  • Connection: MQTT, WebSocket, BLE, cellular modem, or Matter commissioning states need bounded connect attempts and backoff with jitter.
  • Power: ESP-IDF, Zephyr, FreeRTOS, or Arduino firmware should make sleep, wake source, radio-on time, and buffer limits visible.
  • Safety: actuator transitions should move toward SAFE_OFF, EMERGENCY_STOP, or LOCKOUT on watchdog, sensor fault, overcurrent, or invalid command events.
  • Cloud shadow: AWS IoT Device Shadows, Azure Device Twins, or similar desired/reported models need rejected and stale states, not only synchronized success.

Review the table against risk. A telemetry-only node may safely drop low-value readings after a retention limit, but a medical, industrial, or building-control device may need to preserve records until acknowledged. A valve, pump, or door controller should treat local interlock state as stronger than a cloud command. A gateway that bridges Modbus, BLE, Zigbee, or LoRaWAN into MQTT should not report a cloud command as applied until the downstream device confirms it or the command enters a visible failed state.

Then turn the table into implementation and tests. In Zephyr or FreeRTOS, keep transition logic separate from driver actions so the model can be unit tested without real radios and sensors. In a backend service, store command-state transitions with command id, actor id, device id, previous state, target state, reason, and trace id. In CI, replay representative event sequences: normal sampling, offline buffering, duplicate command retry, invalid event in lockout, stale device-shadow version, and watchdog warning during an actuator action. The goal is not to prove every possible event ordering; it is to make the important transitions boring, observable, and repeatable.

Blueprint BinaCheckpoint: Modes and Tables

You now know:

  • Named states remove impossible flag combinations by making the current mode explicit.
  • A transition contract needs the event, guard, action, target, timeout path, and log fields.
  • Tests should replay normal paths, invalid events, offline buffering, stale shadows, watchdog warnings, and recovery paths.

7.4 State Is Data With Time

The implementation must carry enough context to explain a transition later: previous state, event, guard result, target state, action result, timestamp, firmware version, boot reason, command id, sequence number, and correlation id. Without that context, a field failure becomes a guess about which branch ran. With it, the team can reconstruct whether a timeout fired, a guard rejected a command, a retry budget was exhausted, or a stale cloud update arrived after the device had already moved on.

  • Timers: use monotonic timers for connection attempts, sensor warmup, acknowledgements, watchdog windows, and lockout cooldowns.
  • Persistence: persist only states that must survive reboot; recompute volatile states such as transient network connection after startup.
  • Tools: statecharts, SCXML, XState-style models, table-driven dispatch, and property-based tests can keep diagrams, code, and tests aligned.
  • Synchronization: device-shadow version, MQTT retained state, OTA rollback slot, and local event log sequence need explicit rules when cloud and device reconnect.
stateDiagram-v2
  [*] --> Offline
  Offline --> Connecting: wake or retry timer
  Connecting --> Online: MQTT session accepted
  Connecting --> Backoff: timeout or auth failure
  Backoff --> Connecting: retry budget remains
  Online --> Sampling: sample timer
  Sampling --> Publishing: reading valid
  Sampling --> Buffering: uplink unavailable
  Publishing --> Online: acknowledgement received
  Publishing --> Buffering: acknowledgement timeout
  Online --> SafeOff: watchdog or unsafe command
  SafeOff --> Lockout: repeated fault
  Lockout --> Offline: inspected reset recorded
Example device state machine with bounded retries, buffering, safety transitions, and inspected reset.

The dispatch algorithm should prefer deterministic rules over clever branching. Many production systems use a transition table keyed by state and event, with guard functions and action functions attached to each row. The dispatch step finds candidate rows, evaluates guards in a documented order, executes one idempotent action, records the transition, and rejects everything else with a structured log. That pattern works in embedded C, Rust, Python services, TypeScript backends, and test harnesses because the reviewable artifact is the table, not the syntax.

Concurrency is where informal state machines fail. A device may receive a cloud command while it is reconnecting, wake because of a local interrupt while an OTA update is pending, or reboot after writing the action but before writing the final state. The implementation needs atomic update rules for persisted state, sequence checks for command messages, idempotent actions for retry, and a startup policy that decides whether to resume, roll back, or enter a safe state. For cloud shadows and twins, desired and reported versions should be compared explicitly so an old desired property cannot overwrite a newer local decision.

A mature state machine makes normal behavior, rejected events, timeout paths, and recovery paths equally reviewable. It also admits its boundaries. State machines do not replace hardware interlocks, watchdogs, security authorization, or formal safety analysis. They make software behavior explicit enough that those other protections can be checked against it. The deeper engineering habit is to treat every missing transition as a product decision waiting to be made.

7.5 Learning Objectives

By the end of this chapter, you will be able to:

  • Choose when a state machine is a better design than scattered flags or nested conditionals.
  • Model IoT connection, sampling, actuator-safety, and cloud-shadow workflows as state machines.
  • Define transition contracts using states, events, guards, actions, targets, timeouts, and logs.
  • Explain why waiting states need timeout and error transitions.
  • Review state-machine designs for safety, power behavior, observability, and test coverage.
  • Build a table-driven state machine implementation that rejects invalid transitions clearly.
Most Valuable Understanding

A state machine is a reliability tool only when it makes failure behavior explicit. If a diagram shows the happy path but leaves out timeout, error, retry, lockout, and manual-reset transitions, it is documentation decoration, not a production design.

7.6 Prerequisites

7.7 State Machine Pattern Map

Use a state machine when behavior depends on a small set of modes and different events are valid in each mode.

A transition contract maps connection lifecycle, sampling and power, actuator safety, and cloud shadow reconciliation patterns to states, events, guards, actions, targets, timeouts, and logs
Figure 7.1: State machine pattern map for reviewing IoT device behavior

Connection Lifecycle

Models offline, connecting, online, reconnecting, degraded, and disabled modes. Use it for MQTT, cellular modems, BLE pairing, WebSocket sessions, and device provisioning.

Sampling and Power

Models sleep, wake, sample, process, transmit, buffer, and return-to-sleep behavior. Use it when battery life, wake sources, and offline buffering matter.

Actuator Safety

Models safe off, armed, active, limited, emergency stop, and lockout behavior. Use it when a software mistake can move equipment, open a valve, unlock a door, or affect a person.

Cloud Shadow Reconciliation

Models desired state, reported state, pending command, applying, rejected, and synchronized behavior. Use it when devices and cloud services can be temporarily disconnected.

7.8 Transition Contract

Every transition should be readable enough that firmware, cloud, QA, and operations teams can review it together.

A transition contract connects current state, event, guard, action, target state, timeout, and log output
Figure 7.2: A transition contract connects current state, event, guard, action, target state, timeout, and log output

Current state: The only mode where this transition is valid.

Event: The external or internal signal that asks for a transition, such as timer expired, command received, packet acknowledged, sensor fault, or watchdog warning.

Guard: A condition that must be true before the transition is allowed, such as battery above threshold, door closed, authorization valid, or retry budget remaining.

Action: The work performed during the transition, such as enabling a radio, starting a timer, writing a command record, logging a fault, or disabling an actuator.

Target state: The next valid mode. For safety paths, this should often be SAFE_OFF, ERROR, or LOCKOUT.

Timeout and log: Any state that waits must have a timeout, and every meaningful transition should leave enough trace data for field diagnosis.

TRANSITIONS = {
    ("IDLE", "sample_timer", "battery_ok"): ("WAKE_SENSOR", "power_sensor"),
    ("WAKE_SENSOR", "sensor_ready", "within_deadline"): ("SAMPLE", "read_sensor"),
    ("SAMPLE", "sample_ok", "network_available"): ("TRANSMIT", "send_message"),
    ("SAMPLE", "sample_ok", "network_unavailable"): ("BUFFER", "store_locally"),
    ("TRANSMIT", "ack_received", "always"): ("SLEEP", "schedule_next_wake"),
    ("TRANSMIT", "deadline_expired", "always"): ("BUFFER", "store_locally"),
    ("ANY", "watchdog_warning", "always"): ("SAFE_OFF", "disable_outputs"),
}

7.9 Pattern 1: Connection Lifecycle

A connection state machine protects the device from chaotic reconnect behavior and gives the cloud a clear view of whether the device is online, degraded, or intentionally disabled.

Offline Radio or client is inactive. Credentials may be missing, provisioning may be incomplete, or a retry delay may be running.

Connecting The device activates the network, starts a bounded connection attempt, and waits for success or timeout.

Online The session is established. Heartbeats, subscriptions, telemetry, and command channels are active.

Reconnecting The previous connection failed. Retry uses backoff and jitter while the device keeps local work bounded.

Degraded The device cannot reach the cloud but can still sample, buffer, or run local safety rules.

Required Transitions

connect_requested, connected, connection_timeout, heartbeat_lost, retry_timer, credentials_invalid, disable_requested, and local_mode_required.

Review Trap

Do not reconnect every device at the same fixed interval. Connection retries need backoff, jitter, and a maximum local buffer policy.

7.10 Pattern 2: Sampling and Power

Sampling state machines keep energy, latency, and data quality decisions visible. They also prevent work from happening in the wrong power mode.

Sensor duty cycling state machine showing sleep, listen, receive, and transmit states with a duty-cycle timeline and power profile
Figure 7.3: Sensor duty cycling state machine with listen, receive, transmit, and sleep states

Sleep

Only wake sources remain active. The next event may be a timer, interrupt, command, or safety alarm.

Wake

Power rails, clocks, sensors, and communication interfaces are enabled in a known order.

Sample

The device reads sensors, validates values, and applies quality checks.

Process

The device filters, compresses, aggregates, or classifies data while the active budget remains available.

Transmit

The device sends data if the network is available and a deadline remains.

Buffer

If transmission is not possible, bounded local storage preserves the sample and the device returns to sleep.

Power estimates belong in a design record using the selected hardware datasheets and measured firmware behavior. Avoid copying generic current and battery-life numbers into the state-machine pattern itself.

7.11 Pattern 3: Actuator Safety

Actuator state machines need stricter recovery rules than telemetry workflows. The safe state should be easy to enter, and dangerous states should be hard to re-enter accidentally.

Safe Off Outputs disabled. This is the default after reset, unknown state, watchdog warning, or serious fault.

Armed Preconditions are satisfied, but motion or output has not started. Guards are checked again before activation.

Active The actuator is moving or energized. Time limits, sensor checks, and emergency events are monitored continuously.

Limited The system is still operating but reduced. It may slow, derate, or move to a safer position.

Emergency Stop Output stops immediately and the fault is logged. Recovery may require inspection.

Lockout Automatic recovery is blocked. A qualified operator or explicit service workflow must reset the system.

Safety Review Rule

If a transition can energize an actuator, it needs a guard. If a fault can leave the actuator energized, it needs a hardware or independent protection path, not only application logic.

7.12 Pattern 4: Cloud Shadow Reconciliation

Cloud shadows, device twins, and desired-reported property models are state reconciliation patterns. They work best when the device treats cloud instructions as events and reports its actual state honestly.

AWS device shadow diagram showing a physical device, device shadow document, application, and reported, desired, and delta state fields
Figure 7.4: AWS device shadow state reconciliation

Desired State

The cloud records an intended configuration or command. The device should validate it before applying it.

Applying

The device has accepted a desired change and is attempting local work.

Reported State

The device publishes what actually happened, including version, timestamp, and result.

Rejected State

The device rejects an invalid, stale, unsafe, or unsupported desired state and reports why.

Never let the cloud shadow pretend an unsafe command succeeded. Report rejected and degraded states explicitly so operators can distinguish command failure from delayed connectivity.

Blueprint BinaCheckpoint: Reusable IoT Patterns

You now know:

  • Connection lifecycle machines bound retry behavior with backoff, jitter, budgets, and degraded local modes.
  • Sampling and power machines protect battery life by returning to sleep when transmit or validation fails.
  • Actuator and cloud-shadow machines must report rejected, unsafe, stale, and lockout behavior honestly.

7.13 Hierarchical and Parallel State Machines

Simple FSMs are enough for many devices. When the state count grows, use hierarchy or parallel regions rather than multiplying every combination into a new flat state.

Separate link, buffer, command, and service state machines coordinate through event contracts instead of one flat cross-product state model
Figure 7.5: Coordinating separate state machines with events

Hierarchical States

Put shared behavior in parent states. For example, CONNECTED can contain IDLE, SAMPLING, and TRANSMITTING, while connection-loss handling remains defined once at the parent level.

Parallel Regions

Keep independent concerns separate. A device may have one state machine for connectivity and another for actuator safety. Do not combine them into every possible cross-product state.

State Explosion

If every new feature doubles the state count, the model is doing too much in one flat machine.

Shared Events

Global events such as watchdog warning, shutdown, factory reset, or emergency stop should have reviewed priority over local transitions.

7.14 Implementation Pattern

A table-driven state machine is often easier to review and test than a long chain of conditional branches.

from dataclasses import dataclass


@dataclass(frozen=True)
class Transition:
    source: str
    event: str
    guard: str
    target: str
    action: str


class DeviceStateMachine:
    def __init__(self, initial_state, transitions, actions, guards):
        self.state = initial_state
        self.transitions = transitions
        self.actions = actions
        self.guards = guards

    def dispatch(self, event, context):
        candidates = [
            t for t in self.transitions
            if t.event == event and t.source in (self.state, "ANY")
        ]
        for transition in candidates:
            guard = self.guards.get(transition.guard, lambda _: False)
            if guard(context):
                previous = self.state
                self.actions[transition.action](context)
                self.state = transition.target
                context.log_transition(previous, event, self.state)
                return self.state
        context.log_rejected_event(self.state, event)
        return self.state

Why This Helps

The transition table is reviewable, testable, and easy to compare with a diagram or design record.

What It Does Not Solve

You still need good guards, timeout events, action idempotency, safe defaults, and tests for rejected events.

7.15 Architecture Review Checklist

Use this checklist before implementing firmware, cloud workflows, or device-twin reconciliation.

State inventory: Each state has a clear purpose, allowed actions, entry behavior, exit behavior, and owner.

Event inventory: External events, internal timers, watchdog warnings, cloud commands, operator actions, and sensor faults are all named.

Transition contract: Each transition defines source, event, guard, target, action, timeout, and log behavior.

Invalid events: The design says whether each invalid event is ignored, rejected, logged, escalated, or routed to safe state.

Waiting states: Every waiting state has timeout and cancellation behavior.

Safety states: Unsafe outputs default to safe off after reset, unknown state, watchdog warning, or serious fault.

Persistence: The design identifies which state is persisted across reboot and which state must be recomputed.

Observability: Transition logs include previous state, event, guard result, target state, timestamp, firmware version, and correlation ID when available.

Tests: Unit tests cover happy paths, invalid events, timeout paths, power loss, repeated commands, and recovery from persisted state.

Blueprint BinaCheckpoint: Implementation and Review

You now know:

  • Table-driven dispatch keeps diagrams, code, logs, and tests aligned around the same transition rows.
  • Rejected events are not leftovers; they are explicit behavior that must be logged or routed safely.
  • The review checklist checks ownership, events, guards, waiting states, safety states, persistence, observability, and tests before release.

7.16 Common Pitfalls

Hidden State in Flags

Many flags can create impossible combinations. Replace them with named states or separate parallel state machines.

No Timeout Transition

Waiting forever for a packet, sensor, or acknowledgement is a field failure waiting to happen.

Unsafe Automatic Recovery

Some faults should require inspection, manual reset, or a service workflow before reactivation.

Logging Only Errors

Transition history matters before the error. Log meaningful normal transitions too, at a bounded rate.

Diagram and Code Drift

If the diagram and transition table are maintained separately, they will diverge. Prefer one source of truth.

Flat Model Overgrowth

Large flat models become unreadable. Use hierarchy, parallel regions, or split machines by concern.

7.17 Summary

  • State machine patterns turn mode-dependent IoT behavior into explicit states and transitions.
  • The core reusable patterns are connection lifecycle, sampling and power, actuator safety, and cloud-shadow reconciliation.
  • A transition contract should name the current state, event, guard, action, target state, timeout, and log behavior.
  • Waiting states need timeout paths; safety states need safe defaults and reviewed recovery rules.
  • Use hierarchy or parallel regions when a flat FSM starts combining independent concerns.
  • Tests should cover invalid events, timeout paths, power loss, recovery, and safety transitions, not only the happy path.

7.18 Key Takeaway

Use a state machine whenever device behavior depends on mode, history, safety, or connectivity. Explicit states, events, guards, actions, timeouts, and rejected-event logs make field failures easier to prevent and diagnose.

7.19 Knowledge Check

7.20 Try It Yourself: State Machine Design Record

Choose one IoT workflow and complete a design record before coding.

state_machine: device-command-lifecycle
initial_state: SAFE_OFF
states:
  - SAFE_OFF
  - ARMED
  - ACTIVE
  - LIMITED
  - EMERGENCY_STOP
  - LOCKOUT
events:
  - arm_requested
  - start_requested
  - stop_requested
  - limit_exceeded
  - watchdog_warning
  - inspected_reset
transition_example:
  source: ACTIVE
  event: limit_exceeded
  guard: sensor_reading_is_valid
  action: reduce_output_and_log_fault
  target: LIMITED
  timeout: move_to_emergency_stop
invalid_event_policy:
  in_lockout: reject_and_log
observability:
  transition_log_fields:
    - previous_state
    - event
    - guard_result
    - target_state
    - timestamp
    - firmware_version
tests:
  - invalid_event_in_each_state
  - timeout_from_each_waiting_state
  - reboot_from_each_persisted_state
  - emergency_stop_priority

Review the record with someone who owns operations or safety. The best state-machine reviews find missing events before they become field failures.

7.21 References

7.22 What’s Next

Design Patterns Hub

Return to the module hub and compare state-machine patterns with the rest of the IoT architecture pattern set.

SOA Resilience Patterns

Connect timeout, fallback, circuit-breaker, and retry behavior to explicit state transitions.

WSN Duty Cycling

Deepen the sampling and power-management side of state-machine design.

Digital Twins

Apply reported and desired state ideas to cloud and device synchronization.