ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
hotswap.h
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.h
6 * @brief Safeties for removable storage that can vanish mid-write (PC_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 `pc_hotswap_ready()` and report every filesystem outcome through
25 * `pc_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"
40#include <stddef.h>
41#include <stdint.h>
42
43#if PC_ENABLE_HOTSWAP
44
45/** @brief Where a removable volume currently stands. */
46enum class StorageState : uint8_t
47{
48 ABSENT = 0, ///< nothing mounted; no filesystem call is safe.
49 READY = 1, ///< mounted and healthy.
50 FAULTED = 2, ///< was mounted, I/O is failing; unmounted and awaiting a remount probe.
51};
52
53// ---------------------------------------------------------------------------
54// Host-testable core
55// ---------------------------------------------------------------------------
56
57/** @brief The whole state machine. Pure: it decides, the binding acts. */
58struct HotswapCore
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};
68
69/**
70 * @brief Initialize to ABSENT at @p now.
71 * @param fail_threshold consecutive I/O errors that declare the medium gone; clamped to >= 1.
72 *
73 * Starting ABSENT rather than READY is the safe default: nothing may touch the volume until a probe
74 * has actually mounted it.
75 */
76void pc_hotswap_core_init(HotswapCore *c, uint8_t fail_threshold, uint32_t probe_interval_ms, uint32_t now);
77
78/**
79 * @brief Report one filesystem outcome.
80 *
81 * A success while READY clears the failure run. A failure extends it, and on reaching the threshold
82 * the volume becomes FAULTED. Outcomes reported while not READY are ignored - a caller honoring
83 * ready() should not have been touching the volume, and a failure there is already accounted for.
84 *
85 * @return true if the state changed (so the binding knows to unmount + notify).
86 */
87bool pc_hotswap_core_io(HotswapCore *c, bool ok);
88
89/**
90 * @brief Is a (re)mount probe due at @p now?
91 *
92 * Only while not READY, and only once per probe_interval_ms - so a missing card costs one cheap
93 * check every interval instead of a mount storm. Wrap-safe across a millis() rollover.
94 */
95bool pc_hotswap_core_due(const HotswapCore *c, uint32_t now);
96
97/**
98 * @brief Report what a probe found.
99 * @param present true if a medium appears to be there (card-detect, or "assume yes" without one).
100 * @param mounted true if the mount actually succeeded.
101 *
102 * Present-but-unmountable stays ABSENT rather than READY: a card that will not mount is not storage.
103 * @return true if the state changed.
104 */
105bool pc_hotswap_core_probe(HotswapCore *c, bool present, bool mounted, uint32_t now);
106
107// ---------------------------------------------------------------------------
108// Binding
109// ---------------------------------------------------------------------------
110
111/** @brief Mount the volume. @return true on success. */
112typedef bool (*pc_hotswap_mount)(void *ctx);
113/** @brief Drop the mount and any handles it owns. Must tolerate being called when not mounted. */
114typedef void (*pc_hotswap_unmount)(void *ctx);
115/** @brief Optional card-detect probe. nullptr means "assume present and let the mount decide". */
116typedef bool (*pc_hotswap_present)(void *ctx);
117/** @brief Fired on every state change, so an app can log it or light an LED. */
118typedef void (*pc_hotswap_event)(StorageState from, StorageState to, void *ctx);
119
120/** @brief Install the callbacks and reset to ABSENT. A first poll will attempt the mount. */
121void pc_hotswap_begin(pc_hotswap_mount mount, pc_hotswap_unmount unmount, pc_hotswap_present present, void *ctx);
122
123/** @brief Install (or clear, with nullptr) the state-change callback. */
124void pc_hotswap_set_event_cb(pc_hotswap_event cb);
125
126/** @brief Run the state machine: probe when due, unmount on a fresh fault. Cheap; call each loop. */
127void pc_hotswap_poll(void);
128void pc_hotswap_poll_at(uint32_t now);
129
130/**
131 * @brief Is it safe to touch the filesystem right now?
132 *
133 * The gate every caller checks first. False whenever the volume is ABSENT or FAULTED.
134 */
135bool pc_hotswap_ready(void);
136
137/** @brief Report a filesystem outcome; unmounts and notifies if this is the failure that faults it. */
138void pc_hotswap_io(bool ok);
139
140/** @brief Current state. */
141StorageState pc_hotswap_state(void);
142
143/** @brief Short name for @p s ("absent" / "ready" / "faulted"), for logs and JSON. */
144const char *pc_hotswap_state_name(StorageState s);
145
146/**
147 * @brief Serialize as `{"storage":"ready","mounts":N,"faults":N}` for a /health panel.
148 * @return length written (excl NUL), or 0 on overflow.
149 */
150size_t pc_hotswap_json(char *out, size_t cap);
151
152#endif // PC_ENABLE_HOTSWAP
153#endif // PROTOCORE_HOTSWAP_H
User-facing configuration for ProtoCore.