Event-Driven Programming on Arduino: Enterprise Patterns for Embedded
Arduino is traditionally associated with linear sketches and the cyclic loop(). But as projects grow more complex, this approach leads to spaghetti code. We're introducing a method to bring event-driven architecture from enterprise applications into embedded development using the EVA Core library.
The Problem of Structure in Microcontrollers
In desktop and server applications, code is organized around event handlers: clicks, timers, network packets. Arduino, however, forces everything into a single loop(), where you have to manually track states using millis() and flags. This creates two critical limitations:
- Inability to isolate component logic — timings get propagated through the entire object hierarchy
- Lack of clear contracts between modules regarding timing requirements
Standard Arduino libraries address this only piecemeal: there are button handlers, separate timers, but no unified event system. Attempts to build an "all-in-one" solution run into tight coupling to a specific loop() implementation.
EVA Core: Three Components in a Unified System
The EVA Core library (Event-Driven Architecture) solves the problem through three interacting layers:
- Callback mechanism for class methods — allows subscribing to events without global functions
- Tickable interface — gives objects access to the time loop via the tick() method
- Survival Kit — a set of ready-made components (buttons, timers) built on the first two principles
Key innovation — separating timing logic from business rules. The sensor manages its own timings, while the application reacts only to events. This enables:
- Isolating filtering and debouncing algorithms in components
- Guaranteeing update rhythm for nested objects
- Eliminating the need to propagate calls through the hierarchy
Let's look at the implementation using a temperature sensor example:
#include <evaTickable.h>
#include <evaHandler.h>
using namespace eva;
class TempSensor : public Tickable {
private:
IHandler* listener = nullptr;
unsigned long lastRead = 0;
public:
void subscribe(IHandler* handler) {
listener = handler;
}
private:
void tick() override {
if (millis() - lastRead > 1000) { // Interval 1 sek
int value = readTemperature();
if (listener && value != lastValue) {
CallbackInfo info;
info.eventType = TEMP_UPDATE;
info.eventArg = value;
listener->invoke(this, info);
}
lastRead = millis();
}
}
};
class ClimateControl : public IHandler {
private:
TempSensor sensor;
void onTempUpdate(int value) {
if (value > 25) activateCooling();
}
public:
ClimateControl() {
sensor.subscribe(this);
}
void invoke(void* sender, CallbackInfo info) override {
if (info.eventType == TEMP_UPDATE) {
onTempUpdate(info.eventArg);
}
}
};
void setup() {
static ClimateControl system;
}
void loop() {
eva::tac(); // Edinaya point update
}
Advantages of the approach:
- TempSensor encapsulates polling and filtering logic
- ClimateControl only knows about events, not timing details
- Adding new sensors doesn't require changes to loop()
- Testing business logic in isolation from timing
LEGO Architecture for Peripherals
Survival Kit implements the composition principle through compile-time templates. For example, multi-button handling on an analog input is built as a chain of transformers:
template <int PIN, int PIN_MODE, signed short... LEVELS>
using PinMultiButton = Button<QuantizeDecor<DebounceDecor<AnalogPinReader<PIN, PIN_MODE>>, LEVELS...>>;
Signal processing architecture:
- Analog signal →
- Debounce →
- Quantization by levels →
- Button logic →
- Events
This pattern lets you assemble custom handlers from ready-made components. Example — a keypad from discrete buttons:
#include <evaSwitch.h>
class MyKeypad {
public:
MyKeypad() {
pinMode(2, INPUT_PULLUP);
// ... initialization ostalnykh pinov
}
signed short getValue() {
if (digitalRead(2) == LOW) return 'u';
// ... processing drugikh knopok
return 0;
}
};
// Withborka chains processing
Switch<DebounceDecor<MyKeypad>> keypad;
// Podpiska on events
keypad.setListener(new Handler<App>(this, &App::onKeyPress), ON_PRESS);
Each layer handles its own responsibility. If needed, you can swap debounce for a more sophisticated algorithm or add press duration filtering — without altering the core application logic.
Limitations and Use Cases
EVA Core is positioned as a methodological tool, not a universal solution. Use it when:
- Building projects with complex object hierarchies
- Needing clear separation of timing and business logic
- Teaching event-driven architecture principles on simple devices
Critical limitations:
- Increased RAM usage due to virtual tables
- Complexity for beginners (requires knowledge of templates and method pointers)
- Overkill for simple projects (e.g., controlling a single LED)
Important to understand: the library's goal is demonstrating architectural patterns, not resource optimization. For production projects with tight constraints, you'll need to analyze the overhead.
Key Takeaways
- Event model isolates timing dependencies in components
- Tickable interface ensures regular updates for nested objects
- Template composition offers flexibility without runtime overhead
- No more propagating calls simplifies refactoring and testing
- Arduino becomes a playground for enterprise patterns
This approach proves that even on resource-constrained microcontrollers, you can apply professional design methods. The key benefit — code becomes predictable and scalable, rather than just "getting the job done".
— Editorial Team
No comments yet.