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_ttarget_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_cbadf_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_queuehandler 同时设置时以队列模式为准,handler 被忽略;需要两种投递方式时应分别注册两条订阅。

API 参考

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.

备注

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.