From 397518720de5d13023a5fc54f7f121b147ba919b Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:42:18 +0200 Subject: [PATCH 01/14] feat: the firing profiles are compiled in, and nothing edits them OQ-09 asked where programs are authored now that the web is read-only, and offered a display editor, a file import, or shipping only the examples. The answer is the third, as a decision rather than as a default: the profiles are compiled into the firmware and there is no authoring path anywhere. SWR-PRG-07 (create, update, rename, duplicate, delete over the web), SWR-PRG-08 (export and import as JSON) and SWR-PRG-10 (edit the remaining segments of a running program) are withdrawn with the reasoning kept. SWR-PRG-05 validates on start, since there is no save. SWR-PRG-09 now says what it has become: the examples are the device's whole repertoire. THE COST IS REAL AND IS WRITTEN DOWN RATHER THAN MINIMISED. A potter who wants a curve of their own needs a toolchain and a flash, which for most of them means somebody else does it. OQ-09 says so in those words, and names the cheapest reversal if it proves too expensive in use: the file import at service time, which needs physical access and so leaves the security argument intact. What it buys is that there is nothing to secure. No write route, no file format to validate, no parser taking operator data on the processor that commands a heater, and no editor to design for a rotary encoder. The web caught up with it. The Programs view called itself an Editor and was a set of permanently disabled inputs with a note offering to copy an example so you could make changes, which was untrue in three ways at once. It is a program view now: the name and description as text rather than as dead fields, the segments, the curve, and a notice that says where programs come from. The empty save-result line and the empty controls bar are gone with it. Verified by rendering it: three profiles listed, no editor, 25 host suites still green. What this decision leaves behind is recorded as P3 rather than done: kiln_program_store_save and kiln_program_store_delete are now called by nothing but their own tests, and SWR-PRG-04 asks for 20 persistent slots to hold three programs that arrive with the firmware. Removing the store for programs is the consistent finish and it deletes tested code, so it wants a decision rather than momentum. --- README.md | 3 ++- docs/03_software_req.sdoc | 30 ++++++++++++++++++++++++------ tasklist.md | 31 +++++++++++++++++++++++++------ web/app.js | 30 ++++++++++++++---------------- web/index.html | 18 ++++++++---------- 5 files changed, 73 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index e3e48cb..36e0a0e 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,9 @@ An open-source PID controller for electric ceramic and glass kilns, built on the **ESP32-S3** with **ESP-IDF**. - **Simple local display**: a 128×64 OLED showing current and target temperature at a glance, plus state, segment progress and rate of rise. -- **Web interface**: served by the device itself. Live dashboard, charts of the logged data, power and energy, and the firing programs as stored. **Observation only**: nothing reachable over the network can start a firing, heat the kiln, change its configuration or author a program. Every one of those is done at the kiln. +- **Web interface**: served by the device itself. Live dashboard, charts of the logged data, power and energy, and the firing programs the firmware carries. **Observation only**: nothing reachable over the network can start a firing, heat the kiln or change its configuration. Those are done at the kiln. - **PID with automatic tuning**: relay (Åström–Hägglund) autotune on the real kiln; no manual gain hunting. +- **Firing profiles are compiled in**: the device ships with its curves and nothing edits them, not over the network and not at the kiln. That removes every authoring path and the attack surface each one carries, and it has a real cost: a curve of your own means building a firmware that carries it. - **Safety first**: thermal runaway, thermocouple failure, shorted-SSR, over-temperature and door-interlock detection, with a safety supervisor that has sole authority over a heat-enable line that decays unless actively refreshed. - **Current monitoring**: a current transformer turns relay and element failures from slow thermal inferences into fast electrical facts, with the thermal rules retained as an independent backstop. - **Self-contained**: no SD card, no external database, no account, no filesystem. Logs live in a circular partition on internal flash and programs in a fixed-slot one, each built so a power cut cannot tear a record; web assets are embedded in the firmware. The device makes **one** outbound connection, a daily check for a firmware update that carries nothing about the device and can be turned off at the display. diff --git a/docs/03_software_req.sdoc b/docs/03_software_req.sdoc index 29bb745..1290cbf 100644 --- a/docs/03_software_req.sdoc +++ b/docs/03_software_req.sdoc @@ -795,7 +795,7 @@ VERIFICATION: T [REQUIREMENT] UID: SWR-PRG-05 STATEMENT: >>> -The system shall validate a program on save and on start, rejecting it with a specific reason if: no segments; any target above the configured maximum temperature; any field out of range; total predicted duration above 168 h. +The system shall validate a program on start, rejecting it with a specific reason if: no segments; any target above the configured maximum temperature; any field out of range; total predicted duration above 168 h. <<< PRIORITY: M VERIFICATION: T @@ -811,7 +811,11 @@ VERIFICATION: T [REQUIREMENT] UID: SWR-PRG-07 STATEMENT: >>> -The system shall allow creating, reading, updating, renaming, duplicating and deleting programs through the web interface. +**Withdrawn 2026-10-09: programs are pre-compiled and are not edited anywhere.** This required creating, reading, updating, renaming, duplicating and deleting programs through the web interface. + +Half of it was already gone: [LINK: SWR-WEB-26] made the interface read-only, so nothing over the network could create or change a program. The rest goes with the decision that the set of firing profiles is **fixed at build time** ([LINK: SWR-PRG-09]), which answers `OQ-09` by removing the question rather than by choosing one of the editors it offered. + +What that buys, and it is the reason to prefer it: there is no authoring path to secure, no file format to validate, no import to get wrong, and no editor to build on a rotary encoder. What it costs is stated plainly in [LINK: SWR-PRG-09]: a user who wants a curve of their own builds one into the firmware. <<< PRIORITY: M VERIFICATION: T @@ -819,7 +823,9 @@ VERIFICATION: T [REQUIREMENT] UID: SWR-PRG-08 STATEMENT: >>> -The system shall export and import a program as a documented, human-readable JSON file. +**Withdrawn 2026-10-09 with [LINK: SWR-PRG-07].** This required exporting and importing a program as a documented JSON file. + +Import is authoring by another name, and the set of profiles is fixed at build time. Export has no purpose on its own: what a user would export is already in the source, under version control, readable. <<< PRIORITY: M VERIFICATION: T @@ -827,7 +833,11 @@ VERIFICATION: T [REQUIREMENT] UID: SWR-PRG-09 STATEMENT: >>> -The system shall ship with at least **3** example programs (e.g. bisque cone 06, glaze cone 6, glass fusing) marked read-only. +The system shall ship with at least **3** example programs (e.g. bisque cone 06, glaze cone 6, glass fusing), compiled into the firmware and read-only. + +**These are the only firing programs the device has**, as of the 2026-10-09 decision that withdrew [LINK: SWR-PRG-07], [LINK: SWR-PRG-08] and [LINK: SWR-PRG-10]. A user who wants a different curve adds it here and builds, which is a real cost and is the honest consequence of having no authoring path at all: it puts a compiler between a potter and a firing schedule. + +The trade was taken deliberately. Every authoring route costs something this design is not willing to spend: over the network reopens a write path that [LINK: SWR-WEB-26] closed, at the display means editing five segments of target, rate and dwell on a rotary encoder, and an import file is a parser accepting operator-supplied data on the component that commands a heater. <<< PRIORITY: S VERIFICATION: I @@ -835,7 +845,9 @@ VERIFICATION: I [REQUIREMENT] UID: SWR-PRG-10 STATEMENT: >>> -The system shall permit editing the *remaining* segments of a running program; edits to the current or already-completed segments shall be rejected. +**Withdrawn 2026-10-09 with [LINK: SWR-PRG-07].** This required editing the remaining segments of a running program while rejecting edits to the current or completed ones. + +It was the most delicate requirement in this section, because it asks the run controller to accept a change to the thing it is executing, and it is now moot: there is no editor and no segment to change. [LINK: SWR-RUN-07]'s abort remains the way to stop a firing that is doing the wrong thing. <<< PRIORITY: S VERIFICATION: T @@ -2892,7 +2904,13 @@ STATUS: Resolved [QUESTION] UID: OQ-09 STATEMENT: >>> -Where are firing programs authored, now that [LINK: SWR-WEB-26] has made the web interface read-only? A local editor on a rotary encoder and a 128x64 display is possible but unpleasant; importing from a file on first boot, or shipping only the examples of [LINK: SWR-PRG-09], are the alternatives. Until this is answered a user cannot create a curve of their own, which is a real functional gap rather than a nicety. +**Resolved 2026-10-09: nowhere. The profiles are pre-compiled and nothing edits them.** The question was where firing programs are authored now that [LINK: SWR-WEB-26] has made the web interface read-only, with a local editor, a file import, or shipping only the examples as the candidates. + +The answer is the third, taken as a decision rather than as a default. [LINK: SWR-PRG-09]'s examples are the device's whole repertoire; [LINK: SWR-PRG-07], [LINK: SWR-PRG-08] and [LINK: SWR-PRG-10] are withdrawn. + +**The gap this leaves is real and is not being minimised**: a potter who wants a curve of their own needs a toolchain and a flash, which for most of them means somebody else does it. That is a genuine cost of the read-only decision and it should be stated in the user documentation rather than discovered. + +What it buys is that there is no authoring path at all: nothing over the network to secure, no file format to validate, no parser accepting operator data on the processor that commands a heater, and no editor to design for a rotary encoder. If the cost proves too high in use, the cheapest reversal is the file import of [LINK: SWR-PRG-08] at service time, which needs physical access and so leaves the security argument intact. <<< [QUESTION] diff --git a/tasklist.md b/tasklist.md index 7e5e8aa..0efca8d 100644 --- a/tasklist.md +++ b/tasklist.md @@ -708,12 +708,31 @@ weakness first, the deadline second, the sentence third, the engineering last. handler. The UI falls back to nothing: it reads `/api/status` on a timer already, so this is a refinement rather than a gap in function. -- [ ] **P3. Where do firing programs come from now?** Recorded as `OQ-09`. - Authoring is gone from the web and there is no local editor, so a user - cannot create a curve of their own: they get the seeded examples of - `SWR-PRG-09` and nothing else. This is a real functional gap and the most - likely thing to make somebody reverse the read-only decision. A file import - at provisioning time is probably the cheapest answer. +- [ ] **P3. The program store has no writer, and 17 of its 20 slots have no + purpose.** Answered rather than open: `OQ-09` is resolved, the profiles are + compiled into the firmware and nothing edits them, so `SWR-PRG-07`, + `SWR-PRG-08` and `SWR-PRG-10` are withdrawn. What is left is the machinery + those requirements justified. + + `kiln_program_store_save` and `kiln_program_store_delete` are now called by + **nothing but their own tests**. `SWR-PRG-04` asks for 20 program slots in + non-volatile storage persisting across a firmware update, and the three + programs that exist arrive with the firmware, so the persistence is storing + a copy of something the image already contains. + + Two ways to finish it, and this is a decision rather than a task: + + - **Keep the store, narrow the requirement.** Seeding stays, the slots stay, + `SWR-PRG-04` is reworded to say what it is now for. Cheapest, and leaves + tested code that nothing calls. + - **Read the profiles straight from `kiln_profile_example()`.** The program + store stops existing for programs; `kilnfs` keeps the run records, which + are the other half of `SWA-21`. Removes a subsystem, a partition's worth + of wear, the seeding path and its failure modes, and about 200 lines. It + also deletes tested code, which wants care rather than enthusiasm. + + The second is the one consistent with the decision. Neither should be done + without someone deciding which. - [ ] **P4. Untested against a client.** It compiles and passes analysis. No request has been made of it. The host API suite covers every route's diff --git a/web/app.js b/web/app.js index d4ee7b9..5955bb4 100644 --- a/web/app.js +++ b/web/app.js @@ -86,7 +86,7 @@ const I18N = { save: 'Speichern', copy: 'Kopieren', delete: 'Löschen', - editor: 'Editor', + program: 'Programm', run_history: 'Brandverlauf', dl_csv: 'CSV herunterladen', dl_json: 'JSON herunterladen', @@ -372,24 +372,22 @@ function segRow(seg, i) { rm.title = 'delete segment'; rm.onclick = () => { editing.segments.splice(i, 1); - renderEditor(); + renderProgram(); }; btn.append(rm); tr.append(btn); return tr; } -/* SWR-WEB-26: a viewer, not an editor. Every field is disabled unconditionally - rather than by the program's readonly flag, because nothing here can be - saved whatever the flag says. */ -function renderEditor() { +/* A viewer, and since OQ-09 not a disabled editor either. The profiles are + compiled into the firmware (SWR-PRG-09) and nothing edits them, so the name + and description are text rather than inputs somebody has to discover are + dead, and the readonly flag is no longer worth showing: every program has + it. */ +function renderProgram() { if (!editing) return; $('ed-title').textContent = editing.name || 'Program'; - $('ed-ro').hidden = !editing.readonly; - $('ed-name').value = editing.name || ''; - $('ed-desc').value = editing.description || ''; - $('ed-name').disabled = true; - $('ed-desc').disabled = true; + $('ed-desc').textContent = editing.description || ''; const body = $('ed-body'); body.innerHTML = ''; @@ -398,9 +396,9 @@ function renderEditor() { previewEdit(); } -/* SWR-WEB-12: the resulting curve, previewed before saving. Computed locally so - * it follows every keystroke; the server still re-validates on save, which is - * SWR-WEB-13 and is the check that counts. */ +/* SWR-WEB-12: the curve the selected program describes, drawn locally from + * its segments. It used to preview an edit before saving; there is no saving + * now, so it draws what the firmware will run. */ function previewEdit() { if (!edChart || !editing) return; let t = 0, from = 20; @@ -421,7 +419,7 @@ function previewEdit() { async function editProgram(id) { const p = await get(`/api/programs/${id}`); editing = { ...p, segments: p.segments.map(s => ({ ...s })) }; - renderEditor(); + renderProgram(); } /* --- history (SWR-WEB-09, SWR-WEB-18) ------------------------------------ */ @@ -650,7 +648,7 @@ function show(name) { if (name === 'programs' && !edChart) { edChart = new Chart($('ed-chart'), { interactive: false }); } - if (name === 'programs') loadPrograms().then(renderEditor).catch(() => {}); + if (name === 'programs') loadPrograms().then(renderProgram).catch(() => {}); if (name === 'history') { if (!histChart) histChart = new Chart($('hist-chart')); loadRuns().catch(e => toast($('diag'), e.message, 'bad')); diff --git a/web/index.html b/web/index.html index 49f1de1..3f18ef2 100644 --- a/web/index.html +++ b/web/index.html @@ -118,12 +118,11 @@

Programs

-

Editor

- - - + +

Program

+

@@ -131,14 +130,13 @@

Editor

#Target °CRate °C/h
-
-

- Display only. Firing programs are shown here but are - authored at the kiln, not over the network. + Display only. The firing programs are compiled into the + firmware. They are not edited here, and not at the kiln either: adding + one means building a firmware that carries it.

From 62cd5a4c0a124cf8704b5c1f0f7a78133a63035d Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:45:58 +0200 Subject: [PATCH 02/14] fix: the configuration that ships did not compile, and nothing was building it Checking tasklist O and P found something larger than any item in them. Every CI job builds sdkconfig.qemu, which sets CONFIG_KILN_PLANT_SIM and compiles the WiFi, the HTTP server and the whole hardware path out of main.cpp. The configuration that will actually ship was therefore guarded by no build at all, and O3 claimed "it compiles and passes analysis" on that basis. Built for the first time today. It did not compile: two -Wformat-truncation errors, one in the version string that has been there far longer than the WiFi work and one mine from the network scan. Both are bounded explicitly now, which is the same fix the SSID row in hmi.cpp needed last week and the same lesson: truncation written as "%s" into a shorter buffer reads as an accident, and GCC is right to say so. CI builds the hardware configuration on every push now, with a size report in the job summary. It is a build and not a test, because nothing in CI can run it; what it buys is that the half of the firmware aimed at real hardware cannot rot unnoticed between now and the first board. MEASUREMENTS, replacing numbers that predated several IDF releases: the hardware image is 888 kB, 57 % of the OTA slot free, against O2's claimed 831 kB. The simulated image CI publishes is 246 kB and has no WiFi and no HTTP server in it at all, which is worth keeping straight when reading a size report. the web assets are 18 kB gzipped against architecture 12.4's 44 kB budget: index.html 2.2, app.css 3.6, app.js 8.6, chart.js 4.2. That budget had never been measured against real assets, which P1 gave as a reason it was not done. It passes with room, and embedding them takes the hardware image to about 906 kB. --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++++++ firmware/controller/main/main.cpp | 15 ++++++++++++-- tasklist.md | 34 +++++++++++++++++++++++-------- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0b679a..4943927 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -567,6 +567,39 @@ jobs: echo '```' } >> "$GITHUB_STEP_SUMMARY" + # The HARDWARE configuration, which until now nothing built. Every job + # above uses sdkconfig.qemu, which sets CONFIG_KILN_PLANT_SIM and + # compiles out the WiFi, the HTTP server and the whole hardware path in + # main.cpp. So the configuration that will actually ship was guarded by + # no build at all, and when it was first tried it did not compile: two + # -Wformat-truncation errors in code no CI job had ever seen. + # + # It is a build and not a test: nothing here can run it. What it buys is + # that the half of the firmware aimed at real hardware cannot rot + # unnoticed between now and the first board. + - name: Build the hardware configuration + uses: espressif/esp-idf-ci-action@v1 + with: + esp_idf_version: ${{ env.IDF_VERSION }} + target: esp32s3 + path: firmware/controller + command: | + idf.py -B build-hw -D SDKCONFIG=build-hw/sdkconfig -D SDKCONFIG_DEFAULTS="sdkconfig.defaults" build + idf.py -B build-hw size > build-hw/size.txt + + - name: Summarise the hardware image size + if: always() + run: | + { + echo '### Hardware image size' + echo + echo 'The configuration with WiFi, the HTTP server and the hardware' + echo 'adapters, which the simulated build compiles out.' + echo '```' + head -20 firmware/controller/build-hw/size.txt || echo 'build failed' + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@v4 with: name: firmware-esp32s3-simulated diff --git a/firmware/controller/main/main.cpp b/firmware/controller/main/main.cpp index 9123972..399fbdb 100644 --- a/firmware/controller/main/main.cpp +++ b/firmware/controller/main/main.cpp @@ -525,7 +525,13 @@ void hmi_build_view(kiln_hmi_view_t *v) if (s_system.fw_info != NULL) { (void)s_system.fw_info(s_system.ctx, &fw); } - (void)snprintf(v->version, sizeof(v->version), "%s", fw.version); + /* An explicit bound rather than letting snprintf cut: kiln_fw_info_t + * carries 32 characters and the view has 24, so a long version string + * loses its build metadata here. Stating the width says that is the + * design; "%s" into a shorter buffer reads as an accident, and GCC says + * so (-Wformat-truncation). */ + (void)snprintf(v->version, sizeof(v->version), "%.*s", + (int)(sizeof(v->version) - 1u), fw.version); v->uptime_s = (uint32_t)(esp_timer_get_time() / 1000000); /* SWR-HMI-07: where the web interface is, which is the question an operator @@ -557,7 +563,12 @@ void hmi_build_view(kiln_hmi_view_t *v) } v->net_count = s_scan_count; for (uint8_t i = 0; i < s_scan_count && i < KILN_HMI_MAX_NETWORKS; i++) { - (void)snprintf(v->net_list_ssid[i], KILN_HMI_SSID_LEN, "%s", s_scan[i].ssid); + /* Bounded explicitly for the same reason, and here the compiler is + * making a sharper point: it cannot prove the driver NUL-terminated + * the SSID inside its own field, so without a width the copy could + * run on into the rest of the record. */ + (void)snprintf(v->net_list_ssid[i], KILN_HMI_SSID_LEN, "%.*s", + (int)(KILN_HMI_SSID_LEN - 1u), s_scan[i].ssid); v->net_list_rssi[i] = s_scan[i].rssi; v->net_list_secured[i] = s_scan[i].secured; } diff --git a/tasklist.md b/tasklist.md index 0efca8d..08d0589 100644 --- a/tasklist.md +++ b/tasklist.md @@ -651,14 +651,32 @@ weakness first, the deadline second, the sentence third, the engineering last. IP until mDNS is vendored deliberately. The address is on the HMI network screen, which is where an operator would look anyway. -- [ ] **O2. The image grew by 540 kB.** WiFi takes it from 292 kB to 831 kB, - which is 60.4 % of the OTA slot still free, so `SWR-NFR-13` holds comfortably. - Worth watching once the web assets are embedded (`C9`): that budget is - architecture 12.4's and it has not been measured against a real asset build. - -- [ ] **O3. Untested against a radio.** Same class as `L2`: it compiles and - passes analysis. Nobody has watched it associate, recover from a dropped - connection, or sync time. +- [ ] **O2. The hardware image is 888 kB, 57 % of the slot free.** Measured + 2026-10-09, replacing numbers that predated several releases of ESP-IDF. The + simulated build that CI publishes is **246 kB** and contains no WiFi and no + HTTP server at all, which is worth keeping straight when reading a size + report: `CONFIG_KILN_PLANT_SIM` compiles both out. + + `SWR-NFR-13` holds with room. The web assets measure **18 kB gzipped** in + total against architecture 12.4's 44 kB budget (`index.html` 2.2, `app.css` + 3.6, `app.js` 8.6, `chart.js` 4.2), so embedding them takes the hardware + image to about 906 kB and 55 % free. That budget had never been measured; + it has now, and it passes. + +- [ ] **O3. Untested against a radio, and until today it did not even + compile.** The claim here used to be "it compiles and passes analysis", and + that was false in a way nothing could have caught: every CI job builds + `sdkconfig.qemu`, which sets `CONFIG_KILN_PLANT_SIM` and compiles the WiFi, + the HTTP server and the hardware path out of `main.cpp`. The configuration + that ships was guarded by no build at all. + + Built for the first time on 2026-10-09 and it failed on two + `-Wformat-truncation` errors, one of them older than the WiFi work. Both are + fixed and CI now builds the hardware configuration on every push, so this + cannot recur. + + What remains is what the heading says. Nobody has watched it associate, + recover from a dropped connection, or sync time. The WiFi setup of `O4` is in the same position and matters more, because it is now the **only** way credentials reach the device: a scan that returns From 901a9ec5eb08a921ff6cfce29b13832bb93d8ba3 Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:56:59 +0200 Subject: [PATCH 03/14] feat: the browser interface ships inside the image P1 said the UI is not served: no index.html in the firmware, a browser at the device gets a 404, and the API is usable with curl and nothing else. The four assets are gzipped at build time and linked in as binary data now, and httpd answers them before it consults the route table. VERIFIED WITHOUT HARDWARE, which is the part worth describing. The assets were extracted from the linked ELF by address, inflated, and compared byte for byte against the files in web/. All four round-trip. That is not the same as having served one over a socket, and P4 still says so, but it does establish that what is in the image is the interface rather than something shaped like it. index.html 2281 gz -> 7090 app.css 3656 gz -> 11772 app.js 8846 gz -> 24695 chart.js 4316 gz -> 12762 Architecture 12.4's budget was 44 kB gzipped and had never been measured against real assets, which P1 gave as a reason the work was not done. It is 18.6 kB. The hardware image goes from 888 kB to 908 kB, 56 % of the slot free. THE SIMULATED IMAGE IS UNCHANGED AT 246 kB, and that is the linker rather than a special case: CONFIG_KILN_PLANT_SIM compiles out the call that starts the server, nothing then references the asset table, and --gc-sections drops the blobs with it. The configuration that cannot serve the interface does not carry it, without anybody arranging that. Decisions in here worth the reader's time: Gzipped at BUILD time, not committed compressed, so the source of truth stays the readable file in web/ and nobody has to remember to recompress. `gzip -9 -n`, because without -n the header carries an mtime and the image stops being reproducible from the same sources, which the SBOM and the release both assume. Served still compressed. Every browser that can run this can inflate it, and doing it on the device would cost a window buffer per request on the processor running the kiln. Exact-match lookup against a table of four, no path walking. There is no filesystem behind this, so ".." is not dangerous, it simply matches nothing. Cache-Control: no-cache, which means revalidate rather than do not store. A firmware update changes all four assets at once, and a browser holding a stale app.js with a fresh index.html is the failure worth preventing; a 304 on a LAN costs nothing. Found on the way: GCC rejects "/api/*" inside a block comment (-Wcomment), so the comment says "the API routes" instead. --- README.md | 3 +- docs/architecture.md | 31 +++++-- .../components/kiln_web/CMakeLists.txt | 44 +++++++++ .../components/kiln_web/src/httpd.cpp | 91 +++++++++++++++++++ tasklist.md | 15 +-- 5 files changed, 163 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 36e0a0e..4f03491 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ all green plain and under AddressSanitizer and UBSan. `clang-tidy` is clean on host and target with no suppressions baseline, MC/DC over the supervisor's trip logic is 100 % against an 80 % floor, the `esp32s3` image builds with zero warnings at 246 kB (88 % of the OTA slot free), and QEMU boots that image and -fires it. +fires it. The hardware configuration, which that one compiles out, builds at +908 kB with the browser interface gzipped into it and 56 % of the slot free. The controller is C++20; the supervisor is C++17, which is the standard MISRA C++:2023 is written against. What is done, what is not, and what blocks diff --git a/docs/architecture.md b/docs/architecture.md index 3b1598e..395b987 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -805,13 +805,30 @@ stale and reconnects with backoff rather than showing old data as current ### 12.4 Asset budget -| Asset | Budget (gzipped) | -|---|---| -| `index.html` | 4 kB | -| `app.css` | 6 kB | -| `app.js` (views, SSE, API client) | 24 kB | -| `chart.js` | 10 kB | -| Font | 0, system font stack only | +| Asset | Budget (gzipped) | Measured 2026-10-09 | +|---|---|---| +| `index.html` | 4 kB | 2.2 kB | +| `app.css` | 6 kB | 3.6 kB | +| `app.js` (views, SSE, API client) | 24 kB | 8.6 kB | +| `chart.js` | 10 kB | 4.2 kB | +| Font | 0, system font stack only | 0 | +| **Total** | **44 kB** | **18.6 kB** | + +Measured for the first time when the assets were embedded, and they pass with +room. The four are gzipped at build time by `kiln_web`'s CMakeLists and linked +in as binary data, so the hardware image carries them at 908 kB, 56 % of the +OTA slot still free. + +They are served **still compressed**, with `Content-Encoding: gzip`: every +browser that can run this interface can inflate it, and inflating on the device +would cost a window buffer per request on the processor that is also running a +kiln. + +The simulated image is **unchanged at 246 kB**, and that is the linker rather +than a special case. `CONFIG_KILN_PLANT_SIM` compiles out the call that starts +the server, nothing then references the asset table, and `--gc-sections` drops +the blobs with it. The configuration that cannot serve the interface does not +carry it. | **Total** | **≤ 48 kB** embedded in the image (`SWA-11`) | ## 13. Cross-cutting concerns diff --git a/firmware/controller/components/kiln_web/CMakeLists.txt b/firmware/controller/components/kiln_web/CMakeLists.txt index b70800c..35f31fd 100644 --- a/firmware/controller/components/kiln_web/CMakeLists.txt +++ b/firmware/controller/components/kiln_web/CMakeLists.txt @@ -19,6 +19,50 @@ if(ESP_PLATFORM) INCLUDE_DIRS include REQUIRES kiln_app kiln_core kiln_ports esp_http_server ) + + # --- the browser interface, gzipped into the image (SWA-11, SWR-WEB-02) + # + # Gzipped at build time rather than committed compressed: the source of + # truth stays the readable file in web/, and nobody has to remember to + # recompress after an edit. `gzip -9 -n` for the -n: without it the + # header carries the mtime and the image stops being reproducible from + # the same sources, which tools/sbom.py and the release both assume. + # + # Served with Content-Encoding: gzip rather than decompressed on the + # device. Every browser that can run the interface can inflate it, and + # doing it here costs 38 kB of flash and no RAM at all, where inflating + # on demand would cost a window buffer per request on the processor that + # is also running a kiln. + set(KILN_WEB_ASSETS index.html app.css app.js chart.js) + set(KILN_WEB_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../web") + set(KILN_WEB_GZ_DIR "${CMAKE_CURRENT_BINARY_DIR}/assets") + file(MAKE_DIRECTORY "${KILN_WEB_GZ_DIR}") + + foreach(asset ${KILN_WEB_ASSETS}) + set(src "${KILN_WEB_SRC_DIR}/${asset}") + set(gz "${KILN_WEB_GZ_DIR}/${asset}.gz") + if(NOT EXISTS "${src}") + message(FATAL_ERROR + "web asset ${src} is missing: the interface would build " + "without it and 404 at run time, which is worse than failing " + "here") + endif() + add_custom_command( + OUTPUT "${gz}" + COMMAND gzip -9 -n -c "${src}" > "${gz}" + DEPENDS "${src}" + COMMENT "gzip ${asset}" + VERBATIM + ) + list(APPEND KILN_WEB_GZ "${gz}") + target_add_binary_data(${COMPONENT_LIB} "${gz}" BINARY) + endforeach() + + # The custom commands are attached to a target of their own so they run + # before the component is linked; target_add_binary_data alone does not + # create that dependency for a generated file. + add_custom_target(kiln_web_assets DEPENDS ${KILN_WEB_GZ}) + add_dependencies(${COMPONENT_LIB} kiln_web_assets) else() add_library(kiln_web STATIC ${KILN_WEB_SRCS}) target_include_directories(kiln_web PUBLIC include) diff --git a/firmware/controller/components/kiln_web/src/httpd.cpp b/firmware/controller/components/kiln_web/src/httpd.cpp index 79b916d..835b7af 100644 --- a/firmware/controller/components/kiln_web/src/httpd.cpp +++ b/firmware/controller/components/kiln_web/src/httpd.cpp @@ -23,6 +23,20 @@ * Neither is load-bearing alone and both are cheap. * * --------------------------------------------------------------------------- + * The interface itself (SWA-11, SWR-WEB-02) + * --------------------------------------------------------------------------- + * The four browser assets are gzipped into the image by this component's + * CMakeLists and served from flash, which is what SWR-WEB-02 means by every + * asset coming from the device: no CDN, no external font, nothing that + * reaches off the box, and the interface works on a network with no route to + * anywhere. + * + * They are served **still compressed**, with Content-Encoding: gzip. Every + * browser that can run this interface can inflate it, and doing it on the + * device would cost a window buffer per request on the processor that is also + * running a kiln. + * + * --------------------------------------------------------------------------- * SWR-NFR-02: this must not delay a control cycle * --------------------------------------------------------------------------- * The server runs on core 0 with the rest of the UI (SWA-15), at a priority @@ -54,6 +68,67 @@ static httpd_ctx_t s_http; /* --- helpers ------------------------------------------------------------ */ +/* --- the embedded interface -------------------------------------------- + * + * The symbol names are the ones the build generates for an embedded binary: + * _binary__gz_start and _end. Declared here + * rather than generated into a header because there are four of them and a + * generator for four lines would be the more complicated answer. + */ +#define KILN_ASSET(sym) \ + extern const uint8_t sym##_start[] asm("_" #sym "_start"); \ + extern const uint8_t sym##_end[] asm("_" #sym "_end") + +KILN_ASSET(binary_index_html_gz); +KILN_ASSET(binary_app_css_gz); +KILN_ASSET(binary_app_js_gz); +KILN_ASSET(binary_chart_js_gz); + +typedef struct { + const char *path; + const char *type; + const uint8_t *start; + const uint8_t *end; +} asset_t; + +static const asset_t k_assets[] = { + /* "/" first: it is the one every browser asks for. */ + { "/", "text/html", binary_index_html_gz_start, binary_index_html_gz_end }, + { "/index.html","text/html", binary_index_html_gz_start, binary_index_html_gz_end }, + { "/app.css", "text/css", binary_app_css_gz_start, binary_app_css_gz_end }, + { "/app.js", "text/javascript", binary_app_js_gz_start, binary_app_js_gz_end }, + { "/chart.js", "text/javascript", binary_chart_js_gz_start, binary_chart_js_gz_end }, +}; + +/* The asset for this exact path, or NULL. + * + * An exact match and no path walking of any kind: there is no filesystem + * behind this and no directory to escape from, so ".." is not special, it + * simply matches nothing. */ +static const asset_t *find_asset(const char *path) +{ + for (size_t i = 0; i < sizeof(k_assets) / sizeof(k_assets[0]); i++) { + if (strcmp(path, k_assets[i].path) == 0) { + return &k_assets[i]; + } + } + return NULL; +} + +static esp_err_t send_asset(httpd_req_t *req, const asset_t *a) +{ + const size_t len = (size_t)(a->end - a->start); + (void)httpd_resp_set_type(req, a->type); + (void)httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); + /* The assets change only when the firmware does, and a firmware update + * changes every one of them at once, so a browser holding a stale pair of + * app.js and index.html is the failure to avoid. no-cache means + * revalidate, not "do not store": the 304 costs nothing on a LAN and the + * pair can never be mismatched. */ + (void)httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); + return httpd_resp_send(req, (const char *)a->start, (ssize_t)len); +} + static const char *status_line(int code) { switch (code) { @@ -123,6 +198,16 @@ static esp_err_t handle_any(httpd_req_t *req) memcpy(uri, req->uri, uri_len + 1u); const char *query = split_query(uri); + /* The interface before the API, and only on an exact match: every path + * that is not one of the four assets falls through to the route table, + * which answers the API routes and produces the 404 for anything else. */ + if (strncmp(uri, "/api/", 5) != 0) { + const asset_t *a = find_asset(uri); + if (a != NULL) { + return send_asset(req, a); + } + } + kiln_api_req_t ar = {}; ar.method = to_kiln_method((int)req->method); ar.path = uri; @@ -222,6 +307,12 @@ kiln_err_t kiln_httpd_start(kiln_api_ctx_t *api) ESP_LOGI(TAG, "listening on port %u, GET only (SWR-WEB-26)", (unsigned)cfg.server_port); + ESP_LOGI(TAG, "interface: %u bytes gzipped across %u assets", + (unsigned)((binary_index_html_gz_end - binary_index_html_gz_start) + + (binary_app_css_gz_end - binary_app_css_gz_start) + + (binary_app_js_gz_end - binary_app_js_gz_start) + + (binary_chart_js_gz_end - binary_chart_js_gz_start)), + (unsigned)(sizeof(k_assets) / sizeof(k_assets[0]))); return KILN_OK; } diff --git a/tasklist.md b/tasklist.md index 08d0589..3c64a58 100644 --- a/tasklist.md +++ b/tasklist.md @@ -35,7 +35,7 @@ reused: gaps in the numbering are items that have been closed. | | | Open | |---|---|---| | [A](#a-schematic-and-pcb) | Schematic and PCB | 22 | -| [C](#c-build-test-and-ci-infrastructure) | Build, test and CI | 8 | +| [C](#c-build-test-and-ci-infrastructure) | Build, test and CI | 7 | | [D](#d-documentation-and-open-questions) | Documentation and open questions | 2 | | [F](#f-static-analysis) | Static analysis | 2 | | [G](#g-door-interlock-sr-31) | Door interlock | 3 | @@ -47,7 +47,7 @@ reused: gaps in the numbering are items that have been closed. | [M](#m-kiln_hmi-the-local-interface) | `kiln_hmi` | 3 | | [N](#n-security-obligations-and-the-cyber-resilience-act) | Security obligations and the CRA | 5 | | [O](#o-wifi-fr-net) | WiFi | 4 | -| [P](#p-http-transport-and-the-api) | HTTP transport and the API | 4 | +| [P](#p-http-transport-and-the-api) | HTTP transport and the API | 3 | | [Q](#q-the-file-store-ad-21) | The file store | 5 | | [R](#r-the-independent-safety-supervisor-ad-22) | Independent safety supervisor | 6 | @@ -236,11 +236,6 @@ reused: gaps in the numbering are items that have been closed. - [ ] **C7. `tools/logdump`.** Decode a log partition dump to CSV, also the cross-check for the `logrec` codec. -- [ ] **C9. Vendor the web assets into the firmware.** `web/` is no longer empty: - it carries the UI, read-only and translated. What is missing is the build step - that gzips it into `kiln_web/assets` so `SWA-11` holds and the assets ship - inside the image, within the budget of architecture §12.4. - - [ ] **C11. `main.cpp` and `httpd.cpp` are analysed by nothing.** Found while starting section P. `tools/tidy.sh` drives from the host compile database, which cannot reach either file; `tools/tidy-target.sh` hardcodes @@ -714,12 +709,6 @@ weakness first, the deadline second, the sentence third, the engineering last. ## P. HTTP transport and the API -- [ ] **P1. The UI is not served.** The transport answers `/api/*` and nothing - else: `C9`'s asset embedding is not done, so there is no `index.html` in the - image and a browser at the device's address gets a 404. The API is usable - with `curl` today. `SWA-11` wants the assets gzipped into the image, which - also needs the budget of architecture 12.4 measured for the first time. - - [ ] **P2. Server-Sent Events are not implemented.** `SWR-WEB-05` wants live values pushed at least once a second, and `kiln_api_telemetry_event()` is written and tested for exactly that, but the transport has no `/api/events` From 1ccc0e7c56c68e76c970145a4c9eb02a5c862957 Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:13:54 +0200 Subject: [PATCH 04/14] feat: kiln.local, by way of one dependency with conditions on it SWR-NET-04 has been open since mDNS left the ESP-IDF tree for the component registry. The options were vendoring several thousand lines of somebody else's protocol implementation, dropping the requirement, or taking the dependency; UR-CON-04 forbade the third only insofar as it forbade an unpinned fetch, so it is reworded to say what makes a fetched component auditable rather than to forbid fetching: 1. pinned exactly, never to a range, 2. the resolved dependencies.lock committed, so what a release was built from is recoverable from the repository alone, 3. present in the SBOM with its licence. Web assets stay vendored at any version: they are reachable from the network at run time and the component manager's integrity check does not cover them. espressif/mdns 1.14.0 is Apache-2.0, which distributes under GPL-3.0. tools/sbom.py gains managed_component(), because the generic path described this as esp-idf-espressif__mdns at v6.0.1 -- the IDF version, for the one entry in the inventory whose own version is the point. A CRA Article 13(1) question about a known vulnerability is answered by matching a version, so it is now listed as espressif/mdns 1.14.0 with pkg:idf/espressif/mdns@1.14.0 and the registry content hash. Folded YAML values are unfolded rather than cut mid-word. 12 new self-tests, 85/85. ADV-6 no longer claims there is no package manager at build time, which had become false. TH-13 pointed at the withdrawn SWR-NET-05 (the access point's passphrase rule) for a threat about mDNS; it points at SWR-NET-04. Hardware image 940 kB, 54 % of the slot free: mDNS costs 32 kB. The simulated image is unchanged at 246 kB, since hal_net is compiled out of it. --- .gitignore | 4 +- README.md | 2 +- docs/01_user_req.sdoc | 10 +- docs/architecture.md | 2 +- docs/security.sdoc | 6 +- .../kiln_hal_esp32s3/idf_component.yml | 31 +++++ .../kiln_hal_esp32s3/src/hal_net.cpp | 37 ++++-- firmware/controller/dependencies.lock | 20 +++ tasklist.md | 18 +-- tools/sbom.py | 116 ++++++++++++++++++ 10 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 firmware/controller/components/kiln_hal_esp32s3/idf_component.yml create mode 100644 firmware/controller/dependencies.lock diff --git a/.gitignore b/.gitignore index 0999428..8b3ab31 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,10 @@ coverage/ sdkconfig sdkconfig.old sdkconfig.hw +# Fetched by the component manager and reproducible from the lockfile, so it +# is not committed; dependencies.lock IS committed, because UR-CON-04 makes it +# the record of what a release was built from. managed_components/ -dependencies.lock *.o *.d diff --git a/README.md b/README.md index 4f03491..a0d53dd 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ three-phase kiln, so it is out of scope rather than partially supported. | Output | Zero-cross SSR in series with a safety contactor | | Supply | **Single phase only.** One current transformer on the heater conductor | | Door interlock | Optional normally-closed switch, stops the heater immediately when the door opens | -| Connectivity | WiFi station only. **No access point:** the network is selected and its passphrase typed at the display, so setting it up needs somebody at the kiln. mDNS (`kiln.local`) is specified and not implemented, so the device is reached by IP, which the display shows | +| Connectivity | WiFi station only. **No access point:** the network is selected and its passphrase typed at the display, so setting it up needs somebody at the kiln. Reachable at `kiln.local` over mDNS, and by IP, which the display shows | Details and rationale are in [requirements §6](docs/03_software_req.sdoc). diff --git a/docs/01_user_req.sdoc b/docs/01_user_req.sdoc index b959c38..e2d72e0 100644 --- a/docs/01_user_req.sdoc +++ b/docs/01_user_req.sdoc @@ -748,7 +748,15 @@ Logged data and programs shall live on internal flash. **No SD card** shall be r [CONSTRAINT] UID: UR-CON-04 STATEMENT: >>> -The project shall be licensed **GPL-3.0-or-later**, and every third-party component shall carry a licence compatible with distribution under GPL-3.0. Web assets shall be vendored, not fetched at build time from an unpinned source. +The project shall be licensed **GPL-3.0-or-later**, and every third-party component shall carry a licence compatible with distribution under GPL-3.0. + +Third-party code may be fetched at build time only through the ESP-IDF component manager, and only under all three of the following conditions, so that what a given release was built from stays recoverable from the repository alone: + +1. the version is pinned exactly (`==`), never to a range or to the newest available; +2. the resolved `dependencies.lock`, which carries the content hash, is committed; +3. the component appears in the generated SBOM with its licence. + +Web assets shall be vendored rather than fetched, at any version: they are reachable from the network at run time and are not covered by the component manager's integrity check. <<< [CONSTRAINT] diff --git a/docs/architecture.md b/docs/architecture.md index 395b987..6385f7d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -40,7 +40,7 @@ as a decision in [§3](#3-key-decisions). | 3 | **Control and safety must meet hard deadlines regardless of load.** | `SWR-NFR-01`–`SWR-NFR-04` | Core affinity split, priority ordering, no blocking calls on the control path, message passing instead of shared locks. | | 4 | **No SD card, no external database, no external host.** | `UR-CON-03`, `SWR-WEB-02` | Log store as a raw circular flash partition; web assets embedded and gzipped in the firmware image; charting rendered client-side from the device's own API. | | 5 | **Fits an 8 MB / no-PSRAM ESP32-S3 and runs for a week.** | `SWR-NFR-10`–`SWR-NFR-14` | Fixed-size records, static allocation in the core, bounded HTTP buffers, flash write pattern designed against an endurance budget. | -| 6 | **GPL-3.0-or-later, self-contained, vendored dependencies.** | `UR-CON-04` | No build-time network fetch, third-party licences audited and recorded. | +| 6 | **GPL-3.0-or-later, self-contained, dependencies pinned.** | `UR-CON-04` | Web assets vendored. One managed component (`espressif/mdns`), pinned exactly with the content hash in a committed lockfile; third-party licences audited and recorded in the SBOM. | ## 3. Key decisions diff --git a/docs/security.sdoc b/docs/security.sdoc index 38eb1ea..f4273ec 100644 --- a/docs/security.sdoc +++ b/docs/security.sdoc @@ -366,7 +366,7 @@ CAPABILITY: >>> Code execution in the image. <<< REALISTIC: >>> -Low: no package manager at build time, assets vendored ([LINK: UR-CON-04]), dependencies are ESP-IDF itself. +Low: dependencies are ESP-IDF itself plus one pinned managed component (`espressif/mdns`, Apache-2.0), with the content hash in a committed `dependencies.lock`; web assets are vendored, not fetched. [LINK: UR-CON-04] holds the conditions. A compromise of the registry therefore has to produce the hash that is already in the repository, and the upstream of the pinned version remains a path in. <<< [[/SECTION]] @@ -677,7 +677,7 @@ STATEMENT: >>> **mDNS and network reconnaissance.** <<< VIA: >>> -`kiln.local` advertised on the LAN ([LINK: SWR-NET-05]). +`kiln.local` advertised on the LAN ([LINK: SWR-NET-04]). <<< CONSEQUENCE: >>> Makes the device trivially discoverable; a precondition for the rest rather than a threat alone. @@ -685,7 +685,7 @@ Makes the device trivially discoverable; a precondition for the rest rather than SAFETY_RELEVANT: No STATUS: Active COMMENT: >>> -**Has no security goal, and never had one.** Discoverability is accepted: the device is meant to be found by its operator, and `SWR-NET-05` asks for exactly that. Recorded so the acceptance is explicit rather than an omission. +**Has no security goal, and never had one.** Discoverability is accepted: the device is meant to be found by its operator, and `SWR-NET-04` asks for exactly that. Recorded so the acceptance is explicit rather than an omission. Live since 2026-10-09, when mDNS was implemented; the link used to name the withdrawn `SWR-NET-05`, which was the access point's passphrase rule. <<< RELATIONS: - TYPE: Parent diff --git a/firmware/controller/components/kiln_hal_esp32s3/idf_component.yml b/firmware/controller/components/kiln_hal_esp32s3/idf_component.yml new file mode 100644 index 0000000..9edd7c1 --- /dev/null +++ b/firmware/controller/components/kiln_hal_esp32s3/idf_component.yml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2026 Bitcrush Testing +# SPDX-License-Identifier: GPL-3.0-or-later +# +# The one managed dependency this project has (UR-CON-04). +# +# mDNS left the ESP-IDF tree for the component registry, and SWR-NET-04 asks for +# kiln.local, so the choice was between vendoring several thousand lines of +# somebody else's protocol implementation, dropping the requirement, or taking +# the dependency under conditions that make it auditable. UR-CON-04 states +# those conditions and this file meets them: +# +# PINNED TO AN EXACT VERSION, not a range. "~1.14" would let a build pick up +# code nobody reviewed, which is the whole thing the constraint exists to +# prevent; a release has to be reproducible from its tag years later. +# +# dependencies.lock IS COMMITTED, so the resolved version and its checksum +# are in the repository rather than in whatever the registry served that +# afternoon. +# +# IT APPEARS IN THE SBOM automatically, by its registry name and its OWN +# version rather than the ESP-IDF version it was built against, because the +# version is the only thing an advisory can be matched against. tools/sbom.py +# reads the components the build actually linked and scans each one's SPDX +# tags, so this one is listed as espressif/mdns 1.14.0, Apache-2.0 (checked: +# compatible with distribution under GPL-3.0), with the registry content hash +# in its description, and nobody maintains a list. +# +# managed_components/ stays out of git: it is a cache, the lockfile is the +# record, and committing both would be two sources of truth for one fact. +dependencies: + espressif/mdns: "==1.14.0" diff --git a/firmware/controller/components/kiln_hal_esp32s3/src/hal_net.cpp b/firmware/controller/components/kiln_hal_esp32s3/src/hal_net.cpp index 3fca617..b05a178 100644 --- a/firmware/controller/components/kiln_hal_esp32s3/src/hal_net.cpp +++ b/firmware/controller/components/kiln_hal_esp32s3/src/hal_net.cpp @@ -1,8 +1,7 @@ /* SPDX-FileCopyrightText: 2026 Bitcrush Testing * SPDX-License-Identifier: GPL-3.0-or-later * - * WiFi station, scanning, joining and SNTP (SWR-NET-01..SWR-NET-12, - * except SWR-NET-04: see the note on mDNS below). + * WiFi station, scanning, joining, mDNS and SNTP: SWR-NET-01 to SWR-NET-12. * * --------------------------------------------------------------------------- * SWR-NET-07 is the requirement that shapes this file @@ -24,6 +23,8 @@ #include #include +#include "mdns.h" + #include "esp_event.h" #include "esp_log.h" #include "esp_mac.h" @@ -380,17 +381,31 @@ kiln_err_t kiln_hal_net_init(const kiln_config_t *cfg, kiln_port_net_t *out) return KILN_ERR_IO; } - /* SWR-NET-04 wants kiln.local, and it is **not implemented here**. + /* SWR-NET-04: kiln.local. * - * mDNS left the ESP-IDF tree for the component manager, and UR-CON-04 forbids - * a build-time fetch from an unpinned source -- the same constraint that - * has LittleFS waiting to be vendored (tasklist E7). Adding a managed - * dependency to get a convenience feature would be the wrong trade against - * a constraint the project applies to everything else, so the device is - * reachable by IP until mDNS is vendored deliberately. + * The project's one managed dependency, pinned exactly and locked, on the + * terms UR-CON-04 now sets out; see this component's idf_component.yml for + * why that was the trade rather than vendoring a protocol implementation + * or dropping the requirement. * - * The address is on the network screen (SWR-HMI-07), which is where an - * operator standing at the kiln would look for it anyway. */ + * Best effort, like everything else in this file. An operator who cannot + * resolve the name still has the IP on the network screen (SWR-HMI-07), + * and SWR-NET-07 means none of this can touch a firing either way. */ + if (mdns_init() == ESP_OK) { + (void)mdns_hostname_set(s_net.hostname); + (void)mdns_instance_name_set("Safe Kiln Controller"); + /* Advertised so that a browser or a phone finds the interface by + * looking rather than by being told an address. It is the read-only + * interface of SWR-WEB-26: advertising it exposes nothing that an + * HTTP GET to the same port does not. */ + (void)mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0); + ESP_LOGI(TAG, "mdns: %s.local", s_net.hostname); + } + else { + /* Not a failure of anything the kiln needs. The address is on the + * display and the API answers on it. */ + ESP_LOGW(TAG, "mdns did not start; reach the device by IP"); + } /* SWR-NET-06. Best effort by design: no route to the internet is normal in * a workshop, and warning 105 says so rather than anything failing. */ diff --git a/firmware/controller/dependencies.lock b/firmware/controller/dependencies.lock new file mode 100644 index 0000000..56f1d22 --- /dev/null +++ b/firmware/controller/dependencies.lock @@ -0,0 +1,20 @@ +dependencies: + espressif/mdns: + component_hash: b5b30022c46302eab89c1d2e3b6fb5918a627563a7462f0ee284b22b9a934967 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.14.0 + idf: + source: + type: idf + version: 6.0.1 +direct_dependencies: +- espressif/mdns +manifest_hash: 3508f391dcb73e00fc839bb5085e60138fbb3972631b238d446d4efed1901376 +target: esp32s3 +version: 3.0.0 diff --git a/tasklist.md b/tasklist.md index 3c64a58..0063f44 100644 --- a/tasklist.md +++ b/tasklist.md @@ -636,17 +636,7 @@ weakness first, the deadline second, the sentence third, the engineering last. ## O. WiFi, FR-NET -- [ ] **O1. `SWR-NET-04` (mDNS, `kiln.local`) is not implemented.** mDNS left - the ESP-IDF tree for the component manager, and `UR-CON-04` forbids a - build-time fetch from an unpinned source. `E7` met the same constraint over - the file store and resolved it by not needing the dependency at all; mDNS has - no such escape, since the protocol is the feature. Adding a managed - dependency for a convenience feature would be the wrong trade against a - constraint the project applies everywhere else, so the device is reachable by - IP until mDNS is vendored deliberately. The address is on the HMI network screen, which is - where an operator would look anyway. - -- [ ] **O2. The hardware image is 888 kB, 57 % of the slot free.** Measured +- [ ] **O2. The hardware image is 940 kB, 54 % of the slot free.** Measured 2026-10-09, replacing numbers that predated several releases of ESP-IDF. The simulated build that CI publishes is **246 kB** and contains no WiFi and no HTTP server at all, which is worth keeping straight when reading a size @@ -654,9 +644,9 @@ weakness first, the deadline second, the sentence third, the engineering last. `SWR-NFR-13` holds with room. The web assets measure **18 kB gzipped** in total against architecture 12.4's 44 kB budget (`index.html` 2.2, `app.css` - 3.6, `app.js` 8.6, `chart.js` 4.2), so embedding them takes the hardware - image to about 906 kB and 55 % free. That budget had never been measured; - it has now, and it passes. + 3.6, `app.js` 8.6, `chart.js` 4.2). With them embedded and mDNS linked in, + the hardware image measures **940 kB, 54 % of the slot free**. That budget + had never been measured; it has now, and it passes. - [ ] **O3. Untested against a radio, and until today it did not even compile.** The claim here used to be "it compiles and passes analysis", and diff --git a/tools/sbom.py b/tools/sbom.py index d159ab3..a7a8ffd 100755 --- a/tools/sbom.py +++ b/tools/sbom.py @@ -465,6 +465,72 @@ def make_component(name, version, licenses, ctype="library", purl=None, } +def managed_component(name, directory): + """A component the component manager fetched, described as what it is. + + These are the only third-party sources in the image that did not arrive with + ESP-IDF, so they are the only ones whose version an advisory can be matched + against. Describing one as `esp-idf-x` at the IDF version, which is what the + generic path below would do, states the wrong version for the one entry where + the version is the whole point: UR-CON-04 requires the exact pin and the + committed lockfile precisely so that a CRA Article 13(1) question about a + known vulnerability can be answered from the repository. + + Returns None when the directory is not a managed component, so the caller + falls through to treating it as part of IDF. + """ + if "managed_components" not in pathlib.Path(directory).parts: + return None + d = pathlib.Path(directory) + meta = {} + manifest = d / "idf_component.yml" + if manifest.is_file(): + # Read by hand rather than with a YAML parser: this is two flat keys out + # of a file the component manager wrote, and pulling in PyYAML to get + # them would make a build-time gate depend on a package that may not be + # in the CI container's python. + lines = manifest.read_text(errors="replace").splitlines() + for i, line in enumerate(lines): + for key in ("version", "description"): + prefix = key + ":" + if not line.startswith(prefix) or key in meta: + continue + value = line[len(prefix):].strip().strip("'\"") + # The manager writes long values folded, so the rest of the + # sentence sits on the following indented lines. Without this + # the description ends mid-word, which reads as a bug in the + # SBOM rather than as what it is. + for cont in lines[i + 1:]: + if not cont[:1].isspace() or not cont.strip(): + break + value += " " + cont.strip() + meta[key] = value + # `espressif__mdns` is the directory name the manager uses for `espressif/mdns`. + registry_name = name.replace("__", "/", 1) + chash = "" + hashfile = d / ".component_hash" + if hashfile.is_file(): + chash = hashfile.read_text().strip() + version = meta.get("version", "") + description = ( + f"Managed component `{registry_name}` {version or 'at an unknown version'}, " + f"fetched from the ESP-IDF component registry and linked into the image. " + f"Pinned exactly and locked per UR-CON-04") + if chash: + description += f"; registry content hash {chash}" + if meta.get("description"): + description += f". Upstream describes it as: {meta['description']}" + licences = scan_licenses(d) + if not licences: + licences = [NOASSERTION] + return make_component( + registry_name, version, licences, + purl=f"pkg:idf/{registry_name}@{version}" if version else None, + description=description, + supplier="Espressif Systems", + ) + + def web_assets(version=None): """The browser assets, with hashes, and the point they make by being listed. @@ -583,6 +649,16 @@ def build_inventory(build_dir=None, image=None, supervisor_elf=None, repo=ROOT): )) for name, directory in sorted(desc["components"].items()): + managed = managed_component(name, directory) + if managed is not None: + if managed["licenses"] == [NOASSERTION]: + warnings.append(( + "licence", + f"managed component {managed['name']} carries no SPDX tag, " + "and UR-CON-04 allows a fetched component only when its " + "licence is recorded")) + components.append(managed) + continue licences = scan_licenses(directory) description = f"ESP-IDF component `{name}`, linked into the image" if not licences: @@ -932,6 +1008,46 @@ def raises(what, fn): raises("a build dir with no description", lambda: find_build_dir(d)) + # --- managed components, whose version is the point ------------------- + with tempfile.TemporaryDirectory() as tmp: + d = pathlib.Path(tmp) / "managed_components" / "espressif__mdns" + d.mkdir(parents=True) + (d / "idf_component.yml").write_text( + "dependencies:\n idf:\n version: '>=5.0'\n" + "description: Multicast UDP service used to provide local network\n" + " service and host discovery.\n" + "version: 1.14.0\n") + (d / ".component_hash").write_text("b5b3002\n") + (d / "mdns.c").write_bytes(b"/* SPDX-License-Identifier: Apache-2.0 */\n") + c = managed_component("espressif__mdns", str(d)) + check("a managed component is recognised", c is not None, True) + check("named as the registry names it", c["name"], "espressif/mdns") + check("its own version, not IDF's", c["version"], "1.14.0") + check("licence from its sources", c["licenses"], ["Apache-2.0"]) + check("purl identifies it for an advisory match", + c["purl"], "pkg:idf/espressif/mdns@1.14.0") + check("the locked hash is recorded", + "b5b3002" in c["description"], True) + check("and the upstream description, unfolded rather than cut mid-line", + c["description"].endswith( + "Multicast UDP service used to provide local network service " + "and host discovery."), True) + # The version key of a *dependency* is indented and must not be mistaken + # for the component's own, which is the one an advisory is matched on. + check("an indented version key is not the component's", + c["version"], "1.14.0") + plain = pathlib.Path(tmp) / "components" / "freertos" + plain.mkdir(parents=True) + check("a component inside IDF is not treated as managed", + managed_component("freertos", str(plain)), None) + bare = pathlib.Path(tmp) / "managed_components" / "someone__thing" + bare.mkdir(parents=True) + b = managed_component("someone__thing", str(bare)) + check("no manifest, no version asserted", b["version"], NOASSERTION) + check("and no purl rather than a wrong one", b["purl"], None) + check("an untagged managed component is NOASSERTION, not silent", + b["licenses"], [NOASSERTION]) + # --- the documents --------------------------------------------------- product = make_component( "safekiln", "1.4.0", [PROJECT_LICENCE], ctype="firmware", From d879471f9d27fa70bb91c65e73274eb6a6cf62ab Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:33:27 +0200 Subject: [PATCH 05/14] feat: the file store proves its writes, and retires what cannot hold them Q1 was recorded as an accepted trade and the trade was made against the wrong failure mode. A NOR sector at the end of its life does not report an error: it takes the write, returns success, and holds something else. The store's answer was the CRC on the next read, which on this device is after a firing rather than during the write it could still have redirected. Every write is now read back and re-validated through read_copy, the same path a mount uses, before the in-RAM index points at it. A copy that does not verify retires its region and the write is re-attempted in a free region at a higher seq, up to KILN_FILESLOT_WRITE_TRIES regions. The caller gets KILN_OK, because the file is stored; what changed is where. Retirement needs no bad-block table. After the new copy verifies, the old region is erased best effort: - it succeeds, so no stale copy remains to undo a later remove at the next mount, and the region returns to the pool at the next boot, which is the right answer for a sector that can still be erased rather than a life sentence passed on one bad write; - it fails, which is what a finished sector does, so the stale copy stays at a LOWER seq. Two regions then claim one name, and mount resolves it the only way it can be true: the lower seq is the write that failed, its region is retired again, the higher seq is the file. A power cut between the verify and the erase leaves the same state and is resolved the same way. Still no wear levelling, deliberately: architecture 10.4 has four orders of magnitude of headroom. Five tests, driven by two new knobs on the flash fake -- a sector that accepts a write and keeps zeros, which is the failure no error code reports, and a sector that will not erase, addressed by offset so the test says which region is finished rather than how many erases have gone by. Q2: mount cost measured rather than described, and the description was low by a third. 768 reads and 47 808 bytes on the medium this firmware produces; 8 192 and 524 288 on a partition full of maximum-size files. Asserted against a model derived in the test, so making mount lazy fails the suite instead of happening quietly. Q4 and Q5 were disclosures with nowhere to live: the name-length refusal is now stated in port_filestore.h and architecture 10.6 beside the test that proves it, and run_index's twenty-slot walk is explained in run_index.cpp where somebody revisiting the file will see it. The retired count is logged at boot and is the only warning anybody gets that the part is going. Verified: 25/25 host tests, the same under ASan/UBSan, kiln_core line coverage 91.7 % against the 90 % floor, clang-tidy 20 clean on host and target, both IDF configurations build, and the QEMU smoke test passes all seven assertions on the emulated flash the store actually runs on. --- docs/architecture.md | 58 ++- .../components/kiln_app/src/run_index.cpp | 19 +- .../kiln_core/include/kiln_core/fileslots.h | 78 +++- .../components/kiln_core/src/fileslots.cpp | 191 ++++++++-- .../include/kiln_ports/port_filestore.h | 5 + .../include/kiln_hal_host/hal_host.h | 22 ++ .../host/kiln_hal_host/src/hal_host.cpp | 20 +- firmware/controller/main/main.cpp | 10 + .../test/host/unit/test_fileslots.cpp | 350 ++++++++++++++++++ tasklist.md | 104 ++++-- 10 files changed, 771 insertions(+), 86 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 6385f7d..a3502c0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -689,17 +689,63 @@ nothing can disagree with the data. The loser is left in place rather than erased, because it is the next write's target and erasing it at boot would spend an erase cycle on every power-up. +A name longer than 63 bytes is **refused**, not truncated: `KILN_PATH_MAX` is 64 +including the terminator and the stored name field is exactly that, so a longer +path returns `KILN_ERR_INVALID_ARG`. Both callers produce five-character slot +paths, so nothing generates one today; refusing is the answer because storing a +file under a name the caller did not ask for is worse than failing. + Capacity is 40 of 64 regions for the 20 programs of `SWR-PRG-04` and the 20 run records of `SWR-LOG-09`. Endurance is generous for the same reason the log's is: a program is written when a user saves it and a run record once per firing, so a region sees single-digit erases per year against a 100 000 cycle rating. -Three properties carry the weight and each is a host test, driven through a -flash fake that enforces NOR semantics and can cut power part-way through a -write: a file survives a remount, a cut before the commit leaves the previous -copy, and a cut inside the commit header leaves the previous copy. The same -fake drives `program_store` and `run_index` over the real store in -`test_stores_on_flash`, which is the combination that runs on the board. +**Wear is handled at the write rather than at the read.** A NOR sector at the +end of its life does not report an error: it takes the write, returns success, +and holds something else. Every write is therefore read back and re-validated +through the same path a mount uses, and only then does the in-RAM index point at +it. A copy that does not verify retires its region, and the write is +re-attempted in a free region at a higher sequence number, up to three regions. +The caller gets success, because the file is stored; what changed is where. + +Retirement needs no bad-block table. Once the migrated copy has verified the old +region is erased, best effort: if that succeeds the stale copy is gone, which is +what stops a later delete from being undone at the next mount, and the region +returns to the pool at the next boot, which is the right answer for a sector +that can still be erased. If the erase fails, which is what a sector that is +genuinely finished does, the stale copy stays at a *lower* sequence number, two +regions claim one name, and mount resolves it the only way it can be true: the +lower sequence number is the write that failed, its region is retired again, and +the higher one is the file. A power cut between the verify and the erase leaves +the same state and is resolved the same way. The count of retired regions is +logged at boot, and is the only warning anybody gets that the part is going. + +What the store does **not** do is spread wear. A region is chosen by index and +keeps its file for as long as it works, because the endurance arithmetic above +has four orders of magnitude of headroom and levelling would be mechanism bought +for nothing. Retirement is in because its failure mode is losing a run record, +which is cheap to prevent and annoying to explain. + +**What a mount costs**, measured in `test_fileslots` rather than estimated: the +medium this firmware produces, 20 programs of 398 bytes and 20 run records of +554, costs **768 reads and 47 808 bytes** of CRC. A partition full of +maximum-size files, both copies valid, costs **8 192 reads and 524 288 bytes**, +which is the whole partition. Both are paid once, at start-up, and the +expectation is tens of milliseconds rather than a budget item; if a board ever +says otherwise, the only thing mount needs eagerly is which copy of each region +wins, and the payload CRC could be deferred to the read. The numbers are +asserted so that making mount lazy cannot happen quietly. + +Several properties carry the weight and each is a host test, driven through a +flash fake that enforces NOR semantics, can cut power part-way through a write, +and can model a worn sector that accepts a write and keeps zeros: a file +survives a remount, a cut before the commit leaves the previous copy, a cut +inside the commit header leaves the previous copy, a sector that lies about a +write retires its region and the file is still readable, a region that cannot be +erased stays retired across a reboot, and a part with nowhere left to write says +so instead of losing the previous content. The same fake drives `program_store` +and `run_index` over the real store in `test_stores_on_flash`, which is the +combination that runs on the board. ## 11. Configuration diff --git a/firmware/controller/components/kiln_app/src/run_index.cpp b/firmware/controller/components/kiln_app/src/run_index.cpp index 4b05060..83e110f 100644 --- a/firmware/controller/components/kiln_app/src/run_index.cpp +++ b/firmware/controller/components/kiln_app/src/run_index.cpp @@ -1,5 +1,22 @@ /* SPDX-FileCopyrightText: 2026 Bitcrush Testing - * SPDX-License-Identifier: GPL-3.0-or-later */ + * SPDX-License-Identifier: GPL-3.0-or-later + * + * The 20 most recent firings, one record a slot, oldest overwritten first + * (SWR-LOG-09). + * + * Every operation here walks all 20 slots: append reads them to find the oldest, + * and mark_truncated and baseline read them to find the one they want. That is + * not as expensive as it looks -- the file store answers a read from its in-RAM + * index and a 554 byte CRC, with one region write at the end of an append -- and + * the walk is now the only reason this file reads at all. + * + * It is also the one place where the store being a fixed array rather than a + * directory costs something: with a name to look up rather than a slot to scan, + * each of the three would be one access instead of twenty. Left as it is + * because twenty accesses once per firing is not a cost worth a mechanism, and + * recorded here because it is the natural thing to simplify if this file is ever + * revisited for another reason. + */ #include #include diff --git a/firmware/controller/components/kiln_core/include/kiln_core/fileslots.h b/firmware/controller/components/kiln_core/include/kiln_core/fileslots.h index 52e42c6..17f86d0 100644 --- a/firmware/controller/components/kiln_core/include/kiln_core/fileslots.h +++ b/firmware/controller/components/kiln_core/include/kiln_core/fileslots.h @@ -35,6 +35,50 @@ * at which a reader sees a torn file, which is the property SWR-RUN-08 needs and * the one the host tests cut power to prove. * + * --------------------------------------------------------------------------- + * WEAR, AND WHAT THE STORE DOES ABOUT IT + * --------------------------------------------------------------------------- + * A NOR sector at the end of its life does not report an error. It takes the + * write, returns success, and holds something other than what it was given. + * An implementation that trusts the return code therefore discovers the loss + * at the next read, which on this device is after a firing rather than during + * the write it could still have redirected. + * + * So every write is proved before it is believed: the copy is read back and + * re-validated through the same path a mount uses, and only then does the + * in-RAM index point at it. A copy that does not verify retires its region -- + * the region is excluded from further allocation for this power cycle -- and + * the write is re-attempted in a free region at a higher seq. The caller gets + * KILN_OK, because the file is stored; what changed is where. + * + * Retirement needs no bad-block table, because the medium carries the evidence + * on its own. Once the migrated copy has verified, the retired region is + * erased, best effort: + * + * - if the erase succeeds the stale copy is gone, which is what keeps a later + * remove from being undone at the next mount by an old copy of the file + * that nobody asked to keep. The region returns to the pool at the next + * boot and will be retired again the next time it fails, which for a sector + * that can still be erased is the right answer rather than a life sentence + * passed on one bad write; + * - if the erase fails, which is what a sector that is actually gone does, + * the stale copy stays at a LOWER seq than the migrated one. Two regions + * then claim one name, and mount resolves it the only way it can be true: + * the lower seq is the write that failed, so its region is retired again + * and the higher seq is the file. Retirement survives the power cycle + * exactly in the case that deserves it. + * + * A power cut between the verify and the erase leaves both copies on the + * medium, which is the same state and is resolved the same way. + * + * What this does NOT do is spread wear. A region is chosen by index and keeps + * the file for as long as it works; at 40 regions of two sectors, written + * single-digit times a year against a 100 000 cycle rating, the endurance + * arithmetic of architecture 10.4 has four orders of magnitude of headroom and + * levelling would be mechanism bought for nothing. Retirement is in because + * its failure mode is losing a run record, which is cheap to prevent and + * annoying to explain. + * * Pure logic over kiln_port_flash: no clock, no allocation, all state in the * caller's struct (SWA-02, SWA-03). */ @@ -53,6 +97,14 @@ constexpr size_t KILN_FILESLOT_REGIONS_MAX = 64u; /* Byte offsets above. Payload capacity is sector_bytes - KILN_FILESLOT_HDR. */ constexpr uint32_t KILN_FILESLOT_HDR = 76u; +/* How many regions one write may be attempted in before it gives up and + * returns the medium's error. Three rather than "all of them": each attempt + * spends an erase cycle and a write on a part that is already misbehaving, and + * a store that cannot place a file in three separate regions has a problem that + * a fourth attempt does not fix. One is too few, because the first retirement + * is exactly the case this exists for. */ +constexpr unsigned KILN_FILESLOT_WRITE_TRIES = 3u; + typedef struct { char name[KILN_PATH_MAX]; uint32_t seq; /* of the live copy */ @@ -70,15 +122,32 @@ typedef struct { uint16_t payload_max; bool mounted; kiln_fileslot_entry_t entry[KILN_FILESLOT_REGIONS_MAX]; + /* Retired: a region whose write did not verify, or which mount found + * holding the losing copy of a name. Never allocated again until a + * format. One byte a region rather than a bitmap, because 64 bytes of RAM + * is not worth the shift-and-mask at every allocation. */ + bool retired[KILN_FILESLOT_REGIONS_MAX]; } kiln_fileslots_t; /* Scan every region and build the index. A region whose two copies are both * invalid is free; where both are valid the higher seq wins and the loser is * left alone, because it is the next write's target and erasing it here would - * spend an erase cycle on every boot. */ + * spend an erase cycle on every boot. + * + * Two REGIONS holding the same name is the trace a retirement leaves, and is + * resolved here: higher seq is the file, and the other region is retired. See + * the wear note above. + * + * Mount reads and CRCs every copy, which is 768 reads and 47 kB on the medium + * this firmware produces and 8 192 reads and 512 kB on a full one of + * maximum-size files; both are measured in test_fileslots.cpp. That is the + * cost of knowing at start-up what is on the medium, and it is paid once. */ kiln_err_t kiln_fileslots_mount(kiln_fileslots_t *fs, const kiln_port_flash_t *flash); -/* Erase every region. FR-CFG-12's factory reset. */ +/* Erase every region and un-retire all of them. FR-CFG-12's factory reset: a + * part that failed a verify may have been failing for a reason that a full + * erase clears, and refusing to try again after an explicit factory reset would + * leave the operator with no way to find out. */ kiln_err_t kiln_fileslots_format(kiln_fileslots_t *fs); /* Present the mounted store as the port the application holds. */ @@ -87,4 +156,9 @@ void kiln_fileslots_bind(kiln_fileslots_t *fs, kiln_port_filestore_t *out); /* How many regions hold a file, for the log line at boot and the tests. */ uint16_t kiln_fileslots_used_regions(const kiln_fileslots_t *fs); +/* How many regions have been retired. Logged at boot: a store that is quietly + * losing regions is a part on its way out, and the number is the only warning + * anybody gets. */ +uint16_t kiln_fileslots_retired_regions(const kiln_fileslots_t *fs); + #endif /* KILN_CORE_FILESLOTS_H */ diff --git a/firmware/controller/components/kiln_core/src/fileslots.cpp b/firmware/controller/components/kiln_core/src/fileslots.cpp index 32357ee..358807f 100644 --- a/firmware/controller/components/kiln_core/src/fileslots.cpp +++ b/firmware/controller/components/kiln_core/src/fileslots.cpp @@ -126,7 +126,7 @@ uint16_t find_region(const kiln_fileslots_t *fs, const char *name) uint16_t free_region(const kiln_fileslots_t *fs) { for (uint16_t r = 0; r < fs->region_count; r++) { - if (!fs->entry[r].used) { + if (!fs->entry[r].used && !fs->retired[r]) { return r; } } @@ -182,6 +182,29 @@ kiln_err_t kiln_fileslots_mount(kiln_fileslots_t *fs, const kiln_port_flash_t *f } } + /* Two regions claiming one name is the trace of a retirement (see the wear + * note in the header). find_region returns the first match, so leaving + * both would serve whichever happens to sit at the lower index, which half + * the time is the copy a failed write left behind. The higher seq is the + * file; the other region is the one that failed and is retired again. */ + for (uint16_t r = 0; r < fs->region_count; r++) { + if (!fs->entry[r].used) { + continue; + } + for (uint16_t q = (uint16_t)(r + 1u); q < fs->region_count; q++) { + if (!fs->entry[q].used || + (strcmp(fs->entry[q].name, fs->entry[r].name) != 0)) { + continue; + } + const uint16_t loser = (fs->entry[q].seq > fs->entry[r].seq) ? r : q; + fs->retired[loser] = true; + fs->entry[loser] = kiln_fileslot_entry_t{}; + if (loser == r) { + break; /* r holds nothing now; nothing else can match it */ + } + } + } + fs->mounted = true; return KILN_OK; } @@ -198,10 +221,25 @@ kiln_err_t kiln_fileslots_format(kiln_fileslots_t *fs) return e; } fs->entry[r] = kiln_fileslot_entry_t{}; + fs->retired[r] = false; } return KILN_OK; } +uint16_t kiln_fileslots_retired_regions(const kiln_fileslots_t *fs) +{ + if ((fs == nullptr) || !fs->mounted) { + return 0u; + } + uint16_t n = 0; + for (uint16_t r = 0; r < fs->region_count; r++) { + if (fs->retired[r]) { + n++; + } + } + return n; +} + uint16_t kiln_fileslots_used_regions(const kiln_fileslots_t *fs) { if ((fs == nullptr) || !fs->mounted) { @@ -263,29 +301,22 @@ kiln_err_t fs_read(void *ctx, const char *path, void *out, size_t cap, size_t *l return KILN_OK; } -kiln_err_t fs_write_atomic(void *ctx, const char *path, const void *data, size_t len) +/* Write one copy and prove it. + * + * Everything physical about a write is in here, so that the retry above it has + * one thing to call and one outcome to interpret: the copy is there and + * verified, or this region did not take it. + * + * The proof is read_copy, which is the same validation a mount performs. What + * is accepted here is therefore exactly what a reboot would accept, and a + * sector that took the write and kept something else -- which is how NOR fails, + * without an error -- is caught now, while there is still another region to put + * the file in. */ +kiln_err_t write_copy(kiln_fileslots_t *fs, uint16_t region, uint8_t copy, + const uint8_t *namebuf, const void *data, size_t len, + uint32_t seq, uint16_t *crc_out) { - kiln_fileslots_t *fs = static_cast(ctx); - if ((fs == nullptr) || !fs->mounted || !name_ok(path) || - ((data == nullptr) && (len > 0u))) { - return KILN_ERR_INVALID_ARG; - } - if (len > fs->payload_max) { - return KILN_ERR_RANGE; - } - - uint16_t r = find_region(fs, path); - const bool fresh = (r == fs->region_count); - if (fresh) { - r = free_region(fs); - if (r == fs->region_count) { - return KILN_ERR_NO_SPACE; - } - } - - const uint8_t target = fresh ? 0u : (uint8_t)(1u - fs->entry[r].copy); - const uint32_t seq = fresh ? 1u : (fs->entry[r].seq + 1u); - const uint32_t base = copy_off(fs, r, target); + const uint32_t base = copy_off(fs, region, copy); kiln_err_t e = fs->flash->erase(fs->flash->ctx, base, fs->sector_bytes); if (e != KILN_OK) { @@ -294,11 +325,7 @@ kiln_err_t fs_write_atomic(void *ctx, const char *path, const void *data, size_t /* The name field is written in full so that its 64 bytes are what the CRC * covers on both sides, whatever the caller's string length. */ - uint8_t namebuf[KILN_PATH_MAX]; - memset(namebuf, 0, sizeof(namebuf)); - memcpy(namebuf, path, strnlen(path, KILN_PATH_MAX - 1u)); - - e = fs->flash->write(fs->flash->ctx, base + OFF_NAME, namebuf, sizeof(namebuf)); + e = fs->flash->write(fs->flash->ctx, base + OFF_NAME, namebuf, KILN_PATH_MAX); if (e != KILN_OK) { return e; } @@ -322,7 +349,7 @@ kiln_err_t fs_write_atomic(void *ctx, const char *path, const void *data, size_t } } - uint16_t crc = kiln_crc16_update(KILN_CRC16_INIT, namebuf, sizeof(namebuf)); + uint16_t crc = kiln_crc16_update(KILN_CRC16_INIT, namebuf, KILN_PATH_MAX); crc = kiln_crc16_update(crc, static_cast(data), len); /* The commit. Until these twelve bytes land the copy above does not exist, @@ -337,16 +364,108 @@ kiln_err_t fs_write_atomic(void *ctx, const char *path, const void *data, size_t return e; } - kiln_fileslot_entry_t *en = &fs->entry[r]; - memcpy(en->name, namebuf, sizeof(namebuf)); - en->seq = seq; - en->len = (uint16_t)len; - en->crc = crc; - en->copy = target; - en->used = true; + kiln_fileslot_entry_t check = {}; + if (!read_copy(fs, region, copy, &check)) { + return KILN_ERR_CORRUPT; + } + if ((check.seq != seq) || (check.len != (uint16_t)len) || (check.crc != crc) || + (memcmp(check.name, namebuf, KILN_PATH_MAX) != 0)) { + return KILN_ERR_CORRUPT; + } + + *crc_out = crc; return KILN_OK; } +kiln_err_t fs_write_atomic(void *ctx, const char *path, const void *data, size_t len) +{ + kiln_fileslots_t *fs = static_cast(ctx); + if ((fs == nullptr) || !fs->mounted || !name_ok(path) || + ((data == nullptr) && (len > 0u))) { + return KILN_ERR_INVALID_ARG; + } + if (len > fs->payload_max) { + return KILN_ERR_RANGE; + } + + uint8_t namebuf[KILN_PATH_MAX]; + memset(namebuf, 0, sizeof(namebuf)); + memcpy(namebuf, path, strnlen(path, KILN_PATH_MAX - 1u)); + + /* Where the file lives now, if it lives anywhere. Kept separate from the + * region being written, because after a retirement they differ and the old + * one must stay readable until the new one has verified. */ + const uint16_t home = find_region(fs, path); + + uint16_t r = home; + uint8_t target = 0u; + uint32_t seq = 1u; + if (home == fs->region_count) { + r = free_region(fs); + if (r == fs->region_count) { + return KILN_ERR_NO_SPACE; + } + } + else { + target = (uint8_t)(1u - fs->entry[home].copy); + seq = fs->entry[home].seq + 1u; + } + + for (unsigned attempt = 1u; ; attempt++) { + uint16_t crc = 0u; + const kiln_err_t e = + write_copy(fs, r, target, namebuf, data, len, seq, &crc); + + if (e == KILN_OK) { + if ((home != fs->region_count) && (r != home)) { + /* The file has moved. Erase what the retired region holds, + * best effort and only now that the new copy has verified: + * + * - it succeeds, and the stale copy is gone, which is what + * stops a later remove from being undone at the next mount + * by a copy of the file nobody asked to keep; + * - it fails, which is what a dead sector does, and the stale + * copy stays at a lower seq. That is the trace mount reads + * to retire this region again, so retirement outlives the + * power cycle exactly when the medium is genuinely gone. + * + * A power cut between the verify above and this erase leaves + * both copies on the medium, which is the same case and is + * resolved the same way. */ + (void)fs->flash->erase(fs->flash->ctx, + copy_off(fs, home, 0), fs->region_bytes); + fs->entry[home] = kiln_fileslot_entry_t{}; + } + kiln_fileslot_entry_t *en = &fs->entry[r]; + memcpy(en->name, namebuf, sizeof(namebuf)); + en->seq = seq; + en->len = (uint16_t)len; + en->crc = crc; + en->copy = target; + en->used = true; + return KILN_OK; + } + + /* This region did not take the file. It is out of the pool for the + * rest of the power cycle; whether it is out for good is settled at the + * next mount, by whether the erase above could reach it. */ + fs->retired[r] = true; + if (attempt >= KILN_FILESLOT_WRITE_TRIES) { + return e; + } + const uint16_t next = free_region(fs); + if (next == fs->region_count) { + return e; /* nowhere left to put it: the caller has to know */ + } + + r = next; + target = 0u; + /* Above anything the retired region can be holding, so that a mount + * which finds both copies of the name resolves in favour of this one. */ + seq = seq + 1u; + } +} + kiln_err_t fs_remove(void *ctx, const char *path) { kiln_fileslots_t *fs = static_cast(ctx); diff --git a/firmware/controller/components/kiln_ports/include/kiln_ports/port_filestore.h b/firmware/controller/components/kiln_ports/include/kiln_ports/port_filestore.h index e170e27..bc21fd0 100644 --- a/firmware/controller/components/kiln_ports/include/kiln_ports/port_filestore.h +++ b/firmware/controller/components/kiln_ports/include/kiln_ports/port_filestore.h @@ -15,6 +15,11 @@ #include "kiln/err.h" #include "kiln/types.h" +/* Including the terminator, and the stored name field is exactly this, so a + * longer path is REFUSED with KILN_ERR_INVALID_ARG rather than truncated. + * Storing a file under a name the caller did not ask for is the worse of the + * two failures; both callers produce five-character slot paths, so nothing + * generates one today. */ constexpr size_t KILN_PATH_MAX = 64; typedef struct kiln_port_filestore { diff --git a/firmware/controller/host/kiln_hal_host/include/kiln_hal_host/hal_host.h b/firmware/controller/host/kiln_hal_host/include/kiln_hal_host/hal_host.h index ba5849a..bc207a4 100644 --- a/firmware/controller/host/kiln_hal_host/include/kiln_hal_host/hal_host.h +++ b/firmware/controller/host/kiln_hal_host/include/kiln_hal_host/hal_host.h @@ -31,6 +31,9 @@ constexpr uint32_t KILN_HOST_FLASH_MAX_BYTES = 2u * 1024u * 1024u; +/* No sector selected, for the offset-addressed fault injection below. */ +constexpr uint32_t KILN_HOST_NO_OFFSET = 0xFFFFFFFFu; + typedef struct { uint8_t *data; /* caller-provided, so the size is the test's choice */ uint32_t size_bytes; @@ -41,6 +44,9 @@ typedef struct { uint32_t writes; uint32_t reads; uint32_t bytes_written; + /* Mount streams every copy through the CRC, so what a mount costs is this + * number and not the read count (tasklist Q2). */ + uint32_t bytes_read; /* Fault injection. */ uint32_t fail_write_after; /* 0 = never: writes fail once this many have succeeded */ @@ -51,6 +57,22 @@ typedef struct { * That is exactly a torn record: some bytes landed, the rest did not. */ uint32_t cut_power_at_write; uint32_t cut_bytes; + + /* A worn sector: the write returns KILN_OK and the medium keeps zeros + * instead of the data. The failure mode that no error code reports, and + * the one fileslots' write verification exists for. `garble_write_at` is + * the 1-based write to spoil (0 = none); `garble_every_write` spoils all of + * them, which is a part with nowhere left to write. */ + uint32_t garble_write_at; + bool garble_every_write; + + /* Refuse to erase the sector at this offset, for as long as it is set. + * By offset rather than by a count, because what a wear test needs to say + * is WHICH region is finished, not how many erases have gone by: counting + * them means knowing the order fileslots happens to erase in, and a test + * written against that measures the implementation instead of the medium. + * KILN_HOST_NO_OFFSET means no such sector, and is what init leaves. */ + uint32_t fail_erase_off; } kiln_host_flash_t; /* `storage` must be at least size_bytes and is set to 0xFF (erased). */ diff --git a/firmware/controller/host/kiln_hal_host/src/hal_host.cpp b/firmware/controller/host/kiln_hal_host/src/hal_host.cpp index d43be64..91239e3 100644 --- a/firmware/controller/host/kiln_hal_host/src/hal_host.cpp +++ b/firmware/controller/host/kiln_hal_host/src/hal_host.cpp @@ -19,6 +19,9 @@ void kiln_host_flash_init(kiln_host_flash_t *f, uint8_t *storage, f->size_bytes = size_bytes; f->sector_bytes = sector_bytes; f->powered = true; + /* Not zero: zero is the first sector's offset, and a fault injector that is + * armed by default would be found the hard way. */ + f->fail_erase_off = KILN_HOST_NO_OFFSET; memset(storage, 0xFF, size_bytes); } @@ -62,6 +65,7 @@ kiln_err_t hf_read(void *ctx, uint32_t off, void *out, size_t len) memcpy(out, &f->data[off], len); f->reads++; + f->bytes_read += (uint32_t)len; return KILN_OK; } @@ -107,8 +111,15 @@ kiln_err_t hf_write(void *ctx, uint32_t off, const void *data, size_t len) return KILN_ERR_IO; } + /* Wear, as NOR actually presents it: the write is accepted, the status says + * success, and the cell holds something else. Modelled as stuck-at-zero + * because that is the direction a programmed cell fails in and the only one + * a write could produce. Reported as KILN_OK deliberately -- a store that + * only notices the errors it is told about does not notice this one. */ + const bool garble = f->garble_every_write || + ((f->garble_write_at != 0u) && (f->writes == f->garble_write_at)); for (size_t i = 0; i < len; i++) { - f->data[off + i] &= src[i]; + f->data[off + i] &= garble ? 0x00u : src[i]; } f->bytes_written += (uint32_t)len; return KILN_OK; @@ -137,6 +148,13 @@ kiln_err_t hf_erase(void *ctx, uint32_t off, size_t len) if ((f->fail_erase_after != 0u) && f->erases + sectors > f->fail_erase_after) { return KILN_ERR_IO; } + /* A sector that will not erase, which is the other half of how NOR ends: + * the whole call fails, including a multi-sector erase that covers it, and + * nothing in the range is touched. */ + if ((f->fail_erase_off != KILN_HOST_NO_OFFSET) && + (f->fail_erase_off >= off) && (f->fail_erase_off < off + len)) { + return KILN_ERR_IO; + } memset(&f->data[off], 0xFF, len); f->erases += sectors; diff --git a/firmware/controller/main/main.cpp b/firmware/controller/main/main.cpp index 399fbdb..e0cecb0 100644 --- a/firmware/controller/main/main.cpp +++ b/firmware/controller/main/main.cpp @@ -693,9 +693,19 @@ extern "C" void app_main(void) if (fe == KILN_OK) { kiln_fileslots_bind(&s_fileslots, &s_fs); ports.filestore = &s_fs; + const unsigned retired = + (unsigned)kiln_fileslots_retired_regions(&s_fileslots); ESP_LOGI(TAG, "file store: %u of %u regions used", (unsigned)kiln_fileslots_used_regions(&s_fileslots), (unsigned)s_fileslots.region_count); + if (retired > 0u) { + /* A region retires when a write did not read back as written, + * which is how NOR wears out. The store carries on in another + * region, so nothing above this notices; this line is the only + * warning anybody gets that the part is on its way out. */ + ESP_LOGW(TAG, "file store: %u region(s) retired after a failed " + "write; the flash is wearing out", retired); + } } else { /* As with the log: a kiln that cannot store programs is still a * kiln that can be watched, so this warns rather than halting. */ diff --git a/firmware/controller/test/host/unit/test_fileslots.cpp b/firmware/controller/test/host/unit/test_fileslots.cpp index 3aade3c..96a77ca 100644 --- a/firmware/controller/test/host/unit/test_fileslots.cpp +++ b/firmware/controller/test/host/unit/test_fileslots.cpp @@ -10,10 +10,15 @@ * a file that was already there, and here the test can actually cut the power. */ +#include #include #include "kiln_check.h" #include "kiln_core/fileslots.h" #include "kiln_hal_host/hal_host.h" +/* For the two blob sizes the real callers store, in the mount-cost tests at the + * end of this file: a cost measured against made-up payloads measures nothing. */ +#include "kiln_app/program_store.h" +#include "kiln_app/run_index.h" constexpr size_t SECTOR_BYTES = 4096u; /* 20 programs plus 20 run records, plus two spare to prove the store refuses a @@ -427,3 +432,348 @@ KILN_TEST(swa10_the_store_validates_its_arguments) KILN_ERR_INVALID_ARG); CHECK_ERR(kiln_fileslots_format(&cold), KILN_ERR_STATE); } + +/* =========================================================================== + * WEAR (tasklist Q1) + * =========================================================================== + * A NOR sector at the end of its life takes the write, reports success, and + * keeps something other than what it was given. Every test above this point + * drives a medium that does what it is told, so the one failure mode the part + * will actually exhibit was the one nothing exercised. + * + * The flash fake grows two knobs for it: garble_write_at spoils one write, + * garble_every_write spoils all of them, and both return KILN_OK, because an + * error code is exactly what this failure does not come with. + */ + +/* The write counter after priming, so a test can spoil the next write rather + * than guess which one it is. */ +static void spoil_next_write(rig_t *r) +{ + r->flash.garble_write_at = r->flash.writes + 1u; +} + +/* + * @relation(SWA-21, scope=function) + */ +KILN_TEST(swa21_a_sector_that_lies_about_a_write_retires_its_region) +{ + rig_t *r = &g_rig; + rig_init(r); + + CHECK_OK(put(r, "/r/00", "first", 6)); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 1u); + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 0u); + + /* The region the file is in is about to stop holding what it is given. */ + spoil_next_write(r); + CHECK_OK(put(r, "/r/00", "second", 7)); + + /* The caller gets KILN_OK because the file IS stored -- somewhere else. */ + uint8_t buf[16]; + size_t n = 0; + CHECK_OK(get(r, "/r/00", buf, sizeof(buf), &n)); + CHECK_STR_EQ(as_str(buf), "second"); + CHECK_EQ_UINT(n, 7u); + + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 1u); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 1u); + + /* And the retired region is not handed out again. */ + r->flash.garble_write_at = 0; + CHECK_OK(put(r, "/r/01", "other", 6)); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 2u); + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 1u); + CHECK_OK(get(r, "/r/00", buf, sizeof(buf), nullptr)); + CHECK_STR_EQ(as_str(buf), "second"); +} + +/* + * @relation(SWA-21, scope=function) + */ +KILN_TEST(swa21_a_migration_leaves_no_stale_copy_to_come_back_later) +{ + rig_t *r = &g_rig; + rig_init(r); + + CHECK_OK(put(r, "/r/00", "first", 6)); + spoil_next_write(r); + CHECK_OK(put(r, "/r/00", "second", 7)); + r->flash.garble_write_at = 0; + + /* The erase of the retired region succeeded, so nothing on the medium + * claims the name twice and the region is back in the pool at the next + * boot: a sector that can still be erased has not earned a life sentence + * from one bad write. */ + rig_remount(r); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 1u); + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 0u); + + uint8_t buf[16]; + CHECK_OK(get(r, "/r/00", buf, sizeof(buf), nullptr)); + CHECK_STR_EQ(as_str(buf), "second"); + + /* The case this erase is for: a removed file must not be resurrected by a + * copy the migration left behind. */ + CHECK_OK(r->store.remove(r->store.ctx, "/r/00")); + rig_remount(r); + CHECK_ERR(get(r, "/r/00", buf, sizeof(buf), nullptr), KILN_ERR_NOT_FOUND); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 0u); +} + +/* + * @relation(SWA-21, scope=function) + */ +KILN_TEST(swa21_a_region_that_cannot_even_be_erased_stays_retired_across_a_reboot) +{ + rig_t *r = &g_rig; + rig_init(r); + + CHECK_OK(put(r, "/r/00", "first", 6)); + + /* Now region 0, where the file lives, is properly gone: it keeps zeros + * instead of data, and it will not erase either, so the copy it holds + * cannot be cleaned up after the migration. */ + spoil_next_write(r); + r->flash.fail_erase_off = 0u; /* region 0, copy 0 */ + CHECK_OK(put(r, "/r/00", "second", 7)); + r->flash.garble_write_at = 0; + r->flash.fail_erase_off = KILN_HOST_NO_OFFSET; + + uint8_t buf[16]; + CHECK_OK(get(r, "/r/00", buf, sizeof(buf), nullptr)); + CHECK_STR_EQ(as_str(buf), "second"); + + /* Two regions now hold /r/00. Mount resolves it by seq and retires the + * loser, which is the region whose write failed -- so retirement outlives + * the power cycle without a bad-block table anywhere. */ + rig_remount(r); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 1u); + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 1u); + CHECK_OK(get(r, "/r/00", buf, sizeof(buf), nullptr)); + CHECK_STR_EQ(as_str(buf), "second"); + + /* Twice, because a resolution that is not stable is not a resolution. */ + rig_remount(r); + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 1u); + CHECK_OK(get(r, "/r/00", buf, sizeof(buf), nullptr)); + CHECK_STR_EQ(as_str(buf), "second"); + + /* An explicit factory reset takes it back: FR-CFG-12 erases everything, and + * refusing to try the region again would leave the operator no way to find + * out whether the part is really finished. */ + CHECK_OK(kiln_fileslots_format(&r->fs)); + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 0u); +} + +/* + * @relation(SWA-21, scope=function) + */ +KILN_TEST(swa21_a_part_with_nowhere_left_to_write_says_so) +{ + rig_t *r = &g_rig; + rig_init(r); + + CHECK_OK(put(r, "/r/00", "first", 6)); + + /* Every write from here keeps zeros. Three regions are tried, which is + * KILN_FILESLOT_WRITE_TRIES, and then the store stops spending erase + * cycles on a part that is not coming back. */ + r->flash.garble_every_write = true; + CHECK_ERR(put(r, "/r/00", "second", 7), KILN_ERR_CORRUPT); + r->flash.garble_every_write = false; + + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 3u); + + /* The previous content is still there: the write that failed was the one + * that did not happen, not the file. */ + uint8_t buf[16]; + CHECK_OK(get(r, "/r/00", buf, sizeof(buf), nullptr)); + CHECK_STR_EQ(as_str(buf), "first"); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 1u); + + rig_remount(r); + CHECK_OK(get(r, "/r/00", buf, sizeof(buf), nullptr)); + CHECK_STR_EQ(as_str(buf), "first"); +} + +/* + * @relation(SWA-21, scope=function) + */ +KILN_TEST(swa21_a_fresh_file_migrates_too_and_the_store_can_fill_up_with_retirements) +{ + rig_t *r = &g_rig; + rig_init(r); + + /* A file that has no home yet meets the same sector. Nothing has to be + * preserved here, which is the easy half, but the region still has to be + * retired or the next attempt goes straight back into it. */ + spoil_next_write(r); + CHECK_OK(put(r, "/p/00", "program", 8)); + r->flash.garble_write_at = 0; + + uint8_t buf[16]; + CHECK_OK(get(r, "/p/00", buf, sizeof(buf), nullptr)); + CHECK_STR_EQ(as_str(buf), "program"); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 1u); + CHECK_EQ_UINT(kiln_fileslots_retired_regions(&r->fs), 1u); + + /* Retired regions are not capacity. Fill what is left and the store + * reports no space rather than retrying a region it has written off. */ + for (size_t i = 1; i < TEST_REGIONS - 1u; i++) { + char path[16]; + (void)snprintf(path, sizeof(path), "/p/%02zu", i); + CHECK_OK(put(r, path, "x", 2)); + } + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), TEST_REGIONS - 1u); + CHECK_ERR(put(r, "/p/99", "y", 2), KILN_ERR_NO_SPACE); +} + +/* =========================================================================== + * WHAT A MOUNT COSTS (tasklist Q2) + * =========================================================================== + * Mount reads every copy of every region and streams its payload through the + * CRC, so start-up is paid in flash reads and CRC bytes before the controller + * does anything. That cost had been described rather than measured, and the + * description was wrong by 60 %. + * + * These two tests measure it on the medium the partition actually gives us: 64 + * regions, both copies valid, which is the state normal operation leaves behind + * because a region keeps the copy it is replacing until the next write. + * + * The expected numbers are DERIVED here rather than written down, so the tests + * pin the model of the cost instead of a constant somebody would update to + * whatever the code now does. Make mount lazy and they fail, which is the + * point: that is a change to when start-up is paid for, and it should not pass + * unnoticed. + */ +constexpr size_t COST_REGIONS = KILN_FILESLOT_REGIONS_MAX; /* 64 */ +constexpr size_t COST_CRC_CHUNK = 64u; /* read_copy's streaming buffer */ + +typedef struct { + uint8_t storage[COST_REGIONS * 2u * SECTOR_BYTES]; + kiln_host_flash_t flash; + kiln_port_flash_t port; + kiln_fileslots_t fs; + kiln_port_filestore_t store; +} cost_rig_t; + +static cost_rig_t g_cost; + +/* Reads and bytes that validating one committed copy of `len` payload bytes + * costs: the 76 byte header, then the payload in 64 byte chunks. */ +static void copy_cost(size_t len, uint32_t *reads, uint32_t *bytes) +{ + *reads = 1u + (uint32_t)((len + COST_CRC_CHUNK - 1u) / COST_CRC_CHUNK); + *bytes = KILN_FILESLOT_HDR + (uint32_t)len; +} + +/* Fill every region, writing each file twice so that both copies are valid -- + * the state a running kiln leaves the medium in. */ +static void cost_fill(cost_rig_t *r, size_t payload) +{ + memset(r, 0, sizeof(*r)); + kiln_host_flash_init(&r->flash, r->storage, sizeof(r->storage), SECTOR_BYTES); + kiln_host_flash_bind(&r->flash, &r->port); + CHECK_OK(kiln_fileslots_mount(&r->fs, &r->port)); + kiln_fileslots_bind(&r->fs, &r->store); + CHECK_EQ_UINT(r->fs.region_count, COST_REGIONS); + + static uint8_t blob[4096]; + memset(blob, 0xA5, sizeof(blob)); + for (size_t i = 0; i < COST_REGIONS; i++) { + char path[16]; + (void)snprintf(path, sizeof(path), "/x/%02zu", i); + CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, payload)); + CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, payload)); + } + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), COST_REGIONS); +} + +/* + * @relation(SWA-21, scope=function) + */ +KILN_TEST(swa21_a_full_mount_costs_what_the_model_says) +{ + /* The largest payload a 4 096 byte sector can hold once the 76 byte header + * is taken out, rounded down to a word: (4096 - 76) & ~3. */ + const size_t payload = 4020u; + + cost_rig_t *r = &g_cost; + cost_fill(r, payload); + CHECK_EQ_UINT(r->fs.payload_max, payload); + + r->flash.reads = 0; + r->flash.bytes_read = 0; + CHECK_OK(kiln_fileslots_mount(&r->fs, &r->port)); + + uint32_t per_reads = 0; + uint32_t per_bytes = 0; + copy_cost(payload, &per_reads, &per_bytes); + + /* Two valid copies per region, both validated in full. */ + CHECK_EQ_UINT(r->flash.reads, 2u * COST_REGIONS * per_reads); + CHECK_EQ_UINT(r->flash.bytes_read, 2u * COST_REGIONS * per_bytes); + + /* The absolute figures, so a reader of this suite does not have to do the + * arithmetic: 8 192 reads and 524 288 bytes, which is the whole partition. + * That is the number the architecture's start-up budget has to carry, and + * it is 1.6 times the 320 kB the task list had guessed at. */ + CHECK_EQ_UINT(r->flash.reads, 8192u); + CHECK_EQ_UINT(r->flash.bytes_read, 524288u); +} + +/* + * @relation(SWA-21, scope=function) + */ +KILN_TEST(swa21_the_mount_this_product_actually_pays_for) +{ + /* What the two real callers store: 20 programs and 20 run records, at the + * sizes their own serialisers produce. The worst case above is what the + * partition permits; this is what the firmware puts in it, and the + * difference is a factor of nine. */ + constexpr size_t PROG_BLOB = 8u + sizeof(kiln_program_t) + 2u; + constexpr size_t RUN_BLOB = 8u + sizeof(kiln_run_record_t) + 2u; + + cost_rig_t *r = &g_cost; + memset(r, 0, sizeof(*r)); + kiln_host_flash_init(&r->flash, r->storage, sizeof(r->storage), SECTOR_BYTES); + kiln_host_flash_bind(&r->flash, &r->port); + CHECK_OK(kiln_fileslots_mount(&r->fs, &r->port)); + kiln_fileslots_bind(&r->fs, &r->store); + + static uint8_t blob[4096]; + memset(blob, 0x5A, sizeof(blob)); + for (size_t i = 0; i < 20u; i++) { + char path[16]; + (void)snprintf(path, sizeof(path), "/p/%02zu", i); + CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, PROG_BLOB)); + CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, PROG_BLOB)); + (void)snprintf(path, sizeof(path), "/r/%02zu", i); + CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, RUN_BLOB)); + CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, RUN_BLOB)); + } + + r->flash.reads = 0; + r->flash.bytes_read = 0; + CHECK_OK(kiln_fileslots_mount(&r->fs, &r->port)); + + uint32_t pr = 0, pb = 0, rr = 0, rb = 0; + copy_cost(PROG_BLOB, &pr, &pb); + copy_cost(RUN_BLOB, &rr, &rb); + + /* 40 regions hold a file; the other 24 cost one failed header read each. */ + const uint32_t want_reads = 2u * 20u * (pr + rr) + (uint32_t)(COST_REGIONS - 40u) * 2u; + const uint32_t want_bytes = 2u * 20u * (pb + rb) + + (uint32_t)(COST_REGIONS - 40u) * 2u * KILN_FILESLOT_HDR; + CHECK_EQ_UINT(r->flash.reads, want_reads); + CHECK_EQ_UINT(r->flash.bytes_read, want_bytes); + printf(" mount: %u reads, %u bytes (program blob %zu, run blob %zu)\n", + r->flash.reads, r->flash.bytes_read, PROG_BLOB, RUN_BLOB); + + /* Measured: 768 reads and 47 808 bytes, against 8 192 and 524 288 for a + * medium full of maximum-size files. A ninth of the worst case, and the + * reason the worst case is a bound rather than a budget. */ + CHECK_EQ_UINT(r->flash.reads, 768u); + CHECK_EQ_UINT(r->flash.bytes_read, 47808u); +} diff --git a/tasklist.md b/tasklist.md index 0063f44..6cd4e89 100644 --- a/tasklist.md +++ b/tasklist.md @@ -741,46 +741,70 @@ weakness first, the deadline second, the sentence third, the engineering last. ## Q. The file store, SWA-21 -- [ ] **Q1. The store has never seen a worn sector.** Every test runs on a fake - that writes what it is told. A real NOR sector near the end of its life fails - a program or reads back something it was not given, and the store's answer to - that is the CRC, which rejects the copy and falls back to the other one. That - is the designed behaviour and it is tested, but it has been tested against - simulated corruption rather than against a worn part. There is no bad-region - retirement: a region whose both sectors have failed will keep being chosen - and keep failing, where a filesystem would have remapped it. At 40 regions of - two sectors each, written single-digit times a year against a 100 000 cycle - rating, that is a defensible trade rather than an oversight, but it is a trade - and this is where it is recorded. - -- [ ] **Q2. The index is built by reading every region at every boot.** 64 - regions, two copies each, each validated by streaming its payload through the - CRC. That is up to 128 sector reads and about 320 kB of CRC at startup, - measured at nothing in particular because it has only ever run under QEMU and - on the host. If it turns out to cost real time on a board it can be made lazy, - since the only thing mount actually needs eagerly is which copy of each region - wins. Worth measuring before it is worth optimising. - -- [ ] **Q3. `exists` and `list` are implemented and have no caller.** They are - in the port contract, so the store provides them and the tests cover them, but - `program_store` and `run_index` use neither. They are the natural way a local - program editor would enumerate what is there, which is `P3`, so they are kept - rather than removed. - -- [ ] **Q4. A name longer than 63 bytes is refused rather than truncated.** - `KILN_PATH_MAX` is 64 including the terminator and the name field is exactly - that, so a longer path returns `KILN_ERR_INVALID_ARG`. Nothing generates one - today, since both callers produce five-character slot paths, and refusing is - the right answer rather than silently storing a different file than the caller - asked for. Noted because a future caller with user-supplied names will meet it. - -- [ ] **Q5. The run index rewrites more than it needs to.** `kiln_run_index_append` - reads every slot to find the oldest, then writes one. On this store that is 20 - reads served from the in-RAM index plus one region write, which is cheap. But - `kiln_run_index_mark_truncated` and `kiln_run_index_baseline` both walk all 20 - slots too, and the walk is now the only reason `run_index` reads at all. Not a - problem, just the place where the store being an array rather than a directory - would let the index get simpler if it were ever revisited. +- [x] **Q1. Wear is now handled at the write. Done 2026-10-09.** The store had + never seen a worn sector, and the failure mode it was written against was the + wrong one: a NOR sector at the end of its life does not report an error, it + takes the write, returns success and holds something else. The CRC caught that + on the next read, which on this device is after a firing rather than during + the write it could still have redirected. + + Every write is now read back and re-validated through the same path a mount + uses before the index points at it. A copy that does not verify retires its + region and the write is re-attempted in a free region at a higher seq, up to + three regions; the caller gets `KILN_OK`, because the file is stored and only + its location changed. Retirement needs no bad-block table: the old region is + erased best effort after the new copy verifies, and if that erase fails, which + is what a finished sector does, the stale copy stays at a lower seq, two + regions claim one name, and mount resolves it by seq and retires the loser. + The erase is what stops a deleted file from coming back at the next mount. + + Still no wear levelling, and deliberately: the endurance arithmetic of + architecture 10.4 has four orders of magnitude of headroom, so a region keeps + its file for as long as it works. + + Five new host tests, driven by two new knobs on the flash fake that model a + sector which accepts a write and keeps zeros, and a sector that will not + erase. The retired count is logged at boot and is the only warning anybody + gets that the part is going. + +- [x] **Q2. The mount cost is measured. Done 2026-10-09.** It was described + rather than measured, and the description was low by a third. On the medium + this firmware actually produces, 20 programs of 398 bytes and 20 run records + of 554, a mount costs **768 reads and 47 808 bytes** of CRC. A partition full + of maximum-size files with both copies valid costs **8 192 reads and 524 288 + bytes**, the whole partition. + + Both are asserted in `test_fileslots`, against a model derived in the test + rather than a constant, so making mount lazy fails the suite instead of + happening quietly. 47 kB of flash reads is not a start-up problem on this + part, so there is nothing here to optimise; if a board ever says otherwise, + the only thing mount needs eagerly is which copy of each region wins, and the + payload CRC could move to the read. + +- [ ] **Q3. `exists` and `list` are implemented and have no caller, and the + reason they were kept has gone.** They were kept for `P3`'s local program + editor, and the decision there was pre-compiled profiles with nothing + editing them, so there is no prospective caller left. Three implementations + (`fileslots`, the host HAL, the simulator) and two tests exist for an + interface nothing above uses. Either the port contract loses both and the + implementations go with them, or they stay with an honest reason written down. + **A decision rather than a task**, and it travels with `P3`. + +- [x] **Q4. A name longer than 63 bytes is refused rather than truncated. + Recorded 2026-10-09.** `KILN_PATH_MAX` is 64 including the terminator and the + name field is exactly that, so a longer path returns `KILN_ERR_INVALID_ARG`. + Both callers produce five-character slot paths, so nothing generates one + today; storing a file under a name the caller did not ask for is the worse + failure. Now stated where it is enforced, in `port_filestore.h` and + architecture 10.6, with the test that proves it, rather than only here. + +- [x] **Q5. The run index walks all 20 slots, and that is where it stays. + Recorded 2026-10-09.** Append reads every slot to find the oldest, and + `mark_truncated` and `baseline` walk them too. Twenty reads answered from the + in-RAM index plus one region write, once per firing, is not a cost worth a + mechanism. It is the one place where the store being a fixed array rather + than a directory costs anything, and that is now recorded in `run_index.cpp` + where somebody revisiting the file will see it. --- From f8ee8657b2b5cd4a91f9a866942ea56c7bc9d565 Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:35:57 +0200 Subject: [PATCH 06/14] refactor: the file store loses exists and list, which nobody was going to call Q3. Both were kept for the local program editor of P3, and the decision there was pre-compiled profiles with nothing editing them, so the caller they were waiting for is not coming. What was left was three implementations -- fileslots, the host HAL, the simulator -- and two tests for an interface the product never exercises: code that compiles, is covered, and is dead, which is worse than no code because it reads as supported. exists was read with the answer thrown away, and anything that needs to enumerate a fixed array of numbered slots can count to twenty. The two tests that drove them now drive what has callers: the removal is proved by a read rather than by exists, and the usage test additionally checks that usage survives a remount, since it is answered from the in-RAM index and the mount is the only thing that rebuilds it. 25/25 host tests, clang-tidy 20 clean, both IDF configurations build. --- docs/architecture.md | 2 +- .../kiln_core/include/kiln_core/fileslots.h | 6 +-- .../components/kiln_core/src/fileslots.cpp | 35 ----------------- .../include/kiln_ports/port_filestore.h | 15 +++++--- .../components/kiln_sim/src/sim.cpp | 35 ----------------- .../host/kiln_hal_host/src/hal_host.cpp | 38 ------------------- .../test/host/unit/test_fileslots.cpp | 24 +++++------- tasklist.md | 9 ----- 8 files changed, 24 insertions(+), 140 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index a3502c0..77a288e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -152,7 +152,7 @@ double is a compile-time-checked substitution. | `port_system` | `reset_cause()`, `fw_info()`, `stats()`, `wdt_subscribe/feed()` | `esp_system` + task WDT; stub | | `port_update` | `check()`, `fetch()`, `begin/write/finish()`, `confirm_running()`, `rollback()` | `esp_https_ota` for the pull path of §13.5, `esp_ota_ops` for the slots; stub | | `port_kvstore` | `get/set/erase(namespace, key, blob)` | NVS; in-memory fake | -| `port_filestore` | `list/read/write/delete(path)` | `kiln_core/fileslots` over `port_flash` (`SWA-21`); RAM fake on host | +| `port_filestore` | `read/write_atomic/remove(path)`, `usage` | `kiln_core/fileslots` over `port_flash` (`SWA-21`); RAM fake on host | | `port_net` | `status()`, `scan_begin/busy/results()`, `connect()` | WiFi station, scanning and joining (§5.4), plus SNTP; stub. **No access point**: network setup is on the display | ### 5.3 Application (`kiln_app`) diff --git a/firmware/controller/components/kiln_core/include/kiln_core/fileslots.h b/firmware/controller/components/kiln_core/include/kiln_core/fileslots.h index 17f86d0..53c949e 100644 --- a/firmware/controller/components/kiln_core/include/kiln_core/fileslots.h +++ b/firmware/controller/components/kiln_core/include/kiln_core/fileslots.h @@ -9,9 +9,9 @@ * not control. The two callers above it -- program_store and run_index -- do * not need one either. Both address a fixed array of numbered slots, `/p/00` * to `/p/19` and `/r/00` upwards, never an arbitrary name, and between them - * they call read, write_atomic, remove and usage. `list` and `exists` are in - * the port but have no caller. A filesystem would be a general mechanism paid - * for in failure modes and bought for a fixed-size array. + * they call read, write_atomic, remove and usage, which is the whole port. A + * filesystem would be a general mechanism paid for in failure modes and bought + * for a fixed-size array. * * The medium is divided into equal regions of two erase sectors. One file * lives in one region and alternates between its two sectors, so the copy being diff --git a/firmware/controller/components/kiln_core/src/fileslots.cpp b/firmware/controller/components/kiln_core/src/fileslots.cpp index 358807f..f3e2f9a 100644 --- a/firmware/controller/components/kiln_core/src/fileslots.cpp +++ b/firmware/controller/components/kiln_core/src/fileslots.cpp @@ -485,39 +485,6 @@ kiln_err_t fs_remove(void *ctx, const char *path) return KILN_OK; } -kiln_err_t fs_exists(void *ctx, const char *path) -{ - const kiln_fileslots_t *fs = static_cast(ctx); - if ((fs == nullptr) || !fs->mounted || !name_ok(path)) { - return KILN_ERR_INVALID_ARG; - } - return (find_region(fs, path) == fs->region_count) ? KILN_ERR_NOT_FOUND : KILN_OK; -} - -kiln_err_t fs_list(void *ctx, const char *dir, - bool (*fn)(void *user, const char *name, size_t size), - void *user) -{ - const kiln_fileslots_t *fs = static_cast(ctx); - if ((fs == nullptr) || !fs->mounted || (fn == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - const size_t pre = (dir != nullptr) ? strnlen(dir, KILN_PATH_MAX) : 0u; - for (uint16_t r = 0; r < fs->region_count; r++) { - const kiln_fileslot_entry_t *en = &fs->entry[r]; - if (!en->used) { - continue; - } - if ((pre > 0u) && (strncmp(en->name, dir, pre) != 0)) { - continue; - } - if (!fn(user, en->name, en->len)) { - break; - } - } - return KILN_OK; -} - kiln_err_t fs_usage(void *ctx, size_t *total, size_t *used) { const kiln_fileslots_t *fs = static_cast(ctx); @@ -551,7 +518,5 @@ void kiln_fileslots_bind(kiln_fileslots_t *fs, kiln_port_filestore_t *out) out->read = fs_read; out->write_atomic = fs_write_atomic; out->remove = fs_remove; - out->exists = fs_exists; - out->list = fs_list; out->usage = fs_usage; } diff --git a/firmware/controller/components/kiln_ports/include/kiln_ports/port_filestore.h b/firmware/controller/components/kiln_ports/include/kiln_ports/port_filestore.h index bc21fd0..142dc3e 100644 --- a/firmware/controller/components/kiln_ports/include/kiln_ports/port_filestore.h +++ b/firmware/controller/components/kiln_ports/include/kiln_ports/port_filestore.h @@ -7,6 +7,16 @@ * gets the atomicity from alternating between two copies rather than from a * rename. There is no filesystem behind this on the target, and nothing above * it needs one: both callers address a fixed set of numbered slots. + * + * There is no `exists` and no `list`, and there were both until the profiles + * became compile-time data. They existed for a local program editor that would + * have enumerated what is on the medium; with programs arriving in the image + * and nothing editing them, nobody was going to call either, and three + * implementations and two tests for an interface with no caller is a liability + * rather than headroom -- it is code that compiles, is covered, and is never + * exercised by the product. `exists` was in any case `read` with the answer + * thrown away, and anything that needs to enumerate a fixed array of numbered + * slots can count to twenty. */ #ifndef KILN_PORT_FILESTORE_H #define KILN_PORT_FILESTORE_H @@ -27,11 +37,6 @@ typedef struct kiln_port_filestore { kiln_err_t (*read)(void *ctx, const char *path, void *out, size_t cap, size_t *len); kiln_err_t (*write_atomic)(void *ctx, const char *path, const void *data, size_t len); kiln_err_t (*remove)(void *ctx, const char *path); - kiln_err_t (*exists)(void *ctx, const char *path); - /* Enumerate; returning false from fn stops. */ - kiln_err_t (*list)(void *ctx, const char *dir, - bool (*fn)(void *user, const char *name, size_t size), - void *user); kiln_err_t (*usage)(void *ctx, size_t *total, size_t *used); } kiln_port_filestore_t; diff --git a/firmware/controller/components/kiln_sim/src/sim.cpp b/firmware/controller/components/kiln_sim/src/sim.cpp index b881d85..f2d74a5 100644 --- a/firmware/controller/components/kiln_sim/src/sim.cpp +++ b/firmware/controller/components/kiln_sim/src/sim.cpp @@ -706,39 +706,6 @@ kiln_err_t sfs_remove(void *ctx, const char *path) return KILN_OK; } -kiln_err_t sfs_exists(void *ctx, const char *path) -{ - kiln_sim_fs_t *fs = static_cast(ctx); - if ((fs == nullptr) || (path == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - return sfs_find(fs, path) >= 0 ? KILN_OK : KILN_ERR_NOT_FOUND; -} - -kiln_err_t sfs_list(void *ctx, const char *dir, - bool (*fn)(void *user, const char *name, size_t size), - void *user) -{ - kiln_sim_fs_t *fs = static_cast(ctx); - if ((fs == nullptr) || (dir == nullptr) || (fn == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - - const size_t dlen = strlen(dir); - for (size_t i = 0; i < KILN_SIM_FS_FILES; i++) { - if (!fs->files[i].used) { - continue; - } - if (strncmp(fs->files[i].path, dir, dlen) != 0) { - continue; - } - if (!fn(user, fs->files[i].path, fs->files[i].len)) { - break; - } - } - return KILN_OK; -} - kiln_err_t sfs_usage(void *ctx, size_t *total, size_t *used) { const kiln_sim_fs_t *fs = static_cast(ctx); @@ -773,8 +740,6 @@ void kiln_sim_fs_bind(kiln_sim_fs_t *fs, kiln_port_filestore_t *out) out->read = sfs_read; out->write_atomic = sfs_write; out->remove = sfs_remove; - out->exists = sfs_exists; - out->list = sfs_list; out->usage = sfs_usage; } diff --git a/firmware/controller/host/kiln_hal_host/src/hal_host.cpp b/firmware/controller/host/kiln_hal_host/src/hal_host.cpp index 91239e3..5530844 100644 --- a/firmware/controller/host/kiln_hal_host/src/hal_host.cpp +++ b/firmware/controller/host/kiln_hal_host/src/hal_host.cpp @@ -415,42 +415,6 @@ kiln_err_t fs_remove(void *ctx, const char *path) return KILN_OK; } -kiln_err_t fs_exists(void *ctx, const char *path) -{ - kiln_host_fs_t *fs = static_cast(ctx); - if ((fs == nullptr) || (path == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - return (fs_find(fs, path) != nullptr) ? KILN_OK : KILN_ERR_NOT_FOUND; -} - -kiln_err_t fs_list(void *ctx, const char *dir, - bool (*fn)(void *user, const char *name, size_t size), - void *user) -{ - kiln_host_fs_t *fs = static_cast(ctx); - if ((fs == nullptr) || (dir == nullptr) || (fn == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - if (!fs->powered) { - return KILN_ERR_IO; - } - - const size_t dlen = strlen(dir); - for (size_t i = 0; i < KILN_HOST_FS_FILES; i++) { - if (!fs->files[i].used) { - continue; - } - if (strncmp(fs->files[i].path, dir, dlen) != 0) { - continue; - } - if (!fn(user, fs->files[i].path, fs->files[i].len)) { - break; - } - } - return KILN_OK; -} - kiln_err_t fs_usage(void *ctx, size_t *total, size_t *used) { const kiln_host_fs_t *fs = static_cast(ctx); @@ -484,8 +448,6 @@ void kiln_host_fs_bind(kiln_host_fs_t *fs, kiln_port_filestore_t *out) out->read = fs_read; out->write_atomic = fs_write_atomic; out->remove = fs_remove; - out->exists = fs_exists; - out->list = fs_list; out->usage = fs_usage; } diff --git a/firmware/controller/test/host/unit/test_fileslots.cpp b/firmware/controller/test/host/unit/test_fileslots.cpp index 96a77ca..e6d8dda 100644 --- a/firmware/controller/test/host/unit/test_fileslots.cpp +++ b/firmware/controller/test/host/unit/test_fileslots.cpp @@ -285,11 +285,12 @@ KILN_TEST(swa10_remove_frees_the_region_for_another_name) rig_t *r = &g_rig; rig_init(r); + uint8_t buf[16]; CHECK_OK(put(r, "/p/00", "gone", 5)); - CHECK_OK(r->store.exists(r->store.ctx, "/p/00")); + CHECK_OK(get(r, "/p/00", buf, sizeof(buf), nullptr)); CHECK_OK(r->store.remove(r->store.ctx, "/p/00")); - CHECK_ERR(r->store.exists(r->store.ctx, "/p/00"), KILN_ERR_NOT_FOUND); + CHECK_ERR(get(r, "/p/00", buf, sizeof(buf), nullptr), KILN_ERR_NOT_FOUND); CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), 0u); CHECK_ERR(r->store.remove(r->store.ctx, "/p/00"), KILN_ERR_NOT_FOUND); @@ -356,7 +357,7 @@ KILN_TEST(swa10_a_short_buffer_is_refused_rather_than_truncated) /* * @relation(SWA-10, scope=function) */ -KILN_TEST(swa10_usage_and_list_report_what_is_stored) +KILN_TEST(swa10_usage_reports_what_is_stored) { rig_t *r = &g_rig; rig_init(r); @@ -370,17 +371,12 @@ KILN_TEST(swa10_usage_and_list_report_what_is_stored) CHECK_EQ_UINT(used, 10u); CHECK_EQ_UINT(total, (size_t)TEST_REGIONS * r->fs.payload_max); - struct counter { unsigned n; size_t bytes; } c = {0, 0}; - auto const tally = [](void *user, const char *name, size_t size) -> bool { - (void)name; - struct counter *k = static_cast(user); - k->n++; - k->bytes += size; - return true; - }; - CHECK_OK(r->store.list(r->store.ctx, "/p/", tally, &c)); - CHECK_EQ_UINT(c.n, 1u); - CHECK_EQ_UINT(c.bytes, 4u); + /* usage is reported from the in-RAM index, so it has to survive a reboot + * on the strength of the mount alone. */ + rig_remount(r); + used = 0; + CHECK_OK(r->store.usage(r->store.ctx, nullptr, &used)); + CHECK_EQ_UINT(used, 10u); } /* diff --git a/tasklist.md b/tasklist.md index 6cd4e89..ca82c38 100644 --- a/tasklist.md +++ b/tasklist.md @@ -781,15 +781,6 @@ weakness first, the deadline second, the sentence third, the engineering last. the only thing mount needs eagerly is which copy of each region wins, and the payload CRC could move to the read. -- [ ] **Q3. `exists` and `list` are implemented and have no caller, and the - reason they were kept has gone.** They were kept for `P3`'s local program - editor, and the decision there was pre-compiled profiles with nothing - editing them, so there is no prospective caller left. Three implementations - (`fileslots`, the host HAL, the simulator) and two tests exist for an - interface nothing above uses. Either the port contract loses both and the - implementations go with them, or they stay with an honest reason written down. - **A decision rather than a task**, and it travels with `P3`. - - [x] **Q4. A name longer than 63 bytes is refused rather than truncated. Recorded 2026-10-09.** `KILN_PATH_MAX` is 64 including the terminator and the name field is exactly that, so a longer path returns `KILN_ERR_INVALID_ARG`. From 5b4e5b43d92847ce855bdb151f2b0b1eb97504d4 Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:43:31 +0200 Subject: [PATCH 07/14] feat: the buzzer says "finished" briefly and "something is wrong" relentlessly The firing-finished sound was fifteen repetitions of a chirp over thirty seconds, and the fault sound was a 200/200 ms beat that is more polite than the situation deserves. Both are rewritten: completion three 90 ms chirps, then five seconds of quiet. A firing that took nine hours has finished correctly: that is good news and wants a notification, not an alarm. hmi.alarm_duration_s defaults to 4 s rather than 30, so the operator hears the burst once; the 0 to 600 s range is unchanged for anyone who wants the kiln to keep saying so until somebody comes. fault six 60 ms chirps at twice that rate, a 400 ms gap, and again without end until acknowledged. Meant to be unpleasant and to carry through a wall. They differ in rhythm, rate and duty -- a third of the cycle sounding against a twentieth, over a cycle five times shorter -- which is what SWR-SAF-20's "audibly distinguishable" has to mean for a part with one tone and no volume control. The rhythms moved out of the adapter into kiln_core/alarmptn, a table and a step function with no clock and no hardware, because in an esp_timer callback the one thing SWR-SAF-20 requires was asserted by nobody and "continues until acknowledged" was a comment. The adapter now sets a pin and arms a one-shot timer for the interval it is handed. Eight host tests, including the comparison between the two patterns as a comparison, that the fault pattern never stops on its own, and that a buzzer which cannot be commanded -- null sequence, or a pattern value that is not one of the three -- is silent rather than stuck on, since an operator's only remedy for that is the mains switch. SWR-SAF-20 now states the two patterns and its verification method changes from Demonstration to Test. SWR-RUN-06 records the new default and why. Architecture 10.7 carries the numbers. 26/26 host tests, clang-tidy 20 clean on host and target, hardware image builds, StrictDoc exports clean. --- docs/03_software_req.sdoc | 13 +- docs/architecture.md | 29 ++- .../clangd/index/api.cpp.9028AD392AB399C7.idx | Bin 0 -> 24168 bytes .../clangd/index/api.h.7C57622043E2F1F4.idx | Bin 0 -> 4836 bytes .../index/api_log.cpp.ECD6E8B698B80465.idx | Bin 0 -> 9972 bytes .../clangd/index/app.cpp.429EE0B06C87FF78.idx | Bin 0 -> 34728 bytes .../clangd/index/app.h.66207A1D80971935.idx | Bin 0 -> 9442 bytes .../index/autotune.cpp.5E644C8450ADBC62.idx | Bin 0 -> 8014 bytes .../index/autotune.h.ABA8346B3BC72970.idx | Bin 0 -> 5520 bytes .../index/board_pins.h.003CC979DEBB4325.idx | Bin 0 -> 2856 bytes .../configmodel.cpp.38BC0F6BEB86FCF3.idx | Bin 0 -> 29068 bytes .../index/configmodel.h.74606DBA908BC1F1.idx | Bin 0 -> 10892 bytes .../index/current.cpp.AD2AE87B3A3BCB6E.idx | Bin 0 -> 10698 bytes .../index/current.h.620B7FB1FF26858B.idx | Bin 0 -> 7302 bytes .../index/draw.cpp.CE59529FCD350067.idx | Bin 0 -> 3938 bytes .../clangd/index/draw.h.824614D74BA33394.idx | Bin 0 -> 1634 bytes .../clangd/index/err.cpp.AF9C8548C8887FF6.idx | Bin 0 -> 1684 bytes .../clangd/index/err.h.21E50DB0E8019E3F.idx | Bin 0 -> 1188 bytes .../index/faults.cpp.D3BCC87829A3272E.idx | Bin 0 -> 7706 bytes .../index/faults.h.3B90034CCCD79A17.idx | Bin 0 -> 2078 bytes .../index/fileslots.cpp.B4E990AA46522936.idx | Bin 0 -> 11676 bytes .../index/fileslots.h.7D454C4314D7D133.idx | Bin 0 -> 3332 bytes .../index/hal_clock.cpp.3444033FF17237DA.idx | Bin 0 -> 2566 bytes .../hal_counters.cpp.BBCB2E13707BCF16.idx | Bin 0 -> 4866 bytes .../hal_current.cpp.3D3F77BE12D7401F.idx | Bin 0 -> 7334 bytes .../hal_display.cpp.B0F7B92DB11BD7D8.idx | Bin 0 -> 7046 bytes .../index/hal_esp32s3.h.F7C571DF8DED891E.idx | Bin 0 -> 3900 bytes .../index/hal_flash.cpp.BD735EDE9CCE05A3.idx | Bin 0 -> 4124 bytes .../index/hal_heat.cpp.B12AB124127403BA.idx | Bin 0 -> 5776 bytes .../index/hal_input.cpp.10B8CEC564533626.idx | Bin 0 -> 6238 bytes .../index/hal_io.cpp.EC5F8CD87CBB0D9D.idx | Bin 0 -> 5100 bytes .../hal_kvstore.cpp.B7DF5B21F17B1AEA.idx | Bin 0 -> 4388 bytes .../index/hal_net.cpp.5928048DFBE34E30.idx | Bin 0 -> 13292 bytes .../index/hal_prod.cpp.E2093DEBAC13EC5B.idx | Bin 0 -> 4760 bytes .../hal_suplink.cpp.F952145BD4011FA3.idx | Bin 0 -> 3904 bytes .../index/hal_system.cpp.051C49FACAE9C053.idx | Bin 0 -> 4976 bytes .../index/hal_tc.cpp.A936EE8D2D953264.idx | Bin 0 -> 8198 bytes .../clangd/index/hmi.cpp.4C133F4B3BACFE6A.idx | Bin 0 -> 15274 bytes .../clangd/index/hmi.h.55E5381D963422F3.idx | Bin 0 -> 7808 bytes .../index/httpd.cpp.3949980EE3DBFFF2.idx | Bin 0 -> 8688 bytes .../clangd/index/httpd.h.AE03CFFCC5598233.idx | Bin 0 -> 538 bytes .../index/json.cpp.71CA25D08E6E4912.idx | Bin 0 -> 11688 bytes .../clangd/index/json.h.A5CC97710F05390F.idx | Bin 0 -> 5010 bytes .../index/logrec.cpp.C765F885F08DE1A6.idx | Bin 0 -> 11032 bytes .../index/logrec.h.44464D377E7E4D85.idx | Bin 0 -> 7194 bytes .../index/logring.cpp.D4136777A6514E28.idx | Bin 0 -> 11466 bytes .../index/logring.h.906F81554E8AE5F1.idx | Bin 0 -> 3206 bytes .../index/main.cpp.1D658C8212876481.idx | Bin 0 -> 20164 bytes .../clangd/index/mdns.h.C76CB2B1CB5F31EF.idx | Bin 0 -> 15928 bytes .../index/mdns_browser.c.C25D87CE840B727B.idx | Bin 0 -> 11548 bytes .../index/mdns_browser.h.BA6CAACB78CB669F.idx | Bin 0 -> 2092 bytes .../index/mdns_cache.c.294254DF303FA182.idx | Bin 0 -> 26902 bytes .../index/mdns_cache.h.EADAE4363ADEBE57.idx | Bin 0 -> 3064 bytes .../index/mdns_console.c.0C11AABAE1148BC3.idx | Bin 0 -> 45850 bytes .../index/mdns_debug.c.70AFC7F558EBD732.idx | Bin 0 -> 9046 bytes .../index/mdns_debug.h.190DA7C4B218495A.idx | Bin 0 -> 412 bytes .../mdns_mem_caps.c.F3CF0DFBFC63F33E.idx | Bin 0 -> 2974 bytes .../mdns_mem_caps.h.3FECC6AB3E87B33A.idx | Bin 0 -> 1092 bytes .../index/mdns_netif.c.505B78302CC0E92E.idx | Bin 0 -> 13254 bytes .../index/mdns_netif.h.D0D872E6E68AF30A.idx | Bin 0 -> 1334 bytes .../mdns_networking.h.FFFA02FFE676A743.idx | Bin 0 -> 1380 bytes ...dns_networking_lwip.c.BC1C86DD66B32A39.idx | Bin 0 -> 12790 bytes .../index/mdns_pcb.c.A1C59CE9F92A2EB9.idx | Bin 0 -> 11792 bytes .../index/mdns_pcb.h.65809FE99D54C152.idx | Bin 0 -> 2596 bytes .../index/mdns_private.h.E6C7E8B4460B3C26.idx | Bin 0 -> 21350 bytes .../index/mdns_querier.c.1B1086BFD1CC4A05.idx | Bin 0 -> 26136 bytes .../index/mdns_querier.h.8D9747B017BBA1BD.idx | Bin 0 -> 2838 bytes .../index/mdns_receive.c.48FB52E1C144A520.idx | Bin 0 -> 28754 bytes .../index/mdns_receive.h.DE63D8312E409545.idx | Bin 0 -> 524 bytes .../mdns_responder.c.400ADCEFAE8349E5.idx | Bin 0 -> 49360 bytes .../mdns_responder.h.C2F515428F349B06.idx | Bin 0 -> 1896 bytes .../index/mdns_send.c.70C72B8F58248918.idx | Bin 0 -> 34200 bytes .../index/mdns_send.h.4A1D72CBC50AEB40.idx | Bin 0 -> 3516 bytes .../index/mdns_service.c.92D5EB5771567036.idx | Bin 0 -> 10430 bytes .../index/mdns_service.h.7C851492F5E0D8AB.idx | Bin 0 -> 602 bytes .../index/mdns_utils.c.F534F6547BABC748.idx | Bin 0 -> 7792 bytes .../index/mdns_utils.h.82A7215ABE807ABD.idx | Bin 0 -> 3040 bytes .../clangd/index/pid.cpp.7A2C4FF97B438BF3.idx | Bin 0 -> 4162 bytes .../clangd/index/pid.h.21295E2653B28799.idx | Bin 0 -> 2870 bytes .../index/port_alarm.h.398A6B39659208EF.idx | Bin 0 -> 792 bytes .../index/port_clock.h.DB1153E770FCA061.idx | Bin 0 -> 636 bytes .../port_counters.h.C8FD845045631D2B.idx | Bin 0 -> 1110 bytes .../index/port_current.h.DA521570B5762B1B.idx | Bin 0 -> 2248 bytes .../index/port_display.h.0D9E490EBFE7255B.idx | Bin 0 -> 1022 bytes .../index/port_door.h.21BF44FA16DF7DC2.idx | Bin 0 -> 532 bytes .../port_filestore.h.2CDC67F6137FFA29.idx | Bin 0 -> 1192 bytes .../index/port_flash.h.32C19661C6CD7505.idx | Bin 0 -> 1156 bytes .../index/port_heat.h.E8AB1E4F6E4B178D.idx | Bin 0 -> 1334 bytes .../index/port_input.h.09A927FB6F93BEF1.idx | Bin 0 -> 906 bytes .../index/port_kvstore.h.5EAB128A42CCE10A.idx | Bin 0 -> 844 bytes .../port_logstore.h.4AAE0293EEF7E78B.idx | Bin 0 -> 2500 bytes .../index/port_net.h.92B924E7C7938803.idx | Bin 0 -> 2300 bytes .../port_supervisor.h.5FDAC2A543564004.idx | Bin 0 -> 1922 bytes .../index/port_system.h.AE99EF9BA20AE698.idx | Bin 0 -> 3894 bytes .../index/port_tc.h.77F0CC7425708CC7.idx | Bin 0 -> 926 bytes .../index/port_update.h.07DD0CA770131E6B.idx | Bin 0 -> 2744 bytes .../index/profile.cpp.B87D6AA6848F66A1.idx | Bin 0 -> 6252 bytes .../index/profile.h.130A26E1FEB83C21.idx | Bin 0 -> 3104 bytes .../program_store.cpp.BC4CCB967AEDF9AE.idx | Bin 0 -> 6196 bytes .../program_store.h.2EDFE6D17879C6F7.idx | Bin 0 -> 1328 bytes .../index/run_index.cpp.D330C06CDA62E5DC.idx | Bin 0 -> 5680 bytes .../index/run_index.h.394D0BEB2FC87A2E.idx | Bin 0 -> 1554 bytes .../index/runstate.cpp.AADA49FF2FC36235.idx | Bin 0 -> 5122 bytes .../index/runstate.h.AA7C2D3C583492F7.idx | Bin 0 -> 5208 bytes .../index/safety.cpp.627B6D98AF3BA28C.idx | Bin 0 -> 19572 bytes .../index/safety.h.0E0EE6BC58EF2778.idx | Bin 0 -> 10804 bytes .../index/setpoint.cpp.B979F998BBBBEE03.idx | Bin 0 -> 7568 bytes .../index/setpoint.h.ADEF0AAE1F9E133E.idx | Bin 0 -> 3912 bytes .../index/settings.cpp.FE374ADAC354D9D8.idx | Bin 0 -> 5880 bytes .../index/settings.h.BC2BAF94023B13A2.idx | Bin 0 -> 2412 bytes .../clangd/index/sim.cpp.E87AB3FC8C65FB87.idx | Bin 0 -> 16610 bytes .../clangd/index/sim.h.D849C2DDB57F058C.idx | Bin 0 -> 9102 bytes .../index/suplink.cpp.994C7AA985D1F7EC.idx | Bin 0 -> 6246 bytes .../index/suplink.h.DAAAB212E792F5BB.idx | Bin 0 -> 2474 bytes .../index/tempfilt.cpp.4AF84C324F2390E5.idx | Bin 0 -> 3972 bytes .../index/tempfilt.h.2BED8E89BF6BCFA8.idx | Bin 0 -> 2704 bytes .../clangd/index/types.h.AB46E6CB91327A24.idx | Bin 0 -> 14198 bytes .../index/window.cpp.AA9D9AA70187704C.idx | Bin 0 -> 5470 bytes .../index/window.h.5D39ED538CF8C810.idx | Bin 0 -> 3016 bytes .../components/kiln_core/CMakeLists.txt | 1 + .../kiln_core/include/kiln_core/alarmptn.h | 81 +++++++ .../components/kiln_core/src/alarmptn.cpp | 124 +++++++++++ .../components/kiln_core/src/configmodel.cpp | 2 +- .../kiln_hal_esp32s3/src/hal_io.cpp | 47 ++-- firmware/controller/test/host/CMakeLists.txt | 1 + .../test/host/unit/test_alarmptn.cpp | 204 ++++++++++++++++++ 126 files changed, 467 insertions(+), 35 deletions(-) create mode 100644 firmware/controller/.cache/clangd/index/api.cpp.9028AD392AB399C7.idx create mode 100644 firmware/controller/.cache/clangd/index/api.h.7C57622043E2F1F4.idx create mode 100644 firmware/controller/.cache/clangd/index/api_log.cpp.ECD6E8B698B80465.idx create mode 100644 firmware/controller/.cache/clangd/index/app.cpp.429EE0B06C87FF78.idx create mode 100644 firmware/controller/.cache/clangd/index/app.h.66207A1D80971935.idx create mode 100644 firmware/controller/.cache/clangd/index/autotune.cpp.5E644C8450ADBC62.idx create mode 100644 firmware/controller/.cache/clangd/index/autotune.h.ABA8346B3BC72970.idx create mode 100644 firmware/controller/.cache/clangd/index/board_pins.h.003CC979DEBB4325.idx create mode 100644 firmware/controller/.cache/clangd/index/configmodel.cpp.38BC0F6BEB86FCF3.idx create mode 100644 firmware/controller/.cache/clangd/index/configmodel.h.74606DBA908BC1F1.idx create mode 100644 firmware/controller/.cache/clangd/index/current.cpp.AD2AE87B3A3BCB6E.idx create mode 100644 firmware/controller/.cache/clangd/index/current.h.620B7FB1FF26858B.idx create mode 100644 firmware/controller/.cache/clangd/index/draw.cpp.CE59529FCD350067.idx create mode 100644 firmware/controller/.cache/clangd/index/draw.h.824614D74BA33394.idx create mode 100644 firmware/controller/.cache/clangd/index/err.cpp.AF9C8548C8887FF6.idx create mode 100644 firmware/controller/.cache/clangd/index/err.h.21E50DB0E8019E3F.idx create mode 100644 firmware/controller/.cache/clangd/index/faults.cpp.D3BCC87829A3272E.idx create mode 100644 firmware/controller/.cache/clangd/index/faults.h.3B90034CCCD79A17.idx create mode 100644 firmware/controller/.cache/clangd/index/fileslots.cpp.B4E990AA46522936.idx create mode 100644 firmware/controller/.cache/clangd/index/fileslots.h.7D454C4314D7D133.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_clock.cpp.3444033FF17237DA.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_counters.cpp.BBCB2E13707BCF16.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_current.cpp.3D3F77BE12D7401F.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_display.cpp.B0F7B92DB11BD7D8.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_esp32s3.h.F7C571DF8DED891E.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_flash.cpp.BD735EDE9CCE05A3.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_heat.cpp.B12AB124127403BA.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_input.cpp.10B8CEC564533626.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_io.cpp.EC5F8CD87CBB0D9D.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_kvstore.cpp.B7DF5B21F17B1AEA.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_net.cpp.5928048DFBE34E30.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_prod.cpp.E2093DEBAC13EC5B.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_suplink.cpp.F952145BD4011FA3.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_system.cpp.051C49FACAE9C053.idx create mode 100644 firmware/controller/.cache/clangd/index/hal_tc.cpp.A936EE8D2D953264.idx create mode 100644 firmware/controller/.cache/clangd/index/hmi.cpp.4C133F4B3BACFE6A.idx create mode 100644 firmware/controller/.cache/clangd/index/hmi.h.55E5381D963422F3.idx create mode 100644 firmware/controller/.cache/clangd/index/httpd.cpp.3949980EE3DBFFF2.idx create mode 100644 firmware/controller/.cache/clangd/index/httpd.h.AE03CFFCC5598233.idx create mode 100644 firmware/controller/.cache/clangd/index/json.cpp.71CA25D08E6E4912.idx create mode 100644 firmware/controller/.cache/clangd/index/json.h.A5CC97710F05390F.idx create mode 100644 firmware/controller/.cache/clangd/index/logrec.cpp.C765F885F08DE1A6.idx create mode 100644 firmware/controller/.cache/clangd/index/logrec.h.44464D377E7E4D85.idx create mode 100644 firmware/controller/.cache/clangd/index/logring.cpp.D4136777A6514E28.idx create mode 100644 firmware/controller/.cache/clangd/index/logring.h.906F81554E8AE5F1.idx create mode 100644 firmware/controller/.cache/clangd/index/main.cpp.1D658C8212876481.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns.h.C76CB2B1CB5F31EF.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_browser.c.C25D87CE840B727B.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_browser.h.BA6CAACB78CB669F.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_cache.c.294254DF303FA182.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_cache.h.EADAE4363ADEBE57.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_console.c.0C11AABAE1148BC3.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_debug.c.70AFC7F558EBD732.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_debug.h.190DA7C4B218495A.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_mem_caps.c.F3CF0DFBFC63F33E.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_mem_caps.h.3FECC6AB3E87B33A.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_netif.c.505B78302CC0E92E.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_netif.h.D0D872E6E68AF30A.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_networking.h.FFFA02FFE676A743.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_networking_lwip.c.BC1C86DD66B32A39.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_pcb.c.A1C59CE9F92A2EB9.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_pcb.h.65809FE99D54C152.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_private.h.E6C7E8B4460B3C26.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_querier.c.1B1086BFD1CC4A05.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_querier.h.8D9747B017BBA1BD.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_receive.c.48FB52E1C144A520.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_receive.h.DE63D8312E409545.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_responder.c.400ADCEFAE8349E5.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_responder.h.C2F515428F349B06.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_send.c.70C72B8F58248918.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_send.h.4A1D72CBC50AEB40.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_service.c.92D5EB5771567036.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_service.h.7C851492F5E0D8AB.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_utils.c.F534F6547BABC748.idx create mode 100644 firmware/controller/.cache/clangd/index/mdns_utils.h.82A7215ABE807ABD.idx create mode 100644 firmware/controller/.cache/clangd/index/pid.cpp.7A2C4FF97B438BF3.idx create mode 100644 firmware/controller/.cache/clangd/index/pid.h.21295E2653B28799.idx create mode 100644 firmware/controller/.cache/clangd/index/port_alarm.h.398A6B39659208EF.idx create mode 100644 firmware/controller/.cache/clangd/index/port_clock.h.DB1153E770FCA061.idx create mode 100644 firmware/controller/.cache/clangd/index/port_counters.h.C8FD845045631D2B.idx create mode 100644 firmware/controller/.cache/clangd/index/port_current.h.DA521570B5762B1B.idx create mode 100644 firmware/controller/.cache/clangd/index/port_display.h.0D9E490EBFE7255B.idx create mode 100644 firmware/controller/.cache/clangd/index/port_door.h.21BF44FA16DF7DC2.idx create mode 100644 firmware/controller/.cache/clangd/index/port_filestore.h.2CDC67F6137FFA29.idx create mode 100644 firmware/controller/.cache/clangd/index/port_flash.h.32C19661C6CD7505.idx create mode 100644 firmware/controller/.cache/clangd/index/port_heat.h.E8AB1E4F6E4B178D.idx create mode 100644 firmware/controller/.cache/clangd/index/port_input.h.09A927FB6F93BEF1.idx create mode 100644 firmware/controller/.cache/clangd/index/port_kvstore.h.5EAB128A42CCE10A.idx create mode 100644 firmware/controller/.cache/clangd/index/port_logstore.h.4AAE0293EEF7E78B.idx create mode 100644 firmware/controller/.cache/clangd/index/port_net.h.92B924E7C7938803.idx create mode 100644 firmware/controller/.cache/clangd/index/port_supervisor.h.5FDAC2A543564004.idx create mode 100644 firmware/controller/.cache/clangd/index/port_system.h.AE99EF9BA20AE698.idx create mode 100644 firmware/controller/.cache/clangd/index/port_tc.h.77F0CC7425708CC7.idx create mode 100644 firmware/controller/.cache/clangd/index/port_update.h.07DD0CA770131E6B.idx create mode 100644 firmware/controller/.cache/clangd/index/profile.cpp.B87D6AA6848F66A1.idx create mode 100644 firmware/controller/.cache/clangd/index/profile.h.130A26E1FEB83C21.idx create mode 100644 firmware/controller/.cache/clangd/index/program_store.cpp.BC4CCB967AEDF9AE.idx create mode 100644 firmware/controller/.cache/clangd/index/program_store.h.2EDFE6D17879C6F7.idx create mode 100644 firmware/controller/.cache/clangd/index/run_index.cpp.D330C06CDA62E5DC.idx create mode 100644 firmware/controller/.cache/clangd/index/run_index.h.394D0BEB2FC87A2E.idx create mode 100644 firmware/controller/.cache/clangd/index/runstate.cpp.AADA49FF2FC36235.idx create mode 100644 firmware/controller/.cache/clangd/index/runstate.h.AA7C2D3C583492F7.idx create mode 100644 firmware/controller/.cache/clangd/index/safety.cpp.627B6D98AF3BA28C.idx create mode 100644 firmware/controller/.cache/clangd/index/safety.h.0E0EE6BC58EF2778.idx create mode 100644 firmware/controller/.cache/clangd/index/setpoint.cpp.B979F998BBBBEE03.idx create mode 100644 firmware/controller/.cache/clangd/index/setpoint.h.ADEF0AAE1F9E133E.idx create mode 100644 firmware/controller/.cache/clangd/index/settings.cpp.FE374ADAC354D9D8.idx create mode 100644 firmware/controller/.cache/clangd/index/settings.h.BC2BAF94023B13A2.idx create mode 100644 firmware/controller/.cache/clangd/index/sim.cpp.E87AB3FC8C65FB87.idx create mode 100644 firmware/controller/.cache/clangd/index/sim.h.D849C2DDB57F058C.idx create mode 100644 firmware/controller/.cache/clangd/index/suplink.cpp.994C7AA985D1F7EC.idx create mode 100644 firmware/controller/.cache/clangd/index/suplink.h.DAAAB212E792F5BB.idx create mode 100644 firmware/controller/.cache/clangd/index/tempfilt.cpp.4AF84C324F2390E5.idx create mode 100644 firmware/controller/.cache/clangd/index/tempfilt.h.2BED8E89BF6BCFA8.idx create mode 100644 firmware/controller/.cache/clangd/index/types.h.AB46E6CB91327A24.idx create mode 100644 firmware/controller/.cache/clangd/index/window.cpp.AA9D9AA70187704C.idx create mode 100644 firmware/controller/.cache/clangd/index/window.h.5D39ED538CF8C810.idx create mode 100644 firmware/controller/components/kiln_core/include/kiln_core/alarmptn.h create mode 100644 firmware/controller/components/kiln_core/src/alarmptn.cpp create mode 100644 firmware/controller/test/host/unit/test_alarmptn.cpp diff --git a/docs/03_software_req.sdoc b/docs/03_software_req.sdoc index 1290cbf..7d6e5ac 100644 --- a/docs/03_software_req.sdoc +++ b/docs/03_software_req.sdoc @@ -909,7 +909,9 @@ VERIFICATION: T [REQUIREMENT] UID: SWR-RUN-06 STATEMENT: >>> -On program completion the system shall disable heating, sound the alarm for a configurable duration (0–600 s, default 30 s), and enter Complete. +On program completion the system shall disable heating, sound the alarm for a configurable duration (0–600 s, default 4 s), and enter Complete. + +**Amended 2026-10-09.** The default was 30 s, which with the completion pattern of [LINK: SWR-SAF-20] is fifteen repetitions of it. A firing that has just finished correctly is good news, and a notification is what it wants rather than half a minute of chirping, so the default is now one burst's worth. The range is unchanged: an operator who wants the kiln to keep saying so until somebody comes can still set it, and 0 silences the completion annunciation without touching the fault one. <<< PRIORITY: M VERIFICATION: T @@ -2550,8 +2552,15 @@ VERIFICATION: I, T UID: SWR-SAF-20 STATEMENT: >>> The on-board buzzer ([LINK: SYS-HW-09]) shall sound on any fault and on program completion, and the two shall be audibly distinguishable by their pattern. + +**Amended 2026-10-09 to say what the two patterns are**, because "distinguishable" was left to the implementation and the implementation was in a timer callback no test could reach: + +- **Completion** is three 90 ms chirps, then five seconds of silence. Brief and unmistakably a notification. +- **A fault** is six 60 ms chirps at twice that rate, a 400 ms gap, and again without end until the fault is acknowledged. Twice the chirp rate, a third of the cycle sounding against a twentieth, and a cycle five times shorter: the two differ in rhythm, in rate and in duty, so neither a noisy workshop nor a closed door collapses one into the other. + +The patterns are a table in `kiln_core/alarmptn`, and the distinguishability is asserted as a comparison between them rather than described, which is why the verification method changes from Demonstration to Test. A buzzer that cannot be commanded, including a pattern value that is not one of the three, is silent: the failure direction is off, because an operator's only remedy for a buzzer stuck on is the mains switch. <<< -VERIFICATION: D +VERIFICATION: T [REQUIREMENT] UID: SWR-SAF-21 diff --git a/docs/architecture.md b/docs/architecture.md index 77a288e..8dc4538 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -141,7 +141,7 @@ double is a compile-time-checked substitution. |---|---|---| | `port_tc` | `configure(type, filter_hz)`, `read(→ temp_c, cj_c, fault_bits)` | MAX31856; simulator; fault-injecting stub | | `port_heat` | `set_duty(permille)`, `enable_refresh()`, `force_off()` | GPIO + 10 ms timer; simulator; recording spy | -| `port_alarm` | `set(pattern)` | GPIO; spy | +| `port_alarm` | `set(pattern)` | GPIO and a one-shot timer, stepping the rhythm table of `kiln_core/alarmptn` (§10.7); spy | | `port_display` | `blit(framebuffer)`, `set_contrast()`, `present()` | SSD1306 over I²C; in-memory framebuffer for golden-image tests | | `port_input` | `poll(→ events)` | PCNT + GPIO; scripted event source | | `port_current` | `start_burst(window_phase)`, `read_burst(→ samples, n)`, `present()` | ADC continuous-mode DMA burst; simulator; scripted sample source | @@ -747,6 +747,33 @@ so instead of losing the previous content. The same fake drives `program_store` and `run_index` over the real store in `test_stores_on_flash`, which is the combination that runs on the board. +### 10.7 What the buzzer sounds like + +The buzzer is a 5 V active part that makes its own tone (`SYS-HW-09`), so the +only thing the firmware chooses is the rhythm, and `SWR-SAF-20` asks for two +annunciations an operator can tell apart without looking. + +| | Pattern | Cycle | Sounding | +|---|---|---|---| +| Completion | three 90 ms chirps, then silence | 5 s | 270 ms, 5 % | +| Fault | six 60 ms chirps, 400 ms gap, without end | 1.06 s | 360 ms, 34 % | + +They differ in rhythm, in rate and in duty, which is what "distinguishable" +has to mean for a part with one tone and no volume control. Completion is a +notification: a firing that took nine hours has finished correctly, and +`hmi.alarm_duration_s` defaults to 4 s so the operator hears the burst once. +A fault is meant to be unpleasant and to carry through a wall, and it continues +until the fault is acknowledged, because a fault that stopped announcing itself +is a fault nobody heard. + +The table and the stepper are in `kiln_core/alarmptn`, with no clock and no +hardware, so the rhythms are host-tested and the comparison between them is +asserted rather than described (`SWA-01`, `SWA-02`). The adapter sets a pin and +arms a one-shot timer for the interval it is handed; it was previously the whole +pattern, inside an `esp_timer` callback no test could reach. A pattern value +that is not one of the three is silent: the failure direction is off, because +an operator's only remedy for a buzzer stuck on is the mains switch. + ## 11. Configuration `configmodel` holds a static table, one row per item, with key, type, unit, diff --git a/firmware/controller/.cache/clangd/index/api.cpp.9028AD392AB399C7.idx b/firmware/controller/.cache/clangd/index/api.cpp.9028AD392AB399C7.idx new file mode 100644 index 0000000000000000000000000000000000000000..00bd768c129f25cc0281dd4e2c5279d12abf1013 GIT binary patch literal 24168 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 e)~sEsD;qykx_+t-pCmH~y}?UGjgp`6DI( literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/api.h.7C57622043E2F1F4.idx b/firmware/controller/.cache/clangd/index/api.h.7C57622043E2F1F4.idx new file mode 100644 index 0000000000000000000000000000000000000000..297e960d3dbace19f216cd78b58498b9aaa2acce GIT binary patch literal 4836 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 F{|DBJKCb`( literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/app.cpp.429EE0B06C87FF78.idx b/firmware/controller/.cache/clangd/index/app.cpp.429EE0B06C87FF78.idx new file mode 100644 index 0000000000000000000000000000000000000000..4bc9907e79e526983f492601d657b39abe54951a GIT binary patch literal 34728 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/autotune.cpp.5E644C8450ADBC62.idx b/firmware/controller/.cache/clangd/index/autotune.cpp.5E644C8450ADBC62.idx new file mode 100644 index 0000000000000000000000000000000000000000..07a5b68fd6945cf2dcab2e292389d5e23dac6b5e GIT binary patch literal 8014 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 Itbs=8f6LQlG5`Po literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/board_pins.h.003CC979DEBB4325.idx b/firmware/controller/.cache/clangd/index/board_pins.h.003CC979DEBB4325.idx new file mode 100644 index 0000000000000000000000000000000000000000..4dc86e6b8d16a834f3460d5b9225e6d6334913f6 GIT binary patch literal 2856 zcmYL~2{=@HAIHzwsdKt_WFIqjrc9Ps_VybbMEthXP#%i&zaBjdH%n7&h!19mg>XWed`Q9Y^gx+{bK zOisFvNEJeFU*#sQvn`nlGlOR?@gD|Wn(0(2mfjL&vGHc2q0OzsT?6Ff=j^@q#dPKN>O_j37tWI|-=!4c z+BL7N)JwV$+txE|UgXYcD-6y3YdS0GTC@34_sD=v;aNrx{kv{*%9B%8zlb+fX!I}K zVqA6TGTd|7mlIoUUoCf@IsQcO&L~a6*IkdFDa@C6>VjF#Ip44|UzvPn-Y?xN!x{DRKAJ2!R}IL#+wtCo85ZY2iK@VS*mu7 zCvW6Nbk!FuGG@3T=>xqUOfN6zw1&;@EQ>2%=lu+v1H5y!>!byqg*HD8J|H77Hu!R@ zan9@$(NNv9m6eyqbdE+ppVBY+Z z(7hc=V=_X4rjvcoUz)6~{1{j}QTsoNj-s2Yw|QI0?Y(Nb+0*uAts?2invR^gV(;0J zCuJj&tSQ`5!``@z6qei0lt@g)zqUxgs3lGVVc&cEx;37u9zE=7*4Gkf^8Vs})be+# zkz#{`YqUi7m3Gd>3AM^R!z#{^KQ?_|ncrQ{k?IvmNl;RxFa#1FTuxK>aJN0abKBOH z7wKd>juAsfc4=! zyVUixcT6NOnm*6;$!E8IP}6Be9^S6%O0YhK^@UNcu8q8EZK7E&=MPb^-?;Mk&KC+B`^hXEBV^e2? zds0i%;_b=Tl^5ppsP;lxD}kTgtm^wd)nyDjrp<9C=KF3;L?@7!j8Avgyqj!&$sGS0 zTlMIegYS@BM^DuJCT-(#W0})FV|*)ftA>Tmna@h*p6<~5JDQv>IV^QUeAlDy_hB_Y zy+Ib=qW`-)`+l)?!mZeEU*x$T|3tltz4A?hRfa?uzN1mf-#x8L2Im;f{Vb~VZ`p5A z2OA%bgni)jUh>F6=A|lkAGXgi4WFpizxODK@+K^VZ7~%2fj>W<^|)^K$ehCyubSunCod}*_5++Ms9GQ_u5k(;AV!AXUc*27b$Ff3?0u2~@MScQ;E9Ocef+ra0FBcvAn{10B zvOv(pG^s@3BSeT1yBjwgO^uv)KoJTMj4&fPB6ugr3S=&{KQxa;5%8j+JCmJNh#&-Y z{62&Z3I(U!7CC|9uO7N7W=bUjAE88y;3z$a5?hm7k!e2{K1Ovi$Y`Y>6 zxFA3XbDdtbTA^1eP=pEuElf+E2wYGl1oD;ui(xTRC5rqE1UJl$N(ApdSPvTh{veWl z2SwHcAs7p$5rLneOq{@t38IQs$26dbJP?AgAbBEiL6{ICHe2mcOO@;Lha)JW00eDJTb>Afga$D}FylZD1IoNfiY-A@B{_R($1-+ zL@tV;KrqA1C`8~oiTDplbU+|Gf{(XBB(Z-t2P_W$fqMfmzBKnW<0}9$>^{JF@gatd2N9KD)boPbICVJBvC0 z3@uoHVB_6~HY`2Bc;g|4RR-|s3UQgovPQ1o&PIUMMb!U~VRr$>iwkYoSb*`mLJWHfFkVuKVLJiFs|hje vB*1tfA%;x^7_TA3uzvvK<%1ZsE}Vt`U`PA0!IKK%qxVVHPbOyMf$M(&Kos1j literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/configmodel.cpp.38BC0F6BEB86FCF3.idx b/firmware/controller/.cache/clangd/index/configmodel.cpp.38BC0F6BEB86FCF3.idx new file mode 100644 index 0000000000000000000000000000000000000000..e4d7826a304669cbbd308332b78cae1a14a08571 GIT binary patch literal 29068 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/current.cpp.AD2AE87B3A3BCB6E.idx b/firmware/controller/.cache/clangd/index/current.cpp.AD2AE87B3A3BCB6E.idx new file mode 100644 index 0000000000000000000000000000000000000000..249249083f07fea655251be4c61f10e8d70963a5 GIT binary patch literal 10698 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 TVCAX>%h!aijsYuzW!C)<&ZyT7 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/current.h.620B7FB1FF26858B.idx b/firmware/controller/.cache/clangd/index/current.h.620B7FB1FF26858B.idx new file mode 100644 index 0000000000000000000000000000000000000000..24d9f70595030b5d8c16bff78a37574a19236e23 GIT binary patch literal 7302 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/draw.cpp.CE59529FCD350067.idx b/firmware/controller/.cache/clangd/index/draw.cpp.CE59529FCD350067.idx new file mode 100644 index 0000000000000000000000000000000000000000..5fa4894ad576ccf1ae0632dc559b5b7007f7a3dc GIT binary patch literal 3938 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/draw.h.824614D74BA33394.idx b/firmware/controller/.cache/clangd/index/draw.h.824614D74BA33394.idx new file mode 100644 index 0000000000000000000000000000000000000000..1c46c862e8e5cf656bb69a4d31e04c1263c42631 GIT binary patch literal 1634 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 Sg8q!4gfkhDaWpzO2KXOLv->Ro literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/err.h.21E50DB0E8019E3F.idx b/firmware/controller/.cache/clangd/index/err.h.21E50DB0E8019E3F.idx new file mode 100644 index 0000000000000000000000000000000000000000..ff26206a89246a6d7031719347929d288d9a11f6 GIT binary patch literal 1188 zcmYk4eJs>*9LGP$kJCMU-Tm%vcday{=5fbHN{WTKTIiaj$9XtvDdi+a8p~FBR;x&2 zmYP*-^-swyvss8zYdvX9Q!Ru^R!Q~w{rL9lw%>bquf2Xx@AsLE5fQZvz=mXF_Ca%I z0)7QG9+u3E{mvqQAw05cV=H{q+YOFIb6eLMz*g(YicIRzvzLPpRRj#Deh`1gbnQve5g(SeZ?y>pk%SV!D&si%o@C@xNXTs zt;e>i-!mmzYh(8_r?-l}EeEC_C5Cso2Yqt+uxqSlB*!#c__gUqQr~c8&(yWy+NELb z<&}Da3@$=gWCH_8nm#t9K7F6D?J3I#I^M44&#DGfSJg*>CFfw`6KpAB z^2nV@|6ghXb^xr3QK|WWMcAgsQN-GfCdt2hN6TFZV9BhSQ7icngmL@qnD|l;9|AHI z4Mq|WqabCZIz9w}T*&VTd9g8;fE)$F5H%ly@OgJ#+2qobu~N{W zptaG;_}~Qqe8?FG_qv8Ntw{tNP*5-m!iR8$58|>Yw*w<6C{SP-R>y}R9KT-FYy3Et zN`RmsVI)#M1mRuJRl{(_>RbZ$C}1wZEZ%ip5AVERNC^dWtLSOvr_vh{x zos#i?RqPj7y-cqIy^Y9LrNp-&svT3NmA5MhcMj{wIBNM2t`J(0CHj6MW|n{y1sNk# z@gWEq<`hfnf2)cl^J{W9!w=mOF*oEq1fUBd=H{cA?uM8fjbgeMVs0vm=|(n^`E3@s zjMeM)%jv7ro$y?4Bzi7g2QfDd#dH(I+#nRwB@lBnP)z$r%#}|uy|qX`-}VBhU8Btv xO>MWiZ+Z8d#wQVr9mM~xO^Zj&wN7owFsCJOKP(wZ7XJCzJlj;)^7IxDfWJURE6@M{ literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/faults.cpp.D3BCC87829A3272E.idx b/firmware/controller/.cache/clangd/index/faults.cpp.D3BCC87829A3272E.idx new file mode 100644 index 0000000000000000000000000000000000000000..c904d43185fab83e8d95f92b681cac3bb205e15c GIT binary patch literal 7706 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/faults.h.3B90034CCCD79A17.idx b/firmware/controller/.cache/clangd/index/faults.h.3B90034CCCD79A17.idx new file mode 100644 index 0000000000000000000000000000000000000000..e8375a9b312bd8fc4bdcbd0f4dd29ff4218bc602 GIT binary patch literal 2078 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/fileslots.h.7D454C4314D7D133.idx b/firmware/controller/.cache/clangd/index/fileslots.h.7D454C4314D7D133.idx new file mode 100644 index 0000000000000000000000000000000000000000..e1358da1180a6188f6acd689a0d5d7372019785c GIT binary patch literal 3332 zcmYLL3pmu-8$LgV7{3`aXw1|YBcc#Oc7<9BDXjY~gc9R!Fj&*G(pAWm5~BZFNh*Zd zmMq(a$}0aw$SSQ%BuZt=WmoN)X^x%eneR9AzUO@3d%ijE_l%o^o!vh;0NeICq(p}% z1`_}PD)60<$mImd0Pr;lfRy4#UV|}D%_zO~Eg5oBrUp^5eNK6av!~DJ(UT0W2MA-O z&aJ9C?sCz_P4BAp=jO0=(Y$7d6XT^j)cI8NVsn#;hk}8ULG=lR7g5JYn$2!p7M2G2 zHG3$he{ik-tFs;pW^H5YHlmhpa0+Y!$A}4X6z?2=K@q0t_wTKIa3*O zUCFvEecC`tjm{zE=hyk`J~5WOa}TJ^TJyNx!l9~PqB6os>3eS{d!@K_p0j?*e)H^r zR)~|2O&q^pYuAjwW`?qx&nxP!{5ziJO{(Q(>}y#+as#RQev@S<|L`r1;CO4j*-;ar z=FrxeSMc2Gf;uy6dXggVeC3;sa*c_Rn`t8@%i=(!bF6!_YksZ{ssi?ZFiO8!39{Z(BL7PYmznmW&zKV=)wj`*eAYk8S; z%{8@4TGi4k^Q^&-tQvnsAPYNZu|NitE`*8 z-Ej7sN{;jgcedO}M+`GNO|gA#XFlK9t;g(13Fj<9+zf$gl8_5 zO|tLVGbQ%5)e+x@%V#>t%G`)~9sa7%M&R*5+mYp07p^^?sB#-Hw4QM9v=LYCu8emj znI7&o-K;OqFZHjyxIhX#BYYZJb*i%8ZAC<%+Fb=gu5mZ{b*BDI{?Vc_Fr?eRFfTvl z{P|U+)*`KZ?>LiSyQFcVe4nb_3(r6HYGo569^7LdO(fN31P^%|GcI|OA_}a3pY^@} zrpmv##WL;h@w$|yvE1g8=Z2lt&RJKp>U%n-)she8Xj)a*JC6I%&AIoK4VN@`ztJqT zqbQtM%dITPUczdpg^cpULmJMX+xIussOj`rI1YQ2ZC5&#Z_w9xBa)NY?s>)KgS}BX z$B>cJG%f7Y%ir~b;!DrfTPA60XBVZN0<0g&a-Y%s?mJER+&o$4&u(33Rixb*

jz z$Jmy&Vte<~ma(VWoHyr3Eq}Gy;@;@a={B;t%wF9(y2pL`rTxL^b{a>@DGz^Qeas|M zaEnfBYIHWZr#&<_?Ka`qcBG9*4p4d#)wS29j>5S~djHR+o`+>WF(c=uM|=z3W9#o~ z=+<;4z5V#-r@LxRX^O3XSFFhyb<&$~$fv~hnAnGB*14M&ZB?1wY=PXKtQl zjVQBMhne?d*B>IJ2gOLk9KSdX@xp$+gnTvAuqs>WH?&&2U zW0Y#=x^>`L`c_Y?Xra=3uGetO!^a~ZM|*T~dvbDJJ-1&g9u6zXiCuB@x2(ys9{*BBeUK9+skaDV+kEG42KxM(JMS(prZq?&D@({=lW>&kL83TY|PzdP;@y?L7Fb;I`R=QP~X=njcu^7yTV?a6Js zp0A(cOpa&0`yL!oZ^RFM-fyAw>$_JQP6y3-Z>w$H{(R5fq{k2buiq0|_v_a=6(Khr z=6>EbwSAfCdEKL|3O3i#2ocZ*}lk% zj_KGA^;;JbQlmr2(g2Xc%0_Q>38^HY1OSVp%+VJIzyLsT6%Av_P$nyHOgtWCl7XQN zV5O2$>EZz7;IM|T19v*pEH%5#$zTJdzaxF8A`Xbwp}ajWyTiqj3``)+Bk>sGfM|QL zkKb_4#Fh*UARR@DqKX5eiM)5Co2pLjAcNJAjw8iU#R1XI1?`b9UITv|tp6eguu3uc zQcMRhWHHhvk^0{RZoLQB+J=yUG0c#TkL{2~c4N0;z$z;vnh( zkuJ*7Jesnm<+y-@gLSBk08VP39t)jIOE;%)Gc>R0#saPE7p(Vwbd@-gaQbHCh z8BCE|yK>4mnGBdvNh~RrA`VE!rz*#{JKocdC;(lk;vgn<5L1#ofV?;-ETe4n2$v4> zE+zv_m?KFoX^l9b9CHR|IH|ciFOz{Oq@zjE6mdYbUJ+xWX!Gz@9OxjC`ItgJ#s-WP zQn>gWyaV>vRO6dJ4?tR)Vjb}2AK z?Fwt{s5N_p{YeH!P(?B+nIR5H#q-}}7{uP6UXZ~?NT-le7~+8F?iKTi*IahaD1fyv zJy(jWDh~hkAooemv)mI8X2?Jv?vEkGP{aZ4FMC2hXBHXy7a6EQI&D>&x;P-3R+hNP zo4xXp49p-MPl~6C1ERU1`x6-OuLyDlM*QLiNi$5XT8U3m{ZufV6-W%Epqqm%0#}Rt zAz8d^4G4>Tfna0_2#frHV4FoV{#f3iwJgqxA;jS5m^dqz5R0SBgR@cyDL85&&PpYu z;;1^#D#k!DCbxVSp&39aCWl;tyawxvjE3qWk3m>uFa)dHxK)R#YC6KBMkz(9qs@~6 z7f!xaJM1Zivji9rV8mr}u<|jGj}g%&Lsc;b@}wj)awM$x#g3>V>ZUaDi~l4z&Prpa znWLW-hnrTWV+JS-Di6y@JSo|Rx&R4@6D0&{C9E$pA*z0CzID>Wb?z`cZyX^`0d)o0 zQkIC!xGYP6u?3hB5`e?=Yvf}a(fM&!A;u`g)-O8>%FrvuERZf7w#A?n+k*P^x($nc zrL9>E8=FK-T8*48YO|#Pb3$bh4%Lh7kJR4{{ieC7cHadEd&+sLA(&2k^6>%7wE`Zh z5Hl>qwj(uYQo!}1F@e@2@vv!<$sl3SK1fLBfrLQQ045TR0+a~N00@f)0D{rQg|Nu@ z2qs>8`^d8B(>MWV1rvjnP)?i`O^C*kRGh^l@Ni@voRv&S#!(qGG^B`MhJ_?CygGz5 z2D&=PQLwmZYM|nXhOlT_AQ%k_2#cl!f&n))Dp>R-fx9=s0zRc!#IE+8Uh~J=3F>T( VH9%u|C1C&eaXBpj%fA@_{{liZl$ihk literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hal_clock.cpp.3444033FF17237DA.idx b/firmware/controller/.cache/clangd/index/hal_clock.cpp.3444033FF17237DA.idx new file mode 100644 index 0000000000000000000000000000000000000000..effb14c7f77ac48374f549a04cbf88b79a5ff081 GIT binary patch literal 2566 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hal_display.cpp.B0F7B92DB11BD7D8.idx b/firmware/controller/.cache/clangd/index/hal_display.cpp.B0F7B92DB11BD7D8.idx new file mode 100644 index 0000000000000000000000000000000000000000..a1d8ecadb3ca81316d423c2f98c7298808892fd1 GIT binary patch literal 7046 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hal_flash.cpp.BD735EDE9CCE05A3.idx b/firmware/controller/.cache/clangd/index/hal_flash.cpp.BD735EDE9CCE05A3.idx new file mode 100644 index 0000000000000000000000000000000000000000..6e94a2721d41110089706ec958dcc9055f40d400 GIT binary patch literal 4124 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hal_heat.cpp.B12AB124127403BA.idx b/firmware/controller/.cache/clangd/index/hal_heat.cpp.B12AB124127403BA.idx new file mode 100644 index 0000000000000000000000000000000000000000..052d262a48f481bd66b9379a7f3003a264a7ac7f GIT binary patch literal 5776 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 AqW}N^ literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hal_net.cpp.5928048DFBE34E30.idx b/firmware/controller/.cache/clangd/index/hal_net.cpp.5928048DFBE34E30.idx new file mode 100644 index 0000000000000000000000000000000000000000..a9301cbd3a18efe2e9255100437df0523c6bc065 GIT binary patch literal 13292 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 X;278!fmDkDQ6L%JDI5p1Q{evq6G%j2 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hal_prod.cpp.E2093DEBAC13EC5B.idx b/firmware/controller/.cache/clangd/index/hal_prod.cpp.E2093DEBAC13EC5B.idx new file mode 100644 index 0000000000000000000000000000000000000000..27b6fdf7eda6e7cc6832e374e163c2c5aabb0850 GIT binary patch literal 4760 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 WKtdXj3$n`uih&D27QmecApQ^2Um}kH literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hal_system.cpp.051C49FACAE9C053.idx b/firmware/controller/.cache/clangd/index/hal_system.cpp.051C49FACAE9C053.idx new file mode 100644 index 0000000000000000000000000000000000000000..c544f0e0f670aa94d942ffb7d93c165a1a28e307 GIT binary patch literal 4976 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hal_tc.cpp.A936EE8D2D953264.idx b/firmware/controller/.cache/clangd/index/hal_tc.cpp.A936EE8D2D953264.idx new file mode 100644 index 0000000000000000000000000000000000000000..7ba7fef55bc5c55cda39a19a4a92ad90eb236e7c GIT binary patch literal 8198 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 WY1-89XU+^Zy6+d#Swu literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/hmi.h.55E5381D963422F3.idx b/firmware/controller/.cache/clangd/index/hmi.h.55E5381D963422F3.idx new file mode 100644 index 0000000000000000000000000000000000000000..d50daf0d0896a1103d16933179c19ff2e7a61bde GIT binary patch literal 7808 zcmYkA2|QKX+sD_|4msOCI$5?A@)2&nnPn$~ z3y=4y56%ByiT~2d=O5S)jjwD14m@zK;F+CGc$Z4{zt9SKp5= zS9MxCnc&0|h95V7$}&%sT4hxS+7_3&Wfy-b-!$j^3-28le`k8j%UxuhQD@qA-co<~ zd+v>Kwi}OD6;>ZKv#e|Ozn+y+pVqZ{wU^B0^OG|-h{vH(o>x?dov8Zvk87LNZk6tM zCskWIHZM?jv)1i@%`^T=>X;k*Tgf%Ak_AE2lIB_!21YpAy80!kPxVh7>d{!9)Bo@R zm1j?qCzZ7xOq!7^iQE-A`>E04s}4u9{Wsiu^+>#LWLsqGmagt+`VY=^y?rtBLwxi+ zr!5AfUv6kRXY==>gIUKGn@6~s?OW^-l)UTd`#W=n9m!6ZmAdsW_Cb4>(bG9^US~M{ zIOtq*_wpk4N&HLDO~xtJO~m^f;GxTlbdf zq!zP#Rvou8W5+L@xzqlgvigpgw6ts;>ZO$fJN4k8?Y~E@>F2ONR;9OUz{9pD8;j=- z)?FC(C^&ig_0ofhvW%+d!{1GOS#l#gbiw-D33+AlSu?&>IW9BWsy+TN`>?d?gWsK& zt(7I_%^z3q?)tjBW@yHr58|@(r(Cw%_h;H<>oY^D!(#Imci-JLBbrT75(VrH4N;zV z{n}Pt$+xd>(yTsSo_PNGeNW}RL+rY@Z@DQGe_!?8sKq>HPq%|-e*d7!|2&B}^I~$? zuj$VZ{wO}`cD<=)>PAa_7u$cg4~lOa+FG?IVxUh{!ktoiK;9FF>;LNSHGDsK$Phc@ zKd2JDr*og3-*D*73EML7Zp|I0#eOGT3g3F&|F$_}9lK7w@5MQFtJXTVyY{o6b>mG) z^11M;`maaE#pJby9Ifr>TxyvTo*g>N_C-mH`thQ@hg)0w&3i`GDff=bj~QWD_vK30 zt$;CIDgm++i~cE^5m=<>5E1UXDX!O{s(uw-6K*+>BU9zjhR9#v7#ieO^`h1l`d!HvZd5(>W%D3-tv|r2ZV~&dBNu4X}63#lV z88dlDS&Pn<+UxU_e%rFBrta~Ak9(TFEj@e8vdQY1EXMTM%&Gz7J3Dd?FRPJ8-UwqT~SDmj__nxX&FOFGryFMaK`tjz*fYetF>zz_X zE^lccS>IXh-rww-!VWm58(fj!bC_uOFfrhId(FT;Ll~>Jr8=L-d~E*j+XK;vZx0_l zcwFGuBz`R$b8`63-lol28PV3yRbN}~@;MY%dRe`?<$82n;(&wQ zNAFoQCx3BIP9M`|T;NdlSA+VEpn<`WovXwf+qz5d4?mZ)>WA;(V9n%z`?%Llj+?l1 z#GBvTdcCRrdPV)%_kIUyBQjJEIEmie?n!zOI{ zVQYUC%_CE4OQL%x85Q_e_|-2l8q}=&>0+;xIp+)9qoPLJzJ8KvXy5iJ`s{xuZ2f$f z2Ze{9EE#7>9v6HFeOVD-djGj&tA)*$vIXmPmNZ&@|42hB_`Iaj~A3zwLK_ePU3?w7!>oz1yO7k@y=y$bAW3Cw-M2aW=N@zF z{oZ^qtth=ub=hvZ20r;fYQ?W z8vCM5-+1QTnA9aMI_PNky;n~6(cS6u6Qd`I2$`A_6B}|#lVV9)Zz9j9r1?~Tp&_bp zkHO!<(WMPzvyvfX7KF@}%r@o?%Dcl>*pwR3DlAC{d7fmR32#uAO)9?RG%8P3L?Q^0 z@1T@+P>j%!K74*k-n%PLPvlReY7CjeyCAZP>RUyb5M9+?79kE0a%H_+jDC545lcqE zFa?yffHDyp9BQ8Oaoge+cSc0QA;C&YX(gp4G(?w>frLOFm+TR7-B;U97%~xpYAC}R z%0_5NA7zTT4}+k_no$cr+6PnlMeqYBbz%yy5z*xj8}q_cxVg!kS4dI?X-E*lc78O=FaG+_aVR#0=U_c5=*>64wr-*@&@Jj<-=H!zSU%w1cIC@ zNj2gP$|-#+=gd0kG+9jaVe}Ogy@Jvenw}Y4xHV%|%XPEV`WFL0z+?B|ZD9E#h zlCFVcLeXHJouAs`SFLJGV#s93(?IDpP=kbq=qgi#Tpi)HpDwMMUJ&Y<%#swylO{>i z=MCogeT-(+Fo|P2OFTd>qoie&snB4^T;s@b2SOs#84?D=?4z{yQ8J<7hOr#t48y#$ z-gNhZQ*Z`Lf+5cqO1cFeThU;i)q|8X($kDHS+Wp9LZzX`yg_-^T<3E-i<7fi;sbKJ zB;AlVD8G(w*K1U_T_PqXFpFG@&ZYE)rf1R0QnEJ>ERI~t5>Lpnm6C3y41@;r=qBlx z)Viap~LMb&FjJ7!nFO>L{%`%2H@Z9~C8%;{ppN4Y~REl1u+=V989#kswJh z;|=DRXzpB}HtCNdmdpWp6D8e5=?M*njLbX|*UNrev6z^_FiR+U2~1Vd^sLC`%G=kd z;~5*p#0Y{G(~EU@(-Yu!^4UT?lQUac;spbhQ_^x+S4D#ZN%g}H^&2;@oF&sB#7p1H zh&L!FpWZfmTSwXsdp% zX32EOF+noHfH#<4Rt+s?5h3FJabzYx9?MS~&b5s`n*@mSa*CVDW$ zQi@&*i>+vS)}nA|^rhQZru@Z{WXQ9QlCGo7g$DE7yS29XLzL%5mJEiFDUvD1yg_;Y z+umL)oK9aA6H^#w8AUIHvqRBvD`LZM;-c9tyHDDUy~Yw>$g!Q0Zin}xXfO}kHTUQp zRo8!5G9N+$Bmp|SLD{!5x86Tbt&Jt~K+cq88uJF_4@YjyS?8wF&XO3A<0Nq=yg@nf z;FbgH*FU<=k}Qx{Q_|J2eH9IcSXch<;<*30-(yKEghWfC4S0j{iAJ5KHIlXmA~GHx z(_P73hd10K+VR&-RHH00a))WBI_U=iGEJEUZ@2)4zcjt0ItT5_a_(YDA`DPMNlRd% z6b%lNx6Dj8b-_Cb$%K#yNrVw^P;ORHwkyfCe8iGPAcslAOn8H`ed9=@586u}sS_#8 zV1VWT3*K-u7{Jd!tL(A-MaVAbp9=lgQqr|B14V;5#vf_7Sii^cIYTBuj#^5q7PhXU z;rWR_e|l$G3EsVeO88y;0{zmfL9B$aus3pDjO+(40pp zov!9@Suzs(Pn1mT&l~JtF1C-hsOm!Q71Yu zgsG;fj5qwg=7(5us6g`hPX1Rh=?ek&w0%F`^n710zE8LJ7(V?6OM+ky(`N~ z=QRsgxI+Wwj<-AJ1r!koU6=^+IZRvu=QwqwawRon@t;#)P0_0(|b z&B$eHG7bD(7`dsMsRq7JMm|7ofCg@4MsBTUt${Sk$ZgbYG;nj_6T?lVpnRMV!adBC zBYSis(u14hd2}PlgPWsxbR)ono1=GhBe;W`qjq#7u!Ea}b#x=AgPVhNbR(U^ig9R; zZU$D|sfzN&SAdKTy*WU~FkFpZY6>P7{ER%>FxnQ+Fh(Bd5*L7HBGNi!;!qtkd4C?Z zsB1&Jml6XEMwd|x;u*K;WEGS(ZaRc{=&vAo%-}ak<;az|O@2`FZKvq%lsX>s$o9~K z1AB}|rUy3%^yo&ChXvs<9^Kr6kSaW#fle&xY97Wx!K}(xKXj-q=Ds$KRL+(HqoN&_Q+Va1@P0EF71keFm$P>umkeCB-?2YUVZh^Q3WyPnnPeaFj zT&B**y=iX|?juI-L;HyE0AS>?bgT$Z2}VATo+rYyoq;z)uc1Wvsu;Lp>9v#yZ(jzk z3Ob(>;lIr=z+3b>N`xrJz*j~WQ6l{87}zv)F(txX!~itVC6owxf&sLoH&P;`P6iG{ zNe!isCnOdS@T6SzfD3?E0=#EM)qnyP3lPs$3h2fn0p5e6LO_9m4L7EN3c($Sr2_O9 z3Irh%%L5R&+5r1wQ2-~JPzneN1D9D)Ej9lCUuM2_)M%VJmI{!Ms}nFEmI&bH>H~CR zX#j4aAP^KR2SDIz0gQJaA>C(^Fy#nrrwQ}~F-Bo^0MT4&fYDeJfSW4_(2ef|+yeg# z3i3Y)9Q@^PI(ap%cXfHk7PYf(F zT}bKSsw1kvBXL;Y(<##=a~RGSzeX5PV0s}AnH~fV=&?7lJh(ZKM>nDmt)^gi!4DU@ zEY1lR0WKX`yc1qJj6A_SVK{!Zc*;Otf$W78YPGK~x|V)_q|C^J=pY7}g^`ET;S64j zj68ylU~tNeJef{r@cdxl*r&Hp4Bi9Wo#j$ZdEuRfpD7$DLct)6jO7AMiK`XxQIX_e zS-Bbk-3%Nr`kSbMc-~;K0KK_N0mG_YD$G`qfuCT*2GW5Vc*-NQ0Kf`0gU|s96Gr38 z1x&-pGo3T#_!6;dfU&re0fVqs05?}Cpc`KRxVZuW-Dxp%qPXAbX*1GCa6kMxqxc(* dKZufie)r-_8K;}}(*b_|qe;L|h4}US{69$b9XtR4 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/httpd.cpp.3949980EE3DBFFF2.idx b/firmware/controller/.cache/clangd/index/httpd.cpp.3949980EE3DBFFF2.idx new file mode 100644 index 0000000000000000000000000000000000000000..2a2774fce713d9f730dd271bd737ec9ca9d9d6f5 GIT binary patch literal 8688 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 Mw|d*kHT;4911kI3*#H0l literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/httpd.h.AE03CFFCC5598233.idx b/firmware/controller/.cache/clangd/index/httpd.h.AE03CFFCC5598233.idx new file mode 100644 index 0000000000000000000000000000000000000000..8551bb45cf9022874e836e4b3d250c5ea712fe44 GIT binary patch literal 538 zcmWIYbaNA8VqkDi@vO*AElFfyU|hicFn;I%wnrxJvyCwv?e%~&8Ap5DF>%q+jEJPbkJQqkdK4BB{ zKgWEvZkykUhgUnyrmbQW)kxBgPR(>aHiwJz>c=^gN*2WIziDs(CR!`SEa>E#K9LRA zqxN65aFw61c)JZpa8JY+;d2r}ZrA!%`j$WX`m4V8gysw7k2@mVY8e-rnLY0a^vX8n zmNRh`U&grKA=dTz|C6`7pFCWDhkJMT!qbtpw`;X$C!MMivUfa{I;f>I2eOiT>CY>XVNZ!Ue>^Jw1dLLLTg zppqUY?j9y-4gqcfX$DDVMiwqsppv50wBll*X)j~v^?i_S31#Hr5MU7Cg6V`=!YBzb z8mJNkK0SKmb5zbOg^`N`2qhqb3=A+`K-FMdpyGbba`gvJIQ|EjEhC@>Q^in}nv)1p dQe2c=EDmC@g9tES;`n)K@!|LGC;J)M831Hd#`^#O literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/json.cpp.71CA25D08E6E4912.idx b/firmware/controller/.cache/clangd/index/json.cpp.71CA25D08E6E4912.idx new file mode 100644 index 0000000000000000000000000000000000000000..e50c335c44640c3e15b38fd989144db3db8bdefa GIT binary patch literal 11688 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/logrec.cpp.C765F885F08DE1A6.idx b/firmware/controller/.cache/clangd/index/logrec.cpp.C765F885F08DE1A6.idx new file mode 100644 index 0000000000000000000000000000000000000000..10f97f91dad620e218bebe913c79463069fe5466 GIT binary patch literal 11032 zcmZ8m2|QKX_rH6eE3SL5YwF_SUh@z#6-{~}ijoqVXp&M785$IgG7~DIM9EB^DI`Ur zfy%4bAc+!{G`|-i{Lj&UyXXA+e7d#1=eyV1d+oK?T4x_8J6l^xJsQn=i(Qb9=dMk3 z8jU84{|(#~uzf#+M)TIC(Sp*N*81#xF-v-2+qk&e``5#p*OdPAHd+Z5J@a?EXqQ%g zuJV0^%l4?sw#8Y=Ss#PsM!LSJYSw8uw10N_EK;h;j%Z~?{HwL(a<1c;w{n=1m$h$@ z+R=p%pu<1k|87=h z4IcUW^E+=#@FBs*YyC3uC0!5CTbec0^}NdbCO;e++4e#-DlQ^7@86#K z=8Ra*tB^g#4w)x|T<2LGD5vOU8{b8~^(#cIi=BMv#HO%-of))^h)9KC%pU(h_` ziu^O{;b3LZ!AE?&0EC(}Nd zd+l+~qmVVQGha=?cXM7oZ}fIdr?YWR?Bg-rW#Mmb+zDpNySjAllkF^U)Mos1*cE*BPq&N$qfUic)0{lp z`fFEQwkfz#_<5@zGk^cfkfyClK1T~^WxU?zZfWC^aIa&T(Y%k+Rcn_V+^IQlQ(*F) zSl#hezE(;7ZB=#GFQ)S}WezpF)SQnV(o_X+G~U@cyLQ+gsd~KEk!F>D{mtM7ar*Q3 z!^izb*V|QJ^4RgaTCm@lDCS^m^;xlFDQHN4Mv6E=jweX?OqROwN4$z^NvN-5=V% zymC#6Xgc$Yb0K!MOm^#+hHBr7X14@OQjZz;>Rp#H%ig#7Ox1-oaX+uP&i4&J&}Mh< z*Pg<+eX(J~)gN^H@($iAZL0p^TeIYj$-{3;dkoiC3|1LR{MmKf>*_I=|Mr^|uQA#E z?dAR_-sXd$0|!T33a^cSpWi}{=}hoeE8WsM-@72_{?u*@`LC6WEZ0rhurqv(Yrn+R z_5FW;*)#H{bZ`4?!H>n)46T}+?p>aJ-S-ublCXC%(Ce@$rhT(j0a^hH@eqL(IzZ-PO?FfMO3<{YHI_ zHGgN{+Hnbd6jwQ??V4sPXJEIU6zJB$fo*PCmdE@+?@9}9kc1wn3Fs^ zxoBt5l$q7Vr{`|oXI=1E+E(*b9J8*0u5Oxgu;`WOvLg+VTeGVUj~C8u4-G!*SDfTn zx-w^L5tnZKa|X=v^v?~M!k-k9Flw*F?PNO<-d_S@->&F$A237}xM$`~;8nk9e0tN62MRgkUr&Sy90)cDWd`@}I~ z+-p4g(4o%nuTA|;$HqS_cg{30IB4HmW}|D~vrX5}Fk+chho$Y6N0+~gFuJaBEwvW! zu}OB$JNdC|ouNxxP#I>kV^q70Tj$C6?scp&Qhy~)NpN!1q+%f8^{dqt+y2QtceYn%g>BmYI!({5!>9P|>0z|Q zJmEgiy6m|5&r@pe{EzIuc=|?h+dT0MNiT^_Zr_6+JeGdm++*K%O73o$PG^49owV|> zLYe*=(FGe{&yTh(1VM>rJtt&Odb{TbuKMA>zTw?8Q;Dd0n&Dt@&3hHZ1k^0^O%QCdSt^{Wj;em|b$_xHNw#FUh8j z+J0N+c=PGLs_oI``7<3p^LpZv4bEOJJ=1WjvQhqH;W=!)j2ELhWZis9!|c}i?~8JZ z3J*-{(jV^O{@gA@%W#z_OLpJyWq-P9`b9TC%Tk4bL^*S%PjucUv7FcJj(JHt7_)B% zPE$6QvdD#lPP z@Z48w7JP5jq9UIknXZ>Y<`$Zro>wp`x8P=neBeHx&7UEScId9nX)e7;iA6g|qeX#e zC=v~&(7$-6Ht{FzV~mf4x4s68b{r5aM~0*1?_b1tS8MCOuxFOlxHtgENJ83LFBFK%~!kbmbJgdUF=-%mmL_ z#F-TZh+m{>^ELhh#}cPMShTYfj13|ia0c;<7){)`1Ld6;ty#2$35E(og%=528Lm8< zf;3~_IdB*drdC|bt2ZCowU|XaF+uAA%pSmx1QCE40r)fpDX|#v|Fe>LV4GT2ZMcL* zOPruG#h6l&z?tDJO;ccD`_unlqx9$p|8)ulPApp71kHotfjf)%MN=zEow983J}r_( zOPgTWi`e6nCw>t_^3!>}J3)7GM(hM*wb*LhnZz$*@NQU~nRnxQ8jF@Z!Dt4|W}q4g zmNS>D(im(ya^=jg-a4MZD!AffdtAnWi8Pf-}I;2{#C~q}H^FeZpzS zCTLb-R`{feUu5uY*i-A?qoY|YTId90@s!09QGobGjLzEczlb++%vzg9StnAw=7!f1T`Lr!~=YLY(;GG?U4`(IdEiRT7BYA8(>wN>gA$S)_1Y)gFD@!MXh`TKojjh+!lX30xR1GBgGYqoLgn z@Z1`BSD8jz^Utn<&yUstfcw1<&gz3!Xc3z4;w)E9p8==fPs$;q97;@{5CYIChXxd2 zW;vWg0uE!$!r9eRn)FlC_ zkey0lq(VU|g)s~jhvB4lTxk@h5`|4e=5Qby<3wW$2zZc__1a^~7j#xT?yhGu9aJ|G<9gkwAs z5P=CID4`XFaiS=UXiN|-WNdm4;Ja6c6roZSu#vSh>&i223<`xvm6G~+8^@fENdX%{I-NvX0_ z)gsc)eJFY#vd{$Y?$lI0s#vT~?948;T{w1&v7?aRTFc z2!k7EWMjc z4^Z+0oIJrOq5w|)-H4%6f!Z(Pw2+%FT!lZ~pNV1<7X#=4WHFE)NEUa|cTI}oZu_k2 zmPpPfPRUKcy$Li(i+TdMPk;tm1M2PQxOs9%huaIGhfHe3#an=W3owx)9JmeWw}pV! zuvtjY$rqM9+@=amMT}H0R1{UzNumMEK$avL(Tv2S)Q^aF+?szfZU0b4+HvPz@h;1hk*(SC<78@fQz;c zA5sPcWk8z*l!KY&z=8zy0zog(CLNnf$g6~Er1M(~#cQE73LRYOHAufE%uP5@2N`ux zip019**D-+5^xJLZc!NjLGk~fG&&`?j{hLD6E*Z4 z;G6^MB!4{soCiQ1DZv^2fYT4uk+u-t06V^9wH=YE&qDTDIF;ndvrzdg)Fc6EkexRm2+zS`+T4SRfG#NQDD=M8$#2fN>c}A}R_|;s}!8TyUL5K&40+a5(S~%zOyu zA%Ee(3&49JERTKe>Bqk+A9EpmjW_$LKpYvtfo4E!2I6Qe4)g$84-iMbHg@m0@!_8- zYl*S9fWj@HP1@`ifbjyzk;Zlab_Y;IK;WK=z3|KZwM70Mg3KYIj|=mf+JEd2aU*sx z39^$Y9c>a6BvCrrWXMjYFp{AlnZg)`%wb3_zu}llxUlWPtwdm)2w~4_y5U_=T>mR~ zI_@i9y019e1zciXmjMB%ogS&5?JgNrsS z8yS+gn6sEW`3i@pbp>Y?QVv^9TP;e;VeTw<86@_K9$%*U=LZkMk1qk|rSNDR$P2q~ z_x9s9Vs~l)vj#|$cD4&JyMQ#>44e@LnPE^G?W~03$u0X|S^P=#f(vCA3eZe(F?ee5 zWX9{#WlS&G`qG!!s#wU5h0>(66%X0*6rd6^Dk<-4l~A`5PA3higl3g+E(!PvnLjBF za~>v|hq2IMiY)KC2RA^i1`5y!nT?dheg`t| zP=Lpf|CsW=HUI?!!sf z-`fyHH7=e3{4>B15pm!Vobd?SASYXNl_mLig$5Igyad^oDEaadRK7&Xm&K4>Okos5 z1tgKj;>nOi z6+;y>RJL%k5zj~!l_^}bXWPq8-tD8>La5+ep!#Q-}J5D!%2fez_KJr!uA0zFc!R03WlP$dCX zfLBEUu7Cwsz#L7%#H!`Nvpg97*~J3Tvbl>G{o&|cucH^~1c%vv1jwcnkzOA?5Zbslie z3!|lH_^qv^Z)*zS`X->z1O%igX$OpUz(IS_@^PNQ^O`@?h(&lW@OB|>r>~4J`2{@| zD8~X5((`y=5D%ZGV1hs<$Gb<4q+Il|jWG-bjJaW9OFpkhqK2KfpY4+Vd& zKOYUl#Q<&qSq$U`lEq!zU6Uf}JqVrTOXG=nQ^*r;(L}r{o3V}kQ5Ri89 z2w{A%%_p|11hPvg<)H*Bmr%+>DP)&Y7^P6Tl)@N;ier>`yMU=(pcIfyjFTxWASZlh zdxfY8E)n(*1Fgfr6uAfo$^p9^@X*fUz!=mUgR@A%n~rhPF>*9#V9FU3a~Ckq1qve* zQ_d7J3`b|n?M{|0CW2xRhz?S^glACU8B`&eYlqx+s7wO7;k0hyeNs~J5}5?O)1`#6 z2w)rm%t(u=0*oqQBEuPnF~!3eS$h_ymxUQ2?c264?_$-{%LvZ|YXp-o9E$*>h|)2R zLcLKq1C7K7q+pyB$}LhV#!scZ8J)#AXDN&{jGrcC$b=lSlK8Q|im;vw*}0T!tz5{@ zrCj3XL3SR6kq7yC6vilIj#BQ5QZRms&>U_h72~7|C**(smbqQ}$0{P*!~pdeN|Lw& z)UN;&6c=dO#8LeH-2@%R%X1Pe)9rb6#3BOK0?F5*1%Oc?ybi^mk3hWY}wwT~M_r?STXSMvj*dOf(RnIzyS#yg7f z(O7&)D8>oJCSRE=vt&){f0e%>ltlvki`-<+!GUVPsHWVP9KrZUF!J?YHa0gKTa2`$ z98KIp{Qf9~_5sgJ89*!pD5EVCFZz0a-kGttGPoIOx-^T_7;4frS)|5LTSA*l673c2 zRZ(Hb$2!s+#YoNF3f2l)vdCYdMG~FZPE*OEwv!=AbXRfLL4_UH>~7*dha`H+c?w9~ zoTsj*F-hzNGkU>1((d*G+g`AY1dITQ5x^w@-B7%na>ero(%-K35Z0?bj7L=T9g z^ZWZC+A?pN*0Oc`Y(5Z2CmpiWDOo=qDyLJjeg=j{5OxFqmX@FPzm7cg>#7ex5;XOQ`f^6u0Q zHQJ#*IvzNq8!B~EzI6>j#UY_rI*nzG8<=^GeM(=(o7 zZZv(SnThF=r4DOcSFN^RY&UPt+}VQp3;wWJ=eAZ;Lsv&zOI1Z(ZJFb87v~jDD>npg d_Sn9|YwMP6-hX=T^xNdKdzXJ0@C}d&`acD@*$#^`T9xqD9hP5|tux zR3~*JB}%JG>Ch@V>U-Wd_mS)Jzs&!>?`NOq`I+rZQG9iT?8BUw#D}K_vk^jy_%|gr zIoe+aAxHdSNnyj>_=Rc{`JHzLmDX&&)?jlZ*2Yc#jc4%&lS|?-gH2uMtK=SgUfNcl z;h5ljGOXgw@?OiAF%`>PR!W`}Iq(-xQYRzV-LZZ5=1ogfa$0?IcYx_m&s{G!IG&n6 z)cL+-o?qiXVJEi)m}<1Z?`iFgygb{meT-3v8mB|N50RpwEL{mX&^%-HjeTn!=mhw{nkS%2&_ym~bej zCTz%WqtmzYy00mXvci2g=I^{)I+xoF?wi@h&CeR>vm|3XiGg7onT-en50`_=F4YW-jNw%0c26OG89 zRkPdA6x_CyVRwY+wp9lhR}12_Q-V|8$HiVtk-bwgZ;NbTkMj0|s6gj(N#cFA>9ta; z6AY9qU5&hYQ}~a*%lAh<==m~qN3*S_ZRk$q=Uk8eswVZK*9XeY#h>T)kKWT;c>4GX zogbYC5rtbdVxbm0D$y2T@;Oc6;+Pvd-_fS{KlhcpePljDQVDFzZ zrZZjVjeP1klZMo_=+0WtxLA4KN^YZt{~o&{*V4+O$g4|YZ(FYGtMrR6xMm>`>gcU& zDhZVyHC89~YC-r;^XpZRZ@{QVuhmUU4X8h z^r1Q`=leOU`=!-ZY8O_{I$U}4nttF?SzY_lf3&}<3uPT=8G3Q5jMuQg55|msS}!@y z#AgefaM#)LHjipv=Om z-iA)K&DvirC9}TP%GWl%|KQ-It69uBm%e*LU5@eY*u67-_5^gqgdIHB|Mt?1pHrSV zt(`G{sn-7coMSiB2jq)vrtQ)1s+H-3}M(=+b#<1Ie!qwampNK)_TZd>~HSi_%oUN(h& z2iD)I5Qi1!nicQ+<9&Tm%0KddSLB{bpL}7U`_K_KZ||Os9$(Hi{qwwe_uQjaW|j)2 zKPP=(|MSCPHUAyqAxUSCUaoTNjI$L{C#r4~Zl0C%s#CMX#VvY{MT43`GWGg{c=v?2 z8rxSR6U}RBEB~3f@B6){zB%oySF=^F<$mu_Kdtw8;Vxmr&t)xQHDXU=o>#^4#-yT$ z58rcwf7)fv88dj;W!9lNo5uavXFNUWO~;A`-CFirSue*W{WWJwFU94#D7PKdwjEl$ z+E@_Q;!s?;;)u9&qxw=q$zb+(j`WqG?R$N~ zM`dc%xt8UvZ&{ICL8|SKWNGEJbsFerK2PPp>9`--VDfD5 zlO1zVuR-_AFB%H@XKqQqdN=XQiDUm9%W~IRIMq|ZeXFUv+TYcgH+>bi)k@uU`}^W4^=VE^_BknlUo06 zPPupSMbYz~#RWV1;?$f*Wm%GLGsA?f=2eTA{P8UF?V0?D{I(o9OHS0#`iGm$-2A#U z+NKQHpGXY4XK!vy|?c-&6&4)a7=z{-q7JMs;Aua#~N^f=*hur)9MrPXQHKc92+d?PR<>QZ(L@78g3C^S(F3Q!5r`{zF zjN9E@w;Z_&53O9YX8$aGnXkhLgl``{^ei-EERRH!a5z{c zScee^UpXEmuW-0nkwjB*I7B663?mTsjhBxx*fvc_q68dH&`KD~2!tLd!qVN;%|h!F_?eRre3(v?~*IWz_#N|&vx$_SFhv+^6y zqNJ>iE2fl=*VmClnm9p+t)s-qNWwYOpL%PB)w*&>8z*S9wG|mbDhS0hEl3^RxwCSH zwjPOG@d%PcN$QNi2yQGKtG!C?q%nym;;;qJLcj=w=XNZ}9eDT2ghbPEIFc8s$_Ruz z?@lx?dwkfGhxBppd;#B(5oUOkE%Ernf&=8r9sV*Wkr}REC|an+2-J5FN0{wyOU$VF_SsGL}i<`~TKq>-R4`!HIs$Y5er{_A8C*Mc{fXo|PgaP`}f1QBrKS zz*`P!;Yp5Rj}b94GD(ZTM_1prNY5jY8?JZdxvDV&^(l+aEtcqQ!r?d^4&ntVF#_RZ zM;h9{#7G5@COo1LK4~Fa4;`e z#0Z3M+)FAgD!Y(GB6A#$6~$^W0^#Io7fv5ll+Wa%d3b6z5;g)xn5p4Crlh7mIa`qQ zWOXKq#JE0`7plYv)PIrhICcH#^DHj%!}W#|hI~fo17gI~`;>jPoNDj0mP8J?eiCmI zpAo3PxTVoWvXz}pqA(nG<~j2jfw053Jzv(Ebp1~bY2fDd+4@38Mpk~#vN#`)NmjXX zNDU{9V~^dy4&{Zl|ljq;-htw#l6YWraZZx+DKMT>eqKDG%!Hm zKUGvrqFCHQI4_*f2(&PvFHZld{g(X%3c@YKNyUjMF;_eqsSpy%DHHGx@O?tFWZzO{ zB$|QiLq(yQj6nUC1e>U=F`6exWRJs^JWEwZAZ#{dBTCz@c#=f!IGi9#&|n0@1(_~_ zO-4J;{CXa&dDeW276?a1cs?O&*=I>)hpWf)#tRvNupn!>nEaAaNup^u9Ks6`G6LbG zravb-@9U|QL-?Oy%7ASkU}R)*JF7qbYd+Pbia_3Y?#&WX%@WE~4ofPBrHo|xGKiUf z#pF#h_RUhQAyE|W#GGd?Vgx$5QfGA8Df4^%DT@* zJ~$yoB8ATglfWENrRkN63U9kTB#{a3AW@X4$q00icD8=GxY*zkiIQrDxalFQBoAX0p+EQO|9?-*9=~N zd}@ip8xkeq4w6O5T8uyo_OC9{ugeZw90VDJ^ zW610wN)C@m@#Bo}4vX`Pb1)6iKfpQIrXlB-i4gD&wrRKl+iGe(r!9S+q9j3i>wBC2 z_kK`520mv0tp?>g&DR0mH$WlW63s!-5+D$^Y5oBl&#UYp_i-4cGuP*rrJaY=9VS(*y%Hzzeo%aseBl1=}>SfDNdEZJJZS21LO& z%_m?3m|&Zx60lik2dB0?cej)vC=Yj!(Em;~Wu54EVo^XPi&6&-IArh1i@{##%$C0lL>^MRB zs`=`SdQ&yFYHRw1Y)b`k-bLs08kn(JBPfO-J0 z@VscYf_Xuwcoi6!8ukIDa5l|RkPVID>|t;kjsP~{e43dcAC5cDXE15l2S~!%G$BDY zz!A1-G6K6ha9>{E=run0D`b!CIk0a8<%v8wu$u%P5Xxq8V1%#q2JmJ5WoU(Va_ZyjU4$CTs~Tg4m`p z5NzN-Y}4=u_Fp}=CJ!&h7UJVAro{fgt*E)Z<8Lc!LWyBTVfFCWiUGvoreHa62Q(W( z2hbwUW^i!W2N=ZJGzmgB+-2CN2@q_6KWx+F2R57(Y%{Po>;vp!nlkM&wujB|yfuq2OCx@Vv zI7)nY>Ih265%Pg&2uj2e@qvyAN|~d~hY1su3EPAKFd`^twle|eo1kW~XAy9n6O;?v zg@A)dP(kb<05O8Z#jqmJ!5@*|AFSAkat6Dx2j9>w^h1 tE|^YDNe)e!LjU1R%dekV@CT;T&vIKP&l{A^W;;4L&q7Wk5yz1~{|_eg1r`7R literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/logring.cpp.D4136777A6514E28.idx b/firmware/controller/.cache/clangd/index/logring.cpp.D4136777A6514E28.idx new file mode 100644 index 0000000000000000000000000000000000000000..da9d54d11473d0986dc14f824c38465daf6d5d43 GIT binary patch literal 11466 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 PSh!;Ol8uX&Qhxsj&z-=p literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns.h.C76CB2B1CB5F31EF.idx b/firmware/controller/.cache/clangd/index/mdns.h.C76CB2B1CB5F31EF.idx new file mode 100644 index 0000000000000000000000000000000000000000..757c34c2545f49363c779932c434736f8d56a366 GIT binary patch literal 15928 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 L90tdLToU+yOpuIa literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_browser.h.BA6CAACB78CB669F.idx b/firmware/controller/.cache/clangd/index/mdns_browser.h.BA6CAACB78CB669F.idx new file mode 100644 index 0000000000000000000000000000000000000000..99aa91c1941e3408d40a5025274103381d608b69 GIT binary patch literal 2092 zcmYL}2{e>@AIJZOk>#;u%V3(ZOtzVjA}u3U6|9RvTNq;0n+AAlJ!lV7o`Do9k&OC9*r2$ zvSuE!&(P>pD9g^lGHQ2Qk2}>Bd%Loz$tYTJl;unAO~?PTjguC}#D;|0l8>so2j?T^L6uR4VcNiY^3y#UOnX%qTr&rZk z^9;V6Bn64i_@3=@78Z>lVGVQZgVR*PZ^aI#dw&pdoZ=j4BTRI2a+3z!Kj7l+wJw+W zr%Ov;nheY}azwj+h9W)wFuj@L)|opLY4g^|GJ+}3+q7#sZTi@@W2;X#mwBrX{a3BW#9^rHPo?_Q0@@!*hqzBA z+0&`76#5#4M4hX~LVx8Jv$K8*DLI-n+-ad&h`q4f79Zy?^-$71>g{ecN#?YC$vmLqD?3dj$Tddo~yt)3HxBFyvNIu6o$PX zrEHDFIV1XtsV3PlciQ+ODVU}FwIcANSnN1)_ERVnR}wtdma$NHzJ#dm$Rg-)UFGd7yXWuIOd5E1X|KVyH}sEduO zbi_$#DoxrYNPV+iLbbfGZGYU_r2ctug?YovPhUmYAU~O1UH2)>Z1)QzKpdU$OUCh3 zVIA?_pf^(*EzX_2*oBrCV0BTv^{ty%M+x@I(FW$Ou6J12@On3Hb7ta_Xx!w~M0vmV z*;f|4+K1z1yVlpT5}k|e|BNZHv&>XDHLA~1bJ%q$+I~HIeS3xXmy56D?BT?DbB=_R zf?ADO@$}M)s@cde=h1kSR_*2K{*6_en#{Sv(MpCV?Gtxfab9+$@?3M@&mF5qfmJa( z+KywroD{>xhSc=Ko4i2U7R95y-fyXby|VCa;e0%abZ7Ox?~Lu6Q)<1q3MYw!Z7QZa z>;*{E^VD*N=cE5+FNau=ZdZ?V%jEp-p%vFYoNwF=9EK;?^6oVp%G1bb3VN@*6NQoN z2`*%W`{ywZD(>^!EXAmnger&|vmA6%0?Ex2=TAP@cqvg9I`SwK^R_c5{*2+Gl9qX% zbFKD}T=jfFIVyyw=3r~PT5S*e9%4=fKia#r;3FmF0{UOyU!EOga82nh%!VSfWgS3 z`6UfBXBQW9q@sA#GIz$i%x^MOed-h?O0 zc@_dX2tY^EVL%ZQ+6Z^5b=31;J($fbhtAGOVLA0Awh5e-@YIKaSxCA z&IMy-HLH0M>Q60uogE4EADzj1WMf2@g${#Faw%IXywL@6W>!n+%~A494l z7XZice?dW9ZpTgk$kRc6aA%q!jfzm~cXW^K?>W@J*TR<&@gsnIjO>QAXbn#o_~eLw zxTnaJ`h6{gr<>BLNG^o;vGwsl6d_Z%1^+e?v}nh*>{Gw&{bA1>nLfrxAz#tm{ literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_cache.c.294254DF303FA182.idx b/firmware/controller/.cache/clangd/index/mdns_cache.c.294254DF303FA182.idx new file mode 100644 index 0000000000000000000000000000000000000000..77b2dc797f17d8fd531d3e877f68066aa3961cba GIT binary patch literal 26902 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_cache.h.EADAE4363ADEBE57.idx b/firmware/controller/.cache/clangd/index/mdns_cache.h.EADAE4363ADEBE57.idx new file mode 100644 index 0000000000000000000000000000000000000000..dd7f83aa5f2ce140a46f090d66b70ea80d273986 GIT binary patch literal 3064 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_console.c.0C11AABAE1148BC3.idx b/firmware/controller/.cache/clangd/index/mdns_console.c.0C11AABAE1148BC3.idx new file mode 100644 index 0000000000000000000000000000000000000000..4fe3f52c15e5f0fba117632328c8924fc387b419 GIT binary patch literal 45850 zcmb5XcUTq2`~Q7*cXoGX7Zi>M1Q8Svq@y5WFNvBMO^h)y#YAHgH73TyB${p%v490^ zsDKerLBNV4Dt1K$MM(sEZ(zf&*zh-d_H)C`_j>+$a$VuR&dc}C&dyG`r|x^bd-bxe zw^&~Ps`szo4f}a0vREuO`u|Tq|2Q&Vu~=$DES6tWi#|F1B?k|4+@2y*DPuEZ19?|I6rb7mQT)95_=8l-97f$?sX2$n-${y7SrM;S6TpXL|`|KB|*26X) z>$GHWTJaUPZ)<;Z!2svBzo8GdrvREaQ&>b*Q>PRsr~-gZ$&MC{;T8o zzwf=6(Ixfors>bG|M_CZwWY;RUrxO~>ivSQho6`JlJIqxzfwQD(y!yxlT|N%Z1Z}* z_4lMtM`tyT{^>}wsR_wJo?$Dj!7F{1E_8Z-s!)-g@yYsjXU2BN6OLrGYZ1|VdD`x; zH@STB-g;(d8s4kIL=!&Y}~-^bQedYyiPTPah{N1Pg`sRP{_K9%2H$Hxo<4)hKjy-#( z9+=jkO)MGJ_`urOIKPNR6At#9zCIX>xbW{spTyN)Oiz8++i|kFWYijM*d|KQX0v z-O8q;e;T(Sx9#i!vvUegwF_-04_)ez^@H2qeNlxT6_%8CA2c|AvIYOV+?IuDvCCeeM}_eQ2-Uhd*!H^H8h6PgiN}-s!OVqojX52V^g5`p^A= zw!%*yw4Z;mLv44ju+K_=-ad47lMl1B+f%N`g#Z0-{H8zt_*b-rR%9Ug5a=k-K-tVz%{shacri=UPgA2aA zpC9#T&g#Ggfzz%%Ipj3|`Uf?;H%%xRHSfyML+|J9ik~9y+%;x)YcH4l(}81RjHpIve}_dwkx#p}Sq*_~)CQS!?fg8rtCg%eWxF`P;TPnS3#5S-+l1HIGv~4>*;- zA$+o9{Wp(moc)>xr{0{qrO)C3HDdP2yFG@tO%Y;VBYi?)F{9=kIJ$z=JGj-|dh?wy+Ul#mOFvInq z@XpP)ygNL;n?vT|-4EJaopCGKqe~QI|CQqei=4$M&#(2 z55IVv)aq!^l~K;G-T%V8lf=ML>VJbh4erRuxW!-Q2Un-@lXbYh70$AGFQZU6H= z|KUo%=P~i&WzwbevmIJv|MxRGzkBQ0Y2V)S3YSivYavy);wdZhL{ zAbc6zd2Yh;365ED)gGUuFZ#N{tJ=W(D<-_kXtQy|#Uiw$EO_70WM9-^#nsRMKL1~G z&csh&+$_4A^vlp1$HJ3;jqyKv5zW2+qWy_x|9#Wt!NHXORxVxe=P$uMM{h4ya_``m zojpdnFCbq|{rI@gtD5lP7dovy)U{#zRn=#dw5JK*jC=iB>_V?+ulXG6y6xo`iQOl^ z+ok8<{k2syUwpW%?7v}!^Q_6uV~!4~2;J7+rTMaXcluvFH*oZbko>MKT3LTqVqEUz z-xv@xWa#tqwZG;sX!M#(x3w|xSs4N2EZ&aO-2*m0`zZbXjANnyL}`P<+;YCUIY3I9!WW$ z=U<5ED}OjSPrBxkH@WTHuPX`{%MbtjpviaF26su>`tsfA4evKwsOMy+e>^Z{mK;;Wb@O4Z|2*65c>Fs5!5@D0GHFW4 z&0hlZKi;vqY`~JpYeB2d5AE8&;e+FCPFFS^xBK?mH-=0|j=h;&QhcgSe$konH@dyv z>G|1z4$T;$elhd6UKc<3?UnS};f;+fKmGRI7k@OdSf1BpyqW*i2SNi&njlzGg~_sQ zvfLCbZRYI%H9b6b?8OHwB?rp^!9rDQ1&6glOR%6yxbn&ENY`SjnrK(;5%fKRSE^uI zP!kb^g#FsLG7fU2MB+ z#L2XvYWAS1%jf6vu!Ci{{cc6NLy_JhRWL26THGmHP&miO+0nAbuG%4qJ0x4GU|LXV z)*D|hIuz&ZVENCkx~VwaR02{3(}L>Q`VXG$?A*-iU@5VyZb=TeB%f5lw4l;6Dx;?r zhEvr+yDCd^$dYJUnHE$X3d5Gpd9lRWz%tLC60xc+R;ABqT2S2@JS-x{#m(kmIcg7U z1#wtGLQ)0Of=WOAS9o#z@7*A*2iW$&NH5cZ>cA`SPiIbwa(A?>w}-V;6nBa)se)-i zmHgvdp@|#5p{jLu)g)1zWT;FFs=Nyhe~lUbi#xo>YHX{<^gWsuRIX#LZ0`EkVX88p zal2~UZakxDK~;Lf`gEk)rbz?KIeSVxQEg8Qm1#lcGW?4N&wM&ll^NDy#dcVsD$|1M zYoBj-`}W@0)WI^{9{UWUXAu8X!L*=y+Sy~U-oK?M#C`&@O+YmErUjLA?>h_M{^UZP z>Xl-9rBIb=K@}7FZrtvLRa9lhK3=iK8}TwNsP6veE3fNy+Pi^eiTxQ%6bdh=yfaP{k%(9DMc1yMB=N+a=p} z$#{=;3#tYsl_PVDvI81ecG$z3s@SF)&uCgu1^(V8F!@wkprd7*{TXvLF;}DUGA*bs zd@!$d)z0&QaL4J2ZMu=Frma(5TbknMRTS)CIc5)Q33gb5LsJFQf-2(o_n$ldeLdLG zl3`ahXw-m~S>rD#qJt_z|FHZW0{P*UV0$GPZ`y7_6}4^Oh;u1_gf_6G+n=vQv6U#a zo;NM1yvugWkMi$@LCj;3Ef&$3n-*06J?-XrYW9@&@OIkT+S24^(7qvvMCEj2`6#4Y+E=}8~cX6=Hwm;e$Szja53}aeQ<;{+6eEhGz-5e}S?W!Hb zc?W4mt3bO2)v^5fH-2rB-QB@bY**bB9BvA(xr*yD6U6HgeK*tDRUdpl;} zfa==5kYOq`TZKmNXIfCT>0eoVG}-cwgJqvRtgFP~Dxpn}X+iZkZ_iIdG8XoO3^P%* zO%&-JO$(}rW1I%lRQmpX;h9yf2SA}ORBeSSeTSw6Rrp7n;;nDQQI(mh`Pi0k zR9U74Rqc}s*QBJN_Z=+L?6IG!>Qhyk5~c;!+=07mU$*#pAcXZyv^^7PSf&Nlp1;KF z3%;uTsDWj!y|5M%TOpw-VOmgKEc@frpY+!TLBZdw+BT~+RZR=3U;evP>>uaynSMGt`&y-l@kQ|XH~EvWi_m%aUJM7yEz3ZIL%=f*2EEvQZm zUHs?ieqYzAwu`pyhRU>{>ff#X+4A(+UpQLw>`65liIWj+rA-T}sK3T;bXqa~OGit- zsZzzsDy<$S+iji7wa+KkCaJ?3Shm}fYLaT3WT;FFswe$=Rd)C-;cF;h@wzQurv=Qk zpz`f>bL-sa-A6)7OjKa;9l!dmgXNSxC3a~JyR^Vm!L*=y z*4F?0)7QTG-qEt!uKHE{mEO}GcQY-hJ{$dKcw3zQql0CRUA0QnR~bIFX+f2HEk9*v3+JC4EM<1pGSy+3 z8b*D6y9HH6_E$+IIR!sKRa$^;1(+5?(}K!&`R_^U=>1e>df(G@+cce~m1#jW!THPk zj)&g(8Qx)`Xe$)y+c7Pu9=z&$uGQFtu@06)_IPE94q2k%<=QQ%GTt4!$F0QeR|m_V zM$x&4^m~X#%d{YRUYWeA`QN`&(FA)?zw5u#md^MK3jc=Md9}+qsz|ge;$%I}Xy;4| zs_7&CXz|fH%Wsefaf&U@a6n88s&Tc4+`igg{yV(FINcVf(^qI(P(2$bRqAU$`~y^5 zRNEG#_A@Q099PT5aTh}Vggf5Hw);l1n-)}kmqgEPyXW2U4wm1IM9A0me4X~`ObenO zpN4)sf7D-pJ6bl|A90l|u99g`rUli=d*rNNH?J56OQ~J8205%jG}TQDDy4YfXeY-f zlOS3#sx8K-luZk&@15>RYqjOE@Q8P??T%3?n-)~RF7bBB9la{n(XzpQ$0Le(M6sp{ zrUg~U&m7%qTa`_PJ3dxzkBvK;7F46|t@tRgZKr9F|2AOT2BX(tT2T3J7{?MrUlgxJsy9v`btEigJqKajuRw(f<*6VT2O7Oy@J2> ze>LC1vcj%9uFJ=D+T%7YsA^|R(^q{Nmjrj*D%!RhHN0s-wZmc1=TBZ6wZOqL)gIO? zMW3Zmr_HpWx*Z)TT)equk%Q%5yXu_ka83I;I6x`MeJo{=9KFRhjAjK(#$EnqJd_s%B27MPdK+T>`Dn7Hr#M z^lVKFsu{WOeC?T+o#9|PVvpAb#bJZuNAs24f@bQh)yT=F1y#g2Pmij{PjekCC+v4jmK~C1>Xe!mR2#Pb@XY0T>?(N1 z$%<{Vk+)0>sx>WUpM7&m=~}37i;%6z$fu?S)#&~s&i~T(300X6?k~1qXgU~wK{4y? z-;~U>@H_|0UVEILAcrT2mK@W9>Wgvj1b&b`J`ci)wnfu$jK83GQ2wN*!@0v79V`j< zaJFH68>V%PX+bsc^Qz8$N%1B}OO9Q&ABp>sQ>tKEP=)QdT>QnpCYv2BnkFWRMx*4K0MYc_mX{1aGs#at5=O17Ea2u%hA=^HqV`W-U<^6qV@u(Lm z+Z$MZHOAte5!*9ke95#RIu|^$$*qnayCA5QvTdbtL(_ulw~AZtKHqoS3-9HXWP4@g z7t?~uY2~K3tGC5amD$hUD%rM5MrpQNP<=UV*S(S5C+&l<;1!(e^^5Rgq~y zMLuq`Y;?j1sxp2238HO+;ToA1RF7O2q__>3M(>zyzvBxczA*Y8rUlhVOPIBx<&8}{zn}w<@P(?*BtI^bdbQb zpwhp)bfu)xuyRMsa=U7VBF<16rV6G7)q-K?KY3mGhN`yORfSkA#B_+$w4gdW<=ywU z?P*=%XxU^}O_jx|MpJHDP`%S=sAub>E+-uxog+8ucM{Tm z60%J~H0@0bs_&Y51n*lnu1>X0vTc*7%Cw*wpk{kaNqp}VR7X!m+f$?AF)gS@y?c4V z>)Ss(<6t>ykJnDwVW*M5Obe=?ZCCodePk0mi*S6vkyt{QHsX+ahH&*kC2^wz2%UNNFA#uzOyEvT9$kKOz8dpqn6f+{Ao_SqSio>UcQSM5^uT`Ct8`w*`h)mCGS=a?2$HD|l0kFxB#?_epi-|+%+ zxL`CbrUljM`)}+#^Y?oX8d%QQ>xpvBR&G3_X+hO*e}4F^Eh8Q|SpG7quVSPZ8(jj^ zf=Ftho>`JR;t51+hiKbjydcwps_`7J0pAR1@f03$Bercc9?`U*Dwyau(ErS2sxk+C zo)X(rqwX~=sD2zjKJTOQL(d#6zZub*r|a_!*UhvbN>A6PUA(*EIaK!JY~yJ8G5&&L z&4uiCZBo8^2{((fMbTRse?j3iucc4Z`(mx5CDWdk({*vWP918~g6gwoZ+HAW;b<*9 zTCQx%H71oz3o6Tx!-oHqn`p6|{dw`|Kb6ldMlbrD?0Qaa3atdpP9cr@9rl?ixY!|8 zm@PM(Ew`-eU8M?1a_1zuCj(Mtk5t(g+#Q(25^2hq6Ic)!z3+V;-PfCe4-7L|hLoSZ zqS8l#^CO`Jj5xw{mLcV*u&DHwj&A905Gu@RIn$@%=dtM1l#s|0(x=Ylpd$su`w(f* zfHFeLh&Kn?m63NjFuaV6XFvt zB8r#L|1$&zox!eWusaN8zyzT2j`&GG`i@>{&abpUXlP#z3m6o!X`|{!Z`9SjYct3q z^yhf@@$Aq2g8P1f1#zN#oXCQGqWeA$#7ge5600bZ+>1Cc9l1|OtfCaTmvUgH>^@Uw z73*d9^&FU^xX)2oMTO#C!GY)z%%Urj01;=`ys+! z5~I1tXe?N#xv%5EInDhX2Nvq?3w8FA&AR*MdH_?jMgd|cX6ei69ND=?-B2feb>3Yg zn4hnEydJ{*>|yQU%lsUMhPl-BA5(?lXgK?G1RBBq9PKjNpAi=ckwroes4r+l_6y$o zg$M>56g&}0abJ0j)3lP;IZKHSn!8SU}D-RF8pLP z4XljxFC!l@<@Xrrbd0>tfJ)+7Nm@YJgZXPCFn-RO23CuIsKr0k&1%pQRFX^oUo9|b z4soADnu3o76WvDH<0rRi*}JWy+j>)|AEy_5nRcSqSC{CGLc&7YFlBFWy8qgjoa*x!r&g-C&ki3heNCp&39>r2914<-oiPVAt2PM~oQUC+0 zB~&fB9)lTmI_xB&I~=$xp}QQoC!u>BsF7T1q?Ql{%;6i!#LwH)WICs#b9!?~rs2|- zN!Cgyt3aPFQRtf}dH-SK4p5Ku2bNf{KR7X#B50IYi6Q3W{ zwXgM8dcW7BU+>72oEV{VjL^Mq2$T*y3DF0Ij!fR3DYl;}cCH)qqCU1r*RyqGK!O;V zAVxEwL~LIocCMQZqJfIh2gQyII3z|M5~CR~O^Ti-b*vlbqT``bWSkVufMTg#vDArq zg$byA0_t4nCDWut(TS)d17@Sh*(jO;N0IkYiKn3bnfqF3DBzp5C z>dSyrsM{${Fvey3`kO%Cn+uJ2HmJ9 zq;yYGdNLqIX`aFBE5Y${Xi7a6t*XphPf=%SzYF%IgfcqWE7? zf*4S#d{C);%z<&0%I^$FP+KRcQFU{6sX~JKWr8|_0}CmnH6G0CQFmG$G*2DEfca{h z`D!}`EL6W;sD96YMQV>l>YEHmQGZNP$8jKCNKu!w_eoJtQgoI@#cF7=+Pbc{MtdCU zpkj3h1CFQzj;J3ppiFI5rnX_gQFYK!?viq~UAfwcQ5;i;9#e;LV1{r^O%xauivwbD zNL_!Ht_{HbWAO(Jh{OHkI1rB`<8d^jn1y@K!f!Jm0S71Ga0bl99p>UL3`oXbMI|h%#Vu=bFw>IO;(oPw00+j_;@=oBiS(O9 zSVb&}h$U?q#Z>afRMLk5(@4-X62^dd@G5POMH^UDMncL+E5?l| zBcGIU9fdM7g?b*g%!5WM!*7VRAdT+%`=X|156KwG*?+Q*l)&o~fE z;WU9kmDQ_Fd$9um8QSNfe~r?w+z^(`)<=))=luyPO1LJHoXr6cIn-B z={-5{#V&m~fU44+F3tB3_@5_&5(QUq1z>@oL|rm|Q6Np)>1wy>>g!A$n5+8cs{Sxp zy#8T}o)LpHtWBu&8}f}4<82oRq(E?jjy(PM`vSR77dFG{AKI}O$y(8=Zhn{g(juuA zo$5x`s6rxZB`5HIXc!MA@=$UD--&|Fh-^kqb+eVJLM0-V$f<4|iH^u4@(elE%~_-h zX);NZonWMahOtj3`(&rO;YjM3DrAS^R5#f}Z>x}piWB&K^pe#oS*{AeWF_;#9X1lconD)x-&$eX96J zBmZblbpyJz2cwbwnp54P13J8|kr$d1^pK4}b&{()!N>;%hjemCcLG=c(vsdIU21-B zB+`Fr(y&R>x;fEQ!NcDp5`IFY3>TDN5=vdrD>}NOw*VKkP3OJZiQhi486o;O`!t3i z=+7Nw@DB17tP(qp{r1hf8`{{I@=q3=l7$e+krXTu9F_( zI1}b0+4+%dgFz_z?=R(+FXbQruwvE7-25t4Dt(6AKEv&q+H^i~pI_e{s$0iOujSXS z(rfpMk$c5nj9dD#gZ3 ziKr4$6$h@1=sE{(i0B3fZi?t82kwh5_r(ARv~C?PeM)}qEq%(VV&qh@7vu29iQaKy z8wSi2J!Xnw445T0nI(oYAVG9X5Q7=w{1 z$K4nNO;K$rYDcIbVIi`S-T1}F^fpCmry{j0d-*?>Tg}=c)HRjKaG~ao?nPLv=~PCDs3u8V1h=OQnsvgI^v^rK^eaY7zp~ z3#^wmV#KeJrZGxHVTmY;>Eq=gw>;Dg>i!L<{%-Si$ZAic-Z>?pQ-TIvZ3D>Ylw1d3 z!L^YY_(j%KnxFzKAXD|tRD+=MhjrP;Yvb2q(`%P2uFDm7rc;%v zgk~zOIM6Fo>CJ#O%J4PHNCp%t)4O=#1nv&&$-*O=k+8kJmZrp@le@?F~lG+qW(aaFVJ*n|M zsR=kHZ3j-Bu%Y0fzcK9N)ybr@5!t|0PlI zP86ab9gF9UnBXN4+FMn{#&%J4A`TgJ(?%uknGjaUJe}8Tn=hspn&!FMk@1b z?rDVPDB*LI2-1Vf$M$_@uPeU8dJBMi2mFzpzQ(+o(kwG2MR<~AhL=_B6`Gu0tppJtm3(Zo^xO) zLOT(w_!psnIZ%#pIjUPiLnC$);gcMwMyQ&*WTK2F%50!XGD_k=mW;AEuvJD|IZ!I2 zQVy(E&}xMZbf1FuaiCH`l^l4kpywROS5dyohH*zlcQ}xYQ8H!~YcX2OfpUz>IdB7` z8ytuyD4wukEFowK2l5EY?=9&uo~hL&q=7>6}zX~;4<8tIyjsjsAlY%7K zmL&VZASG-rG1?D)LkYdjdBy#_;?1-U=atUqm7WZ^ptQK4gfQT);&NB)V@uUqA$aN{A;1)MH4gDQ}Ml>%BRcmQ~|yJOg$ z={v$3(SNywye=Vs=m?QLs(Yw@Fr5!$d(0 z(qoVnToDQ;A$=0E!dfT_rYibW#R_dO1%;|!s9Iq+O41nBr~BmwQAR>EZ`j-o8WQ@` zJH$JR`RU{DQ`c?1p0!}^fFF*wlTfM<)GDYQBReHHof2#?DMSDLn&5s-@CE=0W|Urb z6BoT-j}|?GndzS4uERZ<(U?O*i$g*%1ViQ3f@`(l38OJE{$@0#<_<7;Wi(f$YtHFf zOBi+NwVCujKJMKvhHv8*?bZPz4|5zw5d5|<8bq<|TrB&;5DSa|7*c+37?oBKTLtlf zK}Cq5A>}uSQRyQcJ*uCQeC=7=Kd;O^-Ch1asY-tuGn5cd`qP-9gruTB%^6ClTPWbu zyq)8--8nr6zh-!hQ*?ET-W<|~f}6T}vmU^vH8=Wt_>F2*TB}CZs&6tKv8=CxPz z24_{vzO!h|h97$wSwHB_pgxeLy1bm%;L6_kH;gKDwPIVXgo4isI}eTW%I`L$LEO}Q zZfgF_aPm#9&rPi#18!-7x3o|O)M#Ecnh#XnFhF4x4t|#+jYtY{PN{DxVB4bc*!)&S z8j)BjDpqR8%)qXa+*U~*FzyPQ8I5b%EsXTa7$Gu7=mv=xXU;Nw#8NVGi? zLtx<2P)N1{DFpH<11P*5t{O63q}yeQbG*!C$QkS{5CuGpR{A&k42uiEm}5XL*aquTDMA&fJ)7Teb1 z5U7=C7&ox(1`c6dy(Pr9q`m-CMIN!`)sK}?@QBzRkr2k!TdvubYa!55Q^jG;c32C6 z`ip`|x^0pk0x#4ERJY~nAuvBc!BO3IR1bmq0oYn>RABseV)~{lHJ?f?h{?H?TE9x| zLk3i7p;cNG1Dousc3{8}HSmZU z1|1I?v7>6}(fXbU1#vha4u>+yHVe0yg##Hd2m8+9e4@E{&|LgEqe#Yi)Hx|&os$yW zq67yrO9hVL=0~s}116D{lSl|O!ZapRiSJYrz<>(UtAbl7T&aarYOR^_a8nDoS>M9L z_I2YG^IO(w=tcOoBK!t(|8o3GIsTIYr?Knl`VrP$QPYQxDRX+?sI4m%Tcy$p-UKAC zQ6Bg`?^L=@bzY~2LDdDD;EiI(Z+tgWUJ8kmS~Gq_j^vgjc|v`h>~p|#LPo}iMg)q8 zUPJ=H@rO^SZ=a{v7OO3a)nF#K98*6! z#x10|pf}ia@bNB6{`}yg$aVRnl^?cvN*6pMJqUvt)grW$&NTd0!e;#UjzRNc4fR&ggZ!D$n%1^_4LycviBVmAqiqg@QC>OG93e zeWLz%zJyUq`71u^M*kcIMKq07k`R{5idPrp&kWuY80%{T~_FB z;E++US&!JPN5MKC3W{}?V%-fqw4H7HZ>v2OFw$6p^Bwul5$Y`ZbBjQ>2#(-lQ}9S2 zj|4|>u_;(DlJ%k^xY!ij6UjZ%5o!oSA(8cxBUCpOJdwx~$q`&^3brD$6*)p-qTmK1 zH;^N^*c7}(E7tBE5_M^kW* zkbA@t+8zqhHIlA5f{RT-iAG8^M{uzzc%_k7nj^Tl6s*?CYTc1}qepadM0bQ22uGP1 z4H|!Z3B5K!okjmoV#{ZWdW06)<1AXC`mRueAuZBc%z804Z0tz;2%W!MYZ#`0(@>1I zf8hu=(Q)6ZYQ}}f=cE4O66y8H>jdoaj90C zz}thPRE)s*<5Q^glIDAf>r_;0pHyo@8K3E)=JinXg9L_@H=1z%loeV*ROzTn_l8-( z$tzO3#yoNUzB|25lpbY;J#_TvSbeM&#yRNET0yTBtdL+7yb|h1}(WGFD zs&7%Pa8wHgTUC9lYK0>lDA=az+f*y~i4;6g^#`gI4xgalp{hSrtp&%dY`Pd5G1`0M}eIvHQepLzzuwHflC0$R_34FcN0fkMHhuzpyr;o8qHj+>eKeM1^p zkI){ip}#|aP7)#~3DL~m4+)`%gx1UyQKA^0C`N)KMlUH9!%M|T7)_xdUJ8$wBAL#~ zbSZi|x4EZ83M-K!z-6PC9F*D~lsYhA4ho-xBEg5Fin%CyF1NWSNg0}?3}eO>)+leT zQTj5VSe-_@elysij$`UpI{LA*4nehRG=BUcTJ)BO=%}LDnt8D6qT6-R6WpM)MQ39d zq<;Q`E0sp;(N;Lxg#P?R|HTR?rqG`;q8=k!Vb?zeJ4AhlXoX$o6ucDmm!cJh<0;rK z>DwhMtTv(`7U{9b3Nz^x>_hrKWQAi%D3~JaQ)DZgWhZQTV_@f|J)nipFto^27yQ*(jt+0TRf*Mt?QLV6VpMot|-$E;j|DBXW!2_&6 zz*bmdOTkv6ZzWbZU4(+?M1M}Ka5@78IhvlMSz!`}g5#QgT(iQd1r)^Vdc1B0pBbDr zqjl#`HKVQjB^_O=Z{43X)qmXZnfH%IA7Y_oTPXQJ#)JtSBM|=3Gb%07oJ+Jo@FKv6 zGNk;;XHA z2=-!zaE{=PM{rjLlwt2OZq(^Kc0G@qK)OJ!q|(s&)75C`sq|n0-J9uKr0Jd0^d1bz z)Lk<5ri^R4O>eSI_hvd9yYzOu^sWrpueJ8fsC239yj1pw9twCd#yjK>d81F6rnsglO_saq85 zmik3`pG5VU+sJd!Z^n@_xdO_qU*}>(L_|wP52%DGxG18Fq6gGfhC)J1B@f8!6jVy6 zQu2T*mV#V_a*+pAu|^mKT_heb#0CwSk%#!h>Fjx^TdLlZ$uepB=V|&d24v|jS$Y#L z4|Uu2e|Dk_+NFQaw)%K^O!>a&-vHE^7H<)%zolGnC__~UnYA&kH zE~+h{=Lb%Q@v`{y@u+kv@tjJ$nIYP#r2ACTlL6C6i)q{-?mXf?k9a~Sv9k4!Q72b~ z{%M?pQZBbHm%G7M0}8InO|Qzq47edTy-^QTsc>Rd+qcFWHPpYz_Al~;0e46nBQNsD z^-<{&-T8>#0#Xak?=zNg@@MwZ$6F^yu9JJiQVa^#%iindXa*F>xIp$|z*aeUtK6Ld zyJfUnZo>4%_sD1u2TEj@5;+(Y3l9`6*>WXff_*ViuaMqQ@zJ|y3GZhKA3}+wV21X= z4DBNr7@5|vYQNOYZ=#VN37RcI3xj|8Xnsb`n}bf+|78zpxQB$m&@qfN7%9acMo0sB zp`#b|mB#YH&OJ}Z{2GJkjf1)ey}?8*q-#h|=4Y1hc9t-Ji|0EtwD%!4OHch#?b4=V zl2OxpHuPkNe!RlH>S}uEQWzPJKLL?mH&1OjPYq^9m*%M-%u_#RzuG zEulpM_uD8X{K<(l^jiF8Eq$>p5_;F}7K#xD_hRVZe)m<+%BB z8~~$N3zzMBO>R*z*(fUI*jA3)f{$>c_oQZ{UoM|&2JTD|0Tlm8MWU*3TRJ^nB^v>BdtHY&5V1^GxT2M(2b&&~fyW znHArxm}w+T<35cC!z=#&XpwYge}@F4g$Z;EWH%{h3bstajj6e3Y7J&;R;an*4I6KT zKSI@bE0Xhi$rpyU!L1jl^#A)DRl~>S&rYTBIVgr56r&((QBWniRn^b2;f;+)BcG2J z7>~4!pk>4ZwnKq`Xhe-aYn4iOE6%&?mn@%HI`Gf&-H$Fb>WRhb+l%XGeJD7g_Bf#q zfXPL0f{al4vstP1f{rfeKJcJZ-L6${88tlFh}tgQwoCU1F9eQpH73OPqg(0o)!@h) z{07t1Jj344a1;Y-F{#C#9EhytE~&+XYVr4sViLiVNK+29nnZdqAeJ<6VLU2-yf2kL zQJkODFT>yW(O$0~KHHvZ%yf++qu80Q%>voX&2&8!$U|nCcu3~bPo9Rj>B$=D(T1ChzZl>!lA$Pf%t~8CLaWh@T z8Y$*xx@t92%guDH(#a}rrt7dy4%g3g!Kl2^s_=(2(>I!+ewv^TW&Gh3HJQ$HEn(-m zvQ*zJH2^$f7+g1k<4<#@!NuZcvDlySO_K3@$=v?fblfBzdxLKR!&*iq&ma0s1KWio zch!4b6kNcr7wXTfyFWK}$fH5W)52+%9p*hOkST?ELd!fMoGFF31@GHJAX5snMc-^O zm??!hqE8N2EOW)?x%E4l={+h%-%2r+)O>yYG773lrRTTGVK8b!!EV`qw;TpN zMG7`5O*bkn7&o^-X;Ps0FyOY*?6%Spdemub zutE)6p|)id7u3KDYHMg5sp6vA@}e5ffOOm>U#a?GQMI!M{B*9S4(=cWe@7eYBJ_U0~vpJ+Cqo^b;D@i!aKvP8( z@vEvoevyJjTFXURI7~@XkgWM9Yheu7qXq8KTEiG8RXo%J9%`*%3YCIKn*SrMZlNy) zkF^$$HFlwBrrs=5ZwbRZRFS27X6b$m*snL+ueXG83aU7udmhmJ00eY=vAM_Is&vEO zc;EJZbC`>OcW<-+{6XqecveSebx&ySU?S8$+Q^-zPRnGm6jm&?1s9%zYN>Ix)C_X; z^~c92%*=eW%y1%oJNnk`$)!IZ%g&EwR~WXTpjK{KD~A9``KQMH&ywxS4gb}(p)0$| zr|}n!zXdN9W&;h6e5?I1cgE2@qWT_DTY{HH13IFx(iuOS9fYMLFDpgyhdO%U8pi|z@Xt#+66f&w($z9w<1dYX@}?$o^S=wrq`%(3F_fXbzc1^1a?yXeM(!9*MmsaP zqfj7)f-|&0^xq!~FLgcid9RE!COH9jh=|g`Jt)QHe<_fZ42@)H&P?t&sF8!3Gm|?e=wyQK%;b(WI$5JTGr6NoCuO=bz9j{#m0qirw;8Zev2Ik{AyHfY^MMl4bn$AV zZVU1W3S_FRYl41FumXVPB1Y}Te=vexc>*;)!HqYZL|smzUXYEc;uLCoiW_gZj2d08 zuRUN2)u`CnPfgG($G0EfiRs^O5Snffnn8Ak>|`Vx|M>};Y^QW|N_T~1gGpQC0obol z&|7qk?AjJe68$;Cb%Z-pfc6WK`-Qg5y!ai#^^V{U1!zM?^s0*VU)CE%vXJP7#0smM zm7lWGJ6y}%;77j#YMdqjVbY)GX#(&R6qu(8K$sMmrwKrzq`*8)01Bm{u%9LXg^~jE zGyy1-6qu(8K%u0-JWT*z3kBwB0t_%u6JUUOngEnzdWm_O03jn?`|ongHZz z3bxu$6M#C50`oKhDCrcKrwKqVrNBH*0CFh>=4k@ZTcp4|O#tcw3e3|4pe~@mJWT-V z0t(F21fWkqfq9w$R1OrFrwPEABL(JZ0#I8}V4fxbLk1L>rwKsyL4kRi08}3on5PLq z^+ADongCQE6qu(8K=na^d71#Md85EQO#tc>3e3|4pgy6%JWT-V6VJa6|8OO_bVII@ zEgQKtsyh{){u~xGj2%}T)@m4geD5fyQ8p;_RGcVuOB8x=pjV>s76*nU3L_biBs5JD znlm6-cqdtSj{!@BZcDg8^MqD;0vn4wp>3Yffw^R};Ivs_BX(N|y3LJ`-WFQj=E8U= zI6dSpStxog6up^1v&E2Xu@wg*vc_tAr5I7k#iL3LuHxcRCAO~O;&ELJyw1hrx)^?)i^pBj z=`I(K1(N3iiAxVDI9p<$Hd|_)EwN9VBL(J2?9=8*;W-lbw31Vv#6ImA$@2_%m$Opf zSuTvTQutXejA|*knu|xZ)Vi9B$79LqF?W|G$a4u|pJg2iS%=tnyADOHL+rcFL&157 zecC+KIuEf=yB-CuN9@zCN8#%c`?Q6~sSvSGdmTAl=fDf(^nwGm$X1K$W?X0vnk~1P zEw^JbOsX87DzkS{&L9(5GFdcrH`e7qwCG z+o-UP$3`W1qr$$I0>!sLshjzrVH7BV1q%CKHYq-v6n`cjo0Nb}N(ciA74JgDj{_|W zm0$+!Qk-@vY?_`^JkD{?a!F}?iF=mYO2BRIF1MAi+e!ozXtm;Btpsr(q*`IK&pW(!nbCX7tAZKldrj+ttwOtl-M$Wq&6sce?YQafa+T^Pj*HF|~G zkprDqsNET`R&`pdvgvVHZG4!E*ng_Wf83`CE~pU~RJMp+P}^NlJ28P?R9jtC+i;-m zMebQHso|GYHuGOnqb{jz=D(pj-QXhjQuTbPvRP^x_L#8M;2WMb57iQqr8JMjdmgB(Xn9YUDarkn~Mm-lh*@nJ4v zr*PmYF3?jr{1g}HY25NOW;5<-9D162mNVGz3}#Ez8613udzQ1f7%HPRWE#(^bT26$jQ3+Zw`Fu;s+FoUr+$g8WfI{$^fwC23X3#iNq6tt4y_ zt0G}lTs*2sn<~N5r6=9H$fkH1HY*rTzDyhm%dM`O!evF23FU2;wHxW;|U1=(PrH5qcY^GbKJFU{$1lq3$?B@dAuZQjD0zIJnAK(H#pobjb0zIrd9j*^_ zvtNWbYy0kvMyI)h+JS8$@9OGZy$(P}!swpzUp1uN(`(rITK!>9b)Pw;-BbS4hV&)} zQRG2x`o0u-m!fd!rcuRFgpVRu29%?qa@39i6{uweiekVCgimmnoJ8R#QD;VR3bj1N zU2+MbOUMeHL3-QE2wmpD6@;#E;3jgpiG1M_81ga_g#R`pO^|!U`5p;?eiAHiHiiWF z4>Z#I&Xn8DlsiJ7lY+&v$70zF`kWmChE5NCd&O3x^OL5dG~F3iSimZ3BUJu_j#OHT zT9u+mCIPOXMpuw4L>@LC7y~c-cOL1rQ?!0lw1KehfPxuX>ls=*1|(}ulQm!H^}%9o zBO?6gAnCR1<#y}kj?iJIV2|vvNA`ja^S^K1x|z_!y4x5@zaqO{ss9KUYz#6&9r-2Ux^gL46fgi8sCw;V6Xx9br^#y{HG@A zwPnPoj0C}}r=Ws#svtc$FruP<@9G8#yg^#Szr1+4vFi!>SC^M#>@7ytW`r?EDK|9U44ZjTzdM}3=_)F@KDi`0$`C{{_a z>dS%l#r4ZqXc)!nh+=gjyS(>^ijSy1%r!^Uh$CuW29&7{%hW&y998jA?vkTw+oS5c zjG|m^RIY|H;F#L^823?*W9m1@)L1qm$J7+M^f;eI7gTgX^@1@b8vTnZy2yb`D!Rmh z%PP9efh#Jy!hx$Qy2^oDs>`kV*-AL#&d84Zr%~zsr%OT8r6^eUM8QhQZDsu`C)luP zT+4qjl}Z!DkOZ+ctYo7gS9Hr2Jz)?MKB;PayNUmdDwR&qA}45XG1Xm+<{iV;e^WFv zMQhF|rfAVqw09UVLvx>@wPHYm7M`H>V!$E|E#jtSk~NgfffNm;a3Di-$*4aQ2lk2? zrGfv*D}9E1%{N~QhTS3*Y|*-H(cWah9?ffy<_`mPuy@a>_4seW(re?eR~)whAr8M0 z$DQ{Qj|1azYp7)CB};LWrPv3CBekJ^AGUY?^gm-x=UehEyWC}yKsIs9T^ho9B0?5%%UxCy zvXWcwa)*#R+;W#C8d<_EcR8Su1Ke_#ml}D=Eq7U|la<_Zmr|XS)-QK~D${Uw_>Tb7 zO8UBvuGgPW3||HIpgD>E5-@%FiArRm(v4}I<|y8ClrRP)DI`g8W1(w+lvCn=vW zAVt9`iVFvVQwmaf;Mqrvw}8r zV2gsbaG+2@g&f$bpsgI(qqyv;U&{jDa5fHuMmCV|1DWXD~X$fwLH$ z<-j?N&T-&8M&~(j0iz2XsKhRn^=oqA6WYdZ8ve7{G(ywm$mw!#rk62O_MRz6Ghnui zXUkp;NRs`N8^G_PiZZftd*SBtP$bk_m{~2%^>NF{A8t1cQ zOK#cqXB@TvepjcE(_>B>j`Cd@-<2CM?)QBe->(OsHfS!8`O+Dqrx4gEklm>h)G3Hv z^Apk`qzkOvq2f%TVW!ZCnP<)t-pmsEGGK<*dxrKlO$-=l|>L+=HvA&H(Q2p1u3+IrrY?E|+BC#v5)zgoGe3Q$)eE zMy1jRK4@vNYAsT1)jmXNJ7ts!2*fKABYANNWSAPFAtFM-j3C4iFbRl^BtQ(0;pIRy z7@Z2So!D=2zT9)#ndXno?(cr*?CyDd=R148^DX9)0G5Cd*&wu0*Q+J<)9F}5MSMyT zpGwb*Ai>iUJcF7gg?{jiA@zgmo~SQ1k({Gk15*-;4qxkwt@Y*5ouNdd&uR1xrxoCq zw=P~-F>&h|uVsD7*q1CoTSDpHz0y=a(T<{lj@zi@u-N|-k(tWetMc=@|GYl9ID&az5$sBG4GczWQXiE1T_4ivbpcOT86A6F?KVWHuo98sKYi$+ZH(0UHk3F1=boP{g5#yQG2; zY~iqlyEH*VaFWAG?vmpKf=U6E!ll=L2pR=63YWIJP`P+bX*m%8h|QwR;h=mF<|-*optCTx(Ngo2CFba)EE$S8?4(1QDZ=` zp0V{TL=TN%k3C|KJ(f1tA~<5RBX)?&9KlKvTPbd&ZEh5(!Rt>)dH5)o`_oB&>q&ka z6+Wiz=h1#1BMs2|dBP}vuBFE-z!y1tF$maRnIFF@77qi7NAat5~?c} zN>u5rN)J%MqlD_4g%X`Q>(m2O6DXlNYoSD?!77aaRgIU2;=P3uCk%GN2vA+2gzC

fePx_3y&RPgpEr1xUr9gzE5x5}g+7v;x!{Q9|_qL!f$qAy7TQ5U3tt2viR+ z1gZxZ0@VWyf$9N1!>cq2)+7R?3{gV$BD*;4ZwFVV-|%?=d;5T{59mXw!(rMYV^WcE7d2@F zOO4o4<0c8LFov&CVwV;hZm}^&l6b*zUNEvH5I0ieMveqtGuUfJ8W~l1mDdgSx&rDA zR?TXORY^|@>6+Yn90pbrcxsw+n5N^*ij5w1KVw-3U7Fer+5 zw#2@EiJeX3*EMkG5?kJtztkSR)E+NMykZY~#g37g_4HrYAA6ww{yuMu!VosZkw$}8 zHTzX9O&Sg2oW+y-Tq#PAr%$O!Q+-O5C3 z(k%)veTjuGu_)agg;4UIaJL@btr&9q^!$DL&7^J2=- zJlva%NKS#$y!zY@;q6L&w!!c=Wk-4&6tpQ3nA;({9mbLggV#L?sV5;yb~J@9^NOqA z4)@z~Ygxu)%XkjiGMIK1pT3IElt2|vui_)g!mj+{t??u4%KN=BGHps)0aX&2HeMO7 z7Gkv+PQT#Oz9B+yh!JFQ)Ax0}GF%v5C@=Ug(Q=n)6QuUFRZHKhWzmGF^q)0_6JOc# zFVBXZ=1hx7HtZs2(cpKP@Yfd2Y0)AyoFZ7RJIj>-HhXnvuM)tf$Z(3305;8r)2sxr zDP~Tw62PXNIqfV$%?Ga%H=Vc{k!;v4rnAM2NH%PV&nfXmBpdc4pYxF~LLSAKN3rD; zTM-&y5Hwp(vlXH71i^si3|JBBNeFg>vl}AR01#Zo09g>B9*&^RcG{Amb`UJ)&SD;+ zYDTb`JDYh#vSGWq)6FAfMxjKxaLPr5DjLBK;p`9*Qri^9(rd)|;e5G~6^<>;r@F#+ zGcR>slqS7++OCal*Yc^Z5PYVkex^lf(wnk+RA%}Y`oK`kH!k$JaHwUWXt|hFE(+1PSaC_AW6@0poTlL9d#`s3tL4l4ue$n#3Fl>=$gm z2-8#+ukyZN?<=5HuvP_pD29HR+#YvJ%6rxSsQUB346NTYf7+n($J2;EBA-V6QTa6H zkIAQ5DOssBaA4YOf42N%7mriHCh0O*m%$})%79af#90H*DxjLN zYQ|~KfO%|Suz>|7P|KiJ0UZoFSWwcfT@1PuP-;S{8I+Xm+a|ni1|?UG0Nv1#Ixa1_jjl{B=G@O48+nE?-d6@g)|N zSV2k0S6fi6fHn)-te~Xyk6Ca`0nY)-Rv?xb`eo z!#;q05R?Xv0{{mUa2VjQ0=@zG27=Pia@dB$c913!nCWpFjw|4V4JQ<^oWpV+lm?%d zIlQcZ?HsoApfno2!{HqToaJzq2c_YuT0pf3lGeuicvrx?B1p<{%9}Z%cOLw3z?;OM zF!E0rw@brF4VzNK?jk@DhdqO--VE0(-sn5bj2-4EYGjz%PV@Gi<_rnEXGY#Lv*=yv zbx+NE;t?d3t#QBkgMH|f4h_jFMxMR8D4 zt|xRsoOGygI;o_Ndxf^+@Slz8eVy<5Ki~81=h5Cjzcp*ESu?ZNnsK(VwssiH;mlcU zvt#qRZJt~XhaFlJb>pC|e}@h~_@5PB1_yQ+U2s|PG;EDo)CAwG zvfkqrj=V7$!{7)FGx3Ks&?Yd#vaiNyIWyZqDQr>A+qX7*`iopDDpXr@>XnUmWQp#E_O>SkT6n{F{Ys3xQI^EeajK85W1SKak*_E;tLG}Wy#xniD~b4BS~bbR@>3A@gvf3n@6 z{8M?f*%+OMz7st?-ER+gP_MQ7*XP)&dl_X6+hWiaQ|I}&doSJhZj^mjV2o2=sgs-C z{*Jm(bNzzytrZ934U#-e#sxaK&3E>Em)LT9MN!#SEv1uZs-|Zo+4Fcj&5Wn}GS3(- z-}t)C)$GXiEzXv6pPV*1xS-x8+mmBFVQJO9;gOKs*BzOl7?IMhdBr)_~wBPG4y zRA`(3pL<^ojgJ{N=g+un=acmKF$)ihvNr$LT$=#gD`GO;Lzbm%pWiBNWNcXyZSU5Qbp1{>Ge>s3zH*wZrXyseJ!{y^zg2ZfbLZ;rd}`?h;)4<8w$?^%)S zk?O4cdy}ex`uYvVrjJ&}Sv+_=|DCUYo@mlM-9r5d_v3mD*0k(x(>p7-Zp_Rg<+-v8 z3$m6(iyt>9w)>X7$qlPHkveJAbyG(Luk`2fQ^vIq-DCN@y5^5NA=++m5^_Q!77b3fKLzzH+&3@tskJ z7q}Tt-ccWMR@hkc&*?UC#U=yK)JbMxbN&ggZY@!3R`~VFlf55&Evr7nxj8M+`AZ)T zuc>emO*#5_#H7L}D{QTtBb()n}-jkbE%Ff^=nmBsGEc3W@gZ~7}~PRbm|v4z`TT3VFs?c5jd zdi$oM^W`InO$YxrReDs=v_&Ol-kmEtzTGzp>gufKk5S?m#2pI?W)C7tr$ zxq09$;ovjdBSASwin`u;^kt}W(-$(LQI zr8r>{HLs!6Yi#P<7m>Mn>G4(fV+S8moDhjj9+JvKTHn8jOo(w}Yi8lp4-_X5i+E99 zW550(G{Ocdu3oQuKDU?K)&2MMpyQ zNT^RRPvL&1_}3O`DIr}5lnTKF5>6p~3n<-U<1jcP4C*m_G#nAl#^<5#d1y}fnDRBb z&7a50N>Owf)GmV)iA9R;fVv%U5`+8UjDBdz#+&=0KZBFe%w%Lm@QZ-49Qlp2C_Lj< zsM-qk$m&-41rE7anJpnsK>i6hnz*taRfY|J#vhJDv5CJ)jVPk%lTiI6)FZh4kd4=f zfnADNED|k>L@NiE8)KhHw3}cRm5hXz`g4ldP%4<53T%l6lUdh?Ht$?67((FH4 zhzArM3i+W>iH*mHLVX5DL4FjQkAmZ)*!%@3xByid`RkB>oh@Gv`Sonv2>Fc+mi3J} zzAb;QGWIwb$R>kfMA7?8a(BY2myN`P2-%B}BblR3ct>}aqO%GP#unu*T4aV4T?f_c znC(;gFvw)1{>f2zd;;JmfT2Y2`Oizrqf}+aVj>P`#evC$=%3iWVDBRbEli|=Noim{ zAri}6?rj(e#$h4_OiTfD2r*YLxcXs7q=ZO7@(D_#V`zrVZ`A7lukQb_PF5r8zOim{kJ8v$A)?_jg>?el17@UF3Gmteg zWL+`DrYXSD9D5LpCdVQZLJ$K;^^xh))MU8H^<+gm@<^rhxGbP6f)TfRzK&z=$-U!|>@~L^>O1 zfRP#O9C{iEPlHK}{28Em225e_10a0B=9_?K6Ps@V8ZE$>krzQN5j16RJk*SbEPV-3 zBLTAHZ$izRZ2lG;a|@b~@m3$F?&(Z=G+TwDy_~$3F=ya3piTn;5u@lcfI7p*HGo$G z_zeF5P!HIA6QG*dxCKxxY%GF&5gf^kkB9ttHco*21U62B{3Hhd=hpjHaTt9!A^#>L zPtms^{}vm!LYG$PPBsqrdxc`~k6W3-_1xw)bRrvziw~-#g0Zorf-(5NZT)W*j_hzQ z{o_%{e16|P-)Yetym4Pc)z?suC>~n$I>Wcr&zeio-O#EV&LbX6GR1N+bSHum9FH?c z;$Y^$=?b}JpcTaCmhNXaJnPMDaLN<`1rg9B!CMqx=C*B-v>i5n3MrmK%A`>WD(O1g zZdBv&HBYVShnv{OHj3d^f_)4>?O(NWh%?sk7S4DJtq75Q_NU|7^Iy7R4Lj9#>XPF~ z(dVH0Ii_JClLD&$Ps@ghZ|xe|knl5(n+z+|4b;G`wI zz-jAnqE7>|X<#@bcpNAn2ZOy1MOOp)YPRAopxwn(K)h3cK{u098q^ANP8z}u*zshb zmkef+sb0Qg6Oxf-z5y4uCs5`IR3-#P_d=OoHcmt`iD-~NT&Z#|-K;^PF$*bXA!V|~ zw2hy_k5j}OWhmO)(A$Iwg^#h1DT9JcgUncHPSAV?ML^>SCf!l=0qAf5Iy14Tgi|Zw z41#mDM4z@BZ1=*R>_>+Cks0yC%Fn;SZ&kEEUTqPw79kr#Nbph-T2Aoqe{F1DVi_Os zwJop;20yss5CInxQ&p+@qiTTzAaT0KDw&P_~^ZN|M5C(g97$#-ZqEQ1%&{ z?}nz`a0cPm{0#23$d(6SS5JfKr-2Q#3=hEc2MqpW^_^G8rH}5!7Sn-jI(vr{1Lfi$ zcgUD(rbtnA7ns(?v_k2%Hx5;H8UPVJqFD>e5)VIMRgp`+*|G&%_x5}bASNL7;9AQs0s9$LmjS3-ndl$raw{k{lm zECS<;fGHs)y@bUXFqok!x(Qe`F?dVk4fMEXMm+8}+o60rR3vkj931&)Y}^M0eQ+e1 zGer+T!2la4A*CdwN){6z6Qq>RV1pYIR13OZC1Te_fGYwN3I3Pzit_M#8Yy^t^nlSl zOwGVi2IF6Yi9|3)_9ai>dtN#gj6x1kOcs$~?Mb^BFi8T~MZrok~AI znGbB7hGf#1)SBewJk$R3%`9voP%V&kr%#0HiOi*p*ufq0w>JtBY$2VGxcO)VxoQaM zlZbZ`sgRPc?PH2~4WDwc{tmdH1KKgc?uNGAa1p_9X2ZK(+Vk^pDo+8jDeTrS3n*s+ zA&Ch^X9MMIHqHUcIc%H@fyiK3+0Qtf8nrNmGuhWMHO+egxD8Hyomi)SFs3^aI%o+Ugfe6>6j z_tbPEP)!6H-^Xzl!I_$~&zj@r8b?hd8%!m{sJ(Z#b{?!R#p~jy;m5j+hauH4G@6KY z-4y{#d)ae%d^4EV%v`O{&-i)!>Q8~^F_8`?r8C6pE?xiPf_)X3XoF5|a3xvbr|0b> zr;O6Qh%Gh%)dn!=JVm#FSuMbh*!=UEQQ)1m5fZ^;Q2iJ*A%x_9s9y|c6Ffu5GiIWF z;YDmJ4^7WQ)?^c&>YF_AO{B_otRV|%WC1fWf}(SQPA>Bfa6-XllJF0g7Y0Yb6%o*rtg57ESbhMmVsIt&sDx_?u9ZQ1rmk+S!QMOrqo1+M zV^@-pLJ}H5juCDTkxeqPXK*@FNN4jI$S#93gBDkCo+qP-# z(}4@!2Pdp5gksmXxWErpGjQ% z>H%@B=wgpmPR5RI9Pnr~KN{IF;fY2b(M)>!5U}Kh&it7@QvWXX|1RDCYpE8zj{0S- zQ^8+2y@jCBA!q{QK^_{Hhm1(vap6U}MQAF?^_P}LAI~wRUg3eypzz{O#Jy#*!Bw27kNe5?p#5YK7n3`UZQH z3S?86<)rANK>jFTy$|FAVLnrcx9vRCyyC>-w|IHN5H}1BCozz6EZ0A|=tM7Gp3Tae z2XAo{T>#Y!mdNLiz%rF0fyN-T~?zgVTG@7-|)!im~=DK`8n;P@khY_$j1Kw{y%K|1yEnu{sluSn0aRx z(EFfZAJbC^@ar)@f_b+W&@oUD!{!e_{sA_h21lmB!N(T?oeKrIZ27}*~mKi@vP$qx>q{Z_~Z zJ=nF|OM2Iy-Ft(A0cX!3$=S>N98C2b4wf1Gtl7NQI}oF7+x;QpN=eJ`h76TmvC>_C zw$&VS3(L70W5$kFAEz@xOH)^Gy!HZ{g|3U2xXhntZDM3RdDzrxrc;(JcT-jxB~(*Y z96mx|YiIB5q{|A#@ED!(y literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_debug.h.190DA7C4B218495A.idx b/firmware/controller/.cache/clangd/index/mdns_debug.h.190DA7C4B218495A.idx new file mode 100644 index 0000000000000000000000000000000000000000..aa3ab357574e2a99ad82e7058a3fa521f41bd1e5 GIT binary patch literal 412 zcmWIYbaR`+$iU#7;#rZKT9U}Zz`!5^#Kk2=ndgBtI}-y##hkMfeEkkP@U&JhirA3C z{#MW-N!&8oKk4eno;Ne6JvMK3d&K1NX3cs3D*JYwQ~C@aqE>qyQaj3I7py3^Aaa`cfTv& zU{))OzOZuVjF0)-%`;C{>lBD>-LmxKq6tT@Yg-*=exWvJm&RSkX_8In?61dLtj(}6 z?p?WN>y7VLGFL5~K3S~W!W$f`ccbD?_Wg$*Hec&SUodX>FyGa5tbw7pGB*k6Erz1h zwBi^b9k{o~X@PInd`2#22Mz~OkQ4~W`kqY`k~cXE6n5lrgbTavIw|yi-p!{#VLJ{x zDTrDIh9@^RR>!gny#fl`bJ#1xgo{#h5<%?ZqU2(I5CasrPyh^a7yyPa6fiM+NQpjs N|2*pgGb0lV0|4ODh%o>F literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_mem_caps.c.F3CF0DFBFC63F33E.idx b/firmware/controller/.cache/clangd/index/mdns_mem_caps.c.F3CF0DFBFC63F33E.idx new file mode 100644 index 0000000000000000000000000000000000000000..6b730b339b3672671be66deb6694bcb4c02b02ce GIT binary patch literal 2974 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 UR&v=->I^3`@e48V!-N?a049U;zW@LL literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_networking.h.FFFA02FFE676A743.idx b/firmware/controller/.cache/clangd/index/mdns_networking.h.FFFA02FFE676A743.idx new file mode 100644 index 0000000000000000000000000000000000000000..5f18013bbacd49e4c374428fce70277258a61bcb GIT binary patch literal 1380 zcmWIYbaRVgWngel@vO*AElFfyU|s3NIeB?3w;IF3Rc6X3_*e<94_^8c`X%BgFo$%BZpnClr(w0@{KSgslbCmJdA?KkeWc*wyKm1LU->C) zkg#RKHiqq!*2*s{SLUpCUDch~_2Apjd47sa>`s%mR;Ni%Ggb_ezHn}n(X|_uLATO7 z-W-nlxI5m;;Qt(bZjMUNvIBxQ_e}TwpeDxIIY;AkT&zu;Xl$x;=Br7KTg~Iz{DpT^ z)ZAvT*{MC-fNk^g)BB_4Jl}H}%1aphI1v)+dPquo{-XEYORs!*6{YaLL-?CwqP()U zmEVn0!886M9zOCb7s)@pREPr@&0|FxOQ*cfe4C;8|Hl6%tJqyH)kU3KdZKo1viTmFiPt1pR6JXY8b278 zbuF>}E)g|3QO2RRDeB#$oZIa;PtDY-O_=kSNl8hGlcBgWH|aSr#cbBeQ7v{YNd`$CPIfr~Mp4$i&yOAZ(iR@f!yo~XXAm`EH<4$Mi0+)$G?-eU}FMI|HrISI{?rJ6_a8!NdS&B#)@2 zw52vIXkem13&4R3wEz~hKw)r@LWL`Do}a~Ty)+ACZ-`bR%wCvbK-J*zg{p4N2fbm}*#{166|q8>(7D#{MOXhs_=q9#K&VQAL;wctoXSq*UR-EGjQ2uMRT- zXs(o&GR$0!3K*|ZpTqAiGBy;t&9AM`D%s4T1 zf&7H`OhBhFNO2-*VUXv9X@Q9XEeFRx)N)|#=OlvI#YM@*%0QY)G{ZG!)=|e#7nnrE N7{p-8AcZKD2LS2l)))W) literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_networking_lwip.c.BC1C86DD66B32A39.idx b/firmware/controller/.cache/clangd/index/mdns_networking_lwip.c.BC1C86DD66B32A39.idx new file mode 100644 index 0000000000000000000000000000000000000000..aae2b047c32bd5261d7e685b69c86de14c105a58 GIT binary patch literal 12790 zcmb7q2{@Ho)c;xgJ%+Mh3H4ES3uXUlqA>@q8&3OT(JQT9a895WcLfNUizBPnp9l35DDKHuhd+!(|sQ zev&gbt)(DsXp8!$!)6C|j8-=O_nJlD!`0(!i!x1CcX&k&3CeJL(d}%r-ZfpbY{ajL zLl!!ZGF>(Honrm&EC=KLTT)fZLuOvd4b^;7X`jXO(lj*m^qOdqXQ}8k-XYa$?!iTy zS0B^-Q0KGu&mm*BH@GgiVEj+U5Hq8_2R9{VWEo``eNBo_o#TA2X`Z{3bK&UOj_zwe zZ5w=7ee~5#Sv^!MG2&t7&bh4FO5R@8%{yntoZ6tG6KwRTE?`5`(JlKIdGFok70glp zrLRTLC%^R8-?Z2=WUTXq)KIOQIjjD?DD^|;`n3lJDd~5bZ$J3+eoR*S zj)TW_`{aIQ0cTTj=$?(2qxe~C0#~j#%$d3OoPEvAK%Z~<4|i?+*_5o?`)l}`k5jbX z99-!3VC3w7n(r27yDobA#OodJLD}{v!?sV6x@}h;&x!YtH@W@zrOe!pKgzWy?VR)W zW1sw=r8e5Td#ZXqwk}%9x%;>?gXjL|#bK}C^z~(4OL$zR>aIWMY3(DU^Ev5n&;9DP zKlP^R?C~SK^=sE|uC9JTx#|9N8RnuHm?c+#Au;*Un~yyb?gzLVuk3pl6UaH0$d9kL z+4^pB#Il}mPj^vndfo{a*0>bA=~d`^ONRff>s{P;SMIN!5hkYP{<4!KtB-Hy{B+cu zbG5$e<+H1f+Y@T#ZmmkonY6>XG2`wZ(hE}cBDXEL-tCs>nZh!7KI^Zkvj&?7O%q&; z%Cc;6I$V4zAkQ% z;e&b8%`Z5ZYEC?n(bh1%ITD_o?5;0%+Q2_yDgTBQp5IM zm+V-sb@^oY%qzhuEe$V&&q>a_mLdQ0*YJ8zlYhgDb5hb(4vpxbcthhVXZLbTuBhKtTA5^Z%8xt(DInZkt{+)u^oa z#i7h4o`WK;aN}O+kG-8TG-9WOf!aJhJ-?q#CGSlPpg^;7N}hV!T7 zjt(8+esby3{mJU1SM8hTZjd@-)BSN;+Rf1xv*w>YdvjCa$=q36>qoy1f#7^|xlI*~ zm%q3Fj68iYG?wL)9oMrw@<6EKJvaB*Cd-LOW{$OspXx8zB!6mG>b#>elb5Vcn?2Z} zdDZbm-b-oy58m~G6K6FzKeo6yeQ#R+nKG%c8JRiuF6L_lu{|MsU%wl3)+b4Dq~U7& z5tHrHJLh@Lc*SMY5 zxlCT2axe0c=l?MNqHVOM{n=xai^h?TWp7R@f7pAgxA&?b{G6-D;d_b6*)z+&1Vq)S zXKQa?7c!4iCVM1*&%S%_R)q5P|GKs1K+-z@{I`4Vs7Jcoo37Rx{KCsO;NK35I9-Rl zccU(by!e{``t-Ofs{`wQ%1zfhYF@Z_kcHi#8{a2BUfX0+@%4L<V&dejW96>->PKjTL@zW$7n!qd4|YBzb4AZ9jHU(o>-M!{m_d?Q_$1 z&ls;#9dSCLsQm5Ul@2Wz1QxsAPJ4Dcb5o)k)qX=my*=V~dCuo=b#2DkZ;Hkzqfa;Z z)AEutzsMDTGLAX-e8yJQyt<9odUf(1KR&E-x6jcj+Gz9a;Gyoz2R*yGyY%U6>rJUH zzfRjEjy-WRw{g__TOJKvHS3=C{<$XxWj(KBuUZ=(GDL#KDj2-u_{T_NEiUU6uvh}< zLsET6GYbYw4c267%F9TV!*+lykf>i8?V0cOg)h%K16<5_4k?{OD&&nt^oSqglp6>85z?-yX?5E z5+PR(aC3ks04uH)kEO_BNn=KhaV}l&``q0XSX+fW`fPo90nkq(rR>G^>%CiNOy{!l zg&f(GR5qob1&&gV`YcTaS&DSfIXV5s{=o0EX8?$(=_^0_cAeE~k|NW}fc8!Ccl!p`0F& z=mGUCP?bocZ8ol6A<){#&q1D6K4TmT=ahyN51Al2Ft|^?Ne8Mvl>3)^QUTa5f1>CdE~`MuQHA8HkiG!=--PRxyn=oH zw$!5%tb;->T?t*R4gDk>BPP1Zp5JgQPo7oWKd8pi#&QCnpM+(f%8wB*yv`PKSto=- z+L25<(#-;W8GT)PFeFp{cLrQZJCzj_e657bIwxe@j=0+q9{>I~5vLS#+hgvggfcEG zPsk;pBn7m;*+|+5{f&JE5`_bD&~D?%ptPA4a;yX)qXo|bo11v;R zg&fb3+;gNQfc`h(vS`aus(vr`k34Iqa8L@7d?D>}{cpl0eac{k(WTZ#Syrx4RuMu) zG*|zdaD@&oM7)pfy<^F0!@!r=`94yO6!N-{Bg~=5PE0|=oLNt?Hr?8GmZUOZz zV9BVR2{8g@yGYRv(fSt*JDy1^@2=29!;)vMEWjER9+6ufy>(r&MVVsser$Sd(62 zpdHNG2S!~@LwBlDOQkAp+fpP|inyY6ln#tiRfsDJn{<$ndXBiF5F&tX#O_8M0(ds; zSN@@vtwFb254?KNVzgTiR`uXW0JRrQQf1SEG$pXP&jR->sFD&4yaMhkG2k_levOn! zGnk_Xv3rml12{&pk5O`D;Ubm+9N7Ak4`NuaZPxxJ&V9PHVJnb%1+roE_YLB`L7GG= z=6H(~-Xc{3oSgE0?)F(xd^$rJB0WTlga}NY2JUIlBV7nwo^CsOa}S?l3&4-7$Pp^hDnVirZiiVI5pE%=_iS<(BkR zpRPDvlMGwiFvBqqWA9l0S4%vcX<(TKjzj%v}7FUEe>&_ z&1E&o%8%%Q$rO-I0VSd-26A9h4lp?ikW2yMk;D^o{6y@ZNRIfHf}XRPzIW3wIwn@b z669*?4-=yA6_w;+nlMqZfijDZT)b(99$hBFY{Gkc(#IqF|D0=tKVgBhi=K0XM>o zK%>46UH>etZ6w__t2tH+h%T7SfjK$A1kNM&dBi1~56sbr7WScKq^AeE?s^~fnc^vh zOKkWywj>&2@+3%~6m!>;V0?0*pF)nwCt(hwbpgc}P`1Qqm?N9wXH&LhMZ-W2rJX}r zkO-fB&vb*~jF+P&{?kEB#({JkC^E*s1EzI=Kcle@Slt2Z#DEOwz#CEmN!{xFMs1TB z`(N|P#&HRT?|t5fNc_sHx$vL$sQY`<>}`hzo#?n@yk77owWMcM!3(u@X9CYVbE^_9x(&ckL1cf=yP@%{)(8eiL5FOYIQ8b+7&A>Td}O#I+a{qAR$QrrmI+dE)<2TUOo zRS4|D;9k-6?@n5K^~arC5pPw$@{ z^c<|nti|L>a5@QNiOp>PmfqmkSiYY2-C*fpHPVg!HC5Rq&+KDq|EvLi4H%Ic7^sDb zwF4m>107)50XBr==p3anZJX9?qL;i9&?x~!QX&+lSu?OR7YZ9)3tkM41~6yc@fI|bgFn~T3-G?@icCn1_i2EJ!2=HpcX8njCM-yaliHEF* z5@YhDe7f6UPXDe>0@i<9;IT7`h7eMMS?eLN9zqE~4uR>6fj;^bOOU;K!IvzOxMmWXn1uXDY{Eb;^2$ZN zB&sPNg|HVlP73G{zZ2Ly#m-`iL8%x-XEB)L7O-!Lar{E;Ur2PoLyn%6v0lsrXIyif z^f-~jVbY83B~SbhlM^`;c?@#0(qvJdh)bvNrsy(Cr%F#{iki-z&J^X#_GOZON`6dH z{&N263{i8X=ZgFuHx#54q)ny)CWF<34H#qyFGQC?&Xb&XZo1*H}p4Qlm_Vr88AtsAkpD1 zW}c@yk6CxA)>1tNb1o?50&`_J0U9Sjhr!Vddd*5bKLl6Lt=Wx z|6Qf*o$Z#?PW8SlJR5Se8o`)XIpCH9Odt;w@_@(SIEQ4{i+lJP$$mx(q*c>H zOCx5T%DEzpXM-oZlKx>vUp-&Z-M~V9=k*)sC{)oq=5U>G2a-;PF$d5?7;8bNHk#=XcTtY%a#-J=S`uRtb@UDkV~_8#dFE0H%nb;NHjaVJwL zUMj^WQtx|@yuJO&>6^4c=@p2~KKkGYuRw$hebPo{tf{`)pPeaQ6myCa&?o_IGG20k+=M6OS6 zdu6bO;y1jwZW&^ii78ZubjpwgL&Ivsu0}kDhSf-?8d)%aJBWP;@fiN{0I?q+9>ZTc z5xW!d7%F!nola!I0CrI99h7LFvOSTL>8}uZhYp+75LyjO$)q4|Dup{Eei4mX%aKhv za$)FRgG_3W9YfV-#A!w<3{@W?&LgD4P_+vU>q4Fk&5|ij^1%H?>mci}t_AIPe>=_J zYAIHQZ@kbB*wYL-VkX zX?h7A59uYvP1;SBBwm>80&ds9<`DzODfV$niKL_{eY-69d&(ZsZ5^W24^dhqdnGv` z?JD9wqrgH^kX#DVBN1Qj#N_>I28mB;AvNGs1FocRpQ3$Vn~ihp>_1U4t^qSrf}Nm$ zln8+9c7Sciz*Ru#O~=YvgMM|=S8L+u_W!3Y1{%e#OURiVy&Dw&bq8*y1r2UN){F`I z1X(^owu}jyN|~lo7R0m2ktZE~nLh);qV9v@eX)J^2ax>$>WsCq7i4?IfF#6DLW;z6 zSVl6EPDUyWU?-B^DF);sb}mvRQxjLoL+rc(;LD<#JqPtu-qB07jg*Zj0mS4OsWBoa z!DN_J81v9I14(5Jh{5IAh@Fk(7!4I7b|I1@>ksB=LaZjlVc4!6vDy)b*p4_J?KI5a z!(f-`1hvkACGD23^$Y#VzCJb{y8pzN|L@rFw?u5lQ9r})^e#?MP0ySh1z<8pJ%)MP zdmRj~58U?R@?LQ4g>j7GN({rbev9N`+w6zW>$Zr!&6O7~dI;@7;{=Y{vIrjPE^+ z?|qE#{R7_=juMy8sOlBnIxD{$pV92WxnVkF;8XDZ_#!M74nQ_!0v5V=pbjf@;Co*9 z{G}Ukw>Cr6CQwr5si>-{YiMePEm`V1)n}UbxpMicwQE+a-w^(19IW2B3)aQpy*2B90LIp4tpET3 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/mdns_pcb.c.A1C59CE9F92A2EB9.idx b/firmware/controller/.cache/clangd/index/mdns_pcb.c.A1C59CE9F92A2EB9.idx new file mode 100644 index 0000000000000000000000000000000000000000..5037f75385037912e3bb18f4e682974a06a957f1 GIT binary patch literal 11792 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 W^@NwFn~$&4$)G^LbKd?q2JnAjNd)=; literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/pid.h.21295E2653B28799.idx b/firmware/controller/.cache/clangd/index/pid.h.21295E2653B28799.idx new file mode 100644 index 0000000000000000000000000000000000000000..fabcf17d94d1493c7d8510711f981b2bc6241171 GIT binary patch literal 2870 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 AL;wH) literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_alarm.h.398A6B39659208EF.idx b/firmware/controller/.cache/clangd/index/port_alarm.h.398A6B39659208EF.idx new file mode 100644 index 0000000000000000000000000000000000000000..62626c5e52a27f38967cc6c59dfbc178c14e0e16 GIT binary patch literal 792 zcmWIYbaNA6W?*nm@vO*AElFfyU|iJ~HhQHM@E7V%b;#J%~PM`k##mep1zr}{)N&NB7$Xa{k6Se(!1&D{n%w zF$!wq7yRDV=cC7RX2T)jUo$2=IJ$AE;dg_bwH*zU8K3^MEFX- ziBDcMeSIvrp5uplV%@v{$=`aG{S}*7^Z5Lpx})1?AK#}V-?`_c#?!|;3mjwE)Z>CL zybjWo{A+(1+VEW$ZNlY0Jyaz{$YC!NGJ-LEFS|a zHv@+tmmn{gVBi2NV3=>JAlta@o-ZE*&;BB|EC_9Wkn^9E|m_!!=`t~t=`v+N$*b`>Yqye&f6 zYR%b2TcXrv2y?I6p?P>l&dn-ii-zw?uO@|h?AelaCac<~DTY0Z<=?3gz03A_@?T~O zMi$DTkqjIiMH-oeMf36vJ#7mx%KFzKSywBlnxhroQn$OUmd#BB`f zHD^8j-<${p3bS*tOF~5%U|s?WgZu+?qux<%sb$aJO=99<=40XGf%%MwS&&7L2c`!m z3e*eoEKDzq1`2~b39||2JD@PgYcN$XUjc zMM*d5hiRxpp?{)K%uD)1jTlNGLK2pF$p|YbtnZwBu>D7sN zL({{qXT2YNl^u0C1-Y}no_K#pkoEk7Z%N;qCK?iNw_lkuZwy>4ZZ4V&v#p?#u4uWN z)4nv{G<)RMw9>_9>Rx$fH?GaS(`kAYiY;S(As*>?p2iiMd!)0kKOxqh$2qR0L>%;O ztcO&-Q2YP5GT!XAX5Q*eEwDJ;Uy7S&m!#FJjl=V6$EM>xuX~e!eWLlv^yNW`g?bn0 zykIZ0N)YO6EQo9~9gf0?QXodB(uIqF7_ujMx(83^MUN}@DiDhh6GnxpML;sKDq#Y7 zcla(8=}*BZnDiS}Mzsjg|NQ-ph4E2k2}UwfHmD405uhBOKcBwbvep)a&Z4A1jXU{34Lk)@M` zKRkri$LJGbXAmPILTn%cc@bI&3uwrR&_YPi3`-DN2nT4mn$SWhAR_NwpG>>Exk$4N zM{_J(MaHFh7J67lm!LDjznaXvPoD)dL!bfje?e{u6GOwbDKHDMFvgex7D6i-MaUN> lbl7T2g>I+gj5C6q6w}aub!gAf>q06>ilke@ zLhQ0a7a3M!!wzeoO-tmqwkcVy!eT@{`=7&yo~QSj=lw4K_kaKUp182!;C2IqW-kg` zm7QLg%0dWH_`jeqKWn`%Lb2lzTD9w3{G8}sFO$ov(`lQYqp!HLSjl}sNdfUEsUO#$ z=qc{=Rh~bvFk@75Z1kC5RN$V37g1!a*!r~mY5zI3ZFg{viy2>(^Z9T{Qn{jfZ0FIO zoPwfP!gccWXl3>6%vthN8PQ!6*Sz*&S#;gF{dA-MN8-T?i`7lRiTV%4@f+qh^jnI& z>thXk<0G0EuzxuIC)p9~RG6!6+_}{-T~Q=2WDh6Xd2coe-ss1A&@S6sc93gtb9l+d z##{Eecb7YNbySf}N3?!5yh@_&>_{HRd2ZF2-l8DU{qBrW`FJ1GtcTU8d#3ih;*TWt z%4K;k{gT^CS4)iyWaj5rx5bMN9IqcR6zP3(-{Oe3XfKbWR4dC<|zjH)1|j(WRK48TORe;Z9_?B%+y5Nm1ok-A9nO^ zAHAeWRpea_Z%vsm%TR>5H4`of!#Ma|9=Z-?b>i>v=Ak>zm`6pEtiQyZD)G&#PTMYn|GXhJ5Ve z>|R!m6`yI@=QJ(m*KWnL!>MCC4mM>T-Q`iERT9ySP2o#zDzt4Yi}ubM7z`PRyII_+ z?x`z&$&=slYd%RN{v4U8`gey%-tM8u!K7BUN3E-j6Mk7)GUtg!Ox=L2K%SjeiFFv- zKa@A3I;BG*dmTi%@!a?fAS_+_FTrEoiX~m+bd8W6)~~=!;K%@)h|qXrjt(_$Szl4q zjeBie5>3MOPCO?815mHbaj%@Y=FxP0x`OgD_Of9B88Y4J`s4)-n%WnEBobI6N@6Op zVgTxM(`E-Ln@6Q2GQsVgdCpc0Kscn;IMl|?Ad!QtaQjHzNOK0>x3}*9U9Yp6y_7_D zxZa)TK8XRSx0^Q@!?%u+lgJi_U3soH3_$p|)1F1cB2_U5O~CCZ>rOUffW8af51!s< zM1ED)@bOL(8R6+grXouQp#Is=Sm{9AT^u&VVJSz-V*tW&W9gzz&`d{e$X1N3nEB>gnyx#a$kA?+;^ z@o{|wCxXiW)Grsbbfz^0Xp9m5Hc|F0dkY5Wdh-cvTwlLd6D^D2Ob0T0X*h_=FcvwD{em> zA(1H_a4LJM5d%<<^3xX=gks%+Ms?V4w8?1ay*4pT3-c|DK#`cH#hgDITFULQ>1B=LkP2uf-%bpd4)RK%o+DWEj) ziyr%pPu?C%P;M+Yb9hC(HEVxQXzn*XWc#(g`CIXrW(H>O*5h%`)pB3|qf-1@t}Isq zE&~JNY}#zFW0(fhw8?;m%VC-}7tnAtrfE|F4T``tZ6;V4+zr#Tg&-T;gK642kPV*E zWxw&tyA4QYO2S};;2T^^n+9sxKMh|NSB&*zNeI{iE_`|k%7x`ZKn;EB~~-l!E+)1$cYK>46M8i++u=aQec9C1E>H9 zVmoTDnYc@YvoY`h^$YO}DS!#Eeg+1hesQIYdj~BPlKB_}fcixRMP3dEVjMZpBZf?VkgcdMUFOL-Z9(Z|it z$FBe;5GJh2uZ`u^k=ww>zyWlDFqbedn1H$9OY6sd50A{+&&vReb8cQfUKucf(EoVv zl!@0|qM!0H@B{UW2#QF637Gz()U@IbVBne99^txC870EV!!2SVVh7U&3t&brNWemZ zjR6*{Kw(gr!h-Q-?7Y4YvMr%ZKtotW_+WtVNfu` zRKY@+i3<|4P~|+_{L1`>u)u5oapYx4;P)1gz1kv1NcL)p8o~^QMFr3TP&~lwojUEs zX-%X1i-5u$930XxVVKcCdqI%`6XxL-FcENrg$gWgfOIsDwrPCwklV<+zPh+FH)%f5Wv09<3p6Z#O7bzVvNJGi zaBGNy2?h?J91tAbRK&ZUdy6_70~-SavkbqCESO;61j{lqv;N>qe(3nWCWw!L5vX65 zOO_W*!1TWoof>f5)nz|7NPt>>wyH!`9NWi+hNXy(LiC4n_;S9js*&XTnbYKb0kn0 v7{GXk0w!h$){6am=569;WMTjSP_UG6 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_filestore.h.2CDC67F6137FFA29.idx b/firmware/controller/.cache/clangd/index/port_filestore.h.2CDC67F6137FFA29.idx new file mode 100644 index 0000000000000000000000000000000000000000..c5dd780681c4eef88fa02b4388685f1c69ee708f GIT binary patch literal 1192 zcmWIYbaPw4!oc91;#rZKT9OE4GKc_iaY<3;X+{QyW6TT;6?2XTpU%5&AaLbo{Ko0` zf`s2*wAI{to2$}f!|X%mjf&x7iCrqww6)(|FV9?_$G4MRfTO5vhp}^4sBn+`T5jtH za*Y$*3HG0DE+JiW<@+mWARu5Ho z9ChePO6}!4_qMP8x-0ix{q^lp7EY_qo0i{+uZLLT(u@GwQ@bhu~EBQ(qGW(?H`XB`ws+*s6W3eDdcKBYtoAb zdEfP(pFFO=S(@5e;HS#8w_@g7g9SH^@)&k>XM8Q>yQ+EB`R{A@>hOH+%^6z4&S@v! z@o$TftXt>R(i*hN@1!;7iE)5nv^s~Cza zbCdo7hAP=C)oKISh*QEmARC8!2|;bP!0$lEDh)6y%B58$G{7eRuNPY1rso7 zd8eRt>Eg;hybMeX44evr3UXkAL6U)ik%gI&lT||O#SnPQHDKFWtuljX>h)gt&F)j=i%H*NkNUg^9+LA+SX@mReZ;??ON@- zZ>w#Zwa?~#-q>?gexZ2VgqSzBP1QBA)0vlx3w)KE9lCqT53!U+=Ata!>ZcQg;;Pp- zTfJM_n%r@6YL1WKtOIv%7FGUaN)ohsWwccDr^Z_2Le2lmJscB$`kk{%new~he8!&F zYEhZqJrksiE*B_$y>UHD>?iB}Z%@VVPS|ufW3OO|SByNHtD#U`q275Jk+brjUwn@$ zuFOrc0fxvy*V)GznhK=(7zDT(*mVVU#lQpu2QbutKt5JW`r*#E%G?Z`3=Hf_OiIFF z0>lFXMt0VtGA(~@G{1FYW8eZRP~lgR0}}`Zwn~n+)6AA7@G&p}El}lB{|2;$HU4ejV_*jA))Ukd0~0Xmw?`TCU#Th2;b!0g z+OE%}FAOFSwx3$*$?kIQ?;Jh`4xoN@L3L3u0n>l)O~Fx9Xa0Sben1Ew108lW&J5@4zYOPi+3u9Ua{6qe+Wl!U3~TJzzo&8&aVSb5kL zSQL0*F5qETWKraSMGDYkN;+Cd9#huUM)H`lo;J*hFf~A1Kv4{{9A*$u7!*b*N=B+=V}D4v?}-dv-$C!<;#~%e!S{aOXQbo zpN#0Fjf`_5E}B0yxjy;vu2)K@UagxTRv{kTblJ!Ee~roY{m;^0P29lV(;!#kyhgR* zoZx*S9Xn3P>gkUD=Idu4{rjx4aQ8gjt)D{I*A#S2ShJkZwen|xX>hcc$n(|+#$CBg zr!N>hSuktH0;6MlQXb@NJ0Kx*{m{molg~J-JVHWdwrkAWEpsLz;l{BkKEjpT%Xyyh z1WA6`lHKhvB}XrAbLWfT@WUNFGEpZ^*Z=(8qsPdj)nOZvB_;Z~;!n?uRclshCm46E zwYH3yeyP;|N_qL7B?l*+`u#|Kj_@nZYp=K``rq49T$!6x1q`jbM_Hz%qZ5ZhuN=_0{Iv?fcjOqRV2X#On-;wgbBe`BEftNf-EgCqBB_j@-c7&EzsfC5djl03yM)$h=!%bLBcwiR53}cjnBrIsOz;po(0i`Kea)2c!pfD&U!BjC6rRF4p)D#yb7mEXF VCa&&>f__V$J_usugeFC>5db9-y4e5# literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_input.h.09A927FB6F93BEF1.idx b/firmware/controller/.cache/clangd/index/port_input.h.09A927FB6F93BEF1.idx new file mode 100644 index 0000000000000000000000000000000000000000..9722408a9adc80424cf716b4ad873aac4024ce7e GIT binary patch literal 906 zcmWIYbaQKBW?*nm@vO*AElFfyU|5l8 z&>vu~UAKU8%J(EayzVmOK@gd0;rCm3c zd89VX37M@l{nBLayJG$vpA;9~G5>G)yQeR|zGu@h`RPAHtLtX3el=~%ypXbhOCn2z z7k7l&=Zmj7#ZX+Co8$)cBVVGvn5jFD4mSfI0|S>Rlc+G5VBiFX0Rsag7we19A6L1^ zZP4LoU;`@PW#JV86AY3J30}}`9d~E*zlAi&XOt|=1_$0vuC=3|Ejx0(|E9M4< zCd-H9<_Y1Inv7grtQ@RBX$XJ?GEf*4!Z2Z2&;o@);R+Lm1t?G$6q+#C!2%K}3<^b< zFf0Ip!l2ND83GG7CN4;DLB)BvL|8<`U^c*l4yYFtZZN&D00RnxLJKAgvk@o^3L}^> rEO3CrppbzHGZdxfB!W1_MajkDK$?kb&Zn8}-qTnYGIBzL7OWNk>4@&L literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_kvstore.h.5EAB128A42CCE10A.idx b/firmware/controller/.cache/clangd/index/port_kvstore.h.5EAB128A42CCE10A.idx new file mode 100644 index 0000000000000000000000000000000000000000..4c3229ee37fbf75196aca209cbfd9bda1a307174 GIT binary patch literal 844 zcmWIYbaQiIW?*nm@vO*AElFfyU|o2a`aOHJqqN{__|II&}uHEYGmf{Ybm~XVbbp4-sTi)8fK9f23 zALHdsn*)j~bCd1@efj##b+gMx3t9LW7&#f(1O)}f!2|;bP!0(E-^{(>T`*sckAWR1 z%_GPo0w!S6ivF*z*3^b;@iA}%rTGN;M8E`0`WPqUvRi(omfQ?n3=C{SOhTeyf`Jok zHX|ErV~_NrR{x+7HjoZBE`BZrFo96;*R66@{3J_cr>1py6bAVo>}?qp{r_OG-Ldc5}i%V92el0WJx5b!uYyYjV z(O!M|vQ@4+pU2;J6<^Mn{L~ibWM+E0wI{u}op524Z%0hpj!Q+-oUF1*CW-F|jC;dFn4c+^6{ft2krUMIfdBqiRvc!nqYj0~;lNFtlCXDWT-gJ54 zJ;$8#Y~$mY6;r+|emlv0G*44FxvKc4A*m8-t zH{IG3d2(c5Q%q5V$Mfd)vXbKX_yY6XRN}119*^C8x*NOQ%#6FFJp0T&ri;;=+S@SoVAU z{pXF=H5WTuPbQVeTuAUJ8>>!k!NW`cCEJdj_tdU$1$t=HO(1(Xv4-7!|_g=tPOef zC~ZmdbcGE<{G*3~38~Aq9OA4HabaCFBB1VcBk0q|TWhZBy<6*mRCG%MtpnvEAi6*F zu_Cq`HSfFPFD&Ki2xlabK^)$Rci=~Y-zlVmwAg|iT4_z@Cd$BIDkW}5|N&go~#HE@AB!_y?^cq&96amb{ZuNn#*r{_k^cjsd@CIiQz#LiH^ zh+sK8bISF+A>K(v9Fo%X0epZ;1c?8uBh+}K^6OF-(TgNfUg|9ZQX*t09<8z8P*j>B z-NGR^THl#v`9d#mDmLmUMQuxH0`ez*KPOIyA zy+Qn5%>N=-;ii>Aqg`k#?BFPTIbOr#R242~QV znM5Xo-Nb~K@G{tROdKVSGH4M5W+nL`EKxWXVNqI8)khYAoUA1uWb~28#MHyo8@3G! zQ71u95Q~YEi&Fs9ot5cSF{twIKw7}QyS)aSKy8#3qy{uhLQ@J71Dd9^by7h}P#TRD zWCrUC3Zt|jFNl2@yLHVom)!AonCKWC2m64Dp3!q~KbRO80|z3+#Ev zjqCL$OXL2o{#s}iGSW zDX#bb0*ZsmX%#`@P#n}vX+hdRgQ_VlNE&EJMrlFH&??*-r3DEC4eF(|AYDiTs-?6b zS)e85&n||Ry!(g6M9#=DtRN-|MuFkVFmYy_G0cLAl2Kx~AWT$@3Pb0Zs2Mee@i5Ua z8Voy#i3{U`L35b6GOie!#l*|U%g=J6Iv<@M%t!Z7YcRrA(LL1pnczs^M(9++mjLIeSxEa{8UN8cLlsLF{+IIP#`s7?li%YmO3NNPk3<>;KM_VhUY$;^Dn#D@QzN% L5or2XZF&74B?ZQ3 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_net.h.92B924E7C7938803.idx b/firmware/controller/.cache/clangd/index/port_net.h.92B924E7C7938803.idx new file mode 100644 index 0000000000000000000000000000000000000000..221cf649c0d0e2d5decaf2ee37fb1633a77f24ba GIT binary patch literal 2300 zcmYk54K$Ql7{{+^vdp~Rd50N>G06BB@|9)sF{@KFOGEj{p6x?IgwXL>I@!{WgH&r( zw$iDUqSA&+WhI?N>Z}??*)2-#I+TN2R;jJM@2e;49RE4*|9PJK-22@73k^^z$8`|$ zT^F!9IW{ATjSv#x|MZNs1S1wgaoE^=xHT+j<0BWM+p~FxEURbxvA(+YQ!B(ot9(*J zJn9Iy+F}^3(hlgJh;7bU?(<;wJFTYnf%^teq;>}<<&_>hyDw{lvtCCvxJ8aMKL~2- zbGyXLY`WU}_~FsWy2gC-i4EhM8&8*QtUmhDqO5!9@zfoKpSr-+`j6ZUo#7<4{I9_p zbVL+kGrsJpokH5uRbCQ*H&9x#-;WYGjB1=$>N)Hx%^q%d-65>a*`|Bg!oS~BCD43t zrZ^r_N3F6F=9b<=a;K!y6n`_V7_)yuo%q-HrHz)KzpGQS*62PvtL)tB+}~yE{T-^7 zbZN?q?Z&^N_7v{Z{-J*{_59#eo{754@L+#*gDlZ!tml#OWmVX%<8Dvl94nIaPaD_N zdQIj0%fEO&K2r1RQf~jxXZP3lTq<&^H>uH8D3jYRvReh^>+ad)e-r0ES~{BYpt>w# zFs9;z*l=!7!L!M>6Qa(5w~3jaehp6EucN#EOg*3K`ImVMB(bZ!KvX?Ubxq# z>XD#*v1UI$QNGjJU;mpbC}YdSF^NXq`Cd{X$qfCdH$k*-U9|Mp9cj+c`nq$4o(IzE z%N##&UcD*H_Ib}w8_DZ<)oT*_GW0^nLXE$uw%^mMx8wO7*KH{HX_*#E&rXi^n1xX1 zvRMoC68a=uBt(d^XW1Jtf^_j4LP(36ow#!N{qC1;b4VmMLX;oh&x8?#-yHp?mX75- z95^W>k);-*WI~yQ5ePd5*xWu@xYvbCKT67+Wv2KGv!dfG14v|!m$wvJnlJ+4{7n(|+bdTtA(1f-+X!up8G-Q4-f!RTohnd~NP@%j zx%2fIfpFX&aa6WmLNJL4+}oSut;Yz2AGeoO{8UvK#zS~clquVk&j_=3fes#xOV`Z? z;$~J1iOlfw?p${sBT&EFVQrr{x;Rk}VFoE@eP<~n$QYq7xVpH0p4%>;1+7^rTx5U~ zJXjt)M(6}>X7+R2x2%f%ZcQo=VUj5cTVlk>%nn9E){5(sUf_h;IKhlhAJAH8Enx)0 zk*C_O^8#z?wUHt2?#1&mWn|`ZbV_&|8;MMCwJ+aS%m{?9=-0uhwLO5qO21BSFX3_CD4Uz%~s7WDi9g*)t|lhtI-iW-CZIGeP;X zeMz_?f>NSL~8X>S@BiLq``Q+!nxiPdnWKxaK>&$WH!+vllwvr2- z@RyHrF?_)tE_9iT=HH&#I~W1;pcgA&Wv>^4t*HF*JsPNC4dxylCfn3JZiuT*V1gk s1|S*RG*e)swAiF5`XfCpCY`y|&sT^L`8kV4wKm@icB(PxW|2JfA9GTQ^#A|> literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_supervisor.h.5FDAC2A543564004.idx b/firmware/controller/.cache/clangd/index/port_supervisor.h.5FDAC2A543564004.idx new file mode 100644 index 0000000000000000000000000000000000000000..79f0c8263a8764df67ab46188c89b9c88afbbdfb GIT binary patch literal 1922 zcmYk4drVVT9LEpk^6c%s!fk6?u<|HSOc9+M23kNNI+!AgZx%rq6(l}pi&D@TMbS*$ z1l__gMgt=L0c=KGS7lOBwz-k1MHwm+p&-++0J;Um=gv9$3v1HvZ9ez+J?Ea=Pg?T) z`NbXxCFUesHW~}E7=(}%f6WD^_2VuG88NUN`)O&)=I$A6XKG*8eo5Zo70&O+1sB_t zB{|8Hy6-s_8!yOflH5bWhU`;2YsYUFZ|1ni3*$O=9CDsKcP(z@&)!xK)v{+_)n&(h ztQ)FH?<%`v%1C{(!L)nDKlQe0UCUQbHh$1Df9jKzsyFGG_t^Kj z264a%PoLhy&m&6J>rcD-7d%uQ_SpW*yt|{m=VtcmJiYdwb&Pp&9amZ%Q9pM?9qWIt z_Orb5`g*7Bdee)>DC<~arn$P%$|aP>*Elkg^UwG+Mtu{SoK~bcVFSF36}&*_sc4={>F%QkiC%c7oD-nvlOR&D)_m9)(9^8IYM z=aktp?69Yf2+|Wv_}YtO!$w|H_^{L(F;Xh~yL`qM@<%n+yL&VpFBJBVw%0WW6$=AR zMdEeB%gtr8oO{?kxovsPjrUgguTfe`>PLqS)fIEMuDqs~6ln(SYU60h>hOd6)P|$m zq&GbBLh5hq@Ms^ZJMuJkrP^cC<4x&Knd@15)&BUB?xb)17TO-kmY&=d{NVP@QEg{b zkL#JVH*DyteoL4$vT60_KFD0OF}qEKP+yM{Nxzo;HRK@b7Ap|qQN*ZK9LI*DO2*sZ1*|8krCnF)=J6=KqHmL3? zOj&Ttp28z6vk=CFDQS>COZ(Ns8y729ty#+>cRZkFv`QM}Jctgs{B^6f`|?mekKFJ; zAQLE~K~@N+19i{r))z;oY~WBJeu4yXf)5RNg5AztTJfCYpYq5X_k)-q84c18qW#LT zk)fIIH5P~o62e8{0uA~E2;rC9Fst9&Ew}Rsht&8T>f}0q8n8iv&8R3k)mO`*N%(e* zBt}jHZeQ9y9yFL2a@rlq@$zweTriD^<=q-Wx2$-y=?5`V;DJO@qJqZ6K=?qOVaf1H z8;7KLgJ?;#Km#@qWp2G{l$dNh!o~<8Oo*HYIgN}S@Vrp>pggI#okJSjpDvj$r2&04 zY8|Q1s<}S3<8ab{h3#%n%nM|IXL8p`;n4JDGlhi)HJK+)}MLKp{e+`UZR)M zfZHbHI`i89x$NTo)?w-&LSQh)MBjH5fwveFX#s<;7!z3mgQXY~MFE4G7!xt!p8zK@ zCPKnAn20ga5OCMk-t@|OuX8X~d8*WaK}(E@lYl`+jERY`B}j-dF%YIfJ(2fY<=$~t z&@nn0Xa`r^CGJ5Nw8NM<2N-t5n79TQB*T~(1{l=Bn0N&kl){+!1Q(6b^of}sTj+bKJ$+F2AWh`si+o`WA)kq#{{dkO7zzLY literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_system.h.AE99EF9BA20AE698.idx b/firmware/controller/.cache/clangd/index/port_system.h.AE99EF9BA20AE698.idx new file mode 100644 index 0000000000000000000000000000000000000000..f9ec6020c59191c49dd01db289972747217996e2 GIT binary patch literal 3894 zcmYk72|U#K8^^!KC^Ke$;}~Nc7t^I=rWF$ zP!px(SnQ^gHga^Rq%HL)TBVl#{(kl3Kd+aM@p+!-`#j(8_xF4AS>Wbo$wO$~ngyF8 zLSloN2q8ZHiHY6t!%6(dTNXl_jyL#AqB>_Pzi6*cFs*#R)RvUpds~}tf8c`Rp{AW= zRu?~0C(y;z$Jp%3qlVF8kXkNh-<(kr9~#)|bO&GvONu9y)Y=p?$+IQV&k{pLSS_I^ASZX38}e!_19 zAGU2wB9rO{o^5r!wL3oPd{%hQ?4_diN4yJJ&1oy&iE~~Y?priH`{1LFTPAITN!_tq zO|y8R6;ba6?Gd}pQrmx*Wi4BHrRvVmvhB@Jw%K2Pl`}lyucLF;uV=P;YI!OQ?z2qq zz7ra|S~`5Puvy1+P`0YlOgOy1;f&xfhMMTi{fvy2`lp5(eg7^%#z$8kn|SrQNzC^) zuM5q>!dWvaQ-ABK$m#Um)l_>qHe1g&u5|i8@fr{HjB-SZm1);6_B_2&<#YqE^HrA0A3zwDU0 zf8%}J6?WNrCdCyS9sJZJJ=mq_9&IdrO0qQ=DY;$q%c38=;#U+|{&>Dl_ipsdPd-~B zwU^}SUlGaQJK=4;Pkb&(d9#JfgISTJv$FI-?uzH_QR}0t8;8C4PB9g(34ZzqJT2pb z|NL&tnwyzNYF=F}MQ_xuaXSw4U9V-is@87Y?Or_VAL9K)xzUZACwF9Z?{=)STl;2b z*zZe6t#=gbq`fOm6`AYy{VXwGQ)=*2*t^QzgW{Rb-(Q?VbfrC%rHMaUEezl95^CCW zrzAbulC|z!jhar}@he?vM?1n3?_NME)umlmXmS%yDxS{2s5kEXH-G=+i4d8~Qrv_N>`uXep=iPN||L?A?} zG1UaLAPhfbE|en`KBw&2TQ1WkcnJ3*MNAQ&mhl9gAUQNINUBC48{B`n%5ots(7-!0 zvwcV}M8rcJT(8bl*PsPyBBV|?Xp8=kB7Z?$UkRDv1b2qJDlJrkGD3I)i<$+mCPxJu zC?Rv4pv%zZ(?U-`Cp6fl%Y62`%_NW>p1@DVPe=<)(A(VB9Gv{pooj&LXbm|o;UP9o z;4}Few2U9ng6NDRJWXLJfo9|*Grog2*;iH=u~r!kx3vi4$o)J7t#XZv+GK{ zdd{n76G$J2ty$K}v_QCLVT|{bKc1iCA^Z=U)M9FB&_W%KDt$TTb}g>)IeW_qWQglM zxt?0IKt1QRtBs~v!c_uk;qXH4LLn^>9x0Ip3|<;&R6-Vb`9+LHsH_6m>L?ij6WQimLJ>SvdJd|a>Q2~ zq!Oe{3pBVHIcZL2UC&b;I0a~;`gAE~TV_xG22M8+1rpXDn2IRyxg-x2KSb&(=rf>;1a1z@TB*6wcVw*xE ztPNDeHibmU1{N~dUl{n+=a2!D0%pK&fQRzzFC6@qWH8xa);AiEiXK{ zOaWxXHibo)0*Huh3Ws3-y)5TIzbMg<$szriegv$Ehhqj1FdT;rWCjv|JPsMe3?jZg zgkMpLcSms)*5#1K7RF96IN%gFr&tO}up+i8g2D~}p4g_y3E2Qm+=(J4*tnCju>o{~ z-*Q@CfcZ5GaEX_sSP8WNNo-SugaH7K*rvz`*#JdsQ$z$CV2EvsgkS>%u}u*WY=9rO zDe{30(8HUfhzB-@G*LD&giQhXa4ki>Z%JQ)hi!^>U<2szk`(2@#+PXrXah$Nu*0d#sD!~OA+k< zYL$)jpq5?Kf6Fm*^aXx+B}@qirsI$vOb-sc2OQFq>B)hk%~kPh1v=D-HN;f+yn0~^17#(p;N`oW8bYbn6NTw7-w zmMF`fea1FNk@M~T(T0%lU}~Y54Xb0OQeVism#iLsia(($uOre$x|H?hBZ4mo>5PB< E4|o#3r2qf` literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_tc.h.77F0CC7425708CC7.idx b/firmware/controller/.cache/clangd/index/port_tc.h.77F0CC7425708CC7.idx new file mode 100644 index 0000000000000000000000000000000000000000..4920f029263b67bd542dfaad48b3a3cd3aba14cf GIT binary patch literal 926 zcmWIYbaR`=%)sEB;#rZKT9U}Zz`!5^#Kk2=nfrkBcSZ(=iaAFo`sOhkao`!{^Kls!@H`dZ#!`N z`jDuh`o%ly)QWlE-%Jx*XTo@3`9!HVo~gQ4yEk!2)?Db!dcEYsxjw6$R^{-{+;wXY z9ojXwTgb+tS7F;)bH&2l?#201lUQHRc-hwwQvNzv@AuZpw$HyY-%Ts)k*`aom23B3D6Y&+k_Gx;ugF`$vjU5S`51V)7&t@(MMS{_ z0|(HDK%lmSpR?%USrtA8W}vjNps*B}fJyJ^(q57oaYd7xfro*CgO7<%7)&s5f;BU8 zu$Hb%p6MUBH;<2j1*l(;OOO{#!1Q+u-^uNte0dr-12<5=D3hoFm_X=1(9IklKK02# zHjog90Kb4Vm_R7_=f2DERm-E(d>_?yy9R2W`VXCpWVCn->&d6@ByW{dAWtb z1WfveyX7Vhr-|SA7?^<4JiI)DU;-x1P?VZh90v>~PXPv1krqcrMlKFk4puR+?|=j> z@PNXgkb^nacG|DQ0q;$u7Qw<+0&=B|uro|AEJ}c?LGc0eDohtp7!(UIVVFaJ!l1B+3B!UNC=3d7m@q83fx@7$ ph6xv?<|KmH#YM@*${+?ih=2e#7y}snF0s=8AN{-T#>B?X001k{{onuq literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/port_update.h.07DD0CA770131E6B.idx b/firmware/controller/.cache/clangd/index/port_update.h.07DD0CA770131E6B.idx new file mode 100644 index 0000000000000000000000000000000000000000..7ef25e06ef41ef1e15ad2ecb4afe3ac0ef95d9e2 GIT binary patch literal 2744 zcmYk83sh4_8pnrlK}jw*CgcVJ5|C&BCCJ-Wc^OO;qLz|kLBR-$R0KpHAEAgCKC8Irq<%86;mD42om)^$Y*TZud5bxrBysSgq0(b>izQAG z`S)wTI%rs)`gGQ<^bwyj!{Wu>sZM>P1?xBY){oWBy?)TYd|jKVKj`K9pt8`6nQx_< zcGiyy|K0iQj(+Py`TIhLH_gvCd+A*D@%4M|+K;fuRo~Vf=l>Ryzl^tcUw3Wm)W>P} z)U|g;Hrc$fcckomi}gz{)sybrzblF_<($znhn)xV`Dd1hzx#(*L_*ZAk*3+7?$21g zS?1^X@q~tpKQ{DPA3IQ1QhEzzqUq{)kg#UbJSVX|#qH+%o1T5{R@G0(5Ael9I# z*M1*9CN$1^U?>_q_r&0Q#f{e;6x6ZFY18@2TMK0ObVc2Xo^B2`)zPc+=3ml%8PJ~I z)6iJfRAp^AapULvQ-7TwKibUaC;V`9`HdxqS^{qUuWW+yw*tH1%&MY&tM^?Pn(eux zS5sFYFA6(ju-ToRmy!I9B|-(gCEIh)CRs6PngwEgSzkvYXd*t@_|s@>%r0z~asZ^1SL6dO4L1b_x0{P1o`j)YmgN_Um;;>$%mlA<+sQMFTNzfS&0dmIW z!+GHnB3yY)DsJz+hQc9p=%eWjvc>rtkw!`c@~51RpI0+4*o#4)IIIcnZIaky z;oiJp26^Dk39${45P`6v^`do5MR62^6gYc@XvK6Q5OztfFlsZuTf`tK4o8Y2?TJA6 z(43-eCm*%GYt41bs)cG7B4lex@3!VnPo>AP1RN2_BIA2sx=jA8t&Ls7ASG_k zpY?Yo0uu!08yCrfjA=rIuPWA)^0Xr|p0AN53d(Y?ZxAAUb+OZ^X(A%y1A0v>E_`vM zrGP;$c!B^nz=a4*&{jS#_E+Xqi2ym_{A6CTgvj^`tlZJTX`i|6XOJ7-fh19qj0ogA z?};c+-SNi(2HE5CVYXrRL?ApfYHC+_yQxBeq(r!fEg&a< z?Wo7FXTtn`A;K84UX&M0gd1Q(26V3T8}qd`n;9g+^GEO_Y>B`GSt+ZtjejB7g8zzK z4qN;Ia8QGRANE1+Cc3bzy+Yiqd8)s2*8SM zj#RJ#QcM0TIA0g1S-nCZ2stzxKO;T_9vNVZD{;(1C72)E9IIdhrr72fh2{ZIY;$bF zXuuNN9Ft)8C;oHyu=BQ98=4KE0vI^@G^?RB4D^GyfzmS22F*rNkqp$MSskTg;LzaJ zQF`VttE1u>SRKt8C<6nVNV7>)5(B6}JGeXUN`SS%U4U)wA^;n10Bm#kgAL?ko5LP# z;2ql>>|n!EvCSb4Hqec24sEcZQEYQSL%+Z>wmFbtG%(ELzhd{bPqV(1uLRybz%kC{ zAckDnE^Kq~!c4#|F3CX)qw)4Db#KDBMCmXD8%LNf!73m}f`a(qKwi<9zyNxQ4f*p^R}JAw_o yW1E8=dd^BoPvVATXRXYhiGO+Q`(ZsBM&ssnSc>f=c@Lh?jO=Pr>?us`e4!Ls@qz@A_=1J?Z}=!w+i$%4*E%)WYwKH+w`ojpXYBG`1IZZ z)yNOsF3I=$6Bjd-67IhGJ4>i=(DI`|?6WxA9|?}tvledh!@7@yDCT;ugFioZ9oDjX zQ>Z_BW9-_?scrFDjp@fXeQ#d=B-t=|pzmFLfb!U*!H-8O$$DdTb4hb6%HsYc&5hRB z_Gii|QmJEoeUg8envOl18Z7(yb7*oVaa(LvuYrGn;*p2LZga~&z8%(gx+y7EKH4!h z*go`$J)1b$?4qAFe2uN}$S>- zJEnCqkrboSwHgeo)gC|EZdZv4nmxI5$Xsf3HQ&}zyL?#Qc&)*x0XLCebLJdiq(tGX*P5Mh~PpM?}b&n1V zd}N7MpJ8?u=0_5qh%Gq$$wyTf=XY!mo->iN}Ai@jk0-MVd#s4vK!7bs6&;JNI>uR*G`jhJfO;0lGw`aqIh;Zn9tQcIuqvm`~G4 zUDBnmzY=|Q-4z4hIU(}`HC zO+g(A0*q6!{msKW%=UY%)ck62q`GRvcx?Oe-jct&_Kq?q>$uw6a;F}&4oAC{uKrSc zap&~!jjc)3R^O-dcW5v#ne0dsO?oxm`s3`Q@wX43N4?+B*!ZK}u9PVIheosQi`y40 zB19tMtgo)3KY!h0tK|ig+nQ%y}i*HsefiUVCIl+dd;&a_8tUV?_Ycs!}?p zJt%SZpv_Su<*vZM!-M3EHszezRCPmVkyhn;gC?<6#Sc2W6SR`|AddE;9$v$a$KCE7 zD6o_8oLXz-T{7C$n@8}Kx1Vr!XT%&DP9JXi(sM24?vZ5okJA!mlF!2BAH5@=xjwX? zaX0qkmfFX$^!tUYV!qt6Jl9~7X7Fx}nP|slovVJt&+EqI+Jb%QmyVSC9F|#;aqD`> zHB+0=#tf#e-;x)Ub(YB@S>bNxUsMc?Nm)yJ+kCDLCGESZcRRmJfu800r0r$=3%5h+ z2Qo9StctJs{;@AtuX^z1i}%`3YP&s37gc1@_s9pGVM;!CcY4v3`Gz$Xzv0G8x@oXb zU7tXMR(HgRn|G+E)DKq!D}S+)jsBy8Thpi4tKYli(+@_J|L~tvq~3q<%_^sRpsjgm zT4x9CrPtt<3yg59&iH|l@xKltypHOl!EL8h4PKRncI)jKb==gFAAk8OrQyVT`H1m# zo=GhN=cfE3EDD#l?=%t^c;D=>S}@_P!>JZAvHZf$S=!s$Z^~DJR!s4o9eYIH-}IBY zTw#>-;p7@%;0f#0GYKi<=t^jn>bF(b>C6cG${9x0aVciJBv7AxH~-N?Nsqf~It;?Cj^qLot*Y9F8HM(=*K-l}`@ zuTNHky5$K62>&)1{RrRwx5gpc&;N!GUEFBL3+-!(axD$~;3vE6?TMeu4IhYZ;B<$n zCQo=4gsfW|SM}U^cZtKeT8oL=`F(D8BU+nW^e-s6E6&C=v@CyGW3w!HuL|>@ zNja%RhS0y>BqVblaSIS`0TK@^tPu4o);>Gy0W z?Ia^5^RiS0RaNroP5OvyV z8@Fap<}p!Z-@L{SKMn(?KqnOf6W*#tYvO1Q4D8OY(rNuLDJ_fyAJ5rG0PTK zsI%ecpFzt#V@Alx{&{KVfZ#bGp9hu;E|*7$R3QWgZDpm4XO>3LXEIVc4-6rPXyyS9 znWKpiMFo&lum>|vco6LB-th%R9hgTQ1wuywBM(>#SuzlY2uREa$rx_Shey(h32(oU zk@9(*h_r|@4`AQWuE`aW6)3vqu1;Yd8YUXDA1n@u#fs#T{<(D}R zq!L6^2}>Zu*z&qaZ<4PBCP-RET9aSrJis6_s5r6#2sZ!KZ2L$GT2owZYtQr=Y zh;OUlz@zfl^>Tu!3TF*xJ)CiAKqC$4@ynP8>VQ-oP~-=n2kL=BJy6FnatWfj1WBk3 z+P4VRif=}F9xx-C(V;4oM7x4dCU-r5$h{S)h6S3fz&3?2l)MCFE&(IxWtFdspiob~ zItFx+aWTgMBZ-l^u#{%Lt-LN|yT}lO`7`}j;b28Tz6h8=5B-B0k(iQeoJAy8J)h+&e@l4spoDeKp5vtEqC ztVz}~I0fiL!;?-*PE6eHPKBL1IOVB8DHSkrj9Q>n3z)b(WD$s21Syz@!wTu__>taw!11t4+($lw^$Kz|xoLd_JH z1Hn57EyDqt7_JOg6{=#%$;3P>Gl;@m3XllUfqayVCB`a2mta8?(5NjOesE5pz+8f2 z?yuPdeZocxb^=1+BIEXu(Usr#U4 z0r`DE9cqAwGFCkQJ&4gKvk3B81QjR~W#kZ)atLaW@$9(JwXm#XX_yil37e(RN|fA6 z+{$mZ?623yJlwuy5K%tOm}Y@1eJ7$54fj+kAf^HuG!2E<0%9$o!Kk2&8$jR&AVH&1 z#soknz+(Op&I6NxJPGJH#xzi#2Aa@j^pP9_F^52d4FA`kee|Oxb1)~YB&_hWXd7`G zzXPfd&{g#V)aPL|dD>E(6Cqk5X0WQ@?FFl<|K3u5y}G8EFg!AP!2H70rrpq`&(B(T6SW`OJr;HS4darAE3iib7Wk*r15Qh{xNl4c5KESyzWfY=qFf$P*pKx+i* zkkJ?tknJYpU5AC3O=2@}PhJ9xF9Asi9Q5Ak^Vas~1q{gLaJe|(8kIH1kOU9KH-xsO zT)|)*362)zqhuU0P66hE>lC`F^q6lG2D6v2H^RZfiQ!rh*09P`;XIwzisive&Wm5g z5RP>{c5#GF_rUS(c%i*`8`hf*fZhNY(0r6J2WWGEe;UEi5>OEfw@N4kg?T7(g||#Ih2t>AZ8FGAfsus2Dt+LHv;Ch zyNo*r_hbYyLKAx3DgW%b1nY4h24hLEG;pvj#4U0VHd;EfD6Go&2{vRafn?>cn*qEH zV}s+rH(*ra9FUy@iqIsKkxo!f|1~(oK5d@86ZPgPrrbip0-u=}Vhles>ytfpkN^Ja z1vX!`7Hg4kF)@=i!?%Sc$&!lmpc#la11Xpm^ywBrY5`QpfGd`kh3h#kEweyi7VyJ+ z=l5kv7qd@~V6A8;XNR8-slYfDtbm<|?$-k2TCf5Rt4-CEJN2~PuY}PZvOakk?mEEI zVzF`7a&$N*IMRY`VT&WJ^sFp!q>aAKG8}2ow#S15m;wAdx5(+qu4NTJUtvK`L=uH@ zmR16CC6IuOIA#6Jk&*WkSOa({dhoLWYXEBi?BxUo33`a}7hXr;4)hTVEdJF4EuJ7! zkOZ`wKRZzv{`~`mv8XKm?HDCFR1TiBpjzNbE2`lS%9``*=n%*TIUpB( zHA8ZL?IS43|K$Aoy(uy{A`IP#4~Y|}OGrvd%P?f+eAjPa+t}J!Ew{GU)?sRD=@}X? z)nys!8#r(s*RA#NT(x4QnTaV!-okR3xwntk5)}<~HC06gWhE!))$VRLnd??#U?~ zl&F-F6rCjX^rYw@uUei|o(Eke6!rdh=bv}iwcoXUukZc+-T%Yq=j$bxn_vLAFZD`} zRwjl~0RV>Zn~)eE^*j6%!2lrn_nL)@*j6XQ=89A;3%W?R^s?8v@TC5Eljkoe|4v(a zLXl6CUpZejv)J3$D?9whhAq2QFLH9jaC@eXYWUig&Jf!v$sdP{^COkK>RnZ;V~-eo zdiS&WgZ*K>rB@n5*1P=^QN_+NQsueWdFKroi@Va}+=5%~3@z+%$kS_|eU-A6M_%*vzK2d3qH5xcO>>Ai>!CHnwfj6T48!6=RQsxk{Z1 zaa=MdG+o@zH7D4)r?b!B^|S4-+;%S|Hm6=RP~ao`JFr^N*x`E2d{zm2pYwyVZ9Zl4 zLjHo80`4(|c7eYYS9IY1fui(Mqh%RWX-`T-l3IR%x4Cxu89#cGaiK%>)@I8(g?;h& zNnD?S!TphwRBv9V@6btgDr!t#%fj!3)IKa%)vcd$r1v?_+o8JLm!*@N73oQEviauQ z#hs!3wq@f!*~qWdO_jDc&m9z9G4aeDjBzNK_<~hf8X5cb`Sbe7sn1w{=zR2;)S1@* z_#w+JEA!~<4wZB6c#&ye;yusD^&!k1I=SWF?DA8V>m`+#-zQQyw}+Oj@=|4Py1}@i z5XggGe%T!`%VB4**~_pfo6>E6l+J596JQs8knu9<6=$`RMTSs(xbsl-3A2aSW2U8X z-7>Qp#&NdhH+U#(r#xI0J2vZ7a;sgI?^{FZ;(5Zc2iIMDpFMb>qHas?=e@YQ-KP#8 z36&A%yQ+Qd(Q1JWEy=bt!}rviIrT1crrmnoa5$xHahr|7ox4G^XD>^f-|64ceW5B$ zc5M5^#tg;kgUz#BBogBI`2*=We#(r&u$0IZ-}bBwU;4gkd+n1s2EJSO_9QP6VTTmw zj$Q7PugUq>0mppv25xnT^M@8z&B^C0rzK7I@GVR6dXz6eZ6&;*UBA{XApY1L7Q+g2 zu~}pj+O>97Yss=%GUd~gMZdM4t#i;#Vz(qL?^13q$c||!oi}F4MKBcH(npE+O;V)Z zKW?xwbncYiedd`DyXxe7uG_>#Ka>|R-sJmt1_>I%7I5q`N*i|t%*4Nqna2!Wbl;+d zDUE$Mu<;1qTJ_jJZyGgLHF*0CYuz*FdNafIZT&0fC{=|s?H?P`Q~WZ-H>;biua32g zYfxT#-W$>x^=DMSt4rjabdjyKX1;D=?du7j1nj3xjjjGdlZ663Mv3Uls-2%huV!iY zd|2IJ(p_nNxO``zWL@#etg?%q!iWL4TjHBNPQA6%!?C-+ylS@=me(|X9r$4Ai6>ou z+ND_RQ2B2kd7XXVM*H(M3Bjplj{>G39C*EG`~ zw7%FQ1O-C>-HU9aXTAuU7V^EAUh382oO^!gN5#Guui`2`nwNcz_*^Zl2->@JL(Go` z{gu5jD^%vkH?>Bu`R06X(cP{cTcY>gvfsSK>gDpF5S8L)?yEuD_Jx;Lo~UWM;uo>G ze!-_(mjeO~Yi$E6t`A?M$6TD9mI{WY8W$4QM2Dwp!dW#OJYh#&HU|Sz049B+Fg{T% z2zQ3N1<+;cfGIWaipz-RMK$$}bO^z`SQ zHDFec0S54S+yEDlMSaxP67?QPutO!YK^2qvAWk5-S!gwLw6M9(4~HRwM%9MLgC0wip72i`04a(a2`{uGRG2kvw6#c}O|@nqL@`1J zWIDgDJK=8WE<~?`1_4Y`PXUyK@YkgO#C}A_2tJi>gvi7QCmSbcboo#>n53QtClhYWfJER8Nm!g)Fh)2gfNuV z{302F3uQI6NJfVWvq?xtbb4v;gQ(@E3*c66~DM~WzzZnBj@^G*qd+g zd*o{=udX$Z`#~93+H>ibQ*m$lcXrAXWKZ9HxExR%Sm~c^-aKeuzN+i{p{6UfWlPFj zvqRgX&!1s!J<@rl@)ypc)S1>dlouegLF?u)O?#{5(AQy)+z)k^nf2v+qSNDh3w>AT z%u@ekRxEo8Ddm*8f%hkv6qsdc+r&o{?g&UrsX1euufFT?YJ8?*ap9Lb^-~>rKFZdj zok!wLf9+V(ueWYvgXF-IicJ5m>wa(Un8YpL{*`U+eZOFt$@}|z@5&^3ZNY_jL$s`U z)z`O2GgIf)h5R;U*{;nO>)tr`Wq+)n`x`q~+jUAGOp1>D!R~M|ll!)?!)o&Lw~9Qw zFLkpMJx!C6JlrK0htnU}_HU!FJn&sh(bYu{-1C~`=d#`K3$`B45k_uZ3h%|@xeV#) z^K9qSJ(t%_irSCm@pZ2n*q3$(D3@-~Vb?YPc*(n_c3PeK@6te8sqX6TYqW=s*Ba_O zzg!S=4_}Qbe6GK83nO&var4^fKi#Ynlx=21e%9A2#L>nz-(Ff4yZ*sziz=v=j#O+&gu+` z&g_|5_>|Qb&wd^Dex_@4w)bDROUjG3T1E@q&v4G>R(Z#i_1@TJZXWen`cB=k$9?Ljl}RV+M7%j%UGJg; zl+1!>&Rb_i(pZBf2P}IHb2!`jF3dQVxk2+;6iQuK_>X=`JbtZw1ejD9!hr||D_eprJ*xIEmwPJ-7^oIJ=mkL zdQqKHC5_leKW$ypa%PL#P|%%2`7@8C%g$cRKmKEaqRlj8aQ!s8ioE!{JXPjJq^XETfGaHVDpZRFu zcAaON?>Iewqi5O_+w_I~KDXDclEN>68H^dAYPQ8usEO*#ymEW?S<@zZ*ja!&EXz}37GtCNS zU&*iD;s{}kX2@M_n@w(xg*88)53_6SJ?d8fsLEoqp-{H(N@9HUG5Ym|b-fyAnR?lap1r(WSG$`U#kx-2;%dz#T`PPs0PTN~u~?Kb%L z%|9-sQ(mro@~LgD_wbTN{$oLB@qia?>Gyeh884FH$$FY-?&-hk^Gb4c&D8o@3Vi7` zl=Idt(@I?z4;}IRPEp?Ip?=VeBkI7yG~-i#?;fa8$dc9k8`N!0>$gVD(5T(~U9l{) z!)>|1@sngl;l&|?uhjGS+)V4d%pczs&TGxM723)*;XQKL`6OKK^6Zz=FKvUksoVXF z)9EGqouh8Q7O=M;Ic(zC^?mm_y$69qjPzqW#0k!kHgZet_G$|IjPG-n3s$*e|8&+u zZ+}KtyV!!ewr&G!b*WCJu&t^z%bM5iwdXMXqqOr62l-(hh{w5 z5O>h%Z~kwFgu z;~@}biy2}O##N`Jj19UQ>(|s}5xok>ibn=9Co-9%4O+>ecFOIJAuhzR^&|aqhFo{F zLH#EV21u)2U035+?nr-@Mwb3)gZi7JKirPJSF|3-){XR60Y(+U)RbXLFr`sb$4D%w zapz}zc>M#8Z5SC8#t73JZIF@t^;_B2;o)a-Z0$(@0ALINaki8p6=PibMCF(fam=&g zb++jbaV(t}!(njPqYbh#%#B|e%NhC`#|lSmGy+B=K}5h12r!z)1Z>R4qo2Dh*d2?W z;@FyzK?xcOx}yy;vg*8^U7=h)fMaPyw;jWdJ=&n|N6F8aOMDGJ<5>R4^v^)!GvH@i zYgqF!ni{}L!1qL2Fy*576e=pOAzZHK}!*Q4>NOV9(aG1_WHzpAjK$X6Hz{m$G zy=B0r40w{G%Ykk=5Rm#tK+pVe!L1)hFOESR5=J7YmH%#|y`^3j2X!KQKmmMA}xP|3}j}H0Bud$Fm4c^i9l2 zWU6nfg82G;739ozHbR*>bHrw1Z_!h2z=Jw;9c?6U2lq?#_H>2rL>zKlIi93ljvr?_ ziR{entdJ!f22q3Pq)QRh2s(1f|JBNxSB{@`3BM<^CYzE|Tv#qD$b;oEj#9ov*ls#s z!X=1p0s32j723n8y~#a4i>8YxJgCH#I!J;%*aq-6po4Uq)BNzJ{7Q@n;Lwa?HVJV? zR)FEZrvOUbOSQD1`5P>4DL6a_bj|^@@tp|&0jz(>bad{UN7HPNl&KRrG!8Z+9WDm^ zVjw~b;IJC-tAU6#(+v2{K!o_=uoLh*fe2+8hwlLY9T1K0T{sN*!$5@g$i{=I#@q6f z+zEe90PPdN1R3`JwPaGwi3ET8##(Y8C|ALMp|Yl6CjjpWt}w7;L$W9yCr~C z0)!;#qkwf32$5|ZUI(n}KuD5)3|NnWkR<&9us#4GB3*uBqS?#fx6_E+m{ZN^hzk#T zQN8GBb@zzO?VslP1`wXq0(vbGm4DAXQ}2-K#rnb-1TChRX^*ssLmdMha}v#9&M+dQ z9?Oh1LNdeQ3&4BlDFSpd)?0wa7NAYyDu7i1 zI*JBPoVi#dpp&tG2(X8Mj-0}Ya~&H1bQF90=-#vq&pIQC4a1}I#>r<_6g10wo-ZbR zvSwJT$ituEuOg2S%@7qYqBNsacqdDeZBdADxEe@S1KaU?3~mMNt-yfvVlQCtRmBy6 zQK9PPEg-oCY)P-$flfOxByj;nQb4gqvOjmHa?ZQgx0f0cDKX%Pk)o*3fN5Y%BAyOU zfDjG^CIV{`2~C9KhzccAi3{qZLJJKGeG*AEq$Phv1joS!r^@3{7FbGR2a?<=Ocu}B#k5vnSvaR z91fX)AArUWfI}wW5Re`M&ZLZ*fMXNzAp?99Xxs!G62AeCZ-5Vy5f1Yy8u=6sN&wn- zRKx$gb9gY28mNiL^I#k`P7~4P!Iyyg5@;f&-b}W&Yx|CoK`7IY>NkE0p~-57|9K(t zU^1Sphn(dRdc#)(J>)JA-U9e7poi%5;77pt2y_vB9xR~X1r$9*zqj@A-mKCKIU_q= zm}-H{B6AZA|MU9d!QH@kH?T&*tvHiOi5WAE^+;sGG*LmOOj8xaXYy5$8PiM! z377&EBxDLzkOkjDLard?OI47af!+A!OPp~|a(9#<9L}Q7Vvs1zIm`p)8i(Q3a0ZDI zump7!Wa6DhOVlD!7M7)s0*^yTgIXlo1n8Rp8^s-mn*nVz&?50}z}yY=NL&x7^?*T& zycN(|ffiB)4!Z%p8?ccAarg#E-T+&q5}bHzVxNIJQV9-oDb!pFgT#3hS{_9UVYCi$ zk&L~_(M7`2Rt$tlj&3Gi4fg@#J}@R3^a55dFhdMbgb2ZnJlD~D9{d&HzmmI`2d@Ce z6`+R}+>!S5+7iE;dkOnWfzp|bWg8H*0cW(D0jK@_pC-Wv=tSa-tr}p- zXW)hc2y~uC_Hn&WkG@{$Cjb)}MiYq0j+H4WGEl;j`0LBy&PxOM_hl3YJv^#eVG&t5)fCum*Pfm25d2^10P zXdywOkwOB+hYH6E2@;PL5@>BI94jP9JXS~`(o{HBNRW7}kU;UI!m&aEQKZ7LLIQ<{ z3dafw6i>8?DTekuM&9JeH6pWP#Usir{m`ps zx3b|L4II+wG;LJg;LwrosPZCl=`X!#d<}w?y&rR%R zixBdJxXMN(bzxXG`AKw)|8mB^u(-s9%a;%^ zIX(&3WU{o_+B&*=b3*N8ZtgCwQ#^zsv6;Za%2r}-Z6mex_VEo0oH@(ObE^HM$=`4s zot+#)W(V_(_@*Yt2Kt6vKmTuMOrItXm>V{4nIb$Uc0p7`^uoBv#Y^TdN?g7)BR+w# F;{Sa-DGLAq literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/program_store.h.2EDFE6D17879C6F7.idx b/firmware/controller/.cache/clangd/index/program_store.h.2EDFE6D17879C6F7.idx new file mode 100644 index 0000000000000000000000000000000000000000..1551a23f0ca290ab12cc25c0877e4201409290c1 GIT binary patch literal 1328 zcmWIYbaT^SWngel@vO*AElFfyU|1}WkK9=K^`HbU&nB=6Sw{HX4jr>RbZ14xZP&B=uFYHh-pyi%NQI_f$c@D9zkMa=YD}B9Qmydu z!jrS>T(?d<%y&ojQkO~c>}~J1ZFBgg*7UGx&3!qWS+Vo$B@SnGicK;6dd5ggal{w?iT}Ar*fitBOV<|h>$e=-A){Hd-g4O? z{llx`%(_m_4A^rfY>|ErlUu_^VcW1TKcu8q+VF4f^_}>@`N8$7tJxMY|LR4rKKd;y zyRhPe&)l77F53IQSKCs}?sDo{*qc57&(7_LO^g=iluF4ttutY9*2mjLMT;hF+cACl zJ*nx>FJ<Cenvr7 zudUuiI})6Oco@VX@(hA%+-fQelI%=8U7qK}G=R$fDG=;ysKE48~Enp1Q03WMkwK6y*?wxd-k^MoEb0 zpoYM_1{4N+2`c5`z*K+~tC53~F33^LPX` z88o?I-rj$8Wysf$S2CG-1Qj$D%n=4Cn<_ga4A3&wa)xQKndo@=WTJ8*&>Q?5{0dNa zF|_|U@-igwdkYhfpq9Fp9!xclpbmo$J1mS~<^w$ijs~b^39%dBPRIo=X9IdlQ_>E} zQ=0O2Fg-kiN}5V`2sf!|t2rQfN>|ef$xd}0802Ou+z$D0U*Tegs!|g6+Az={)VVF1rgAjrV03Wu^uK)l5 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/run_index.cpp.D330C06CDA62E5DC.idx b/firmware/controller/.cache/clangd/index/run_index.cpp.D330C06CDA62E5DC.idx new file mode 100644 index 0000000000000000000000000000000000000000..4f5f2b5c67ccfe750001fe03a5af57aec1e3d617 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/safety.cpp.627B6D98AF3BA28C.idx b/firmware/controller/.cache/clangd/index/safety.cpp.627B6D98AF3BA28C.idx new file mode 100644 index 0000000000000000000000000000000000000000..3f6a3cbf7312a91fbbaf3da5ae1707402e056024 GIT binary patch literal 19572 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 E1L4G+LjV8( literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/safety.h.0E0EE6BC58EF2778.idx b/firmware/controller/.cache/clangd/index/safety.h.0E0EE6BC58EF2778.idx new file mode 100644 index 0000000000000000000000000000000000000000..f10ff470350a38ef5f8ee980417bed02756568e9 GIT binary patch literal 10804 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 E09Rp~z5oCK literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/setpoint.cpp.B979F998BBBBEE03.idx b/firmware/controller/.cache/clangd/index/setpoint.cpp.B979F998BBBBEE03.idx new file mode 100644 index 0000000000000000000000000000000000000000..b6165e49f72ffc2c394848b52a93b4b518b4931a GIT binary patch literal 7568 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/setpoint.h.ADEF0AAE1F9E133E.idx b/firmware/controller/.cache/clangd/index/setpoint.h.ADEF0AAE1F9E133E.idx new file mode 100644 index 0000000000000000000000000000000000000000..1d81ee341445c096a979b69ddbb53f00dca93da5 GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/settings.cpp.FE374ADAC354D9D8.idx b/firmware/controller/.cache/clangd/index/settings.cpp.FE374ADAC354D9D8.idx new file mode 100644 index 0000000000000000000000000000000000000000..d77bfbde0593d502dc7761c1fadfde723dc1590f GIT binary patch literal 5880 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/sim.h.D849C2DDB57F058C.idx b/firmware/controller/.cache/clangd/index/sim.h.D849C2DDB57F058C.idx new file mode 100644 index 0000000000000000000000000000000000000000..6ac65410b87d9815dd2507c6ae59b220d1afd062 GIT binary patch literal 9102 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⁣PC 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 ICN+Zaf78*ECjbBd literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/suplink.h.DAAAB212E792F5BB.idx b/firmware/controller/.cache/clangd/index/suplink.h.DAAAB212E792F5BB.idx new file mode 100644 index 0000000000000000000000000000000000000000..a2522a41a4596f8b7f46fa230a41d3db4b6ad5d3 GIT binary patch literal 2474 zcmYLJ2~<sAPUh312 z{AF!@WZ=0C>E>Q~%;F#(r*SZR>Ftrzi#f!P*Ir?+Ic|=n(=E)&;lGEU7F_!#UHF90 zwt69*z7@rt`sm*nX{zJl3cDK~Wt<@H7xGgkd1cQE-5*Y$_v84DXDoi#8k}qDa8xny zPOOonl|T2>;rr!PLHXs{FVkI4o$fy_PY!CC3|RkV;RHj_e2lOzyl+^f7;hg{9h0!T z!zxpbmoJ26I*RXx<{U|xSFGbFCU`7q|7g0iW`3X4=9yGo;A51r$>>1Nj&F8uE4G{q zOZgP$xYMpLJ7 zW(|EZBt~YPH??>@@vYR5(0wY4GTK+H`M8cl^}Z6-vThR3iM!uE^mcKL54-QbRloI4 z)IZ-cwmq8tAhd3a9Q=vFU&RyaMlr#3C`sO8*LfLw8SEUDK+xKtdV;$JpAWuH*n z*|jd}FOzIDKT-ZrlIhu!``Ed3wWdQUyH7SVHq5+>_e%0@ctm|w70)oCaJHBwoHcTi z-kzX2HYdK0dwF2KFz7;#&xz3Cm|dYA{<}N%ofqzQT@9;CEc#ZF*)i&+W7}izJ34DX z(FnVd*5zRwd?PE@zh?OFJeQ^GORoDH(lrpzLk) zIG#5@7W?e8BG>qxi9G>BU()PwyL+rutI~R^z;%1rP{i)iNWBOT^VB?r{*r3-;^WBp zq^NLT6#z1|)ABElry9_Kg9adYOr8!IfCh9ce9m1J>^T0$!kPx`03f_nyclEv(A)6A z2)3xMerjCut#AVR1i2Eb5DMVDm=DJmE5%--rS;n9I3Y{6&pS!6&h zIQObYI;-e^<=gKetu94F?vG@DE1)2h%5{RH;5N z{79WiQg|WPHpjSIAHl&|NSVQ?Gng46qsq*HAzf`1N!gmHh{&p6dzudH;S_F6H$5_- zDY|RY(q^M%KhVJ%m=-VvEHa?9ARHr#E`C=@2kT*4#1t8j0j2#^t1j^^{Hp1I3)AjQ zcRezo^lbFR(4EEU+jQUz(?X_@O$L-s>xrK9xk_~5zzR-ZiBT&t77<7dWC24>OrP9g zcf$P8^##j&IIw|~25eOW#wFydKbKglwg}5gUyQ*`%VE zanv8eq>7i}?_W?vk9t=#)d^moiF3r3vH7@ z+vdN5tLIU8G_+*6dV9$Rw6NCf1@%jRj(iJAZWK2Lat2O>ID~khD7*qtA1;R?FAQ&wW z79q{CjB8cR^tcuOJ4V3?0Y!kLUEqW##S=#za6(8C;%I1`5K%-p+5^%qtV=p-xo+Iv z1ACOm_&9ldpz=UOPxd!TD};S&ChU*RR&xMHiN1f#&;fvtKO9%q0>FxN09gHj@PF@O BL^A*Y literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/tempfilt.cpp.4AF84C324F2390E5.idx b/firmware/controller/.cache/clangd/index/tempfilt.cpp.4AF84C324F2390E5.idx new file mode 100644 index 0000000000000000000000000000000000000000..fedba20cfc5262d05691106212ec8e188446a258 GIT binary patch literal 3972 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/tempfilt.h.2BED8E89BF6BCFA8.idx b/firmware/controller/.cache/clangd/index/tempfilt.h.2BED8E89BF6BCFA8.idx new file mode 100644 index 0000000000000000000000000000000000000000..1c2d653674b4c21b48a9d20cf1e8bbfd5337db7c GIT binary patch literal 2704 zcmYjR3sh5A7QKlf3CYVNkPo~NFp5CJCy-!cftW-)1(c#)SnCLFRTQe_*8=5Zb@*z9 z;y{)bs0Gm)T*|;2M37PI2bF5At6IGR|v z+*sjqAt!6g_P>i)UD3O}=9Tn^&X6$~&^~YC>_m9=f1I}$e!A7+mgWogV{y^M{ts_1 z`zVW_rL=_KGTgT+ibAq0QB6T!ql0}{@A{_mdka5#T`2j0&+Q01{n9<|Mb8$6XTd+0 zkydeYr0E;3tgCkpUv)C9H8Z4FCFoJu#(oh!u&w(#`yYXS9Ut0z;D9t`JSRA2#mexN zmsLlqA9TbmG{0T?Re<-6$X8fg|D8uRF0DTqX)gLT^i;Zzcd%i_C8Qwuv9-Gkei)1> z4yl*gU2N*%{pQAbi&uI~8m)giw$#ACn))b(Er|8;;PqGsjaF?V?@>}`=Iz11^p?E0 z)cQWUDZS<3p`DZ>mQ#0DX=TJA5`+Hq(>}%IoKI$vQlg0a5Bm13=x*gH6THCdX@UVhDTYY_*HKi_Q!~47WK6b;(uLi2d7Vg(JY^~cv z1RGXw*<4apuxNE-;ZG%bT`yUhZ~Sn=Rup+G^lF{=>CEpp?Px3i-1>6TWUhU@S6_{y zKBGHsD1FtM=Ch62PA4Ox!>o-vGD~C5R=wao$r)%92|C66tVR zE7HmsfU;Xcd#0vqcQAoGr_Mu^k!y58ls!k_D8bvGn>djl`BU{9Bnrg(`GWZl41oUV zFT)Af`Uhi0h{WwlB~lFoh(bv0?1c5--ORl_vO6z1B9_ZSolR)ivK% zl1Pa=a22^y3_u5kF1D1R+VCn765<175}AsD*&A#++9})LT=*G*G;lx=H;6*^JoeN9 zJB4kN+t<3)lE@Vw;34o3GXMuH9=w&e?)=xd?2gL{k;0JyD96TzT^KDrbdW@HTvoZL zTp57!mV@Tsh9l0k63833XOfyc8JK-PfmP#A@44omB9TAVFAyvcGXVPg?Y!is2W^)~ zq{U@_fxnyqC~vDB|CIa4_X>f$@b(s|#fyR2?XO&Ctv4t6b&|*z>nRZ>V*vE^!u5Ie zhNACDG#8h>McxtypzKb)>0#=rxk(~FT=o_Dx-bCcmM?B+zNne|kU(k}t)8o&JNxZ^ z{xR--KUK^_5fb6{Boc{+0s475FwZM3J|X@0Ke>=4m~!9rcHlmiV#;;XG2vfX)a?!_g36o~_STEE(;*yc{APf(^PQ#3pr z=mcwNdxI8^#WZbdprJRsC2eM)3CdgQt%i>cZDB2KXjlYZ<_o4gJk1l7j?$@TY)a`> z`hb}aPX#H0G&2sPjFeFgBDe{r>6ic-CWPNO9S=Z5F5uO4C;$z4fE%F00BAhW zD9c(HDE#6r=}3S@kOg>4ItYMm54I|$g_f0-z5tNprCBUyJEk0=jtw>MXn9jI)<~<`e<^G|q+A%lqO_l=vALmdF Ar2qf` literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/types.h.AB46E6CB91327A24.idx b/firmware/controller/.cache/clangd/index/types.h.AB46E6CB91327A24.idx new file mode 100644 index 0000000000000000000000000000000000000000..e431e4b87167cde36389da5540c2c63afc7f6576 GIT binary patch literal 14198 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 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/window.cpp.AA9D9AA70187704C.idx b/firmware/controller/.cache/clangd/index/window.cpp.AA9D9AA70187704C.idx new file mode 100644 index 0000000000000000000000000000000000000000..3ccd8ae5fa3d24ea17facae3b198d14eaf33d480 GIT binary patch literal 5470 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 TM8qeA#-^quu8d0}rbGV+pL^E9 literal 0 HcmV?d00001 diff --git a/firmware/controller/.cache/clangd/index/window.h.5D39ED538CF8C810.idx b/firmware/controller/.cache/clangd/index/window.h.5D39ED538CF8C810.idx new file mode 100644 index 0000000000000000000000000000000000000000..99251fec94e3b797b9c94ebdec7a02ffc2c3dfa1 GIT binary patch literal 3016 zcmYk73s}rqAIDE)rsndWd(AZUPr6Js(R4K^Gu6^)rkcD|gik3jUfPlU!jh0*t83df zg)fWzR6p{U(0;MB**AXuh?lM3%Y7%r9p&|9mCIBf4<@%<7n}E_K3{e9ao($u+J5iY zOSzTJj3o(HIhET(b@Vmj+S~h@$@aykt==u`P1UY#=xUn!0*}-kTf4IL!~5_RvzR)g zfo(Z4|0z(ND)6+A_Mlb14xVB4q%Y@==Ka>0K@%%_J3fD2RHJ6hE1E6*ZeMMAU<9`A zVdIv2m41@MS%*Dj_V$6{^gA9Q4*kcwhBk%g*H+A4D={l|KT_SirTycvYf(cT_h;Lt z?5NzhmyMZq2BTp?eB6u! zNx{>W3qmfc7W+r6qN#imH$CIHn<-3hu8v!Iw7YWhUB=^Sx5_uH@lSbPb+6aBOWw&+ z&Lg#2vleg?;;)_(>da3MOgxwCUbDN98dknrTYkj3*jK$@b8A;mk9L&=R19%S^4{-` z$Z}5cJsaO@?$A8v)6>{?HwzDL=AmDj1@AXT`<`{&E4=Ivj&DB7 zC^Tb*I9l2pWTqFhm3yW$*Hezm+p;T4Z*9Oi{q;2zU2lvogm0F4YM|W>;i4^-6h^CyY5O&>E2mPra#8k|UXLKl{GtHgpa|8%^j~W1_C%)b%aFF8 zZh0y@;~00ZgYr{hi!3o-@_^p@aF-E9-rD7N2RpZEPU#y@;~Ja(7R#_NS)}RS$<8^( z|Na!5|G7+@`==Mvv!Ss{8CTx;u{&z9$xxT@a{diZUDWkn!~0$%4HiGk50yS$xMXqD zV9}J&i|^XjxY(@kRz!w+Rc0oNmG-mJ4@|w_6CSB*v<*w!`(p!1m_4k*F09Ygp6TBC z)XOWwYM`au`o5jfwv>|Pp=BT~V?~VBH~=yu3J&K!7L%BO1pvN>YtNHdz?lMYXQnfkFn|gJ;GdQhU%t}Hj>ZC$pX`v zJXydM=1ZAU3&NoMMU%Nzr(=wLSilLoU6?L7VUSy}HT2oiOK)effEjc<37kZPLGFXw zL*n0v)JZJh0^LHU(3mjDJ(b)t7#U`s!vt)2b`GB-Cd{|qa~mGD`JLUcnF*LMfXCxrcHIr<8-?*V1tgQp=s=a$k2f6uD`v{^>d7p_U|ZUfk|)&4ors$gh4yF z*-$Q?>rq*P0b7)>q$w@*`8+p$zGYx)i^?_rAPY!fzKAIj5(ec<@{?=uk4Y6QAcbx> zrkjW`$Q?tef0@_v_%{sLqw;Es+60JbWV(qdEYC3=R5!g~)U&`unD5GTWfKPFs}8VMs*B#B0HI&X~J;1hum-MY`DTp5jZx;xX*YWXVe%(4_rl$ z4q62<17$rhNJgwcSq}=5QE3CwS2jjRHxDqI- zV}pe&WlF`jFSUc(Wo#;NjgQ73jW9YqtgGh;)$NZspnGYtF_Z+G0_61nYYI@%(asTP zFjo&5%0+xZSq~SI5mivuBZXwN5!eQTp-~+X1f%u%plEa?7)=l}st;?QE!tgkt^`KY zsdO_GjWB|;o*^V7e4wmn2g#^Gur`6qsE%NP(Rxx)G@1n%O~5g#qfSAYKw?y;pLlt5 z`p+Xz*ci^Hvw3KMFq}i@@KD1roJ;5O(7pattern = pattern; + s->step = 0; +} + +kiln_alarm_step_t kiln_alarm_seq_next(kiln_alarm_seq_t *s) +{ + kiln_alarm_step_t out = { false, 0u }; + if (s == nullptr) { + return out; + } + + uint8_t count = 0; + const interval_t *tab = table_for(s->pattern, &count); + if ((tab == nullptr) || (count == 0u)) { + return out; + } + + if (s->step >= count) { + /* Past the end. KILN_ALARM_FAULT cycles, because a fault that stopped + * announcing itself after six seconds would be a fault nobody heard; + * KILN_ALARM_COMPLETE also cycles, and is kept brief by the caller + * stopping it -- the application turns the alarm off after + * hmi.alarm_duration_s, whose default is one burst's worth, so the + * normal case is that the operator hears it once. Both are expressed + * the same way here, and the difference is entirely the five seconds of + * quiet in the completion table. */ + s->step = 0; + } + + out.on = tab[s->step].on; + out.hold_ms = tab[s->step].ms; + s->step = (uint8_t)(s->step + 1u); + return out; +} + +uint32_t kiln_alarm_cycle_ms(kiln_alarm_pattern_t pattern) +{ + uint8_t count = 0; + const interval_t *tab = table_for(pattern, &count); + uint32_t total = 0; + for (uint8_t i = 0; i < count; i++) { + total += tab[i].ms; + } + return total; +} + +uint32_t kiln_alarm_on_ms(kiln_alarm_pattern_t pattern) +{ + uint8_t count = 0; + const interval_t *tab = table_for(pattern, &count); + uint32_t total = 0; + for (uint8_t i = 0; i < count; i++) { + if (tab[i].on) { + total += tab[i].ms; + } + } + return total; +} diff --git a/firmware/controller/components/kiln_core/src/configmodel.cpp b/firmware/controller/components/kiln_core/src/configmodel.cpp index b8594b9..d118fba 100644 --- a/firmware/controller/components/kiln_core/src/configmodel.cpp +++ b/firmware/controller/components/kiln_core/src/configmodel.cpp @@ -118,7 +118,7 @@ const kiln_cfg_item_t k_items[] = { ENUMI("hmi.units", "SWR-HMI-13", units, k_units_names, KILN_UNITS_COUNT, KILN_UNITS_C, 0), ENUMI("hmi.language", "SWR-NFR-23", language, k_lang_names, KILN_LANG_COUNT, KILN_LANG_EN, 0), NUM("hmi.dim_timeout_s", "s", "SWR-HMI-12", KILN_CFG_T_U16, dim_timeout_s, 0, 3600, 60, 0), - NUM("hmi.alarm_duration_s", "s", "SWR-RUN-06", KILN_CFG_T_U16, alarm_duration_s, 0, 600, 30, 0), + NUM("hmi.alarm_duration_s", "s", "SWR-RUN-06", KILN_CFG_T_U16, alarm_duration_s, 0, 600, 4, 0), /* network */ /* Set at the display (SWR-NET-12) rather than typed into a configuration diff --git a/firmware/controller/components/kiln_hal_esp32s3/src/hal_io.cpp b/firmware/controller/components/kiln_hal_esp32s3/src/hal_io.cpp index 9108a42..a1afe3a 100644 --- a/firmware/controller/components/kiln_hal_esp32s3/src/hal_io.cpp +++ b/firmware/controller/components/kiln_hal_esp32s3/src/hal_io.cpp @@ -11,6 +11,7 @@ #include "esp_log.h" #include "esp_timer.h" +#include "kiln_core/alarmptn.h" #include "kiln_hal/board_pins.h" #include "kiln_hal/hal_esp32s3.h" @@ -82,12 +83,8 @@ namespace { /* --- buzzer (SYS-HW-09, SWR-SAF-20) ---------------------------------------------- */ /* - * SWR-SAF-20 wants fault and completion audibly distinguishable, so this is a - * pattern and not a level. A 5 V active buzzer makes its own tone (SYS-HW-09), so - * all that is needed is gating it on and off: - * - * fault 200 ms on, 200 ms off urgent, and continues until acknowledged - * complete 200 ms on, 1800 ms off a periodic chirp, not an alarm + * The rhythms are in kiln_core/alarmptn, where they are host-tested; this is + * the half that cannot be: a pin and a one-shot timer. * * Driven by esp_timer rather than by the control task, because the pattern must * keep sounding while the operator is doing something else, and because a fault @@ -95,8 +92,7 @@ namespace { */ typedef struct { esp_timer_handle_t timer; - kiln_alarm_pattern_t pattern; - bool on; + kiln_alarm_seq_t seq; } alarm_t; alarm_t s_alarm; @@ -104,19 +100,15 @@ alarm_t s_alarm; void alarm_tick(void *arg) { alarm_t *a = static_cast(arg); - if (a->pattern == KILN_ALARM_OFF) { - a->on = false; - gpio_set_level((gpio_num_t)KILN_PIN_ALARM, 0); - return; - } - - a->on = !a->on; - gpio_set_level((gpio_num_t)KILN_PIN_ALARM, a->on ? 1 : 0); + const kiln_alarm_step_t s = kiln_alarm_seq_next(&a->seq); - const uint64_t next_us = - a->on ? 200u * 1000u - : ((a->pattern == KILN_ALARM_FAULT) ? 200u * 1000u : 1800u * 1000u); - (void)esp_timer_start_once(a->timer, next_us); + gpio_set_level((gpio_num_t)KILN_PIN_ALARM, s.on ? 1 : 0); + if (s.hold_ms > 0u) { + (void)esp_timer_start_once(a->timer, (uint64_t)s.hold_ms * 1000u); + } + /* hold_ms == 0 means nothing further is due, and the level set above is + * already the silent one. No timer is armed, so the buzzer stays quiet + * until something sets a pattern. */ } void alarm_set(void *ctx, kiln_alarm_pattern_t pattern) @@ -125,19 +117,13 @@ void alarm_set(void *ctx, kiln_alarm_pattern_t pattern) if (a == nullptr) { return; } - if (a->pattern == pattern) { + if (a->seq.pattern == pattern) { return; /* re-asserting must not restart the beat */ } - a->pattern = pattern; (void)esp_timer_stop(a->timer); + kiln_alarm_seq_begin(&a->seq, pattern); - if (pattern == KILN_ALARM_OFF) { - a->on = false; - gpio_set_level((gpio_num_t)KILN_PIN_ALARM, 0); - return; - } - /* Start loud: the first edge is immediate, not one period away. */ - a->on = false; + /* Start loud: the first interval is applied now, not one period away. */ alarm_tick(a); } @@ -164,8 +150,7 @@ void kiln_hal_alarm_init(kiln_port_alarm_t *out) args.name = "alarm"; (void)esp_timer_create(&args, &s_alarm.timer); - s_alarm.pattern = KILN_ALARM_OFF; - s_alarm.on = false; + kiln_alarm_seq_begin(&s_alarm.seq, KILN_ALARM_OFF); out->ctx = &s_alarm; out->set = alarm_set; diff --git a/firmware/controller/test/host/CMakeLists.txt b/firmware/controller/test/host/CMakeLists.txt index 662c2d2..3b0fd8d 100644 --- a/firmware/controller/test/host/CMakeLists.txt +++ b/firmware/controller/test/host/CMakeLists.txt @@ -72,6 +72,7 @@ function(kiln_add_test name) add_test(NAME ${name} COMMAND ${name}) endfunction() +kiln_add_test(test_alarmptn unit/test_alarmptn.cpp) kiln_add_test(test_pid unit/test_pid.cpp) kiln_add_test(test_window unit/test_window.cpp) kiln_add_test(test_suplink unit/test_suplink.cpp) diff --git a/firmware/controller/test/host/unit/test_alarmptn.cpp b/firmware/controller/test/host/unit/test_alarmptn.cpp new file mode 100644 index 0000000..969c030 --- /dev/null +++ b/firmware/controller/test/host/unit/test_alarmptn.cpp @@ -0,0 +1,204 @@ +/* SPDX-FileCopyrightText: 2026 Bitcrush Testing + * SPDX-License-Identifier: GPL-3.0-or-later + * + * The buzzer's two rhythms -- SWR-SAF-20, SWR-RUN-06, SYS-HW-09. + * + * SWR-SAF-20's requirement is that fault and completion are *audibly + * distinguishable*, which is a property of numbers and can therefore be + * asserted rather than described. Until the pattern moved into the core it + * lived in an esp_timer callback where nothing could reach it, and "the two + * sound different" was a comment. + */ + +#include "kiln_check.h" +#include "kiln_core/alarmptn.h" + +namespace { + +/* Walk a pattern for one full cycle and report what was heard. */ +typedef struct { + unsigned chirps; /* on-intervals */ + uint32_t on_ms; + uint32_t total_ms; + uint32_t longest_gap; /* the silence that separates bursts */ + uint32_t shortest_on; +} heard_t; + +heard_t listen(kiln_alarm_pattern_t p, unsigned max_steps) +{ + heard_t h = {0, 0, 0, 0, 0xFFFFFFFFu}; + kiln_alarm_seq_t s; + kiln_alarm_seq_begin(&s, p); + + const uint32_t cycle = kiln_alarm_cycle_ms(p); + for (unsigned i = 0; i < max_steps && h.total_ms < cycle; i++) { + const kiln_alarm_step_t step = kiln_alarm_seq_next(&s); + if (step.hold_ms == 0u) { + break; + } + h.total_ms += step.hold_ms; + if (step.on) { + h.chirps++; + h.on_ms += step.hold_ms; + if (step.hold_ms < h.shortest_on) { h.shortest_on = step.hold_ms; } + } + else if (step.hold_ms > h.longest_gap) { + h.longest_gap = step.hold_ms; + } + } + return h; +} + +} // namespace + +/* + * @relation(SWR-RUN-06, scope=function) + */ +KILN_TEST(swr_run_06_the_finished_firing_is_brief_and_over) +{ + const heard_t h = listen(KILN_ALARM_COMPLETE, 64); + + /* Three chirps, and the whole burst inside half a second: a firing that + * took nine hours is finished, which is a notification and not an alarm. */ + CHECK_EQ_UINT(h.chirps, 3u); + CHECK_EQ_UINT(h.on_ms, 270u); + CHECK(h.on_ms + 180u <= 500u); /* burst length, chirps and gaps */ + + /* And then it gets out of the way for five seconds, which is what makes it + * brief in practice: the application turns the alarm off after + * hmi.alarm_duration_s, and the default is shorter than this gap, so the + * operator hears the burst once. */ + CHECK(h.longest_gap >= 4000u); + CHECK_EQ_UINT(kiln_alarm_cycle_ms(KILN_ALARM_COMPLETE), 5000u); +} + +/* + * @relation(SWR-SAF-20, scope=function) + */ +KILN_TEST(swr_saf_20_a_safety_incident_is_announced_aggressively) +{ + const heard_t h = listen(KILN_ALARM_FAULT, 64); + + /* Six fast chirps and a short gap. Something is wrong with a kiln at + * 1 200 degC: this is meant to be unpleasant and to carry through a wall. */ + CHECK_EQ_UINT(h.chirps, 6u); + CHECK(h.longest_gap <= 500u); + + /* Chirping for a third of the time (360 ms of every 1 060), against a + * twentieth for completion (270 ms of every 5 000). That ratio is what + * "audibly distinguishable" means with an active buzzer that has one tone + * and no volume control. */ + const uint32_t cycle = kiln_alarm_cycle_ms(KILN_ALARM_FAULT); + CHECK_EQ_UINT(cycle, 1060u); + CHECK_EQ_UINT(h.on_ms, 360u); + CHECK(h.on_ms * 3u >= cycle); +} + +/* + * @relation(SWR-SAF-20, scope=function) + */ +KILN_TEST(swr_saf_20_the_two_patterns_cannot_be_confused) +{ + /* The requirement is a comparison, so it is asserted as one rather than as + * two sets of numbers that happen to differ. */ + const uint32_t fault_cycle = kiln_alarm_cycle_ms(KILN_ALARM_FAULT); + const uint32_t complete_cycle = kiln_alarm_cycle_ms(KILN_ALARM_COMPLETE); + + /* Duty: the fault sounds for a third of its cycle, completion for a + * twentieth of a cycle five times as long. */ + const uint32_t fault_duty = kiln_alarm_on_ms(KILN_ALARM_FAULT) * 100u / fault_cycle; + const uint32_t complete_duty = kiln_alarm_on_ms(KILN_ALARM_COMPLETE) * 100u / complete_cycle; + CHECK(fault_duty >= 4u * complete_duty); + + /* Rate: the fault repeats several times a second, completion once every + * few seconds. Both the rhythm and the repetition differ, so neither a + * noisy workshop nor a closed door collapses one into the other. */ + CHECK(complete_cycle >= 4u * fault_cycle); + + /* And the chirps themselves are different lengths. */ + const heard_t f = listen(KILN_ALARM_FAULT, 64); + const heard_t c = listen(KILN_ALARM_COMPLETE, 64); + CHECK(f.shortest_on < c.shortest_on); +} + +/* + * @relation(SWR-SAF-20, scope=function) + */ +KILN_TEST(swr_saf_20_the_fault_pattern_never_stops_on_its_own) +{ + /* A fault is cleared by an acknowledgement, never by the buzzer giving up. + * Ten cycles' worth of steps, and every one of them is still due. */ + kiln_alarm_seq_t s; + kiln_alarm_seq_begin(&s, KILN_ALARM_FAULT); + uint32_t sounded = 0; + for (unsigned i = 0; i < 200u; i++) { + const kiln_alarm_step_t step = kiln_alarm_seq_next(&s); + CHECK(step.hold_ms > 0u); + if (step.on) { sounded += step.hold_ms; } + } + /* Not merely "a step was returned": it was still making noise. */ + CHECK(sounded > 5000u); +} + +/* + * @relation(SWR-SAF-20, scope=function) + */ +KILN_TEST(swr_saf_20_off_is_silent_and_asks_for_nothing_further) +{ + kiln_alarm_seq_t s; + kiln_alarm_seq_begin(&s, KILN_ALARM_OFF); + const kiln_alarm_step_t step = kiln_alarm_seq_next(&s); + CHECK_EQ_INT(step.on, false); + CHECK_EQ_UINT(step.hold_ms, 0u); + CHECK_EQ_UINT(kiln_alarm_cycle_ms(KILN_ALARM_OFF), 0u); + + /* Asked again, it says the same thing: an adapter that keeps calling must + * not eventually be told to sound. */ + for (unsigned i = 0; i < 8u; i++) { + const kiln_alarm_step_t again = kiln_alarm_seq_next(&s); + CHECK_EQ_INT(again.on, false); + CHECK_EQ_UINT(again.hold_ms, 0u); + } +} + +/* + * @relation(SWR-SAF-20, scope=function) + */ +KILN_TEST(swr_saf_20_a_buzzer_that_cannot_be_commanded_stays_quiet) +{ + /* A null sequence, and a pattern value that is not one of the three: both + * answer silence. The buzzer's failure direction is off -- a part that + * cannot be commanded must not be a part that is stuck on, because the + * operator's only remedy for that is the mains switch. */ + const kiln_alarm_step_t none = kiln_alarm_seq_next(nullptr); + CHECK_EQ_INT(none.on, false); + CHECK_EQ_UINT(none.hold_ms, 0u); + kiln_alarm_seq_begin(nullptr, KILN_ALARM_FAULT); /* must not fault */ + + kiln_alarm_seq_t s; + kiln_alarm_seq_begin(&s, (kiln_alarm_pattern_t)99); + const kiln_alarm_step_t bogus = kiln_alarm_seq_next(&s); + CHECK_EQ_INT(bogus.on, false); + CHECK_EQ_UINT(bogus.hold_ms, 0u); +} + +/* + * @relation(SWR-SAF-20, scope=function) + */ +KILN_TEST(swr_saf_20_a_pattern_resumes_rather_than_restarting_mid_cycle) +{ + /* The adapter refuses to re-begin the pattern that is already running, so + * that a status update ten times a second cannot hold the buzzer at its + * first interval forever. What that relies on from here is that stepping + * is the only thing that advances the sequence. */ + kiln_alarm_seq_t s; + kiln_alarm_seq_begin(&s, KILN_ALARM_FAULT); + const kiln_alarm_step_t first = kiln_alarm_seq_next(&s); + const kiln_alarm_step_t second = kiln_alarm_seq_next(&s); + CHECK_EQ_INT(first.on, true); + CHECK_EQ_INT(second.on, false); + + kiln_alarm_seq_begin(&s, KILN_ALARM_FAULT); + const kiln_alarm_step_t again = kiln_alarm_seq_next(&s); + CHECK_EQ_INT(again.on, true); /* begun again, so back to the start */ +} From 873ccb01b3517597f4d58973d998732a9c72dd47 Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:51:52 +0200 Subject: [PATCH 08/14] feat: the display speaks German, and says why a join failed Three things O4 was waiting on, none of which needs hardware. **The frame was English.** kiln_core/faults has carried faults and warnings in both languages from the start, so a device set to German announced its faults in German inside an English frame: every screen title, the whole menu, both answers to a confirmation and every footer were string literals at their draw sites. SWR-NFR-23's "no text embedded at its point of use" was half true, and is now true: 46 strings in kiln_hmi/strings, in two languages, and no operator-facing literal left in hmi.cpp. The German uses the transliteration kiln_core/faults has always used, because draw.cpp's 5x7 font is the 95 printable ASCII glyphs and nothing else -- which answers J1 from the code rather than from a hardware session. Two of the choices are worth knowing: HOLD is WARTET rather than the literal HALT, which a German reader would take as "stopped", the opposite of a kiln that is still firing; and ACK? is QUIT?, from quittieren, which is the verb German machinery uses for acknowledging an alarm. J4 still stands: no native speaker has read any of it. Six tests cover what a reviewer cannot see by reading: nothing missing in either language, nothing over the panel's 21 columns, no byte outside 0x20 to 0x7E, an unknown language falling back to English, an out-of-range identifier answering with nothing rather than a wrong label, and the menu and the confirmation actually changing when the language does. **A failed join now says so.** last_reason was recorded in the adapter and reported to nobody, so the display returned to the network screen saying "not connected" -- which is not an answer an operator can act on, and with the display the only route in (there is no access point), acting on it is the whole job. The screen now separates a join in flight from one the radio refused and prints the code: 15 is a failed four-way handshake, a wrong passphrase in almost every case, 201 is "no AP of that name answered". The number is there to be read out over a phone to somebody who can look it up, which beats a sentence that guesses between three different remedies. kiln_net_status_t carries the field and net_connect clears it, so a fresh attempt is not reported as an already-failed one. **The knob resumes where the last character left it**, which it always did; what was missing was a test saying so, since that property is the sort one "sensible" reset silently reverses. SWR-NET-12 gains the outcome-reporting and resume obligations. SWR-NFR-23 records that the single-resource rule was half kept, and the two constraints the panel puts on German. 26/26 host suites, 39 HMI tests, clang-tidy 20 clean on host and target, hardware image builds, StrictDoc exports clean. --- docs/03_software_req.sdoc | 11 + .../kiln_hal_esp32s3/src/hal_net.cpp | 6 + .../components/kiln_hmi/CMakeLists.txt | 1 + .../kiln_hmi/include/kiln_hmi/hmi.h | 5 + .../kiln_hmi/include/kiln_hmi/strings.h | 114 +++++++++ .../components/kiln_hmi/src/hmi.cpp | 146 ++++++++---- .../components/kiln_hmi/src/strings.cpp | 108 +++++++++ .../kiln_ports/include/kiln_ports/port_net.h | 14 ++ firmware/controller/main/main.cpp | 2 + .../controller/test/host/unit/test_hmi.cpp | 219 ++++++++++++++++++ tasklist.md | 65 ++++-- 11 files changed, 627 insertions(+), 64 deletions(-) create mode 100644 firmware/controller/components/kiln_hmi/include/kiln_hmi/strings.h create mode 100644 firmware/controller/components/kiln_hmi/src/strings.cpp diff --git a/docs/03_software_req.sdoc b/docs/03_software_req.sdoc index 7d6e5ac..936507b 100644 --- a/docs/03_software_req.sdoc +++ b/docs/03_software_req.sdoc @@ -1657,6 +1657,10 @@ What the entry has to provide, each because its absence makes a network unjoinab - **An open network shall not ask for a passphrase at all.** The credentials shall be **persisted before the join is attempted**, so that a device which joins successfully and then loses power comes back to the same network rather than to a display nobody is standing at. + +**Added 2026-10-09.** The display shall also show **the outcome of the last attempt**, distinguishing a join in progress from one the radio refused, and shall show the refusal's reason code. "Not connected" is not an answer an operator can act on, and since this screen is the only route in, acting on it is the whole job: a mistyped passphrase, an SSID out of range and a radio that never came up are three different remedies and were previously one message. The reason code is shown as the number the driver reports, to be read out to somebody who can look it up, rather than translated into a sentence that guesses which of them it was. + +A knob is a poor keyboard, so entry shall **resume at the character the previous press selected** rather than returning to the start of the set. <<< PRIORITY: M VERIFICATION: T @@ -2188,6 +2192,13 @@ VERIFICATION: I UID: SWR-NFR-23 STATEMENT: >>> Operator-facing text shall be held in a **single resource location**, indexed by language, with no text embedded at its point of use. **English and German** shall both be provided, selected by the `hmi.language` configuration item and offered on the local display and in the web interface. A language with no entry for a string shall fall back to English rather than showing nothing. Diagnostic output, the event log and requirement identifiers shall remain English regardless of the setting, so that a fault quoted in a report reads the same whoever filed it. + +**Clarified 2026-10-09, after "no text embedded at its point of use" was found to be half true.** Faults and warnings were in a table from the start; the display's own frame -- screen titles, the menu, the two answers to a confirmation, the footers that say what the knob does -- was string literals at their draw sites, so a device set to German announced its faults in German inside an English frame. There are now two tables, `kiln_core/faults` for what has gone wrong and `kiln_hmi/strings` for the display's own words, and no operator-facing literal remains at a draw site. + +Two constraints on the German, both from the panel rather than from the language, and both asserted rather than reviewed: + +- **The 5x7 font carries the 95 printable ASCII glyphs and nothing else**, so German text is transliterated (AE, OE, UE, SS). A byte outside 0x20 to 0x7E reaches the panel as a blank, which nobody would see until the device was in a workshop. +- **21 characters is the display's width** at scale 1, past which the framebuffer clips silently. German runs about 15 per cent longer than English, so this is the bound the translation is written against rather than something to discover on hardware. <<< PRIORITY: M VERIFICATION: T diff --git a/firmware/controller/components/kiln_hal_esp32s3/src/hal_net.cpp b/firmware/controller/components/kiln_hal_esp32s3/src/hal_net.cpp index b05a178..dd154d5 100644 --- a/firmware/controller/components/kiln_hal_esp32s3/src/hal_net.cpp +++ b/firmware/controller/components/kiln_hal_esp32s3/src/hal_net.cpp @@ -184,6 +184,11 @@ kiln_err_t net_connect(void *ctx, const char *ssid, const char *pass) s_net.sta_configured = true; s_net.retry_ms = RETRY_MIN_MS; s_net.state = KILN_NET_CONNECTING; + /* A fresh attempt, so the previous attempt's reason stops being reported: + * the display distinguishes "joining" from "it failed, and here is why" by + * exactly this field, and a stale code would show the new attempt as + * already failed. */ + s_net.last_reason = 0u; (void)copy_checked(s_net.ssid, sizeof(s_net.ssid), ssid, "ssid"); ESP_LOGI(TAG, "joining '%s' as asked at the display", ssid); return (esp_wifi_connect() == ESP_OK) ? KILN_OK : KILN_ERR_IO; @@ -286,6 +291,7 @@ kiln_err_t net_status(void *ctx, kiln_net_status_t *out) (void)snprintf(out->hostname, sizeof(out->hostname), "%s", n->hostname); out->disconnect_count = n->disconnects; out->time_synced = n->time_synced; + out->last_reason = n->last_reason; out->uptime_s = (n->state == KILN_NET_STA_CONNECTED && n->up_since_us > 0) ? (uint32_t)((esp_timer_get_time() - n->up_since_us) / 1000000) : 0u; diff --git a/firmware/controller/components/kiln_hmi/CMakeLists.txt b/firmware/controller/components/kiln_hmi/CMakeLists.txt index 3c9f42d..a35a5b3 100644 --- a/firmware/controller/components/kiln_hmi/CMakeLists.txt +++ b/firmware/controller/components/kiln_hmi/CMakeLists.txt @@ -9,6 +9,7 @@ set(KILN_HMI_SRCS src/draw.cpp src/hmi.cpp + src/strings.cpp ) if(ESP_PLATFORM) diff --git a/firmware/controller/components/kiln_hmi/include/kiln_hmi/hmi.h b/firmware/controller/components/kiln_hmi/include/kiln_hmi/hmi.h index 6781948..35168bf 100644 --- a/firmware/controller/components/kiln_hmi/include/kiln_hmi/hmi.h +++ b/firmware/controller/components/kiln_hmi/include/kiln_hmi/hmi.h @@ -102,6 +102,11 @@ typedef struct { * between "no networks here" and "ask again in a moment", which is the * whole of what an operator needs during the two seconds a scan takes. */ bool net_scanning; + /* SWR-NET-12: the radio's reason for the last disconnect or refused join, + * straight from kiln_net_status_t. With the display the only way + * credentials reach the device, "it did not work" is not a useful answer, + * and a reason code is something an operator can read out over a phone. */ + uint8_t net_last_reason; uint8_t net_count; char net_list_ssid[KILN_HMI_MAX_NETWORKS][KILN_HMI_SSID_LEN]; int8_t net_list_rssi[KILN_HMI_MAX_NETWORKS]; diff --git a/firmware/controller/components/kiln_hmi/include/kiln_hmi/strings.h b/firmware/controller/components/kiln_hmi/include/kiln_hmi/strings.h new file mode 100644 index 0000000..f24f4c2 --- /dev/null +++ b/firmware/controller/components/kiln_hmi/include/kiln_hmi/strings.h @@ -0,0 +1,114 @@ +/* SPDX-FileCopyrightText: 2026 Bitcrush Testing + * SPDX-License-Identifier: GPL-3.0-or-later + * + * The display's own words -- SWR-NFR-23, SWR-HMI-15. + * + * `kiln_core/faults` already holds every fault and warning in both languages, + * which is the prose an operator reads when something has gone wrong. This is + * the other half: the screen titles, the menu, the two answers to a + * confirmation and the footers that say what the knob does. They were English + * string literals at their draw sites, so a device configured for German + * announced its faults in German inside an English frame. + * + * --------------------------------------------------------------------------- + * WHY THE GERMAN IS SPELLED LIKE THAT + * --------------------------------------------------------------------------- + * The 5x7 font in draw.cpp carries the 95 printable ASCII glyphs and nothing + * else, so there are no umlauts and no sharp s to draw with. Every German + * string here is therefore transliterated -- AE OE UE SS -- which is what + * `kiln_core/faults` has always done and is a convention German readers are + * used to from equipment displays. A test asserts it, because a single + * umlaut typed into this table would reach the panel as a blank or a + * question mark, and nobody would see it until the device was in a workshop. + * + * The strings are also bounded: 21 characters is the display's width at + * scale 1 (128 pixels, 6 per glyph), and a label that overruns it is silently + * clipped by the framebuffer. German runs about 15 per cent longer than + * English, so this is the bound the translation has to be written against + * rather than a thing to check afterwards -- the test holds every line-length + * string to it. + */ +#ifndef KILN_HMI_STRINGS_H +#define KILN_HMI_STRINGS_H + +#include "kiln/types.h" + +/* One identifier per piece of text. An enum rather than keys, so that adding a + * screen without translating it does not compile. */ +typedef enum { + /* Screen titles */ + KILN_HMI_STR_MENU = 0, + KILN_HMI_STR_PROGRAMS, + KILN_HMI_STR_NETWORK, + KILN_HMI_STR_NETWORKS, + KILN_HMI_STR_PASSPHRASE, + KILN_HMI_STR_DIAGNOSTICS, + KILN_HMI_STR_INFO, + KILN_HMI_STR_FAULT, + KILN_HMI_STR_SUPERVISOR, + + /* The main screen's badges. Four or five characters: these sit beside one + * another on one row and are read at a glance from across a workshop. */ + KILN_HMI_STR_HEAT, + KILN_HMI_STR_HOLD, + KILN_HMI_STR_ACK, + + /* Menu */ + KILN_HMI_STR_START_PROGRAM, + KILN_HMI_STR_PAUSE, + KILN_HMI_STR_RESUME, + KILN_HMI_STR_PAUSE_RESUME, + KILN_HMI_STR_ABORT, + KILN_HMI_STR_NONE_STORED, + + /* Confirmation */ + KILN_HMI_STR_START_FIRING_Q, + KILN_HMI_STR_ABORT_FIRING_Q, + KILN_HMI_STR_YES, + KILN_HMI_STR_NO, + KILN_HMI_STR_TURN_THEN_PRESS, + + /* Network */ + KILN_HMI_STR_CONNECTED, + KILN_HMI_STR_NOT_CONNECTED, + KILN_HMI_STR_PRESS_SETUP_WIFI, + KILN_HMI_STR_SCANNING, + KILN_HMI_STR_NONE_FOUND, + KILN_HMI_STR_HOLD_BACK, + KILN_HMI_STR_NEEDS_KEY, + KILN_HMI_STR_OF, + KILN_HMI_STR_JOIN_FAILED, + KILN_HMI_STR_JOINING, + + /* Passphrase entry */ + KILN_HMI_STR_DELETE, + KILN_HMI_STR_CONNECT, + KILN_HMI_STR_EIGHT_OR_MORE, + KILN_HMI_STR_CHARS, + KILN_HMI_STR_HOLD_CANCEL, + + /* Diagnostics and info. Column labels, padded to a common width at the + * draw site rather than here, so the padding is not part of the + * translation. */ + KILN_HMI_STR_CASE, + KILN_HMI_STR_AMPS, + KILN_HMI_STR_POWER, + KILN_HMI_STR_USED, + KILN_HMI_STR_DUTY, + KILN_HMI_STR_UPTIME, + KILN_HMI_STR_TUNED, + KILN_HMI_STR_UNTUNED, + + KILN_HMI_STR_COUNT, +} kiln_hmi_str_id_t; + +/* The string, in the configured language. A language the table does not carry + * and an identifier out of range both fall back to English rather than to an + * empty screen, which is what types.h promises for kiln_lang_t. */ +const char *kiln_hmi_str(kiln_hmi_str_id_t id, kiln_lang_t lang); + +/* The display's width in characters at scale 1, which is the bound every line + * of text here is written against. */ +constexpr unsigned KILN_HMI_COLS = 21u; + +#endif /* KILN_HMI_STRINGS_H */ diff --git a/firmware/controller/components/kiln_hmi/src/hmi.cpp b/firmware/controller/components/kiln_hmi/src/hmi.cpp index 982ad0c..f65698a 100644 --- a/firmware/controller/components/kiln_hmi/src/hmi.cpp +++ b/firmware/controller/components/kiln_hmi/src/hmi.cpp @@ -10,6 +10,7 @@ #include "kiln_core/faults.h" #include "kiln_hmi/hmi.h" +#include "kiln_hmi/strings.h" /* --- small helpers ------------------------------------------------------ */ @@ -67,6 +68,14 @@ void fmt_hms(char *buf, size_t n, uint32_t s) else { (void)snprintf(buf, n, "%um%02u", (unsigned)m, (unsigned)(s % 60u)); } } +/* The one place the language is read, so a draw site is a lookup rather than a + * conditional. SWR-NFR-23 is a presentation concern: nothing downstream of + * here knows what language the panel is in. */ +const char *S(const kiln_hmi_view_t *v, kiln_hmi_str_id_t id) +{ + return kiln_hmi_str(id, v->language); +} + const char *state_text(const kiln_hmi_view_t *v) { /* SWR-NFR-23 / SWR-HMI-15: the one table, in the configured language. */ @@ -123,9 +132,9 @@ void draw_main(kiln_hmi_t *h, const kiln_hmi_view_t *v) /* Duty, and the two states worth seeing at a glance. */ (void)snprintf(buf, sizeof(buf), "%u%%", (unsigned)(v->snap.duty_permille / 10u)); kiln_fb_text(&h->fb, 0, 40, buf, 1, true); - if (v->snap.heat_authorised) { kiln_fb_text(&h->fb, 30, 40, "HEAT", 1, true); } - if (v->snap.holdback_active) { kiln_fb_text(&h->fb, 62, 40, "HOLD", 1, true); } - if (v->awaiting_ack) { kiln_fb_text(&h->fb, 94, 40, "ACK?", 1, true); } + if (v->snap.heat_authorised) { kiln_fb_text(&h->fb, 30, 40, S(v, KILN_HMI_STR_HEAT), 1, true); } + if (v->snap.holdback_active) { kiln_fb_text(&h->fb, 62, 40, S(v, KILN_HMI_STR_HOLD), 1, true); } + if (v->awaiting_ack) { kiln_fb_text(&h->fb, 94, 40, S(v, KILN_HMI_STR_ACK), 1, true); } /* SWR-HMI-04's progress indication: segment position through the program. */ uint8_t pct = 0; @@ -158,9 +167,10 @@ void draw_fault(kiln_hmi_t *h, const kiln_hmi_view_t *v) char buf[40]; if (sup_blame) { - (void)snprintf(buf, sizeof(buf), "SUPERVISOR"); + (void)snprintf(buf, sizeof(buf), "%s", S(v, KILN_HMI_STR_SUPERVISOR)); } else { - (void)snprintf(buf, sizeof(buf), "FAULT %u", (unsigned)v->fault); + (void)snprintf(buf, sizeof(buf), "%s %u", S(v, KILN_HMI_STR_FAULT), + (unsigned)v->fault); } kiln_fb_text(&h->fb, 2, 2, buf, 1, false); kiln_fb_text_right(&h->fb, KILN_DISPLAY_W - 2, 2, @@ -200,12 +210,14 @@ const char *menu_label(uint8_t i, const kiln_hmi_view_t *v) const bool running = (v->snap.state == (uint8_t)KILN_STATE_RUNNING); const bool paused = (v->snap.state == (uint8_t)KILN_STATE_PAUSED); switch (i) { - case 0: return "Start program"; - case 1: return running ? "Pause" : (paused ? "Resume" : "Pause/Resume"); - case 2: return "Abort"; - case 3: return "Network"; - case 4: return "Diagnostics"; - case 5: return "Info"; + case 0: return S(v, KILN_HMI_STR_START_PROGRAM); + case 1: return running ? S(v, KILN_HMI_STR_PAUSE) + : (paused ? S(v, KILN_HMI_STR_RESUME) + : S(v, KILN_HMI_STR_PAUSE_RESUME)); + case 2: return S(v, KILN_HMI_STR_ABORT); + case 3: return S(v, KILN_HMI_STR_NETWORK); + case 4: return S(v, KILN_HMI_STR_DIAGNOSTICS); + case 5: return S(v, KILN_HMI_STR_INFO); default: return ""; } } @@ -250,7 +262,9 @@ const char *program_label(uint8_t i, const kiln_hmi_view_t *v) void draw_confirm(kiln_hmi_t *h, const kiln_hmi_view_t *v) { /* SWR-HMI-11: starting and aborting both require this step. */ - const char *what = (h->pending == KILN_HMI_ACT_START) ? "Start firing?" : "Abort firing?"; + const char *what = (h->pending == KILN_HMI_ACT_START) + ? S(v, KILN_HMI_STR_START_FIRING_Q) + : S(v, KILN_HMI_STR_ABORT_FIRING_Q); kiln_fb_text(&h->fb, 0, 4, what, 1, true); if (h->pending == KILN_HMI_ACT_START && h->pending_program < v->program_count) { @@ -258,29 +272,58 @@ void draw_confirm(kiln_hmi_t *h, const kiln_hmi_view_t *v) } if (h->confirm_yes) { - kiln_fb_text_inv(&h->fb, 14, 40, "YES", 1); - kiln_fb_text(&h->fb, 80, 40, "NO", 1, true); + kiln_fb_text_inv(&h->fb, 14, 40, S(v, KILN_HMI_STR_YES), 1); + kiln_fb_text(&h->fb, 80, 40, S(v, KILN_HMI_STR_NO), 1, true); } else { - kiln_fb_text(&h->fb, 14, 40, "YES", 1, true); - kiln_fb_text_inv(&h->fb, 80, 40, "NO", 1); + kiln_fb_text(&h->fb, 14, 40, S(v, KILN_HMI_STR_YES), 1, true); + kiln_fb_text_inv(&h->fb, 80, 40, S(v, KILN_HMI_STR_NO), 1); } - kiln_fb_text(&h->fb, 0, 54, "turn to choose, press", 1, true); + kiln_fb_text(&h->fb, 0, 54, S(v, KILN_HMI_STR_TURN_THEN_PRESS), 1, true); } void draw_network(kiln_hmi_t *h, const kiln_hmi_view_t *v) { /* SWR-HMI-07: where the web interface is, which is the question an operator * standing at the kiln actually has. */ - kiln_fb_text(&h->fb, 0, 0, "NETWORK", 1, true); + kiln_fb_text(&h->fb, 0, 0, S(v, KILN_HMI_STR_NETWORK), 1, true); kiln_fb_hline(&h->fb, 0, 9, KILN_DISPLAY_W, true); - kiln_fb_text(&h->fb, 0, 13, v->net_up ? "connected" : "not connected", 1, true); + + /* SWR-NET-12: what became of the last attempt. + * + * "not connected" on its own is the answer an operator cannot act on, and + * with the display the only route in -- there is no access point -- acting + * on it is the whole job. So a join in progress says so, and a join that + * the radio refused says so with the radio's reason code: 15 is a failed + * four-way handshake, which is a wrong passphrase in almost every case, + * 201 is "no AP of that name answered", and 2 and 4 are the AP dropping + * the association. The number is there to be read out over a phone to + * somebody who can look it up, which beats a sentence that guesses. */ + const char *first = v->net_up ? S(v, KILN_HMI_STR_CONNECTED) + : S(v, KILN_HMI_STR_NOT_CONNECTED); + kiln_fb_text(&h->fb, 0, 13, first, 1, true); kiln_fb_text(&h->fb, 0, 23, v->net_up ? v->net_ssid : "", 1, true); - kiln_fb_text(&h->fb, 0, 33, v->hostname, 1, true); + + if (!v->net_up && v->net_last_reason != 0u) { + char why[32]; + (void)snprintf(why, sizeof(why), "%s (%u)", S(v, KILN_HMI_STR_JOIN_FAILED), + (unsigned)v->net_last_reason); + kiln_fb_text(&h->fb, 0, 33, why, 1, true); + } + else if (!v->net_up) { + /* No reason recorded: either nothing has been attempted yet, or an + * attempt is in flight. Both are "ask again in a moment" rather than + * a failure, and saying so is what stops an operator abandoning a join + * that was going to work. */ + kiln_fb_text(&h->fb, 0, 33, S(v, KILN_HMI_STR_JOINING), 1, true); + } + else { + kiln_fb_text(&h->fb, 0, 33, v->hostname, 1, true); + } kiln_fb_text(&h->fb, 0, 43, v->ip, 1, true); /* SWR-NET-11: this is the only route into WiFi setup, and it says so. The * device raises no access point, so if this screen cannot be reached the * network cannot be configured at all. */ - kiln_fb_text(&h->fb, 0, 54, "press: set up wifi", 1, true); + kiln_fb_text(&h->fb, 0, 54, S(v, KILN_HMI_STR_PRESS_SETUP_WIFI), 1, true); } /* SWR-NET-11. Four rows at a time, strongest first, with a bar for signal and @@ -289,13 +332,14 @@ void draw_network(kiln_hmi_t *h, const kiln_hmi_view_t *v) * the operator can see that it was cut. */ void draw_networks(kiln_hmi_t *h, const kiln_hmi_view_t *v) { - kiln_fb_text(&h->fb, 0, 0, "WIFI NETWORKS", 1, true); + kiln_fb_text(&h->fb, 0, 0, S(v, KILN_HMI_STR_NETWORKS), 1, true); kiln_fb_hline(&h->fb, 0, 9, KILN_DISPLAY_W, true); if (v->net_count == 0u) { kiln_fb_text(&h->fb, 0, 24, - v->net_scanning ? "scanning..." : "none found", 1, true); - kiln_fb_text(&h->fb, 0, 54, "hold: back", 1, true); + v->net_scanning ? S(v, KILN_HMI_STR_SCANNING) + : S(v, KILN_HMI_STR_NONE_FOUND), 1, true); + kiln_fb_text(&h->fb, 0, 54, S(v, KILN_HMI_STR_HOLD_BACK), 1, true); return; } @@ -327,8 +371,9 @@ void draw_networks(kiln_hmi_t *h, const kiln_hmi_view_t *v) * drawn on: the compiler is right that %u of an unsigned is ten digits, * and a truncated footer is a worse answer than a wide buffer. */ char foot[40]; - (void)snprintf(foot, sizeof(foot), "%u of %u * needs key", - (unsigned)(h->net_sel + 1u), (unsigned)v->net_count); + (void)snprintf(foot, sizeof(foot), "%u %s %u %s", + (unsigned)(h->net_sel + 1u), S(v, KILN_HMI_STR_OF), + (unsigned)v->net_count, S(v, KILN_HMI_STR_NEEDS_KEY)); kiln_fb_text(&h->fb, 0, 54, foot, 1, true); } @@ -339,7 +384,7 @@ void draw_networks(kiln_hmi_t *h, const kiln_hmi_view_t *v) void draw_passphrase(kiln_hmi_t *h, const kiln_hmi_view_t *v) { (void)v; - kiln_fb_text(&h->fb, 0, 0, "PASSPHRASE", 1, true); + kiln_fb_text(&h->fb, 0, 0, S(v, KILN_HMI_STR_PASSPHRASE), 1, true); kiln_fb_hline(&h->fb, 0, 9, KILN_DISPLAY_W, true); /* The tail, because that is where the cursor is: 20 characters fit and a @@ -352,43 +397,48 @@ void draw_passphrase(kiln_hmi_t *h, const kiln_hmi_view_t *v) char pick[24]; if (h->charset_sel == K_SEL_DEL) { - (void)snprintf(pick, sizeof(pick), "[ delete ]"); + (void)snprintf(pick, sizeof(pick), "%s", S(v, KILN_HMI_STR_DELETE)); } else if (h->charset_sel == K_SEL_OK) { /* The length rule is shown where it is enforced, so "nothing happened * when I pressed" never has to be guessed at. */ (void)snprintf(pick, sizeof(pick), "%s", - (h->pass_len >= 8u) ? "[ connect ]" : "[ 8 or more ]"); + (h->pass_len >= 8u) ? S(v, KILN_HMI_STR_CONNECT) + : S(v, KILN_HMI_STR_EIGHT_OR_MORE)); } else { (void)snprintf(pick, sizeof(pick), " %c", k_charset[h->charset_sel]); } kiln_fb_text(&h->fb, 0, 30, pick, 1, true); char foot[40]; - (void)snprintf(foot, sizeof(foot), "%u chars hold: cancel", - (unsigned)h->pass_len); + (void)snprintf(foot, sizeof(foot), "%u %s %s", (unsigned)h->pass_len, + S(v, KILN_HMI_STR_CHARS), S(v, KILN_HMI_STR_HOLD_CANCEL)); kiln_fb_text(&h->fb, 0, 54, foot, 1, true); } void draw_diag(kiln_hmi_t *h, const kiln_hmi_view_t *v) { char buf[32], t[12]; - kiln_fb_text(&h->fb, 0, 0, "DIAGNOSTICS", 1, true); + kiln_fb_text(&h->fb, 0, 0, S(v, KILN_HMI_STR_DIAGNOSTICS), 1, true); kiln_fb_hline(&h->fb, 0, 9, KILN_DISPLAY_W, true); fmt_temp(t, sizeof(t), v, v->snap.case_c, v->snap.case_valid); - (void)snprintf(buf, sizeof(buf), "case %s%s", t, unit_str(v)); + (void)snprintf(buf, sizeof(buf), "%-5s %s%s", S(v, KILN_HMI_STR_CASE), t, + unit_str(v)); kiln_fb_text(&h->fb, 0, 13, buf, 1, true); - (void)snprintf(buf, sizeof(buf), "amps %.1f", (double)v->snap.current_a); + (void)snprintf(buf, sizeof(buf), "%-5s %.1f", S(v, KILN_HMI_STR_AMPS), + (double)v->snap.current_a); kiln_fb_text(&h->fb, 0, 23, buf, 1, true); /* SWR-CUR-07, in the units a kiln owner thinks in. */ - (void)snprintf(buf, sizeof(buf), "power %.2fkW", v->power_w / 1000.0); + (void)snprintf(buf, sizeof(buf), "%-5s %.2fkW", S(v, KILN_HMI_STR_POWER), + v->power_w / 1000.0); kiln_fb_text(&h->fb, 0, 33, buf, 1, true); - (void)snprintf(buf, sizeof(buf), "used %.2fkWh", v->energy_wh / 1000.0); + (void)snprintf(buf, sizeof(buf), "%-5s %.2fkWh", S(v, KILN_HMI_STR_USED), + v->energy_wh / 1000.0); kiln_fb_text(&h->fb, 0, 43, buf, 1, true); - (void)snprintf(buf, sizeof(buf), "duty %u%%", + (void)snprintf(buf, sizeof(buf), "%-5s %u%%", S(v, KILN_HMI_STR_DUTY), (unsigned)(v->snap.duty_permille / 10u)); kiln_fb_text(&h->fb, 0, 53, buf, 1, true); } @@ -396,12 +446,12 @@ void draw_diag(kiln_hmi_t *h, const kiln_hmi_view_t *v) void draw_info(kiln_hmi_t *h, const kiln_hmi_view_t *v) { char buf[40], up[12]; - kiln_fb_text(&h->fb, 0, 0, "INFO", 1, true); + kiln_fb_text(&h->fb, 0, 0, S(v, KILN_HMI_STR_INFO), 1, true); kiln_fb_hline(&h->fb, 0, 9, KILN_DISPLAY_W, true); kiln_fb_text(&h->fb, 0, 13, v->version, 1, true); fmt_hms(up, sizeof(up), v->uptime_s); - (void)snprintf(buf, sizeof(buf), "up %s", up); + (void)snprintf(buf, sizeof(buf), "%s %s", S(v, KILN_HMI_STR_UPTIME), up); kiln_fb_text(&h->fb, 0, 23, buf, 1, true); (void)snprintf(buf, sizeof(buf), "P%.1f I%.3f", (double)v->kp, (double)v->ki); @@ -411,7 +461,9 @@ void draw_info(kiln_hmi_t *h, const kiln_hmi_view_t *v) /* SWR-TUN-11: factory gains are not gains for *this* kiln, and the screen * that shows them is the right place to say so. */ - kiln_fb_text(&h->fb, 0, 53, v->gains_tuned ? "tuned" : "UNTUNED defaults", 1, true); + kiln_fb_text(&h->fb, 0, 53, + v->gains_tuned ? S(v, KILN_HMI_STR_TUNED) + : S(v, KILN_HMI_STR_UNTUNED), 1, true); } } // namespace @@ -652,15 +704,21 @@ kiln_hmi_action_t kiln_hmi_update(kiln_hmi_t *h, const kiln_hmi_view_t *view, switch (h->screen) { case KILN_HMI_SCREEN_FAULT: draw_fault(h, view); break; case KILN_HMI_SCREEN_MENU: - draw_list(h, "MENU", MENU_ITEMS, h->menu_sel, 0, menu_label, view); + draw_list(h, kiln_hmi_str(KILN_HMI_STR_MENU, view->language), + MENU_ITEMS, h->menu_sel, 0, menu_label, view); break; case KILN_HMI_SCREEN_PROGRAMS: if (view->program_count == 0u) { - kiln_fb_text(&h->fb, 0, 0, "PROGRAMS", 1, true); + kiln_fb_text(&h->fb, 0, 0, + kiln_hmi_str(KILN_HMI_STR_PROGRAMS, view->language), + 1, true); kiln_fb_hline(&h->fb, 0, 9, KILN_DISPLAY_W, true); - kiln_fb_text(&h->fb, 0, 20, "none stored", 1, true); + kiln_fb_text(&h->fb, 0, 20, + kiln_hmi_str(KILN_HMI_STR_NONE_STORED, view->language), + 1, true); } else { - draw_list(h, "PROGRAMS", view->program_count, h->prog_sel, h->prog_top, + draw_list(h, kiln_hmi_str(KILN_HMI_STR_PROGRAMS, view->language), + view->program_count, h->prog_sel, h->prog_top, program_label, view); } break; diff --git a/firmware/controller/components/kiln_hmi/src/strings.cpp b/firmware/controller/components/kiln_hmi/src/strings.cpp new file mode 100644 index 0000000..92d3508 --- /dev/null +++ b/firmware/controller/components/kiln_hmi/src/strings.cpp @@ -0,0 +1,108 @@ +/* SPDX-FileCopyrightText: 2026 Bitcrush Testing + * SPDX-License-Identifier: GPL-3.0-or-later + * + * The display's own words, in both languages. The conventions, the ASCII + * transliteration and the 21 character bound are explained in strings.h. + * + * The German is written for somebody standing at a kiln, which is why it uses + * the abbreviations that appear on industrial panels rather than the full words + * a letter would use: HEIZ, QUIT and STOERUNG are what a German-speaking + * operator already reads on machinery, and a display is not the place to teach + * new vocabulary. Two of them are worth a note: + * + * HOLD -> WARTET. Holdback is the program waiting for the chamber to catch + * up, so "wartet" (waiting) says what is happening. "HALT" would have been + * the literal translation and would have been read as "stopped", which is the + * opposite of a kiln that is still firing. + * + * ACK? -> QUIT? Quittieren is the standard German verb for acknowledging an + * alarm on industrial equipment. It has nothing to do with quitting, and a + * German operator will not read it that way. + * + * J4 still applies: none of this has been reviewed by a native speaker. + */ + +#include "kiln_hmi/strings.h" + +namespace { + +/* [identifier][language]. A plain table so that the whole translation can be + * read in one go, which is how a reviewer can actually check it. */ +const char *const k_str[KILN_HMI_STR_COUNT][KILN_LANG_COUNT] = { + /* Screen titles */ + { "MENU", "MENUE" }, + { "PROGRAMS", "PROGRAMME" }, + { "NETWORK", "NETZWERK" }, + { "WIFI NETWORKS", "WLAN NETZE" }, + { "PASSPHRASE", "PASSWORT" }, + { "DIAGNOSTICS", "DIAGNOSE" }, + { "INFO", "INFO" }, + { "FAULT", "STOERUNG" }, + { "SUPERVISOR", "WAECHTER" }, + + /* Main screen badges */ + { "HEAT", "HEIZ" }, + { "HOLD", "WARTET" }, + { "ACK?", "QUIT?" }, + + /* Menu */ + { "Start program", "Programm starten" }, + { "Pause", "Pause" }, + { "Resume", "Fortsetzen" }, + { "Pause/Resume", "Pause/Fortsetzen" }, + { "Abort", "Abbrechen" }, + { "none stored", "keine gespeichert" }, + + /* Confirmation */ + { "Start firing?", "Brennen starten?" }, + { "Abort firing?", "Brennen abbrechen?" }, + { "YES", "JA" }, + { "NO", "NEIN" }, + { "turn to choose, press", "drehen, dann druecken" }, + + /* Network */ + { "connected", "verbunden" }, + { "not connected", "nicht verbunden" }, + { "press: set up wifi", "druecken: WLAN einr." }, + { "scanning...", "suche..." }, + { "none found", "nichts gefunden" }, + { "hold: back", "halten: zurueck" }, + { "* needs key", "* Schluessel" }, + { "of", "von" }, + { "join failed", "Verb. gescheitert" }, + { "joining...", "verbinde..." }, + + /* Passphrase entry */ + { "[ delete ]", "[ loeschen ]" }, + { "[ connect ]", "[ verbinden ]" }, + { "[ 8 or more ]", "[ min. 8 ]" }, + { "chars", "Zeichen" }, + { "hold: cancel", "halten: Abbruch" }, + + /* Diagnostics and info */ + { "case", "Geh." }, + { "amps", "Strom" }, + { "power", "Leist" }, + { "used", "Verbr" }, + { "duty", "Takt" }, + { "up", "Lauf" }, + { "tuned", "abgestimmt" }, + { "UNTUNED defaults", "NICHT abgestimmt" }, +}; + +} // namespace + +const char *kiln_hmi_str(kiln_hmi_str_id_t id, kiln_lang_t lang) +{ + if ((unsigned)id >= (unsigned)KILN_HMI_STR_COUNT) { + /* Not an identifier this build knows. An empty string rather than a + * guess: a wrong label on a kiln's display is worse than a gap, which + * at least looks like what it is. */ + return ""; + } + if ((unsigned)lang >= (unsigned)KILN_LANG_COUNT) { + lang = KILN_LANG_EN; + } + const char *s = k_str[id][lang]; + return (s != nullptr) ? s : k_str[id][KILN_LANG_EN]; +} diff --git a/firmware/controller/components/kiln_ports/include/kiln_ports/port_net.h b/firmware/controller/components/kiln_ports/include/kiln_ports/port_net.h index 31d7cc5..4fb07f1 100644 --- a/firmware/controller/components/kiln_ports/include/kiln_ports/port_net.h +++ b/firmware/controller/components/kiln_ports/include/kiln_ports/port_net.h @@ -40,6 +40,20 @@ typedef struct { uint32_t disconnect_count; uint32_t uptime_s; bool time_synced; + /* SWR-NET-12. The radio's own reason for the last disconnect or refused + * join -- IEEE 802.11 reason codes as the driver reports them, plus the + * vendor range above 200 that carries "no AP found" and "handshake + * timeout", which are the two an operator at the display will actually + * meet. Zero means none has been recorded, which is also what a device + * that has never tried to associate says. + * + * Carried because the display had nothing to show when a join failed: it + * returned to the network screen saying "not connected", leaving the + * operator to guess between a typed passphrase that was wrong, an SSID + * that is out of range, and a radio that never came up. With the setup at + * the display the only route in (there is no access point), guessing is the + * whole cost of getting it wrong. */ + uint8_t last_reason; } kiln_net_status_t; /* The most networks a scan will report. Sixteen rather than "all of them": diff --git a/firmware/controller/main/main.cpp b/firmware/controller/main/main.cpp index e0cecb0..8641fc7 100644 --- a/firmware/controller/main/main.cpp +++ b/firmware/controller/main/main.cpp @@ -546,6 +546,8 @@ void hmi_build_view(kiln_hmi_view_t *v) (void)snprintf(v->hostname, sizeof(v->hostname), "%s", ns.hostname); (void)snprintf(v->ip, sizeof(v->ip), "%s", (ns.ip[0] != '\0') ? ns.ip : "no address"); + /* SWR-NET-12: why the last attempt did not take, if it did not. */ + v->net_last_reason = ns.last_reason; } else { v->net_up = false; (void)snprintf(v->hostname, sizeof(v->hostname), "%s", "wifi down"); diff --git a/firmware/controller/test/host/unit/test_hmi.cpp b/firmware/controller/test/host/unit/test_hmi.cpp index 16f95aa..d7eb920 100644 --- a/firmware/controller/test/host/unit/test_hmi.cpp +++ b/firmware/controller/test/host/unit/test_hmi.cpp @@ -18,6 +18,7 @@ #include "kiln_check.h" #include "kiln_core/faults.h" #include "kiln_hmi/hmi.h" +#include "kiln_hmi/strings.h" static kiln_hmi_view_t base_view(void) { @@ -745,3 +746,221 @@ KILN_TEST(swrnet11_the_list_scrolls_and_keeps_the_selection_visible) (void)feed(&h, &v, KILN_INPUT_CW); } } + +/* =========================================================================== + * THE DISPLAY'S OWN WORDS (SWR-NFR-23, tasklist O4 and J2) + * =========================================================================== + * kiln_core/faults has always carried faults and warnings in both languages, + * so a device set to German announced its faults in German inside an English + * frame: every screen title, the whole menu, both answers to a confirmation + * and every footer were string literals at their draw sites. + * + * The table these check is the one a reviewer reads (J4 is still open: no + * native speaker has been through it). What is asserted here is what a + * reviewer cannot see by reading -- that nothing is missing, that nothing + * overruns the panel, and that nothing contains a character the font cannot + * draw. + */ + +/* + * @relation(SWR-NFR-23, scope=function) + */ +KILN_TEST(swrnfr23_every_string_exists_in_both_languages) +{ + for (unsigned id = 0; id < (unsigned)KILN_HMI_STR_COUNT; id++) { + for (unsigned lang = 0; lang < (unsigned)KILN_LANG_COUNT; lang++) { + const char *s = kiln_hmi_str((kiln_hmi_str_id_t)id, (kiln_lang_t)lang); + CHECK_MSG(s != nullptr && s[0] != '\0', + "string %u is missing in language %u", id, lang); + } + } +} + +/* + * @relation(SWR-NFR-23, scope=function) + */ +KILN_TEST(swrnfr23_the_german_uses_only_glyphs_the_font_has) +{ + /* draw.cpp's 5x7 font is the 95 printable ASCII glyphs and nothing else: + * no umlauts, no sharp s. So the German is transliterated -- AE OE UE SS, + * as kiln_core/faults has always done -- and a single umlaut typed into + * the table would reach the panel as a blank nobody sees until the device + * is in a workshop. */ + for (unsigned id = 0; id < (unsigned)KILN_HMI_STR_COUNT; id++) { + for (unsigned lang = 0; lang < (unsigned)KILN_LANG_COUNT; lang++) { + const char *s = kiln_hmi_str((kiln_hmi_str_id_t)id, (kiln_lang_t)lang); + for (const char *p = s; *p != '\0'; p++) { + const unsigned char c = (unsigned char)*p; + CHECK_MSG(c >= 0x20u && c <= 0x7Eu, + "string %u language %u has byte 0x%02X, which the font " + "cannot draw", id, lang, (unsigned)c); + } + } + } +} + +/* + * @relation(SWR-NFR-23, scope=function) + */ +KILN_TEST(swrnfr23_no_label_overruns_the_panel) +{ + /* 21 characters at scale 1, and the framebuffer clips silently past that. + * German runs about 15 per cent longer than English, so this is the bound + * the translation has to be written against rather than something to + * discover on hardware. */ + for (unsigned id = 0; id < (unsigned)KILN_HMI_STR_COUNT; id++) { + for (unsigned lang = 0; lang < (unsigned)KILN_LANG_COUNT; lang++) { + const char *s = kiln_hmi_str((kiln_hmi_str_id_t)id, (kiln_lang_t)lang); + CHECK_MSG(strlen(s) <= KILN_HMI_COLS, + "string %u language %u is %u characters, over the %u the " + "panel has", id, lang, (unsigned)strlen(s), + (unsigned)KILN_HMI_COLS); + } + } +} + +/* + * @relation(SWR-NFR-23, scope=function) + */ +KILN_TEST(swrnfr23_an_unknown_language_falls_back_rather_than_blanking) +{ + /* types.h promises that a language the build does not carry shows English + * rather than an empty screen. A blank menu on a kiln is worse than an + * English one. */ + const char *en = kiln_hmi_str(KILN_HMI_STR_ABORT, KILN_LANG_EN); + CHECK_STR_EQ(kiln_hmi_str(KILN_HMI_STR_ABORT, (kiln_lang_t)47), en); + + /* An identifier out of range is a programming error rather than a + * configuration one, and answers with nothing: a wrong label on a kiln is + * worse than a gap, which at least looks like what it is. */ + CHECK_STR_EQ(kiln_hmi_str((kiln_hmi_str_id_t)KILN_HMI_STR_COUNT, KILN_LANG_EN), ""); +} + +/* + * @relation(SWR-NFR-23, scope=function) + */ +KILN_TEST(swrnfr23_the_screens_themselves_change_language) +{ + /* The table existing is not the point; the screens using it is. Each of + * these is a screen that carried English literals until the table landed, + * and the pixel count moving is what proves the draw site was changed. */ + kiln_hmi_t h; kiln_hmi_init(&h, 0); + kiln_hmi_view_t v = base_view(); + + /* The menu, which is the first screen an operator meets. */ + v.language = KILN_LANG_EN; + (void)feed(&h, &v, KILN_INPUT_PRESS); + CHECK_EQ_INT(h.screen, KILN_HMI_SCREEN_MENU); + const int menu_en = ink(&h, 0, 0, 128, 64); + v.language = KILN_LANG_DE; + (void)feed(&h, &v, KILN_INPUT_NONE); + CHECK_MSG(ink(&h, 0, 0, 128, 64) != menu_en, "the menu did not change"); + + /* The confirmation, where the two answers are the whole screen. */ + v.language = KILN_LANG_EN; + (void)feed(&h, &v, KILN_INPUT_NONE); + (void)feed(&h, &v, KILN_INPUT_PRESS); /* Start program */ + (void)feed(&h, &v, KILN_INPUT_PRESS); /* pick the first */ + CHECK_EQ_INT(h.screen, KILN_HMI_SCREEN_CONFIRM); + const int confirm_en = ink(&h, 0, 0, 128, 64); + v.language = KILN_LANG_DE; + (void)feed(&h, &v, KILN_INPUT_NONE); + CHECK_MSG(ink(&h, 0, 0, 128, 64) != confirm_en, + "the confirmation did not change"); +} + +/* + * @relation(SWR-NET-12, scope=function) + */ +KILN_TEST(swrnet12_a_failed_join_says_so_with_the_radios_reason) +{ + /* Until now the display returned to the network screen saying "not + * connected", leaving the operator to guess between a mistyped + * passphrase, an SSID out of range and a radio that never came up. With + * the display the only route in -- there is no access point -- guessing is + * the entire cost of getting it wrong. */ + kiln_hmi_t h; kiln_hmi_init(&h, 0); + kiln_hmi_view_t v = base_view(); + v.net_up = false; + + (void)feed(&h, &v, KILN_INPUT_PRESS); + for (int i = 0; i < 3; i++) { (void)feed(&h, &v, KILN_INPUT_CW); } + (void)feed(&h, &v, KILN_INPUT_PRESS); + CHECK_EQ_INT(h.screen, KILN_HMI_SCREEN_NETWORK); + + /* Nothing attempted yet: the third row says an attempt is in flight + * rather than that it failed. */ + v.net_last_reason = 0; + (void)feed(&h, &v, KILN_INPUT_NONE); + const int joining = ink(&h, 0, 32, 128, 10); + CHECK(joining > 0); + + /* Reason 15 is a failed four-way handshake, which is a wrong passphrase in + * almost every case. The row has to change, and visibly. */ + v.net_last_reason = 15; + (void)feed(&h, &v, KILN_INPUT_NONE); + const int failed = ink(&h, 0, 32, 128, 10); + CHECK_MSG(failed != joining, + "the network screen shows the same thing whether a join is in " + "progress or has failed"); + + /* The code itself is on the screen: it is there to be read out to somebody + * who can look it up, so a different code must render differently. */ + v.net_last_reason = 201; /* no AP of that name answered */ + (void)feed(&h, &v, KILN_INPUT_NONE); + CHECK_MSG(ink(&h, 0, 32, 128, 10) != failed, + "the reason code is not rendered, only the fact of the failure"); + + /* And once it is up, the row goes back to being the hostname. */ + v.net_up = true; + (void)feed(&h, &v, KILN_INPUT_NONE); + CHECK(ink(&h, 0, 32, 128, 10) > 0); +} + +/* + * @relation(SWR-NET-12, scope=function) + */ +KILN_TEST(swrnet12_the_knob_stays_where_the_last_character_left_it) +{ + /* A rotary encoder is a poor keyboard and 95 characters is a long way + * round, so the one cheap improvement is not sending the knob back to 'a' + * after every press: passphrases repeat characters and cluster inside a + * region of the set, so resuming where the last press landed is usually + * several turns saved and never more. + * + * This is a property of the entry screen that nothing asserted, and it is + * the sort of thing a later edit silently reverses by adding one + * "sensible" reset. + */ + kiln_hmi_t h; kiln_hmi_init(&h, 0); + kiln_hmi_view_t v = base_view(); + v.net_count = 1; + v.net_list_secured[0] = true; + (void)snprintf(v.net_list_ssid[0], KILN_HMI_SSID_LEN, "workshop"); + + (void)feed(&h, &v, KILN_INPUT_PRESS); + for (int i = 0; i < 3; i++) { (void)feed(&h, &v, KILN_INPUT_CW); } + (void)feed(&h, &v, KILN_INPUT_PRESS); /* network screen */ + (void)feed(&h, &v, KILN_INPUT_PRESS); /* the scan list */ + CHECK_EQ_INT(h.screen, KILN_HMI_SCREEN_NETWORKS); + (void)feed(&h, &v, KILN_INPUT_PRESS); /* secured, so type */ + CHECK_EQ_INT(h.screen, KILN_HMI_SCREEN_PASSPHRASE); + + /* Entry starts at the first character, which is right for the first one. */ + CHECK_EQ_UINT(h.charset_sel, 0u); + + for (int i = 0; i < 7; i++) { (void)feed(&h, &v, KILN_INPUT_CW); } + const uint8_t landed = h.charset_sel; + CHECK(landed == 7u); + (void)feed(&h, &v, KILN_INPUT_PRESS); + CHECK_EQ_UINT(h.pass_len, 1u); + CHECK_MSG(h.charset_sel == landed, + "the knob went back to the start after a press, which costs the " + "operator the whole way round again for a repeated character"); + + /* And again, so the second press is not a special case either. */ + (void)feed(&h, &v, KILN_INPUT_PRESS); + CHECK_EQ_UINT(h.pass_len, 2u); + CHECK_EQ_UINT(h.charset_sel, landed); + CHECK_EQ_INT(h.pass[0], h.pass[1]); /* the same character twice */ +} diff --git a/tasklist.md b/tasklist.md index ca82c38..1e3759f 100644 --- a/tasklist.md +++ b/tasklist.md @@ -442,15 +442,21 @@ question is whether it is worth its cost. ## J. German translation, SWR-NFR-23 -- [ ] **J1. The OLED font and encoding are unverified.** The labels avoid - umlauts for that reason, but nobody has yet confirmed what the SSD1306 font - in `kiln_hmi` will carry. If it does handle Latin-1 or a UTF-8 subset, the - labels can use proper German and the test's 16-character bound should be - re-checked against the real glyph widths rather than character count. - -- [ ] **J2. The long causes are not shown on the display yet.** `kiln_hmi` does - not exist, so only the web renders them. When the fault screen lands it will - need to wrap German text, which runs roughly 15 per cent longer than English. +- [x] **J1. The font carries ASCII and nothing else. Answered 2026-10-09.** + Not a hardware question after all: `draw.cpp` defines the 95 printable ASCII + glyphs, so there are no umlauts and no sharp s to draw with, at any encoding. + Every German string in the firmware is therefore transliterated (AE OE UE + SS), and a test over `kiln_hmi/strings` rejects any byte outside 0x20 to 0x7E + so that one umlaut typed into the table cannot reach a workshop as a blank. + The bound that matters is 21 characters, the panel's width at scale 1, and it + is asserted on every string rather than on a sample. + +- [x] **J2. The display renders the long causes. Done.** `kiln_hmi` exists and + the fault screen word-wraps the cause from `kiln_core/faults` in the + configured language, at 21 columns, wrapping on spaces rather than cutting + words -- these sentences are the operator's instructions, and a word split + across two lines in a hurry reads as a different word. German's extra 15 per + cent is absorbed by the wrap rather than by a shorter translation. - [ ] **J3. Config item names and units are still English.** `/api/config` returns keys like `safety.max_temp_c` with English descriptions. The keys @@ -683,17 +689,36 @@ weakness first, the deadline second, the sentence third, the engineering last. through the composition root, persisting credentials before attempting the radio. - What is left: - - - **The German strings** (`J`). The new screens are English-only, which - `SWR-NFR-23` does not allow. - - **A rotary encoder is a poor keyboard**, and 95 characters is a long way - round. If hardware testing finds this painful, the cheapest improvement is - to start the knob on the character the previous press landed on rather than - at `a`, which costs one byte of state. - - **Nothing shows the join failing.** The display returns to the network - screen and the operator infers it from "not connected". A result line, - including the radio's reason code, belongs on that screen. + What was left on 2026-10-09 is now done, except the hardware session: + + - **The display speaks German.** Every screen title, the whole menu, both + answers to a confirmation and every footer were English literals at their + draw sites, so a device set to German announced its faults in German inside + an English frame. They are now one table in `kiln_hmi/strings`, 46 strings + in two languages, with the transliteration `kiln_core/faults` has always + used because the 5x7 font carries the 95 printable ASCII glyphs and nothing + else. That answers `J1` from the code rather than from a hardware session. + Six tests: nothing missing, nothing over the panel's 21 columns, no byte + the font cannot draw, an unknown language falls back to English, and the + menu and the confirmation actually change when the language does. `J4` + still stands: no native speaker has read it. + + - **The knob resumes where the last character left it**, and always did; what + was missing was a test saying so, which is now there. A passphrase repeats + characters and clusters inside a region of the set, so not sending the knob + back to `a` is usually several turns saved and never more, and it is the + sort of property one "sensible" reset silently reverses. + + - **A failed join says so, with the radio's reason code.** `last_reason` was + recorded in the adapter and reported to nobody. The network screen now + distinguishes a join in flight from one the radio refused, and prints the + code: 15 is a failed four-way handshake, which is a wrong passphrase in + almost every case, and 201 is "no AP of that name answered". The number is + there to be read out over a phone to somebody who can look it up, which + beats a sentence that guesses. With the display the only route in, guessing + was the entire cost of getting it wrong. + + What remains is the hardware session of `O3`, and nothing else. --- From 2b3e09f30176cc8558e2afab6b5f7578453f5b3b Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:00:53 +0200 Subject: [PATCH 09/14] feat: /api/events, parked on an async handler rather than holding the server SWR-WEB-05 asks for live values at least once a second by push. The payload has been written and tested since kiln_api_telemetry_event landed, and the browser has been asking all along -- app.js opens an EventSource on /api/events and listens for a `telemetry` event -- but nothing answered, so the dashboard went stale after four seconds exactly as SWR-WEB-25 says it should. The handler does not stream. esp_http_server runs one task and serialises handlers on it, so a handler looping once a second would hold that task for as long as a browser tab stayed open: the other sockets would never be served again and the kiln would appear to hang the moment somebody left a dashboard open and then tried to load the log. So the handler hands the socket to httpd_req_async_handler_begin, parks it in a two-slot table and returns. One task at priority 3 -- below the server's 4, far below control and safety -- pushes to every parked subscriber once a second, with an SSE comment frame every fifteen pushes to stop an idle proxy or a phone's radio dropping a stream that is working. Two subscribers of six sockets. A stream holds its socket indefinitely, so letting every socket become one would leave nothing for the page, the log or an update check; a third is refused with 503 and Retry-After, because a client told to come back does and one left hanging does not. Six rather than four because SWR-WEB-21 counts clients, and a client with the dashboard open holds one socket parked and needs another for everything else. The framing lives in kiln_web/api, not the transport: it is the part of SSE that can be got wrong and the transport cannot be host-tested. A frame that does not fit is not written at all, and a payload containing a newline is refused -- a newline in a data field is SSE's own multi-line syntax, so neither mistake fails visibly, both move the frame boundary into the middle of a JSON document and every frame after it is misread. Five tests, including the device's real telemetry payload framed and re-parsed. Driven against a real client over a socket as well: four frames in three and a half seconds, each event: telemetry with a parseable payload and a rising temperature. That found a bug no unit test would have -- the development harness announced Transfer-Encoding: chunked on the event stream and then sent unframed SSE, a response contradicting its own headers, which a browser may reject outright. The log stream stays chunked; the event stream now declares neither and ends with the connection. The harness also uses the shared framing function now, so the format the browser is tested against is the format the device sends. 26/26 host suites, 47 API tests, clang-tidy 20 clean on host and target (including one real null-deref it found in a new test, where CHECK records and carries on rather than aborting), hardware image builds at 54 % free. --- docs/architecture.md | 33 +++ .../kiln_web/include/kiln_web/api.h | 23 ++ .../components/kiln_web/src/api.cpp | 68 +++++ .../components/kiln_web/src/httpd.cpp | 235 +++++++++++++++++- firmware/controller/host/webhost/webhost.cpp | 29 ++- .../test/host/integration/test_api.cpp | 150 +++++++++++ tasklist.md | 39 ++- 7 files changed, 563 insertions(+), 14 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8dc4538..10afdab 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -876,6 +876,39 @@ The SSE connection drives every live value; on disconnect the UI marks values stale and reconnects with backoff rather than showing old data as current (`SWR-WEB-25`). +**How the stream is served, and why not from the handler.** `esp_http_server` +runs one task and serialises handlers on it, so a handler that looped pushing a +frame a second would hold that task for as long as a browser tab stayed open: +the other sockets would never be served again, and the kiln would appear to +hang the moment somebody left a dashboard open and then tried to load the log. + +So `/api/events` does not stream from its handler. It hands the socket to the +server's async machinery (`httpd_req_async_handler_begin`), parks the request in +a two-slot table and returns, which releases the server task immediately. One +task at priority 3 -- below the server's 4, far below control and safety -- +pushes to every parked subscriber once a second, with an SSE comment frame every +fifteen pushes to keep an idle proxy or a phone's radio from dropping a stream +that is working. + +Two subscribers, not four: a stream holds its socket indefinitely, so letting +every socket become one would leave nothing for the page, the log or an update +check. Two is a dashboard on a phone and one on a laptop. A third is refused +with `503` and `Retry-After` rather than queued, because a client that is told +to come back does and one left hanging does not. The socket count is six rather +than four for the same reason: `SWR-WEB-21` counts *clients*, and a client with +a dashboard open holds one socket parked on the stream and needs another to +fetch anything else. + +The framing is in `kiln_web/api` rather than in the transport, because it is the +part of SSE that can be got wrong and the transport cannot be host-tested. A +frame that does not fit its buffer is not written at all, and a payload +containing a newline is refused: a newline inside a data field is SSE's own way +of writing a multi-line payload, so half a frame or a stray newline does not +fail visibly, it moves the frame boundary into the middle of a JSON document and +every frame after it is misread. The development harness in +`firmware/controller/host/webhost` uses the same framing function, so the format +the browser is tested against is the format the device sends. + ### 12.4 Asset budget | Asset | Budget (gzipped) | Measured 2026-10-09 | diff --git a/firmware/controller/components/kiln_web/include/kiln_web/api.h b/firmware/controller/components/kiln_web/include/kiln_web/api.h index 170b1ca..f391a86 100644 --- a/firmware/controller/components/kiln_web/include/kiln_web/api.h +++ b/firmware/controller/components/kiln_web/include/kiln_web/api.h @@ -145,4 +145,27 @@ bool kiln_api_needs_auth(const kiln_api_req_t *req); * so the UI has one parser. */ size_t kiln_api_telemetry_event(kiln_api_ctx_t *ctx, char *buf, size_t cap); +/* Wrap a payload as one Server-Sent Events frame: an optional `event:` line, + * then `data:`, then the blank line that ends it. Returns the length written, + * or 0 if it did not fit, in which case nothing is written -- half an SSE frame + * on the wire desynchronises the stream for every frame after it, so the only + * safe partial write is none. + * + * `data` must contain no newline. A newline inside a data field is how SSE + * expresses a multi-line payload, so one arriving unannounced would be read by + * the client as a frame boundary in the middle of a JSON document. The JSON + * this device produces has none; the check is here because the consequence of + * being wrong about that is silent and remote. + * + * Here rather than in the transport because it is the part of SSE that can be + * got wrong, and the transport cannot be host-tested (SWA-01). */ +size_t kiln_api_sse_frame(char *buf, size_t cap, const char *event, + const char *data, size_t data_len); + +/* The comment frame SSE uses as a keepalive. A proxy or a phone's radio will + * drop an idle TCP connection, and a stream that has been pushing once a second + * is only idle because the device stopped; this is what keeps the distinction + * visible rather than papering over it. */ +size_t kiln_api_sse_comment(char *buf, size_t cap, const char *text); + #endif diff --git a/firmware/controller/components/kiln_web/src/api.cpp b/firmware/controller/components/kiln_web/src/api.cpp index 9403d76..a6d7530 100644 --- a/firmware/controller/components/kiln_web/src/api.cpp +++ b/firmware/controller/components/kiln_web/src/api.cpp @@ -956,6 +956,74 @@ size_t kiln_api_telemetry_event(kiln_api_ctx_t *ctx, char *buf, size_t cap) return kiln_json_ok(&j) ? kiln_json_len(&j) : 0; } +/* --- Server-Sent Events framing (SWR-WEB-05) ---------------------------- */ + +size_t kiln_api_sse_frame(char *buf, size_t cap, const char *event, + const char *data, size_t data_len) +{ + if ((buf == nullptr) || (cap == 0u) || (data == nullptr)) { + return 0; + } + for (size_t i = 0; i < data_len; i++) { + if ((data[i] == '\n') || (data[i] == '\r')) { + /* Refused rather than escaped. A newline in a data field is SSE's + * own way of writing a multi-line payload, so passing one through + * would move the frame boundary into the middle of a JSON + * document, and every frame after it would be misread. Nothing + * this device serialises contains one; if that changes, the fix is + * at the serialiser rather than a rewrite here. */ + return 0; + } + } + + const size_t ev_len = (event != nullptr) ? strlen(event) : 0u; + /* "event: " + name + "\n" + "data: " + payload + "\n\n", and the NUL that + * makes the result printable in a log. */ + const size_t needed = (ev_len > 0u ? (7u + ev_len + 1u) : 0u) + + 6u + data_len + 2u + 1u; + if (needed > cap) { + return 0; + } + + size_t n = 0; + if (ev_len > 0u) { + memcpy(&buf[n], "event: ", 7u); n += 7u; + memcpy(&buf[n], event, ev_len); n += ev_len; + buf[n++] = '\n'; + } + memcpy(&buf[n], "data: ", 6u); n += 6u; + memcpy(&buf[n], data, data_len); n += data_len; + buf[n++] = '\n'; + buf[n++] = '\n'; + buf[n] = '\0'; + return n; +} + +size_t kiln_api_sse_comment(char *buf, size_t cap, const char *text) +{ + if ((buf == nullptr) || (cap == 0u)) { + return 0; + } + const char *s = (text != nullptr) ? text : ""; + const size_t len = strlen(s); + for (size_t i = 0; i < len; i++) { + if ((s[i] == '\n') || (s[i] == '\r')) { + return 0; + } + } + if ((2u + len + 2u + 1u) > cap) { + return 0; + } + size_t n = 0; + buf[n++] = ':'; + buf[n++] = ' '; + memcpy(&buf[n], s, len); n += len; + buf[n++] = '\n'; + buf[n++] = '\n'; + buf[n] = '\0'; + return n; +} + /* --- dispatch ---------------------------------------------------------- */ kiln_err_t kiln_api_handle(kiln_api_ctx_t *ctx, const kiln_api_req_t *req, diff --git a/firmware/controller/components/kiln_web/src/httpd.cpp b/firmware/controller/components/kiln_web/src/httpd.cpp index 835b7af..78f5360 100644 --- a/firmware/controller/components/kiln_web/src/httpd.cpp +++ b/firmware/controller/components/kiln_web/src/httpd.cpp @@ -47,6 +47,9 @@ #include "esp_http_server.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" #include "kiln_web/httpd.h" @@ -177,6 +180,195 @@ static bool stream_sink(void *user, const char *data, size_t len) return httpd_resp_send_chunk(req, data, (ssize_t)len) == ESP_OK; } +/* --- the telemetry stream (SWR-WEB-05) ---------------------------------- + * + * SWR-WEB-05 asks for live values at least once a second by push rather than + * by polling, and the browser already asks for it: app.js opens an + * EventSource on /api/events and listens for a `telemetry` event. Until now + * nothing answered, so the interface fell back on nothing and the dashboard + * went stale after four seconds exactly as SWR-WEB-25 says it should. + * + * --------------------------------------------------------------------------- + * WHY A TASK, AND NOT A LOOP IN THE HANDLER + * --------------------------------------------------------------------------- + * esp_http_server runs one task and serialises handlers on it. A handler that + * sat in a loop pushing a frame a second would therefore hold that task for as + * long as the browser tab stayed open, and the other three sockets of + * SWR-WEB-21 would never be served again: the kiln would appear to hang the + * moment somebody left a dashboard open and then tried to load the log. + * + * So the handler does not stream. It hands the socket to the server's async + * machinery (httpd_req_async_handler_begin), parks the request in the table + * below, and returns, which frees the server task immediately. One low + * priority task then pushes to every parked subscriber once a second. + * + * Bounded at two subscribers of the four sockets SWR-WEB-21 allows. A stream + * holds its socket indefinitely, so letting every socket become a stream would + * leave nothing for the page itself, the log or an update check; two is a + * dashboard on a phone and one on a laptop, which is the case this is for. + * A third is refused with 503 and a Retry-After rather than queued, because a + * client that is told to come back does, and one left hanging does not. + */ +#define SSE_MAX_SUBS 2 +#define SSE_PERIOD_MS 1000 +#define SSE_BUF_BYTES 2048 +/* A comment frame every fifteen pushes, which is what keeps an idle proxy or a + * phone's radio from dropping a stream that is working. */ +#define SSE_PING_EVERY 15 + +typedef struct { + httpd_req_t *req; /* the async copy; NULL when the slot is free */ + uint32_t pushes; +} sse_sub_t; + +static sse_sub_t s_subs[SSE_MAX_SUBS]; +static SemaphoreHandle_t s_subs_lock; +static TaskHandle_t s_sse_task; +static char s_sse_buf[SSE_BUF_BYTES]; + +/* Drop a subscriber and give its socket back to the server. Called with the + * lock held, from the pushing task only: the handler never closes a request it + * has already parked, because the task may be writing to it. */ +static void sse_release(sse_sub_t *s) +{ + if (s->req != NULL) { + (void)httpd_req_async_handler_complete(s->req); + s->req = NULL; + } + s->pushes = 0; +} + +/* One frame to one subscriber. Returns false when the socket is gone, which + * is the only way a stream ends: the client closes the tab, the radio drops, + * or the page is reloaded, and none of those is an error worth logging at + * anything above debug. */ +static bool sse_push(sse_sub_t *s, const char *frame, size_t len) +{ + const esp_err_t e = httpd_resp_send_chunk(s->req, frame, (ssize_t)len); + return e == ESP_OK; +} + +static void sse_task(void *arg) +{ + (void)arg; + for (;;) { + vTaskDelay(pdMS_TO_TICKS(SSE_PERIOD_MS)); + + if (xSemaphoreTake(s_subs_lock, pdMS_TO_TICKS(100)) != pdTRUE) { + continue; + } + + bool any = false; + for (int i = 0; i < SSE_MAX_SUBS; i++) { + if (s_subs[i].req != NULL) { any = true; break; } + } + if (!any) { + (void)xSemaphoreGive(s_subs_lock); + continue; + } + + /* Serialised once for every subscriber: the payload is identical, and + * building it per socket would be the same work twice on the processor + * that is also running a kiln. */ + char payload[1024]; + const size_t plen = kiln_api_telemetry_event(s_http.api, payload, + sizeof(payload)); + size_t flen = 0; + if (plen > 0u) { + flen = kiln_api_sse_frame(s_sse_buf, sizeof(s_sse_buf), "telemetry", + payload, plen); + } + if (flen == 0u) { + /* Nothing to say, or it did not fit. A half frame would + * desynchronise the stream for good, so the second is skipped as + * silently as the first and SWR-WEB-25 shows the dashboard going + * stale, which is the truth. */ + ESP_LOGW(TAG, "sse: no frame this second (payload %u bytes)", + (unsigned)plen); + (void)xSemaphoreGive(s_subs_lock); + continue; + } + + for (int i = 0; i < SSE_MAX_SUBS; i++) { + sse_sub_t *s = &s_subs[i]; + if (s->req == NULL) { + continue; + } + if (!sse_push(s, s_sse_buf, flen)) { + ESP_LOGD(TAG, "sse: subscriber %d gone after %u pushes", + i, (unsigned)s->pushes); + sse_release(s); + continue; + } + s->pushes++; + if ((s->pushes % SSE_PING_EVERY) == 0u) { + char ping[32]; + const size_t n = kiln_api_sse_comment(ping, sizeof(ping), "ping"); + if ((n > 0u) && !sse_push(s, ping, n)) { + sse_release(s); + } + } + } + (void)xSemaphoreGive(s_subs_lock); + } +} + +/* Park this request as a subscriber. Returns false when there is no room, in + * which case the caller answers rather than this. */ +static bool sse_subscribe(httpd_req_t *req) +{ + if ((s_subs_lock == NULL) || (s_sse_task == NULL)) { + return false; + } + httpd_req_t *copy = NULL; + if (httpd_req_async_handler_begin(req, ©) != ESP_OK) { + return false; + } + + bool parked = false; + if (xSemaphoreTake(s_subs_lock, pdMS_TO_TICKS(100)) == pdTRUE) { + for (int i = 0; i < SSE_MAX_SUBS; i++) { + if (s_subs[i].req == NULL) { + s_subs[i].req = copy; + s_subs[i].pushes = 0; + parked = true; + break; + } + } + (void)xSemaphoreGive(s_subs_lock); + } + if (!parked) { + (void)httpd_req_async_handler_complete(copy); + } + return parked; +} + +static esp_err_t handle_events(httpd_req_t *req) +{ + (void)httpd_resp_set_type(req, "text/event-stream"); + /* No caching and no buffering anywhere in between: a cached event stream is + * a dashboard showing a firing that finished yesterday. */ + (void)httpd_resp_set_hdr(req, "Cache-Control", "no-store"); + (void)httpd_resp_set_hdr(req, "X-Accel-Buffering", "no"); + (void)httpd_resp_set_hdr(req, "X-Content-Type-Options", "nosniff"); + + if (!sse_subscribe(req)) { + /* Full. Said plainly, with how long to wait: app.js reconnects with + * backoff on any error, so a 503 costs the client a second and a + * silent drop costs it the same with no explanation in the log. */ + (void)httpd_resp_set_status(req, status_line(503)); + (void)httpd_resp_set_type(req, "application/json"); + (void)httpd_resp_set_hdr(req, "Retry-After", "5"); + return httpd_resp_sendstr(req, + "{\"error\":{\"code\":\"too_many_streams\"," + "\"message\":\"the device streams to two clients at a time\"}}"); + } + + /* Parked. The first frame comes from the pushing task within the second, + * and returning here releases the server task for everybody else. */ + return ESP_OK; +} + /* --- the single handler ------------------------------------------------- */ static esp_err_t handle_any(httpd_req_t *req) @@ -225,6 +417,12 @@ static esp_err_t handle_any(httpd_req_t *req) resp.body = h->buf; resp.body_cap = sizeof(h->buf); + /* The telemetry stream, which does not answer here at all: it parks the + * socket and returns, so this task is free for the next request. */ + if ((strcmp(uri, "/api/events") == 0) && (ar.method == KILN_HTTP_GET)) { + return handle_events(req); + } + /* The log is the one route that streams: a 24 h run is far larger than any * buffer worth having, so it is pushed in chunks rather than built. */ if (strncmp(uri, "/api/log", 8) == 0 && ar.method == KILN_HTTP_GET) { @@ -277,9 +475,12 @@ kiln_err_t kiln_httpd_start(kiln_api_ctx_t *api) s_http.api = api; httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); - /* SWR-WEB-21: four concurrent clients, which is what the requirement asks - * for and what architecture 13.4's RAM budget was written against. */ - cfg.max_open_sockets = 4; + /* SWR-WEB-21 asks for four concurrent clients. Six sockets, because a + * client with the dashboard open holds one of them parked on the telemetry + * stream and still needs another to fetch the log or the config: four with + * two of them streaming would be the requirement met on paper and not in a + * workshop. Architecture 13.4's RAM budget carries the extra two. */ + cfg.max_open_sockets = 6; cfg.lru_purge_enable = true; cfg.uri_match_fn = httpd_uri_match_wildcard; cfg.max_uri_handlers = 2; @@ -294,6 +495,22 @@ kiln_err_t kiln_httpd_start(kiln_api_ctx_t *api) return KILN_ERR_IO; } + /* SWR-WEB-05's pushing task. Priority 3, below the server's 4 and well + * below control and safety: a frame that is late is a dashboard that + * updates in 1.1 s, and SWR-NFR-02 does not negotiate. */ + if (s_subs_lock == NULL) { + s_subs_lock = xSemaphoreCreateMutex(); + } + if ((s_subs_lock != NULL) && (s_sse_task == NULL)) { + if (xTaskCreatePinnedToCore(sse_task, "kiln_sse", 3072, NULL, 3, + &s_sse_task, 0) != pdPASS) { + s_sse_task = NULL; + /* The interface still works: app.js falls back to showing the + * dashboard going stale, which is SWR-WEB-25 and is honest. */ + ESP_LOGE(TAG, "sse task would not start; /api/events will answer 503"); + } + } + /* SWR-WEB-26: GET and nothing else is ever registered. A POST does not * reach a handler at all; the server answers 405 on its own, and the * error handler below rewrites that into the API's own read_only body so @@ -318,6 +535,18 @@ kiln_err_t kiln_httpd_start(kiln_api_ctx_t *api) void kiln_httpd_stop(void) { + /* The task goes first, and its subscribers with it: stopping the server + * under a task that is writing to one of its sockets is a use-after-free + * with a kiln attached. */ + if (s_sse_task != nullptr) { + vTaskDelete(s_sse_task); + s_sse_task = nullptr; + } + if (s_subs_lock != nullptr) { + for (int i = 0; i < SSE_MAX_SUBS; i++) { + sse_release(&s_subs[i]); + } + } if (s_http.server != nullptr) { (void)httpd_stop(s_http.server); s_http.server = nullptr; diff --git a/firmware/controller/host/webhost/webhost.cpp b/firmware/controller/host/webhost/webhost.cpp index 415a74c..eea07bb 100644 --- a/firmware/controller/host/webhost/webhost.cpp +++ b/firmware/controller/host/webhost/webhost.cpp @@ -209,6 +209,17 @@ bool send_all(int fd, const char *data, size_t len) bool send_str(int fd, const char *s) { return send_all(fd, s, strlen(s)); } +/* The two lengths that are not a length. CHUNKED is the log stream, which is + * framed by chunk_write below; UNFRAMED is the event stream, which is framed by + * SSE itself and ends when the connection closes. + * + * They were one value, -1, and the event stream therefore announced + * Transfer-Encoding: chunked and then sent raw SSE frames -- a response whose + * body does not match its own framing, which a browser is entitled to reject + * outright and which this harness got away with only because nothing checked. */ +constexpr long BODY_CHUNKED = -1; +constexpr long BODY_UNFRAMED = -2; + void send_headers(int fd, int status, const char *content_type, long content_length, const char *extra) { @@ -233,9 +244,11 @@ void send_headers(int fd, int status, const char *content_type, if (content_length >= 0) { n += snprintf(h + n, sizeof(h) - (size_t)n, "Content-Length: %ld\r\n", content_length); - } else { + } else if (content_length == BODY_CHUNKED) { n += snprintf(h + n, sizeof(h) - (size_t)n, "Transfer-Encoding: chunked\r\n"); } + /* BODY_UNFRAMED: neither header. With Connection: close above, the body + * runs until the socket does, which is what an event stream is. */ if (extra != nullptr) { n += snprintf(h + n, sizeof(h) - (size_t)n, "%s", extra); } @@ -379,7 +392,7 @@ void handle_request(client_t *c) /* SWR-WEB-05: the SSE stream. */ if (strcmp(target, "/api/events") == 0 && m == KILN_HTTP_GET) { - send_headers(c->fd, 200, "text/event-stream", -1, + send_headers(c->fd, 200, "text/event-stream", BODY_UNFRAMED, "X-Accel-Buffering: no\r\n"); c->sse = true; return; @@ -415,7 +428,7 @@ void handle_request(client_t *c) * envelope. The device's transport can do better because * esp_http_server lets it set the status after the handler returns; * this is a harness, and the API suite covers the status codes. */ - send_headers(c->fd, 200, "application/json", -1, NULL); + send_headers(c->fd, 200, "application/json", BODY_CHUNKED, NULL); (void)kiln_api_log_stream(&g_api, &req, chunk_write, &sink, &meta); if (sink.ok && meta.streaming) { (void)send_str(c->fd, "0\r\n\r\n"); @@ -449,9 +462,13 @@ void push_sse(void) return; } + /* The same framing the device uses, rather than this harness's own + * snprintf: two implementations of one wire format is how a harness comes + * to pass while the firmware does not, and this one was the implementation + * the browser had actually been tested against. */ char frame[8600]; - const int fn = snprintf(frame, sizeof(frame), "event: telemetry\ndata: %s\n\n", ev); - if (fn <= 0) { + const size_t fn = kiln_api_sse_frame(frame, sizeof(frame), "telemetry", ev, n); + if (fn == 0u) { return; } @@ -459,7 +476,7 @@ void push_sse(void) if (!g_clients[i].in_use || !g_clients[i].sse) { continue; } - if (!send_all(g_clients[i].fd, frame, (size_t)fn)) { + if (!send_all(g_clients[i].fd, frame, fn)) { close(g_clients[i].fd); g_clients[i].in_use = false; } diff --git a/firmware/controller/test/host/integration/test_api.cpp b/firmware/controller/test/host/integration/test_api.cpp index fcd8d1c..2b9b147 100644 --- a/firmware/controller/test/host/integration/test_api.cpp +++ b/firmware/controller/test/host/integration/test_api.cpp @@ -1420,3 +1420,153 @@ KILN_TEST(the_api_validates_its_own_arguments) .body_len = 0, .authenticated = true}; CHECK_ERR(kiln_api_handle(&r.api, &nopath, &resp), KILN_ERR_INVALID_ARG); } + +/* =========================================================================== + * THE EVENT STREAM'S FRAMING (SWR-WEB-05, tasklist P2) + * =========================================================================== + * The payload has been tested since it was written; what was missing was + * anything to send it over. The transport cannot be host-tested -- it is + * esp_http_server sockets and a FreeRTOS task -- so the part of SSE that can + * be got wrong lives here instead: the framing. A frame that is half written, + * or that carries a newline into a data field, does not fail visibly. It + * desynchronises the stream for every frame after it, on a browser in a + * workshop, where nobody is reading a log. + */ + +/* + * @relation(SWR-WEB-05, scope=function) + */ +KILN_TEST(swrweb05_a_frame_is_what_the_browser_is_listening_for) +{ + /* app.js opens an EventSource on /api/events and listens for a named + * `telemetry` event, so the frame has to carry the name and end with the + * blank line that completes it. */ + char buf[128]; + const char *data = "{\"kiln_c\":523.5}"; + const size_t n = kiln_api_sse_frame(buf, sizeof(buf), "telemetry", + data, strlen(data)); + CHECK(n > 0); + CHECK_STR_EQ(buf, "event: telemetry\ndata: {\"kiln_c\":523.5}\n\n"); + CHECK_EQ_UINT(n, strlen(buf)); + + /* Without a name it is still a frame, which is what a client using + * onmessage rather than addEventListener receives. */ + const size_t m = kiln_api_sse_frame(buf, sizeof(buf), nullptr, data, strlen(data)); + CHECK_STR_EQ(buf, "data: {\"kiln_c\":523.5}\n\n"); + CHECK_EQ_UINT(m, strlen(buf)); +} + +/* + * @relation(SWR-WEB-05, scope=function) + */ +KILN_TEST(swrweb05_a_frame_that_does_not_fit_is_not_written_at_all) +{ + /* Half an SSE frame is worse than no frame: the client reads the next + * frame's `event:` line as part of this one's data and never recovers, + * because there is no resynchronisation in the protocol other than a blank + * line that has already been consumed. */ + char buf[32]; + memset(buf, 'x', sizeof(buf)); + const char *data = "{\"this\":\"does not fit in thirty-two bytes at all\"}"; + CHECK_EQ_UINT(kiln_api_sse_frame(buf, sizeof(buf), "telemetry", + data, strlen(data)), 0u); + /* Nothing written: not a prefix, not a terminator. */ + for (size_t i = 0; i < sizeof(buf); i++) { CHECK_EQ_INT(buf[i], 'x'); } + + /* Exactly one byte short still writes nothing. */ + const char *small = "{\"a\":1}"; + const size_t need = strlen("data: ") + strlen(small) + 2u + 1u; + char tight[64]; + CHECK_EQ_UINT(kiln_api_sse_frame(tight, need - 1u, nullptr, small, + strlen(small)), 0u); + CHECK_EQ_UINT(kiln_api_sse_frame(tight, need, nullptr, small, + strlen(small)), need - 1u); +} + +/* + * @relation(SWR-WEB-05, scope=function) + */ +KILN_TEST(swrweb05_a_newline_in_the_payload_is_refused_not_passed_on) +{ + /* A newline inside a data field is SSE's own way of writing a multi-line + * payload, so passing one through would move the frame boundary into the + * middle of a JSON document. Nothing this device serialises contains one; + * the check is here because the consequence of being wrong about that is + * silent and remote. */ + char buf[128]; + const char *lf = "{\"a\":1}\n{\"b\":2}"; + CHECK_EQ_UINT(kiln_api_sse_frame(buf, sizeof(buf), "telemetry", lf, strlen(lf)), 0u); + + const char *cr = "{\"a\":1}\r"; + CHECK_EQ_UINT(kiln_api_sse_frame(buf, sizeof(buf), "telemetry", cr, strlen(cr)), 0u); + + /* And the event name is not a way round it either. */ + CHECK_EQ_UINT(kiln_api_sse_comment(buf, sizeof(buf), "two\nlines"), 0u); +} + +/* + * @relation(SWR-WEB-05, scope=function) + */ +KILN_TEST(swrweb05_the_keepalive_is_a_comment_and_carries_no_data) +{ + /* A proxy or a phone's radio drops an idle TCP connection, and a stream + * pushing once a second is only idle because the device stopped. The + * keepalive has to be invisible to the client's event handlers, which in + * SSE means a comment line. */ + char buf[32]; + const size_t n = kiln_api_sse_comment(buf, sizeof(buf), "ping"); + CHECK(n > 0); + CHECK_STR_EQ(buf, ": ping\n\n"); + CHECK_EQ_UINT(n, strlen(buf)); + CHECK_EQ_INT(buf[0], ':'); /* not "data:", so no event fires */ +} + +/* + * @relation(SWR-WEB-05, scope=function) + */ +KILN_TEST(swrweb05_the_real_telemetry_payload_frames_cleanly) +{ + /* The two halves together, against the payload the device actually + * produces rather than a short literal: this is what proves the status + * JSON has no newline in it, which is the assumption the refusal above + * rests on. */ + static rig_t r; + rig_init(&r); + rig_run(&r, 2.0); + + char payload[1024]; + const size_t plen = kiln_api_telemetry_event(&r.api, payload, sizeof(payload)); + CHECK(plen > 0); + + char frame[2048]; + const size_t flen = kiln_api_sse_frame(frame, sizeof(frame), "telemetry", + payload, plen); + CHECK_MSG(flen > 0, "the device's own telemetry does not frame: %u bytes", + (unsigned)plen); + + /* The frame ends the way SSE requires and nowhere else. */ + CHECK_EQ_INT(frame[flen - 1], '\n'); + CHECK_EQ_INT(frame[flen - 2], '\n'); + CHECK_EQ_UINT(flen, strlen(frame)); + + /* And the data line still parses as the status object, so the UI's one + * parser is one parser. */ + const char *p = strstr(frame, "data: "); + CHECK(p != nullptr); + if (p == nullptr) { + /* CHECK records and carries on, so without this the next line + * dereferences the null it just reported and takes the runner with it, + * turning a failed assertion into a crash with no summary. */ + return; + } + p += 6; + kiln_json_tok_t t[RTOKS]; + const int tn = kiln_json_parse(p, strlen(p) - 2u, t, RTOKS); + CHECK(tn > 0); + CHECK(kiln_json_find(p, t, tn, 0, "kiln_c") > 0); + + /* The transport's buffer is 2 kB and the frame has to fit it with the + * event line on top, which is the sizing this test exists to pin. */ + CHECK_MSG(flen < 2048u, "the frame is %u bytes and httpd's SSE buffer is " + "2048", (unsigned)flen); +} diff --git a/tasklist.md b/tasklist.md index 1e3759f..2b1af9d 100644 --- a/tasklist.md +++ b/tasklist.md @@ -724,11 +724,40 @@ weakness first, the deadline second, the sentence third, the engineering last. ## P. HTTP transport and the API -- [ ] **P2. Server-Sent Events are not implemented.** `SWR-WEB-05` wants live - values pushed at least once a second, and `kiln_api_telemetry_event()` is - written and tested for exactly that, but the transport has no `/api/events` - handler. The UI falls back to nothing: it reads `/api/status` on a timer - already, so this is a refinement rather than a gap in function. +- [x] **P2. The telemetry stream is implemented. Done 2026-10-10.** The + payload had been written and tested since `kiln_api_telemetry_event` landed, + and the browser had been asking for it all along: `app.js` opens an + `EventSource` on `/api/events` and listens for a `telemetry` event. Nothing + answered, so the dashboard went stale after four seconds exactly as + `SWR-WEB-25` says it should. + + It does not stream from the handler, and that is the whole design. + `esp_http_server` runs one task and serialises handlers on it, so a handler + looping once a second would hold that task for as long as a browser tab + stayed open and the kiln would appear to hang the moment somebody left a + dashboard open and then loaded the log. Instead the handler parks the socket + with `httpd_req_async_handler_begin` and returns; one task at priority 3 + pushes to at most two parked subscribers once a second, with a comment frame + every fifteen pushes so an idle proxy does not drop a stream that is working. + A third subscriber gets `503` with `Retry-After`. Six sockets rather than + four, because `SWR-WEB-21` counts clients and a client with the dashboard + open needs a second socket for everything else. + + The framing is in `kiln_web/api`, host-tested, and shared with the + development harness: a frame that does not fit is not written at all and a + payload with a newline is refused, because half a frame does not fail + visibly, it moves the frame boundary into the middle of a JSON document and + every frame after it is misread. Five tests. + + Verified against a real client, not only by unit test: the harness served + four frames in three and a half seconds over a socket, each + `event: telemetry` with a parseable payload and a rising temperature. That + also found a bug nothing else would have: the harness announced + `Transfer-Encoding: chunked` on the event stream and then sent unframed SSE, + a response whose body contradicts its own headers, which a browser may + reject outright. The log stream is chunked and stays chunked; the event + stream now declares neither length nor chunking and ends with the + connection, which is what an event stream is. - [ ] **P3. The program store has no writer, and 17 of its 20 slots have no purpose.** Answered rather than open: `OQ-09` is resolved, the profiles are From fc2ba4f8de19900fdd0beb44c8f4250b7e1fa47c Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:14:22 +0200 Subject: [PATCH 10/14] refactor: the programs are firmware, so the program store goes P3, finished the way the pre-compiled-profiles decision implied. SWR-PRG-04 asked for 20 program slots in non-volatile storage, and with SWR-PRG-07, SWR-PRG-08 and SWR-PRG-10 withdrawn there was nothing on the device that could create or edit one: the store held a copy of the three examples that arrive in the image and nothing else. Persistence of something already persistent, in a medium that wears out, behind a seeding path that could fail on a first boot and a store that could fail to mount. It is withdrawn, program_store is deleted, and the API, the HMI and the development harness all read kiln_profile_example. What that fixed, which is why this is not only a deletion: - **The local start path worked for the first time.** KILN_HMI_ACT_START returned KILN_ERR_NOT_FOUND on hardware with a note that there was no program store, and the view builder never filled the program list. So a device carrying three firing profiles in its own flash showed "none stored", and the first entry of its menu did nothing when pressed. Both are now wired to the compiled-in profiles: SWR-HMI-10 delivered rather than written. - A device whose flash will not mount still has its programs. The list used to answer 503 for programs the firmware was carrying. - A factory reset no longer costs the operator their programs. The partition holds run history, and losing history is what a reset is for. - The first boot has nothing to get wrong. About 200 lines and nine tests went. The properties worth keeping were re-pointed rather than dropped: every compiled-in example is a valid program with a distinct name and an index past the end is refused (it comes off a URL); the atomic-replace-across-a-power-cut property is now driven through run_index, which is what still writes to the store and does rewrite a slot when it marks a run truncated; and the partition-sharing test became the measurement that 20 of 64 regions are used. /api/storage reports the program count outside "files", because programs are not files any more and saying they were said the opposite of what is true. Q2's mount cost halves with the medium: 488 reads and 31 888 bytes, from 768 and 47 808. SWR-PRG-05 stays, and the reason is safety.max_temp_c: the programs are trusted as data, but an operator can lower the ceiling below a program's peak, and validation on start is what refuses that firing rather than running it into a limit. A test now asserts every example validates against the default ceiling, because an example that cannot start is one an operator discovers by selecting it. 26/26 host suites, the same under ASan/UBSan, kiln_core line coverage 91.8 % against the 90 % floor, clang-tidy 20 clean on host and target, both IDF configurations build, StrictDoc exports clean, and the harness still serves the three programs over HTTP with 404 past the end. --- docs/03_software_req.sdoc | 16 +- docs/architecture.md | 41 +-- .../components/kiln_app/CMakeLists.txt | 1 - .../kiln_app/include/kiln_app/program_store.h | 47 --- .../components/kiln_app/src/app.cpp | 7 +- .../components/kiln_app/src/program_store.cpp | 267 ------------------ .../components/kiln_web/src/api.cpp | 40 +-- firmware/controller/host/webhost/webhost.cpp | 10 +- firmware/controller/main/main.cpp | 38 ++- .../test/host/integration/test_api.cpp | 5 +- .../host/integration/test_persistence.cpp | 30 +- .../host/integration/test_stores_on_flash.cpp | 144 +++------- .../test/host/unit/test_fileslots.cpp | 47 ++- .../controller/test/host/unit/test_stores.cpp | 227 +++++---------- tasklist.md | 64 +++-- 15 files changed, 300 insertions(+), 684 deletions(-) delete mode 100644 firmware/controller/components/kiln_app/include/kiln_app/program_store.h delete mode 100644 firmware/controller/components/kiln_app/src/program_store.cpp diff --git a/docs/03_software_req.sdoc b/docs/03_software_req.sdoc index 936507b..6da98b5 100644 --- a/docs/03_software_req.sdoc +++ b/docs/03_software_req.sdoc @@ -787,7 +787,19 @@ VERIFICATION: T [REQUIREMENT] UID: SWR-PRG-04 STATEMENT: >>> -The system shall store at least **20** programs in non-volatile storage, persisting across power loss and firmware update. +**Withdrawn 2026-10-10.** This required at least 20 programs in non-volatile storage, persisting across power loss and firmware update. + +It was the last requirement standing on the authoring path that [LINK: SWR-PRG-07], [LINK: SWR-PRG-08] and [LINK: SWR-PRG-10] were withdrawn with. With nothing on the device able to create or edit a program, the storage held a copy of the three examples that arrive in the image ([LINK: SWR-PRG-09]) and nothing else: persistence of something already persistent, in a medium that wears out, behind a seeding path that could fail on a first boot and a store that could fail to mount. + +So the programs are read from the firmware. What that *removes* is worth stating, because this is a deletion that makes the device better rather than smaller: + +- A device whose flash will not mount now still has its programs. It used to serve `503` for the list of programs it was carrying in its own image. +- A factory reset ([LINK: SWR-CFG-09]) no longer costs the operator their programs. The partition holds run history, and losing history is what a factory reset is for. +- The first boot has nothing to get wrong. Seeding was idempotent and tested, and it was still a write to flash on every power-on of a brand-new device. + +"Persisting across a firmware update" is now trivially true in the only sense left: the programs are part of what the update installs. A user who wants a different curve edits `kiln_core/profile` and builds, which is the cost [LINK: SWR-PRG-09] states plainly. + +The 20-slot figure is gone with the requirement. `kiln_core/fileslots` still carries 64 regions and the run index still uses 20 of them ([LINK: SWR-LOG-09]), so the partition is now a third used rather than two thirds. That is recorded in architecture 10.6 and deliberately not acted on: shrinking the partition is a flash-layout change that breaks every device already flashed, to recover space nothing needs. <<< PRIORITY: M VERIFICATION: T @@ -796,6 +808,8 @@ VERIFICATION: T UID: SWR-PRG-05 STATEMENT: >>> The system shall validate a program on start, rejecting it with a specific reason if: no segments; any target above the configured maximum temperature; any field out of range; total predicted duration above 168 h. + +**Still required after [LINK: SWR-PRG-04]'s withdrawal, and the reason is the configured maximum.** The programs are compiled in and therefore trusted as *data*, but `safety.max_temp_c` is set by the operator and can be lowered below a program's peak; validation on start is what refuses that firing rather than running it into a ceiling. A test asserts that every compiled-in example validates against the default maximum, because an example that cannot start is one an operator discovers by selecting it. <<< PRIORITY: M VERIFICATION: T diff --git a/docs/architecture.md b/docs/architecture.md index 10afdab..f3dacc5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -167,7 +167,6 @@ double is a compile-time-checked substitution. | `telemetry` | Maintains the current published snapshot for queries, lock-free for readers. | | `logger_task` | Drains the log queue to `port_logstore`; the only component that blocks on flash. | | `settings` | Configuration load, validate, migrate, apply, persist; marks reboot-required items. | -| `program_store` | Program CRUD over `port_filestore`, atomic replace, seeding of read-only examples. | | `run_index` | Run record persistence and the 20-run retention policy. | ### 5.4 Presentation @@ -658,8 +657,9 @@ decimated series equal the extrema of the full series. ### 10.6 The file store -`kilnfs` holds programs and run records as a fixed array rather than a -filesystem (`SWA-21`). The partition divides into equal **regions of two erase +`kilnfs` holds run records as a fixed array rather than a filesystem +(`SWA-21`). It held programs too until `SWR-PRG-04` was withdrawn; the store +below is unchanged, and what changed is that only one caller writes to it. The partition divides into equal **regions of two erase sectors**, 64 regions in 512 kB. One file occupies one region and alternates between its two sectors, so the copy being replaced is never the copy being erased. @@ -695,10 +695,14 @@ path returns `KILN_ERR_INVALID_ARG`. Both callers produce five-character slot paths, so nothing generates one today; refusing is the answer because storing a file under a name the caller did not ask for is worse than failing. -Capacity is 40 of 64 regions for the 20 programs of `SWR-PRG-04` and the 20 run -records of `SWR-LOG-09`. Endurance is generous for the same reason the log's is: -a program is written when a user saves it and a run record once per firing, so a -region sees single-digit erases per year against a 100 000 cycle rating. +Capacity is 20 of 64 regions, for the run records of `SWR-LOG-09` and nothing +else. It was 40, half of them programs, until `SWR-PRG-04` was withdrawn and the +firing profiles moved into the image: the partition is now a third used rather +than two thirds. That is recorded and deliberately not acted on, because +shrinking it is a flash-layout change that breaks every device already flashed, +to recover space nothing needs. Endurance is generous for the same reason the +log's is: a run record is written once per firing, so a region sees single-digit +erases per year against a 100 000 cycle rating. **Wear is handled at the write rather than at the read.** A NOR sector at the end of its life does not report an error: it takes the write, returns success, @@ -727,8 +731,8 @@ for nothing. Retirement is in because its failure mode is losing a run record, which is cheap to prevent and annoying to explain. **What a mount costs**, measured in `test_fileslots` rather than estimated: the -medium this firmware produces, 20 programs of 398 bytes and 20 run records of -554, costs **768 reads and 47 808 bytes** of CRC. A partition full of +medium this firmware produces, 20 run records of 554 bytes, costs **488 reads +and 31 888 bytes** of CRC. A partition full of maximum-size files, both copies valid, costs **8 192 reads and 524 288 bytes**, which is the whole partition. Both are paid once, at start-up, and the expectation is tens of milliseconds rather than a budget item; if a board ever @@ -743,9 +747,9 @@ survives a remount, a cut before the commit leaves the previous copy, a cut inside the commit header leaves the previous copy, a sector that lies about a write retires its region and the file is still readable, a region that cannot be erased stays retired across a reboot, and a part with nowhere left to write says -so instead of losing the previous content. The same fake drives `program_store` -and `run_index` over the real store in `test_stores_on_flash`, which is the -combination that runs on the board. +so instead of losing the previous content. The same fake drives `run_index` +over the real store in `test_stores_on_flash`, which is the combination that +runs on the board. ### 10.7 What the buzzer sounds like @@ -803,9 +807,8 @@ set (`SWR-WEB-23`). | `GET` | `/api/info` | Version, git revision, build time, target, uptime, gains + provenance | `SWR-UPD-06` | | `GET` `PUT` | `/api/config` | Read / atomically write configuration | `SWR-CFG-03` | | `POST` | `/api/config/defaults` | Restore defaults | `SWR-CFG-06` | -| `GET` `POST` | `/api/programs` | List / create | `SWR-PRG-07` | -| `GET` `PUT` `DELETE` | `/api/programs/{id}` | Read / update / delete | `SWR-PRG-07` | -| `POST` | `/api/programs/{id}/copy` | Duplicate | `SWR-PRG-07` | +| `GET` | `/api/programs` | List the programs compiled into the firmware | `SWR-PRG-09` | +| `GET` | `/api/programs/{id}` | Read one; an id past the end is `404` | `SWR-PRG-09` | | `GET` | `/api/programs/{id}/preview` | Predicted setpoint curve, duration, end time | `SWR-PRG-06` | | `POST` | `/api/run` | Start `{program_id}` | `SWR-RUN-02` | | `POST` | `/api/run/pause` `resume` `abort` `ack` | Run control and segment acknowledgement | `SWR-RUN-03/04`, `SWR-PRG-03` | @@ -1035,8 +1038,8 @@ Five properties of this that are decisions rather than details: is enforced in `kiln_app`, where the state machine lives, not in the port. 5. **Rollback is cancelled late** (`SWR-UPD-15`). The first boot of a new image stays pending-verify until the supervisor link, both thermocouples, the - configuration, the stored programs and the display have all proved themselves - in the running system. `confirm_running()` in start-up code would make + configuration and the display have all proved themselves in the running + system. `confirm_running()` in start-up code would make `SWR-UPD-02` decoration. #### The manifest @@ -1264,7 +1267,7 @@ Requirement-level traceability is maintained mechanically by `tools/trace` | `FR-CUR` | `core/current`, `hal/adc_ct`, `current` task | Unit, integration, driver, HIL | | `FR-CTL` | `core/pid`, `core/setpoint`, `core/window`, `control` task, `heat_window` | Unit, integration, driver (timing) | | `FR-TUN` | `core/autotune` | Unit, integration across plant sets | -| `FR-PRG` | `core/profile`, `app/program_store` | Unit, API | +| `FR-PRG` | `core/profile` (the programs are compiled in) | Unit, API | | `FR-RUN` | `app/run_controller`, `core/runstate` | Unit, integration (incl. power loss) | | `FR-HMI` | `kiln_hmi`, `hal/ssd1306`, `hal/encoder` | Golden image, driver, demonstration | | `FR-WEB` | `kiln_web`, `web/` | API, browser demonstration | @@ -1304,7 +1307,7 @@ Requirement-level traceability is maintained mechanically by `tools/trace` | **M3, Control** | `pid`, `window`, `setpoint`, `profile`, `control` task, heat output. | A program runs closed-loop against the simulator to `SWR-NFR-05`. | | **M4, Safety** | `core/safety`, safety task, charge-pump enable, latching, watchdogs. | Every rule in [§8.2](#82-rule-table) has a passing automated test; HIL confirms contactor release. | | **M4b, Current** | `core/current`, CT adapter, `SWR-SAF-25`–`SWR-SAF-30`, weld discrimination. | Every current rule has a passing automated test; HIL confirms `SWR-SAF-27` against an emulated welded contactor. | -| **M5, Persist** | Log ring, run index, programs, configuration, power-loss recovery. | `FR-LOG`, `FR-CFG`, `SWR-RUN-08` pass; endurance analysis confirmed by measurement. | +| **M5, Persist** | Log ring, run index, configuration, power-loss recovery. | `FR-LOG`, `FR-CFG`, `SWR-RUN-08` pass; endurance analysis confirmed by measurement. | | **M6, Web** | HTTP server, REST API, SSE, dashboard, chart, program editor, settings, OTA. | `FR-WEB`, `FR-UPD` pass; API suite green. | | **M7, Tune** | `autotune` and its UI. | `FR-TUN` passes across the simulator's plant parameter sweep. | | **M8, Commission** | Soak test, timing report, documentation, real kiln firing. | `SWR-NFR-10`, `SWR-TST-18` reports published; a real firing completed and logged. | diff --git a/firmware/controller/components/kiln_app/CMakeLists.txt b/firmware/controller/components/kiln_app/CMakeLists.txt index fe8674e..2feda20 100644 --- a/firmware/controller/components/kiln_app/CMakeLists.txt +++ b/firmware/controller/components/kiln_app/CMakeLists.txt @@ -3,7 +3,6 @@ set(KILN_APP_SRCS src/app.cpp - src/program_store.cpp src/run_index.cpp src/settings.cpp ) diff --git a/firmware/controller/components/kiln_app/include/kiln_app/program_store.h b/firmware/controller/components/kiln_app/include/kiln_app/program_store.h deleted file mode 100644 index 9446e11..0000000 --- a/firmware/controller/components/kiln_app/include/kiln_app/program_store.h +++ /dev/null @@ -1,47 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Bitcrush Testing - * SPDX-License-Identifier: GPL-3.0-or-later - * - * Stored firing programs -- SWR-PRG-04, SWR-PRG-07, SWR-PRG-08, SWR-PRG-09, - * SWA-10 (programs in LittleFS: they are user files that want names, import and - * export, and atomic replace). - * - * Addressed by slot rather than by a filename derived from the name. Names - * arrive from the network (SWR-NFR-19), and turning untrusted UTF-8 into a path is a - * directory-traversal bug waiting to be written; a fixed set of slots has no - * such surface, and the name lives inside the file where it is data rather than - * structure. Twenty slots is SWR-PRG-04's limit, so a lookup by name is a scan - * of twenty short reads -- nothing worth an index that could disagree. - */ -#ifndef KILN_APP_PROGRAM_STORE_H -#define KILN_APP_PROGRAM_STORE_H - -#include "kiln/err.h" -#include "kiln/types.h" -#include "kiln_ports/port_filestore.h" - -#define KILN_PROGRAM_SLOTS KILN_MAX_PROGRAMS /* SWR-PRG-04 */ - -/* Seed the built-in read-only examples if they are not present (SWR-PRG-09). - * Idempotent, so it can run on every boot. */ -kiln_err_t kiln_program_store_seed(const kiln_port_filestore_t *fs); - -/* Save under its own name, replacing an existing program of that name. - * - * Validates first (SWR-PRG-05), refuses to overwrite a read-only example - * (SWR-PRG-09), and returns KILN_ERR_NO_SPACE when all slots are taken. */ -kiln_err_t kiln_program_store_save(const kiln_port_filestore_t *fs, - const kiln_program_t *p, float max_temp_c); - -kiln_err_t kiln_program_store_load(const kiln_port_filestore_t *fs, - const char *name, kiln_program_t *out); - -/* Refuses a read-only example (SWR-PRG-09). */ -kiln_err_t kiln_program_store_delete(const kiln_port_filestore_t *fs, const char *name); - -/* Read a slot directly, for enumeration. KILN_ERR_NOT_FOUND for an empty slot. */ -kiln_err_t kiln_program_store_get_slot(const kiln_port_filestore_t *fs, - uint8_t slot, kiln_program_t *out); - -uint8_t kiln_program_store_count(const kiln_port_filestore_t *fs); - -#endif diff --git a/firmware/controller/components/kiln_app/src/app.cpp b/firmware/controller/components/kiln_app/src/app.cpp index 5ac98a5..dddb4aa 100644 --- a/firmware/controller/components/kiln_app/src/app.cpp +++ b/firmware/controller/components/kiln_app/src/app.cpp @@ -3,7 +3,6 @@ #include #include "kiln_app/app.h" -#include "kiln_app/program_store.h" #include "kiln_app/run_index.h" #include "kiln_app/settings.h" @@ -856,10 +855,10 @@ kiln_err_t kiln_app_boot(kiln_app_t *app, kiln_reset_cause_t cause, float outage } } - /* SWR-PRG-09: the examples, idempotently. */ + /* The file store now holds run records and nothing else: SWR-PRG-09's + * examples are compiled into the image and read from there, so there is + * nothing to seed and no copy of them on the medium to go stale. */ if (app->ports.filestore != nullptr) { - (void)kiln_program_store_seed(app->ports.filestore); - /* Run numbering continues across a reboot rather than restarting and * colliding with records already on disk. */ app->next_run_id = kiln_run_index_next_run_id(app->ports.filestore); diff --git a/firmware/controller/components/kiln_app/src/program_store.cpp b/firmware/controller/components/kiln_app/src/program_store.cpp deleted file mode 100644 index e69ad20..0000000 --- a/firmware/controller/components/kiln_app/src/program_store.cpp +++ /dev/null @@ -1,267 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Bitcrush Testing - * SPDX-License-Identifier: GPL-3.0-or-later */ - -#include -#include -#include "kiln_app/program_store.h" -#include "kiln_core/logrec.h" /* kiln_crc16 */ -#include "kiln_core/profile.h" - -constexpr uint32_t PROG_MAGIC = 0x47525021U; /* "!PRG" */ -#define PROG_BLOB_BYTES (8u + sizeof(kiln_program_t) + 2u) - -namespace { - -void slot_path(uint8_t slot, char out[KILN_PATH_MAX]) -{ - /* slot is a uint8_t, so the longest result is "/p/255": 6 characters into - * KILN_PATH_MAX (64). Truncation is unreachable, which is the handling - * SWR-NFR-17 asks for here -- stated rather than checked at runtime. */ - (void)snprintf(out, KILN_PATH_MAX, "/p/%02u", (unsigned)slot); -} - -kiln_err_t encode(const kiln_program_t *p, uint8_t *out, size_t cap, size_t *len) -{ - if (cap < PROG_BLOB_BYTES) { - return KILN_ERR_NO_SPACE; - } - - memset(out, 0, PROG_BLOB_BYTES); - out[0] = (uint8_t)PROG_MAGIC; - out[1] = (uint8_t)(PROG_MAGIC >> 8u); - out[2] = (uint8_t)(PROG_MAGIC >> 16u); - out[3] = (uint8_t)(PROG_MAGIC >> 24u); - out[4] = (uint8_t)KILN_PROGRAM_SCHEMA_VERSION; - out[5] = (uint8_t)(sizeof(kiln_program_t)); - out[6] = (uint8_t)(sizeof(kiln_program_t) >> 8u); - out[7] = 0; - memcpy(&out[8], p, sizeof(*p)); - - const uint16_t crc = kiln_crc16(out, PROG_BLOB_BYTES - 2); - out[PROG_BLOB_BYTES - 2] = (uint8_t)crc; - out[PROG_BLOB_BYTES - 1] = (uint8_t)(crc >> 8u); - - *len = PROG_BLOB_BYTES; - return KILN_OK; -} - -kiln_err_t decode(const uint8_t *in, size_t len, kiln_program_t *out) -{ - if (len != PROG_BLOB_BYTES) { - return KILN_ERR_CORRUPT; - } - - const uint32_t magic = (uint32_t)in[0] | ((uint32_t)in[1] << 8u) | - ((uint32_t)in[2] << 16u) | ((uint32_t)in[3] << 24u); - if (magic != PROG_MAGIC) { - return KILN_ERR_CORRUPT; - } - - const uint16_t crc = (uint16_t)((uint32_t)in[PROG_BLOB_BYTES - 2] | - ((uint32_t)in[PROG_BLOB_BYTES - 1] << 8u)); - if (kiln_crc16(in, PROG_BLOB_BYTES - 2) != crc) { - return KILN_ERR_CORRUPT; - } - - memcpy(out, &in[8], sizeof(*out)); - - /* SWR-NFR-19: this came off storage, so it is untrusted in exactly the way a - * network body is. Force termination before anything reads the strings. */ - kiln_profile_terminate_strings(out); - return KILN_OK; -} - -kiln_err_t read_slot(const kiln_port_filestore_t *fs, uint8_t slot, - kiln_program_t *out) -{ - char path[KILN_PATH_MAX]; - slot_path(slot, path); - - uint8_t blob[PROG_BLOB_BYTES]; - size_t len = 0; - const kiln_err_t e = fs->read(fs->ctx, path, blob, sizeof(blob), &len); - if (e != KILN_OK) { - return e; - } - return decode(blob, len, out); -} - -kiln_err_t write_slot(const kiln_port_filestore_t *fs, uint8_t slot, - const kiln_program_t *p) -{ - uint8_t blob[PROG_BLOB_BYTES]; - size_t len = 0; - const kiln_err_t e = encode(p, blob, sizeof(blob), &len); - if (e != KILN_OK) { - return e; - } - - char path[KILN_PATH_MAX]; - slot_path(slot, path); - /* Atomic by rename, so a power cut leaves the previous program rather than - * half of the new one. */ - return fs->write_atomic(fs->ctx, path, blob, len); -} - -/* Slot holding `name`, or KILN_PROGRAM_SLOTS if there is none. */ -uint8_t find_by_name(const kiln_port_filestore_t *fs, const char *name, - bool *readonly_out) -{ - for (uint8_t slot = 0; slot < KILN_PROGRAM_SLOTS; slot++) { - kiln_program_t p; - if (read_slot(fs, slot, &p) != KILN_OK) { - continue; - } - if (strncmp(p.name, name, KILN_PROGRAM_NAME_LEN) == 0) { - if (readonly_out != nullptr) { - *readonly_out = (p.flags & KILN_PROG_FLAG_READONLY) != 0; - } - return slot; - } - } - return KILN_PROGRAM_SLOTS; -} - -uint8_t first_free_slot(const kiln_port_filestore_t *fs) -{ - for (uint8_t slot = 0; slot < KILN_PROGRAM_SLOTS; slot++) { - kiln_program_t p; - if (read_slot(fs, slot, &p) != KILN_OK) { - return slot; - } - } - return KILN_PROGRAM_SLOTS; -} - -} // namespace - -kiln_err_t kiln_program_store_seed(const kiln_port_filestore_t *fs) -{ - if ((fs == nullptr) || (fs->read == nullptr) || (fs->write_atomic == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - - const uint8_t n = kiln_profile_example_count(); - for (uint8_t i = 0; i < n; i++) { - kiln_program_t p; - kiln_err_t e = kiln_profile_example(i, &p); - if (e != KILN_OK) { - return e; - } - - /* Idempotent: an example the operator has not deleted is left exactly - * as it is, so seeding on every boot costs one read per example. */ - bool ro = false; - if (find_by_name(fs, p.name, &ro) != KILN_PROGRAM_SLOTS) { - continue; - } - - const uint8_t slot = first_free_slot(fs); - if (slot >= KILN_PROGRAM_SLOTS) { - return KILN_ERR_NO_SPACE; - } - - e = write_slot(fs, slot, &p); - if (e != KILN_OK) { - return e; - } - } - return KILN_OK; -} - -kiln_err_t kiln_program_store_save(const kiln_port_filestore_t *fs, - const kiln_program_t *p, float max_temp_c) -{ - if ((fs == nullptr) || (fs->read == nullptr) || (fs->write_atomic == nullptr) || - (p == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - - /* SWR-PRG-05 before anything reaches storage. */ - const kiln_prog_validation_t v = kiln_profile_validate(p, max_temp_c); - if (v.code != KILN_PROG_OK) { - return KILN_ERR_RANGE; - } - - bool ro = false; - uint8_t slot = find_by_name(fs, p->name, &ro); - if (slot < KILN_PROGRAM_SLOTS) { - /* SWR-PRG-09: the built-in examples are read-only. Saving over one would - * leave the operator with no way back to a known-good program. */ - if (ro) { - return KILN_ERR_STATE; - } - } else { - slot = first_free_slot(fs); - if (slot >= KILN_PROGRAM_SLOTS) { - return KILN_ERR_NO_SPACE; - } - } - return write_slot(fs, slot, p); -} - -kiln_err_t kiln_program_store_load(const kiln_port_filestore_t *fs, - const char *name, kiln_program_t *out) -{ - if ((fs == nullptr) || (fs->read == nullptr) || (name == nullptr) || (out == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - - for (uint8_t slot = 0; slot < KILN_PROGRAM_SLOTS; slot++) { - kiln_program_t p; - if (read_slot(fs, slot, &p) != KILN_OK) { - continue; - } - if (strncmp(p.name, name, KILN_PROGRAM_NAME_LEN) == 0) { - *out = p; - return KILN_OK; - } - } - return KILN_ERR_NOT_FOUND; -} - -kiln_err_t kiln_program_store_delete(const kiln_port_filestore_t *fs, const char *name) -{ - if ((fs == nullptr) || (fs->read == nullptr) || (fs->remove == nullptr) || (name == nullptr)) { - return KILN_ERR_INVALID_ARG; - } - - bool ro = false; - const uint8_t slot = find_by_name(fs, name, &ro); - if (slot >= KILN_PROGRAM_SLOTS) { - return KILN_ERR_NOT_FOUND; - } - if (ro) { - return KILN_ERR_STATE; /* SWR-PRG-09 */ - } - - char path[KILN_PATH_MAX]; - slot_path(slot, path); - return fs->remove(fs->ctx, path); -} - -kiln_err_t kiln_program_store_get_slot(const kiln_port_filestore_t *fs, - uint8_t slot, kiln_program_t *out) -{ - if ((fs == nullptr) || (fs->read == nullptr) || (out == nullptr) || - slot >= KILN_PROGRAM_SLOTS) { - return KILN_ERR_INVALID_ARG; - } - return read_slot(fs, slot, out); -} - -uint8_t kiln_program_store_count(const kiln_port_filestore_t *fs) -{ - if ((fs == nullptr) || (fs->read == nullptr)) { - return 0; - } - - uint8_t n = 0; - for (uint8_t slot = 0; slot < KILN_PROGRAM_SLOTS; slot++) { - kiln_program_t p; - if (read_slot(fs, slot, &p) == KILN_OK) { - n++; - } - } - return n; -} diff --git a/firmware/controller/components/kiln_web/src/api.cpp b/firmware/controller/components/kiln_web/src/api.cpp index a6d7530..4cf58a0 100644 --- a/firmware/controller/components/kiln_web/src/api.cpp +++ b/firmware/controller/components/kiln_web/src/api.cpp @@ -4,7 +4,6 @@ #include #include #include -#include "kiln_app/program_store.h" #include "kiln_app/run_index.h" #include "kiln_core/faults.h" #include "kiln_core/profile.h" @@ -687,22 +686,25 @@ kiln_err_t handle_tune(kiln_api_ctx_t *ctx, const kiln_api_req_t *req, /* --- /api/runs, /api/current, /api/storage, /api/net ------------------- */ -/* SWR-PRG-07: the stored programs, read only. */ +/* SWR-PRG-09: the programs, read only and compiled in. + * + * No storage check any more, and that is the point of the change: these came + * from the file store, which meant a device whose flash would not mount served + * 503 for the list of programs it was carrying in its own image. The + * programs are now exactly as available as the firmware is. */ kiln_err_t handle_program_list(kiln_api_ctx_t *ctx, kiln_api_resp_t *resp) { - if (ctx->filestore == nullptr) { - kiln_api_error(resp, 503, "no_storage", "program storage is unavailable"); - return KILN_ERR_IO; - } kiln_json_t j; resp_begin(resp, &j); kiln_json_obj_open(&j); kiln_json_key(&j, "programs"); kiln_json_arr_open(&j); - const uint8_t n = kiln_program_store_count(ctx->filestore); + /* SWR-PRG-09, and now the whole of it: the programs are compiled into the + * image, so this enumerates the firmware rather than the medium. */ + const uint8_t n = kiln_profile_example_count(); for (uint8_t i = 0; i < n; i++) { kiln_program_t prog; - if (kiln_program_store_get_slot(ctx->filestore, i, &prog) != KILN_OK) { + if (kiln_profile_example(i, &prog) != KILN_OK) { continue; } write_program(&j, &prog, i, ctx->app->cfg.max_temp_c); @@ -712,19 +714,15 @@ kiln_err_t handle_program_list(kiln_api_ctx_t *ctx, kiln_api_resp_t *resp) return resp_end(resp, &j); } -/* One stored program by slot. A missing slot is 404 and not an empty list: - * a client asking for a program that is not there has made a different - * mistake from one asking what programs exist. */ +/* One program by id. An id past the end is 404 and not an empty list: a + * client asking for a program that is not there has made a different mistake + * from one asking what programs exist. */ kiln_err_t handle_program_one(kiln_api_ctx_t *ctx, uint8_t id, kiln_api_resp_t *resp) { - if (ctx->filestore == nullptr) { - kiln_api_error(resp, 503, "no_storage", "program storage is unavailable"); - return KILN_ERR_IO; - } kiln_program_t prog; - if (kiln_program_store_get_slot(ctx->filestore, id, &prog) != KILN_OK) { - kiln_api_error(resp, 404, "not_found", "no program in that slot"); + if (kiln_profile_example(id, &prog) != KILN_OK) { + kiln_api_error(resp, 404, "not_found", "no program with that id"); return KILN_ERR_NOT_FOUND; } kiln_json_t j; @@ -895,7 +893,6 @@ kiln_err_t handle_storage(kiln_api_ctx_t *ctx, kiln_api_resp_t *resp) kiln_json_kv_uint(&j, "total_bytes", total); kiln_json_kv_uint(&j, "used_bytes", used); } - kiln_json_kv_uint(&j, "programs", kiln_program_store_count(ctx->filestore)); kiln_json_kv_uint(&j, "runs", kiln_run_index_count(ctx->filestore)); } else { @@ -903,6 +900,13 @@ kiln_err_t handle_storage(kiln_api_ctx_t *ctx, kiln_api_resp_t *resp) } kiln_json_obj_close(&j); + /* Outside "files", because the programs are no longer files: they are in + * the image, and reporting them as occupants of the medium said the + * opposite of what is true. Kept in this response because "how many + * programs does this device have" is a storage-shaped question even when + * the answer no longer involves storage. */ + kiln_json_kv_uint(&j, "programs", kiln_profile_example_count()); + kiln_json_obj_close(&j); return resp_end(resp, &j); } diff --git a/firmware/controller/host/webhost/webhost.cpp b/firmware/controller/host/webhost/webhost.cpp index eea07bb..7647dc8 100644 --- a/firmware/controller/host/webhost/webhost.cpp +++ b/firmware/controller/host/webhost/webhost.cpp @@ -39,7 +39,6 @@ #include #include "kiln_app/app.h" -#include "kiln_app/program_store.h" #include "kiln_app/run_index.h" #include "kiln_core/faults.h" #include "kiln_core/logring.h" @@ -132,9 +131,12 @@ void device_init(void) } (void)kiln_app_boot(&g_app, KILN_RESET_POWER_ON, -1.0f); - /* The built-in examples of SWR-PRG-09, so the Programs view has something in - * it. Idempotent by design, and the device does this on every boot too. */ - (void)kiln_program_store_seed(&g_fs_port); + /* Nothing to seed: SWR-PRG-09's examples are compiled into the firmware and + * the Programs view enumerates them from there, which is also what the + * device does. + + * The file store below is still bound, because the run records are real + * files and /api/runs reads them. */ g_api.app = &g_app; g_api.filestore = &g_fs_port; diff --git a/firmware/controller/main/main.cpp b/firmware/controller/main/main.cpp index 8641fc7..5711b05 100644 --- a/firmware/controller/main/main.cpp +++ b/firmware/controller/main/main.cpp @@ -456,12 +456,23 @@ void hmi_apply(const kiln_hmi_action_t *a) kiln_hal_heat_rearm(); /* SWR-SAF-18: let the output arm again */ } break; - case KILN_HMI_ACT_START: - /* No program store on hardware yet (tasklist E7), so there is nothing - * to start. The HMI already refuses to offer an empty list; this is - * the belt to that braces. */ - e = KILN_ERR_NOT_FOUND; + case KILN_HMI_ACT_START: { + /* SWR-HMI-10 and SWR-PRG-09. This used to return NOT_FOUND with a + * note that there was no program store on hardware, which made the + * menu's first entry dead on the device that ships: an operator could + * select a program and press, and nothing happened. + * + * With the profiles compiled into the image there is nothing to look + * up and nothing that can fail to mount, so the local input can start + * a firing on its own, which is what SWR-HMI-10 asked for and what a + * kiln with no network needs. */ + kiln_program_t prog; + e = kiln_profile_example(a->program_index, &prog); + if (e == KILN_OK) { + e = kiln_app_start(&s_app, &prog); + } break; + } case KILN_HMI_ACT_WIFI_SCAN: /* SWR-NET-11. The results arrive asynchronously; the view builder @@ -554,6 +565,23 @@ void hmi_build_view(kiln_hmi_view_t *v) (void)snprintf(v->ip, sizeof(v->ip), "%s", "-"); } + /* SWR-HMI-10: the programs the menu offers. Read from the image, which is + * also why there are any: the list was never filled on hardware, so the + * programs screen said "none stored" on a device carrying three firing + * profiles in its own flash. */ + const uint8_t progs = kiln_profile_example_count(); + v->program_count = (progs < KILN_HMI_MAX_PROGRAMS) ? progs + : (uint8_t)KILN_HMI_MAX_PROGRAMS; + for (uint8_t i = 0; i < v->program_count; i++) { + kiln_program_t prog; + if (kiln_profile_example(i, &prog) != KILN_OK) { + v->program_name[i][0] = '\0'; + continue; + } + (void)snprintf(v->program_name[i], KILN_HMI_NAME_LEN, "%.*s", + (int)(KILN_HMI_NAME_LEN - 1u), prog.name); + } + /* SWR-NET-11: collect the scan once, when the radio says it has finished. */ if (s_net_port.scan_busy != nullptr) { v->net_scanning = s_net_port.scan_busy(s_net_port.ctx); diff --git a/firmware/controller/test/host/integration/test_api.cpp b/firmware/controller/test/host/integration/test_api.cpp index 2b9b147..99c840a 100644 --- a/firmware/controller/test/host/integration/test_api.cpp +++ b/firmware/controller/test/host/integration/test_api.cpp @@ -13,7 +13,6 @@ #include #include #include "kiln_check.h" -#include "kiln_app/program_store.h" #include "kiln_app/run_index.h" #include "kiln_core/logring.h" #include "kiln_core/profile.h" @@ -124,8 +123,10 @@ static void rig_run(rig_t *r, double seconds) */ static void local_start(rig_t *r, uint8_t program_id) { + /* From the image, which is where the programs are and where the local HMI + * reads them from too. */ kiln_program_t prog; - CHECK_OK(kiln_program_store_get_slot(&r->fs_port, program_id, &prog)); + CHECK_OK(kiln_profile_example(program_id, &prog)); CHECK_OK(kiln_app_start(&r->app, &prog)); } diff --git a/firmware/controller/test/host/integration/test_persistence.cpp b/firmware/controller/test/host/integration/test_persistence.cpp index 13ca417..1bbbf95 100644 --- a/firmware/controller/test/host/integration/test_persistence.cpp +++ b/firmware/controller/test/host/integration/test_persistence.cpp @@ -13,7 +13,6 @@ #include #include "kiln_check.h" #include "kiln_app/app.h" -#include "kiln_app/program_store.h" #include "kiln_app/run_index.h" #include "kiln_app/settings.h" #include "kiln_core/faults.h" @@ -326,18 +325,37 @@ KILN_TEST(swrcfg05_configuration_survives_a_reboot) /* * @relation(SWR-PRG-09, scope=function) */ -KILN_TEST(swrprg09_the_examples_are_present_after_a_first_boot) +KILN_TEST(swrprg09_the_examples_need_no_first_boot_at_all) { + /* This used to check that the first boot seeded the examples onto the + * medium. There is no seeding: they are in the image, so they are present + * on a device whose flash has never been written, whose flash will not + * mount, and immediately rather than after whatever the boot sequence gets + * round to. A first boot is no longer a thing that can go wrong here. + * + * Checked after a boot anyway, because what the test is really about is + * that an operator has programs to start, and the boot is how a device + * arrives at that state. */ static medium_t m; static boot_t b; medium_init(&m); CHECK_OK(boot(&b, &m, KILN_RESET_POWER_ON, -1.0f, 20.0f)); - CHECK_EQ_UINT(kiln_program_store_count(&b.fs_port), kiln_profile_example_count()); + const uint8_t n = kiln_profile_example_count(); + CHECK(n > 0u); - kiln_program_t p; - CHECK_OK(kiln_program_store_load(&b.fs_port, "Glaze cone 6", &p)); - CHECK(p.flags & KILN_PROG_FLAG_READONLY); + bool found_glaze = false; + for (uint8_t i = 0; i < n; i++) { + kiln_program_t p; + CHECK_OK(kiln_profile_example(i, &p)); + CHECK(p.flags & KILN_PROG_FLAG_READONLY); + if (strcmp(p.name, "Glaze cone 6") == 0) { found_glaze = true; } + } + CHECK(found_glaze); + + /* And the medium is untouched by any of it: a first boot no longer writes + * a copy of what the firmware already contains. */ + CHECK_EQ_UINT(m.fs.writes, 0u); } /* --- SWR-SAF-17 across a reboot ---------------------------------------------- */ diff --git a/firmware/controller/test/host/integration/test_stores_on_flash.cpp b/firmware/controller/test/host/integration/test_stores_on_flash.cpp index 18953c1..bd53dfc 100644 --- a/firmware/controller/test/host/integration/test_stores_on_flash.cpp +++ b/firmware/controller/test/host/integration/test_stores_on_flash.cpp @@ -12,7 +12,6 @@ #include #include "kiln_check.h" -#include "kiln_app/program_store.h" #include "kiln_app/run_index.h" #include "kiln_core/fileslots.h" #include "kiln_core/profile.h" @@ -56,72 +55,15 @@ static kiln_program_t named(const char *name, uint16_t top_c) return p; } -/* - * @relation(SWA-10, scope=function) - */ -KILN_TEST(swa10_the_seeded_examples_survive_a_reboot_on_flash) -{ - rig_t *r = &g_rig; - power_up_fresh(r); - - /* This is the call that produced nothing on a real board before the store - * existed, which is why the display offered an empty list (M1). */ - CHECK_OK(kiln_program_store_seed(&r->store)); - const uint8_t seeded = kiln_program_store_count(&r->store); - CHECK(seeded > 0u); - - boot(r); /* reboot */ - CHECK_EQ_UINT(kiln_program_store_count(&r->store), seeded); - - /* Seeding is idempotent (SWR-PRG-09), including across the reboot. */ - CHECK_OK(kiln_program_store_seed(&r->store)); - CHECK_EQ_UINT(kiln_program_store_count(&r->store), seeded); -} - -/* - * @relation(SWR-PRG-04, scope=function) - */ -KILN_TEST(swr_prg_04_a_saved_program_reloads_byte_for_byte_after_a_reboot) -{ - rig_t *r = &g_rig; - power_up_fresh(r); - - const kiln_program_t p = named("bisque", 1000); - CHECK_OK(kiln_program_store_save(&r->store, &p, 1300.0f)); - - boot(r); - - kiln_program_t back = {}; - CHECK_OK(kiln_program_store_load(&r->store, "bisque", &back)); - CHECK_STR_EQ(back.name, "bisque"); - CHECK(memcmp(&back, &p, sizeof(p)) == 0); -} - -/* - * @relation(SWR-PRG-04, scope=function) +/* The three tests that stood here saved programs to this flash, reloaded them + * byte for byte after a reboot, and filled twenty slots before refusing the + * twenty-first. They went with the program store: the profiles are compiled + * into the image now, so a program cannot be lost by a medium it was never on. + * + * What the partition still holds is run records, and every property they + * relied on -- atomicity across a power cut, the ring, the identifier + * sequence, surviving a reboot -- is exercised below over the same flash fake. */ -KILN_TEST(swr_prg_04_the_store_fills_to_its_slot_count_and_then_refuses) -{ - rig_t *r = &g_rig; - power_up_fresh(r); - - char name[KILN_PROGRAM_NAME_LEN]; - unsigned saved = 0; - for (unsigned i = 0; i < KILN_PROGRAM_SLOTS + 4u; i++) { - (void)snprintf(name, sizeof(name), "p%02u", i); - const kiln_program_t p = named(name, 900); - if (kiln_program_store_save(&r->store, &p, 1300.0f) == KILN_OK) { - saved++; - } - } - /* The slot count is the limit, not the region count: 64 regions are there - * to be shared with the run records. */ - CHECK_EQ_UINT(saved, KILN_PROGRAM_SLOTS); - CHECK_EQ_UINT(kiln_program_store_count(&r->store), KILN_PROGRAM_SLOTS); - - boot(r); - CHECK_EQ_UINT(kiln_program_store_count(&r->store), KILN_PROGRAM_SLOTS); -} /* * @relation(SWR-LOG-09, scope=function) @@ -153,19 +95,20 @@ KILN_TEST(swr_log_09_run_records_ring_and_survive_a_reboot_on_flash) } /* - * @relation(SWR-PRG-04, scope=function) + * @relation(SWR-LOG-09, scope=function) */ -KILN_TEST(swr_prg_04_twenty_programs_and_twenty_runs_share_the_partition) +KILN_TEST(swr_log_09_the_run_records_are_now_the_only_thing_on_the_partition) { + /* This test used to fill twenty program slots and twenty run slots and + * check that 40 of 64 regions were in use with headroom to spare. With + * the programs in the image, the partition holds twenty regions of the + * sixty-four: it is now oversized by a factor of three rather than by half, + * which is recorded rather than acted on, because a 512 kB partition that + * is a third used costs nothing and shrinking it is a flash layout change + * that breaks every device already flashed. */ rig_t *r = &g_rig; power_up_fresh(r); - char name[KILN_PROGRAM_NAME_LEN]; - for (unsigned i = 0; i < KILN_PROGRAM_SLOTS; i++) { - (void)snprintf(name, sizeof(name), "p%02u", i); - const kiln_program_t p = named(name, 900); - CHECK_OK(kiln_program_store_save(&r->store, &p, 1300.0f)); - } for (unsigned i = 1; i <= KILN_RUN_SLOTS; i++) { kiln_run_record_t rec = {}; rec.run_id = i; @@ -173,43 +116,46 @@ KILN_TEST(swr_prg_04_twenty_programs_and_twenty_runs_share_the_partition) CHECK_OK(kiln_run_index_append(&r->store, &rec)); } - CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), - KILN_PROGRAM_SLOTS + KILN_RUN_SLOTS); - - /* 40 of 64, so the partition is sized with headroom and not to the inch. */ - CHECK(kiln_fileslots_used_regions(&r->fs) < TEST_REGIONS); + CHECK_EQ_UINT(kiln_fileslots_used_regions(&r->fs), KILN_RUN_SLOTS); + CHECK(kiln_fileslots_used_regions(&r->fs) * 3u <= TEST_REGIONS); boot(r); - CHECK_EQ_UINT(kiln_program_store_count(&r->store), KILN_PROGRAM_SLOTS); CHECK_EQ_UINT(kiln_run_index_count(&r->store), KILN_RUN_SLOTS); } /* * @relation(SWR-RUN-08, scope=function) */ -KILN_TEST(swr_run_08_a_power_cut_while_saving_a_program_keeps_the_old_one) +KILN_TEST(swr_run_08_a_power_cut_while_rewriting_a_run_keeps_the_old_one) { + /* The same property the program save used to prove, over the file that is + * still written at run time: a rewrite of an existing name that loses + * power inside the payload leaves the previous content, not half of each. + * + * run_index rewrites a slot when it marks a run truncated, so this is not + * a hypothetical path. */ rig_t *r = &g_rig; power_up_fresh(r); - const kiln_program_t first = named("glaze", 1200); - CHECK_OK(kiln_program_store_save(&r->store, &first, 1300.0f)); + kiln_run_record_t first = {}; + first.run_id = 7; + first.duration_s = 3600; + first.program = named("glaze", 1200); + CHECK_OK(kiln_run_index_append(&r->store, &first)); - /* Overwrite the same name, and lose power inside the payload. */ - const kiln_program_t second = named("glaze", 1250); + kiln_run_record_t edited = first; + edited.duration_s = 9999; r->flash.cut_power_at_write = r->flash.writes + 2u; r->flash.cut_bytes = 16u; - (void)kiln_program_store_save(&r->store, &second, 1300.0f); + (void)kiln_run_index_append(&r->store, &edited); kiln_host_flash_power_on(&r->flash); boot(r); - /* Neither the old program nor the slot is gone, and the half-written one is - * nowhere: a kiln that loses power mid-save still has its firing program. */ - kiln_program_t back = {}; - CHECK_OK(kiln_program_store_load(&r->store, "glaze", &back)); - CHECK_EQ_UINT(back.segments[0].target_c, 1200u); - CHECK_EQ_UINT(kiln_program_store_count(&r->store), 1u); + kiln_run_record_t back = {}; + CHECK_OK(kiln_run_index_find(&r->store, 7u, &back)); + CHECK_EQ_UINT(back.duration_s, 3600u); + CHECK_EQ_UINT(kiln_run_index_count(&r->store), 1u); } /* @@ -245,25 +191,25 @@ KILN_TEST(swr_run_08_a_power_cut_while_appending_a_run_keeps_the_earlier_runs) CHECK_EQ_UINT(kiln_run_index_next_run_id(&r->store), 4u); } -KILN_TEST(fr_cfg_12_a_format_clears_programs_and_runs_together) +KILN_TEST(fr_cfg_12_a_format_clears_the_run_history) { rig_t *r = &g_rig; power_up_fresh(r); - CHECK_OK(kiln_program_store_seed(&r->store)); kiln_run_record_t rec = {}; rec.run_id = 1; rec.program = named("x", 900); CHECK_OK(kiln_run_index_append(&r->store, &rec)); CHECK_OK(kiln_fileslots_format(&r->fs)); - CHECK_EQ_UINT(kiln_program_store_count(&r->store), 0u); CHECK_EQ_UINT(kiln_run_index_count(&r->store), 0u); boot(r); - CHECK_EQ_UINT(kiln_program_store_count(&r->store), 0u); + CHECK_EQ_UINT(kiln_run_index_count(&r->store), 0u); - /* And seeding brings the examples back, so a reset is recoverable. */ - CHECK_OK(kiln_program_store_seed(&r->store)); - CHECK(kiln_program_store_count(&r->store) > 0u); + /* A factory reset no longer costs the operator their programs, which is + * the other half of compiling them in: the only thing on this partition is + * history, and losing history is what a factory reset is for. The next + * run also starts from 1 again, since the sequence is read from here. */ + CHECK_EQ_UINT(kiln_run_index_next_run_id(&r->store), 1u); } diff --git a/firmware/controller/test/host/unit/test_fileslots.cpp b/firmware/controller/test/host/unit/test_fileslots.cpp index e6d8dda..8b2dde5 100644 --- a/firmware/controller/test/host/unit/test_fileslots.cpp +++ b/firmware/controller/test/host/unit/test_fileslots.cpp @@ -15,9 +15,8 @@ #include "kiln_check.h" #include "kiln_core/fileslots.h" #include "kiln_hal_host/hal_host.h" -/* For the two blob sizes the real callers store, in the mount-cost tests at the +/* For the blob size the one real caller stores, in the mount-cost tests at the * end of this file: a cost measured against made-up payloads measures nothing. */ -#include "kiln_app/program_store.h" #include "kiln_app/run_index.h" constexpr size_t SECTOR_BYTES = 4096u; @@ -724,11 +723,13 @@ KILN_TEST(swa21_a_full_mount_costs_what_the_model_says) */ KILN_TEST(swa21_the_mount_this_product_actually_pays_for) { - /* What the two real callers store: 20 programs and 20 run records, at the - * sizes their own serialisers produce. The worst case above is what the - * partition permits; this is what the firmware puts in it, and the - * difference is a factor of nine. */ - constexpr size_t PROG_BLOB = 8u + sizeof(kiln_program_t) + 2u; + /* What the one real caller stores: 20 run records at the size run_index's + * own serialiser produces. The worst case above is what the partition + * permits; this is what the firmware puts in it. + * + * It used to be 20 programs as well, which is why the medium here is half + * what it was: the programs are compiled into the image and the partition + * holds history and nothing else. */ constexpr size_t RUN_BLOB = 8u + sizeof(kiln_run_record_t) + 2u; cost_rig_t *r = &g_cost; @@ -742,9 +743,6 @@ KILN_TEST(swa21_the_mount_this_product_actually_pays_for) memset(blob, 0x5A, sizeof(blob)); for (size_t i = 0; i < 20u; i++) { char path[16]; - (void)snprintf(path, sizeof(path), "/p/%02zu", i); - CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, PROG_BLOB)); - CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, PROG_BLOB)); (void)snprintf(path, sizeof(path), "/r/%02zu", i); CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, RUN_BLOB)); CHECK_OK(r->store.write_atomic(r->store.ctx, path, blob, RUN_BLOB)); @@ -754,22 +752,23 @@ KILN_TEST(swa21_the_mount_this_product_actually_pays_for) r->flash.bytes_read = 0; CHECK_OK(kiln_fileslots_mount(&r->fs, &r->port)); - uint32_t pr = 0, pb = 0, rr = 0, rb = 0; - copy_cost(PROG_BLOB, &pr, &pb); + uint32_t rr = 0, rb = 0; copy_cost(RUN_BLOB, &rr, &rb); - /* 40 regions hold a file; the other 24 cost one failed header read each. */ - const uint32_t want_reads = 2u * 20u * (pr + rr) + (uint32_t)(COST_REGIONS - 40u) * 2u; - const uint32_t want_bytes = 2u * 20u * (pb + rb) - + (uint32_t)(COST_REGIONS - 40u) * 2u * KILN_FILESLOT_HDR; + /* 20 regions hold a file; the other 44 cost one failed header read each. */ + const uint32_t want_reads = 2u * 20u * rr + (uint32_t)(COST_REGIONS - 20u) * 2u; + const uint32_t want_bytes = 2u * 20u * rb + + (uint32_t)(COST_REGIONS - 20u) * 2u * KILN_FILESLOT_HDR; CHECK_EQ_UINT(r->flash.reads, want_reads); CHECK_EQ_UINT(r->flash.bytes_read, want_bytes); - printf(" mount: %u reads, %u bytes (program blob %zu, run blob %zu)\n", - r->flash.reads, r->flash.bytes_read, PROG_BLOB, RUN_BLOB); - - /* Measured: 768 reads and 47 808 bytes, against 8 192 and 524 288 for a - * medium full of maximum-size files. A ninth of the worst case, and the - * reason the worst case is a bound rather than a budget. */ - CHECK_EQ_UINT(r->flash.reads, 768u); - CHECK_EQ_UINT(r->flash.bytes_read, 47808u); + printf(" mount: %u reads, %u bytes (run blob %zu)\n", + r->flash.reads, r->flash.bytes_read, RUN_BLOB); + + /* Measured, against 8 192 reads and 524 288 bytes for a medium full of + * maximum-size files: a small fraction of the worst case, which is the + * reason the worst case is a bound rather than a budget. The exact numbers + * are asserted so that making mount lazy, or putting something else on + * this partition, shows up here rather than on a board. */ + CHECK_EQ_UINT(r->flash.reads, 488u); + CHECK_EQ_UINT(r->flash.bytes_read, 31888u); } diff --git a/firmware/controller/test/host/unit/test_stores.cpp b/firmware/controller/test/host/unit/test_stores.cpp index 3753e8a..0bbd83f 100644 --- a/firmware/controller/test/host/unit/test_stores.cpp +++ b/firmware/controller/test/host/unit/test_stores.cpp @@ -7,7 +7,6 @@ #include #include "kiln_check.h" -#include "kiln_app/program_store.h" #include "kiln_app/run_index.h" #include "kiln_app/settings.h" #include "kiln_core/profile.h" @@ -175,159 +174,69 @@ static kiln_program_t named(const char *name, uint16_t target) return p; } -/* - * @relation(SWR-PRG-09, scope=function) +/* --- the programs, which are now firmware (SWR-PRG-09) ------------------ + * + * Six tests used to live here, over a store that saved programs to flash, + * replaced them by name, refused to overwrite the read-only examples, filled + * twenty slots and then said no, and re-validated what it read back because a + * stored program is untrusted input. + * + * All of it is gone, with the store. SWR-PRG-07, SWR-PRG-08 and SWR-PRG-10 are + * withdrawn -- nothing authors a program on this device -- and the three + * profiles that exist arrive in the image, so persisting them was keeping a + * copy of something the firmware already contained, in a medium that can wear + * out, behind a seeding path that could fail. + * + * What survives is the property that actually matters, and it is now checkable + * without a medium at all: the programs the firmware carries are valid + * programs. A profile compiled in wrong is a firing that fails at the moment + * somebody starts it. */ -KILN_TEST(swrprg09_the_examples_are_seeded_once_and_are_read_only) -{ - kiln_host_fs_t fs; - kiln_port_filestore_t port; - kiln_host_fs_init(&fs); - kiln_host_fs_bind(&fs, &port); - - CHECK_OK(kiln_program_store_seed(&port)); - const uint8_t n = kiln_profile_example_count(); - CHECK_EQ_UINT(kiln_program_store_count(&port), n); - - const uint32_t writes = fs.writes; - /* Idempotent, so it can run on every boot without rewriting flash. */ - CHECK_OK(kiln_program_store_seed(&port)); - CHECK_EQ_UINT(fs.writes, writes); - CHECK_EQ_UINT(kiln_program_store_count(&port), n); - - kiln_program_t example; - CHECK_OK(kiln_profile_example(0, &example)); - - /* SWR-PRG-09: neither overwritable nor deletable, so the operator always has - * a known-good program to fall back to. */ - kiln_program_t edited = example; - edited.segments[0].target_c = 500; - CHECK_ERR(kiln_program_store_save(&port, &edited, 1280.0f), KILN_ERR_STATE); - CHECK_ERR(kiln_program_store_delete(&port, example.name), KILN_ERR_STATE); -} - -KILN_TEST(swrprg_programs_round_trip_and_replace_by_name) -{ - kiln_host_fs_t fs; - kiln_port_filestore_t port; - kiln_host_fs_init(&fs); - kiln_host_fs_bind(&fs, &port); - - const kiln_program_t p = named("my firing", 900); - CHECK_OK(kiln_program_store_save(&port, &p, 1280.0f)); - - kiln_program_t out; - CHECK_OK(kiln_program_store_load(&port, "my firing", &out)); - CHECK_STR_EQ(out.name, "my firing"); - CHECK_EQ_UINT(out.segments[0].target_c, 900u); - - /* Saving the same name replaces rather than accumulating. */ - const kiln_program_t edited = named("my firing", 1000); - CHECK_OK(kiln_program_store_save(&port, &edited, 1280.0f)); - CHECK_EQ_UINT(kiln_program_store_count(&port), 1u); - CHECK_OK(kiln_program_store_load(&port, "my firing", &out)); - CHECK_EQ_UINT(out.segments[0].target_c, 1000u); - - CHECK_OK(kiln_program_store_delete(&port, "my firing")); - CHECK_ERR(kiln_program_store_load(&port, "my firing", &out), KILN_ERR_NOT_FOUND); - CHECK_ERR(kiln_program_store_delete(&port, "my firing"), KILN_ERR_NOT_FOUND); -} /* - * @relation(SWR-PRG-05, scope=function) - */ -KILN_TEST(swrprg05_an_invalid_program_never_reaches_storage) -{ - kiln_host_fs_t fs; - kiln_port_filestore_t port; - kiln_host_fs_init(&fs); - kiln_host_fs_bind(&fs, &port); - - kiln_program_t bad = named("too hot", 1340); - CHECK_ERR(kiln_program_store_save(&port, &bad, 1280.0f), KILN_ERR_RANGE); - CHECK_EQ_UINT(kiln_program_store_count(&port), 0u); - - bad = named("", 900); - CHECK_ERR(kiln_program_store_save(&port, &bad, 1280.0f), KILN_ERR_RANGE); - CHECK_EQ_UINT(fs.writes, 0u); -} - -/* - * @relation(SWR-PRG-04, scope=function) + * @relation(SWR-PRG-09, scope=function) */ -KILN_TEST(swrprg04_the_store_holds_twenty_programs_and_then_says_no) +KILN_TEST(swrprg09_every_compiled_in_program_is_a_valid_program) { - kiln_host_fs_t fs; - kiln_port_filestore_t port; - kiln_host_fs_init(&fs); - kiln_host_fs_bind(&fs, &port); - - char name[KILN_PROGRAM_NAME_LEN]; - for (unsigned i = 0; i < KILN_PROGRAM_SLOTS; i++) { - (void)snprintf(name, sizeof(name), "program %u", i); - const kiln_program_t p = named(name, 900); - CHECK_OK(kiln_program_store_save(&port, &p, 1280.0f)); + const uint8_t n = kiln_profile_example_count(); + CHECK(n > 0u); + CHECK(n <= KILN_MAX_PROGRAMS); + + for (uint8_t i = 0; i < n; i++) { + kiln_program_t p; + CHECK_OK(kiln_profile_example(i, &p)); + + /* Named, terminated, and not empty: the name is what the menu and the + * API show, and the run record keeps a copy of it. */ + CHECK(p.name[0] != '\0'); + CHECK(strnlen(p.name, KILN_PROGRAM_NAME_LEN) < KILN_PROGRAM_NAME_LEN); + + /* Validates against the default ceiling of SWR-SAF-01. An example that + * does not is one an operator cannot start, which they would discover + * by selecting it. */ + CHECK_MSG(kiln_profile_validate(&p, 1280.0f).code == KILN_PROG_OK, + "example %u (%s) does not validate", i, p.name); + CHECK(p.segment_count > 0u); + CHECK(kiln_profile_peak_c(&p) > 0.0f); } - CHECK_EQ_UINT(kiln_program_store_count(&port), KILN_PROGRAM_SLOTS); - CHECK_EQ_UINT(KILN_PROGRAM_SLOTS, KILN_MAX_PROGRAMS); - - const kiln_program_t one_too_many = named("overflow", 900); - CHECK_ERR(kiln_program_store_save(&port, &one_too_many, 1280.0f), KILN_ERR_NO_SPACE); - - /* But replacing an existing one still works when full, which is the case a - * naive "is there a free slot" check gets wrong. */ - const kiln_program_t replace = named("program 3", 950); - CHECK_OK(kiln_program_store_save(&port, &replace, 1280.0f)); -} - -/* - * @relation(SWR-NFR-19, scope=function) - */ -KILN_TEST(swrnfr19_a_stored_program_is_treated_as_untrusted_on_the_way_back_in) -{ - kiln_host_fs_t fs; - kiln_port_filestore_t port; - kiln_host_fs_init(&fs); - kiln_host_fs_bind(&fs, &port); - - const kiln_program_t p = named("ok", 900); - CHECK_OK(kiln_program_store_save(&port, &p, 1280.0f)); - - /* Scribble over the stored name so it has no terminator, then fix the CRC so - * it passes -- which is what a firmware with a different struct layout, or a - * sufficiently unlucky corruption, looks like. */ - CHECK(fs.files[0].used); - uint8_t *blob = fs.files[0].data; - memset(&blob[8], 'A', KILN_PROGRAM_NAME_LEN); - const size_t n = fs.files[0].len; - const uint16_t crc = kiln_crc16(blob, n - 2); - blob[n - 2] = (uint8_t)crc; - blob[n - 1] = (uint8_t)(crc >> 8u); - - kiln_program_t out; - CHECK_OK(kiln_program_store_get_slot(&port, 0, &out)); - /* Terminated on the way in, so nothing downstream reads off the end. */ - CHECK_EQ_UINT(strlen(out.name), KILN_PROGRAM_NAME_LEN - 1u); -} - -KILN_TEST(a_corrupt_program_file_is_skipped_not_returned) -{ - kiln_host_fs_t fs; - kiln_port_filestore_t port; - kiln_host_fs_init(&fs); - kiln_host_fs_bind(&fs, &port); - - const kiln_program_t a = named("good", 900); - CHECK_OK(kiln_program_store_save(&port, &a, 1280.0f)); - const kiln_program_t b = named("also good", 800); - CHECK_OK(kiln_program_store_save(&port, &b, 1280.0f)); - fs.files[0].data[20] ^= 0xFFu; /* corrupt the first */ + /* Names are distinct, because they are what the operator picks by. */ + for (uint8_t i = 0; i < n; i++) { + for (uint8_t k = (uint8_t)(i + 1u); k < n; k++) { + kiln_program_t a, b; + CHECK_OK(kiln_profile_example(i, &a)); + CHECK_OK(kiln_profile_example(k, &b)); + CHECK_MSG(strcmp(a.name, b.name) != 0, + "examples %u and %u are both called %s", i, k, a.name); + } + } - CHECK_EQ_UINT(kiln_program_store_count(&port), 1u); + /* And an index past the end is refused rather than returning whatever is + * next in memory: the id comes off a URL (/api/programs/{id}). */ kiln_program_t out; - CHECK_ERR(kiln_program_store_load(&port, "good", &out), KILN_ERR_NOT_FOUND); - CHECK_OK(kiln_program_store_load(&port, "also good", &out)); + CHECK_ERR(kiln_profile_example(n, &out), KILN_ERR_NOT_FOUND); + CHECK_ERR(kiln_profile_example(255, &out), KILN_ERR_NOT_FOUND); + CHECK_ERR(kiln_profile_example(0, nullptr), KILN_ERR_INVALID_ARG); } /* --- run records (SWR-RUN-07, SWR-LOG-09) -------------------------------- */ @@ -485,24 +394,28 @@ KILN_TEST(swrsaf12_the_baseline_comes_from_the_stored_run_history) KILN_TEST(an_atomic_replace_leaves_the_previous_content_on_a_power_cut) { - /* The property atomic-by-rename buys, and the one a plain write loses: half - * a program on disk is worse than the old program. */ + /* The property atomic-by-two-copies buys, and the one a plain write loses: + * half a record on disk is worse than the old record. Driven through the + * run index, which is what still writes to this store now that the + * programs are compiled into the image. */ kiln_host_fs_t fs; kiln_port_filestore_t port; kiln_host_fs_init(&fs); kiln_host_fs_bind(&fs, &port); - const kiln_program_t a = named("keeper", 900); - CHECK_OK(kiln_program_store_save(&port, &a, 1280.0f)); + kiln_run_record_t a = run(1, KILN_END_COMPLETE); + a.duration_s = 3600u; + CHECK_OK(kiln_run_index_append(&port, &a)); fs.cut_power_at_write = fs.writes + 1u; - const kiln_program_t b = named("keeper", 1000); - CHECK_ERR(kiln_program_store_save(&port, &b, 1280.0f), KILN_ERR_IO); + kiln_run_record_t b = a; + b.duration_s = 7200u; + (void)kiln_run_index_append(&port, &b); fs.powered = true; - kiln_program_t out; - CHECK_OK(kiln_program_store_load(&port, "keeper", &out)); - CHECK_EQ_UINT(out.segments[0].target_c, 900u); /* the old one, intact */ + kiln_run_record_t out = {}; + CHECK_OK(kiln_run_index_find(&port, 1u, &out)); + CHECK_EQ_UINT(out.duration_s, 3600u); /* the old one, intact */ } KILN_TEST(the_stores_validate_their_arguments) @@ -512,13 +425,7 @@ KILN_TEST(the_stores_validate_their_arguments) kiln_host_fs_init(&fs); kiln_host_fs_bind(&fs, &port); - kiln_program_t p; kiln_run_record_t r; - CHECK_ERR(kiln_program_store_seed(NULL), KILN_ERR_INVALID_ARG); - CHECK_ERR(kiln_program_store_save(&port, NULL, 1280.0f), KILN_ERR_INVALID_ARG); - CHECK_ERR(kiln_program_store_load(&port, NULL, &p), KILN_ERR_INVALID_ARG); - CHECK_ERR(kiln_program_store_get_slot(&port, 200, &p), KILN_ERR_INVALID_ARG); - CHECK_EQ_UINT(kiln_program_store_count(NULL), 0u); CHECK_ERR(kiln_run_index_append(&port, NULL), KILN_ERR_INVALID_ARG); CHECK_ERR(kiln_run_index_get_slot(&port, 200, &r), KILN_ERR_INVALID_ARG); diff --git a/tasklist.md b/tasklist.md index 2b1af9d..d188283 100644 --- a/tasklist.md +++ b/tasklist.md @@ -759,31 +759,40 @@ weakness first, the deadline second, the sentence third, the engineering last. stream now declares neither length nor chunking and ends with the connection, which is what an event stream is. -- [ ] **P3. The program store has no writer, and 17 of its 20 slots have no - purpose.** Answered rather than open: `OQ-09` is resolved, the profiles are - compiled into the firmware and nothing edits them, so `SWR-PRG-07`, - `SWR-PRG-08` and `SWR-PRG-10` are withdrawn. What is left is the machinery - those requirements justified. - - `kiln_program_store_save` and `kiln_program_store_delete` are now called by - **nothing but their own tests**. `SWR-PRG-04` asks for 20 program slots in - non-volatile storage persisting across a firmware update, and the three - programs that exist arrive with the firmware, so the persistence is storing - a copy of something the image already contains. - - Two ways to finish it, and this is a decision rather than a task: - - - **Keep the store, narrow the requirement.** Seeding stays, the slots stay, - `SWR-PRG-04` is reworded to say what it is now for. Cheapest, and leaves - tested code that nothing calls. - - **Read the profiles straight from `kiln_profile_example()`.** The program - store stops existing for programs; `kilnfs` keeps the run records, which - are the other half of `SWA-21`. Removes a subsystem, a partition's worth - of wear, the seeding path and its failure modes, and about 200 lines. It - also deletes tested code, which wants care rather than enthusiasm. - - The second is the one consistent with the decision. Neither should be done - without someone deciding which. +- [x] **P3. The program store is gone. Done 2026-10-10.** The decision was + pre-compiled profiles with nothing editing them, which left `SWR-PRG-04` + asking for 20 program slots in non-volatile storage to hold a copy of the + three programs that arrive in the image. It is withdrawn, the store is + deleted, and the profiles are read from `kiln_profile_example` by the API, + the HMI and the development harness alike. + + It is a deletion that makes the device better rather than smaller: + + - **The local start path worked for the first time.** `KILN_HMI_ACT_START` + returned `KILN_ERR_NOT_FOUND` on hardware with a note that there was no + program store, and the view builder never filled the program list, so a + device carrying three firing profiles in its own flash showed "none + stored" and its menu's first entry did nothing. Both are now wired to the + compiled-in profiles, which is `SWR-HMI-10` delivered rather than written. + - **A device whose flash will not mount still has its programs.** The list + used to answer `503` for programs the firmware was carrying. + - **A factory reset no longer costs the operator their programs.** The + partition holds run history, and losing history is what a reset is for. + - **The first boot has nothing to get wrong.** Seeding was idempotent and + tested, and still a flash write on every power-on of a new device. + + About 200 lines of implementation and nine tests went with it. The three + properties worth keeping were re-pointed rather than dropped: every + compiled-in example is a valid program with a distinct name and an index past + the end is refused, the atomic-replace-on-power-cut property is now driven + through `run_index`, which is what still writes to the store, and the + partition-sharing test became the measurement that 20 of 64 regions are used. + The mount cost of `Q2` halves with it: **488 reads and 31 888 bytes**. + + `SWR-PRG-05` stays, and the reason is `safety.max_temp_c`: the programs are + trusted as data, but an operator can lower the ceiling below a program's + peak, and validation on start is what refuses that firing rather than running + it into a limit. - [ ] **P4. Untested against a client.** It compiles and passes analysis. No request has been made of it. The host API suite covers every route's @@ -823,8 +832,9 @@ weakness first, the deadline second, the sentence third, the engineering last. - [x] **Q2. The mount cost is measured. Done 2026-10-09.** It was described rather than measured, and the description was low by a third. On the medium - this firmware actually produces, 20 programs of 398 bytes and 20 run records - of 554, a mount costs **768 reads and 47 808 bytes** of CRC. A partition full + this firmware actually produces, a mount costs **488 reads and 31 888 bytes** + of CRC: 20 run records of 554 bytes, both copies valid. It was 768 reads and + 47 808 bytes while the programs were stored too, which `P3` ended. A partition full of maximum-size files with both copies valid costs **8 192 reads and 524 288 bytes**, the whole partition. From bcbb3be8f0749b8d7324f81b24db977542a14d3b Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:21:12 +0200 Subject: [PATCH 11/14] refactor: the chamber thermocouple type leaves the schema R9. SWR-ACQ-02 fixes the chamber couple at type K because the independent supervisor linearises for it, and a type register this side could change is a backstop that means something else: with the supervisor expecting K and an S couple fitted, the reading at 1300 degC is about a quarter of K's output and the backstop never fires. kiln_suplink's configure already refused, so sense.tc_type was a setting an operator could change and believe while nothing acted on it, which is worse than no setting at all. It is gone from the struct, from the item table and therefore from /api/config and the display. The enclosure channel's type stays configurable; that channel is not in the supervisor's remit. KILN_CFG_SCHEMA_VERSION is 2, and the migration is the part worth reviewing. Every earlier version change ADDED fields, which the prefix copy handles. This one removes a byte from the middle of the struct, so every field after the hole sits one lower: a prefix copy would have landed the whole sensing, control and network block one byte out of place, with the stored line filter frequency reading as half of itself plus half a calibration offset -- and the CRC would have passed on the way in, so nothing would have reported it. A device would have come back from an update quietly mis-calibrated. kiln_config_decode therefore splices version 1 explicitly, which meets SWR-CFG-05's "older: migrate" rather than answering it with defaults and costing an operator their configuration. The test forges a real version 1 blob and checks fields on both sides of the hole, the enclosure type that moved, and a re-encode round trip. SWR-CFG-02 now says there is no chamber TC type and why, and a test asserts the item does not come back. kiln_app_boot passes KILN_TC_TYPE_K to the chamber port from the code: refused on the product, which is the point, and correct on the simulated build where the port accepts it. 26/26 host suites, the same under ASan/UBSan, clang-tidy 20 clean on host and target, hardware image builds, StrictDoc exports clean. --- docs/03_software_req.sdoc | 2 +- .../components/kiln_app/src/app.cpp | 11 +- .../kiln_core/include/kiln_core/configmodel.h | 16 ++- .../components/kiln_core/src/configmodel.cpp | 38 +++++- .../test/host/unit/test_configmodel.cpp | 113 +++++++++++++++++- tasklist.md | 53 +++++--- 6 files changed, 209 insertions(+), 24 deletions(-) diff --git a/docs/03_software_req.sdoc b/docs/03_software_req.sdoc index 6da98b5..be1b420 100644 --- a/docs/03_software_req.sdoc +++ b/docs/03_software_req.sdoc @@ -1484,7 +1484,7 @@ VERIFICATION: T [REQUIREMENT] UID: SWR-CFG-02 STATEMENT: >>> -The configuration shall include at minimum: **safety**: maximum chamber temperature, maximum enclosure temperature, runaway detection parameters, power-loss recovery policy; **control**: Kp, Ki, Kd, loop period, PWM window, minimum on/off time, maximum duty, hold-back band, dwell tolerance; **sensing**: TC types, line filter frequency, filter time constant, calibration offset and gain for both channels; **tuning**: amplitude, hysteresis, rule, timeout; **current**: CT ratio and output type, nominal heater current, mains voltage, fail-on and fail-off thresholds and settle times, deviation bands, over-current limit, relay life limits, monitoring enable; **logging**: sample interval; **HMI**: units, dim timeout, alarm duration; **network**: WiFi credentials, hostname, NTP server, timezone, there being no mode to choose and no access point since [LINK: SWR-NET-11]; **security**: none, since [LINK: SWR-WEB-26] left nothing reachable over the network to authenticate. +The configuration shall include at minimum: **safety**: maximum chamber temperature, maximum enclosure temperature, runaway detection parameters, power-loss recovery policy; **control**: Kp, Ki, Kd, loop period, PWM window, minimum on/off time, maximum duty, hold-back band, dwell tolerance; **sensing**: the *enclosure* TC type, line filter frequency, filter time constant, calibration offset and gain for both channels (there is no chamber TC type: [LINK: SWR-ACQ-02] fixes it at K and schema 2 removed the item, because a setting that is offered and ignored is worse than one that is absent); **tuning**: amplitude, hysteresis, rule, timeout; **current**: CT ratio and output type, nominal heater current, mains voltage, fail-on and fail-off thresholds and settle times, deviation bands, over-current limit, relay life limits, monitoring enable; **logging**: sample interval; **HMI**: units, dim timeout, alarm duration; **network**: WiFi credentials, hostname, NTP server, timezone, there being no mode to choose and no access point since [LINK: SWR-NET-11]; **security**: none, since [LINK: SWR-WEB-26] left nothing reachable over the network to authenticate. <<< PRIORITY: M VERIFICATION: I diff --git a/firmware/controller/components/kiln_app/src/app.cpp b/firmware/controller/components/kiln_app/src/app.cpp index dddb4aa..ea4e6e0 100644 --- a/firmware/controller/components/kiln_app/src/app.cpp +++ b/firmware/controller/components/kiln_app/src/app.cpp @@ -162,8 +162,15 @@ kiln_err_t kiln_app_init(kiln_app_t *app, const kiln_app_ports_t *ports, (void)app->ports.counters->load(app->ports.counters->ctx, &app->counters); } if (app->ports.tc->configure != nullptr) { - (void)app->ports.tc->configure(app->ports.tc->ctx, - (kiln_tc_type_t)app->cfg.tc_type, + /* SWR-ACQ-02: type K, from here and not from the configuration, which + * no longer carries a chamber type at all. On the product this call + * is refused anyway -- the supervisor owns that front end and + * kiln_suplink returns KILN_ERR_UNSUPPORTED -- and the refusal is the + * point: a caller that still thinks it owns the front end finds out. + * On a host or simulated build, where the chamber port is a fake that + * accepts it, type K is what the supervisor would have been + * linearising for. */ + (void)app->ports.tc->configure(app->ports.tc->ctx, KILN_TC_TYPE_K, (uint8_t)app->cfg.line_filter_hz); } if ((app->ports.case_tc != nullptr) && (app->ports.case_tc->configure != nullptr)) { diff --git a/firmware/controller/components/kiln_core/include/kiln_core/configmodel.h b/firmware/controller/components/kiln_core/include/kiln_core/configmodel.h index 12fad39..3eb252b 100644 --- a/firmware/controller/components/kiln_core/include/kiln_core/configmodel.h +++ b/firmware/controller/components/kiln_core/include/kiln_core/configmodel.h @@ -21,7 +21,12 @@ #include "kiln/err.h" #include "kiln/types.h" -constexpr uint16_t KILN_CFG_SCHEMA_VERSION = 1; +/* 2: `sense.tc_type` removed (SWR-ACQ-02, tasklist R9). A removal in the + * middle of the struct shifts every field after it, so a version 1 blob cannot + * be read as a version 2 one; kiln_config_decode carries the splice that + * migrates it, and SWR-CFG-05's "older: migrate" is met rather than answered + * with defaults. */ +constexpr uint16_t KILN_CFG_SCHEMA_VERSION = 2; constexpr size_t KILN_CFG_STR_LEN = 33; /* WiFi SSID is 32 + NUL */ constexpr size_t KILN_CFG_PASS_LEN = 65; @@ -108,7 +113,14 @@ typedef struct { float dwell_tol_c; /* sensing */ - uint8_t tc_type; /* kiln_tc_type_t */ + /* There is no chamber tc_type, and that is SWR-ACQ-02: the chamber front + * end belongs to the independent supervisor, which linearises for type K + * and whose backstop means nothing if this side can change that. Removed + * from the schema at version 2 rather than left as an item nothing could + * act on -- a setting that is offered and ignored is worse than one that + * is absent, because an operator can set it and believe it. The + * enclosure channel's type stays configurable; that channel is not in the + * supervisor's remit. */ uint8_t case_tc_type; uint16_t line_filter_hz; float filter_tau_s; diff --git a/firmware/controller/components/kiln_core/src/configmodel.cpp b/firmware/controller/components/kiln_core/src/configmodel.cpp index d118fba..d1526d6 100644 --- a/firmware/controller/components/kiln_core/src/configmodel.cpp +++ b/firmware/controller/components/kiln_core/src/configmodel.cpp @@ -1,6 +1,7 @@ /* SPDX-FileCopyrightText: 2026 Bitcrush Testing * SPDX-License-Identifier: GPL-3.0-or-later */ +#include #include #include "kiln_core/configmodel.h" #include "kiln_core/logrec.h" /* kiln_crc16 */ @@ -68,7 +69,6 @@ const kiln_cfg_item_t k_items[] = { NUM("control.dwell_tol_c", "degC", "SWR-CTL-12", KILN_CFG_T_FLOAT, dwell_tol_c, 0.5, 50, 5, 0), /* sensing */ - ENUMI("sense.tc_type", "SWR-ACQ-02", tc_type, k_tc_names, KILN_TC_TYPE_COUNT, KILN_TC_TYPE_K, SAFE | BOOTR), ENUMI("sense.case_tc_type", "SWR-ACQ-02", case_tc_type, k_tc_names, KILN_TC_TYPE_COUNT, KILN_TC_TYPE_K, SAFE | BOOTR), NUM("sense.line_filter_hz", "Hz", "SWR-ACQ-06", KILN_CFG_T_U16, line_filter_hz, 50, 60, 50, LOCKED), NUM("sense.filter_tau_s", "s", "SWR-ACQ-07", KILN_CFG_T_FLOAT, filter_tau_s, 0, 30, 2, 0), @@ -511,9 +511,39 @@ kiln_err_t kiln_config_decode(const void *blob, size_t len, kiln_config_t *cfg) /* Equal or older: take what the stored payload holds, leave the rest at the * defaults already in place. That is exactly "migrate, filling new items - * with defaults". */ - const size_t take = payload < sizeof(kiln_config_t) ? payload : sizeof(kiln_config_t); - memcpy(cfg, &p[CFG_HDR_BYTES], take); + * with defaults". + * + * Version 1 needs more than that, because schema 2 REMOVED a field rather + * than adding one. `sense.tc_type` was one byte immediately before + * `case_tc_type` (SWR-ACQ-02 took it away), so every byte after that hole + * sits one lower in the version 2 struct and a straight copy would land + * the whole sensing, control and network block one byte out of place: + * `line_filter_hz` would read as half of itself plus half a calibration + * offset, and the CRC would have passed on the way in. The splice is + * explicit for that reason, and is what keeps an operator's configuration + * across the update instead of resetting it to defaults. */ + const uint8_t *stored = &p[CFG_HDR_BYTES]; + if (ver == 1u) { + const size_t hole = offsetof(kiln_config_t, case_tc_type); + const size_t head = (payload < hole) ? payload : hole; + memcpy(cfg, stored, head); + if (payload > (hole + 1u)) { + size_t tail = payload - hole - 1u; + if (tail > (sizeof(kiln_config_t) - hole)) { + tail = sizeof(kiln_config_t) - hole; + } + /* Byte arithmetic on the struct, for the reason field_of above + * gives: offsetof yields a byte offset and the address has to be + * walked as bytes. + * NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) */ + memcpy(reinterpret_cast(cfg) + hole, stored + hole + 1u, tail); + } + } + else { + const size_t take = payload < sizeof(kiln_config_t) ? payload + : sizeof(kiln_config_t); + memcpy(cfg, stored, take); + } cfg->schema_version = KILN_CFG_SCHEMA_VERSION; /* A blob that passed its CRC can still hold a value outside a range this diff --git a/firmware/controller/test/host/unit/test_configmodel.cpp b/firmware/controller/test/host/unit/test_configmodel.cpp index b3e0310..97aeb62 100644 --- a/firmware/controller/test/host/unit/test_configmodel.cpp +++ b/firmware/controller/test/host/unit/test_configmodel.cpp @@ -3,6 +3,7 @@ * kiln_core/configmodel -- SWR-CFG-01..SWR-CFG-08, SWR-NFR-19. */ +#include #include #include "kiln_check.h" #include "kiln_core/configmodel.h" @@ -85,7 +86,7 @@ KILN_TEST(swrcfg02_the_minimum_item_set_is_present) "control.kp", "control.ki", "control.kd", "control.loop_period_ms", "control.window_ms", "control.min_on_ms", "control.min_off_ms", "control.duty_max", "control.holdback_band_c", "control.dwell_tol_c", - "sense.tc_type", "sense.line_filter_hz", "sense.filter_tau_s", + "sense.case_tc_type", "sense.line_filter_hz", "sense.filter_tau_s", "sense.cal_offset_c", "sense.cal_gain", "tune.amplitude", "tune.hysteresis_c", "tune.rule", "tune.timeout_s", "current.ct_a_per_v", "current.nominal_a", "current.mains_v", @@ -102,6 +103,14 @@ KILN_TEST(swrcfg02_the_minimum_item_set_is_present) CHECK_MSG(kiln_config_find(required[i]) != NULL, "SWR-CFG-02 requires an item %s", required[i]); } + + /* And one item SWR-ACQ-02 requires NOT to exist. The chamber front end + * belongs to the supervisor, which linearises for type K; an item that let + * this side change that would make the backstop mean something else, and + * one that was offered and then ignored would be worse, because an + * operator can set it and believe it. */ + CHECK_MSG(kiln_config_find("sense.tc_type") == NULL, + "SWR-ACQ-02: the chamber TC type is configurable again"); } /* @@ -423,3 +432,105 @@ KILN_TEST(encode_refuses_a_buffer_that_is_too_small) CHECK_ERR(kiln_config_encode(&cfg, tiny, sizeof(tiny), NULL), KILN_ERR_NO_SPACE); CHECK_ERR(kiln_config_encode(NULL, tiny, sizeof(tiny), NULL), KILN_ERR_INVALID_ARG); } + +/* =========================================================================== + * SCHEMA 2: A REMOVED FIELD (SWR-CFG-05, SWR-ACQ-02, tasklist R9) + * =========================================================================== + * Schema 1 carried `sense.tc_type` as one byte immediately before + * `case_tc_type`. SWR-ACQ-02 took it away, because the chamber front end + * belongs to the supervisor and an item that let this side change the type + * would change what the supervisor's backstop means. + * + * Every earlier migration added fields, which a prefix copy handles. A removal + * in the middle shifts every byte after the hole, and a prefix copy would land + * the whole sensing, control and network block one byte out of place: the + * stored line filter frequency would read as half of itself plus half a + * calibration offset, and the CRC would have passed on the way in, so nothing + * would report it. The device would come back from an update quietly + * mis-calibrated. + * + * This forges a real version 1 blob and checks the fields on both sides of the + * hole, because that is the only way to know the splice is right. + */ + +/* + * @relation(SWR-CFG-05, scope=function) + */ +KILN_TEST(swrcfg05_a_version_1_blob_survives_the_removed_field) +{ + /* The version 1 layout is the version 2 layout with one extra byte at + * offsetof(case_tc_type). Built by taking a version 2 encoding apart and + * inserting the byte, which is exactly what the old firmware wrote. */ + kiln_config_t in; + kiln_config_defaults(&in); + + /* Distinctive values on both sides of the hole, chosen in range so that a + * clamp cannot hide a misplaced byte. */ + in.max_temp_c = 1150.0f; /* before */ + in.case_tc_type = (uint8_t)KILN_TC_TYPE_N; + in.line_filter_hz = 60u; /* after, and the one a shift ruins */ + in.filter_tau_s = 7.5f; + in.cal_offset_c = -3.25f; + in.cal_gain = 1.002f; + in.log_interval_s = 30u; + in.dim_timeout_s = 45u; + CHECK_OK(kiln_config_validate(&in, nullptr)); + + uint8_t v2[1024]; + size_t v2len = 0; + CHECK_OK(kiln_config_encode(&in, v2, sizeof(v2), &v2len)); + + constexpr size_t HDR = 8u; + const size_t hole = offsetof(kiln_config_t, case_tc_type); + const size_t v1_payload = sizeof(kiln_config_t) + 1u; + + uint8_t v1[1200]; + memset(v1, 0, sizeof(v1)); + /* Header: magic as encoded, version 1, the longer payload. */ + memcpy(v1, v2, HDR); + v1[4] = 1u; v1[5] = 0u; + v1[6] = (uint8_t)(v1_payload & 0xFFu); + v1[7] = (uint8_t)(v1_payload >> 8u); + /* Payload: everything before the hole, the removed byte, then the rest. */ + memcpy(&v1[HDR], &v2[HDR], hole); + v1[HDR + hole] = (uint8_t)KILN_TC_TYPE_S; /* the field that went away */ + memcpy(&v1[HDR + hole + 1u], &v2[HDR + hole], sizeof(kiln_config_t) - hole); + const size_t crc_at = HDR + v1_payload; + const uint16_t crc = kiln_crc16(v1, crc_at); + v1[crc_at] = (uint8_t)crc; + v1[crc_at + 1u] = (uint8_t)(crc >> 8u); + + kiln_config_t out; + kiln_config_defaults(&out); + /* Migrated, so the caller is told to write it back. */ + CHECK_ERR(kiln_config_decode(v1, crc_at + 2u, &out), KILN_ERR_UNSUPPORTED); + CHECK_EQ_UINT(out.schema_version, KILN_CFG_SCHEMA_VERSION); + + /* Before the hole. */ + CHECK_NEAR(out.max_temp_c, 1150.0f, 0.01f); + + /* At the hole: the enclosure type, which in version 1 sat one byte later. */ + CHECK_EQ_UINT(out.case_tc_type, (uint8_t)KILN_TC_TYPE_N); + + /* After the hole, which is where a prefix copy would have gone wrong. */ + CHECK_EQ_UINT(out.line_filter_hz, 60u); + CHECK_NEAR(out.filter_tau_s, 7.5f, 0.001f); + CHECK_NEAR(out.cal_offset_c, -3.25f, 0.001f); + CHECK_NEAR(out.cal_gain, 1.002f, 0.0001f); + CHECK_EQ_UINT(out.log_interval_s, 30u); + CHECK_EQ_UINT(out.dim_timeout_s, 45u); + + /* And the whole thing is still a valid configuration, which is what the + * device is about to run a kiln on. */ + CHECK_OK(kiln_config_validate(&out, nullptr)); + + /* The migration is not a one-way street into nonsense: re-encoded and + * decoded, it is now a clean version 2 blob. */ + uint8_t again[1024]; + size_t again_len = 0; + CHECK_OK(kiln_config_encode(&out, again, sizeof(again), &again_len)); + kiln_config_t back; + CHECK_OK(kiln_config_decode(again, again_len, &back)); + CHECK_EQ_UINT(back.line_filter_hz, 60u); + CHECK_EQ_UINT(back.case_tc_type, (uint8_t)KILN_TC_TYPE_N); +} diff --git a/tasklist.md b/tasklist.md index d188283..d127542 100644 --- a/tasklist.md +++ b/tasklist.md @@ -794,11 +794,21 @@ weakness first, the deadline second, the sentence third, the engineering last. peak, and validation on start is what refuses that firing rather than running it into a limit. -- [ ] **P4. Untested against a client.** It compiles and passes analysis. No - request has been made of it. The host API suite covers every route's - behaviour, so what is unverified is specifically the socket half: URI - splitting, chunked streaming, the four-session limit, and whether a slow - client can hold a buffer long enough to matter. +- [ ] **P4. The API has been driven by a client; `esp_http_server` has not.** + Narrowed on 2026-10-10 rather than closed. The development harness is a real + socket server running the same handler code, and it has now answered real + requests over TCP: the program routes, an id past the end as `404`, the + storage response, a log query with its chunk framing and terminator, and four + SSE frames in three and a half seconds each carrying a parseable payload. + That found a bug no unit test would have, the harness announcing + `Transfer-Encoding: chunked` and then sending unframed SSE. + + What remains unverified is specifically `esp_http_server`, which the harness + does not use and which no host test can reach: URI splitting in its parser, + `httpd_resp_send_chunk` against a real client, the socket limit with two + sessions parked on streams, the async handler's lifetime when a browser tab + closes mid-push, and whether a slow client can hold the shared 4 kB response + buffer long enough to matter. All of it needs the device. --- @@ -869,15 +879,30 @@ Designed in [`docs/safety-supervisor.md`](docs/safety-supervisor.md). Nothing below can start until `R1` to `R4` are answered, because each of them changes either the supervisor's pin count or a requirement. -- [ ] **R9. Retire `sense.tc_type` from the chamber channel.** The reason is - now in the code: `kiln_suplink`'s `configure` returns - `KILN_ERR_UNSUPPORTED`, because the ESP32 no longer owns that front end. - The config item is therefore dead on this side. `SWR-ACQ-02` now - fixes the chamber couple as type K, and after `SWA-22` the ESP32 does not - configure that front end at all, so the config item is meaningless on this - side. Removing it is a schema change (`KILN_CFG_SCHEMA_VERSION`, - `SWR-CFG-05` migration), so do it in the same commit as the rest of the - supervisor's firmware work rather than bumping the schema twice. +- [x] **R9. `sense.tc_type` is retired. Done 2026-10-10.** The chamber front + end belongs to the supervisor, which linearises for type K, so an item that + let this side change the type would change what the supervisor's backstop + means. `kiln_suplink`'s `configure` already refused, which made the config + item a setting an operator could set and believe while nothing acted on it. + + `KILN_CFG_SCHEMA_VERSION` is 2, and the migration is the interesting part. + Every earlier version change added fields, which a prefix copy handles; this + one *removes* a byte from the middle, so every field after the hole sits one + lower. A prefix copy would have landed the whole sensing, control and network + block one byte out of place -- the stored line filter frequency reading as + half of itself plus half a calibration offset -- and the CRC would have + passed on the way in, so nothing would have reported it. A device would come + back from an update quietly mis-calibrated. + + `kiln_config_decode` therefore splices version 1 explicitly, which is + `SWR-CFG-05`'s "older: migrate" met rather than answered with defaults. The + test forges a real version 1 blob and checks fields on both sides of the hole + plus a re-encode round trip. `SWR-CFG-02` records that there is no chamber TC + type, and a test asserts the item does not come back. + + `kiln_app_boot` now passes `KILN_TC_TYPE_K` to the chamber port from the code + rather than from configuration: refused on the product, which is the point, + and correct on the simulated build where the port accepts it. - [ ] **R5. Write the requirement deltas.** Six new requirements and nine changed ones, listed in section 11 of the design. `SWR-SAF-23` and `SWR-ACQ-02` are From ada5a965f049b5e3656cece5517d829854ff851b Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:25:02 +0200 Subject: [PATCH 12/14] test: silence on the supervisor link takes the heat away, end to end R8's remaining item. Each stage of the chain was tested and the chain was not: test_suplink proved that a quiet link and a stale sequence number both become KILN_TC_FAULT_COMMS, test_safety_thermal proved that bit becomes KILN_FAULT_TC_COMMS, and other tests proved a latched fault withholds heat. An assembly of proven parts is not a proven assembly, and the chamber temperature enters this firmware at exactly one place, over a wire: if silence there does not stop the heater, nothing else in the safety argument matters. Two integration tests now run the real application with the real decoder bound as the chamber port, the way main.cpp binds it on the board, against the simulated plant: - **Silence.** The board is pulled, the wire breaks, the other processor hangs, and nothing announces it. After SWR-SAF-04's grace the fault latches as TC_COMMS, authority is withdrawn and the contactor opens. - **A supervisor repeating itself.** Frames keep arriving, the CRC is good, the temperature is plausible, and the sequence number never advances -- a transmit buffer being replayed, or a processor stuck in a loop. This is the failure that looks like health: a link check counting bytes would see a working channel and let the kiln run on a reading from minutes ago. The test asserts frames really were arriving throughout, so it cannot pass as the silence case wearing a different hat. Both assert the kiln was heating before the failure, because a test where the heat never came on would pass the interesting half for the wrong reason. 31 integration tests, 26/26 suites, clean under ASan/UBSan and clang-tidy 20. --- .../host/integration/test_integration.cpp | 199 ++++++++++++++++++ tasklist.md | 30 ++- 2 files changed, 224 insertions(+), 5 deletions(-) diff --git a/firmware/controller/test/host/integration/test_integration.cpp b/firmware/controller/test/host/integration/test_integration.cpp index 542244e..2ddc351 100644 --- a/firmware/controller/test/host/integration/test_integration.cpp +++ b/firmware/controller/test/host/integration/test_integration.cpp @@ -15,6 +15,7 @@ #include "kiln_app/app.h" #include "kiln_core/faults.h" #include "kiln_core/profile.h" +#include "kiln_core/suplink.h" #include "kiln_sim/sim.h" /* --- the harness -------------------------------------------------------- */ @@ -881,3 +882,201 @@ KILN_TEST(a_multi_segment_firing_completes_without_a_spurious_fault) CHECK(r.app.record.peak_c > 750.0f); CHECK(kiln_current_energy_wh(&r.app.cur) > 0.0); } + +/* =========================================================================== + * THE SUPERVISOR LINK, END TO END (SWA-22, tasklist R8) + * =========================================================================== + * The pieces were each tested and the chain was not. test_suplink proves that + * a quiet link and a stale sequence number both become KILN_TC_FAULT_COMMS; + * test_safety_thermal proves that bit becomes KILN_FAULT_TC_COMMS; other tests + * prove a latched fault withholds heat. What R8 actually asks for is the + * property those three add up to, and an assembly of proven parts is not a + * proven assembly: the chamber temperature enters this firmware at exactly one + * place, over a wire, and if silence on that wire does not stop the heater then + * nothing else in the safety argument matters. + * + * So these two run the real app with the real link bound as the chamber port, + * the way main.cpp binds it on the board, and watch the heater. + */ + +namespace { + +/* The chamber port, as the product has it: a decoder fed bytes from a UART. */ +typedef struct { + kiln_suplink_t link; + kiln_port_tc_t tc; +} suplink_rig_t; + +sup_report_t sup_rep(uint8_t seq, float c) +{ + sup_report_t r = {}; + r.version = SUP_VERSION; + r.seq = seq; + r.chamber_dc = (int16_t)lroundf(c * (float)SUP_DC_PER_C); + r.cj_dc = (int16_t)(25 * SUP_DC_PER_C); + r.fault_bits = 0u; + r.flags = (uint8_t)(SUP_FLAG_PERMIT | SUP_FLAG_TC_VALID | SUP_FLAG_SELFTEST_OK); + r.trip_reason = SUP_TRIP_NONE; + return r; +} + +void sup_send(kiln_suplink_t *s, const sup_report_t &r) +{ + uint8_t f[SUP_FRAME_BYTES]; + (void)sup_encode(&r, f, sizeof(f)); + kiln_suplink_feed(s, f, sizeof(f)); +} + +/* One acquisition period's worth of link service, which on the board is the + * UART draining into the decoder and kiln_suplink_tick ageing it. */ +void sup_service(suplink_rig_t *sl, rig_t *r, uint8_t seq, bool speak) +{ + if (speak) { + sup_send(&sl->link, sup_rep(seq, (float)kiln_sim_temperature(&r->sim))); + } + kiln_suplink_tick(&sl->link, (float)ACQUIRE_DT); +} + +} // namespace + +/* + * @relation(SWA-22, scope=function) + */ +KILN_TEST(swa22_silence_on_the_link_takes_the_heat_away) +{ + static rig_t r; + static suplink_rig_t sl; + rig_init(&r, 20.0f); + + memset(&sl, 0, sizeof(sl)); + kiln_suplink_init(&sl.link); + kiln_suplink_bind(&sl.link, &sl.tc); + + /* Rebind the chamber port to the link, which is what the board does: the + * simulated plant still provides the physics, and the temperature reaches + * the core only by way of a frame. */ + kiln_app_ports_t ports = {}; + ports.tc = &sl.tc; + ports.case_tc = &r.ports.case_tc; + ports.heat = &r.ports.heat; + ports.current = &r.ports.current; + ports.counters = &r.ports.counters; + kiln_config_t cfg; + kiln_config_defaults(&cfg); + cfg.kp = 6.0f; cfg.ki = 0.02f; cfg.kd = 30.0f; + cfg.filter_tau_s = 1.0f; + cfg.holdback_band_c = 0.0f; + CHECK_OK(kiln_app_init(&r.app, &ports, &cfg)); + + kiln_program_t prog; + CHECK_OK(kiln_profile_example(0, &prog)); + + /* Let acquisition answer first, which over this port means letting a few + * frames arrive: a start is refused while the chamber has no reading, and + * on this device a reading is a frame. */ + uint8_t seq = 1; + for (int i = 0; i < 200; i++) { + rig_step(&r); + if ((i % 25) == 0) { sup_service(&sl, &r, seq++, true); } + } + CHECK_OK(kiln_app_start(&r.app, &prog)); + + /* A talking link: the kiln heats. Asserted, because a test where the heat + * never came on would pass the interesting half for the wrong reason. */ + for (int i = 0; i < 400; i++) { + rig_step(&r); + if ((i % 25) == 0) { sup_service(&sl, &r, seq++, true); } + } + CHECK_MSG(r.app.heat_authorised, "the kiln never started heating"); + CHECK_EQ_INT(r.app.fault, KILN_FAULT_NONE); + + /* Now the supervisor stops talking: the board is pulled, the wire breaks, + * the other processor hangs. Nothing announces it. */ + bool heat_seen = false; + kiln_fault_t fault = KILN_FAULT_NONE; + for (int i = 0; i < 4000; i++) { + rig_step(&r); + if ((i % 25) == 0) { sup_service(&sl, &r, seq++, false); } + if (r.app.fault != KILN_FAULT_NONE) { fault = r.app.fault; break; } + if (r.app.heat_authorised) { heat_seen = true; } + } + + /* SWR-SAF-04's grace, then a comms fault: not a temperature of zero, not + * the last reading held for ever. */ + CHECK_EQ_INT(fault, KILN_FAULT_TC_COMMS); + CHECK(heat_seen); /* it was heating before */ + CHECK(!r.app.heat_authorised); /* and is not now */ + + /* And the contactor is open, which is the part the kiln cares about. */ + rig_run(&r, 1.0); + CHECK(!kiln_sim_contactor(&r.sim)); +} + +/* + * @relation(SWA-22, scope=function) + */ +KILN_TEST(swa22_a_supervisor_repeating_itself_takes_the_heat_away_too) +{ + /* The failure that looks like health: frames keep arriving, the CRC is + * good, the temperature is plausible, and the sequence number never + * advances -- a supervisor stuck in a loop, or a transmit buffer being + * replayed. A link check that only counted bytes would see a healthy + * channel and let the kiln run on a reading from minutes ago. + */ + static rig_t r; + static suplink_rig_t sl; + rig_init(&r, 20.0f); + + memset(&sl, 0, sizeof(sl)); + kiln_suplink_init(&sl.link); + kiln_suplink_bind(&sl.link, &sl.tc); + + kiln_app_ports_t ports = {}; + ports.tc = &sl.tc; + ports.case_tc = &r.ports.case_tc; + ports.heat = &r.ports.heat; + ports.current = &r.ports.current; + ports.counters = &r.ports.counters; + kiln_config_t cfg; + kiln_config_defaults(&cfg); + cfg.kp = 6.0f; cfg.ki = 0.02f; cfg.kd = 30.0f; + cfg.filter_tau_s = 1.0f; + cfg.holdback_band_c = 0.0f; + CHECK_OK(kiln_app_init(&r.app, &ports, &cfg)); + + kiln_program_t prog; + CHECK_OK(kiln_profile_example(0, &prog)); + uint8_t seq = 1; + for (int i = 0; i < 200; i++) { + rig_step(&r); + if ((i % 25) == 0) { sup_service(&sl, &r, seq++, true); } + } + CHECK_OK(kiln_app_start(&r.app, &prog)); + for (int i = 0; i < 400; i++) { + rig_step(&r); + if ((i % 25) == 0) { sup_service(&sl, &r, seq++, true); } + } + CHECK(r.app.heat_authorised); + + /* Frames keep coming, all of them seq 9, all of them well formed. */ + const uint8_t stuck = 9; + kiln_fault_t fault = KILN_FAULT_NONE; + const uint32_t frames_before = sl.link.frames; + for (int i = 0; i < 4000; i++) { + rig_step(&r); + if ((i % 25) == 0) { + sup_send(&sl.link, sup_rep(stuck, 600.0f)); + kiln_suplink_tick(&sl.link, (float)ACQUIRE_DT); + } + if (r.app.fault != KILN_FAULT_NONE) { fault = r.app.fault; break; } + } + + CHECK_EQ_INT(fault, KILN_FAULT_TC_COMMS); + CHECK(!r.app.heat_authorised); + /* Frames really were arriving throughout: this is not the silence test + * wearing a different hat. */ + CHECK(sl.link.frames > frames_before + 10u); + + rig_run(&r, 1.0); + CHECK(!kiln_sim_contactor(&r.sim)); +} diff --git a/tasklist.md b/tasklist.md index d127542..d35166a 100644 --- a/tasklist.md +++ b/tasklist.md @@ -961,8 +961,28 @@ either the supervisor's pin count or a requirement. on every cold load. So `SWR-SAF-31` and `SYS-HW-21` are unchanged, the lid sense stays on the ESP32, and `Q5`'s branch is the only one section K loses. -- [ ] **R8. The supervisor's firmware, and its own test strategy.** Small - enough to read in one sitting, which is a design constraint and not an - aspiration. It needs its own host-testable core on the same argument as - `SWA-01`, and the link needs a test that proves the ESP32 withholds heat on - silence and on a stale sequence number, which are different failures. +- [x] **R8. The supervisor's firmware and its test strategy. Done 2026-10-10.** + The firmware is 3 488 bytes of program memory across four source files, which + is the design constraint met rather than aspired to, and it has its own + host-testable core on `SWA-01`'s argument: `trip`, `selfcheck`, `max31856` + and the wire protocol all build and run on a development host with no + hardware, with MC/DC over the trip logic and a generated Unit Design + Document. + + The link test this asked for is now there, and it was the piece genuinely + missing. Each stage was tested and the chain was not: `test_suplink` proved + silence and a stale sequence number both become `KILN_TC_FAULT_COMMS`, + `test_safety_thermal` proved that bit becomes `KILN_FAULT_TC_COMMS`, and + other tests proved a latched fault withholds heat. An assembly of proven + parts is not a proven assembly, and the chamber temperature enters this + firmware at exactly one place, over a wire. + + Two integration tests now run the real application with the real decoder + bound as the chamber port, the way `main.cpp` binds it on the board, and + watch the heater: **silence** takes the heat away and opens the contactor + after `SWR-SAF-04`'s grace, and so does **a supervisor repeating itself** -- + well-formed frames, good CRC, plausible temperature, sequence number + stuck -- which is the failure that looks like health and which a link check + counting bytes would have called a working channel while the kiln ran on a + reading from minutes ago. Both assert the kiln was heating first, so neither + can pass for the wrong reason. From dc41a09c6a72fce4e9393943947e1f1c49871c47 Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:35:28 +0200 Subject: [PATCH 13/14] docs: the supervisor enters the requirements, and section K leaves R5 and R7. The supervisor appeared nowhere in the system or safety requirements, which meant the design document was the only place it existed: a part holding one element of the contactor coil with no requirement to satisfy and nothing to verify against. Six new requirements in 02_system_req.sdoc: SYS-HW-26 the separate microcontroller: own clock, own watchdog, own front end on its own bus, own series element, own programming interface, and the external pull-down the reset window needs, since every GPIO is high impedance until the first instruction. SYS-HW-27 the link: one direction, report only, and no message able to raise the threshold, lower the sensitivity, clear the latch or silence the reports. A link that only carries information out cannot carry instruction in. SYS-HW-28 the local clear button, edge triggered, deliberate hold, and a line stuck low never arming it, so a short fails towards the latch holding. Also that the panel must not imply there is one acknowledgement, because there are two. SYS-SAF-25 the compiled-in backstop and the persistent-fault trip, and why a ceiling an operator can raise is not a backstop. SYS-SAF-26 the latch, what clears it, and why it deliberately does not survive a power cycle: the supervisor holds no non-volatile state, and the controller's persisted fault meets the system-level obligation. SYS-SAF-27 4 Hz reporting, silence as a sensor fault rather than a comms problem to retry, and the trip acting within one acquisition period whether or not anything is listening. Nine changed. SWR-ACQ-01 no longer says "the system shall measure" for a front end the controller cannot reach. SYS-HW-02 required "a shared SPI bus with individual chip selects", which is now exactly what must not happen: a supervisor whose chip select can be driven by a controller that crashed with a pin latched is not independent of it. SYS-HW-24 is superseded and rewritten rather than deleted, keeping its test points. SYS-HW-25's freewheel reference follows. SWA-04 says there are two supervisors and which one every sentence means. SWA-05 explains why the supervisor's element is deliberately a static level with a pull-down rather than a second charge pump. SG-01 is stronger on MCU failure and explicitly weaker on the sensor. SG-03's "two" became "at least two", and its "independent control" became true. safety.md gains L2S -- lettered, not numbered, because the layer numbers are cited from safety.sdoc, the firmware and the hardware design. Section 6's independence table gains the three rows the design asked for: a genuine yes for the supervisor against the controller sharing only the 3V3 rail; an explicit no for the chamber couple, which both parts read from one junction; and the thermal-versus-current row promoted from defence in depth to load-bearing, because with one couple the CT is the only physically independent detection channel left. The second part made the processor-failure row writable and the couple row worse at the same time, and both are said. The boundary diagram and the coil-path table follow: three elements break the coil now, not four, and a dashed line that is not a signal keeps the diagram from reading as two independent temperature channels. Section K is superseded with the chain it described. K1 is answered rather than mitigated, K2 dissolves, K5 drops from four hardware tests to three. K4 has grown rather than shrunk: the board carries the chain the design dropped and does not carry the supervisor at all. StrictDoc exports clean, 26/26 host suites unaffected. --- docs/02_system_req.sdoc | 98 +++++++++++++++++++++++- docs/03_software_req.sdoc | 10 ++- docs/04_software_arch.sdoc | 11 +++ docs/safety.md | 141 +++++++++++++++++++++++++++------- docs/safety.sdoc | 28 ++++++- tasklist.md | 153 ++++++++++++++++++++++++------------- 6 files changed, 356 insertions(+), 85 deletions(-) diff --git a/docs/02_system_req.sdoc b/docs/02_system_req.sdoc index add42ac..72bc0a4 100644 --- a/docs/02_system_req.sdoc +++ b/docs/02_system_req.sdoc @@ -132,7 +132,14 @@ VERIFICATION: I [REQUIREMENT] UID: SYS-HW-02 STATEMENT: >>> -The chamber and enclosure thermocouple front-ends shall be MAX31856 devices on a shared SPI bus with individual chip selects. +Both thermocouple front ends shall be MAX31856 devices, and they shall be on **separate SPI buses belonging to different microcontrollers**: + +- the **chamber** front end on the supervisor's bus ([LINK: SYS-HW-26]), read by the supervisor and by nothing else; +- the **enclosure** front end on the controller's bus, read by the controller. + +**Amended 2026-10-10 with the supervisor (SWA-22).** This said "a shared SPI bus with individual chip selects", which was true when one microcontroller read both. A shared bus is now exactly what must not happen: a supervisor whose chamber reading can be disturbed by the controller's bus traffic, or whose chip select can be driven by a controller that has crashed with a pin latched, is not independent of it, and independence is the whole of what the second part buys. + +One **common cause** survives any bus arrangement, and it is the chamber couple itself: both microcontrollers derive chamber temperature from the same junction, so the independence this requirement buys is independence against software and silicon, not against the sensor. It is recorded in `safety.md` section 6 and in section 9 of the supervisor's design rather than claimed away here. <<< PRIORITY: M VERIFICATION: I @@ -268,7 +275,18 @@ VERIFICATION: I [REQUIREMENT] UID: SYS-HW-24 STATEMENT: >>> -The thermocouple front ends' **`FAULT` outputs shall interrupt the contactor coil in hardware**, each through its own series switching element, so that a reported sensor fault removes the heater without the firmware taking part. Each `FAULT` net shall retain its own test point. The limits of this interlock shall be documented: the outputs are open-drain, so an **unpowered or absent** front end leaves the path closed, and the MAX31856 detects an open circuit only once its fault mask has been configured. The hardware interlock therefore covers faults the device actively reports, and [LINK: SWR-SAF-04] in firmware remains the cover for a dead or unconfigured front end. +**Superseded 2026-10-10 by [LINK: SYS-HW-26] and [LINK: SYS-SAF-25]. Rewritten rather than deleted, because the property it reached for is now delivered differently.** + +It required the thermocouple front ends' `FAULT` outputs to interrupt the contactor coil in hardware, each through its own series switching element, so that a reported sensor fault removed the heater with no firmware taking part. What that was buying is a sensor fault that stops the kiln without trusting the controller's firmware, and it bought it with an interlock whose limits had to be documented at length: the outputs are open-drain, so an unpowered or absent front end leaves the path closed, and a MAX31856 reports an open circuit only once its fault mask is configured, which is firmware work. The chain covered faults the device actively reported and nothing else, so [LINK: SWR-SAF-04] remained the real cover for a dead or unconfigured front end. + +The supervisor delivers the same property and more of it. It reads the chamber front end itself, decides on a persistent fault, and holds its own series element in the coil; a front end that is dead, absent or unconfigured is a front end that does not answer the supervisor either, and that is a trip rather than a documented limit. The discrete chain is therefore not built. + +What is **kept** from this requirement: + +- each `FAULT` net retains its own test point, which costs nothing and is how the front ends are diagnosed on a bench; +- the enclosure channel has no hardware interlock and never did; [LINK: SWR-SAF-11] in the controller's firmware is its cover, and that channel is deliberately outside the supervisor's remit so that an enclosure fault can never cause the supervisor's trip. + +The lid's series contact of [LINK: SYS-HW-21] is **unchanged** and stays in the coil. It is the one element in the design that depends on no firmware at all. <<< PRIORITY: M VERIFICATION: I, T @@ -281,10 +299,51 @@ Where a door or lid interlock is fitted, its switch shall be **normally closed** PRIORITY: S VERIFICATION: I, T +[REQUIREMENT] +UID: SYS-HW-26 +STATEMENT: >>> +**The independent safety supervisor shall be a separate microcontroller** with: + +- **its own clock and its own watchdog**, neither derived from nor serviced by the controller; +- **its own series interrupting element in the contactor coil path**, in addition to the elements of [LINK: SYS-SAF-03] and the lid contact of [LINK: SYS-HW-21]; +- **its own chamber thermocouple front end** on its own SPI bus ([LINK: SYS-HW-02]); +- **its own programming and debug interface**, brought out to test points, so that its firmware can be reviewed, built and flashed without the controller taking part. + +The permit output shall be **active high**, and the series element shall have an **external pull-down**: every GPIO is high impedance between reset and the first instruction, so without one there is a window at every supervisor reset in which the element's state is whatever the board leaks to. + +It can refuse heat on its own and it cannot command heat on its own: holding one element of a series path is permission, not control. That asymmetry is the whole design, and it is why the supervisor needs no safety case for its own failure to permit -- a supervisor that fails off is a kiln that does not fire. +<<< +PRIORITY: M +VERIFICATION: I, T + +[REQUIREMENT] +UID: SYS-HW-27 +STATEMENT: >>> +**The link between the controller and the supervisor shall be point-to-point serial, one direction, and report only.** The supervisor transmits; it shall not act on anything received, and the controller shall have no electrical path able to change what the supervisor does. + +No message shall exist that can raise the supervisor's trip threshold, lower its sensitivity, clear its latch, or suppress its reports. The threshold is compiled in ([LINK: SYS-SAF-25]) and the latch is cleared locally ([LINK: SYS-HW-28]). + +The reason is the attack surface and the failure surface at once: a controller that can be compromised over the network, or that can simply crash with a transmitter stuck, must not be able to reach the part whose job is to disbelieve it. A link that only carries information out cannot carry instruction in. +<<< +PRIORITY: M +VERIFICATION: I, T + +[REQUIREMENT] +UID: SYS-HW-28 +STATEMENT: >>> +**The supervisor's latch shall be cleared by a local action at the panel**, by a button wired to the supervisor and to nothing else, or by a power cycle. + +The button shall be edge triggered and shall require a deliberate hold, and a line stuck low shall never arm it, so that a short circuit fails towards the latch holding rather than towards it clearing. + +The panel shall not imply that there is one acknowledgement. There are two and they are not the same: this one clears the supervisor, and the controller's own latched fault ([LINK: SWR-SAF-17]) is acknowledged at the display. An operator cannot see the difference, so the labelling has to make it. +<<< +PRIORITY: M +VERIFICATION: I, T + [REQUIREMENT] UID: SYS-HW-25 STATEMENT: >>> -The contactor coil's freewheel diode shall be connected **across the coil itself**, on the kiln side of every series interrupting element of [LINK: SYS-HW-21] and [LINK: SYS-HW-24], so that opening any of them leaves the coil current a path to decay through. A freewheel path referenced to the raw supply instead would put the inductive transient across the opening contacts, eroding the lid switch and stressing the sense divider. +The contactor coil's freewheel diode shall be connected **across the coil itself**, on the kiln side of every series interrupting element of [LINK: SYS-HW-21] and [LINK: SYS-HW-26], so that opening any of them leaves the coil current a path to decay through. A freewheel path referenced to the raw supply instead would put the inductive transient across the opening contacts, eroding the lid switch and stressing the sense divider. <<< PRIORITY: M VERIFICATION: I @@ -341,6 +400,39 @@ The heating output stage shall consist of an SSR for modulation **in series with <<< VERIFICATION: I +[REQUIREMENT] +UID: SYS-SAF-25 +STATEMENT: >>> +**An independent supervisor shall de-energise the heater above a hard-coded absolute chamber temperature, and on a persistent chamber thermocouple fault, independently of the controller.** + +The threshold shall be **compiled into the supervisor's firmware** and shall not be reachable by configuration, by the link, or by any other means short of reflashing that part. A ceiling an operator can raise is a ceiling, not a backstop: [LINK: SWR-SAF-23]'s configurable maximum is the operator's limit, and this is the one that exists for when the controller's own rules, or the controller itself, have stopped working. The two values are stated together wherever either appears, because limits that can be edited apart eventually cross. + +It supersedes the hardware fault chain of [LINK: SYS-HW-24], and it is a stronger claim than that chain could make: a front end that is dead, absent or unconfigured does not answer the supervisor, and not answering is a trip. +<<< +VERIFICATION: A, T + +[REQUIREMENT] +UID: SYS-SAF-26 +STATEMENT: >>> +**The supervisor's trip shall latch**, and shall be clearable only by a power cycle or the local action of [LINK: SYS-HW-28] -- never by a command over the link ([LINK: SYS-HW-27]). + +A latch that the controller could clear is not a latch: the controller is the part that may have failed, and the condition that tripped the supervisor is one the controller either did not see or did not act on. Somebody has to go and look at the kiln. + +The latch is **not required to survive a power cycle**, and that is deliberate rather than an omission. The supervisor holds no non-volatile state at all, so it cannot be left in a state that a reflash or a flat battery changes, and the system-level obligation to remember a fault across a power loss is met by the controller's persisted fault ([LINK: SWR-SAF-17]). Section 3 of the supervisor's design states what that leaves: a power cycle clears the supervisor's own memory of why it tripped, and the controller's record is what the operator is shown. +<<< +VERIFICATION: A, T + +[REQUIREMENT] +UID: SYS-SAF-27 +STATEMENT: >>> +**Chamber temperature shall be acquired by the supervisor and reported at not less than 4 Hz, and a stale report shall be treated by the controller as a sensor fault.** + +The controller has no other source of chamber temperature, so the report rate is the controller's sample rate and silence on the link is indistinguishable from a dead front end -- which is why it is treated as one rather than as a communications problem to retry. + +**The supervisor's own trip shall not depend on the link.** It shall act within one acquisition period of the condition arising, whether or not anything is listening. The link exists so that the controller can display a temperature and say why the kiln stopped; the protection works with the link unplugged. +<<< +VERIFICATION: A, T + [REQUIREMENT] UID: SYS-SAF-24 STATEMENT: >>> diff --git a/docs/03_software_req.sdoc b/docs/03_software_req.sdoc index be1b420..0cda039 100644 --- a/docs/03_software_req.sdoc +++ b/docs/03_software_req.sdoc @@ -413,7 +413,15 @@ TITLE: 3.1 Temperature acquisition (FR-ACQ) [REQUIREMENT] UID: SWR-ACQ-01 STATEMENT: >>> -The system shall measure chamber temperature with a MAX31856 thermocouple front-end over SPI. +Chamber temperature shall be measured with a MAX31856 thermocouple front end over SPI **by the independent supervisor**, and reported to the controller over the link of [LINK: SYS-HW-27] at not less than 4 Hz. The **enclosure** channel's MAX31856 remains the controller's own, on the controller's bus. + +**Amended 2026-10-10 with the supervisor (SWA-22).** This said "the system shall measure", which was accurate when one microcontroller read both front ends. It no longer is, and the difference matters to every requirement downstream: the controller does not own the chamber front end, cannot configure it ([LINK: SWR-ACQ-02]), and has no access to it other than a frame arriving on a serial line. + +What that makes the controller's obligation here: + +- **A report is a sample.** The link's rate is the controller's sample rate; there is nothing behind it to poll faster. +- **Silence is a sensor fault, not a comms problem.** A link that has gone quiet, or that is repeating a sequence number, presents to the controller exactly as a front end that has stopped answering, and is handled by [LINK: SWR-SAF-04]'s grace and latch rather than retried. That is tested end to end: the kiln is heating, the link stops, the fault latches and the contactor opens. +- **A reading is not trusted further than its source.** The supervisor's own fault bits and its trip reason come up the same link and are displayed, never acted on as permission. <<< PRIORITY: M VERIFICATION: T diff --git a/docs/04_software_arch.sdoc b/docs/04_software_arch.sdoc index 54861c1..db6de8a 100644 --- a/docs/04_software_arch.sdoc +++ b/docs/04_software_arch.sdoc @@ -109,6 +109,13 @@ RELATIONS: 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. @@ -131,6 +138,10 @@ RELATIONS: 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. diff --git a/docs/safety.md b/docs/safety.md index a02a253..c65a220 100644 --- a/docs/safety.md +++ b/docs/safety.md @@ -81,26 +81,41 @@ flowchart TB KILN["Kiln body, elements,
lid interlock, wiring"] end subgraph IN["Inside the boundary, Safe Kiln Controller"] - MCU["ESP32-S3 firmware:
control + safety supervisor"] - CP["Charge pump → contactor coil"] + MCU["ESP32-S3 firmware:
control + safety task"] + SUPV["STM32G031 supervisor:
backstop + permit (SWA-22)"] + CP["Charge pump → coil element"] CON["Safety contactor"] SSR["Zero-cross SSR"] - TC["2 × MAX31856
chamber + enclosure"] + TCC["MAX31856, chamber
on the supervisor's bus"] + TCE["MAX31856, enclosure
on the controller's bus"] CT["Current transformer"] BUZ["Buzzer"] end SUP --> CUT --> CON --> SSR --> KILN MCU --> CP --> CON + SUPV -->|"permit, a second
series element"| CON MCU --> SSR - TC --> MCU + TCC --> SUPV + TCC -.->|"one junction,
the common cause"| TCE + SUPV -->|"report only, 4 Hz"| MCU + TCE --> MCU CT --> MCU MCU --> BUZ --> OP OP -.->|"acknowledge, abort,
isolate at supply"| MCU + OP -.->|"clear the latch
(SYS-HW-28)"| SUPV ``` -**Inside the boundary.** The firmware, the two switching devices and their -drive circuits, the two thermocouple front ends, the current transformer and -its conditioning, and the annunciation. +The dashed line between the two front ends is not a signal. It is there because +the diagram would otherwise suggest two independent temperature channels: both +MAX31856 read **one chamber junction**, which is the common cause §6 records and +the one thing the second part does not remove. + +**Inside the boundary.** Both firmwares, the switching devices and their drive +circuits, the two thermocouple front ends, the current transformer and its +conditioning, and the annunciation. The supervisor is inside the boundary and +is **not** a substitute for the independent cutout outside it: it shares the +chamber couple, the 3V3 rail and the enclosure, and `SYS-HW-13` stays mandatory +for exactly those reasons. **Outside the boundary, but relied upon.** The supply protection, the independent cutout, the kiln's own construction and the operator. Each is the @@ -152,14 +167,16 @@ software being correct. Nothing below is a substitute for anything above it. ```mermaid flowchart TB L1["**L1: Control**
setpoint clamped to the configured max,
configured max clamped to 1350 °C (SWR-SAF-23)"] - L2["**L2: Safety supervisor**
18 rules, 100 ms cycle, sole heat authority (SWA-04)
thermal channel + current channel"] + L2["**L2: Safety supervisor (a task)**
18 rules, 100 ms cycle, sole heat authority (SWA-04)
thermal channel + current channel"] + L2S["**L2S: Independent supervisor (a part)**
second MCU, own clock, watchdog, front end
1350 °C backstop, own series element (SWA-22)"] L3["**L3: Heat authority hardware**
charge pump: a square wave, not a level (SWA-05)
contactor in series with the SSR (SYS-SAF-03)"] L4["**L4: Reset to safe**
task + RTC watchdog, brownout detector,
output pull-downs (SYS-HW-08), latched fault in NVS"] L5["**L5: Independent hardware cutout**
own sensor, own contacts, outside this project (SYS-HW-13)"] L6["**L6: Installation and operator**
supply protection, competent installation,
attendance (SYS-ASM-04, SYS-ASM-06)"] - L1 --> L2 --> L3 --> L4 --> L5 --> L6 + L1 --> L2 --> L2S --> L3 --> L4 --> L5 --> L6 style L1 fill:#e8f0fe,stroke:#4a76c4 style L2 fill:#e8f0fe,stroke:#4a76c4 + style L2S fill:#fef0e0,stroke:#c48a4a style L3 fill:#fef0e0,stroke:#c48a4a style L4 fill:#fef0e0,stroke:#c48a4a style L5 fill:#e9f6ea,stroke:#4aa052 @@ -200,6 +217,61 @@ of [§6](#6-independence): gated to the commanded output state (`SWA-17`) so that the measurement is a statement about the *relay*, not an average over a mostly-off window. +### 5.2a L2S, The independent supervisor + +**It is lettered rather than numbered, and that is deliberate.** The layer +numbers are cited from `safety.sdoc`'s `LAYERS` fields, from the firmware and +from the hardware design; renumbering L3 to L4 and so on to open a gap would +break every one of those citations to no benefit, which is the same reasoning +the system requirements give for their own numbering. + +A second microcontroller, an STM32G031K8T6, with its own clock, its own +windowed watchdog cross-checked against an independent oscillator, its own +MAX31856 on its own SPI bus, and **one element of the contactor coil** +(`SYS-HW-26`). It reads the chamber couple, and above a backstop compiled into +its firmware, or on a chamber thermocouple fault that persists, it opens its +element. The whole of it is 3 488 bytes of program memory across four source +files, which is a design constraint and not an aspiration: it is small enough +to be read in one sitting by somebody assessing it. + +What it protects against is **the failure of the entire controller**, which is +the one thing no layer above it could cover. L2 is a task on the processor that +may itself be the problem: a controller that stops with the charge-pump pin +toggling, or that fires on a reading it has mis-scaled, is a single failure that +L1 and L2 are both downstream of. Until this layer existed, the honest answer to +that was the operator's own hardware cutout at L5. + +What it deliberately **does not** do, each because adding it would cost more +than it buys: + +- **It runs no rules of its own beyond two.** Runaway detection, the current + rules, the enclosure channel, hold-back, the operator's configurable maximum: + all of those stay at L2, where the configuration is. A backstop with + configuration is not a backstop. +- **It takes no instruction.** The link is one direction and report only + (`SYS-HW-27`). No message can raise its threshold, lower its sensitivity, + clear its latch or silence it, because the part that might be compromised or + crashed is the part at the other end. +- **It cannot command heat.** Holding one element of a series path is + permission, not control: it can refuse heat alone and cannot grant it alone. + So it needs no safety case for failing to permit -- a supervisor that fails + off is a kiln that does not fire. +- **It keeps no non-volatile state.** Its latch does not survive a power cycle, + by design, and the system-level obligation to remember a fault across a power + loss is met by the controller's persisted fault (`SWR-SAF-17`). Section 3 of + [`safety-supervisor.md`](safety-supervisor.md) states the gap that leaves. + +Its latch is cleared by a button at the panel wired to it and to nothing else +(`SYS-HW-28`). There are therefore **two acknowledgements** in this system and +they are not the same: this one, and the controller's own latched fault at the +display. An operator cannot see the difference, so the panel has to make it. + +**No performance level and no SIL is claimed for it**, and §9 of this document +is not improved on by its existence. What is claimed is narrower and checkable: +a documented language subset, the diagnostics of EN IEC 60730-1 Annex H listed +in its unit design document, MC/DC over its trip logic, and an architecture that +keeps the safety function on a part the network cannot reach. + ### 5.3 L3, The heat authority hardware This is the layer that does not depend on the firmware being correct, only on @@ -219,30 +291,37 @@ failing to run. > circuit as safety-critical, and the HIL suite verifies contactor release by > halting the safety task. -**Four independent things now break the coil, three of them without firmware.** +**Three independent things break the coil, and they do not share a decider.** The coil circuit is a series chain, and any element opening drops the contactor: -| Element | Opens when | Firmware involved? | +| Element | Opens when | What has to be working | |---|---|---| -| Lid switch contacts (`SYS-HW-21`) | the door opens | **No.** Mechanical, in the coil circuit. | -| `Q6`, gated by the chamber `FAULT` output (`SYS-HW-24`) | the chamber front end reports a fault | No, once configured. See the limit below. | -| `Q5`, gated by the enclosure `FAULT` output (`SYS-HW-24`) | the enclosure front end reports a fault | As above. | -| `Q3`, gated by the charge pump (`SWA-05`) | the supervisor stops toggling | Only in that it must keep running. | +| Lid switch contacts (`SYS-HW-21`) | the door opens | **Nothing.** A mechanical contact in the coil circuit. | +| The supervisor's permit (`SYS-HW-26`) | the chamber passes the compiled-in backstop, or the chamber front end stops answering, or the supervisor itself stops | The supervisor's own firmware, on its own clock, on a part the network cannot reach. | +| `Q3`, gated by the charge pump (`SWA-05`) | the controller's safety task stops toggling | Only that the controller is still executing; not that it is correct. | -The freewheel diode sits **across the coil**, on the kiln side of all four +The freewheel diode sits **across the coil**, on the kiln side of all three (`SYS-HW-25`), so opening any of them leaves the coil current somewhere to decay and does not put the transient across the opening contacts. -The limit of the two `FAULT` elements is worth stating rather than discovering: -the outputs are open-drain, so an **unpowered or absent** front end leaves its -transistor on and the path closed, and the MAX31856 detects an open circuit -only after its fault mask has been configured. They interrupt on faults the -device actively *reports*. A dead or unconfigured front end is covered by -`SWR-SAF-04` in firmware (fault 5, front-end communication failure), not here. The -lid switch carries no such caveat: it is a contact in the circuit. +**It was four, two of them gated by the front ends' `FAULT` outputs +(`SYS-HW-24`), and that chain is superseded** by the row above it. The limit of +those two elements had to be stated rather than discovered: the outputs are +open-drain, so an unpowered or absent front end left its transistor on and the +path closed, and a MAX31856 reports an open circuit only after its fault mask +has been configured, which is firmware work. They interrupted on faults the +device actively *reported*, and a dead or unconfigured front end was covered by +`SWR-SAF-04` in firmware rather than by the hardware. + +The supervisor has neither caveat, because it is not gated by a pin: a front end +that is dead, absent or unconfigured is a front end that does not answer the +supervisor, and not answering is a trip. The enclosure channel loses its +hardware element and keeps `SWR-SAF-11` in firmware, deliberately: it is outside +the supervisor's remit so that a failed enclosure probe can never stop a firing +mid-glaze, which is how protections come to be disabled (`HZ-10`). In series with the charge-pump-driven contactor sits the SSR, modulating under -the supervisor's duty command (`SYS-SAF-03`). Two devices, two drive circuits, two +the controller's duty command (`SYS-SAF-03`). Two devices, two drive circuits, two failure modes. All heater and contactor control outputs carry external pull-downs to the de-energised state and avoid strapping pins and pins that glitch during reset (`SYS-HW-08`), so the safe state holds through the window before @@ -286,12 +365,22 @@ section states where the claim is genuine and where it is not. | Pair | Independent? | Shared element, what defeats both | |---|---|---| | L1 control vs. L2 thermal rules | **No** | The same chamber thermocouple. A plausible-but-wrong reading (HZ-03) misleads both. This is the entire reason `SWR-SAF-04`–`SWR-SAF-06` exist: they do not measure temperature, they interrogate the *measurement*, fault bits, polarity, and whether the number moves at all. | -| L2 thermal rules vs. L2 current rules | **Yes, physically** | Different sensor (CT vs. thermocouple), different quantity (amps vs. degrees), different front end, different failure modes. They share the supervisor task and the MCU. | +| **L2S supervisor vs. the controller (L1–L4)** | **Yes** | Separate silicon, separate firmware in a separate language subset, separate clock, separate watchdog, separate thermocouple front end on a separate bus, separate series element in the coil, separate programming interface. **The only thing shared is the 3V3 rail**, and a failure of that rail removes heat by construction, since every element in the coil path fails towards de-energised. This is the row the second part was added to be able to write, and it is the only "yes" in this table that covers the failure of a whole processor. | +| **L2S supervisor vs. the controller, on the chamber couple** | **No** | **One thermocouple feeds both microcontrollers.** Each has its own MAX31856, its own bus and its own linearisation, so a front-end failure is independent; the *junction* is not. A couple that reads plausibly low in a way both parts believe defeats the thermal channel at both L2 and L2S simultaneously. This is the common cause the architecture does not remove, and the reason the row below is load-bearing rather than defence in depth. It is argued at length in §9 of [`safety-supervisor.md`](safety-supervisor.md), including the two-couple option that was declined and why. | +| L2 thermal rules vs. L2 current rules | **Yes, physically, and now load-bearing** | Different sensor (CT vs. thermocouple), different quantity (amps vs. degrees), different front end, different failure modes. They share the supervisor task and the MCU. **Promoted from defence in depth 2026-10-10**: with one chamber couple feeding both microcontrollers, the CT is the only physically independent detection channel left in the system. A kiln whose current monitoring is disabled or whose transformer was never fitted (`SWR-CUR-12`) has, for the shared-couple failure mode, exactly one channel and the operator's L5 cutout. That is a decision an installer can make; it should be made knowing this. | | L2 vs. L3 | **Yes, for the failure class that matters** | L3 does not depend on L2 being *correct*, only on it *running*. A logic error in a rule does not stop the toggle; a hang, crash or overrun stops it immediately. The converse is also true: L3 cannot detect anything, so a subtly wrong rule is L2's problem alone. | | L3 SSR vs. L3 contactor | **Yes** | Separate devices on separate outputs. A shorted SSR leaves the contactor able to interrupt; this is what `SWR-SAF-27`'s weld discrimination tests, and what makes "SSR shorted" a recoverable fault and "contactor welded" not. | -| L2+L3 vs. L4 watchdogs | **Partly** | Same MCU and same power rail. A supply fault defeats all three, and is then safe by construction, since every one of them fails towards de-energised. | +| L2+L3 vs. L4 watchdogs | **Partly** | Same MCU and same power rail. A supply fault defeats all three, and is then safe by construction, since every one of them fails towards de-energised. The supervisor's watchdog at L2S is not in this row: it is on the other part. | | L1–L4 vs. L5 cutout | **Yes, fully** | Nothing is shared: separate sensor, separate contacts, separate failure modes. This is why L5 is mandatory and why no amount of software may be offered as a substitute. | +**What the second part changed in this section**, stated plainly because it is +the point of the exercise: the table used to have no row in which the failure of +a whole microcontroller was covered by anything inside this project. It has one +now. What it still has no row for is the chamber couple, and adding the second +part made that *more* exposed rather than less, because it is now the only +shared element between two otherwise independent channels. Both things are true +at once and neither is a reason to leave the other unsaid. + **Defence in depth, stated as a rule rather than an aspiration.** The current rules are the *primary* detection of a failed relay, contactor or element, because they are fast and unambiguous. The thermal rules are retained diff --git a/docs/safety.sdoc b/docs/safety.sdoc index 61c4d85..94968b3 100644 --- a/docs/safety.sdoc +++ b/docs/safety.sdoc @@ -389,9 +389,15 @@ UID: SG-01 STATEMENT: >>> No single failure of software, MCU, sensor or switching device shall result in heating; every such failure shall result in heating being removed. <<< -LAYERS: L2, L3, L5 +LAYERS: L2, L2S, L3, L5 COMMENT: >>> Realised by [LINK: SYS-SAF-01] and by the whole of [`safety.md` §5](safety.md#5-the-protection-layers), which is why only the one requirement is named here. + +**Strengthened 2026-10-10 by the independent supervisor ([LINK: SYS-SAF-25]), and in one respect it has to be read more carefully than before.** + +"No single failure of **MCU**" was the weakest word in this goal while there was one microcontroller. A processor that stops with the charge-pump pin toggling, or that fires on a reading it has mis-scaled, is a single failure of the MCU that the MCU itself was being asked to catch; the honest position was that layers L2 and L3 are mostly *software on the part that may have failed*, with the operator's own hardware cutout ([LINK: SYS-HW-13]) behind them. That is now a second part, with its own clock, watchdog, front end and series element, and a failure of either processor removes heat. + +What a careful reader must not conclude is that the sensor clause is equally covered. **One thermocouple feeds both microcontrollers**, so a failure of that junction is a single failure that both channels see identically, and it is the common cause this architecture does not remove. It is detected rather than tolerated: an open, shorted or out-of-range couple is a fault to both parts, and the current rules of `SG-05` are the only physically independent detection channel left. Section 6 of `safety.md` records it as load-bearing for exactly that reason. <<< RELATIONS: - TYPE: Parent @@ -436,9 +442,22 @@ RELATIONS: [SAFETY_GOAL] UID: SG-03 STATEMENT: >>> -There shall be two series interrupting devices under independent control, so that one failing closed does not remove the ability to interrupt. +There shall be **at least two** series interrupting devices under independent control, so that one failing closed does not remove the ability to interrupt. +<<< +LAYERS: L2S, L3 +COMMENT: >>> +**Amended 2026-10-10: "two" became "at least two", and "independent control" became true.** + +The original pair is the SSR and the contactor of [LINK: SYS-SAF-03], each with its own output. They are two devices in series, which defeats one failing closed, but both outputs came from one microcontroller: a processor that failed with both pins asserted left both devices conducting, so "independent control" meant independent *drivers* rather than independent *deciders*. + +The coil path now has three elements that can open it, and they do not share a decider: + +- the controller's charge pump ([LINK: SWA-05]), which needs its safety task to keep toggling; +- the supervisor's permit ([LINK: SYS-HW-26]), on a different part with its own clock and watchdog, which needs the chamber below a compiled-in ceiling and the front end answering; +- the lid's series contact ([LINK: SYS-HW-21]), which depends on no firmware at all. + +Any one of the three opens the coil. The first two are now genuinely under independent control, which is what this goal was reaching for and could not previously claim. <<< -LAYERS: L3 RELATIONS: - TYPE: Parent VALUE: HZ-01 @@ -452,6 +471,9 @@ RELATIONS: - TYPE: Child VALUE: SYS-HW-07 ROLE: Realised by +- TYPE: Child + VALUE: SYS-HW-26 + ROLE: Realised by [SAFETY_GOAL] UID: SG-04 diff --git a/tasklist.md b/tasklist.md index d35166a..58f6e27 100644 --- a/tasklist.md +++ b/tasklist.md @@ -472,29 +472,37 @@ question is whether it is worth its cost. ## K. Hardware interlock chain -- [ ] **K1. The hardware TC interlock is not firmware-independent, and the - documentation now says so.** The `FAULT` outputs are open-drain: an unpowered - or absent front end leaves the path closed. The MAX31856 also detects an open - circuit only once its fault mask is configured, so out of reset the interlock - does not act. It covers faults the device actively reports; `SWR-SAF-04` in - firmware remains the cover for a dead or unconfigured front end. The lid - contact has no such caveat. - -- [ ] **K2. Should an enclosure thermocouple fault really stop the kiln?** - Both front ends are in the chain, because the request said fault pins. But - `SWR-SAF-11` (enclosure over-temperature) is a backstop, and a failed enclosure - probe killing a firing mid-glaze is a nuisance trip, which `HZ-10` says is - how protections get disabled. Consider a fitted-by-default `0R` in `Q5`'s - drain so the enclosure branch can be depopulated without cutting a track. - - Decide this together with the parts analysis that used to live in - `docs/bom-optimisation.md` (removed from the repository; see its history), - which reached the same question from the parts end: if this branch is depopulatable then the - MAX31856 on the enclosure channel has nothing left to justify it, since it is - bought for its `FAULT` pin rather than for measuring 40 to 90 degC. That - document's preferred option removes the channel entirely, using the chamber - front end's own cold-junction reading for `SWR-SAF-11` and a bimetallic cutout in - the coil for a hardware trip. +**Superseded 2026-10-10 by the independent supervisor (`R7`).** The discrete +chain in this section existed to put the thermocouple front ends' `FAULT` +outputs in series with the contactor coil (`SYS-HW-24`). The supervisor reads +the chamber front end itself, decides on a persistent fault, and holds its own +series element, which is the same property delivered without the chain's +caveats: a front end that is dead, absent or unconfigured does not answer the +supervisor either, and not answering is a trip rather than a documented limit. + +`SYS-HW-24` is rewritten rather than deleted, because what it reached for is now +delivered differently, and it keeps the one part worth keeping: each `FAULT` net +retains its own test point. + +- [x] **K1. Answered rather than mitigated.** The chain was not + firmware-independent: open-drain outputs leave the path closed when a front + end is unpowered or absent, and a MAX31856 reports an open circuit only once + its fault mask has been configured, which is firmware work. That is why this + was recorded as a limitation rather than a protection. The supervisor has + neither caveat. + +- [x] **K2. Dissolved.** The question was whether an enclosure thermocouple + fault should stop a firing, given that `SWR-SAF-11` is a backstop and a + nuisance trip is how protections get disabled (`HZ-10`). The enclosure + channel is not in the supervisor's remit at all, by decision: it stays an + ESP32 rule and can never cause the supervisor's trip. There is no branch to + depopulate because there is no chain. + +- [x] **K5. Reduced to three tests, and they are still hardware tests.** Four + elements in series became three: the lid contact, the controller's charge + pump, and the supervisor's permit. Each must drop the contactor on its own, + which is `G2`'s class of claim and cannot be settled in simulation. The + `FAULT`-pin tests are gone with the chain. - [ ] **K3. `D7` is an unwired LED.** Pre-existing, not from this change: the coil indicator's cathode is on `COIL_DRV` and its anode goes nowhere. It @@ -502,17 +510,13 @@ question is whether it is worth its cost. `LID_SWITCH` rather than `+5V` makes it indicate "coil actually energised" rather than "the MCU asked". -- [ ] **K4. The PCB carries section K, but is not routed.** - `update_pcb_from_schematic` has been run and the interlock chain's parts are - on the board (`J9`, `Q5`, `Q6`, `R28`–`R31`); placement is started, with 74 - footprints positioned. **There are no tracks and no vias on the board at - all**, and the coil interrupt is a mains-adjacent net, so routing it is not - a formality: see `A23`. - -- [ ] **K5. HIL: verify each interrupt separately.** Four elements in series - means four tests: open the lid, pull each `FAULT` low, and halt the safety - task. Each should drop the contactor on its own. This is the same class of - claim as `G2` and cannot be settled in simulation. +- [ ] **K4. The board carries the chain that is no longer wanted, and is not + routed.** `J9`, `Q5`, `Q6` and `R28`–`R31` are placed on the PCB for an + interlock the design has dropped, and the supervisor's own parts are not on + it at all: that is `R6`, the schematic. **There are no tracks and no vias on + the board**, and the coil interrupt is a mains-adjacent net, so routing it is + not a formality: see `A23`. Depopulating the chain's parts is the smaller + half of this; the supervisor and its series element are the larger. --- @@ -904,24 +908,69 @@ either the supervisor's pin count or a requirement. rather than from configuration: refused on the product, which is the point, and correct on the simulated build where the port accepts it. -- [ ] **R5. Write the requirement deltas.** Six new requirements and nine - changed ones, listed in section 11 of the design. `SWR-SAF-23` and `SWR-ACQ-02` are - done; still to do are `SWR-ACQ-01`, `SYS-HW-02`, `SYS-HW-24` superseded, `SWA-04`, - `SWA-05`, `SG-01`, `SG-03` and `safety.md` sections 5 and 6. - - With `R3` and `R4` settled, §6's independence table is now writable and says - three specific things. A new row for the supervisor against the ESP32 that - genuinely says **yes**: separate silicon, firmware, clock and watchdog, - sharing only the 3V3 rail. A new row, or an amendment to the L1-vs-L2 one, - recording that the chamber couple is shared by *both* MCUs, so the - independence is against software and not against a plausible-but-wrong - reading. And the existing "thermal rules vs. current rules" row promoted from - defence in depth to **load-bearing**, because with one couple the CT is the - only physically independent detection channel left in the system. - - `SWR-SAF-17` also needs a sentence: the supervisor's latch does not survive a - power cycle by design, and the system-level obligation is met by the ESP32's - persisted fault. Section 3 of the design states the gap that leaves. +- [x] **R5. The requirement deltas are written. Done 2026-10-10.** Six new + requirements and nine changed ones. The supervisor appeared nowhere in the + system or safety requirements before this, which meant the design document + was the only place it existed: a part holding one element of the contactor + coil, with no requirement to satisfy and nothing to verify against. + + **New**, in `02_system_req.sdoc`: `SYS-HW-26` the separate microcontroller + with its own clock, watchdog, front end, series element and programming + interface, and the external pull-down the reset window needs; `SYS-HW-27` the + one-direction report-only link, with no message able to raise the threshold + or clear the latch; `SYS-HW-28` the local clear button, edge triggered, a + stuck line never arming it; `SYS-SAF-25` the compiled-in backstop and + persistent-fault trip; `SYS-SAF-26` the latch and what clears it, including + why it deliberately does not survive a power cycle; `SYS-SAF-27` the 4 Hz + report, silence as a sensor fault, and the trip acting within one acquisition + period whether or not anything is listening. + + **Changed**: `SWR-ACQ-01` (the chamber front end is the supervisor's, and a + report is the controller's sample), `SYS-HW-02` (separate buses on different + microcontrollers, where a shared bus is now exactly what must not happen), + `SYS-HW-24` (superseded, rewritten, test points kept), `SYS-HW-25` (its + freewheel reference follows), `SWA-04` (two supervisors, and which one every + sentence means), `SWA-05` (the charge pump is one of two paths, and why the + supervisor's element is deliberately a static level instead), `SG-01` + (strengthened on MCU failure, and more exposed on the sensor), `SG-03` ("two" + became "at least two", and "independent control" became true). + `SWR-SAF-23` and `SWR-ACQ-02` were already done. + + `safety.md` gains **L2S**, lettered rather than numbered because the layer + numbers are cited from `safety.sdoc`, the firmware and the hardware design, + and renumbering to open a gap would break every citation to no benefit. Its + section says what the layer protects against -- the failure of the entire + controller, which no layer above it could cover -- and what it deliberately + does not do. + + `safety.md`'s system-boundary diagram and its coil-path table are updated + with it: three elements break the coil now rather than four, and the diagram + carries a dashed line between the two front ends that is not a signal, so + that it cannot be read as two independent temperature channels when both + MAX31856 read one junction. + + Section 6's independence table gains the three rows the design asked for: a + genuine **yes** for the supervisor against the controller, sharing only the + 3V3 rail; an explicit **no** for the chamber couple, which both + microcontrollers read from one junction; and the thermal-versus-current row + **promoted from defence in depth to load-bearing**, because with one couple + the CT is the only physically independent detection channel left. The second + part made the processor-failure row writable and the couple row worse at the + same time, and both are said. + +- [x] **R7. What the supervisor supersedes is removed. Done 2026-10-10.** + Section K is superseded: `K1` is answered rather than mitigated, `K2` + dissolves, `K5` drops from four tests to three. `SYS-HW-24` is rewritten + rather than deleted and keeps its test points. `SWR-SAF-31` and `SYS-HW-21` + are unchanged, as the design said they should be: the lid's series contact + depends on no firmware at all, costs nothing and stays, and the lid sense + stays the controller's input because `SWR-SAF-31`'s latch is gated on a + heating state only the controller knows. A supervisor latching on the lid + regardless would trip on every cold load. + + What is left in section K is `K3`, an unwired indicator LED that predates all + of this, and `K4`, which has grown rather than shrunk: the board carries the + chain the design has dropped and does not carry the supervisor at all. - [ ] **R11. The clear button on the panel.** `R4` decided a local button and the firmware implements it (edge triggered, held 0.5 s, and a line stuck low From 3e48ef599f4ffaedf12a599cb59c380b2477a34c Mon Sep 17 00:00:00 2001 From: Boernsman <5207214+Boernsman@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:36:19 +0200 Subject: [PATCH 14/14] docs(tasklist): close the duplicated R7, re-measure O2, and say what is left Housekeeping after the O/P/Q/R sweep. R7 appeared twice: the entry recording it done, and the original further down the section, which the replacement had skipped because the section is not in numeric order. O2's image size is re-measured at 945 kB after mDNS, the embedded assets, the event stream and the string table (of the growth from 888 kB, mDNS is 32 kB and the assets 18). O4 is checked off, its three code items being done and the hardware session belonging to O3. Section R's intro said nothing in it could start until R1 to R4 were answered. They are, and so are the firmware, the requirement deltas and the link tests: what is left there is the schematic and the panel button, both hardware. --- tasklist.md | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/tasklist.md b/tasklist.md index 58f6e27..91dbccf 100644 --- a/tasklist.md +++ b/tasklist.md @@ -646,17 +646,20 @@ weakness first, the deadline second, the sentence third, the engineering last. ## O. WiFi, FR-NET -- [ ] **O2. The hardware image is 940 kB, 54 % of the slot free.** Measured - 2026-10-09, replacing numbers that predated several releases of ESP-IDF. The +- [x] **O2. The hardware image is 945 kB, 54 % of the slot free.** A record + rather than a task, re-measured 2026-10-10 after mDNS, the embedded assets, + the event stream and the German strings all landed: 0xec520 bytes against a + 2 MB slot. The simulated build that CI publishes is **246 kB** and contains no WiFi and no HTTP server at all, which is worth keeping straight when reading a size report: `CONFIG_KILN_PLANT_SIM` compiles both out. `SWR-NFR-13` holds with room. The web assets measure **18 kB gzipped** in total against architecture 12.4's 44 kB budget (`index.html` 2.2, `app.css` - 3.6, `app.js` 8.6, `chart.js` 4.2). With them embedded and mDNS linked in, - the hardware image measures **940 kB, 54 % of the slot free**. That budget - had never been measured; it has now, and it passes. + 3.6, `app.js` 8.6, `chart.js` 4.2). That budget had never been measured; it + has now, and it passes. Of the growth since 888 kB, mDNS is 32 kB and the + assets 18 kB; the rest is the event stream, the string table and the + supervisor link. - [ ] **O3. Untested against a radio, and until today it did not even compile.** The claim here used to be "it compiles and passes analysis", and @@ -681,8 +684,9 @@ weakness first, the deadline second, the sentence third, the engineering last. containing symbols, power-cycle and confirm it comes back, then join a different network to confirm it replaces rather than accumulates. -- [ ] **O4. The provisioning AP is gone; WiFi is set up at the display.** - Done in firmware and unproven on hardware. `SWR-NET-02` and `SWR-NET-05` are +- [x] **O4. The provisioning AP is gone; WiFi is set up at the display.** + Done in firmware and unproven on hardware, which is `O3`'s session rather + than a task of its own. `SWR-NET-02` and `SWR-NET-05` are withdrawn, `SWR-NET-11` and `SWR-NET-12` replace them, `net.ap_ssid` and `net.ap_pass` leave the schema, and `TH-11`, `SRR-08` and `OQ-S4` close with them: the surface is deleted rather than regulated. @@ -879,9 +883,12 @@ weakness first, the deadline second, the sentence third, the engineering last. ## R. The independent safety supervisor, SWA-22 -Designed in [`docs/safety-supervisor.md`](docs/safety-supervisor.md). Nothing -below can start until `R1` to `R4` are answered, because each of them changes -either the supervisor's pin count or a requirement. +Designed in [`docs/safety-supervisor.md`](docs/safety-supervisor.md). That +intro said nothing below could start until `R1` to `R4` were answered, because +each of them changed the supervisor's pin count or a requirement. They are +answered, and the firmware, the requirement deltas and the link tests are done: +what is left in this section is **hardware**, which is `R6` the schematic and +`R11` the button on the panel. - [x] **R9. `sense.tc_type` is retired. Done 2026-10-10.** The chamber front end belongs to the supervisor, which linearises for type K, so an item that @@ -997,19 +1004,6 @@ either the supervisor's pin count or a requirement. `KILN_PIN_EXP_IO2` or `KILN_PIN_EXP_IO42`, one pin, receive only; `UART0` is the console and must not be used. -- [ ] **R7. Remove what the supervisor supersedes.** `Q5`, `Q6` and `R28` to - `R31` of section K, and `SYS-HW-24` rewritten rather than deleted, because the - property it reached for is now delivered differently. Keep the lid's series - contact (`SYS-HW-21`): it depends on no firmware at all and costs nothing. `K1` - is answered by this change and `K2` dissolves, the enclosure channel not - being in the supervisor's remit. - - The lid is likewise not in it. Its switch breaks the coil in hardware, which - is already safe without firmware, and `SWR-SAF-31`'s latch is gated on a heating - state only the ESP32 knows; a supervisor latching on it regardless would trip - on every cold load. So `SWR-SAF-31` and `SYS-HW-21` are unchanged, the lid sense stays - on the ESP32, and `Q5`'s branch is the only one section K loses. - - [x] **R8. The supervisor's firmware and its test strategy. Done 2026-10-10.** The firmware is 3 488 bytes of program memory across four source files, which is the design constraint met rather than aspired to, and it has its own