Voor wie: Ervaren Java/TypeScript Software Architecten die embedded development willen benaderen met dezelfde architecturele discipline als enterprise software. Platform: ESP32 + PlatformIO + C++17 Aanpak: OO-patterns first, hardware second.
# Inhoudsopgave
- De mentale switch: van JVM/V8 naar bare metal
- C++ vs Java/TS: de essentiΓ«le verschillen
- Geheugenbeheer zonder Garbage Collector
- Interfaces & Dependency Injection in C++
- Het Transport Strategy Pattern
- De Middleware Stack voor Telemetrie
- Hardware Abstractie: de MVC-split
- Alles samenvoegen: de Boordcomputer Architectuur
- PlatformIO Project Setup
- Testing zonder OS
# 1. De mentale switch
# Wat er niet meer is
In Java/TypeScript neem je een hoop zaken voor lief die op embedded systemen simpelweg niet bestaan:
| Java/TypeScript | ESP32 bare metal |
|---|---|
| Garbage Collector | Jij beheert het geheugen |
| Exceptions (try/catch overal) | Exceptions kosten heap β gebruik spaarzaam |
| Onbeperkt heap | ~320KB SRAM totaal, ~200KB beschikbaar |
| Threads / async event loop | FreeRTOS tasks of de Arduino loop() |
console.log / SLF4J |
Serial.println β soms letterlijk de enige debugger |
| Reflection / runtime type info | Standaard uitgeschakeld (-fno-rtti) |
| ClassLoader / module system | Linker + preprocessor directives |
# Wat er beter is dan je verwacht
- Compileertijd-polymorfisme via templates is krachtiger dan Java generics
- Zero-cost abstractions: een goed ontworpen C++ interface heeft nul runtime overhead t.o.v. directe hardware-aanroepen
- Deterministische timing: geen GC-pauses, geen JIT-compilatie β essentieel voor real-time sensordata
constexpr: berekeningen op compile-time uitvoeren die in Java runtime zouden kosten
# De gouden regel voor embedded OO
Ontwerp voor minimale heap-allocatie. Gebruik stack en statische allocatie waar mogelijk.
In Java alloceer je alles op de heap (new) en de GC ruimt op. Op de ESP32 fragmenteert de heap snel, en er is niemand die opruimt. We lossen dit op met std::unique_ptr, stack-allocatie, en objectpools.
# 2. C++ vs Java/TS: de essentiΓ«le verschillen
# 2.1 Pointers: wat zijn ze en waarom bestaan ze?
In Java heb je referenties, maar je ziet ze nooit. In C++ zijn ze expliciet β en dat is bewust, want jij bepaalt waar het object leeft.
// Java: altijd impliciet een referentie naar heap
MyClass obj = new MyClass(); // obj is een verborgen pointer
// C++: jij kiest bewust
MyClass onStack; // Object leeft op de stack β automatisch vernietigd
MyClass* rawPtr = new MyClass(); // Object op heap β JIJ moet delete aanroepen β οΈ
std::unique_ptr<MyClass> safePtr = std::make_unique<MyClass>(); // RAII β automatisch
*De `en&` tekens in context:**
void processData(const SensorReading& reading) {
// ^ ampersand = "geef me een referentie, geen kopie"
// Dit is C++'s equivalent van Java's object-parameters.
// Zonder & krijg je een *kopie* van het object β duur en zelden gewenst.
float temp = reading.temperature; // normale toegang via referentie
}
void updateBuffer(float* buffer, size_t length) {
// ^ asterisk = "dit is een pointer naar een float-array"
// Pointers naar arrays zijn common in hardware-code voor DMA-buffers etc.
buffer[0] = 23.5f;
}
Waarom heeft hardware dit nodig? Een sensorchip heeft een I2C-adres en registers in zijn eigen geheugenruimte. De enige manier om erin te schrijven is via een pointer naar dat geheugeradres. C++ maakt dit expliciet, zodat de compiler en jij precies weten wat er naar welk stuk geheugen gaat.
# 2.2 const correctheid: meer dan final
In Java/TS is final / const vrij simpel. In C++ heeft het drie dimensies:
// 1. Const variabele β equivalent aan Java final / TS const
const int BAUD_RATE = 115200;
// 2. Const referentie β "ik ga dit object niet aanpassen"
// Dit is de standaard voor functie-parameters in C++
void logReading(const SensorReading& reading) {
// reading.temperature = 0; // β Compileerfout! Goed.
Serial.println(reading.temperature);
}
// 3. Const methode β "deze methode verandert het object niet"
class Sensor {
public:
float getTemperature() const { // β const na de parameterlijst
return _temperature; // mag _temperature niet aanpassen
}
private:
float _temperature;
};
Vuistregel: Markeer alles const tenzij je een expliciete reden hebt om dat niet te doen. De compiler helpt je dan als je per ongeluk state aanpast.
# 2.3 Header files: de "interface declaration" van C++
C++ splitst declaratie en implementatie. Dit voelt vreemd voor Java-ontwikkelaars, maar het is feitelijk vergelijkbaar met TypeScript .d.ts declaration files:
ITransport.h ββ interface ITransport.java / ITransport.d.ts
ITransport.cpp ββ AbstractTransport.java / transport.ts (implementatie)
// ITransport.h β de "interface" (puur virtuele klasse)
#pragma once // Equivalent van Java's package system: voorkomt dubbele includes
class ITransport {
public:
virtual ~ITransport() = default; // Altijd een virtuele destructor in interfaces!
virtual bool connect() = 0; // = 0 betekent "puur virtueel" = abstract method
virtual bool send(const uint8_t* data, size_t length) = 0;
virtual bool isConnected() const = 0;
};
// WiFiTransport.cpp β de implementatie
#include "ITransport.h"
#include <WiFi.h>
class WiFiTransport : public ITransport { // "implements ITransport" in Java
public:
bool connect() override { // override = @Override in Java
WiFi.begin(_ssid, _password);
return WiFi.waitForConnectResult() == WL_CONNECTED;
}
// ...
};
Waarom = default bij de destructor? In Java/TS worden objecten automatisch opgeruimd. In C++ moet de juiste destructor aangeroepen worden als je een ITransport* pointer hebt die eigenlijk naar een WiFiTransport wijst. Zonder virtual ~ITransport() roep je de verkeerde destructor aan β memory leak gegarandeerd.
# 2.4 Namespaces vs packages
// Java: package nl.mijnbedrijf.tractorcomputer;
// C++:
namespace TractorOS {
namespace Transport {
class WiFiTransport : public ITransport { /* ... */ };
} // namespace Transport
} // namespace TractorOS
// Gebruik:
TractorOS::Transport::WiFiTransport wifi;
// Of met using:
using namespace TractorOS::Transport;
WiFiTransport wifi;
# 2.5 nullptr vs null
ITransport* transport = nullptr; // C++ nullptr β type-safe, niet gewoon 0
if (transport != nullptr) {
transport->send(data, length);
}
In embedded code wil je nullptr-checks overal waar je raw pointers gebruikt. Met unique_ptr en referenties kun je dit grotendeels elimineren (zie volgende hoofdstuk).
# 3. Geheugenbeheer zonder Garbage Collector
Dit is waarschijnlijk het meest kritieke hoofdstuk voor een Java-architect.
# 3.1 RAII: het patroon dat alles oplost
Resource Acquisition Is Initialization. Wanneer een object gecreΓ«erd wordt, verwerft het zijn resources. Wanneer het vernietigd wordt, geeft het ze vrij β automatisch, determinΓstisch, zonder GC.
// Java: try-with-resources (handmatig resource management)
try (InputStream is = new FileInputStream("data.txt")) {
// is automatisch gesloten bij einde blok
}
// C++ RAII: het object regelt zichzelf altijd
class SpiDevice {
public:
SpiDevice(uint8_t csPin) : _csPin(csPin) {
SPI.begin(); // Resource verwerven in constructor
pinMode(_csPin, OUTPUT);
Serial.println("SPI geopend");
}
~SpiDevice() {
SPI.end(); // Resource vrijgeven in destructor β altijd, ook bij exceptions
Serial.println("SPI gesloten");
}
// Geen copy toegestaan β resource ownership is exclusief
SpiDevice(const SpiDevice&) = delete;
SpiDevice& operator=(const SpiDevice&) = delete;
private:
uint8_t _csPin;
};
void measureData() {
SpiDevice sensor(GPIO_NUM_5); // GecreΓ«erd op stack
// ... gebruik sensor ...
} // β sensor destructor aangeroepen hier β altijd, ook bij early return
# 3.2 Smart Pointers: de Garbage Collector van C++
#include <memory>
// unique_ptr β één eigenaar, automatisch vrijgegeven
// Equivalent van: final MyClass obj = new MyClass(); maar dan met automatische cleanup
auto transport = std::make_unique<WiFiTransport>("ssid", "wachtwoord");
transport->connect();
// Geen delete nodig β unique_ptr vernietigt WiFiTransport als het scope verlaat
// shared_ptr β gedeeld eigenaarschap (gebruik spaarzaam op embedded!)
// Equivalent van normale Java/TS objectreferenties
auto sharedConfig = std::make_shared<SystemConfig>();
auto component1 = std::make_unique<TelemetryModule>(sharedConfig);
auto component2 = std::make_unique<LoggingModule>(sharedConfig);
// sharedConfig vrijgegeven als BEIDE modules vernietigd zijn
// weak_ptr β observeren zonder eigenaarschap (voorkomt cycli)
std::weak_ptr<SystemConfig> configObserver = sharedConfig;
ESP32-waarschuwing: shared_ptr gebruikt atomic reference counting, wat cycles-duur is op de ESP32. Prefereer unique_ptr en overdraag eigenaarschap expliciet.
# 3.3 Stack vs Heap: de strategische keuze
// SLECHT voor embedded: alles op heap alloceren (Java-gewoonte)
void badPattern() {
auto* sensor = new TemperatureSensor(GPIO_NUM_4); // heap allocatie
auto* buffer = new float[100]; // heap allocatie
// Heap fragmenteert over tijd β crash na uren/dagen draaien
delete sensor;
delete[] buffer;
}
// GOED: stack allocatie voor kortstondige objecten
void goodPattern() {
TemperatureSensor sensor(GPIO_NUM_4); // stack β automatisch vrijgegeven
float buffer[100]; // stack array β geen heap
// ...
} // alles automatisch vrijgegeven
// GOED: statische allocatie voor langlevende singletons
// Eenmalig gealloceerd, nooit vrijgegeven β geen fragmentatie
class SensorManager {
static TemperatureSensor _tempSensor; // statisch = leeft voor altijd
};
# 3.4 De "geen dynamische allocatie in loop()" regel
void loop() {
// β NOOIT DIT β creΓ«ert en vernietigt objecten elke iteratie
auto reading = std::make_unique<SensorReading>();
// β
WEL DIT β hergebruik stack-objecten
SensorReading reading; // stack allocatie, elke loop opnieuw geΓ―nitialiseerd
sensor.read(reading);
// β
OF: objecten als class members β eenmalig gealloceerd
_transport->send(_readingBuffer.data(), _readingBuffer.size());
}
# 4. Interfaces & Dependency Injection in C++
Nu kunnen we de echte architectuur bouwen. We maken het systeem testbaar door afhankelijkheden via constructors te injecteren β precies zoals Spring DI of Angular's inject().
# 4.1 De Interface HiΓ«rarchie
ITransport (pure interface)
βββ WiFiTransport
βββ LoRaTransport
βββ MockTransport (voor testing)
IMiddleware (pure interface)
βββ CompressionMiddleware
βββ EncryptionMiddleware
βββ LoggingMiddleware
ISensor (pure interface)
βββ DS18B20Sensor (temperatuur)
βββ GPSSensor
βββ MockSensor (voor testing)
# 4.2 De Transport Interface
// src/interfaces/ITransport.h
#pragma once
#include <cstdint>
#include <cstddef>
namespace TractorOS {
/**
* Transportlaag interface β uitwisselbaar via Strategy Pattern.
* Equivalent van:
* Java: interface ITransport { boolean connect(); ... }
* TS: interface ITransport { connect(): Promise<boolean>; ... }
*/
class ITransport {
public:
virtual ~ITransport() = default;
virtual bool connect() = 0;
virtual void disconnect() = 0;
virtual bool send(const uint8_t* data, size_t length) = 0;
virtual bool receive(uint8_t* buffer, size_t& bytesRead, size_t maxLength) = 0;
virtual bool isConnected() const = 0;
virtual const char* getName() const = 0;
};
} // namespace TractorOS
# 4.3 Concrete Implementaties
// src/transport/WiFiTransport.h
#pragma once
#include "interfaces/ITransport.h"
#include <WiFi.h>
#include <WiFiClient.h>
namespace TractorOS {
class WiFiTransport : public ITransport {
public:
WiFiTransport(const char* ssid, const char* password,
const char* serverHost, uint16_t serverPort)
: _ssid(ssid)
, _password(password)
, _serverHost(serverHost)
, _serverPort(serverPort) {}
bool connect() override {
WiFi.begin(_ssid, _password);
uint8_t attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
attempts++;
}
if (WiFi.status() != WL_CONNECTED) return false;
return _client.connect(_serverHost, _serverPort);
}
void disconnect() override {
_client.stop();
WiFi.disconnect();
}
bool send(const uint8_t* data, size_t length) override {
if (!isConnected()) return false;
return _client.write(data, length) == length;
}
bool receive(uint8_t* buffer, size_t& bytesRead, size_t maxLength) override {
bytesRead = 0;
while (_client.available() && bytesRead < maxLength) {
buffer[bytesRead++] = _client.read();
}
return bytesRead > 0;
}
bool isConnected() const override {
return WiFi.status() == WL_CONNECTED && _client.connected();
}
const char* getName() const override { return "WiFi"; }
private:
const char* _ssid;
const char* _password;
const char* _serverHost;
uint16_t _serverPort;
WiFiClient _client;
};
} // namespace TractorOS
// src/transport/LoRaTransport.h
#pragma once
#include "interfaces/ITransport.h"
#include <LoRa.h>
namespace TractorOS {
class LoRaTransport : public ITransport {
public:
explicit LoRaTransport(long frequency = 868E6)
: _frequency(frequency) {}
bool connect() override {
return LoRa.begin(_frequency);
}
void disconnect() override {
LoRa.end();
}
bool send(const uint8_t* data, size_t length) override {
LoRa.beginPacket();
LoRa.write(data, length);
return LoRa.endPacket() == 1;
}
bool receive(uint8_t* buffer, size_t& bytesRead, size_t maxLength) override {
int packetSize = LoRa.parsePacket();
bytesRead = 0;
if (packetSize == 0) return false;
while (LoRa.available() && bytesRead < maxLength) {
buffer[bytesRead++] = LoRa.read();
}
return true;
}
bool isConnected() const override { return _connected; }
const char* getName() const override { return "LoRa"; }
private:
long _frequency;
bool _connected = false;
};
} // namespace TractorOS
# 4.4 De Dependency Injection Container
We bouwen een simpele DI-container zonder frameworks. In Java/Spring zou dit @Autowired zijn; in Angular providedIn: 'root'. In C++ doen we dit expliciet in de "composition root" (je main.cpp of setup()):
// src/core/ServiceLocator.h
#pragma once
#include <memory>
#include "interfaces/ITransport.h"
namespace TractorOS {
/**
* Simpele service locator / DI container.
* In een groter project kun je dit vervangen door constructor injection
* in elke klasse afzonderlijk (zuiverder, beter testbaar).
*/
class ServiceLocator {
public:
static ServiceLocator& instance() {
static ServiceLocator inst; // Thread-safe singleton in C++11+
return inst;
}
void registerTransport(std::unique_ptr<ITransport> transport) {
_transport = std::move(transport); // std::move = eigenaarschap overdragen
}
ITransport* getTransport() const {
return _transport.get(); // .get() geeft raw pointer zonder eigenaarschap
}
private:
ServiceLocator() = default;
std::unique_ptr<ITransport> _transport;
};
} // namespace TractorOS
std::move uitgelegd:
In Java koppier je referenties vrijelijk. In C++ heeft een unique_ptr precies één eigenaar. std::move zegt letterlijk: "ik draag eigenaarschap over β na deze regel is mijn pointer leeg." Dit is de expliciete versie van wat de JVM's GC impliciet doet.
# 5. Strategy Pattern voor Transport
Het Strategy Pattern is in C++ nΓ³g krachtiger dan in Java, omdat we ook compile-time strategieΓ«n kunnen selecteren via templates.
# 5.1 Runtime Strategy (polymorfisme β equivalent aan Java)
// src/core/TelemetryDispatcher.h
#pragma once
#include <memory>
#include <functional>
#include "interfaces/ITransport.h"
namespace TractorOS {
struct TelemetryPacket {
uint32_t timestamp;
float engineTemp;
float fuelLevel;
float gpsLat;
float gpsLon;
uint16_t rpm;
uint8_t checksum;
};
/**
* Gebruikt transport als uitwisselbare strategie.
* In Java: class TelemetryDispatcher { private ITransport transport; ... }
* In TS: class TelemetryDispatcher { constructor(private transport: ITransport) {} }
*/
class TelemetryDispatcher {
public:
// Constructor injection β de zuiverste vorm van DI
explicit TelemetryDispatcher(ITransport& transport)
: _transport(transport) {}
bool dispatch(const TelemetryPacket& packet) {
if (!_transport.isConnected()) {
if (!_transport.connect()) {
Serial.printf("[%s] Verbinding mislukt\n", _transport.getName());
return false;
}
}
// Serialiseer struct naar bytes
const uint8_t* rawData = reinterpret_cast<const uint8_t*>(&packet);
// ^ reinterpret_cast: "behandel dit als byte-array"
// In Java: ByteBuffer.wrap(...) In TS: new Uint8Array(buffer)
return _transport.send(rawData, sizeof(TelemetryPacket));
}
// Strategy wisselen op runtime β typisch voor "fallback naar LoRa als WiFi wegvalt"
void setTransport(ITransport& newTransport) {
_transport = newTransport;
}
private:
ITransport& _transport; // Referentie, geen pointer β kan niet nullptr zijn!
// ^ Referentie als member = "dit object MOET een transport hebben"
// Dit is een bewuste keuze: we elimineren de nullptr-mogelijkheid by design.
};
} // namespace TractorOS
# 5.2 Transport Failover Strategy
// src/transport/FailoverTransport.h
#pragma once
#include "interfaces/ITransport.h"
#include <vector>
#include <memory>
namespace TractorOS {
/**
* Composite Strategy: probeert transports in volgorde van prioriteit.
* Design pattern: Chain of Responsibility + Strategy gecombineerd.
*/
class FailoverTransport : public ITransport {
public:
void addTransport(ITransport* transport, uint8_t priority) {
_transports.push_back({transport, priority});
// Sorteer op prioriteit
std::sort(_transports.begin(), _transports.end(),
[](const auto& a, const auto& b) { return a.priority < b.priority; });
// ^ Lambda β equivalent van Java's Comparator.comparingInt(...)
}
bool connect() override {
for (auto& entry : _transports) {
if (entry.transport->connect()) {
_active = entry.transport;
Serial.printf("Failover: actief transport = %s\n", _active->getName());
return true;
}
}
return false;
}
bool send(const uint8_t* data, size_t length) override {
if (!_active) return false;
if (_active->send(data, length)) return true;
// Automatische failover bij verzendingsfout
Serial.printf("%s mislukt, failover...\n", _active->getName());
for (auto& entry : _transports) {
if (entry.transport == _active) continue;
if (entry.transport->connect() && entry.transport->send(data, length)) {
_active = entry.transport;
return true;
}
}
return false;
}
// ... overige interface-methoden ...
void disconnect() override { if (_active) _active->disconnect(); }
bool receive(uint8_t* buf, size_t& read, size_t max) override {
return _active ? _active->receive(buf, read, max) : false;
}
bool isConnected() const override { return _active && _active->isConnected(); }
const char* getName() const override { return "Failover"; }
private:
struct TransportEntry {
ITransport* transport;
uint8_t priority;
};
std::vector<TransportEntry> _transports;
ITransport* _active = nullptr;
};
} // namespace TractorOS
# 6. Middleware Stack voor Telemetrie
Dit is het meest architectureel interessante deel. We bouwen een pipeline vergelijkbaar met Express.js middleware, ASP.NET middleware, of Java Servlet filters.
# 6.1 De Middleware Interface
// src/middleware/IMiddleware.h
#pragma once
#include <cstdint>
#include <functional>
namespace TractorOS {
/**
* Middleware context: de "request/response" van onze telemetrie-pipeline.
* Equivalent van Express.js's `req` object of Java's HttpServletRequest.
*/
struct TelemetryContext {
uint8_t* data;
size_t dataLength;
bool shouldContinue = true; // false = pipeline afbreken (zoals next(err) in Express)
bool hasError = false;
char errorMessage[64] = {};
// Metadata voor middleware
uint32_t timestamp;
uint8_t retryCount = 0;
};
/**
* Middleware interface β elke schakel in de pipeline.
*
* Java equivalent:
* interface Middleware { void process(Context ctx, Runnable next); }
*
* TS/Express equivalent:
* type Middleware = (ctx: Context, next: () => void) => void;
*/
class IMiddleware {
public:
virtual ~IMiddleware() = default;
/**
* @param context De telemetrie-context (mutable β middleware kan data aanpassen)
* @param next Aanroepen om door te gaan naar volgende middleware
* std::function is C++'s equivalent van Java's Function<T,R>
* of TS's () => void
*/
virtual void process(TelemetryContext& context,
std::function<void()> next) = 0;
virtual const char* getName() const = 0;
};
} // namespace TractorOS
# 6.2 Concrete Middleware Implementaties
// src/middleware/ChecksumMiddleware.h
#pragma once
#include "IMiddleware.h"
namespace TractorOS {
class ChecksumMiddleware : public IMiddleware {
public:
void process(TelemetryContext& ctx, std::function<void()> next) override {
// Pre-processing: checksum toevoegen
uint8_t checksum = 0;
for (size_t i = 0; i < ctx.dataLength; ++i) {
checksum ^= ctx.data[i]; // XOR checksum
}
ctx.data[ctx.dataLength++] = checksum; // Append checksum
next(); // Doorgaan naar volgende middleware
// Post-processing kan hier (na next()) β zoals Express.js "response" fase
}
const char* getName() const override { return "Checksum"; }
};
} // namespace TractorOS
// src/middleware/CompressionMiddleware.h
#pragma once
#include "IMiddleware.h"
namespace TractorOS {
/**
* Simpele run-length encoding compressie voor telemetrie-data.
* Op embedded systemen geen zlib/gzip β te zwaar.
*/
class CompressionMiddleware : public IMiddleware {
public:
explicit CompressionMiddleware(size_t threshold = 32)
: _threshold(threshold) {}
void process(TelemetryContext& ctx, std::function<void()> next) override {
if (ctx.dataLength < _threshold) {
// Kleine packets niet comprimeren β overhead niet waard
next();
return;
}
// In een echte implementatie: compress data hier
// We markeren compressed data met een header byte
_compressedBuffer[0] = 0xC0; // Compression marker
// ... compressie-logica ...
// Swap buffer β middleware modificeert transparant de context
uint8_t* originalData = ctx.data;
ctx.data = _compressedBuffer;
next(); // Verstuur gecomprimeerd
ctx.data = originalData; // Herstel na pipeline (indien nodig)
}
const char* getName() const override { return "Compression"; }
private:
size_t _threshold;
uint8_t _compressedBuffer[512]; // Pre-gealloceerd! Geen new[] in middleware
};
} // namespace TractorOS
// src/middleware/RateLimitMiddleware.h
#pragma once
#include "IMiddleware.h"
#include <Arduino.h>
namespace TractorOS {
/**
* Rate limiting middleware β beschermt het netwerk tegen overbelasting.
* Equivalent van een leaky bucket algorithm.
*/
class RateLimitMiddleware : public IMiddleware {
public:
explicit RateLimitMiddleware(uint32_t minIntervalMs = 100)
: _minIntervalMs(minIntervalMs)
, _lastSendTime(0) {}
void process(TelemetryContext& ctx, std::function<void()> next) override {
uint32_t now = millis();
uint32_t elapsed = now - _lastSendTime;
if (elapsed < _minIntervalMs) {
// Te vroeg β packet droppen of bufferen
snprintf(ctx.errorMessage, sizeof(ctx.errorMessage),
"Rate limit: wacht nog %ums", _minIntervalMs - elapsed);
ctx.shouldContinue = false;
return; // next() NIET aanroepen = pipeline stoppen
}
_lastSendTime = now;
next();
}
const char* getName() const override { return "RateLimit"; }
private:
uint32_t _minIntervalMs;
uint32_t _lastSendTime;
};
} // namespace TractorOS
# 6.3 De Pipeline Orchestrator
// src/core/MiddlewarePipeline.h
#pragma once
#include <vector>
#include <memory>
#include "middleware/IMiddleware.h"
namespace TractorOS {
/**
* Bouwt en executeert de middleware pipeline.
*
* Java equivalent: interceptor chain in Spring
* TS equivalent: Express app.use() + router
*
* Gebruik:
* pipeline.use(make_unique<ChecksumMiddleware>())
* .use(make_unique<CompressionMiddleware>())
* .use(make_unique<RateLimitMiddleware>(100));
* pipeline.execute(context);
*/
class MiddlewarePipeline {
public:
// Method chaining β builder pattern
// Retourtype is referentie naar *this, voor chaining: pipeline.use(...).use(...)
MiddlewarePipeline& use(std::unique_ptr<IMiddleware> middleware) {
_chain.push_back(std::move(middleware));
return *this;
}
void execute(TelemetryContext& ctx) {
_executeFrom(ctx, 0);
}
private:
/**
* Recursieve pipeline executie.
* Dit is het hart van het middleware pattern:
* elke middleware ontvangt een 'next' functie die de VOLGENDE middleware aanroept.
*/
void _executeFrom(TelemetryContext& ctx, size_t index) {
if (index >= _chain.size() || !ctx.shouldContinue) {
return; // Einde van de pipeline
}
// Maak een lambda die de volgende stap aanroept
// In Java: Runnable next = () -> executeFrom(ctx, index + 1);
auto next = [this, &ctx, index]() {
_executeFrom(ctx, index + 1);
};
_chain[index]->process(ctx, next);
}
std::vector<std::unique_ptr<IMiddleware>> _chain;
};
} // namespace TractorOS
# 7. Hardware Abstractie: de MVC-split
Dit is het meest embedded-specifieke onderdeel. We scheiden drie lagen:
Model: Sensordata structuren + domeinlogica
View: Display-output (LCD, Serial, LED-indicatoren)
Controller: Sensor polling, event handling, main loop
# 7.1 De Sensor Interface (Model laag)
// src/interfaces/ISensor.h
#pragma once
#include <cstdint>
namespace TractorOS {
/**
* Generieke sensordata container.
* Gebruik een tagged union (of std::variant in C++17) voor type-veiligheid.
*/
struct SensorReading {
enum class Type : uint8_t {
TEMPERATURE,
FUEL_LEVEL,
RPM,
GPS_POSITION,
INVALID
};
Type type = Type::INVALID;
uint32_t timestamp; // millis() op moment van meting
bool isValid = false;
union { // Union bespaart geheugen β slechts één waarde tegelijk
float floatValue;
int32_t intValue;
struct { float lat; float lon; } gps;
} value;
};
/**
* Sensorinterface β hardware-agnostisch.
* Testbaar door MockSensor, herbruikbaar voor alle sensortypen.
*/
class ISensor {
public:
virtual ~ISensor() = default;
virtual bool initialize() = 0;
virtual SensorReading read() = 0;
virtual bool isReady() const = 0;
virtual const char* getLabel() const = 0;
};
} // namespace TractorOS
# 7.2 Concrete Sensor Implementatie
// src/sensors/DS18B20Sensor.h
#pragma once
#include "interfaces/ISensor.h"
#include <OneWire.h>
#include <DallasTemperature.h>
namespace TractorOS {
/**
* DS18B20 temperatuursensor (populair voor motor-temp meting).
* De hardware-interactie is volledig ingekapseld in deze klasse.
* TelemetryDispatcher weet niets van OneWire of Dallas protocol.
*/
class DS18B20Sensor : public ISensor {
public:
explicit DS18B20Sensor(uint8_t dataPin)
: _oneWire(dataPin)
, _dallasSensors(&_oneWire) {}
bool initialize() override {
_dallasSensors.begin();
_deviceCount = _dallasSensors.getDeviceCount();
Serial.printf("DS18B20: %d sensor(s) gevonden\n", _deviceCount);
return _deviceCount > 0;
}
SensorReading read() override {
SensorReading reading;
reading.type = SensorReading::Type::TEMPERATURE;
reading.timestamp = millis();
_dallasSensors.requestTemperatures();
float temp = _dallasSensors.getTempCByIndex(0);
// Valideer de meting β hardware faalt soms
if (temp == DEVICE_DISCONNECTED_C || temp < -55.0f || temp > 125.0f) {
reading.isValid = false;
return reading;
}
reading.isValid = true;
reading.value.floatValue = temp;
return reading;
}
bool isReady() const override { return _deviceCount > 0; }
const char* getLabel() const override { return "Motortemperatuur"; }
private:
OneWire _oneWire;
DallasTemperature _dallasSensors;
uint8_t _deviceCount = 0;
};
} // namespace TractorOS
# 7.3 De Mock Sensor (voor testing zonder hardware)
// src/sensors/MockSensor.h
#pragma once
#include "interfaces/ISensor.h"
#include <functional>
namespace TractorOS {
/**
* Mock implementatie voor unit testing en development zonder hardware.
*
* Java equivalent: @Mock ISensor sensor; Mockito.when(sensor.read()).thenReturn(...)
* TS equivalent: jest.fn().mockReturnValue(...)
*
* In C++ maken we een expliciete mock met configureerbaar gedrag via een lambda.
*/
class MockSensor : public ISensor {
public:
using ReadingProvider = std::function<SensorReading()>;
MockSensor(const char* label, ReadingProvider provider)
: _label(label)
, _provider(provider) {}
// Factory method voor simpele statische waarden
static std::unique_ptr<MockSensor> withConstantValue(
const char* label, float value, SensorReading::Type type) {
return std::make_unique<MockSensor>(label, [value, type]() {
SensorReading r;
r.type = type;
r.isValid = true;
r.timestamp = millis();
r.value.floatValue = value;
return r;
});
}
bool initialize() override { return true; }
SensorReading read() override { return _provider(); }
bool isReady() const override { return true; }
const char* getLabel() const override { return _label; }
private:
const char* _label;
ReadingProvider _provider;
};
} // namespace TractorOS
# 7.4 De SensorManager (Controller laag)
// src/core/SensorManager.h
#pragma once
#include <vector>
#include <memory>
#include <functional>
#include "interfaces/ISensor.h"
namespace TractorOS {
/**
* CoΓΆrdineert alle sensoren.
* Observer pattern: consumers registreren callbacks voor nieuwe metingen.
*
* Java: EventBus / Spring ApplicationEventPublisher
* TS: EventEmitter / RxJS Subject
*/
class SensorManager {
public:
using ReadingCallback = std::function<void(const SensorReading&)>;
void registerSensor(std::unique_ptr<ISensor> sensor) {
if (sensor->initialize()) {
Serial.printf("Sensor geregistreerd: %s\n", sensor->getLabel());
_sensors.push_back(std::move(sensor));
}
}
void onReading(ReadingCallback callback) {
_callbacks.push_back(callback);
}
// Aanroepen in de main loop
void poll() {
for (auto& sensor : _sensors) {
if (!sensor->isReady()) continue;
SensorReading reading = sensor->read();
if (!reading.isValid) continue;
// Notificeer alle geregistreerde consumers
for (auto& cb : _callbacks) {
cb(reading);
}
}
}
private:
std::vector<std::unique_ptr<ISensor>> _sensors;
std::vector<ReadingCallback> _callbacks;
};
} // namespace TractorOS
# 8. Alles samenvoegen: de Boordcomputer Architectuur
// src/main.cpp β de "composition root"
// Dit is waar alle DI plaatsvindt. Vergelijk met:
// Java Spring: @SpringBootApplication main()
// Angular: app.module.ts providers[]
// NestJS: AppModule providers
#include <Arduino.h>
#include <memory>
#include "core/ServiceLocator.h"
#include "core/TelemetryDispatcher.h"
#include "core/MiddlewarePipeline.h"
#include "core/SensorManager.h"
#include "transport/WiFiTransport.h"
#include "transport/LoRaTransport.h"
#include "transport/FailoverTransport.h"
#include "middleware/ChecksumMiddleware.h"
#include "middleware/CompressionMiddleware.h"
#include "middleware/RateLimitMiddleware.h"
#include "sensors/DS18B20Sensor.h"
#include "sensors/MockSensor.h"
using namespace TractorOS;
// ===== CONFIGURATIE =====
constexpr const char* WIFI_SSID = "TractorNet";
constexpr const char* WIFI_PASS = "geheim123";
constexpr const char* SERVER_HOST = "192.168.1.100";
constexpr uint16_t SERVER_PORT = 8765;
constexpr uint32_t POLL_INTERVAL = 1000; // ms
// ===== LANGLEVENDE OBJECTEN (buiten loop() om heap-allocatie te vermijden) =====
// Statische allocatie in BSS segment β meest memory-efficiΓ«nt
static WiFiTransport wifiTransport(WIFI_SSID, WIFI_PASS, SERVER_HOST, SERVER_PORT);
static LoRaTransport loraTransport(868E6);
static FailoverTransport failoverTransport;
static MiddlewarePipeline pipeline;
static SensorManager sensorManager;
// TelemetryDispatcher heeft een referentie nodig β initialiseren na failoverTransport
static TelemetryDispatcher* dispatcher = nullptr;
void setup() {
Serial.begin(115200);
Serial.println("TractorOS booting...");
// === Transport configureren (Strategy + Failover) ===
failoverTransport.addTransport(&wifiTransport, 0); // Primair
failoverTransport.addTransport(&loraTransport, 1); // Fallback
// === Middleware pipeline configureren ===
pipeline
.use(std::make_unique<RateLimitMiddleware>(100)) // Max 10 packets/sec
.use(std::make_unique<ChecksumMiddleware>()) // Integriteitscheck
.use(std::make_unique<CompressionMiddleware>(64)); // Comprimeer >64 bytes
// === Sensoren registreren ===
#ifdef HARDWARE_BUILD
sensorManager.registerSensor(
std::make_unique<DS18B20Sensor>(GPIO_NUM_4)
);
#else
// Development zonder hardware: mock sensoren
sensorManager.registerSensor(
MockSensor::withConstantValue("Motortemp", 87.5f,
SensorReading::Type::TEMPERATURE)
);
#endif
// === Observer: sensor readings β telemetrie dispatcher ===
dispatcher = new TelemetryDispatcher(failoverTransport);
sensorManager.onReading([](const SensorReading& reading) {
// Bouw een telemetrie packet
TelemetryPacket packet{};
packet.timestamp = reading.timestamp;
if (reading.type == SensorReading::Type::TEMPERATURE) {
packet.engineTemp = reading.value.floatValue;
}
// Stuur door de middleware pipeline
uint8_t buffer[sizeof(TelemetryPacket)];
memcpy(buffer, &packet, sizeof(packet));
TelemetryContext ctx{};
ctx.data = buffer;
ctx.dataLength = sizeof(packet);
ctx.timestamp = packet.timestamp;
pipeline.execute(ctx);
if (ctx.shouldContinue) {
dispatcher->dispatch(packet);
}
});
Serial.println("TractorOS gereed.");
}
void loop() {
static uint32_t lastPoll = 0;
uint32_t now = millis();
if (now - lastPoll >= POLL_INTERVAL) {
lastPoll = now;
sensorManager.poll();
}
// Andere taken (display update, knop-input, etc.) kunnen hier
delay(10);
}
# Architecturale samenvatting
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β main.cpp β
β (Composition Root / DI) β
ββββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬βββββββββββββ
β β β
ββββββββΌβββββββ βββββββΌβββββββ ββββββΌβββββββββββ
βSensorManagerβ βMiddleware β βTelemetry β
β(Controller) β βPipeline β βDispatcher β
ββββββββ¬βββββββ βββββββ¬βββββββ ββββββ¬βββββββββββ
β β β
ββββββββΌβββββββ βββββββΌβββββββ ββββββΌβββββββββββ
β ISensor β βIMiddleware β β ITransport β
β (Model) β β(Pipeline) β β (Strategy) β
ββββββββ¬βββββββ βββββββ¬βββββββ ββββββ¬βββββββββββ
β β β
DS18B20 ChecksumβCompress WiFiβLoRaβ4G
MockSensor RateLimit FailoverTransport
# 9. PlatformIO Project Setup
# platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
; C++17 voor structured bindings, std::optional, if constexpr
build_flags =
-std=gnu++17
-DCORE_DEBUG_LEVEL=3
-DCONFIG_ARDUHAL_LOG_COLORS=1
; Activeer hardware build voor echte sensoren:
; -DHARDWARE_BUILD
lib_deps =
milesburton/DallasTemperature @ ^3.11.0
paulstoffregen/OneWire @ ^2.3.7
sandeepmistry/LoRa @ ^0.8.0
; Testenvironment β draait native op je development machine
[env:native_test]
platform = native
build_flags = -std=gnu++17 -DNATIVE_TEST
# Projectstructuur
tractor-computer/
βββ platformio.ini
βββ src/
β βββ main.cpp
β βββ interfaces/
β β βββ ITransport.h
β β βββ ISensor.h
β β βββ IMiddleware.h
β βββ transport/
β β βββ WiFiTransport.h
β β βββ LoRaTransport.h
β β βββ FailoverTransport.h
β βββ middleware/
β β βββ ChecksumMiddleware.h
β β βββ CompressionMiddleware.h
β β βββ RateLimitMiddleware.h
β βββ sensors/
β β βββ DS18B20Sensor.h
β β βββ MockSensor.h
β βββ core/
β βββ SensorManager.h
β βββ TelemetryDispatcher.h
β βββ MiddlewarePipeline.h
β βββ ServiceLocator.h
βββ test/
βββ test_pipeline/
β βββ test_main.cpp
βββ test_transport/
βββ test_main.cpp
# 10. Testing zonder OS
Op embedded systemen heb je geen JUnit of Jest. PlatformIO biedt wel Unity (C test framework), en met de native omgeving kun je pure logica testen zonder ESP32.
# Unit test met PlatformIO Unity
// test/test_pipeline/test_main.cpp
#include <unity.h>
#include "../../src/core/MiddlewarePipeline.h"
#include "../../src/middleware/ChecksumMiddleware.h"
#include "../../src/middleware/RateLimitMiddleware.h"
using namespace TractorOS;
void test_checksum_appended() {
MiddlewarePipeline pipeline;
pipeline.use(std::make_unique<ChecksumMiddleware>());
uint8_t data[] = {0x01, 0x02, 0x03, 0x04};
TelemetryContext ctx{};
ctx.data = data;
ctx.dataLength = 4;
pipeline.execute(ctx);
// XOR van 01^02^03^04 = 0x04
TEST_ASSERT_EQUAL(5, ctx.dataLength);
TEST_ASSERT_EQUAL(0x04, data[4]);
}
void test_rate_limit_blocks_rapid_sends() {
MiddlewarePipeline pipeline;
pipeline.use(std::make_unique<RateLimitMiddleware>(1000)); // 1 per seconde
uint8_t data[4] = {};
TelemetryContext ctx1{}, ctx2{};
ctx1.data = ctx2.data = data;
ctx1.dataLength = ctx2.dataLength = 4;
pipeline.execute(ctx1);
pipeline.execute(ctx2); // Direct daarna β moet geblokkeerd worden
TEST_ASSERT_TRUE(ctx1.shouldContinue);
TEST_ASSERT_FALSE(ctx2.shouldContinue);
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_checksum_appended);
RUN_TEST(test_rate_limit_blocks_rapid_sends);
return UNITY_END();
}
# Mock-gebaseerde integratietest
void test_dispatcher_uses_mock_transport() {
// Arrange
bool sendCalled = false;
// Lambda-based mock transport β geen volledige klasse nodig voor simpele tests
class InlineTransportMock : public ITransport {
public:
bool& _called;
InlineTransportMock(bool& called) : _called(called) {}
bool connect() override { return true; }
void disconnect() override {}
bool send(const uint8_t*, size_t) override { _called = true; return true; }
bool receive(uint8_t*, size_t&, size_t) override { return false; }
bool isConnected() const override { return true; }
const char* getName() const override { return "Mock"; }
} mockTransport(sendCalled);
TelemetryDispatcher dispatcher(mockTransport);
TelemetryPacket packet{};
// Act
dispatcher.dispatch(packet);
// Assert
TEST_ASSERT_TRUE(sendCalled);
}
# Bijlage: Veelgestelde Syntax Vragen
# Waarom -> soms en . soms?
MySensor sensor; // Object op stack
sensor.read(); // Punt: direct object toegang
MySensor* ptr = &sensor; // Pointer
ptr->read(); // Pijl: pointer dereference + member access
// ptr->read() is exact hetzelfde als (*ptr).read()
# Wat is uint8_t, size_t etc.?
// Java: byte, int, long (altijd gesigned, altijd dezelfde grootte)
// C++: int kan 16, 32 of 64 bit zijn afhankelijk van platform!
#include <cstdint> // Gegarandeerde groottes:
uint8_t byte_val; // 0-255 (= Java's byte maar unsigned)
uint16_t word_val; // 0-65535
uint32_t dword_val; // 0-4294967295
int32_t signed_val; // -2^31 tot 2^31-1
size_t size; // Platform-native unsigned integer voor maten/indices
// = Java's int voor arrays, maar unsigned
# Waarom #pragma once en niet #ifndef?
// Oude stijl (nog veel gebruikt):
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... inhoud ...
#endif
// Moderne stijl (ondersteund door alle C++ compilers voor embedded):
#pragma once
// ... inhoud ...
// Simpeler, sneller, en voorkomt typo-bugs in de guard naam
# Wat is constexpr?
// Runtime constante (Java final): waarde bepaald bij uitvoering
const int bufferSize = getConfiguredSize();
// Compile-time constante (geen Java equivalent): waarde bepaald bij compilatie
constexpr int MAX_SENSORS = 8;
constexpr int BUFFER_SIZE = MAX_SENSORS * sizeof(SensorReading);
// De array-grootte is nu bekend bij compilatie β essentieel voor stack-allocatie
uint8_t buffer[BUFFER_SIZE]; // Dit werkt alleen met constexpr, niet met const
# override vs Java's @Override
class WiFiTransport : public ITransport {
bool connect() override { ... }
// ^^^^^^^^
// override vertelt de compiler: "dit MOET een bestaande virtual methode overschrijven"
// Zonder override: compileert ook, maar geeft geen fout bij een typefout in de naam!
// Met override: compileert NIET als er geen virtual connect() in de base class is.
// Altijd gebruiken β equivalent van Java's @Override.
};
Cursus einde. Volgende stappen: implementeer de GPS-sensor, voeg een LCD display toe als View-laag, en experimenteer met FreeRTOS tasks voor concurrent sensor polling.
Reacties (0 )
Geen reacties beschikbaar.
Log in om een reactie te plaatsen.