Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a0b069c
docs(spec): add multicore task architecture design
stritti Aug 1, 2026
9c57229
docs(multicore): add EN/DE architecture documentation
stritti Aug 1, 2026
3c5a962
docs(plan): add multicore task architecture implementation plan
stritti Aug 1, 2026
c48c38d
feat(multicore): add SPSC telemetry queue and lock-free sensor slots
stritti Aug 1, 2026
6eb56a5
refactor: split DallasTemperatureNode measurement into begin/finish w…
stritti Aug 1, 2026
fdab6f9
feat: run DS18B20 sensor reads in a dedicated Core-0 task
stritti Aug 1, 2026
bf1ef34
feat: move MQTT serialization into a dedicated Core-0 publish task
stritti Aug 1, 2026
48ae8b0
feat: render NORVI OLED on a Core-0 display task
stritti Aug 1, 2026
cebcafc
feat: thread-safe sensor status reporting and task watchdog wrapper
stritti Aug 1, 2026
5800153
docs: sync multicore architecture pages with implementation
stritti Aug 1, 2026
ac4785b
docs(plan): mark all multicore implementation tasks complete
stritti Aug 1, 2026
b6894da
style: fix clang-format and markdownlint violations for MegaLinter
stritti Aug 9, 2026
f8c310a
Merge main into feat/multicore-task-architecture
stritti Oct 1, 2026
d29086c
style: fix clang-format violations after main merge
stritti Oct 1, 2026
4b08008
style: format native test runner
stritti Oct 1, 2026
4d671a7
fix(multicore): keep TelemetryQueue indices inside the ring storage
claude Oct 1, 2026
6c182b7
fix(multicore): start the pool conversion on dedicated sensor buses
claude Oct 1, 2026
8ed3cbf
fix(multicore): replace volatile cross-core state with real synchroni…
claude Oct 1, 2026
ab06fc4
fix(multicore): handle Core-0 task creation failures
claude Oct 1, 2026
50ddd4b
style: align TelemetryQueue members
claude Oct 1, 2026
f082218
Merge main into feat/multicore-task-architecture
stritti Oct 3, 2026
af46b21
fix(multicore): synchronize sensor, display, and task watchdog state
stritti Oct 3, 2026
715a3e2
style: format multicore sensor snapshot changes
stritti Oct 3, 2026
09faf09
fix: enforce multicore ownership boundaries
stritti Oct 3, 2026
3c7b55f
test: cover Dallas recovery scheduling
stritti Oct 3, 2026
003c99e
docs: align multicore architecture with ownership model
stritti Oct 3, 2026
a0bf33b
style: format CoreScheduler ownership fix
stritti Oct 3, 2026
72c60b2
refactor: remove obsolete multicore worker artifacts
stritti Oct 4, 2026
ca2b155
docs: align multicore ownership with final runtime model
stritti Oct 4, 2026
3f00319
docs: fix NORVI display ownership comments
stritti Oct 4, 2026
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
117 changes: 117 additions & 0 deletions docs/multicore-architecture.de.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: Multicore-Architektur
summary: Wie die Firmware blockierende Sensor-I/O auf ESP32-Kern 0 isoliert und veränderlichen Controller-Zustand auf Kern 1 serialisiert
date: "2026-08-01"
lastmod: "2026-10-04"
draft: false
toc: true
type: docs
featured: false
tags: ["docs", "controller", "architektur", "multicore", "tasks"]
menu:
docs:
parent: Pool Controller
name: Multicore-Architektur
weight: 33
---

## Überblick

Die Firmware verwendet ein explizites Ownership-Modell, statt jede I/O-Operation auf den zweiten Kern
zu verschieben. Das sicherheitsrelevante Controller-Modell bleibt Single-Writer in der Arduino-Loop,
während die blockierende DS18B20-Konvertierung auf Kern 0 isoliert wird.

| Kern | Rolle | Inhalt |
| ---- | ----- | ------ |
| **Kern 0** (PRO_CPU) | Sensor-I/O | `SensorTask`: DS18B20-Konvertierung/-Auslesen und interne ESP32-Temperatur |
| **Kern 1** (APP_CPU) | Regelung und zustandsbehaftete I/O | Arduino-`loop()`: Watchdog, Degradation, Regeln, Relais, Status-LED, Netzwerk/OTA, MQTT-Serialisierung, Frontpanel-UI und OLED-Rendering |

## Warum diese Grenze

Eine 12-Bit-DS18B20-Konvertierung kann ungefähr 750 ms dauern. In der Arduino-Loop würde sie
Regelauswertung, Watchdog-Verarbeitung und Relaisentscheidungen blockieren. `SensorTask` entfernt genau
diese blockierende Operation aus der Regelschleife.

MQTT-Serialisierung und OLED-Rendering bleiben bewusst auf Kern 1. Beide lesen veränderlichen
Controller-Zustand wie Betriebsmodus, Regeln, Relais und Konfiguration. Ein paralleles Lesen auf Kern 0
würde Datenrennen etwa um Arduino-`String` und Regelzustände erzeugen. Die Ausführung auf dem besitzenden
Task ist einfacher und sicherer als Locks über den gesamten Objektgraphen.

