Use when reviewing or fixing async protocols, retries, ACK/NACK handling, pending flags, timeout logic, or state-machine lockups in embedded firmware. NOT for generic network protocol design (TCP/HTTP/MQTT) unless targeting embedded firmware stack.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add AmethystLuna/embedded-workbench --skill state-machine-design --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of State Machine Design?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/amethystluna-state-machine-design)More formats (shields.io, HTML) on the badges page.
---
name: state-machine-design
description: "Use when reviewing or fixing async protocols, retries, ACK/NACK handling, pending flags, timeout logic, or state-machine lockups in embedded firmware. NOT for generic network protocol design (TCP/HTTP/MQTT) unless targeting embedded firmware stack."
---
<HARD-GATE>
This is a domain implementation skill. If you are planning, designing, or entering plan mode — load `Skill("embedded-workbench")` first to activate the workflow gates (Plan Verification Gate, Approval Gate, Closure Gate). Domain skills carry implementation guidance, not workflow enforcement.
</HARD-GATE>
# State Machine Design
## Core Rules
- Fix the state model, not the symptom. Every in-progress or pending flag must have explicit success, failure, timeout, and reset exits.
- Timeout logic must be gated on real pending work. Idle states must not trigger retry, recovery, or error transitions.
- Do not trust a low-level send return value as proof of delivery when an application-layer ACK exists. Use the protocol's completion signal.
- When adding retries, also define attempt timestamps, backoff rules, and cleanup paths so the state machine cannot lock up silently.
- If pause, stop, or reconnect can interrupt the normal flow, add an explicit recovery or re-drive branch instead of assuming the old path will naturally resume.
## Transition Gates
- When a state transition depends on multiple preconditions, verify every one explicitly at the transition gate. Do not rely on implicit assumptions (e.g., "the timer expired, therefore everything must be healthy"). A single unchecked precondition is the most common source of silent state corruption.
- If a target state's preconditions can become false while already in that state, define a reverse transition back to the source state. One-way state latches without fallback paths will eventually leak incorrect state to downstream consumers.
## Transient Tolerance
- Distinguish between genuine state-changing events and transient perturbations during mode switches, direction reversals, or re-initialization windows. The latter need a tolerance or grace window; only the former should advance the state machine or increment error counters.
## Implementation Patterns
### Pattern A: Per-State Handlers + Unified Error Gate
Each state gets its own handler function. The dispatcher is a pure `switch(state)`. A unified fault-threshold check runs **after all** state handlers — no handler triggers the error transition itself. This keeps handlers simple and fault logic centralized.
```c
// === State enum: exactly one valid state at all times ===
typedef enum {
COMM_STATE_INIT,
COMM_STATE_IDLE,
COMM_STATE_SAMPLE_STARTING,
COMM_STATE_SAMPLING,
COMM_STATE_ERROR,
COMM_STATE_RECOVERING,
} comm_state_t;
// === Runtime context: all flags explicit in one struct ===
typedef struct {
comm_state_t state;
uint32_t command_fail_count;
bool data_ready;
bool communication_lost;
} comm_runtime_t;
// === Per-state handlers: each reads only what it needs ===
static void comm_handle_idle(comm_runtime_t *rt) {
rt->warmup_start_time = sys_tick();
comm_start_sample();
rt->state = COMM_STATE_SAMPLE_STARTING;
}
static void comm_handle_error(comm_runtime_t *rt) {
static uint32_t retry_tick = 0;
if (retry_tick == 0) {
retry_tick = sys_tick();
rt->data_ready = false;
}
comm_power_off();
if (sys_tick() - retry_tick < 500) return; // 500ms cooldown
retry_tick = 0;
rt->state = COMM_STATE_RECOVERING;
}
static void comm_handle_recovering(comm_runtime_t *rt) {
comm_handle_initializing(rt); // Recovery RE-USES init — no duplicated paths
}
// === Dispatcher: pure switch, single exit ===
static void comm_state_process(comm_runtime_t *rt) {
switch (rt->state) {
case COMM_STATE_INIT: comm_handle_initializing(rt); break;
case COMM_STATE_IDLE: comm_handle_idle(rt); break;
case COMM_STATE_SAMPLE_STARTING: comm_handle_sample_starting(rt);break;
case COMM_STATE_SAMPLING: comm_handle_sampling(rt); break;
case COMM_STATE_ERROR: comm_handle_error(rt); break;
case COMM_STATE_RECOVERING: comm_handle_recovering(rt); break;
default:
rt->state = COMM_STATE_INIT; // Unknown state → safe fallback
break;
}
// Unified error gate: checked AFTER every state, not buried inside handlers.
// A new state cannot accidentally bypass this check.
if (rt->command_fail_count >= COMM_MAX_FAILS) {
rt->state = COMM_STATE_ERROR;
rt->command_fail_count = 0;
rt->communication_lost = true;
}
}
```
Key properties:
- **Fault logic is centralized** — the error gate runs exactly once, after every state. New states cannot bypass it.
- **Recovery reuses init** — `comm_handle_recovering()` calls `comm_handle_initializing()`. No duplicated paths to drift apart.
- **All exits are explicit** — `Error` has a cooldown period (500ms), then transitions to `Recovering`. No fall-through, no implicit assumption.
- **Unknown state → safe fallback** — the `default` case resets to `Init`.
### Pattern B: Function-Pointer Table Dispatch
Heavier than switch-case, but useful when states are added/removed frequently or handlers need different signatures.
```c
static const struct {
comm_state_t state;
void (*process)(void);
} comm_state_table[] = {
{COMM_STATE_INIT, comm_init_process},
{COMM_STATE_IDLE, comm_idle_process},
{COMM_STATE_CONNECTED, comm_connected_process},
{COMM_STATE_ERROR, comm_error_process},
{COMM_STATE_RECOVERING, comm_recovering_process},
};
void comm_state_dispatch(void) {
for (size_t i = 0; i < ARRAY_LEN(comm_state_table); i++) {
if (g_comm_runtime.state == comm_state_table[i].state
&& comm_state_table[i].process != NULL) {
comm_state_table[i].process();
return;
}
}
// Unknown state: reset to safe default
g_comm_runtime.state = COMM_STATE_INIT;
}
```
### Pattern C: ACK Timeout With Explicit Retry Limit
All core rules in one function: timeout gated only when work is pending, explicit retry count, predefined max retries, all exits defined.
```c
static void comm_ack_check(uint32_t now_sec) {
// GUARD: timeout logic only runs when there is real pending work
if (!g_comm.report_in_progress) return;
// GUARD: timeout hasn't expired yet
if (elapsed_sec(g_comm.send_time, now_sec) < COMM_ACK_TIMEOUT_S) return;
// Timeout fired. Explicit retry branch:
if (g_comm.retry_count == 0) {
g_comm.retry_count++;
g_comm.send_time = now_sec;
comm_send_report(); // One automatic retry
return;
}
// All retries exhausted → terminal exit
g_comm.report_in_progress = false;
g_comm.retry_count = 0; // Reset for next cycle
comm_report_result(false); // Notify caller: failed
}
```
### Anti-Patterns
```c
// BAD: implicit state via flags — new flag creates untested state combinations
if (g_flags.busy && !g_flags.paused && g_flags.online) { ... }
// Fix: use explicit enum — exactly one valid state at all times
// BAD: idle state triggers timeout — retry fires with nothing pending
if (elapsed_ms(t0, now) > TIMEOUT) { retry(); }
// t0 is always running, even when no work is in flight
// BAD: retry loop with no exit condition
void retry_forever(void) {
while (!send_packet()) { delay(100); } // Will lock up if HW is dead
}
// BAD: recovery path duplicates init logic instead of reusing it.
// The copy drifts over time — one path gets a fix, the other doesn't.
// BAD: Error handler directly calls power_off() without cooldown period.
// Power-cycling faster than the hardware spec causes unpredictable state.
```
## When To Escalate
- When diagnostics point to an architecture-level or state-machine design defect, proactively offer high-level remediation focused on boundary clarity, lifecycle contracts, and reversible transitions — don't just propose ad-hoc runtime patches.
**REQUIRED SUB-SKILL:** If you find a state machine bug, also load `Skill("debug-methodology")` to apply structured root-cause analysis. If the bug involves async lifecycle flags or hardware events, load `Skill("embedded-firmware-dev")`. If the state machine lockup triggers a watchdog reset or HardFault, load `Skill("hardfault-triage")`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!