Code Craft

State Machines for Device Behavior

How to model a device as states and transitions so control code stays manageable, with a simple example, common pitfalls and tips for testing the logic.

On this page
  1. The problem they solve
  2. The three ingredients
  3. A small example
  4. Draw it first
  5. Implementing it
  6. Handling safety and faults
  7. Testing state logic
  8. When a simple one is enough, and when it is not

The problem they solve

A device that only does one thing is easy to program. The trouble starts when it has modes: waiting, heating, cooling down, reporting a fault, being configured. Control code often grows into a thicket of flags and nested conditions, such as “if the button was pressed and not already running and the timer is not finished”. Each new feature adds more flags, and eventually nobody can say what the device does in a given situation without running it.

A state machine is a way of replacing that tangle with a simple picture: the device is always in exactly one state, and specific events cause it to move to another.

The three ingredients

  • States are the situations the device can be in. For a kettle-like heater these might be Idle, Heating, Holding and Fault.
  • Events are things that happen: a button press, a temperature reaching a threshold, a timer expiring, a sensor failing.
  • Transitions say what happens when a given event occurs in a given state: which state comes next, and what action to take on the way.

Often the actions are attached to entering or leaving a state, such as switching an output on when entering Heating and off when leaving it. That keeps the effects of each state in one place.

A small example

Imagine a plant-watering controller with a pump. A sensible set of states is:

  • Waiting: the pump is off, and the device checks soil moisture at intervals.
  • Watering: the pump is on for a fixed time.
  • Resting: the pump is off and the device ignores the moisture reading for a while, so water has time to soak in.
  • Fault: the pump stays off and an indicator shows a problem.

The transitions are easy to state. From Waiting, a “soil too dry” event leads to Watering. From Watering, “time elapsed” leads to Resting. From Resting, “time elapsed” leads to Waiting. From any state, a “sensor reading invalid” event leads to Fault.

Notice what the structure gives you for free. The pump can only be on in one state, so it is hard to leave it running by accident. The Resting state prevents the device from flooding the pot, which would otherwise need extra flags. And the Fault state makes failure behaviour a deliberate design decision.

Draw it first

Before writing code, sketch circles for the states and arrows for the transitions, labelling each arrow with its event. A drawing on paper is enough. Walking through the diagram with imagined events quickly shows gaps: what happens if the button is pressed during Resting? What if the power drops in the middle of Watering? Questions that would have surfaced as bugs appear while changing a drawing costs nothing.

Implementing it

You do not need a library. A common pattern keeps the current state in a variable and has a function that reacts to events, deciding by state what to do. Some people write this with a switch statement over the state, others with a table of transitions. The details vary by language, but the principle is the same: one variable names the state, and each state handles only the events that matter to it.

A few habits help:

  • Use named states, such as an enumeration, instead of bare numbers.
  • Put the code for each state’s behaviour in one place, rather than scattering checks through the program.
  • Decide what each state does with events it does not expect. Ignoring them explicitly, or treating them as faults, is better than silently doing something random.
  • Make timing explicit. Use a clock-based timer that records when a state was entered, instead of blocking delays, so the device can keep responding to other events. This also connects with the way a robot control loop repeatedly senses, decides and acts.

Handling safety and faults

States are a natural home for safe behaviour. Define a safe state, one where outputs are off or harmless, and make sure faults lead there. For machines that move or heat, a hardware emergency stop should not depend on your software at all, and the state machine is a complement to that, not a replacement. When power-related hardware is involved, follow the manufacturer’s guidance and local rules.

Think also about startup. What state should the device be in when it powers on, and what if it restarts unexpectedly in the middle of an operation? Starting in a known safe state is usually the right choice.

Testing state logic

State machines are pleasant to test because they are mostly pure logic. If the transition function takes the current state and an event and returns the next state, you can check it on an ordinary computer without any hardware. List the expected transitions, feed in the events and compare. This is a good use of simple automated testing, and it catches mistakes when you add a state later.

When a simple one is enough, and when it is not

A device with two modes may not need formal states. As soon as you notice several boolean flags interacting, or you have trouble describing behaviour in words, consider switching to states. For large systems, extensions exist, such as hierarchical states where some events are handled by a parent state, but a plain machine with a handful of states carries most hobby projects a long way.

Keeping behaviour in a clear diagram also helps others understand your device, and helps you when you return to it after months away.

The complete guide to code craft

Software Habits Every Maker Should Learn
Read the guide

Keep exploring

Guides from other topics

All topics →