## Task-Modell

`CoreScheduler` erzeugt genau einen erforderlichen Worker-Task:

| Task | Kern | Priorität | Stack | Zweck |
| ---- | ---- | --------- | ----- | ----- |
| `SensorTask` | 0 | 2 | 6 KB | DS18B20- und interne Temperaturmessung |

Kann `SensorTask` nicht erzeugt werden, startet der Controller neu. Ein dauerhafter Fehler wird durch die
bestehende Boot-Loop-Erkennung und den Safe-Mode behandelt.

MQTT-Anforderungen verwenden weiterhin `TelemetryQueue`. `PoolController::loop()` leert die Queue direkt
auf Kern 1 und ruft dort `MqttPublisher` auf. `CoreScheduler` ist ausschließlich für den Lebenszyklus von
`SensorTask` und dessen Stack-High-Water-Logging zuständig.

Beim NORVI-Display gilt dieselbe Regel: Taster, UI-Zustandsübergänge und OLED-Rendering werden durch
`DisplayCoordinator` auf Kern 1 serialisiert. Die veralteten Worker-Implementierungen `PublishTask` und
`DisplayTask` wurden entfernt, nachdem das Concurrency-Review die Ownership-Grenze enger gezogen hat.

## Sensor-Ownership

Nach dem Start ist `SensorTask` alleiniger Besitzer aller `OneWire`- und `DallasTemperature`-Operationen.
Code auf Kern 1 scannt oder liest den Bus nicht direkt.

`DallasTemperatureNode` stellt deshalb zwei Core-übergreifende Kanäle bereit:

- Messwerte werden über `SensorSlots` publiziert.
- Sensor-Erkennungsdaten und ausgewählte ROM-Adressen werden in einen kleinen Cache kopiert, der durch
einen kurzen atomaren Lock geschützt ist. Web- und MQTT-Code lesen nur diesen Cache.

Änderungen der Sensor-Zuordnung werden zur Laufzeit als pending Konfiguration gespeichert. `SensorTask`
wendet sie zu Beginn eines späteren Messzyklus an. WebPortal- oder MQTT-Aufrufe führen damit keine
OneWire-Transaktion auf Kern 1 aus.

## DS18B20-Messzyklus

`DallasTemperature` wird mit `setWaitForConversion(false)` konfiguriert. Ein Messzyklus besteht aus:

1. Konvertierung auf dem benötigten Bus beziehungsweise den Bussen starten.
2. `SensorTask` für 800 ms freigeben.
3. Fertige Messwerte lesen.
4. Ergebnis in `SensorSlots` publizieren und Discovery-Cache aktualisieren.

Damit entfällt die frühere doppelte Wartezeit, bei der `requestTemperatures()` bereits intern blockierte
und der Worker anschließend nochmals 800 ms wartete.

Fehlt mindestens einer der Dallas-Sensoren oder ist sein Wert ungültig, verwendet der gemeinsame Zyklus
den kürzeren Recovery-Intervall von 5 Sekunden. Sobald beide Sensoren gültig sind, gilt wieder das
konfigurierte `loopInterval`.

## Datenfluss

```text
SensorTask (Kern 0) ── SensorSlots ──────▶ Regelschleife (Kern 1): Regeln / Relais
SensorTask (Kern 0) ── Discovery-Cache ──▶ WebPortal / MQTT / UI (Kern 1)
Regelschleife (Kern 1) ─ TelemetryQueue ─▶ MQTT-Serialisierung (Kern 1)
DisplayCoordinator: Eingabe + Zustand + Rendering ───────────▶ Kern 1
```

Die entscheidende Regel ist Ownership: Hardware-Buszustand gehört `SensorTask`; veränderlicher
Controller-Zustand gehört der Regelschleife. Core-übergreifende Kommunikation verwendet begrenzte
Snapshots statt gemeinsam veränderter Objekte.

## Zuverlässigkeit

- Sensorwert und Found-Status werden konsistent über `SensorSlots` übertragen.
- `DegradationManager` verwendet atomare Core-übergreifende Status-Flags.
- `SensorTask` registriert sich beim Task-Watchdog und füttert ihn.
- Stack-High-Water-Logging bleibt für den Worker verfügbar.
- OTA-Verarbeitung und MQTT-Zustandsänderungen bleiben auf Kern 1 serialisiert.

## Historische Design-Dokumente

Das ursprüngliche Design und der Migrationsplan bleiben unter
[`docs/superpowers/specs/2026-08-01-multicore-task-architecture-design.md`](../superpowers/specs/2026-08-01-multicore-task-architecture-design.md)
und `docs/superpowers/plans/2026-08-01-multicore-task-architecture.md` erhalten. Sie dokumentieren den
früheren Drei-Worker-Entwurf und bleiben als Implementierungshistorie bestehen. Diese Seite beschreibt
die maßgebliche Laufzeitarchitektur nach dem Concurrency-Review.
117 changes: 117 additions & 0 deletions docs/multicore-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: Multicore Architecture
summary: How the firmware isolates blocking sensor I/O on ESP32 Core 0 while keeping mutable controller state serialized on Core 1
date: "2026-08-01"
lastmod: "2026-10-04"
draft: false
toc: true
type: docs
featured: false
tags: ["docs", "controller", "architecture", "multicore", "tasks"]
menu:
docs:
parent: Pool Controller
name: Multicore Architecture
weight: 33
---

