Modern C++ voor Software Architecten: Gevorderd β€” Deel 2

πŸ–‹οΈ bert

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

  1. C++ Templates: compileertijd-polymorfisme
  2. FreeRTOS: concurrent programmeren op de ESP32
  3. Event-Driven Architectuur: de Event Bus
  4. Geavanceerd Geheugenbeheer: Object Pools & Static Allocation
  5. std::optional en std::variant: type-veilig foutafhandelen
  6. CRTP: statisch polymorfisme zonder vtable overhead
  7. De Config-laag: type-veilige configuratie uit NVS/Flash
  8. OTA Updates: de Deployment Pipeline voor embedded
  9. Diagnostics & Observability: embedded APM
  10. 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.