| 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. |
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.
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.
typedef enum {
COMM_STATE_INIT,
COMM_STATE_IDLE,
COMM_STATE_SAMPLE_STARTING,
COMM_STATE_SAMPLING,
COMM_STATE_ERROR,
COMM_STATE_RECOVERING,
} comm_state_t;
typedef struct {
comm_state_t state;
uint32_t command_fail_count;
bool data_ready;
bool communication_lost;
} comm_runtime_t;
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;
retry_tick = 0;
rt->state = COMM_STATE_RECOVERING;
}
static void comm_handle_recovering(comm_runtime_t *rt) {
comm_handle_initializing(rt);
}
{
(rt->state) {
COMM_STATE_INIT: comm_handle_initializing(rt); ;
COMM_STATE_IDLE: comm_handle_idle(rt); ;
COMM_STATE_SAMPLE_STARTING: comm_handle_sample_starting(rt);;
COMM_STATE_SAMPLING: comm_handle_sampling(rt); ;
COMM_STATE_ERROR: comm_handle_error(rt); ;
COMM_STATE_RECOVERING: comm_handle_recovering(rt); ;
:
rt->state = COMM_STATE_INIT;
;
}
(rt->command_fail_count >= COMM_MAX_FAILS) {
rt->state = COMM_STATE_ERROR;
rt->command_fail_count = ;
rt->communication_lost = ;
}
}
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.
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;
}
}
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.
static void comm_ack_check(uint32_t now_sec) {
if (!g_comm.report_in_progress) return;
if (elapsed_sec(g_comm.send_time, now_sec) < COMM_ACK_TIMEOUT_S) return;
if (g_comm.retry_count == 0) {
g_comm.retry_count++;
g_comm.send_time = now_sec;
comm_send_report();
return;
}
g_comm.report_in_progress = false;
g_comm.retry_count = 0;
comm_report_result(false);
}
Anti-Patterns
if (g_flags.busy && !g_flags.paused && g_flags.online) { ... }
if (elapsed_ms(t0, now) > TIMEOUT) { retry(); }
void retry_forever(void) {
while (!send_packet()) { delay(100); }
}
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").