ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
failsafe.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file failsafe.cpp
6 * @brief Software watchdog / deadlock detection + safe-state (see failsafe.h).
7 */
8
10
11#if PC_ENABLE_FAILSAFE
12
13#include "services/system/clock.h" // pc_millis() - the pluggable monotonic clock
14
15namespace
16{
17// All failsafe state, owned by one instance (internal linkage via the anon namespace):
18// grouped for auditability, unreachable from any other translation unit.
19struct FailsafeCtx
20{
21 pc_lifeline lines[PC_FAILSAFE_MAX_LIFELINES];
22 pc_failsafe_cb cb = nullptr;
23 void *cb_arg = nullptr;
24};
25FailsafeCtx s_fs;
26
27// Minimal unsigned -> decimal, no stdlib; returns chars written.
28size_t u32_dec(uint32_t v, char *out)
29{
30 char tmp[10];
31 size_t n = 0;
32 do
33 {
34 tmp[n++] = (char)('0' + v % 10);
35 v /= 10;
36 } while (v);
37 for (size_t i = 0; i < n; i++)
38 {
39 out[i] = tmp[n - 1 - i];
40 }
41 return n;
42}
43} // namespace
44
45void pc_failsafe_reset(void)
46{
47 for (int i = 0; i < PC_FAILSAFE_MAX_LIFELINES; i++)
48 {
49 s_fs.lines[i] = pc_lifeline{};
50 }
51 s_fs.cb = nullptr;
52 s_fs.cb_arg = nullptr;
53}
54
55int pc_failsafe_register_at(const char *name, uint32_t deadline_ms, uint32_t now)
56{
57 for (int i = 0; i < PC_FAILSAFE_MAX_LIFELINES; i++)
58 {
59 if (!s_fs.lines[i].armed)
60 {
61 s_fs.lines[i].name = name;
62 s_fs.lines[i].deadline_ms = deadline_ms;
63 s_fs.lines[i].last_feed_ms = now; // starts fed, so it is not instantly overdue
64 s_fs.lines[i].armed = true;
65 s_fs.lines[i].breached = false;
66 return i;
67 }
68 }
69 return -1;
70}
71
72int pc_failsafe_register(const char *name, uint32_t deadline_ms)
73{
74 return pc_failsafe_register_at(name, deadline_ms, pc_millis());
75}
76
77bool pc_failsafe_feed_at(int id, uint32_t now)
78{
79 if (id < 0 || id >= PC_FAILSAFE_MAX_LIFELINES || !s_fs.lines[id].armed)
80 {
81 return false;
82 }
83 s_fs.lines[id].last_feed_ms = now;
84 s_fs.lines[id].breached = false; // a fresh check-in clears the breach so it can fire again next time
85 return true;
86}
87
88bool pc_failsafe_feed(int id)
89{
90 return pc_failsafe_feed_at(id, pc_millis());
91}
92
93void pc_failsafe_on_breach(pc_failsafe_cb cb, void *arg)
94{
95 s_fs.cb = cb;
96 s_fs.cb_arg = arg;
97}
98
99uint32_t pc_failsafe_check_at(uint32_t now)
100{
101 uint32_t mask = 0;
102 for (int i = 0; i < PC_FAILSAFE_MAX_LIFELINES; i++)
103 {
104 pc_lifeline &l = s_fs.lines[i];
105 if (!l.armed)
106 {
107 continue;
108 }
109 if (!pc_lifeline_overdue(now, l.last_feed_ms, l.deadline_ms))
110 {
111 continue;
112 }
113 mask |= (1u << i);
114 if (l.breached) // fire once per stuck episode
115 {
116 continue;
117 }
118 l.breached = true;
119 if (s_fs.cb)
120 {
121 s_fs.cb(i, l.name, s_fs.cb_arg);
122 }
123 }
124 return mask;
125}
126
127uint32_t pc_failsafe_check(void)
128{
129 return pc_failsafe_check_at(pc_millis());
130}
131
132// append a literal into out[*n], bounded by cap (leaving room for the NUL); truncates safely on overflow.
133static void fs_put(char *out, size_t cap, size_t *n, const char *s)
134{
135 while (*s && *n + 1 < cap)
136 {
137 out[(*n)++] = *s++;
138 }
139}
140// append @p v as decimal into out[*n], same bound.
141static void fs_put_u32(char *out, size_t cap, size_t *n, uint32_t v)
142{
143 char b[10];
144 size_t k = u32_dec(v, b);
145 for (size_t i = 0; i < k && *n + 1 < cap; i++)
146 {
147 out[(*n)++] = b[i];
148 }
149}
150
151int pc_failsafe_json_at(uint32_t now, char *out, size_t cap)
152{
153 // {"lifelines":[{"name":"...","overdue":false,"age_ms":N,"deadline_ms":N},...]}
154 if (!out || cap == 0)
155 {
156 return 0;
157 }
158 size_t n = 0;
159 fs_put(out, cap, &n, "{\"lifelines\":[");
160 bool first = true;
161 for (int i = 0; i < PC_FAILSAFE_MAX_LIFELINES; i++)
162 {
163 const pc_lifeline &l = s_fs.lines[i];
164 if (!l.armed)
165 {
166 continue;
167 }
168 if (!first)
169 {
170 fs_put(out, cap, &n, ",");
171 }
172 first = false;
173 fs_put(out, cap, &n, "{\"name\":\"");
174 fs_put(out, cap, &n, l.name ? l.name : "");
175 fs_put(out, cap, &n, "\",\"overdue\":");
176 fs_put(out, cap, &n, pc_lifeline_overdue(now, l.last_feed_ms, l.deadline_ms) ? "true" : "false");
177 fs_put(out, cap, &n, ",\"age_ms\":");
178 fs_put_u32(out, cap, &n, now - l.last_feed_ms);
179 fs_put(out, cap, &n, ",\"deadline_ms\":");
180 fs_put_u32(out, cap, &n, l.deadline_ms);
181 fs_put(out, cap, &n, "}");
182 }
183 fs_put(out, cap, &n, "]}");
184 // The n >= cap arm is unreachable: fs_put/fs_put_u32 only ever advance n while n + 1 < cap, so n
185 // can never reach cap by the time we get here (cap > 0 was already established above).
186 out[n < cap ? n : cap - 1] = '\0'; // GCOVR_EXCL_BR_LINE
187 return (int)n;
188}
189
190#endif // PC_ENABLE_FAILSAFE
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
Software watchdog: deadlock detection + fail-safe safe-state (PC_ENABLE_FAILSAFE).
uint32_t mask(uint8_t width)
Mask of width low bits (width 32 handled without a 32-bit shift, which is UB).
Definition crc.h:61
#define PC_FAILSAFE_MAX_LIFELINES
Max monitored lifelines in the fail-safe registry (static, zero-heap).