Event-Driven Observer Pattern Components of the Flowduino ESPressio Development Platform.
ESPressio Event provides asynchronous typed Event routing, Event-aware Threads, bounded receiver queues, listener/observer registration, priority dispatch, and high-resolution Event timing for ESP32 applications.
Version 5.1.0 extends the 5.x architecture with opt-in System Clock Observer-to-Event bridging, aligned with:
ESPressio Threads >= 3.0.0
ESPressio Observable >= 3.0.0
ESPressio Timing >= 2.2.0
Timing 2.2 is now also declared directly because the optional System Clock Event bridge compiles against its public Observer API. ESPressio Units remains available transitively through the dependency stack.
Serializable Event support remains optional: ordinary Event and Timing Event bridge users do not acquire an ESPressio Serializable dependency.
Version 5.1.0 adds an explicit bridge from ESPressio Timing 2.2 System Clock Observer notifications into asynchronous ESPressio Events.
The ordinary bridge is opt-in:
#include <ESPressio_SystemClockEventBridge.hpp>
Event::SystemClockEventBridge::GetInstance().Initialize();Timing Events are grouped under src/timing-events/ and batch-imported by:
#include <ESPressio_TimingEvents.hpp>Every ISystemClockObserver callback has a corresponding strongly typed Event carrying the callback snapshot, including synchronization before/after values, immediate clock difference, synchronization result/status, state changes, configuration changes, and callback lifecycle information.
Serializable counterparts are also provided without making ESPressio Serializable a mandatory dependency:
#include <ESPressio_TimingEvents_Serializable.hpp>
#include <ESPressio_SystemClockEventBridge_Serializable.hpp>
Event::SerializableSystemClockEventBridge::GetInstance().Initialize();Serializable timing Events flatten Timing synchronization/configuration structures into stable primitive payload fields. std::exception_ptr is deliberately converted to a portable exception-message string in the Serializable callback-failure Event.
Both bridges are dormant until Initialize() is explicitly called, and Shutdown() releases the retained Observable registration handle.
This patch release resolves several defects discovered after the initial 5.0.0 release:
- fixes
ESPressio_EventListener.hppcompilation by explicitly including the Observable exception declaration and its direct standard-library dependencies; - neutralizes the obsolete
ESPressio_EventListener.cpptemplate definitions which no longer matched the header API; - removes the obsolete Arduino
String GetThreadNamePrefix()API fromEventThread; - removes public-header namespace pollution from the corrected Event Thread and enum headers;
- fixes
EventListenerInterestincrement/decrement wrap-around to use its actual three enum values; - hardens Event reference counting against unmatched
__unref()underflow; - prevents EventDispatcher from creating empty receiver buckets for unhandled Event types;
- removes raw heap allocation for dispatcher receiver buckets;
- avoids invoking receiver code while holding the EventDispatcher receiver-map mutex;
- removes empty receiver buckets when the last receiver unregisters.
The Event 5.x generic Timing/Threads architecture and optional Serializable Event design remain unchanged.
ESPressio Event targets the ESP32 family using Arduino-ESP32.
The implementation uses ESP-IDF FreeRTOS facilities, C++ RTTI, std::shared_mutex, and Arduino APIs through its dependency stack.
RTTI must be enabled:
build_unflags =
-fno-rttiNormal Event applications require only the dependencies declared by the library:
lib_deps =
flowduino/ESPressio-Event@^5.1.0The mandatory dependency graph is:
ESPressio Event 5.x
|
+-- ESPressio Threads >= 3.0.0
| |
| +-- ESPressio Timing 2.x
| +-- ESPressio Units
|
+-- ESPressio Observable >= 3.0.0
ESPressio Serializable is not a mandatory Event dependency.
Only applications that explicitly include the optional Serializable Event header need to add ESPressio Serializable.
The base Event is now:
template<
typename TTime = Timing::DefaultClockTime
>
class Event;Ordinary Event code therefore uses:
class MyEvent :
public ESPressio::Event::Event<> {
};A different Timing-compatible public representation can be selected:
using MyTime = SomeCompatibleTimeType;
class MyEvent :
public ESPressio::Event::Event<MyTime> {
};IEvent remains non-templated.
Routing, receivers, the Event Manager, reference ownership, and listeners continue to operate using:
IEvent*The Event engine exposes raw lifecycle timing through:
uint64_t GetDispatchTimeNanoseconds() const;
uint64_t GetTimeSinceDispatchNanoseconds() const;while Event<TTime> exposes the strongly typed API:
TTime GetDispatchTime() const;
TTime GetTimeSinceDispatch() const;This keeps the routing infrastructure independent of the selected public Unit representation.
Event lifecycle timing is stored internally as raw nanoseconds:
dispatch state
|
+-- was dispatched
+-- dispatch time nanoseconds
The selected TTime representation is created only at the public API boundary through:
Timing::TimeTraits<TTime>This means choosing a Serializable or another richer Unit representation does not add representation-specific state to Event routing.
The Event System Clock uses:
Timing::SystemClock<TTime>::GetInstance()Timing 2.x typed System Clock facades share one underlying global System Clock core.
EventListenerInterest::YoungerThan is now representation-independent.
The public threshold remains the default EventTime Unit, but the listener converts it through:
Timing::TimeTraits<EventTime>and compares it directly with the Event's type-erased raw nanosecond age.
This removes the former dependency on Timing 1.x ClockBase conversion helpers.
PrecisionEventThread now mirrors the generic ESPressio Threads 3.x Precision Thread model:
template<
typename TTime = Timing::DefaultClockTime,
typename TRepresentationTraits =
Threads::PrecisionThreadTraits<TTime>
>
class PrecisionEventThread;Ordinary usage:
class ControlThread :
public ESPressio::Event::PrecisionEventThread<> {
};A different time representation can be selected without duplicating the Event-processing or precision-scheduling implementation.
The injected clock type is:
Timing::ISystemClock<
PrecisionEventThread::IterationTime
>*and the default clock uses the shared Timing 2.x System Clock.
The existing policies remain:
PrecisionEventProcessOrder::EventsBeforeIteration
PrecisionEventProcessOrder::EventsAfterIterationand:
PrecisionEventArrivalPolicy::ProcessOnNextIteration
PrecisionEventArrivalPolicy::TriggerImmediateIteration
PrecisionEventArrivalPolicy::ProcessImmediatelySerializable Event support lives in:
#include <ESPressio_Event_Serializable.hpp>or the equivalent convenience umbrella:
#include <ESPressio_SerializableEvent.hpp>Neither header is imported by normal ESPressio_Event.hpp.
A consuming application which uses Serializable Events declares:
lib_deps =
flowduino/ESPressio-Event@^5.1.0
flowduino/ESPressio-Serializable@^0.9.0The optional base is:
template<
typename TDerived,
typename TTime =
Units::SerializableNanoSeconds<uint64_t>
>
class SerializableEvent;Example:
#include <ESPressio_Event_Serializable.hpp>
class TemperatureEvent final :
public ESPressio::Event::SerializableEvent<
TemperatureEvent
> {
private:
float _temperature = 0.0f;
uint32_t _sensorId = 0;
public:
ESPRESSIO_SERIALIZABLE_TYPE(
TemperatureEvent
)
ESPRESSIO_SERIALIZABLE_SCHEMA_VERSION(
1
)
ESPRESSIO_SERIALIZABLE_PROPERTIES(
ESPRESSIO_PROPERTY(
"temperature",
_temperature
),
ESPRESSIO_PROPERTY(
"sensorId",
_sensorId
)
)
};The inherited Event TimeType is itself a Serializable ESPressio Unit.
The derived Event payload can therefore use the complete ESPressio Serializable feature set, including JSON, CBOR, Binary, schema versions, aliases, validation, migrations, streaming, and schema introspection.
The Event engine deliberately does not serialize local runtime lifecycle state.
The following are not Event payload:
reference count
local dispatch status
local System Clock dispatch timestamp
routing/listener state
Serializing these values would produce incorrect semantics when an Event is transmitted to another ESP32 or restored after restart.
A deserialized Serializable Event is therefore a new local Event which can subsequently be queued or stacked normally.
Transport metadata such as origin device identity, correlation IDs, sequence numbers, or source timestamps should be represented explicitly in the Event payload or in a separate transport envelope.
This code:
#include <ESPressio_Event.hpp>
class ButtonEvent :
public ESPressio::Event::Event<> {
};does not include, compile, link, or require ESPressio Serializable.
The normal Event library metadata deliberately contains no Serializable dependency.
This preserves the pay-for-what-you-use design:
ordinary Event
-> no Serializable dependency
SerializableEvent
-> consuming application opts into Serializable
4.x:
class MyEvent :
public Event::Event {
};5.x:
class MyEvent :
public Event::Event<> {
};4.x:
class MyThread :
public Event::PrecisionEventThread {
};5.x:
class MyThread :
public Event::PrecisionEventThread<> {
};Timing 1.x's global:
Timing::ClockTimeis no longer used.
The default Event representation is:
Timing::DefaultClockTimeand generic Event code should prefer:
typename MyEvent::TimeTypeIEvent no longer exposes a fixed typed return value for dispatch time.
Type-erased infrastructure should use:
GetDispatchTimeNanoseconds()
GetTimeSinceDispatchNanoseconds()Concrete Event consumers use:
event->GetDispatchTime()
event->GetTimeSinceDispatch()on their typed Event<TTime> descendant.
class SetpointEvent final :
public Event::Event<> {
// payload
};
class ControlThread final :
public Event::PrecisionEventThread<> {
protected:
void OnIteration(
IterationTime delta,
IterationTime startTime,
Threads::SkippedIterationCount skipped
) override {
// ...
}
};See:
examples/PrecisionEventThread
See:
examples/SerializableEvent
The example defines a Serializable Event payload and serializes it through the normal ESPressio Serializable archive API.
Event 5.0 establishes these boundaries:
IEvent
type-erased Event core
|
v
Event<TTime>
|
+----------+----------+
| |
v v
DefaultClockTime SerializableNanoSeconds
|
v
SerializableEvent<TDerived>
and:
Threads::PrecisionThread<TTime, Traits>
|
v
PrecisionEventThread<TTime, Traits>
The architecture provides one Event engine, one routing system, and one PrecisionEventThread implementation while allowing the public Unit representation to vary at compile time.
Most importantly, optional serialization remains genuinely optional.