Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -60,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
Expand Down Expand Up @@ -221,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).
Expand Down
10 changes: 9 additions & 1 deletion docs/01_user_req.sdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
98 changes: 95 additions & 3 deletions docs/02_system_req.sdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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: >>>
Expand Down
Loading
Loading