ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
hotswap.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 hotswap.cpp
6 * @brief Removable-storage state machine + its binding (see hotswap.h).
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_HOTSWAP
13
14#include "services/system/clock.h" // pc_millis
15#include <stdio.h>
16
17// ---------------------------------------------------------------------------
18// Pure core
19// ---------------------------------------------------------------------------
20
21void pc_hotswap_core_init(HotswapCore *c, uint8_t fail_threshold, uint32_t probe_interval_ms, uint32_t now)
22{
23 if (!c)
24 {
25 return;
26 }
27 c->state = StorageState::ABSENT;
28 c->fail_run = 0;
29 // A zero threshold would fault the volume before any failure had been seen.
30 c->fail_threshold = fail_threshold ? fail_threshold : 1;
31 c->probe_interval_ms = probe_interval_ms;
32 // Back-date the first probe so a mount is attempted immediately rather than one interval late.
33 c->last_probe_ms = now - probe_interval_ms;
34 c->mounts = 0;
35 c->faults = 0;
36}
37
38bool pc_hotswap_core_io(HotswapCore *c, bool ok)
39{
40 if (!c || c->state != StorageState::READY)
41 {
42 return false; // not mounted: the caller should not have been touching it
43 }
44 if (ok)
45 {
46 c->fail_run = 0; // any success proves the medium is still there
47 return false;
48 }
49 if (c->fail_run < 0xFF)
50 {
51 c->fail_run++;
52 }
53 if (c->fail_run < c->fail_threshold)
54 {
55 return false; // one bad write is not a removal
56 }
57 c->state = StorageState::FAULTED;
58 c->faults++;
59 return true;
60}
61
62bool pc_hotswap_core_due(const HotswapCore *c, uint32_t now)
63{
64 if (!c || c->state == StorageState::READY)
65 {
66 return false;
67 }
68 // Unsigned delta, so this is correct across a millis() rollover.
69 return (now - c->last_probe_ms) >= c->probe_interval_ms;
70}
71
72bool pc_hotswap_core_probe(HotswapCore *c, bool present, bool mounted, uint32_t now)
73{
74 if (!c)
75 {
76 return false;
77 }
78 c->last_probe_ms = now;
79 StorageState was = c->state;
80 if (present && mounted)
81 {
82 c->state = StorageState::READY;
83 c->fail_run = 0;
84 if (was != StorageState::READY)
85 {
86 c->mounts++;
87 }
88 }
89 else
90 {
91 // Present but unmountable is not storage, so it reads the same as absent.
92 c->state = StorageState::ABSENT;
93 c->fail_run = 0;
94 }
95 return c->state != was;
96}
97
98// ---------------------------------------------------------------------------
99// Binding
100// ---------------------------------------------------------------------------
101
102/** @brief Owned state: the core plus the app's callbacks. */
103struct HotswapCtx
104{
105 HotswapCore core;
106 pc_hotswap_mount mount;
107 pc_hotswap_unmount unmount;
108 pc_hotswap_present present;
109 pc_hotswap_event event;
110 void *ctx;
111 bool begun;
112};
113static HotswapCtx s_hs = {{StorageState::ABSENT, 0, 1, 0, 0, 0, 0}, nullptr, nullptr, nullptr, nullptr, nullptr, false};
114
115static void hs_notify(StorageState from, StorageState to)
116{
117 // `from != to` has no false branch to reach: both call sites (poll_at, io) only invoke hs_notify
118 // after pc_hotswap_core_probe / pc_hotswap_core_io reported an actual state change.
119 if (s_hs.event && from != to) // GCOVR_EXCL_LINE - see above
120 {
121 s_hs.event(from, to, s_hs.ctx);
122 }
123}
124
125void pc_hotswap_begin(pc_hotswap_mount mount, pc_hotswap_unmount unmount, pc_hotswap_present present, void *ctx)
126{
127 s_hs.mount = mount;
128 s_hs.unmount = unmount;
129 s_hs.present = present;
130 s_hs.ctx = ctx;
131 s_hs.begun = true;
132 pc_hotswap_core_init(&s_hs.core, PC_HOTSWAP_FAIL_THRESHOLD, PC_HOTSWAP_PROBE_MS, pc_millis());
133}
134
135void pc_hotswap_set_event_cb(pc_hotswap_event cb)
136{
137 s_hs.event = cb;
138}
139
140void pc_hotswap_poll_at(uint32_t now)
141{
142 if (!s_hs.begun || !pc_hotswap_core_due(&s_hs.core, now))
143 {
144 return;
145 }
146
147 // A volume that faulted is still mounted as far as the driver knows. Drop it before retrying,
148 // so the remount starts from a clean state instead of reusing handles to a card that left.
149 if (s_hs.core.state == StorageState::FAULTED && s_hs.unmount)
150 {
151 s_hs.unmount(s_hs.ctx);
152 }
153
154 bool present = s_hs.present ? s_hs.present(s_hs.ctx) : true;
155 bool mounted = false;
156 if (present && s_hs.mount)
157 {
158 mounted = s_hs.mount(s_hs.ctx);
159 }
160
161 StorageState was = s_hs.core.state;
162 if (pc_hotswap_core_probe(&s_hs.core, present, mounted, now))
163 {
164 hs_notify(was, s_hs.core.state);
165 }
166}
167
168void pc_hotswap_poll(void)
169{
170 pc_hotswap_poll_at(pc_millis());
171}
172
173bool pc_hotswap_ready(void)
174{
175 return s_hs.core.state == StorageState::READY;
176}
177
178void pc_hotswap_io(bool ok)
179{
180 StorageState was = s_hs.core.state;
181 if (!pc_hotswap_core_io(&s_hs.core, ok))
182 {
183 return;
184 }
185 // Just faulted: drop the mount now rather than at the next poll, so nothing else can write
186 // through a handle to a card that is no longer there.
187 if (s_hs.unmount)
188 {
189 s_hs.unmount(s_hs.ctx);
190 }
191 hs_notify(was, s_hs.core.state);
192}
193
194StorageState pc_hotswap_state(void)
195{
196 return s_hs.core.state;
197}
198
199const char *pc_hotswap_state_name(StorageState s)
200{
201 switch (s)
202 {
203 case StorageState::READY:
204 return "ready";
205 case StorageState::FAULTED:
206 return "faulted";
207 case StorageState::ABSENT:
208 default:
209 return "absent";
210 }
211}
212
213size_t pc_hotswap_json(char *out, size_t cap)
214{
215 if (!out || cap == 0)
216 {
217 return 0;
218 }
219 pc_sb sb_out = {out, cap, 0, true};
220 pc_sb_put(&sb_out, "{\"storage\":\"");
221 pc_sb_put(&sb_out, pc_hotswap_state_name(s_hs.core.state));
222 pc_sb_put(&sb_out, "\",\"mounts\":");
223 pc_sb_u32(&sb_out, (uint32_t)((unsigned)s_hs.core.mounts));
224 pc_sb_put(&sb_out, ",\"faults\":");
225 pc_sb_u32(&sb_out, (uint32_t)((unsigned)s_hs.core.faults));
226 pc_sb_put(&sb_out, "}");
227 int n = (int)pc_sb_finish(&sb_out);
228 // `n < 0` has no true branch to reach: the format is a fixed literal with no encoding-dependent
229 // conversion and cap == 0 was rejected above, so snprintf can only ever report truncation here.
230 if (!sb_out.ok) // GCOVR_EXCL_LINE - see above
231 {
232 out[0] = '\0';
233 return 0; // fail closed rather than emit a truncated object
234 }
235 return (size_t)n;
236}
237
238#endif // PC_ENABLE_HOTSWAP
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
Safeties for removable storage that can vanish mid-write (PC_ENABLE_HOTSWAP).
#define PC_HOTSWAP_FAIL_THRESHOLD
Consecutive I/O failures that declare a removable volume gone.
#define PC_HOTSWAP_PROBE_MS
Minimum gap between remount attempts while a volume is absent or faulted (ms).
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
bool ok
Definition strbuf.h:34