ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
failsafe.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file failsafe.h
6 * @brief Software watchdog: deadlock detection + fail-safe safe-state (PROTOCORE_ENABLE_FAILSAFE).
7 *
8 * A fixed registry of "lifelines" - a task, worker, or control loop that must check in
9 * (`protocore_failsafe_feed`) at least every `deadline_ms`. If one stops checking in (a hang, a
10 * deadlock, a wedged loop), `protocore_failsafe_check()` detects it and fires a breach callback exactly
11 * once per stuck episode, so the app can drive its outputs to a known-safe state (motors off, valves
12 * closed), log, and optionally reset. It complements the hardware task watchdog: this one is
13 * app-defined, per-lifeline, and knows *which* subsystem wedged.
14 *
15 * Zero heap (a static registry), no stdlib. The overdue test is a wrap-safe unsigned time delta, so it
16 * is correct across a `millis()` rollover. The evaluation core takes an explicit `now`, so it is fully
17 * host-testable with a synthetic clock; the no-`now` wrappers read the pluggable `protocore_millis()`.
18 */
19
20#ifndef PROTOCORE_FAILSAFE_H
21#define PROTOCORE_FAILSAFE_H
22
23#include "protocore_config.h" // the entry point: protocore_types.h for the widths
24
25#if PROTOCORE_ENABLE_FAILSAFE
26
28
29/** @brief One monitored lifeline. */
30typedef struct
31{
32 const char *name; ///< label (for the breach callback + JSON); not copied.
33 uint32_t deadline_ms; ///< max interval between feeds before it is considered stuck.
34 uint32_t last_feed_ms; ///< time of the last check-in (protocore_millis units).
35 proto_bool armed; ///< slot in use.
36 proto_bool breached; ///< currently in breach (so the callback fires once per episode).
37} protocore_lifeline;
38
39/** @brief Breach callback: invoked once when @p id (named @p name) misses its deadline. */
40typedef void (*protocore_failsafe_cb)(int id, const char *name, void *arg);
41
42// ---------------------------------------------------------------------------
43// Host-testable core
44// ---------------------------------------------------------------------------
45
46/**
47 * @brief Is a lifeline overdue at @p now?
48 *
49 * Wrap-safe: the unsigned delta `now - last_feed` is correct across a millis() rollover as long as the
50 * true gap is under 2^32 ms (~49 days), which any real deadline is.
51 */
52static inline proto_bool protocore_lifeline_overdue(uint32_t now, uint32_t last_feed_ms, uint32_t deadline_ms)
53{
54 return (uint32_t)(now - last_feed_ms) > deadline_ms;
55}
56
57// ---------------------------------------------------------------------------
58// Registry API
59// ---------------------------------------------------------------------------
60
61/** @brief The lifeline a call names, and what arming it takes. */
62typedef struct
63{
64 const char *name; ///< the lifeline's label; a persistent string
65 uint32_t deadline_ms; ///< how long it may go unfed before it is overdue
66 int id; ///< the lifeline a feed names
67 uint32_t now; ///< the caller's clock, so the module stays testable against a synthetic one
68} FailsafeArgs;
69
70/** @brief What a breach fires, and where a report is written. */
71typedef struct
72{
73 protocore_failsafe_cb cb; ///< what a breach fires; NULL leaves the callback off
74 void *arg; ///< the opaque pointer it is given back
75 char *out; ///< where the JSON lands
76 size_t cap; ///< how much room it has
77} FailsafeOutArgs;
78
79/**
80 * @brief The lifeline watchdog.
81 *
82 * A caller sets the members a call takes, invokes it through ::Failsafe, and reads the outcome off
83 * the same handle. The lifeline table is behind @ref internal.
84 *
85 * @var FailsafeNs::args the lifeline a call names, and what arming it takes
86 * @var FailsafeNs::out_args what a breach fires, and where a report is written
87 * @var FailsafeNs::ok a call's true/false outcome
88 * @var FailsafeNs::i32 the lifeline a register took, or < 0 when the table is full
89 * @var FailsafeNs::breached one bit per lifeline that went overdue on this check
90 * @var FailsafeNs::n characters a report wrote
91 * @var FailsafeNs::reset clear every lifeline and drop the breach callback
92 * @var FailsafeNs::add arm a lifeline against args.now; it starts fed
93 * @var FailsafeNs::feed check one in against args.now
94 * @var FailsafeNs::on_breach install what a breach fires
95 * @var FailsafeNs::check judge every armed lifeline against args.now
96 * @var FailsafeNs::json report every armed lifeline
97 *
98 * Every call takes the clock as @c args.now rather than reading one, so the whole module runs
99 * against a synthetic clock on the host. A feed clears the breach so the lifeline can fire again.
100 */
101typedef struct
102{
103 FailsafeArgs args;
104 FailsafeOutArgs out_args;
105 proto_bool ok;
106 int i32;
107 uint32_t breached;
108 int n;
109} FailsafeVars;
110
111/** @brief The operands and the outcome. */
112extern FailsafeVars FailsafeV;
113
114/** @brief The entries. */
115typedef struct
116{
117 void (*const reset)(uint8_t *work);
118 void (*const add)(uint8_t *work);
119 void (*const feed)(uint8_t *work);
120 void (*const on_breach)(uint8_t *work);
121 void (*const check)(uint8_t *work);
122 void (*const json)(uint8_t *work);
123} FailsafeNs;
124
125// What the table binds, defined once in the .c and taking one parameter each: everything
126// else an entry needs is an operand in FailsafeV or a region of the borrow at a fixed offset.
127void protocore_failsafe_reset(uint8_t *work);
128void protocore_failsafe_add(uint8_t *work);
129void protocore_failsafe_feed(uint8_t *work);
130void protocore_failsafe_on_breach(uint8_t *work);
131void protocore_failsafe_check(uint8_t *work);
132void protocore_failsafe_json(uint8_t *work);
133
134// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
135// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
136// `Failsafe.reset(work)` resolves to a named function and becomes a DIRECT call. An extern table
137// leaves the call indirect and the symbol live at every level, -O2 -flto included.
138static const FailsafeNs Failsafe __attribute__((unused)) = {
139 .reset = protocore_failsafe_reset,
140 .add = protocore_failsafe_add,
141 .feed = protocore_failsafe_feed,
142 .on_breach = protocore_failsafe_on_breach,
143 .check = protocore_failsafe_check,
144 .json = protocore_failsafe_json,
145};
146
147/**
148 * @brief The PROTOCORE_FAILSAFE_BORROW bytes this module's state lives in.
149 *
150 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
151 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
152 * walks, so the state lasts the life of the program.
153 *
154 * @return the span.
155 */
156uint8_t *protocore_failsafe_span(void);
157
159
160#endif // PROTOCORE_ENABLE_FAILSAFE
161
162#endif // PROTOCORE_FAILSAFE_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97