Vereisten: Deel 1 afgerond. Je kent RAII, smart pointers, interfaces via pure virtuals, het Strategy Pattern, de Middleware Pipeline, en de MVC-split voor hardware. Platform: ESP32 + PlatformIO + C++17 Focus: Templates, FreeRTOS concurrency, geavanceerd geheugenbeheer, event-driven architectuur, en productie-hardening.
# Inhoudsopgave
- C++ Templates: compileertijd-polymorfisme
- FreeRTOS: concurrent programmeren op de ESP32
- Event-Driven Architectuur: de Event Bus
- Geavanceerd Geheugenbeheer: Object Pools & Static Allocation
std::optionalenstd::variant: type-veilig foutafhandelen- CRTP: statisch polymorfisme zonder vtable overhead
- De Config-laag: type-veilige configuratie uit NVS/Flash
- OTA Updates: de Deployment Pipeline voor embedded
- Diagnostics & Observability: embedded APM
- Productie-architectuur: alles samenvoegen
# 1. Templates: compileertijd-polymorfisme
In deel 1 gebruikten we runtime polymorfisme: een ITransport* pointer die op een WiFiTransport of LoRaTransport kan wijzen. De dispatch naar de juiste methode gebeurt via een vtable β een kleine maar reΓ«le runtime-kost per aanroep.
Templates zijn C++'s tweede polymorfisme-mechanisme, en het heeft nul runtime overhead: de compiler genereert separate code per type op compile-tijd.
# 1.1 De parallel met Java Generics / TS Generics
// Java Generics: type-parameter voor compilatie, maar runtime is alles Object (type erasure)
// class Repository<T> { T findById(int id) {...} }
// TypeScript Generics: puur compileertijd, verdwijnt bij transpilatie
// function wrap<T>(value: T): Result<T> { ... }
// C++ Templates: puur compileertijd, genereert *echte* aparte code per type
template<typename T>
class CircularBuffer {
// Hier is T een echte type-parameter, niet gewist
};
# 1.2 Een type-veilige Circular Buffer
Op embedded systemen is een circulaire buffer essentieel: je wil sensordata bufferen zonder de heap te fragmenteren. In Java zou je ArrayDeque<SensorReading> gebruiken. In C++ bouwen we een stack-gealloceerde versie die de grootte op compile-tijd vastlegt.
// src/containers/CircularBuffer.h
#pragma once
#include <cstddef>
#include <cstdint>
#include <array>
namespace TractorOS {
/**
* Lock-free circular buffer voor sensordata.
*
* Java equivalent: new ArrayDeque<T>(capacity) β maar heap-gealloceerd
* Dit: CircularBuffer<SensorReading, 32> β stack/statisch gealloceerd
*
* Template parameters:
* T = het type dat gebufferd wordt
* Capacity = buffergrootte op compile-tijd (geen runtime configuratie mogelijk)
*
* Gebruik: CircularBuffer<float, 16> temperatuurHistory;
*/
template<typename T, size_t Capacity>
class CircularBuffer {
public:
static_assert(Capacity > 0, "Buffer capacity moet groter dan 0 zijn");
static_assert((Capacity & (Capacity - 1)) == 0,
"Capacity moet een macht van 2 zijn voor efficiΓ«nte modulo");
// ^ static_assert: compileerfout als conditie false is. Vergelijk Java's
// assert in statische initializers, maar dit stopt de compilatie.
/**
* Voeg element toe. Overschrijft oudste element als buffer vol is.
* @return false als oudste element overschreven werd (data verloren)
*/
bool push(const T& item) {
bool overwrite = isFull();
_buffer[_head & _mask] = item; // & _mask = snelle modulo voor macht-van-2
_head++;
if (overwrite) _tail++;
return !overwrite;
}
/**
* Lees oudste element. Retourneert false als buffer leeg is.
*/
bool pop(T& out) {
if (isEmpty()) return false;
out = _buffer[_tail & _mask];
_tail++;
return true;
}
/**
* Peek zonder te consumeren β const methode.
*/
const T* peek() const {
if (isEmpty()) return nullptr;
return &_buffer[_tail & _mask];
}
bool isEmpty() const { return _head == _tail; }
bool isFull() const { return size() == Capacity; }
size_t size() const { return _head - _tail; }
size_t capacity() const { return Capacity; }
/**
* Range-based for loop ondersteuning.
* In Java: for (T item : collection) {}
* In C++: for (const auto& item : buffer) {}
*/
class Iterator {
public:
Iterator(const CircularBuffer& buf, size_t index)
: _buf(buf), _index(index) {}
const T& operator*() const { return _buf._buffer[_index & _buf._mask]; }
Iterator& operator++() { ++_index; return *this; }
bool operator!=(const Iterator& o) const { return _index != o._index; }
private:
const CircularBuffer& _buf;
size_t _index;
};
Iterator begin() const { return Iterator(*this, _tail); }
Iterator end() const { return Iterator(*this, _head); }
private:
std::array<T, Capacity> _buffer{}; // Stack-gealloceerd array van precies Capacity elementen
size_t _head = 0;
size_t _tail = 0;
static constexpr size_t _mask = Capacity - 1;
};
} // namespace TractorOS
Gebruik in de SensorManager:
// In SensorManager.h
#include "containers/CircularBuffer.h"
class SensorManager {
// 32 metingen bufferen zonder heap-allocatie
CircularBuffer<SensorReading, 32> _history;
void poll() {
for (auto& sensor : _sensors) {
SensorReading r = sensor->read();
if (r.isValid) {
_history.push(r);
for (auto& cb : _callbacks) cb(r);
}
}
}
// Geef de laatste N metingen terug β voor trending/moving average
void getHistory(std::function<void(const SensorReading&)> visitor) const {
for (const auto& reading : _history) {
visitor(reading);
}
}
};
# 1.3 Template Specialisatie: ander gedrag per type
/**
* Standaard serializer β werkt voor POD types (Plain Old Data: structs zonder pointers)
* "POD" is het C++ equivalent van Java's "value types" / primitieven
*/
template<typename T>
struct Serializer {
static size_t serialize(const T& value, uint8_t* buffer, size_t maxLen) {
static_assert(std::is_trivially_copyable_v<T>,
"Standaard serializer werkt alleen voor trivially copyable types");
if (sizeof(T) > maxLen) return 0;
memcpy(buffer, &value, sizeof(T));
return sizeof(T);
}
};
// Template specialisatie voor float β big-endian netwerk byte order
template<>
struct Serializer<float> {
static size_t serialize(const float& value, uint8_t* buffer, size_t maxLen) {
if (4 > maxLen) return 0;
uint32_t bits;
memcpy(&bits, &value, 4); // float bits als uint32
buffer[0] = (bits >> 24) & 0xFF; // Big-endian voor netwerkverzending
buffer[1] = (bits >> 16) & 0xFF;
buffer[2] = (bits >> 8) & 0xFF;
buffer[3] = (bits ) & 0xFF;
return 4;
}
};
// Gebruik:
float temp = 87.5f;
uint8_t buf[4];
size_t written = Serializer<float>::serialize(temp, buf, sizeof(buf));
# 1.4 if constexpr: conditionele compilatie als superieur alternatief voor #ifdef
// OUD: preprocessor conditionele compilatie (C-stijl, geen type-checking)
#ifdef HARDWARE_BUILD
auto sensor = std::make_unique<DS18B20Sensor>(GPIO_NUM_4);
#else
auto sensor = MockSensor::withConstantValue("Temp", 20.0f, Type::TEMPERATURE);
#endif
// MODERN C++17: if constexpr β hetzelfde maar type-veilig en debugbaar
template<bool IsHardware>
auto createTemperatureSensor() {
if constexpr (IsHardware) {
return std::make_unique<DS18B20Sensor>(GPIO_NUM_4);
} else {
return MockSensor::withConstantValue("Temp", 20.0f,
SensorReading::Type::TEMPERATURE);
}
// De compiler verwijdert de *niet-gekozen* branch volledig β geen dead code
}
// Aanroep:
#ifdef HARDWARE_BUILD
auto sensor = createTemperatureSensor<true>();
#else
auto sensor = createTemperatureSensor<false>();
#endif
# 2. FreeRTOS: concurrent programmeren op de ESP32
De Arduino loop() functie is single-threaded. Voor een boordcomputer met meerdere sensoren, netwerkverzending Γ©n een display is dat onvoldoende. De ESP32 draait FreeRTOS, een real-time besturingssysteem met taken (tasks), wachtrijen (queues) en semaforen.
# 2.1 De mentale switch: Threads vs FreeRTOS Tasks
Java/TS FreeRTOS equivalent
ββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββ
Thread / Worker xTaskCreate / xTaskCreatePinnedToCore
BlockingQueue<T> xQueueCreate + xQueueSend/Receive
synchronized / mutex xSemaphoreCreateMutex
CountDownLatch xEventGroupCreate + xEventGroupSetBits
ScheduledExecutorService xTimerCreate
Thread.sleep(ms) vTaskDelay(pdMS_TO_TICKS(ms))
# 2.2 Task-gebaseerde architectuur
We verdelen het systeem in drie onafhankelijke taken met duidelijke verantwoordelijkheden:
ββββββββββββββββββββ Queue ββββββββββββββββββββ
β SensorTask β βββββββββββΊ β ProcessingTask β
β Core 0, prio 3 β β Core 0, prio 2 β
β Leest sensoren β β Middleware+DI β
ββββββββββββββββββββ ββββββββββ¬ββββββββββ
β Queue
βΌ
ββββββββββββββββββββ
β TransmitTask β
β Core 1, prio 1 β
β WiFi/LoRa send β
ββββββββββββββββββββ
# 2.3 Type-veilige Queue wrapper
FreeRTOS queues zijn C API's met void*. We wikkelen ze in een type-veilige C++ template:
// src/rtos/TypedQueue.h
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <cstddef>
namespace TractorOS {
/**
* Type-veilige wrapper om FreeRTOS QueueHandle_t.
*
* Java equivalent: LinkedBlockingQueue<T>
* TS equivalent: geen directe equivalent (single-threaded)
*
* BELANGRIJK: T moet trivially copyable zijn β FreeRTOS kopieert de data
* via memcpy. Geen shared_ptr, geen std::string als T!
*/
template<typename T, size_t Depth>
class TypedQueue {
static_assert(std::is_trivially_copyable_v<T>,
"Queue items moeten trivially copyable zijn (geen heap-allocaties)");
public:
TypedQueue() {
_handle = xQueueCreate(Depth, sizeof(T));
// In productie: assert(_handle != nullptr)
}
~TypedQueue() {
if (_handle) vQueueDelete(_handle);
}
// Niet kopieerbaar β één queue, één eigenaar
TypedQueue(const TypedQueue&) = delete;
TypedQueue& operator=(const TypedQueue&) = delete;
/**
* Stuur item naar queue. Blokkeert tot timeoutMs verstreken.
* @param timeoutMs 0 = niet-blokkerend, portMAX_DELAY = oneindig wachten
* @return true als succesvol verstuurd
*/
bool send(const T& item, uint32_t timeoutMs = 10) {
return xQueueSend(_handle, &item, pdMS_TO_TICKS(timeoutMs)) == pdTRUE;
}
/**
* Stuur vanuit een ISR (Interrupt Service Routine).
* Normale send() mag NIET vanuit een ISR aangeroepen worden!
*/
bool sendFromISR(const T& item) {
BaseType_t higherPriorityTaskWoken = pdFALSE;
bool ok = xQueueSendFromISR(_handle, &item, &higherPriorityTaskWoken) == pdTRUE;
if (higherPriorityTaskWoken) portYIELD_FROM_ISR();
return ok;
}
/**
* Ontvang item. Blokkeert tot item beschikbaar of timeout.
*/
bool receive(T& out, uint32_t timeoutMs = portMAX_DELAY) {
return xQueueReceive(_handle, &out, pdMS_TO_TICKS(timeoutMs)) == pdTRUE;
}
size_t waiting() const { return uxQueueMessagesWaiting(_handle); }
size_t spacesLeft() const { return uxQueueSpacesAvailable(_handle); }
bool isEmpty() const { return waiting() == 0; }
private:
QueueHandle_t _handle = nullptr;
};
} // namespace TractorOS
# 2.4 De Sensor Task
// src/tasks/SensorTask.h
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "rtos/TypedQueue.h"
#include "core/SensorManager.h"
#include "interfaces/ISensor.h"
namespace TractorOS {
/**
* FreeRTOS task die sensoren periodiek uitleest en readings naar een queue pusht.
*
* Java equivalent: ScheduledExecutorService.scheduleAtFixedRate(...)
*
* Ontwerp:
* - Draait op Core 0 (samen met WiFi-stack op Core 1 vermijden)
* - Hoge prioriteit: sensordata mag niet gemist worden
* - Communiceert via queue β nooit shared state zonder mutex!
*/
class SensorTask {
public:
using SensorQueue = TypedQueue<SensorReading, 32>;
SensorTask(SensorManager& manager, SensorQueue& outputQueue)
: _manager(manager)
, _queue(outputQueue) {}
void start(uint32_t intervalMs = 100, UBaseType_t priority = 3) {
_intervalMs = intervalMs;
// xTaskCreatePinnedToCore: maak task aan op specifieke CPU core
// Stack: 4096 bytes β pas aan op basis van stack usage analyse
xTaskCreatePinnedToCore(
_taskFunction, // Functie die de task uitvoert
"SensorTask", // Debug naam
4096, // Stack size in bytes
this, // pvParameters: we sturen 'this' pointer door
priority, // Prioriteit (hoger = belangrijker)
&_taskHandle, // Handle voor latere controle
0 // Core 0
);
}
void stop() {
if (_taskHandle) {
vTaskDelete(_taskHandle);
_taskHandle = nullptr;
}
}
private:
// FreeRTOS verwacht een static C-stijl functie β we bridgen naar een methode
static void _taskFunction(void* pvParams) {
auto* self = static_cast<SensorTask*>(pvParams);
self->_run();
}
void _run() {
TickType_t lastWakeTime = xTaskGetTickCount();
while (true) {
_manager.poll();
// Stuur alle nieuwe readings naar de queue
// (SensorManager triggert een callback die we hier registreren)
// vTaskDelayUntil: slaap PRECIES tot volgende interval
// Dit is drift-vrij: compenseert voor de tijd die poll() kostte
// Java equivalent: scheduledExecutor met fixed-rate (niet fixed-delay!)
vTaskDelayUntil(&lastWakeTime, pdMS_TO_TICKS(_intervalMs));
}
}
SensorManager& _manager;
SensorQueue& _queue;
uint32_t _intervalMs = 100;
TaskHandle_t _taskHandle = nullptr;
};
} // namespace TractorOS
# 2.5 Mutex-beschermde gedeelde state
Wanneer meerdere tasks dezelfde data lezen/schrijven, is een mutex verplicht:
// src/rtos/Mutex.h
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
namespace TractorOS {
/**
* RAII mutex guard β equivalent van Java's synchronized blok of TS's async mutex.
*
* Gebruik:
* {
* MutexGuard lock(_statsMutex); // Lock verworven
* _stats.packetsSent++; // Veilig aanpassen
* } // Lock automatisch vrijgegeven
*/
class Mutex {
public:
Mutex() { _handle = xSemaphoreCreateMutex(); }
~Mutex() { if (_handle) vSemaphoreDelete(_handle); }
Mutex(const Mutex&) = delete;
Mutex& operator=(const Mutex&) = delete;
bool lock(uint32_t timeoutMs = portMAX_DELAY) {
return xSemaphoreTake(_handle, pdMS_TO_TICKS(timeoutMs)) == pdTRUE;
}
void unlock() { xSemaphoreGive(_handle); }
private:
SemaphoreHandle_t _handle = nullptr;
friend class MutexGuard;
};
/**
* RAII guard β Lock in constructor, unlock in destructor.
* Vergelijk: Java synchronized(obj) {} of TS's Lock.acquire()
*/
class MutexGuard {
public:
explicit MutexGuard(Mutex& mutex) : _mutex(mutex) {
_acquired = mutex.lock();
}
~MutexGuard() {
if (_acquired) _mutex.unlock();
}
bool isAcquired() const { return _acquired; }
// Niet kopieerbaar of verplaatsbaar β guard heeft enkelvoudig eigenaarschap
MutexGuard(const MutexGuard&) = delete;
MutexGuard& operator=(const MutexGuard&) = delete;
private:
Mutex& _mutex;
bool _acquired;
};
} // namespace TractorOS
Gebruik:
class SystemStats {
public:
void recordPacketSent() {
MutexGuard lock(_mutex); // Automatisch vrijgegeven bij scope-exit
_packetsSent++;
_lastSendTime = millis();
}
uint32_t getPacketsSent() const {
MutexGuard lock(_mutex); // const methode mag toch locking doen
return _packetsSent;
}
private:
mutable Mutex _mutex; // mutable: mag aangepast worden in const methoden
// ^^^^^^ Equivalent van Java's volatile/synchronized fields in const context
uint32_t _packetsSent = 0;
uint32_t _lastSendTime = 0;
};
# 3. Event-Driven Architectuur: de Event Bus
De SensorManager uit deel 1 gebruikte directe callbacks. Dat schaalt slecht: producers kennen consumers, en je hebt nauw gekoppelde afhankelijkheden. De oplossing is een Event Bus β het Observer Pattern op systeemniveau.
Java equivalent: Spring ApplicationEventPublisher / Guava EventBus
TS equivalent: RxJS Subject / EventEmitter (Node.js)
# 3.1 Type-veilige Events met std::variant
// src/events/Events.h
#pragma once
#include <variant>
#include <cstdint>
#include "interfaces/ISensor.h"
namespace TractorOS {
// Definieer alle mogelijke event-types als afzonderlijke structs
// Dit is C++'s equivalent van een sealed interface in Java/Kotlin of discriminated union in TS
struct SensorReadingEvent {
SensorReading reading;
};
struct TransportConnectedEvent {
const char* transportName;
uint32_t timestamp;
};
struct TransportFailedEvent {
const char* transportName;
const char* reason;
uint8_t retryCount;
};
struct SystemAlertEvent {
enum class Severity : uint8_t { INFO, WARNING, CRITICAL };
Severity severity;
const char* message;
uint32_t timestamp;
};
struct ConfigChangedEvent {
const char* key;
const char* newValue;
};
/**
* Variant = tagged union β precies één type tegelijk, type-veilig.
*
* Java equivalent: sealed interface Event permits SensorReadingEvent, ...
* TS equivalent: type Event = SensorReadingEvent | TransportConnectedEvent | ...
*
* std::variant gooit een exception als je het verkeerde type opvraagt.
* Op embedded gebruiken we std::visit voor exhaustive matching.
*/
using Event = std::variant<
SensorReadingEvent,
TransportConnectedEvent,
TransportFailedEvent,
SystemAlertEvent,
ConfigChangedEvent
>;
} // namespace TractorOS
# 3.2 De Event Bus implementatie
// src/events/EventBus.h
#pragma once
#include <functional>
#include <vector>
#include <unordered_map>
#include "Events.h"
#include "rtos/Mutex.h"
namespace TractorOS {
/**
* Thread-veilige Event Bus.
*
* Subscribers registreren handlers per event-type via type index.
* Publishers sturen events zonder kennis van subscribers.
*
* WAARSCHUWING: Handlers worden aangeroepen op de thread van de publisher.
* Voor cross-task events: gebruik een queue in de handler om door te sturen.
*/
class EventBus {
public:
using Handler = std::function<void(const Event&)>;
using SubscriptionId = uint32_t;
static EventBus& instance() {
static EventBus bus;
return bus;
}
/**
* Registreer een handler voor een specifiek event-type.
*
* Gebruik:
* bus.subscribe<SensorReadingEvent>([](const SensorReadingEvent& e) {
* Serial.println(e.reading.value.floatValue);
* });
*
* Template + lambda = equivalent van Java's bus.on(SensorReadingEvent.class, handler)
*/
template<typename EventType>
SubscriptionId subscribe(std::function<void(const EventType&)> handler) {
MutexGuard lock(_mutex);
// Wrap de typed handler in een generic Event handler
auto wrappedHandler = [handler](const Event& event) {
// std::get_if: type-veilig opvragen β retourneert nullptr als type niet klopt
if (const auto* typed = std::get_if<EventType>(&event)) {
handler(*typed);
}
};
SubscriptionId id = _nextId++;
size_t typeIndex = event_index<EventType>();
_handlers[typeIndex].push_back({id, wrappedHandler});
return id;
}
/**
* Publiceer een event naar alle geregistreerde handlers.
*
* Gebruik:
* bus.publish(SensorReadingEvent{reading});
*/
template<typename EventType>
void publish(EventType&& eventData) {
Event event = std::forward<EventType>(eventData);
size_t typeIndex = event_index<std::decay_t<EventType>>();
// Kopieer handlers om mutex niet te houden tijdens uitvoering
std::vector<std::pair<SubscriptionId, Handler>> handlers;
{
MutexGuard lock(_mutex);
auto it = _handlers.find(typeIndex);
if (it != _handlers.end()) {
handlers = it->second;
}
}
for (auto& [id, handler] : handlers) {
handler(event);
}
}
void unsubscribe(SubscriptionId id) {
MutexGuard lock(_mutex);
for (auto& [typeIdx, handlerList] : _handlers) {
handlerList.erase(
std::remove_if(handlerList.begin(), handlerList.end(),
[id](const auto& pair) { return pair.first == id; }),
handlerList.end()
);
}
}
private:
EventBus() = default;
// Genereer een unieke index per type op compile-tijd
template<typename T>
static size_t event_index() {
// Elke instantiatie van deze template heeft zijn eigen statische variabele
static size_t index = _typeCounter++;
return index;
}
static inline size_t _typeCounter = 0;
std::unordered_map<size_t, std::vector<std::pair<SubscriptionId, Handler>>> _handlers;
mutable Mutex _mutex;
SubscriptionId _nextId = 0;
};
// Convenience macro voor kortere subscribe syntax
#define ON_EVENT(EventType, handler) \
EventBus::instance().subscribe<EventType>(handler)
} // namespace TractorOS
# 3.3 Gebruik in de architectuur
// In TransmitTask: reageer op sensor readings
void TransmitTask::setup() {
// Subscribe op SensorReadingEvent β geen directe koppeling aan SensorManager
_subscriptionId = ON_EVENT(SensorReadingEvent, [this](const SensorReadingEvent& e) {
_sendQueue.send(e.reading);
});
// Subscribe op transport events voor reconnect-logica
ON_EVENT(TransportFailedEvent, [this](const TransportFailedEvent& e) {
Serial.printf("Transport mislukt: %s (%s)\n", e.transportName, e.reason);
_reconnectNeeded = true;
});
}
// In SensorManager: publiceer events in plaats van directe callbacks
void SensorManager::poll() {
for (auto& sensor : _sensors) {
SensorReading r = sensor->read();
if (r.isValid) {
EventBus::instance().publish(SensorReadingEvent{r});
}
}
}
// In AlertManager: reageer op kritische waarden
void AlertManager::setup() {
ON_EVENT(SensorReadingEvent, [this](const SensorReadingEvent& e) {
if (e.reading.type == SensorReading::Type::TEMPERATURE &&
e.reading.value.floatValue > 95.0f) {
EventBus::instance().publish(SystemAlertEvent{
SystemAlertEvent::Severity::CRITICAL,
"Motortemperatuur kritisch!",
e.reading.timestamp
});
}
});
}
# 4. Geavanceerd Geheugenbeheer: Object Pools & Static Allocation
In deel 1 leerden we dat heap-allocatie op embedded systemen gevaarlijk is door fragmentatie. Nu leren we de professionele alternatieven.
# 4.1 Object Pool
Een object pool pre-alloceert een vaste set objecten en geeft ze uit op aanvraag. Geen new, geen fragmentatie.
// src/memory/ObjectPool.h
#pragma once
#include <array>
#include <cstddef>
#include <cassert>
namespace TractorOS {
/**
* Fixed-size object pool.
*
* Java equivalent: Apache Commons Pool2 / org.apache.commons.pool2.ObjectPool
* maar dan zonder heap-overhead en GC.
*
* Alle objecten leven in een aaneengesloten std::array β geen fragmentatie.
* Acquire/release zijn O(1) operaties.
*
* T: Type van de gepoolde objecten
* PoolSize: Maximum aantal gelijktijdige objecten
*/
template<typename T, size_t PoolSize>
class ObjectPool {
public:
/**
* Custom deleter voor gebruik met unique_ptr.
* Geeft het object terug aan de pool bij destructie.
*/
struct Deleter {
ObjectPool* pool;
void operator()(T* ptr) const {
if (pool && ptr) pool->release(ptr);
}
};
using Handle = std::unique_ptr<T, Deleter>;
ObjectPool() {
// Initialiseer de vrije lijst
for (size_t i = 0; i < PoolSize; ++i) {
_freeList[i] = i;
}
_freeCount = PoolSize;
}
/**
* Haal een object op uit de pool.
* Retourneert nullptr als de pool leeg is.
*
* Het geretourneerde Handle geeft het object automatisch terug
* aan de pool wanneer het uit scope gaat β RAII!
*/
Handle acquire() {
if (_freeCount == 0) return Handle(nullptr, Deleter{this});
size_t index = _freeList[--_freeCount];
T* obj = &_storage[index];
new (obj) T{}; // Placement new: construeer T in pre-gealloceerde geheugen
// ^^^^^^^^^^ Dit roept de constructor aan ZONDER nieuw geheugen te alloceren
return Handle(obj, Deleter{this});
}
size_t available() const { return _freeCount; }
size_t capacity() const { return PoolSize; }
private:
void release(T* obj) {
obj->~T(); // Expliciete destructor aanroep β pendant van placement new
// Bereken index via pointer arithmetic
size_t index = static_cast<size_t>(obj - _storage.data());
assert(index < PoolSize);
_freeList[_freeCount++] = index;
}
// Gebruik aligned_storage voor correct geheugenalignment van T
alignas(T) std::array<T, PoolSize> _storage;
std::array<size_t, PoolSize> _freeList;
size_t _freeCount;
};
} // namespace TractorOS
Gebruik:
// Maak een pool van 16 TelemetryPackets β eenmalig, nooit opnieuw gealloceerd
static ObjectPool<TelemetryPacket, 16> packetPool;
void processSensorReading(const SensorReading& reading) {
auto packet = packetPool.acquire();
if (!packet) {
Serial.println("Pool leeg β packet gedropt");
return;
}
packet->timestamp = reading.timestamp;
packet->engineTemp = reading.value.floatValue;
dispatcher.dispatch(*packet);
// packet gaat hier uit scope β automatisch terug naar de pool
// Geen delete, geen new, geen heap-fragmentatie
}
# 4.2 Stack gebruik analyseren
Op de ESP32 is de default task stack 8KB. Overflow is een veelvoorkomende crashoorzaak:
// src/diagnostics/StackMonitor.h
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
namespace TractorOS {
class StackMonitor {
public:
/**
* Rapporteer stack gebruik voor alle lopende tasks.
* Gebruik in setup() of periodiek in een diagnostics-task.
*
* uxTaskGetStackHighWaterMark retourneert het MINIMUM aantal woorden
* dat ooit vrij was. Als dit 0 nadert: stack overflow risico!
*/
static void report() {
Serial.println("=== Stack High Water Marks ===");
// FreeRTOS task list
char taskListBuffer[512];
vTaskList(taskListBuffer); // Vult buffer met naam, status, prio, stack, num
Serial.println(taskListBuffer);
}
/**
* Controleer een specifieke task op kritisch stack gebruik.
* @return true als meer dan 10% van de stack ongebruikt is
*/
static bool isSafe(TaskHandle_t task, size_t stackSizeWords) {
UBaseType_t highWaterMark = uxTaskGetStackHighWaterMark(task);
float usagePercent = 100.0f - (100.0f * highWaterMark / stackSizeWords);
Serial.printf("Stack gebruik: %.1f%%\n", usagePercent);
return highWaterMark > (stackSizeWords / 10); // >10% vrij = veilig
}
/**
* Heap fragmentatie analyse.
* Vergelijk met Java MemoryMXBean.getHeapMemoryUsage()
*/
static void reportHeap() {
Serial.printf("Heap vrij: %u bytes\n", ESP.getFreeHeap());
Serial.printf("Heap minimum: %u bytes\n", ESP.getMinFreeHeap());
Serial.printf("Heap totaal: %u bytes\n", ESP.getHeapSize());
Serial.printf("Grootste blok:%u bytes\n", ESP.getMaxAllocHeap());
// ^ Als dit << getFreeHeap(): fragmentatie!
}
};
} // namespace TractorOS
# 5. std::optional en std::variant: type-veilig foutafhandelen
In Java retourneer je null of gooi je een exception. In C++ op embedded systemen wil je geen van beide. std::optional en std::variant zijn de moderne alternatieven.
# 5.1 std::optional: nullable zonder null
#include <optional>
// Java: T findById(int id) throws NotFoundException
// C++: std::optional<T> findById(int id)
/**
* Lees een float waarde van een sensor.
* Retourneert geen waarde als de sensor niet gereed is.
*
* Java: Optional<Float> β maar in C++ is dit zero-cost, geen heap-allocatie
* TS: T | undefined
*/
std::optional<float> readTemperature(ISensor& sensor) {
if (!sensor.isReady()) return std::nullopt; // "geen waarde"
SensorReading r = sensor.read();
if (!r.isValid) return std::nullopt;
return r.value.floatValue; // Impliciete constructie van optional<float>
}
// Gebruik:
void processTemperature() {
auto temp = readTemperature(myTempSensor);
// Patroon 1: if-check (meest expliciet)
if (temp.has_value()) {
Serial.printf("Temperatuur: %.2fΒ°C\n", temp.value());
}
// Patroon 2: value_or β geef standaard als geen waarde
float safeTemp = temp.value_or(0.0f);
// Patroon 3: if-with-initializer (C++17) β meest elegant
if (auto t = readTemperature(myTempSensor)) {
Serial.printf("Temperatuur: %.2fΒ°C\n", *t);
// ^ * operator op optional = .value()
}
}
# 5.2 std::variant als Result type
In Rust is Result<T, E> ingebouwd. In C++ bouwen we het met std::variant:
// src/core/Result.h
#pragma once
#include <variant>
#include <cstring>
namespace TractorOS {
/**
* Result type voor operaties die kunnen falen.
*
* Rust: Result<T, E>
* Java: T of throws Exception (maar geen compile-time afdwinging)
* TS: { data: T } | { error: E } (discriminated union)
*
* Forceert de aanroeper om te kiezen: verwerk Γ³f de waarde Γ³f de fout.
* Nooit beide, nooit negeren (zonder bewuste keuze).
*/
template<typename T>
class Result {
public:
// Succesvolle constructie
static Result<T> ok(T value) {
Result r;
r._data = std::move(value);
return r;
}
// Fout-constructie
static Result<T> error(const char* message) {
Result r;
r._data = ErrorState{message};
return r;
}
bool isOk() const { return std::holds_alternative<T>(_data); }
bool isError() const { return std::holds_alternative<ErrorState>(_data); }
// Waarde opvragen (alleen als isOk())
const T& value() const {
return std::get<T>(_data);
}
// Foutmelding opvragen (alleen als isError())
const char* errorMessage() const {
return std::get<ErrorState>(_data).message;
}
// Pattern matching via std::visit
template<typename OkHandler, typename ErrHandler>
auto match(OkHandler onOk, ErrHandler onError) const {
return std::visit([&](const auto& v) {
using V = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<V, T>) {
return onOk(v);
} else {
return onError(v.message);
}
}, _data);
}
private:
struct ErrorState { const char* message; };
std::variant<T, ErrorState> _data;
};
// Specialisatie voor void (operaties zonder retourwaarde)
template<>
class Result<void> {
public:
static Result<void> ok() { return Result{true, nullptr}; }
static Result<void> error(const char* msg) { return Result{false, msg}; }
bool isOk() const { return _ok; }
bool isError() const { return !_ok; }
const char* errorMessage() const { return _errorMsg; }
private:
Result(bool ok, const char* msg) : _ok(ok), _errorMsg(msg) {}
bool _ok;
const char* _errorMsg;
};
} // namespace TractorOS
Gebruik:
Result<float> parseGpsCoordinate(const char* nmea) {
if (nmea == nullptr || strlen(nmea) == 0) {
return Result<float>::error("Lege NMEA string");
}
char* end;
float value = strtof(nmea, &end);
if (end == nmea) {
return Result<float>::error("Ongeldige coΓΆrdinaat notatie");
}
return Result<float>::ok(value);
}
// Aanroepsite β de fout kan niet genegeerd worden
void processGpsData(const char* nmeaSentence) {
parseGpsCoordinate(nmeaSentence).match(
[](float coord) {
Serial.printf("CoΓΆrdinaat: %.6f\n", coord);
},
[](const char* err) {
Serial.printf("GPS fout: %s\n", err);
}
);
}
# 6. CRTP: statisch polymorfisme zonder vtable overhead
Virtuele functies kosten een vtable-lookup per aanroep β minuscuul op een desktop, maar merkbaar als je 1000x per seconde sensordata verwerkt. CRTP (Curiously Recurring Template Pattern) biedt polymorfisme op compile-tijd: nul runtime overhead.
# 6.1 Het CRTP principe
// Runtime polymorfisme (vtable overhead):
class ISensor { virtual SensorReading read() = 0; };
class DS18B20Sensor : public ISensor { ... };
// Compileertijd polymorfisme (CRTP, nul overhead):
template<typename Derived>
class SensorBase {
public:
SensorReading read() {
return static_cast<Derived*>(this)->readImpl();
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cast naar het concrete type
// De compiler weet dit op compile-tijd β geen vtable!
}
// Template method pattern β hooks voor subklassen
void calibrate() {
static_cast<Derived*>(this)->beginCalibration();
// ... gemeenschappelijke logica ...
static_cast<Derived*>(this)->endCalibration();
}
};
class DS18B20Sensor : public SensorBase<DS18B20Sensor> {
friend class SensorBase<DS18B20Sensor>;
SensorReading readImpl() {
// Daadwerkelijke hardware-aanroep
_dallasSensors.requestTemperatures();
float temp = _dallasSensors.getTempCByIndex(0);
return makeSensorReading(temp);
}
};
# 6.2 CRTP voor een type-veilige Builder
// src/core/TelemetryPacketBuilder.h
#pragma once
#include "middleware/IMiddleware.h"
namespace TractorOS {
/**
* CRTP Builder voor TelemetryPackets.
* Elk setter-methode retourneert de concrete builder β type-veilig method chaining.
*
* Java: Builder pattern met return type Builder<T extends Builder<T, R>, R>
* Dit is dezelfde truc maar zonder heap-allocatie van de builder zelf.
*/
template<typename Derived>
class TelemetryPacketBuilderBase {
public:
Derived& withTimestamp(uint32_t ts) {
_packet.timestamp = ts;
return self();
}
Derived& withEngineTemp(float temp) {
_packet.engineTemp = temp;
return self();
}
Derived& withFuelLevel(float level) {
_packet.fuelLevel = level;
return self();
}
Derived& withRPM(uint16_t rpm) {
_packet.rpm = rpm;
return self();
}
TelemetryPacket build() const {
return _packet;
}
protected:
TelemetryPacket _packet{};
private:
Derived& self() { return static_cast<Derived&>(*this); }
};
// Basis builder
class TelemetryPacketBuilder
: public TelemetryPacketBuilderBase<TelemetryPacketBuilder> {};
// Uitgebreide builder met GPS β erft alle methoden type-veilig
class GpsTelemetryPacketBuilder
: public TelemetryPacketBuilderBase<GpsTelemetryPacketBuilder> {
public:
GpsTelemetryPacketBuilder& withGPS(float lat, float lon) {
_packet.gpsLat = lat;
_packet.gpsLon = lon;
return *this;
}
};
} // namespace TractorOS
Gebruik:
auto packet = GpsTelemetryPacketBuilder{}
.withTimestamp(millis())
.withEngineTemp(87.5f)
.withFuelLevel(0.65f)
.withRPM(2200)
.withGPS(52.3676f, 4.9041f)
.build();
// Alle methoden zijn type-veilig: .withGPS() is alleen beschikbaar op GpsTelemetryPacketBuilder
# 7. De Config-laag: type-veilige configuratie uit NVS/Flash
De ESP32 heeft NVS (Non-Volatile Storage) β vergelijkbaar met een sleutel-waarde store in flash-geheugen. We bouwen een type-veilige config-laag erop.
# 7.1 De NVS-abstractie
// src/config/ConfigStore.h
#pragma once
#include <nvs_flash.h>
#include <nvs.h>
#include <optional>
#include <cstring>
#include "core/Result.h"
namespace TractorOS {
/**
* Type-veilige wrapper voor ESP32 NVS (Non-Volatile Storage).
*
* Java equivalent: java.util.prefs.Preferences
* TS equivalent: localStorage (maar dan in flash, niet RAM)
*
* NVS kan strings, integers en blobs (byte arrays) opslaan.
* Wij voegen type-veiligheid toe via templates.
*/
class ConfigStore {
public:
explicit ConfigStore(const char* namespaceName)
: _namespace(namespaceName) {}
Result<void> open() {
esp_err_t err = nvs_open(_namespace, NVS_READWRITE, &_handle);
if (err != ESP_OK) {
return Result<void>::error(esp_err_to_name(err));
}
_isOpen = true;
return Result<void>::ok();
}
void close() {
if (_isOpen) {
nvs_close(_handle);
_isOpen = false;
}
}
// Template specialisaties per type
template<typename T>
Result<void> set(const char* key, const T& value);
template<typename T>
std::optional<T> get(const char* key);
Result<void> commit() {
esp_err_t err = nvs_commit(_handle);
return err == ESP_OK ? Result<void>::ok()
: Result<void>::error(esp_err_to_name(err));
}
bool erase(const char* key) {
return nvs_erase_key(_handle, key) == ESP_OK;
}
private:
const char* _namespace;
nvs_handle_t _handle = 0;
bool _isOpen = false;
};
// Expliciete template specialisaties voor ondersteunde types
template<>
Result<void> ConfigStore::set<uint32_t>(const char* key, const uint32_t& value) {
esp_err_t err = nvs_set_u32(_handle, key, value);
return err == ESP_OK ? Result<void>::ok()
: Result<void>::error(esp_err_to_name(err));
}
template<>
std::optional<uint32_t> ConfigStore::get<uint32_t>(const char* key) {
uint32_t value;
if (nvs_get_u32(_handle, key, &value) == ESP_OK) return value;
return std::nullopt;
}
template<>
Result<void> ConfigStore::set<float>(const char* key, const float& value) {
// NVS heeft geen float type β sla op als uint32 via type punning
uint32_t bits;
memcpy(&bits, &value, sizeof(float));
return set<uint32_t>(key, bits);
}
template<>
std::optional<float> ConfigStore::get<float>(const char* key) {
auto bits = get<uint32_t>(key);
if (!bits) return std::nullopt;
float value;
memcpy(&value, &*bits, sizeof(float));
return value;
}
} // namespace TractorOS
# 7.2 Typed Config met validatie
// src/config/SystemConfig.h
#pragma once
#include "ConfigStore.h"
#include <array>
namespace TractorOS {
/**
* Gestructureerde systeemconfiguratie met validatie.
*
* Vergelijk: Spring Boot's @ConfigurationProperties
* of TS's zod schema validatie.
*/
struct SystemConfig {
// Netwerk
char wifiSsid[64] = "TractorNet";
char wifiPassword[64] = "";
char serverHost[64] = "192.168.1.100";
uint16_t serverPort = 8765;
// Telemetrie
uint32_t telemetryIntervalMs = 1000;
uint8_t maxRetries = 3;
bool compressionEnabled = true;
// Drempelwaarden voor alerts
float maxEngineTemp = 95.0f;
float minFuelLevel = 0.10f;
// Valideer de configuratie β retourneert beschrijving van eerste fout
std::optional<const char*> validate() const {
if (serverPort == 0) return "serverPort mag niet 0 zijn";
if (telemetryIntervalMs < 100) return "telemetryIntervalMs minimum is 100ms";
if (maxRetries == 0) return "maxRetries moet minimaal 1 zijn";
if (maxEngineTemp < 50.0f || maxEngineTemp > 150.0f)
return "maxEngineTemp buiten verwacht bereik (50-150Β°C)";
if (minFuelLevel < 0.0f || minFuelLevel > 1.0f)
return "minFuelLevel moet tussen 0.0 en 1.0 liggen";
return std::nullopt; // Valide
}
};
class ConfigManager {
public:
ConfigManager() : _store("tractor_cfg") {}
/**
* Laad config uit NVS. Vult aan met defaults voor ontbrekende sleutels.
*/
SystemConfig load() {
SystemConfig cfg;
if (_store.open().isError()) {
Serial.println("NVS open mislukt, defaults gebruikt");
return cfg;
}
// Laad elk veld β gebruik default als afwezig
if (auto v = _store.get<uint32_t>("tel_interval")) cfg.telemetryIntervalMs = *v;
if (auto v = _store.get<float>("max_eng_temp")) cfg.maxEngineTemp = *v;
if (auto v = _store.get<float>("min_fuel")) cfg.minFuelLevel = *v;
// String sleutels: aparte NVS aanroep
loadString("wifi_ssid", cfg.wifiSsid, sizeof(cfg.wifiSsid));
loadString("server_host", cfg.serverHost, sizeof(cfg.serverHost));
_store.close();
// Valideer geladen config
if (auto err = cfg.validate()) {
Serial.printf("Config validatie mislukt: %s β defaults gebruikt\n", *err);
return SystemConfig{};
}
return cfg;
}
Result<void> save(const SystemConfig& cfg) {
if (auto err = cfg.validate()) {
return Result<void>::error(*err);
}
auto openResult = _store.open();
if (openResult.isError()) return openResult;
_store.set<uint32_t>("tel_interval", cfg.telemetryIntervalMs);
_store.set<float>("max_eng_temp", cfg.maxEngineTemp);
_store.set<float>("min_fuel", cfg.minFuelLevel);
// ... overige velden ...
auto commitResult = _store.commit();
_store.close();
if (commitResult.isOk()) {
EventBus::instance().publish(ConfigChangedEvent{"all", "saved"});
}
return commitResult;
}
private:
void loadString(const char* key, char* dest, size_t maxLen) {
size_t actualLen = maxLen;
if (nvs_get_str(_store._handle, key, dest, &actualLen) != ESP_OK) {
// Key niet gevonden β dest behoudt zijn default waarde
}
}
ConfigStore _store;
};
} // namespace TractorOS
# 8. OTA Updates: de Deployment Pipeline voor embedded
"Je boordcomputer staat midden op een akker" β een firmware-update moet draadloos kunnen. De ESP32 ondersteunt OTA (Over-The-Air) updates natively.
# 8.1 De OTA Architectuur
Ontwikkelaar β CI/CD β HTTPS Server β ESP32 OTA Task
β
βΌ
Validatie
(hash check)
β
βΌ
Flash naar
inactieve partitie
β
βΌ
Reboot naar
nieuwe partitie
# 8.2 De OTA Manager
// src/ota/OtaManager.h
#pragma once
#include <esp_ota_ops.h>
#include <esp_https_ota.h>
#include <esp_http_client.h>
#include "core/Result.h"
#include "events/EventBus.h"
namespace TractorOS {
struct OtaProgressEvent {
uint32_t bytesWritten;
uint32_t totalBytes;
uint8_t percentage;
};
struct OtaCompletedEvent {
bool success;
const char* message;
};
class OtaManager {
public:
struct Config {
const char* updateUrl; // https://server/firmware.bin
const char* serverCert; // PEM root certificate voor HTTPS verificatie
uint32_t timeoutMs = 30000;
};
explicit OtaManager(const Config& config) : _config(config) {}
/**
* Start OTA update in een achtergrond-task.
* Blokkeert NIET de sensor- of telemetrie-taken.
*/
void checkAndUpdate() {
xTaskCreate(_otaTaskFunction, "OTA", 8192, this, 1, nullptr);
}
/**
* Huidige firmware versie β handig voor update-check op server.
*/
static const char* currentVersion() {
const esp_app_desc_t* desc = esp_app_get_description();
return desc->version;
}
private:
static void _otaTaskFunction(void* params) {
auto* self = static_cast<OtaManager*>(params);
self->_performUpdate();
vTaskDelete(nullptr); // Task verwijdert zichzelf na afloop
}
void _performUpdate() {
Serial.printf("OTA gestart: %s\n", _config.updateUrl);
esp_http_client_config_t httpConfig = {};
httpConfig.url = _config.updateUrl;
httpConfig.cert_pem = _config.serverCert;
httpConfig.timeout_ms = _config.timeoutMs;
httpConfig.skip_cert_common_name_check = false; // Altijd cert valideren!
esp_https_ota_config_t otaConfig = {};
otaConfig.http_config = &httpConfig;
esp_https_ota_handle_t otaHandle;
esp_err_t err = esp_https_ota_begin(&otaConfig, &otaHandle);
if (err != ESP_OK) {
EventBus::instance().publish(OtaCompletedEvent{false, esp_err_to_name(err)});
return;
}
esp_app_image_format_t imageInfo;
esp_https_ota_get_img_desc(otaHandle, &imageInfo);
while (true) {
err = esp_https_ota_perform(otaHandle);
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) break;
// Publiceer voortgang
int32_t written = esp_https_ota_get_image_len_read(otaHandle);
int32_t total = esp_https_ota_get_image_size(otaHandle);
if (total > 0) {
EventBus::instance().publish(OtaProgressEvent{
static_cast<uint32_t>(written),
static_cast<uint32_t>(total),
static_cast<uint8_t>(100 * written / total)
});
}
}
bool success = (esp_https_ota_finish(otaHandle) == ESP_OK);
EventBus::instance().publish(OtaCompletedEvent{
success,
success ? "Update succesvol β herstart..." : "Update mislukt"
});
if (success) {
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart(); // Herstart naar nieuwe firmware
}
}
Config _config;
};
} // namespace TractorOS
# 8.3 Rollback strategie
// In setup(): markeer de partitie als succesvol als alles opstart
void setup() {
// ... initialisatie ...
// Als we hier komen zonder crash: deze firmware werkt
// Markeer de OTA-partitie als "valid" β anders herstart ESP32
// automatisch naar de vorige versie na een watchdog timeout
esp_ota_mark_app_valid_cancel_rollback();
Serial.printf("Firmware v%s gevalideerd\n", OtaManager::currentVersion());
}
# 9. Diagnostics & Observability: embedded APM
"Op een akker kun je niet debuggen met een IDE." Je hebt observability nodig.
# 9.1 Structured Logging
// src/diagnostics/Logger.h
#pragma once
#include <cstdio>
#include <cstdarg>
namespace TractorOS {
/**
* Gestructureerde logger met levels en module-tags.
*
* Java: SLF4J β Logger log = LoggerFactory.getLogger(MyClass.class);
* TS: winston β const logger = winston.createLogger({...});
*/
class Logger {
public:
enum class Level : uint8_t {
DEBUG = 0,
INFO = 1,
WARNING = 2,
ERROR = 3,
NONE = 4
};
static void setLevel(Level level) { _minLevel = level; }
template<typename... Args>
static void debug(const char* module, const char* fmt, Args... args) {
log(Level::DEBUG, module, fmt, args...);
}
template<typename... Args>
static void info(const char* module, const char* fmt, Args... args) {
log(Level::INFO, module, fmt, args...);
}
template<typename... Args>
static void warn(const char* module, const char* fmt, Args... args) {
log(Level::WARNING, module, fmt, args...);
}
template<typename... Args>
static void error(const char* module, const char* fmt, Args... args) {
log(Level::ERROR, module, fmt, args...);
}
private:
static void log(Level level, const char* module, const char* fmt, ...) {
if (level < _minLevel) return;
const char* levelStr[] = {"DBG", "INF", "WRN", "ERR"};
// Timestamp in milliseconden
uint32_t ms = millis();
char message[256];
va_list args;
va_start(args, fmt);
vsnprintf(message, sizeof(message), fmt, args);
va_end(args);
// Gestructureerd formaat: ISO-8601-achtig voor log aggregatie
Serial.printf("[%07u][%s][%-12s] %s\n",
ms, levelStr[static_cast<uint8_t>(level)], module, message);
// Eventueel: stuur naar remote logging endpoint via EventBus
if (level >= Level::WARNING) {
// Publiceer als event β TransmitTask kan dit remote doorsturen
// EventBus::instance().publish(LogEvent{level, module, message});
}
}
static inline Level _minLevel = Level::INFO;
};
// Convenience macros met automatische module-naam via __func__
#define LOG_D(fmt, ...) Logger::debug(__func__, fmt, ##__VA_ARGS__)
#define LOG_I(fmt, ...) Logger::info(__func__, fmt, ##__VA_ARGS__)
#define LOG_W(fmt, ...) Logger::warn(__func__, fmt, ##__VA_ARGS__)
#define LOG_E(fmt, ...) Logger::error(__func__, fmt, ##__VA_ARGS__)
} // namespace TractorOS
# 9.2 Health Check systeem
// src/diagnostics/HealthMonitor.h
#pragma once
#include <vector>
#include <functional>
namespace TractorOS {
/**
* Health check systeem.
*
* Java: Spring Boot Actuator /health endpoint
* TS: @nestjs/terminus HealthCheckService
*
* Elk subsysteem registreert een health check.
* De monitor publiceert periodiek de algehele status.
*/
class HealthMonitor {
public:
enum class Status : uint8_t { HEALTHY, DEGRADED, UNHEALTHY };
struct HealthCheck {
const char* name;
std::function<Status()> checker;
Status lastStatus = Status::HEALTHY;
uint32_t lastCheckMs = 0;
uint32_t intervalMs = 5000;
};
void registerCheck(const char* name,
std::function<Status()> checker,
uint32_t intervalMs = 5000) {
_checks.push_back({name, checker, Status::HEALTHY, 0, intervalMs});
}
/**
* Voer alle checks uit die aan hun interval toe zijn.
* Aanroepen in een lage-prioriteit diagnostics-task.
*/
Status runChecks() {
Status overall = Status::HEALTHY;
uint32_t now = millis();
for (auto& check : _checks) {
if (now - check.lastCheckMs < check.intervalMs) continue;
check.lastCheckMs = now;
check.lastStatus = check.checker();
if (check.lastStatus == Status::UNHEALTHY) {
overall = Status::UNHEALTHY;
Logger::error("HealthMonitor", "UNHEALTHY: %s", check.name);
} else if (check.lastStatus == Status::DEGRADED &&
overall == Status::HEALTHY) {
overall = Status::DEGRADED;
Logger::warn("HealthMonitor", "DEGRADED: %s", check.name);
}
}
return overall;
}
void report() const {
Serial.println("=== Health Status ===");
for (const auto& check : _checks) {
const char* statusStr[] = {"HEALTHY ", "DEGRADED ", "UNHEALTHY"};
Serial.printf(" [%s] %s\n",
statusStr[static_cast<uint8_t>(check.lastStatus)],
check.name);
}
}
private:
std::vector<HealthCheck> _checks;
};
} // namespace TractorOS
Registratie:
// In setup():
healthMonitor.registerCheck("WiFi", []() {
return WiFi.isConnected() ? HealthMonitor::Status::HEALTHY
: HealthMonitor::Status::UNHEALTHY;
});
healthMonitor.registerCheck("Sensor", [&sensorMgr]() {
return sensorMgr.allSensorsReady() ? HealthMonitor::Status::HEALTHY
: HealthMonitor::Status::DEGRADED;
});
healthMonitor.registerCheck("Heap", []() {
size_t freeHeap = ESP.getFreeHeap();
if (freeHeap > 50000) return HealthMonitor::Status::HEALTHY;
if (freeHeap > 20000) return HealthMonitor::Status::DEGRADED;
return HealthMonitor::Status::UNHEALTHY;
});
# 10. Productie-architectuur: alles samenvoegen
# 10.1 De complete component map
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TractorOS Productie-architectuur β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ Events ββββββββββββββββββββββββββββββββββ β
β β SensorTask β βββββββββββΊ β Event Bus β β
β β (Core 0, p3) β β (thread-safe pub/sub) β β
β ββββββββββββββββ ββββ¬βββββββββββ¬βββββββββββ¬ββββββββ β
β β β β β
β ββββββββββββββββ βββββββΌβββββ βββββΌβββββ ββββΌβββββββ β
β β OTA Task β βTransmit β β Alert β βDiagnos- β β
β β (Core 0, p1) β βTask β βManager β βtics β β
β ββββββββββββββββ β(Core 1) β β β βTask β β
β βββββββ¬βββββ ββββββββββ βββββββββββ β
β ββββββββββββββββ β β
β β Config β ββββββββΌββββββββββββββββββββββββββββ β
β β Manager β β Middleware Pipeline β β
β β (NVS/Flash) β β RateLimit β Checksum β Compress β β
β ββββββββββββββββ ββββββββ¬ββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββ ββββββββΌββββββββββββββββββββββββββββ β
β β Health β β FailoverTransport β β
β β Monitor β β WiFi (p0) β LoRa (p1) β 4G(p2) β β
β ββββββββββββββββ ββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 10.2 De productie main.cpp
// src/main.cpp
#include <Arduino.h>
#include <nvs_flash.h>
// Core infrastructure
#include "events/EventBus.h"
#include "diagnostics/Logger.h"
#include "diagnostics/HealthMonitor.h"
#include "diagnostics/StackMonitor.h"
#include "config/ConfigManager.h"
#include "memory/ObjectPool.h"
// Transport
#include "transport/WiFiTransport.h"
#include "transport/LoRaTransport.h"
#include "transport/FailoverTransport.h"
// Middleware
#include "middleware/ChecksumMiddleware.h"
#include "middleware/CompressionMiddleware.h"
#include "middleware/RateLimitMiddleware.h"
// Sensors
#include "sensors/DS18B20Sensor.h"
// Tasks
#include "tasks/SensorTask.h"
#include "core/SensorManager.h"
#include "core/MiddlewarePipeline.h"
using namespace TractorOS;
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// STATISCHE ALLOCATIES β alles pre-gealloceerd, nul heap gebruik
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Config
static ConfigManager configManager;
static SystemConfig config;
// Transport (objecten leven voor de duur van het programma)
static WiFiTransport wifiTransport("", "", "", 0); // Gevuld vanuit config
static LoRaTransport loraTransport(868E6);
static FailoverTransport failoverTransport;
// Middleware
static MiddlewarePipeline pipeline;
// Sensor infrastructure
static SensorManager sensorManager;
static SensorTask::SensorQueue sensorQueue;
static SensorTask sensorTask(sensorManager, sensorQueue);
// Memory pool voor telemetrie packets
static ObjectPool<TelemetryPacket, 32> packetPool;
// Diagnostics
static HealthMonitor healthMonitor;
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// EVENT HANDLERS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void setupEventHandlers() {
// Sensor reading β build packet β stuur via pipeline + transport
EventBus::instance().subscribe<SensorReadingEvent>([](const SensorReadingEvent& e) {
auto packet = packetPool.acquire();
if (!packet) {
LOG_W("main", "PacketPool leeg β reading gedropt");
return;
}
packet->timestamp = e.reading.timestamp;
if (e.reading.type == SensorReading::Type::TEMPERATURE) {
packet->engineTemp = e.reading.value.floatValue;
}
// Dispatch via middleware pipeline
uint8_t buffer[sizeof(TelemetryPacket) + 8];
memcpy(buffer, packet.get(), sizeof(TelemetryPacket));
TelemetryContext ctx{};
ctx.data = buffer;
ctx.dataLength = sizeof(TelemetryPacket);
ctx.timestamp = packet->timestamp;
pipeline.execute(ctx);
if (ctx.shouldContinue && failoverTransport.isConnected()) {
failoverTransport.send(ctx.data, ctx.dataLength);
}
// packet automatisch terug naar pool bij scope-exit
});
// Systeem alerts β log + verhoogde telemetrie frequentie
EventBus::instance().subscribe<SystemAlertEvent>([](const SystemAlertEvent& alert) {
LOG_E("Alert", "[%s] %s",
alert.severity == SystemAlertEvent::Severity::CRITICAL ? "KRITIEK" : "WAARSCH.",
alert.message);
});
// OTA progress logging
EventBus::instance().subscribe<OtaProgressEvent>([](const OtaProgressEvent& e) {
LOG_I("OTA", "Voortgang: %u%%", e.percentage);
});
EventBus::instance().subscribe<OtaCompletedEvent>([](const OtaCompletedEvent& e) {
if (e.success) LOG_I("OTA", "Update geslaagd");
else LOG_E("OTA", "Update mislukt: %s", e.message);
});
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// SETUP
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void setup() {
Serial.begin(115200);
// NVS initialiseren (vereist voor WiFi en eigen config)
nvs_flash_init();
LOG_I("main", "TractorOS v%s opstarten...", OtaManager::currentVersion());
// 1. Config laden
config = configManager.load();
LOG_I("main", "Config geladen β telemetrie interval: %ums",
config.telemetryIntervalMs);
// 2. Transport configureren met config-waarden
new (&wifiTransport) WiFiTransport(
config.wifiSsid, config.wifiPassword,
config.serverHost, config.serverPort
);
failoverTransport.addTransport(&wifiTransport, 0);
failoverTransport.addTransport(&loraTransport, 1);
Reacties (0 )
Geen reacties beschikbaar.
Log in om een reactie te plaatsen.