ADF Event Hub
简介
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
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 huband 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).
-
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
-
adf_event_t event
-
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
-
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().
备注
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.
备注
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.