ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
hotswap.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 hotswap.h
6 * @brief Safeties for removable storage that can vanish mid-write (PROTOCORE_ENABLE_HOTSWAP).
7 *
8 * An SD card is a connector, and a connector can be pulled - during a log append, an upload, a
9 * core-dump save. The failure is nasty because it is quiet: the driver keeps handing back a mounted
10 * volume, every write reports an error nobody checks, and the code carries on believing it has
11 * storage. What should happen instead is that the medium is declared unusable, stale handles are
12 * dropped, and callers are told to stop rather than write into nothing.
13 *
14 * That is what this owns. One state machine per volume:
15 *
16 * ABSENT --probe finds a card, mount succeeds--> READY
17 * READY --fail_threshold consecutive I/O errors--> FAULTED (unmount fires immediately)
18 * FAULTED --probe interval elapses, remount succeeds--> READY
19 *
20 * The threshold matters: a single failed write is not proof a card left (a transient bus error, a
21 * full volume), so one error does not tear down a working volume. A run of them is proof enough.
22 * Any success resets the run, so intermittent noise never accumulates into a false removal.
23 *
24 * Callers gate on `protocore_hotswap_ready()` and report every filesystem outcome through
25 * `protocore_hotswap_io()`. It is deliberately **fail-closed**: while not READY, ready() is false, so a
26 * caller that honors it writes nothing rather than writing into a stale mount.
27 *
28 * The core is pure and takes an explicit `now`, so the whole state machine is host-testable with a
29 * synthetic clock; the device binding is three app callbacks (mount / unmount / optional
30 * card-detect), because how a volume is mounted is the application's business, not this owner's.
31 *
32 * @author Douglas Quigg (dstroy0)
33 * @date 2026
34 */
35
36#ifndef PROTOCORE_HOTSWAP_H
37#define PROTOCORE_HOTSWAP_H
38
39#include "protocore_config.h" // the entry point: protocore_types.h for the widths
40
41#if PROTOCORE_ENABLE_HOTSWAP
42
44
45// PROTOCORE_HOTSWAP_BORROW - the bytes this module runs out of - is stated in protocore_config.h, which sums
46// it into its arena. A caller takes them once and passes the pointer to every call. How they
47// are carved is this module's and is never named here.
48
49/** @brief Where a removable volume currently stands. */
50typedef enum PROTO_ENUM_PACKED
51{
52 STORAGE_STATE_ABSENT = 0, ///< nothing mounted; no filesystem call is safe.
53 STORAGE_STATE_READY = 1, ///< mounted and healthy.
54 STORAGE_STATE_FAULTED = 2, ///< was mounted, I/O is failing; unmounted and awaiting a remount probe.
55} StorageState;
56
57/** @brief The whole state machine. Pure: it decides, the binding acts. */
58typedef struct
59{
60 StorageState state; ///< current state.
61 uint8_t fail_run; ///< consecutive I/O failures seen while READY.
62 uint8_t fail_threshold; ///< failures in a row that declare the medium gone (>= 1).
63 uint32_t probe_interval_ms; ///< minimum gap between remount attempts while not READY.
64 uint32_t last_probe_ms; ///< when the last probe ran.
65 uint32_t mounts; ///< successful mounts since init (a removal/insert cycle count).
66 uint32_t faults; ///< times a healthy volume was declared faulted.
67} HotswapCore;
68
69/** @brief Mount the volume. @return true on success. */
70typedef proto_bool (*protocore_hotswap_mount)(void *ctx);
71
72/** @brief Drop the mount and any handles it owns. Must tolerate being called when not mounted. */
73typedef void (*protocore_hotswap_unmount)(void *ctx);
74
75/** @brief Optional card-detect probe. nullptr means "assume present and let the mount decide". */
76typedef proto_bool (*protocore_hotswap_present)(void *ctx);
77
78/** @brief Fired on every state change, so an app can log it or light an LED. */
79typedef void (*protocore_hotswap_event)(StorageState from, StorageState to, void *ctx);
80
81/** @brief What core_init takes: c, fail_threshold, probe_interval_ms, ... */
82typedef struct
83{
84 HotswapCore *c;
85 uint8_t
86 fail_threshold; ///< consecutive I/O errors that declare the medium gone; clamped to >= 1. Starting ABSENT ...
87 uint32_t probe_interval_ms;
88 uint32_t now;
89} HotswapCoreInitArgs;
90
91/** @brief What core_io takes: c, ok. */
92typedef struct
93{
94 HotswapCore *c;
95 proto_bool ok;
96} HotswapCoreIoArgs;
97
98/** @brief What core_due takes: c, now. */
99typedef struct
100{
101 const HotswapCore *c;
102 uint32_t now;
103} HotswapCoreDueArgs;
104
105/** @brief What core_probe takes: c, present, mounted, now. */
106typedef struct
107{
108 HotswapCore *c;
109 proto_bool present; ///< true if a medium appears to be there (card-detect, or "assume yes" without one)
110 proto_bool mounted; ///< true if the mount actually succeeded. Present-but-unmountable stays ABSENT rather than ...
111 uint32_t now;
112} HotswapCoreProbeArgs;
113
114/** @brief What begin takes: mount, unmount, present, ctx. */
115typedef struct
116{
117 protocore_hotswap_mount mount;
118 protocore_hotswap_unmount unmount;
119 protocore_hotswap_present present;
120 void *ctx;
121} HotswapBeginArgs;
122
123/** @brief What set_event_cb takes: cb. */
124typedef struct
125{
126 protocore_hotswap_event cb;
127} HotswapSetEventCbArgs;
128
129/** @brief What poll_at takes: now. */
130typedef struct
131{
132 uint32_t now;
133} HotswapPollAtArgs;
134
135/** @brief What io takes: ok. */
136typedef struct
137{
138 proto_bool ok;
139} HotswapIoArgs;
140
141/** @brief What state_name takes: s. */
142typedef struct
143{
144 StorageState s;
145} HotswapStateNameArgs;
146
147/** @brief What json takes: out, cap. */
148typedef struct
149{
150 char *out;
151 size_t cap;
152} HotswapJsonArgs;
153
154/**
155 * @brief Safeties for removable storage that can vanish mid-write (PROTOCORE_ENABLE_HOTSWAP).
156 *
157 * A caller sets the members a call takes, invokes it through ::Hotswap with the bytes it runs
158 * out of, and reads the outcome off the same handle.
159 *
160 * Hotswap.core_init_args.c = ...;
161 * Hotswap.core_init_args.fail_threshold = ...;
162 * Hotswap.core_init_args.probe_interval_ms = ...;
163 * Hotswap.core_init_args.now = ...;
164 * Hotswap.core_init(work);
165 *
166 * @var HotswapNs::core_init_args what core_init takes: c, fail_threshold, probe_interval_ms,
167 * @var HotswapNs::core_io_args what core_io takes: c, ok
168 * @var HotswapNs::core_due_args what core_due takes: c, now
169 * @var HotswapNs::core_probe_args what core_probe takes: c, present, mounted, now
170 * @var HotswapNs::begin_args what begin takes: mount, unmount, present, ctx
171 * @var HotswapNs::set_event_cb_args what set_event_cb takes: cb
172 * @var HotswapNs::poll_at_args what poll_at takes: now
173 * @var HotswapNs::io_args what io takes: ok
174 * @var HotswapNs::state_name_args what state_name takes: s
175 * @var HotswapNs::json_args what json takes: out, cap
176 * @var HotswapNs::ok true if the state changed (so the binding knows to unmount + notify)
177 * @var HotswapNs::value the value a call reports
178 * @var HotswapNs::text the string a call reports
179 * @var HotswapNs::n length written (excl NUL), or 0 on overflow
180 * @var HotswapNs::core_init initialize to ABSENT at now
181 * @var HotswapNs::core_io report one filesystem outcome. A success while READY clears the ...
182 * @var HotswapNs::core_due is a (re)mount probe due at now? Only while not READY, and only ...
183 * @var HotswapNs::core_probe report what a probe found
184 * @var HotswapNs::begin install the callbacks and reset to ABSENT. A first poll will ...
185 * @var HotswapNs::set_event_cb install (or clear, with nullptr) the state-change callback
186 * @var HotswapNs::poll run the state machine: probe when due, unmount on a fresh fault. ...
187 * @var HotswapNs::poll_at poll_at
188 * @var HotswapNs::ready is it safe to touch the filesystem right now? The gate every caller ...
189 * @var HotswapNs::io report a filesystem outcome; unmounts and notifies if this is the ...
190 * @var HotswapNs::state current state
191 * @var HotswapNs::state_name short name for s ("absent" / "ready" / "faulted"), for logs and JSON
192 * @var HotswapNs::json serialize as `{"storage":"ready","mounts":N,"faults":N}` for a ...
193 *
194 * @c work is PROTOCORE_HOTSWAP_BORROW bytes the CALLER took, at an address it knows. It is not held past the call, so
195 * nothing here aliases it. How those bytes are carved is this module's and is never named here.
196 */
197typedef struct
198{
199 HotswapCoreInitArgs core_init_args;
200 HotswapCoreIoArgs core_io_args;
201 HotswapCoreDueArgs core_due_args;
202 HotswapCoreProbeArgs core_probe_args;
203 HotswapBeginArgs begin_args;
204 HotswapSetEventCbArgs set_event_cb_args;
205 HotswapPollAtArgs poll_at_args;
206 HotswapIoArgs io_args;
207 HotswapStateNameArgs state_name_args;
208 HotswapJsonArgs json_args;
209 proto_bool ok;
210 StorageState value;
211 const char *text;
212 size_t n;
213} HotswapVars;
214
215/** @brief The operands and the outcome. */
216extern HotswapVars HotswapV;
217
218/** @brief The entries. */
219typedef struct
220{
221 void (*const core_init)(uint8_t *work);
222 void (*const core_io)(uint8_t *work);
223 void (*const core_due)(uint8_t *work);
224 void (*const core_probe)(uint8_t *work);
225 void (*const begin)(uint8_t *work);
226 void (*const set_event_cb)(uint8_t *work);
227 void (*const poll)(uint8_t *work);
228 void (*const poll_at)(uint8_t *work);
229 void (*const ready)(uint8_t *work);
230 void (*const io)(uint8_t *work);
231 void (*const state)(uint8_t *work);
232 void (*const state_name)(uint8_t *work);
233 void (*const json)(uint8_t *work);
234} HotswapNs;
235
236// What the table binds, defined once in the .c and taking one parameter each: everything
237// else an entry needs is an operand in HotswapV or a region of the borrow at a fixed offset.
238void protocore_hotswap_core_init(uint8_t *work);
239void protocore_hotswap_core_io(uint8_t *work);
240void protocore_hotswap_core_due(uint8_t *work);
241void protocore_hotswap_core_probe(uint8_t *work);
242void protocore_hotswap_begin(uint8_t *work);
243void protocore_hotswap_set_event_cb(uint8_t *work);
244void protocore_hotswap_poll(uint8_t *work);
245void protocore_hotswap_poll_at(uint8_t *work);
246void protocore_hotswap_ready(uint8_t *work);
247void protocore_hotswap_io(uint8_t *work);
248void protocore_hotswap_state(uint8_t *work);
249void protocore_hotswap_state_name(uint8_t *work);
250void protocore_hotswap_json(uint8_t *work);
251
252// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
253// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
254// `Hotswap.core_init(work)` resolves to a named function and becomes a DIRECT call. An extern table
255// leaves the call indirect and the symbol live at every level, -O2 -flto included.
256static const HotswapNs Hotswap __attribute__((unused)) = {
257 .core_init = protocore_hotswap_core_init,
258 .core_io = protocore_hotswap_core_io,
259 .core_due = protocore_hotswap_core_due,
260 .core_probe = protocore_hotswap_core_probe,
261 .begin = protocore_hotswap_begin,
262 .set_event_cb = protocore_hotswap_set_event_cb,
263 .poll = protocore_hotswap_poll,
264 .poll_at = protocore_hotswap_poll_at,
265 .ready = protocore_hotswap_ready,
266 .io = protocore_hotswap_io,
267 .state = protocore_hotswap_state,
268 .state_name = protocore_hotswap_state_name,
269 .json = protocore_hotswap_json,
270};
271
272/**
273 * @brief The PROTOCORE_HOTSWAP_BORROW bytes this module's state lives in.
274 *
275 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
276 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
277 * walks, so the state lasts the life of the program.
278 *
279 * @return the span.
280 */
281uint8_t *protocore_hotswap_span(void);
282
284
285#endif // PROTOCORE_ENABLE_HOTSWAP
286
287#endif // PROTOCORE_HOTSWAP_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#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