|
Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
|
Status: Draft Date: 2026-08-02 Applies to: safeAPIFreamwork, include/safeapi/common/sapi_safestate.h, include/safeapi/os/sapi_reboot.h
CLAUDE.md bans standard library assert() in production paths, and CENELEC EN 50128/50129 requires that a SIL 3/4 application react to a detected fault by moving to a defined, known-safe condition rather than continuing in an unknown state or simply crashing. Until now, nothing in the framework defined what that reaction actually looks like or gave application code a standard way to say "this invariant just broke."
This ADR defines a small, layer-agnostic safe-state transition facility
Three levels, not one, because not every detected anomaly warrants the same response: a redundant sensor mismatch that a voting scheme can absorb is not the same event as a corrupted invariant inside the RBC core. Only DEGRADED is expected to return control to the caller; SAFE and Controlled Reboot are terminal for the calling context (see 2.4).
Three static handler slots (one per level), no dynamic allocation, consistent with every other module in the framework. This is also how an integrator plugs in a real implementation: the framework ships no built-in reaction beyond a last-resort defensive halt (2.4); the actual fail-safe action (drive outputs to their safe values, notify a supervisory channel, trigger a watchdog reset) is supplied by registering a handler for each level during system initialization.
sapi_safestate_reason_t is uint16_t. Values 0-4095 are reserved for the framework itself (sapi_safestate_reason.h-style constants such as SAPI_SAFESTATE_REASON_ASSERT_FAILED); application/RBC-specific reason codes use 4096 and above. This keeps reason codes traceable without forcing a shared central enum between the framework and every application built on it.
SAPI_REBOOT is sugar for entering SAPI_SAFESTATE_LEVEL_REBOOT. The mechanism that actually restarts the system (a watchdog trigger, a CPU reset instruction) is inherently OS/hardware-specific and lives in a new OAL service, sapi_reboot.h (section 3). src/common has no OS dependency (see ADR-002, section 4) and this ADR does not change that: sapi_safestate.c never calls into src/os. Instead, the integrator's Controlled Reboot-level handler is expected to call sapi_reboot_request() itself:
This keeps the dependency direction the same as everywhere else in the framework (os may depend on common; common never depends on os) and keeps the wiring between "a fault was detected" and "how this specific target recovers" entirely in integrator-owned code, exactly like every other OAL backend decision (see the "How will I provide my own implementation" note added to this ADR's open items, and the forthcoming backend-selection ADR referenced from ADR-001 section 7).
C99 has no portable way to guarantee a function never returns. SAFE and Controlled Reboot handlers are documented as must-not-return, but sapi_safestate_enter does not simply trust that:
This means the worst case for a misbehaving or missing handler is "the system halts" rather than "the caller continues executing past a known-unsafe condition" - the correct fail-safe default for a railway signaling context (absence of movement authority is the safe condition, not an assumption that everything is fine). DEGRADED has no such fallback: a missing DEGRADED handler simply means sapi_safestate_enter returns immediately after recording nothing, which is acceptable since DEGRADED is defined as non-terminal.
SAPI_ASSERT(cond) compiles into every build, including production - it is not compiled out via an NDEBUG-style flag. On failure it calls sapi_safestate_enter(SAPI_SAFESTATE_LEVEL_SAFE, SAPI_SAFESTATE_REASON_ASSERT_FAILED, __FILE__, __LINE__, #cond). This is deliberately the same fail-safe path a manually-written runtime fault check would use - there is no second, weaker notion of "assertion" in this codebase. This is also why CLAUDE.md's ban on standard assert() matters in practice: standard assert() calls abort() (or is compiled to nothing under NDEBUG, which is worse - it silently disables the check in release builds, exactly where SIL 3/4 code needs it most).
Function-like macros here only forward to a real function (sapi_safestate_enter) and capture __FILE__/__LINE__ at the call site, which a plain function call cannot do; they do not duplicate any argument evaluation with side effects beyond the single (cond)/(level)/(reason) expression each takes once, consistent with the macro-avoidance rationale in ADR-003 section 2.5.
Requests a controlled system restart. Backend-defined mechanism (watchdog trigger, CPU reset instruction, supervisory processor command). Documented as not expected to return on success; the skeleton stub returns SAPI_STATUS_NOT_IMPLEMENTED like every other OAL stub in this codebase. This is the 7th OAL service, extending the six defined in ADR-001 section 4.
Superseded by ADR-007. See below for the original paths; the current physical layout is include/safeapi/safestate/sapi_safestate.h + src/safestate/sapi_safestate.c (target safeapi::safestate) and include/safeapi/reboot/sapi_reboot.h + src/reboot/sapi_reboot.c (target safeapi::reboot).
include/safeapi/common/sapi_safestate.h + src/common/sapi_safestate.c (added to safeapi_common, see ADR-002/ADR-003). include/safeapi/os/sapi_reboot.h + src/os/sapi_reboot.c (added to safeapi_os, see ADR-001).