## Overview

The firmware uses an explicit ownership model instead of moving every I/O operation to the second core.
The safety-critical controller model remains single-writer on the Arduino loop task, while the blocking
DS18B20 conversion is isolated on Core 0.

| Core | Role | Contents |
| ---- | ---- | -------- |
| **Core 0** (PRO_CPU) | Sensor I/O | `SensorTask`: DS18B20 conversion/readout and internal ESP32 temperature |
| **Core 1** (APP_CPU) | Control and stateful I/O | Arduino `loop()`: watchdog, degradation, rules, relays, StatusLED, network/OTA, MQTT serialization, front-panel UI and OLED rendering |

## Why this boundary

A 12-bit DS18B20 conversion can take roughly 750 ms. Running it in the Arduino loop stalls rule
evaluation, watchdog handling and relay decisions. `SensorTask` removes that blocking conversion from
the control loop.

MQTT serialization and OLED rendering are intentionally kept on Core 1. Both read mutable controller
state such as operation mode, rules, relay state and configuration. Running those readers concurrently
with the control loop would create cross-core data races around objects such as Arduino `String` and
rule state. Keeping them on the owning task is simpler and safer than protecting the complete object
graph with locks.

## Task model

`CoreScheduler` creates one required worker task:

| Task | Core | Priority | Stack | Purpose |
| ---- | ---- | -------- | ----- | ------- |
| `SensorTask` | 0 | 2 | 6 KB | DS18B20 and internal temperature acquisition |

If `SensorTask` cannot be created, the controller restarts. A persistent failure is handled by the
existing boot-loop detection and safe-mode path.

MQTT requests still use `TelemetryQueue`. `PoolController::loop()` drains that queue directly on Core 1
and invokes `MqttPublisher` there. `CoreScheduler` is responsible only for `SensorTask` lifecycle and
stack high-water logging.

The NORVI display follows the same rule: button handling, UI state transitions and OLED rendering are
serialized on Core 1 by `DisplayCoordinator`. The obsolete `PublishTask` and `DisplayTask` worker
implementations were removed after the concurrency review narrowed the ownership boundary.

## Sensor ownership

After startup, `SensorTask` is the exclusive owner of `OneWire` and `DallasTemperature` operations.
Core-1 code never scans or reads the bus directly.

`DallasTemperatureNode` therefore exposes two cross-core channels:

- Measurements are published through `SensorSlots`.
- Sensor discovery metadata and selected ROM addresses are copied into a small cached snapshot guarded
by a short atomic lock. Web and MQTT code read only this cache.

Runtime sensor-mapping changes are stored as pending configuration. `SensorTask` applies them at the
start of a later measurement cycle, so calls from WebPortal or MQTT never execute OneWire transactions
on Core 1.

## DS18B20 measurement cycle

`DallasTemperature` is configured with `setWaitForConversion(false)`. A measurement cycle is:

1. Start conversions on the required bus or buses.
2. Yield `SensorTask` for 800 ms.
3. Read the completed values.
4. Publish the result to `SensorSlots` and refresh the discovery cache.

This avoids the former double wait where `requestTemperatures()` blocked internally and the worker then
waited another 800 ms.

If either Dallas sensor is missing or invalid, the common cycle uses the shorter 5-second recovery
interval. Once both sensors are valid, the configured `loopInterval` applies again.

## Data flow

```text
SensorTask (Core 0) ── SensorSlots ──────▶ control loop (Core 1): rules / relays
SensorTask (Core 0) ── discovery cache ──▶ WebPortal / MQTT / UI (Core 1)
control loop (Core 1) ── TelemetryQueue ─▶ MQTT serialization (Core 1)
DisplayCoordinator: input + state + render ───────────────────▶ Core 1
```

The important rule is ownership: hardware bus state belongs to `SensorTask`; mutable controller state
belongs to the control loop. Cross-core communication uses bounded snapshots instead of shared mutable
objects.

## Reliability

- Sensor values and found-state are transferred consistently through `SensorSlots`.
- `DegradationManager` uses atomic cross-core status flags.
- `SensorTask` registers with and feeds the task watchdog.
- Stack high-water logging remains available for the worker task.
- OTA handling and MQTT state mutation stay serialized on Core 1.

## Historical design documents

The original design and migration plan remain available under
[`docs/superpowers/specs/2026-08-01-multicore-task-architecture-design.md`](../superpowers/specs/2026-08-01-multicore-task-architecture-design.md)
and `docs/superpowers/plans/2026-08-01-multicore-task-architecture.md`. They document the earlier
three-worker proposal and are retained as implementation history. This page describes the authoritative
runtime architecture after the concurrency review.
Loading
Loading