ADF Event Hub
Introduction
ADF Event Hub is a domain-based publish-subscribe mechanism for ADF and GMF components. Each handle corresponds to one event source domain, such as Wi-Fi or OTA. Publishers deliver events into that domain. Subscribers filter by domain and event identifier, then receive events through a queue or a callback. Each service instance in ESP Service automatically creates and binds an event hub to publish domain events for itself and derived services.
Feature List
Domain-based publish-subscribe: one hub handle represents one event source domain; subscribers filter by
(event_domain, event_id), andADF_EVENT_ANY_IDacts as a wildcard that matches all eventsTwo delivery modes: queue mode (non-blocking
xQueueSend) and callback mode (executed synchronously in the publisher’s task), selectable independently per subscriberStartup-order independence: subscribers can complete their subscription before a publisher in the target domain calls
create(); the target domain is created automaticallyReference-counted hub lifecycle:
adf_event_hub_create()/adf_event_hub_destroy()are used in pairs, and a domain is only actually removed once its reference count reaches zeroReference-counted envelope delivery: the optional
release_cbis invoked exactly once per successful publish, regardless of whether the delivery path hit any queue subscribersThread-safe: all public APIs are protected by an internal mutex and can be called from any task (ISR context is not supported)
Observable:
adf_event_hub_get_stats()returns the subscriber count and envelope pool usage for each domain, andadf_event_hub_dump()outputs a full status log
Technical Deep Dive
Publish-Subscribe Model
Each domain is identified by a case-sensitive string (such as "wifi"). Publishers and subscribers each hold their own hub handle: a publisher’s handle identifies the domain it belongs to, while a subscriber’s handle merely identifies the caller itself; the domain to listen on is specified through the event_domain field of adf_event_hub_subscribe(), and defaults to the domain of the subscriber’s own hub when set to NULL.
adf_event_hub_t wifi_hub = NULL;
adf_event_hub_t app_hub = NULL;
adf_event_hub_create("wifi", &wifi_hub);
adf_event_hub_create("app", &app_hub);
adf_event_subscribe_info_t info = ADF_EVENT_SUBSCRIBE_INFO_DEFAULT();
info.event_domain = "wifi";
info.event_id = 1;
info.handler = on_wifi_event;
adf_event_hub_subscribe(app_hub, &info);
adf_event_t ev = { .domain = "wifi", .event_id = 1 };
adf_event_hub_publish(wifi_hub, &ev, NULL, NULL);
Delivery Modes and Reference Release
Queue mode is used when target_queue in adf_event_subscribe_info_t is non-null; otherwise the handler callback mode is used. If both are set, queue mode takes precedence. In queue mode, the publish call only performs a non-blocking xQueueSend(), so a full queue on one subscriber only affects that single delivery to it; in callback mode, handler runs synchronously in the publisher’s task and must not block noticeably.
sequenceDiagram
participant Pub as Publisher
participant Hub as event hub
participant Sub as Queue Subscriber
Pub->>Hub: adf_event_hub_publish()
Hub-)Sub: xQueueSend(delivery)
Sub->>Hub: adf_event_hub_delivery_done()
Hub--)Pub: release_cb(payload)
Event payloads are delivered by shallow copy, so heap-allocated payloads must be reclaimed via release_cb: when no queue-mode subscriber is hit, release_cb is invoked before adf_event_hub_publish() returns; when a queue-mode subscriber is hit, invocation is deferred until after the last call to adf_event_hub_delivery_done(), and it is triggered exactly once.
static void release_payload(const void *payload, void *ctx)
{
free((void *)payload); /* Invoked exactly once, after the last delivery_done call */
}
char *msg = strdup("hello");
adf_event_t ev = { .domain = "wifi", .event_id = 7, .payload = msg, .payload_len = strlen(msg) + 1 };
adf_event_hub_publish(wifi_hub, &ev, release_payload, NULL);
Note
After a queue-mode subscriber receives an adf_event_delivery_t, it must call adf_event_hub_delivery_done() exactly once for each delivery, otherwise the envelope pool slot will be held and never released.
Lifecycle and Reference Counting
adf_event_hub_create() manages domains by reference count: calling create() multiple times for the same domain only increments the reference count, and the domain is actually removed and its subscribers cleaned up only once the count reaches zero (that is, after adf_event_hub_destroy() has been called the same number of times). This allows a shared hub to be held jointly by multiple owners, so that any single owner destroying its own reference does not affect the others. esp_service_init() in ESP Service already encapsulates this process, so application code typically does not need to call the event hub’s create/destroy interfaces directly.
Application Examples
components/adf_event_hub/examples/demonstrates several services interacting through the ADF Event Hub; the same code can be built and run both on a PC host (FreeRTOS POSIX simulator) and on ESP-IDF targets.
FAQ
Q1: Can queue mode and callback mode be used on the same subscription at the same time?
No. When target_queue and handler are both set, queue mode takes precedence and handler is ignored; if both delivery methods are needed, register two separate subscriptions.
API Reference
Header File
Functions
-
esp_err_t adf_event_hub_create(const char *domain, adf_event_hub_t *out_hub)
Register a domain and return its hub handle.
Use this balanced create/destroy pair as the idiomatic way to “retain” a handle that will outlive the immediate creator (e.g. when a shared hub is handed to a service that will destroy it on deinit).
The module copies the domain string; the caller need not keep it alive after this call.
Note
Reference-counted. Each successful call increments the internal reference count of the domain — whether the domain was newly created or already existed. Every call to adf_event_hub_create() MUST be balanced by exactly one call to adf_event_hub_destroy(); the domain is physically removed only when the count reaches zero.
- Parameters:
domain – [in] Non-NULL, non-empty domain name (e.g. “wifi”). Matching is case-sensitive (see file-level Conventions).
out_hub – [out] Receives the hub handle on success. Store the result as
adf_event_hub_t huband pass hub (not &hub) to all subsequent APIs.
- Returns:
ESP_OK Domain registered (or retained); *out_hub is valid.
ESP_ERR_INVALID_ARG domain or out_hub is NULL, or domain is empty.
ESP_ERR_NO_MEM Heap or internal vector allocation failed.
-
esp_err_t adf_event_hub_destroy(adf_event_hub_t hub)
Release one reference to a domain hub.
In-flight deliveries already queued in subscriber inboxes are not invalidated when the domain is finally removed; drain subscriber queues before the last destroy call. The caller must set hub to NULL after the call returns.
Note
Reference-counted counterpart to adf_event_hub_create(). Each call decrements the domain’s reference count. The domain is physically unregistered and all its subscribers are removed only when the count reaches zero.
- Parameters:
hub – [in] Hub handle returned by adf_event_hub_create().
- Returns:
ESP_OK Reference released. Domain removed if refcount reached zero; otherwise removal is deferred.
ESP_ERR_INVALID_ARG hub is NULL.
ESP_ERR_INVALID_STATE Module not initialised.
ESP_ERR_NOT_FOUND hub does not match any registered domain.
-
esp_err_t adf_event_hub_subscribe(adf_event_hub_t hub, const adf_event_subscribe_info_t *info)
Register a subscriber on a domain.
Note
At least one delivery target (target_queue or handler) must be set. If info->event_domain names a domain not yet created, that domain is auto-created (see file-level “Startup ordering”). event_domain matching is case-sensitive (see file-level Conventions).
- Parameters:
hub – [in] Hub handle of the calling service’s context.
info – [in] Subscription parameters; event_domain NULL = hub’s own domain.
- Returns:
ESP_OK Subscriber registered.
ESP_ERR_INVALID_ARG hub or info is NULL; both target_queue and handler are NULL.
ESP_ERR_INVALID_STATE Module not initialised.
ESP_ERR_NOT_FOUND event_domain is NULL and hub’s own domain is not registered (should not happen in normal use).
ESP_ERR_NO_MEM Domain auto-create or subscriber vector grow failed.
-
esp_err_t adf_event_hub_unsubscribe(adf_event_hub_t hub, const char *domain, uint16_t event_id)
Remove subscriptions from a domain.
Note
Removes all subscriptions whose event_id matches the filter. The current implementation does not track subscriber ownership, so this call is not limited to subscriptions created by the calling hub.
- Parameters:
hub – [in] Hub handle of the calling service’s context.
domain – [in] Domain to unsubscribe from; NULL = hub’s own domain. Case-sensitive; must match the spelling used at subscribe/create.
event_id – [in] Event ID filter; ADF_EVENT_ANY_ID removes all.
- Returns:
ESP_OK At least one subscriber removed.
ESP_ERR_INVALID_ARG hub is NULL.
ESP_ERR_INVALID_STATE Module not initialised.
ESP_ERR_NOT_FOUND Domain not registered, or no matching subscriber.
-
esp_err_t adf_event_hub_publish(adf_event_hub_t hub, const adf_event_t *event, adf_event_payload_release_cb_t release_cb, void *release_ctx)
Deliver an event to all matching subscribers.
Note
Queue-mode delivery is non-blocking and best-effort; a full queue causes that subscriber’s delivery to be dropped. release_cb semantics: see adf_event_payload_release_cb_t.
- Parameters:
hub – [in] Hub handle; must be a registered domain.
event – [in] Event descriptor (shallow-copied; see adf_event_t).
release_cb – [in] Payload release callback; may be NULL.
release_ctx – [in] Opaque context forwarded to release_cb.
- Returns:
ESP_OK Event published.
ESP_ERR_INVALID_ARG hub or event is NULL. Caller retains ownership of event->payload; release_cb is NOT invoked. All other return codes transfer payload ownership to the hub and guarantee release_cb is invoked exactly once.
ESP_ERR_INVALID_STATE Module not initialised.
ESP_ERR_NOT_FOUND hub is not a registered domain.
ESP_ERR_NO_MEM Internal envelope storage could not grow.
-
esp_err_t adf_event_hub_delivery_done(adf_event_hub_t hub, adf_event_delivery_t *delivery)
Release a queue-mode delivery reference.
Note
Must be called exactly once per received adf_event_delivery_t. Skipping leaks the envelope slot; a redundant second call is safe and ignored. Not ISR-safe.
- Parameters:
hub – [in] Any valid hub handle (used to verify module is active).
delivery – [in] Delivery item from the subscriber queue; delivery->_opaque must not be modified by the caller.
- Returns:
ESP_OK Reference released; payload may have been freed.
ESP_ERR_INVALID_ARG hub or delivery is NULL; _opaque index out of range.
ESP_ERR_INVALID_STATE Module not initialised.
-
void adf_event_hub_dump(void)
Dump all internal state to the log (ESP_LOGI).
Note
Intended for debugging only; not for production hot-paths.
-
esp_err_t adf_event_hub_get_stats(adf_event_hub_stats_t *stats)
Populate a statistics snapshot.
Note
If stats->domains is non-NULL and stats->domains_capacity > 0, per-domain details are written up to min(domain_count, domains_capacity).
- Parameters:
stats – [out] Caller-owned structure; zero-initialise before calling if per-domain detail is not needed.
- Returns:
ESP_OK Stats populated.
ESP_ERR_INVALID_ARG stats is NULL.
ESP_ERR_INVALID_STATE No hub has been created yet.
Structures
-
struct adf_event_t
Event descriptor passed to subscribers and to publish.
Note
See file-level “Ownership” for shallow-copy and lifetime rules. Routing uses the publisher’s hub (registered domain); event->domain is delivered to subscribers unchanged. Use the same spelling as at create() time if code compares event->domain (see file-level Conventions).
-
struct adf_event_delivery_t
Item delivered to a queue-mode subscriber.
Note
Caller must call adf_event_hub_delivery_done() exactly once after processing to release the shared payload reference.
Public Members
-
adf_event_t event
Shallow copy of the published event
-
uint32_t _opaque
Internal envelope index + generation; do not modify
-
adf_event_t event
-
struct adf_event_subscribe_info_t
Subscription parameters for adf_event_hub_subscribe().
Note
See file-level comment for queue/callback mode semantics. Use ADF_EVENT_SUBSCRIBE_INFO_DEFAULT() to zero-initialise, then set the fields you need.
Public Members
-
const char *event_domain
Domain to subscribe to; NULL = hub’s own domain; case-sensitive
-
uint16_t event_id
Event ID filter; ADF_EVENT_ANY_ID matches all
-
QueueHandle_t target_queue
Queue-mode inbox; non-NULL enables queue mode
-
adf_event_handler_t handler
Callback-mode handler; used when target_queue is NULL
-
void *handler_ctx
Opaque context forwarded to handler
-
const char *event_domain
-
struct adf_event_domain_stat_t
Per-domain statistics returned by adf_event_hub_get_stats().
-
struct adf_event_hub_stats_t
Statistics snapshot returned by adf_event_hub_get_stats().
Note
Set domains and domains_capacity before calling; all other fields are populated by adf_event_hub_get_stats().
Public Members
-
size_t domain_count
Total registered domains
-
size_t envelope_pool_size
Total envelope slots (only grows)
-
size_t envelopes_in_use
Currently active (referenced) envelopes
-
adf_event_domain_stat_t *domains
Caller-provided array; filled on return
-
size_t domains_capacity
Capacity of the domains array
-
size_t domain_count
Macros
-
ADF_EVENT_ANY_ID
Wildcard event ID: matches any event_id in subscribe / unsubscribe.
-
ADF_EVENT_SUBSCRIBE_INFO_DEFAULT()
Zero-initialise an adf_event_subscribe_info_t with event_id set to ADF_EVENT_ANY_ID and all other fields NULL.
Usage: adf_event_subscribe_info_t info = ADF_EVENT_SUBSCRIBE_INFO_DEFAULT(); info.event_domain = “wifi”; info.handler = my_handler;
Type Definitions
-
typedef void *adf_event_hub_t
adf_event_hub — domain-scoped publish-subscribe event facility.
Each hub handle represents one source domain (e.g. “wifi”, “ota”). Publishers call adf_event_hub_publish(); subscribers receive events via either a target_queue (queue mode, takes precedence when both are set) or a handler callback invoked synchronously in the publisher’s task.
Lifecycle: create() -> subscribe / publish / unsubscribe -> destroy(). subscribe() may target a domain before its publisher calls create(); the domain is auto-created and a later create() returns the same handle.
Ownership: adf_event_t fields are shallow-copied. The caller retains ownership of the domain string and payload for the entire delivery.
Conventions:
Thread-safe (internal mutex); no API blocks on events; not ISR-safe.
Callbacks must not call adf_event_hub_publish() recursively.
All APIs except create() require at least one hub to exist, otherwise ESP_ERR_INVALID_STATE is returned.
Domain identity is case-sensitive: “WiFi” and “wifi” are different domains. Use the same spelling in adf_event_hub_create(), adf_event_hub_subscribe() (event_domain), adf_event_hub_unsubscribe(), and in adf_event_t.domain when subscribers compare that string.
Minimal usage:
adf_event_hub_t wifi_hub; adf_event_hub_create(“wifi”, &wifi_hub);
adf_event_t ev = { .domain = “wifi”, .event_id = 1, .payload = NULL }; adf_event_hub_publish(wifi_hub, &ev, NULL, NULL);
adf_event_hub_t svc_hub; adf_event_hub_create(“my_service”, &svc_hub);
adf_event_subscribe_info_t info = ADF_EVENT_SUBSCRIBE_INFO_DEFAULT(); info.event_domain = “wifi”; info.event_id = 1; info.handler = my_wifi_handler; adf_event_hub_subscribe(svc_hub, &info); Opaque hub handle. Do not perform pointer arithmetic or semantic comparisons on this value.
-
typedef void (*adf_event_payload_release_cb_t)(const void *payload, void *ctx)
Payload release callback.
Note
Called exactly once per successful adf_event_hub_publish() invocation (i.e. publish() that returns anything other than ESP_ERR_INVALID_ARG due to a NULL hub or event):
immediately after publish() when there are no queue-mode subscribers (or queue-envelope allocation failed);
otherwise after the last delivery_done() call for queue-mode subscribers, or immediately on queue-full drops. Publish also invokes release_cb on its error paths (module not initialised, hub not registered, snapshot/envelope OOM) so callers never leak a heap-allocated payload when publish returns an error. The callee is responsible for freeing payload if it was heap-allocated.
- Param payload:
[in] Pointer passed as event.payload at publish time.
- Param ctx:
[in] Opaque context passed to adf_event_hub_publish().
-
typedef void (*adf_event_handler_t)(const adf_event_t *event, void *ctx)
Synchronous event callback for callback-mode subscribers.
Note
Invoked in the publisher’s task context. Must not block for a significant time. Not invoked from ISR context.
- Param event:
[in] Pointer to the delivered event descriptor.
- Param ctx:
[in] Opaque context registered at subscribe time.