# 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)`, and `ADF_EVENT_ANY_ID` acts as a wildcard that matches all events

- Two delivery modes: queue mode (non-blocking `xQueueSend`) and callback mode (executed synchronously in the publisher’s task), selectable independently per subscriber

- Startup-order independence: subscribers can complete their subscription before a publisher in the target domain calls `create()`; the target domain is created automatically

- Reference-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 zero

- Reference-counted envelope delivery: the optional `release_cb` is invoked exactly once per successful publish, regardless of whether the delivery path hit any queue subscribers

- Thread-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, and `adf_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

- components/adf_event_hub/include/adf_event_hub.h

### 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 hub` and 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).

  Public Members

  const char *domain

    Source domain label (e.g. “wifi”); not normalized

  uint16_t event_id

    Application-defined event identifier

  const void *payload

    Optional event data; may be NULL

  uint16_t payload_len

    Byte length of payload; 0 when payload is NULL

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

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

struct adf_event_domain_stat_t

  Per-domain statistics returned by adf_event_hub_get_stats().

  Public Members

  const char *domain

    Domain name (valid until hub is destroyed)

  size_t cb_subscriber_count

    Callback-mode subscriber count

  size_t queue_subscriber_count

    Queue-mode subscriber count

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

### 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.

Provide feedback about this document