# ADF Event Hub

[English]

## 简介

ADF Event Hub 是面向 ADF 与 GMF 组件的按域发布订阅机制。每个句柄对应一个事件源域，例如无线网络或 OTA。发布者向该域投递事件，订阅者按域和事件编号过滤后，以队列或回调方式接收。ESP Service 中的每个服务实例都会自动创建并绑定一个事件中心，用于发布服务自身及派生服务的领域事件。

## 功能清单

- 按域发布订阅：一个 hub 句柄代表一个事件源域，订阅者按 `(event_domain, event_id)`过滤，`ADF_EVENT_ANY_ID` 作为通配符匹配全部事件

- 两种投递模式：队列模式（非阻塞 `xQueueSend`）与回调模式（在发布者任务中同步执行），按订阅者粒度独立选择

- 启动顺序无关：订阅者可以在目标域的发布者调用 `create()` 之前完成订阅，目标域会被自动创建

- 引用计数的 hub 生命周期：`adf_event_hub_create()` / `adf_event_hub_destroy()` 成对使用，域在引用计数归零时才被真正移除

- 带引用计数的 envelope 投递：可选的 `release_cb` 在每次成功发布后被精确调用一次，无论投递路径是否命中队列订阅者

- 线程安全：所有公开 API 由内部互斥锁保护，可在任意任务中调用（不支持 ISR 上下文）

- 可观测：`adf_event_hub_get_stats()` 返回各域的订阅者数量与 envelope 池使用情况，`adf_event_hub_dump()` 输出完整状态日志

## 技术拆解

### 发布订阅模型

每个域用一个字符串标识（如 `"wifi"`），大小写敏感。发布者和订阅者各自持有一个 hub 句柄：发布者的句柄标识自己所属的域，订阅者的句柄只是调用方的身份标识，通过 `adf_event_hub_subscribe()` 的 `event_domain` 字段指定要监听的域，为 `NULL` 时默认监听自己 hub 所属的域。

    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);

### 投递模式与引用释放

`adf_event_subscribe_info_t` 中 `target_queue` 非空时使用队列模式，否则使用 `handler` 回调模式；两者都设置时以队列模式为准。队列模式下，发布调用只做非阻塞的 `xQueueSend()`，某个订阅者的队列满只影响它自己的这一次投递；回调模式下 `handler` 在发布者任务里同步执行，不能有明显阻塞。

            sequenceDiagram
        participant Pub as 发布者
        participant Hub as event hub
        participant Sub as 队列订阅者

        Pub->>Hub: adf_event_hub_publish()
        Hub-)Sub: xQueueSend(delivery)
        Sub->>Hub: adf_event_hub_delivery_done()
        Hub--)Pub: release_cb(payload)

事件载荷按浅拷贝方式投递，堆上分配的 payload 需要通过 `release_cb` 回收：没有队列模式订阅者命中时，`release_cb` 在 `adf_event_hub_publish()` 返回前就会被调用；命中队列订阅者时，则延迟到最后一次 `adf_event_hub_delivery_done()` 调用之后才触发，且只会被调用一次。

    static void release_payload(const void *payload, void *ctx)
    {
        free((void *)payload);  /* 在最后一次 delivery_done 之后被精确调用一次 */
    }

    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);

备注

队列模式订阅者收到 `adf_event_delivery_t` 后，必须对每次投递调用恰好一次 `adf_event_hub_delivery_done()`，否则会占用 envelope 池而不释放。

### 生命周期与引用计数

`adf_event_hub_create()` 按引用计数管理域：多次对同一域调用 `create()` 只会增加引用计数，域仅在计数归零（即调用了同样次数的 `adf_event_hub_destroy()`）时才被真正移除并清理其订阅者。这一特性使得共享 hub 可以被多个持有者共同持有，任意一方销毁自己的引用都不会影响其他持有者。ESP Service 中的 `esp_service_init()` 已经封装了这个流程，业务代码通常不需要直接调用 event hub 的创建/销毁接口。

## 应用示例

- `components/adf_event_hub/examples/` 演示多个服务通过 ADF Event Hub 互动，同一份代码可在 PC 主机（FreeRTOS POSIX 模拟器）和 ESP-IDF 目标上编译运行。

## FAQ

Q1：队列模式和回调模式可以同时用在同一个订阅上吗？

不能。`target_queue` 和 `handler` 同时设置时以队列模式为准，`handler` 被忽略；需要两种投递方式时应分别注册两条订阅。

## API 参考

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

  备注

  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.

  参数:

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

  返回:

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

  备注

  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.

  参数:

    hub – [in] Hub handle returned by adf_event_hub_create().

  返回:

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

  备注

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

  参数:

    - hub – [in] Hub handle of the calling service’s context.

    - info – [in] Subscription parameters; event_domain NULL = hub’s own domain.

  返回:

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

  备注

  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.

  参数:

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

  返回:

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

  备注

  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.

  参数:

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

  返回:

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

  备注

  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.

  参数:

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

  返回:

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

  备注

  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.

  备注

  If stats->domains is non-NULL and stats->domains_capacity > 0, per-domain details are written up to min(domain_count, domains_capacity).

  参数:

    stats – [out] Caller-owned structure; zero-initialise before calling if per-domain detail is not needed.

  返回:

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

  备注

  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.

  备注

  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().

  备注

  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().

  备注

  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.

  备注

  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.

  备注

  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.

提供有关此文档的反馈