Repository navigation
Expand file tree
/
Copy path04_software_arch.sdoc
More file actions
521 lines (486 loc) · 20.6 KB
/
Copy path04_software_arch.sdoc
File metadata and controls
521 lines (486 loc) · 20.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
[DOCUMENT]
TITLE: Safe Kiln Controller, Software Architecture
UID: SAFEKILN-SWA
OPTIONS:
MARKUP: Markdown
VIEW_STYLE: Table
NODE_IN_TOC: True
METADATA:
SPDX-FileCopyrightText: 2026 Bitcrush Testing
SPDX-License-Identifier: GPL-3.0-or-later
Project: Safe Kiln Controller, PID kiln controller
[GRAMMAR]
ELEMENTS:
- TAG: TEXT
FIELDS:
- TITLE: STATEMENT
TYPE: String
REQUIRED: True
- TAG: DECISION
FIELDS:
- TITLE: UID
TYPE: String
REQUIRED: True
- TITLE: STATEMENT
TYPE: String
REQUIRED: True
- TITLE: RATIONALE
TYPE: String
REQUIRED: False
- TITLE: COMMENT
TYPE: String
REQUIRED: False
RELATIONS:
- TYPE: Parent
ROLE: Satisfies
REVERSE_ROLE: Satisfied by
[TEXT]
STATEMENT: >>>
The decisions this architecture rests on, each with the requirement that drove
it and the cost it carries.
These were a markdown table in `architecture.md`. They are here instead because
they are the part of the architecture that has identity: 21 stable IDs, cited
over a hundred times from the firmware, each answering to a requirement. A
`Satisfies` relation to that requirement is checked when this document is
built, so a decision can no longer cite a requirement that does not exist, and
the requirements document can show what implements each of its clauses.
The rest of the architecture -- the component diagrams, the task and timing
tables, the memory budgets, the log record layout -- stays in
[`architecture.md`](architecture.md). None of it has an ID, so there is nothing
here for a tool to check, and a diagram is better read rendered than quoted.
<<<
[DECISION]
UID: SWA-01
STATEMENT: >>>
**Ports and adapters** (hexagonal). A `kiln_core` component holds all decision logic; hardware is reached only through interface headers in `kiln_ports`; adapters in `kiln_hal_esp32s3` implement them.
<<<
RATIONALE: >>>
Directly realises [LINK: SWR-TST-01], [LINK: SWR-TST-02]. Logic becomes ordinary C that a host compiler builds and a debugger steps through in microseconds.
<<<
COMMENT: >>>
One indirection on every hardware access; interfaces must be designed, not grown.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-TST-01
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-TST-02
ROLE: Satisfies
[DECISION]
UID: SWA-02
STATEMENT: >>>
**Time is injected**, never read. Core functions receive `dt` or take a `kiln_clock_t*`.
<<<
RATIONALE: >>>
[LINK: SWR-TST-03]. A 168 h firing, a 15 min runaway timer and a 2 h autotune are all testable in milliseconds of wall time.
<<<
COMMENT: >>>
Every core API grows a time parameter; discipline needed to keep `esp_timer_get_time()` out of the core.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-TST-03
ROLE: Satisfies
[DECISION]
UID: SWA-03
STATEMENT: >>>
**Explicit context structs, no globals** in the core.
<<<
RATIONALE: >>>
[LINK: SWR-TST-04]. Lets one test process run several controllers, and makes every test independent.
<<<
COMMENT: >>>
Slightly more verbose call sites.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-TST-04
ROLE: Satisfies
[DECISION]
UID: SWA-04
STATEMENT: >>>
**The safety supervisor is a separate, higher-priority task that owns the heat-enable output.** The control path can only *request* heat; safety alone grants it.
**There are now two supervisors and this is the one made of software.** Since [LINK: SWA-22] the kiln has an independent supervisor on its own microcontroller holding its own series element in the coil, and the distinction has to be kept straight in every sentence that says "the supervisor":
- **this** one is a task inside the controller's firmware. It protects against bugs, deadlocks and overruns in control, HMI, web and networking, and it enforces every configurable rule: the operator's maximum, runaway detection, the thermal and current rules;
- **that** one is a second part. It protects against the failure of this entire processor, and enforces one rule that nothing here can change.
Neither replaces the other, and the reason is what each can be wrong about. A task cannot protect against the processor it runs on; a part with no configuration, no network and 3 488 bytes of program cannot run a firing schedule. The configurable rules have to live where the configuration is, which is here, and the backstop has to live where nothing can reach it, which is there.
<<<
RATIONALE: >>>
[LINK: SYS-SAF-01], [LINK: SYS-SAF-02], [LINK: SWR-SAF-13]. A bug, deadlock or overrun anywhere in control, HMI, web or networking cannot produce heat.
<<<
COMMENT: >>>
Two components must agree on a shared view of the plant; that view is passed as an immutable snapshot, not shared state.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SYS-SAF-01
ROLE: Satisfies
- TYPE: Parent
VALUE: SYS-SAF-02
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-SAF-13
ROLE: Satisfies
[DECISION]
UID: SWA-05
STATEMENT: >>>
**Heat enable is a software-generated square wave into a hardware charge pump**, not a static level.
**It remains the controller's path to the coil and is no longer the only path.** Since [LINK: SWA-22] the supervisor holds a second series element with its own output, so the coil needs the charge pump *and* the supervisor's permit. The charge pump still does what it was put there for -- a hung safety task stops toggling and the contactor drops in about a second with no code involved -- and that property is now one of two, not the whole of the argument.
The supervisor's element is **a static level with an external pull-down** rather than a second charge pump ([LINK: SYS-HW-26]), and that asymmetry is deliberate. A charge pump protects against a processor that has stopped executing while leaving a pin latched high, which is the failure a 240 MHz processor running WiFi, a web server and a filesystem actually has. The supervisor executes 3 488 bytes in a loop with a windowed watchdog cross-checked against an independent oscillator; what protects against *it* stopping is that stopping removes the permit. A second charge pump would add a timing requirement to the part whose value is having almost none.
<<<
RATIONALE: >>>
[LINK: SYS-SAF-02], [LINK: SYS-HW-07], and gives [LINK: SWR-SAF-14] for free: a hung safety task stops toggling and the contactor drops in ~1 s with no code involved.
<<<
COMMENT: >>>
Requires the RC/charge-pump circuit on the board; the toggle must never be delegated to a hardware PWM peripheral, or the property is lost.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SYS-SAF-02
ROLE: Satisfies
- TYPE: Parent
VALUE: SYS-HW-07
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-SAF-14
ROLE: Satisfies
[DECISION]
UID: SWA-06
STATEMENT: >>>
**The setpoint generator is separate from the PID.** The program produces a moving setpoint; the PID only ever tracks the setpoint it is given.
<<<
RATIONALE: >>>
[LINK: SWR-CTL-09]. The two hardest pieces of behaviour, curve execution with hold-back, and loop tuning, become independently testable. Also makes manual mode and autotune trivially reuse the same PID.
<<<
COMMENT: >>>
An extra component and an extra data hand-off per cycle.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-CTL-09
ROLE: Satisfies
[DECISION]
UID: SWA-07
STATEMENT: >>>
**The SSR window is driven by a 10 ms periodic timer callback** that reads a duty value published atomically by the control task.
<<<
RATIONALE: >>>
[LINK: SWR-CTL-07], [LINK: SWR-CTL-08], [LINK: SWR-NFR-04]. Switching precision is decoupled from the 1 s control period and from any task scheduling delay.
<<<
COMMENT: >>>
The callback runs in a timer context: it must be allocation-free, lock-free and short.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-CTL-07
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-CTL-08
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-NFR-04
ROLE: Satisfies
[DECISION]
UID: SWA-17
STATEMENT: >>>
**Heater current is measured gated to the commanded output state**, not continuously averaged: conduction current is sampled inside a commanded-on interval and leakage current inside a commanded-off interval, each after a settle delay.
<<<
RATIONALE: >>>
[LINK: SWR-CUR-04]. A time-proportional output at 2 % duty is off 98 % of the time; a blind average would read ~2 % of full current and make [LINK: SWR-SAF-25]/[LINK: SWR-SAF-26] meaningless. Gating is what turns the measurement into a statement about the *relay*.
<<<
COMMENT: >>>
The sampler must know the window phase, so it is driven by the same 10 ms timer that owns the SSR pin ([LINK: SWA-07]), not by an independent ADC loop. Windows too short to measure are skipped rather than mis-measured.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-CUR-04
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-SAF-25
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-SAF-26
ROLE: Satisfies
[DECISION]
UID: SWA-18
STATEMENT: >>>
**The log record grows from 16 to 20 bytes** to carry current and its validity flags.
<<<
RATIONALE: >>>
[LINK: SWR-CUR-09]. Current without a flag saying whether the sample was a conduction, leakage or skipped measurement is uninterpretable.
<<<
COMMENT: >>>
204 records per sector instead of 255; capacity falls from 362 h to **290 h** at the default interval, still well above the 150 h of [LINK: SWR-LOG-07]. Recomputed in [§10.4](#104-flash-endurance-analysis).
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-CUR-09
ROLE: Satisfies
[DECISION]
UID: SWA-08
STATEMENT: >>>
**Sample logs live in a dedicated raw flash partition as a circular array of fixed 16-byte records**, not in a filesystem.
<<<
RATIONALE: >>>
[LINK: SWR-LOG-05]–[LINK: SWR-LOG-08], [LINK: UR-CON-03]. A filesystem adds metadata writes, fragmentation, and a torn-write failure mode across structures we do not control. A ring of fixed records has a trivially provable wear pattern ([§10.4](#104-flash-endurance-analysis)) and a reader that cannot be confused by a power cut.
<<<
COMMENT: >>>
A bespoke store to implement and test; no `ls` over logs. Mitigated by `tools/logdump`.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-LOG-05
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-LOG-08
ROLE: Satisfies
- TYPE: Parent
VALUE: UR-CON-03
ROLE: Satisfies
[DECISION]
UID: SWA-09
STATEMENT: >>>
**The log is also the power-loss journal.** Recovery state is reconstructed from the log tail rather than from a separate periodic write.
<<<
RATIONALE: >>>
[LINK: SWR-RUN-08], [LINK: SWR-RUN-09], [LINK: SWR-NFR-14]. Removes a 60 000-write-per-run NVS hot spot; the data was already being written.
<<<
COMMENT: >>>
Recovery granularity equals the log sample interval (default 10 s), which is well inside the [LINK: SWR-RUN-08] tolerance.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-RUN-08
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-RUN-09
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-NFR-14
ROLE: Satisfies
[DECISION]
UID: SWA-10
STATEMENT: >>>
**Configuration in NVS; programs and run records in a raw partition of fixed slots.** Originally LittleFS, revised by [LINK: SWA-21].
<<<
RATIONALE: >>>
Config is small, typed, and benefits from NVS wear levelling. Programs and run records want names and atomic replace, which is what the slot store provides; they do not want a directory, because nothing addresses them by anything but a slot number.
<<<
COMMENT: >>>
Two storage mechanisms to initialise. No general filesystem on the device, so there is no path for a file that is not a program or a run record.
<<<
[DECISION]
UID: SWA-11
STATEMENT: >>>
**Web assets are gzipped and embedded in the application image**, not stored in a filesystem.
<<<
RATIONALE: >>>
[LINK: SWR-WEB-02], [LINK: SWR-UPD-05]. Assets and firmware version can never disagree, and an OTA update is atomic across both.
<<<
COMMENT: >>>
Asset size counts against the 2 MB OTA partition; budget in [§12.4](#124-asset-budget).
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-WEB-02
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-UPD-05
ROLE: Satisfies
[DECISION]
UID: SWA-12
STATEMENT: >>>
**Live updates by Server-Sent Events**, not WebSocket.
<<<
RATIONALE: >>>
[LINK: SWR-WEB-05]. Telemetry is one-directional; SSE is plain HTTP, needs no framing layer, reconnects by itself, and costs one socket. Commands go over ordinary `POST`.
<<<
COMMENT: >>>
No client→server push channel; not needed, since no command is latency-critical.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-WEB-05
ROLE: Satisfies
[DECISION]
UID: SWA-13
STATEMENT: >>>
**Tasks communicate by queues and immutable snapshots**, with no mutex on the control path.
<<<
RATIONALE: >>>
[LINK: SWR-NFR-01], [LINK: SWR-NFR-02]. Removes priority inversion and unbounded blocking as a class of failure.
<<<
COMMENT: >>>
Snapshot copying each cycle; at ~64 bytes per snapshot this is negligible.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-NFR-01
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-NFR-02
ROLE: Satisfies
[DECISION]
UID: SWA-14
STATEMENT: >>>
**Dual-target build**: the same core sources compile as ESP-IDF components and as a plain CMake library for host tests.
<<<
RATIONALE: >>>
[LINK: SWR-TST-01], [LINK: SWR-TST-13], [LINK: SWR-TST-19]. Coverage and sanitisers are available where the logic actually lives.
<<<
COMMENT: >>>
The core may not include a single IDF header; enforced by a build check ([§14.5](#145-enforcement)).
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-TST-01
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-TST-13
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-TST-19
ROLE: Satisfies
[DECISION]
UID: SWA-15
STATEMENT: >>>
**Core affinity split**: connectivity and UI on core 0, control and safety on core 1.
<<<
RATIONALE: >>>
[LINK: SWR-NFR-01], [LINK: SWR-NFR-02]. WiFi (priority 23) and lwIP cannot preempt the control or safety task at all, so their deadlines do not depend on radio behaviour.
<<<
COMMENT: >>>
The core is tied to a dual-core target; [LINK: UR-CON-02] fixes ESP32-S3, which is dual-core.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-NFR-01
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-NFR-02
ROLE: Satisfies
[DECISION]
UID: SWA-19
STATEMENT: >>>
**The log ring is `kiln_core/logring` over a `port_flash`**, not logic inside the log-store adapter.
<<<
RATIONALE: >>>
Everything interesting about the ring is a decision with a failure mode [LINK: SWR-LOG-06] and [LINK: SWR-LOG-08] name explicitly: head discovery across 512 sector headers, erase-immediately-before-write ordering so a power cut cannot destroy data the index still claims, and terminating a sector scan at a torn record. Behind `esp_partition` none of it is testable; in the core it is driven by a fake that can cut power mid-write. Revises [§5.2](#52-ports)'s original placement.
<<<
COMMENT: >>>
One more port, and the core now provides an implementation of `port_logstore` rather than only consuming ports. The adapter in exchange has no logic left to get wrong, three calls that pass straight through.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-LOG-06
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-LOG-08
ROLE: Satisfies
[DECISION]
UID: SWA-20
STATEMENT: >>>
**The firmware is C++20, analysed by `clang-tidy` as a high-integrity profile.** Not MISRA: `clang-tidy` implements no MISRA checks, and the `hicpp-*` module that approximated High Integrity C++ has been removed from LLVM. The equivalent content is enabled under its current names (`cppcoreguidelines-*`, `bugprone-*`, `cert-*`, `clang-analyzer-*`).
<<<
RATIONALE: >>>
[LINK: UR-CON-05] permits C or C++; [LINK: SWR-NFR-25] requires a static analysis configuration and [LINK: SWR-TST-24] requires CI to run it, and the C toolchain offered no comparable check set. The migration cost was 9 compile errors across 22 597 lines, because the code already had no heap, no VLAs and explicit context structs. C++20 rather than C++17 because the 152 designated initialisers that name every safety default are a C99 feature C++ regained only in C++20.
<<<
COMMENT: >>>
A `-std=gnu++20` floor on both build halves. Genuine MISRA C++ compliance is now a *commercial tooling* decision rather than a language one, and nothing in the repository may claim it. Three check groups are deferred rather than clean, `tasklist.md` section F.
<<<
RELATIONS:
- TYPE: Parent
VALUE: UR-CON-05
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-NFR-25
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-TST-24
ROLE: Satisfies
[DECISION]
UID: SWA-21
STATEMENT: >>>
**`port_filestore` is `kiln_core/fileslots` over a `port_flash`, a fixed array of two-sector regions, not a filesystem.** Revises [LINK: SWA-10].
<<<
RATIONALE: >>>
Two reasons, and either alone would be enough. The first is [LINK: UR-CON-04]: LittleFS is not in the ESP-IDF tree, so it arrives either as a managed dependency, which [LINK: UR-CON-04] forbids at build time, or as several thousand vendored lines to be maintained and licence-audited. The second is that nothing above it wants a filesystem. `program_store` and `run_index` address `/p/00` to `/p/19` and `/r/00` upward, and between them call read, write_atomic, remove and usage; `list` and `exists` have no caller at all. That is an array. [LINK: SWA-08] already refused a filesystem for the log, on the grounds that it adds metadata writes, fragmentation and a torn-write mode across structures we do not control, and the same reasoning survives being applied to a second fixed-size store. The in-tree alternatives were considered and rejected: SPIFFS and FAT are both reachable without a fetch and neither is power-fail safe, which is the one property this store is required to have.
<<<
COMMENT: >>>
One file occupies one region of two erase sectors and alternates between them, so the copy being replaced is never the copy being erased. A name maps to a region through an index built at mount, 64 entries of 72 bytes. The cost is capacity: 40 files in 512 kB where a filesystem would hold hundreds of small ones, which is irrelevant at a fixed 20 programs and 20 run records and would not be if that ever stopped being fixed. See [§10.6](#106-the-file-store).
<<<
RELATIONS:
- TYPE: Parent
VALUE: UR-CON-04
ROLE: Satisfies
[DECISION]
UID: SWA-16
STATEMENT: >>>
**The web UI is served only through the public REST API** it documents.
<<<
RATIONALE: >>>
[LINK: SWR-WEB-19]. The API is exercised by the UI on every use, so it cannot rot; and the UI is replaceable.
<<<
COMMENT: >>>
No shortcuts for the UI; occasionally a slightly chattier interaction.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-WEB-19
ROLE: Satisfies
[DECISION]
UID: SWA-22
STATEMENT: >>>
**The absolute over-temperature trip and the thermocouple-fault trip move to a
second microcontroller**, an STM32, which owns the chamber front end and one
series element in the contactor coil. The lid is not among them: its switch
breaks the coil in hardware (`SYS-HW-21`) and `SWR-SAF-31`'s latch needs run state that
only the ESP32 has, so the lid sense stays on the ESP32. It reports temperature
and status to the ESP32 over a point-to-point serial link and takes no
instruction from it. Every other function stays in the ESP32.
<<<
RATIONALE: >>>
`SWR-SAF-09` and `SWR-SAF-04`, and the independence claim of
[`safety.md`](safety.md) section 6, which currently has to admit that L1 to L4
all share one MCU and that the only fully independent layer is `SYS-HW-13`, the
external cutout, which is outside this product.
This is the first protection layer inside the product boundary that a defect in
the ESP32 firmware cannot reach, including a defect in the safety supervisor
task of [LINK: SWA-04]. It also fixes what tasklist `K1` records about the
discrete fault chain: an open-drain output is closed when the front end is
absent, and the MAX31856 reports no open circuit until its fault mask is
configured, so that interlock is inert out of reset. A supervisor starts
de-energised and permits heat only after it has valid conversions, which turns
absence of evidence into a trip rather than a permission.
<<<
COMMENT: >>>
A second firmware to build, review and flash, and the independence is only
worth what that firmware's simplicity is worth: a scheduler, a parser or a
configuration model in it would start to rot the claim.
The ESP32's process value now arrives over the link, so the link is
availability-critical even though it is not protection-critical: silence means
no temperature, which means no heat. The chamber thermocouple remains shared,
so the independence won is against software and MCU failure and not against a
sensor that reads plausibly but wrongly.
Supersedes `SYS-HW-24` and removes the discrete chain of tasklist section K
(`Q5`, `Q6`, `R28` to `R31`). The lid's series contact is kept, being the one
element that depends on no firmware at all. Forces `SWR-SAF-23`'s 1350 degC
configurable ceiling to move, because a hard-coded backstop needs room above
the highest configurable limit.
Designed in [`safety-supervisor.md`](safety-supervisor.md), which carries the
open questions. Does not replace `SYS-HW-13`.
<<<
RELATIONS:
- TYPE: Parent
VALUE: SWR-SAF-09
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-SAF-04
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-NFR-04
ROLE: Satisfies
- TYPE: Parent
VALUE: SWR-TST-29
ROLE: Satisfies