← Pinprint console

Safety model

This document is normative. station/state.py implements the transition table below verbatim, and a test asserts they match. If you change one, change both.

The hazard

The dangerous operation in this system is changing a route while an output is live — a relay opening under current arcs and welds; a relay closing into a biased net back-drives a pin no profile mentions. The sweep itself is the least dangerous step. Every rule below exists to make the hazardous transition impossible in software, and to be honest when software cannot know the physical state.

States

DISCONNECTED
CONNECTED_SAFE        both drivers connected; outputs off; all routes open
CONFIGURED            a compiled plan is loaded; nothing energized
ARMED                 preflight token issued and consumed; about to run
RUNNING_ROUTING       relays may move; outputs are OFF (verified where possible)
RUNNING_MEASURING     output may be live; routes are FROZEN
COMPLETED | ABORTED   terminal, safe
FAULT_SAFE            terminal; something failed but outputs confirmed off
FAULT_UNCONFIRMED     terminal, LATCHED; output state is UNKNOWN

RUNNING_ROUTING and RUNNING_MEASURING are separate states, not flags. The mutual exclusion is by construction: there is no state in which relays may move while an output may be live.

Transition table

From Allowed to
DISCONNECTED CONNECTED_SAFE
CONNECTED_SAFE CONFIGURED, DISCONNECTED
CONFIGURED ARMED, CONNECTED_SAFE, DISCONNECTED
ARMED RUNNING_ROUTING, CONNECTED_SAFE
RUNNING_ROUTING RUNNING_MEASURING, COMPLETED, ABORTED, FAULT_SAFE, FAULT_UNCONFIRMED
RUNNING_MEASURING RUNNING_ROUTING, ABORTED, FAULT_SAFE, FAULT_UNCONFIRMED
COMPLETED CONFIGURED, CONNECTED_SAFE
ABORTED CONFIGURED, CONNECTED_SAFE
FAULT_SAFE CONNECTED_SAFE
FAULT_UNCONFIRMED DISCONNECTED (manual recovery only)

Any other transition raises InvalidTransition.

FAULT_UNCONFIRMED — why it exists

Reaching a safe state is not guaranteed. If outputs_off() times out, errors, or the instrument disconnects mid-run, the physical output state is unknown. Opening relays at that moment is exactly the hazard. Therefore:

Cancellation is not an escape hatch

Cancelling the run task (client disconnect, service shutdown, Ctrl-C) runs the same safe sequence as an abort, shielded so a second cancel cannot interrupt it: instrument abort → output-off confirmed → open all → ABORTED, or latch FAULT_UNCONFIRMED and touch nothing. The cancellation then proceeds. A cancelled run's partial bundle is finalized, never left interrupted.

Connecting cannot move a relay

MotherboardController.connect() is contractually NON-ACTUATING, and the station does not rely on that promise: the instrument is connected first and its output confirmed off before any matrix operation of any kind. A station booting into a world where an output is already live refuses to connect.

Output-state provenance

A boolean outputs_off in a log is a software assertion, not physics. Every route record and every output-off attempt records a verification status:

Status Meaning
confirmed the instrument reports its output state and we read it back off
command_acknowledged the command returned success; the instrument cannot be queried
unsupported the driver offers no acknowledgement at all
unknown the call failed or timed out

Reports must never describe a command_acknowledged or unsupported output as "proven off."

Arming

Arming is server-owned. preflight() validates every route against the matrix and every sweep against the instrument, then issues an expiring, single-use token bound to: the exact plan hash, both capability snapshots, the station connection epoch, and the module identity. execute() consumes the token under a single-run lock. A UI gesture (the 600 ms hold) may request preflight; it never is the authorization.

Known gap — hardware interlock

Software shutdown is best-effort: a kernel panic, a yanked USB cable, or a hung driver leaves software unable to guarantee anything. Real operation needs a hardware interlock or dead-man path (instrument hardware compliance at minimum). Whether the ABSMAX board interlocks route changes against load is sponsor question §03 Q7 in the buildbook and is unanswered. Until answered, treat the matrix as having no interlock.

Simulation is never silent

Simulated mode is entered explicitly, shows a persistent SIMULATED banner in any UI, is recorded in every run and event record (source_mode), and cannot be switched during a run. There is no automatic live→demo fallback anywhere.

Generated from docs/SAFETY.md at build time — that file is the source of truth.