fault.hpp¶
Fault discipline (master plan §18.4; WS13, chunk A1) — the stable numeric fault-code enum and the latched first-fault capture.
This header declares 2 types (21 members) and 1 free function.
Extracted from include/shulib/diag/fault.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.
Contents¶
enum class FaultCodeNonePreconditionNanPoseLoopOverrunOdoStuckImuLostGpsGateRejectBrownoutMotionTimeoutMotorOverTempImplausibleMechanismStalledfaultCodeName— free functionclass FaultLatchFaultLatchraisehasFaultraiseCountfirstFaultfirstFaultTimelastFaultfaultCountclear
enum class FaultCode¶
Stable numeric fault codes (§18.4). WIRE-STABLE: explicit values, append-only — pinned by test. None (0) means "no fault" and is not raisable.
enum class, declared at include/shulib/diag/fault.hpp:44.
FaultCode::None¶
no fault (the DebugRecord default; never latched)
enumerator, declared at include/shulib/diag/fault.hpp:45.
FaultCode::Precondition¶
SHULIB_PRECONDITION violated (routed here on-robot via the check.hpp policy seam; host builds throw instead — §18.4)
enumerator, declared at include/shulib/diag/fault.hpp:46.
FaultCode::NanPose¶
a non-finite pose/quantity was caught and recovered from
enumerator, declared at include/shulib/diag/fault.hpp:48.
FaultCode::LoopOverrun¶
a control tick blew its dt budget (corrupts PID dt → §18.4)
enumerator, declared at include/shulib/diag/fault.hpp:49.
FaultCode::OdoStuck¶
odometry implausible / wheel stuck (raised by the C/E layers)
enumerator, declared at include/shulib/diag/fault.hpp:50.
FaultCode::ImuLost¶
IMU not ready / lost mid-run
enumerator, declared at include/shulib/diag/fault.hpp:51.
FaultCode::GpsGateReject¶
a GPS fix was rejected by the fusion gate (E2)
enumerator, declared at include/shulib/diag/fault.hpp:52.
FaultCode::Brownout¶
battery collapsed below the brownout threshold
enumerator, declared at include/shulib/diag/fault.hpp:53.
FaultCode::MotionTimeout¶
a motion hit its watchdog (FAULT_ABORT / TimedOut, C1/C2)
enumerator, declared at include/shulib/diag/fault.hpp:54.
FaultCode::MotorOverTemp¶
a motor crossed the thermal-throttle threshold (~55 °C) — the droop corrupts kS/kV/kA, so it must be visible (§8/§18.4; APPENDED at chunk A3, per the append-only rule above)
enumerator, declared at include/shulib/diag/fault.hpp:55.
FaultCode::Implausible¶
a physical-plausibility invariant fired: per-tick pose delta beyond the drivetrain's physical maximum, a commanded speed outside its budget, or a wheel volt inconsistent with the battery ceiling (diagnostics-plan D-5 — FiniteGuard's log-and-recover posture extended beyond finiteness; APPENDED at chunk C5, per the append-only rule above)
enumerator, declared at include/shulib/diag/fault.hpp:58.
FaultCode::MechanismStalled¶
a mechanism's stall detector tripped: stall-grade current with the shaft not turning, held past the persistence window — a jam or mechanical bind (manipulation layer, T6: the one mechanism failure that IS a pathology; an operation merely timing out raises nothing — see manipulation/mechanism_op.hpp. Lands on the CONTINUE side of the C2 abort mask by default: a jammed intake must not abort a drive. APPENDED at chunk F1, per the append-only rule above)
enumerator, declared at include/shulib/diag/fault.hpp:64.
faultCodeName¶
The §18.4 spelling of each code, for TermSink lines and the run summary. Never returns null; an out-of-range cast renders as "UNKNOWN" (never a crash).
free function, declared at include/shulib/diag/fault.hpp:77.
class FaultLatch¶
Latched first-fault capture + cascade counting (§18.4). See the header note for the root-cause rationale and the noexcept/concurrency contracts.
class, declared at include/shulib/diag/fault.hpp:97.
FaultLatch::FaultLatch¶
Both references must outlive the latch. The sink receives one Error-level line per raised fault; the clock timestamps the first fault.
function, declared at include/shulib/diag/fault.hpp:101.
FaultLatch::raise¶
Raise a fault: latch it (first-fault immutably), count it, and log one structured Error line — fault=<NAME> n=<count>[ FIRST] <detail>. Raising FaultCode::None is a defensive NO-OP (it is "no fault", and the error path must never crash — a precondition throw here would turn a bad raise into a dead robot).
function, declared at include/shulib/diag/fault.hpp:108.
FaultLatch::hasFault¶
True once ANY fault has been raised since construction/clear(), and true for the rest of the run thereafter — this is a LATCH, not a live "is something wrong right now" query, and nothing but clear() lowers it. Raising FaultCode::None is a no-op and never sets it. For triage read firstFault(): the root cause is the first fault, not the last or the loudest.
function, declared at include/shulib/diag/fault.hpp:142.
FaultLatch::raiseCount¶
How many times code has been raised since construction/clear(). ADDED at chunk C2 (additive, like the A3 MotorOverTemp append): the scheduler's fault policy must distinguish a fault raised DURING the current motion from one latched by an earlier motion — a since-clear bitmask cannot see a RE-raise (a dead encoder that faulted in motion 1 must still abort motion 2), so the latch keeps a per-code tally. Saturates at UINT16_MAX; codes beyond the fixed slot capacity (far past today's 11) count only in faultCount().
function, declared at include/shulib/diag/fault.hpp:150.
FaultLatch::firstFault¶
The ROOT CAUSE: the first fault raised since construction/clear() (None if none).
function, declared at include/shulib/diag/fault.hpp:155.
FaultLatch::firstFaultTime¶
When the first fault was raised (Time{0} if none, or if the clock threw).
function, declared at include/shulib/diag/fault.hpp:157.
FaultLatch::lastFault¶
The most recent fault in the cascade (None if none) — display only, never triage.
function, declared at include/shulib/diag/fault.hpp:159.
FaultLatch::faultCount¶
Total faults raised since construction/clear() (first + cascade).
function, declared at include/shulib/diag/fault.hpp:161.
FaultLatch::clear¶
Reset between runs. The first-fault latch is immutable WITHIN a run by design; only an explicit new-run boundary may clear it.
function, declared at include/shulib/diag/fault.hpp:165.
Design commentary, from the header¶
The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.
The header’s own reasoning — 28 lines
Fault discipline (master plan §18.4; WS13, chunk A1) — the stable numeric fault-code
enum and the latched first-fault capture. The rule this file enforces: FAULTS LOG AND
RECOVER, THEY NEVER CRASH. A NaN pose, a sensor pathology, or a loop overrun raises a
code and the run continues on a safe fallback; nothing in this file can abort an auton.
Why the FIRST fault is latched distinctly from the cascade: one root cause (say a NaN
pose) typically triggers a burst of follow-on faults (odom stuck, gate rejects, motion
timeout). At 2am you need the ROOT CAUSE, and it is the first fault, not the loudest or
the last. FaultLatch therefore records the first (code + time) immutably until clear(),
while still counting and logging every subsequent fault in the cascade.
Why the enum values are EXPLICIT and never reordered: these numbers go on the F9 wire
(the SHUL/2 serialization of DebugRecord, frozen at H1) and into SdSink blackbox files
(E1). A reorder would silently re-label historical logs. The numeric values are pinned
by test/fault_test.cpp — reordering turns the suite red. New codes are ADDED at the
end, never inserted.
Concurrency contract (the legacy logger's racing flush, designed against): FaultLatch
is owned and mutated by ONE task (the control loop). It has no background task, no
buffering, and no flush — raise() formats into a stack buffer and hands one line to the
sink synchronously on the caller's task. Cross-task use requires external serialization.
raise() is noexcept BY CONTRACT — it is the error path, so it must be unconditionally
safe to call from any failure handler. A sink or clock that throws violates its own
interface contract ("implementations MUST NOT throw"); raise() swallows such a throw
(after the latch state is already updated) rather than crashing the run over a broken
diagnostic channel. The latch always latches, even if logging fails.
Last updated 2026-08-15 — published from the main release branch, commit fa545ff. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.