Rename system_event api functions and add subscribe mechanism

This commit is contained in:
Ken Van Hoeylandt 2026-08-04 18:48:23 +02:00
parent d2c69ee7e8
commit 8e7f997597
8 changed files with 268 additions and 60 deletions

View File

@ -17,12 +17,12 @@ static void on_boot_completed(struct SystemEvent* /*event*/, void* /*context*/)
}
static error_t start() {
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr);
return ERROR_NONE;
}
static error_t stop() {
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed);
return ERROR_NONE;
}

View File

@ -12,6 +12,8 @@
## Higher Priority
- Improve Setup: Show "Step done" screen
- Improve Setup: Add keyboard/keypad navigation explanation
- display.h API: get_backlight does not change ref counting, but it should
- bluetooth: various getters for child devices do not change ref counting, but they should
- Improve kernel_init.cpp (and other modules): create driver_ensure_added() and driver_ensure_destructed()

View File

@ -133,7 +133,7 @@ static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj)
if (!statusbar_data.time_update_timer->isRunning()) {
statusbar_data.time_update_timer->start();
system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, onTimeChanged, nullptr);
system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, onTimeChanged, nullptr);
}
}

View File

@ -120,7 +120,7 @@ bool RtcTimeService::onStart(ServiceContext& serviceContext) {
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
}
if (system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline, this) == ERROR_NONE) {
if (system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline, this) == ERROR_NONE) {
timeEventSubscribed = true;
}
@ -129,7 +129,7 @@ bool RtcTimeService::onStart(ServiceContext& serviceContext) {
void RtcTimeService::onStop(ServiceContext& serviceContext) {
if (timeEventSubscribed) {
system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline);
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline);
timeEventSubscribed = false;
}

View File

@ -510,7 +510,7 @@ public:
LOG_W(TAG, "No WiFi device found");
}
if (system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) {
if (system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) {
state.bootEventSubscribed = true;
}
@ -531,7 +531,7 @@ public:
state.autoConnectTimer = nullptr; // Must release as it holds a reference via its callback.
if (state.bootEventSubscribed) {
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted);
state.bootEventSubscribed = false;
}

View File

@ -5,6 +5,8 @@
#include <stdint.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/task.h>
#ifdef __cplusplus
extern "C" {
@ -73,7 +75,7 @@ struct ServiceStoppedEvent {
/**
* @param[in] event the event being delivered; only valid for the duration of the call
* @param[in] context the context pointer passed to system_event_subscribe()
* @param[in] context the context pointer passed to system_event_callback_add()
*/
typedef void (*system_event_callback_t)(struct SystemEvent* event, void* context);
@ -82,7 +84,7 @@ typedef void (*system_event_callback_t)(struct SystemEvent* event, void* context
* @warning Does not work in ISR context.
* @warning @a callback is invoked synchronously, on the caller's task, from within
* system_event_emit(). The internal subscription lock is not held during the call, so
* @a callback may itself call system_event_subscribe(), system_event_unsubscribe() or
* @a callback may itself call system_event_callback_add(), system_event_callback_remove() or
* system_event_emit() without deadlocking - but a subscribe/unsubscribe made from within
* a callback only takes effect for events emitted after the current system_event_emit()
* call returns, since that call already snapshotted the subscriptions it will invoke.
@ -91,7 +93,7 @@ typedef void (*system_event_callback_t)(struct SystemEvent* event, void* context
* @param[in] context an opaque pointer passed back to @a callback unmodified
* @return ERROR_NONE on success
*/
error_t system_event_subscribe(
error_t system_event_callback_add(
enum SystemEventType type,
system_event_callback_t callback,
void *context
@ -100,11 +102,11 @@ error_t system_event_subscribe(
/**
* Remove a previously added subscription.
* @warning Does not work in ISR context.
* @param[in] type the event type passed to the matching system_event_subscribe() call
* @param[in] callback the callback passed to the matching system_event_subscribe() call
* @param[in] type the event type passed to the matching system_event_callback_add() call
* @param[in] callback the callback passed to the matching system_event_callback_add() call
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t system_event_unsubscribe(
error_t system_event_callback_remove(
enum SystemEventType type,
system_event_callback_t callback
);
@ -125,6 +127,59 @@ error_t system_event_emit(
size_t data_len
);
/** Size of the largest type-specific event struct documented in SystemEventType, i.e. the
* embedded buffer size needed by SystemEventSubscription to hold any event's payload by value. */
#define SYSTEM_EVENT_MAX_DATA_SIZE (sizeof(struct NetworkConnectedEvent))
/**
* gps.h-style poll subscription: caller-owned node, registered with system_event_subscribe()
* and polled with system_event_await(). Unlike system_event_callback_t, the payload is copied
* by value into @a data (up to SYSTEM_EVENT_MAX_DATA_SIZE bytes) so it remains valid after
* system_event_emit() returns.
* @warning Fields other than `type` are for internal use only; do not read or write them
* directly.
*/
struct SystemEventSubscription {
/** Event type to subscribe to; set by the caller before system_event_subscribe(). */
enum SystemEventType type;
TaskHandle_t task;
uint64_t timestamp;
uint8_t data[SYSTEM_EVENT_MAX_DATA_SIZE];
size_t data_len;
uint32_t sequence;
uint32_t consumed_sequence;
struct SystemEventSubscription* next;
};
/**
* Register a poll subscription for events of @a sub->type.
* @warning Does not work in ISR context.
* @param[in,out] sub subscription to register; caller sets @a sub->type beforehand, owns the
* storage, and must keep it alive (and stationary) until unsubscribed
* @return ERROR_NONE on success
*/
error_t system_event_subscribe(struct SystemEventSubscription* sub);
/**
* Remove a previously registered poll subscription.
* @warning Does not work in ISR context.
* @param[in] sub subscription to remove, as passed to system_event_subscribe()
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t system_event_unsubscribe(struct SystemEventSubscription* sub);
/**
* Blocks the calling task until a new event arrives for @a sub, or timeout elapses.
* @param[in,out] sub subscription to wait on, as passed to system_event_subscribe()
* @param[in] timeout max ticks to wait
* @return ERROR_NONE if an event arrived, ERROR_TIMEOUT if the timeout elapsed
*/
error_t system_event_await(struct SystemEventSubscription* sub, TickType_t timeout);
#ifdef __cplusplus
}
#endif

View File

@ -5,6 +5,7 @@
#include <tactility/time.h>
#include <algorithm>
#include <cstring>
#include <new>
#include <vector>
@ -27,9 +28,16 @@ struct KernelEventMutex {
static KernelEventMutex subscriptions_mutex;
// Intrusive singly-linked list of poll subscriptions (system_event_subscribe()/_unsubscribe()/
// _await()), separate from the callback-based `subscriptions` vector above. Guarded by its own
// mutex since notifying a poll subscriber never invokes caller code (just a memcpy and an
// xTaskNotifyGive), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
static SystemEventSubscription* poll_subscriptions = nullptr;
static KernelEventMutex poll_subscriptions_mutex;
extern "C" {
error_t system_event_subscribe(
error_t system_event_callback_add(
SystemEventType type,
system_event_callback_t callback,
void* context
@ -41,7 +49,7 @@ error_t system_event_subscribe(
return ERROR_NONE;
}
error_t system_event_unsubscribe(
error_t system_event_callback_remove(
SystemEventType type,
system_event_callback_t callback
) {
@ -59,20 +67,37 @@ error_t system_event_unsubscribe(
return result;
}
error_t system_event_emit(
enum SystemEventType type,
// Copies `data` into every current poll subscriber of `type` and wakes its waiting task.
// Held entirely under the lock: unlike the callback path, this never invokes caller code
// (just a memcpy and an xTaskNotifyGive), so there is nothing that could reenter and deadlock.
static void notify_poll_subscribers(
SystemEventType type,
uint64_t timestamp,
const void* data,
size_t data_len
) {
SystemEvent event = {
.type = type,
.timestamp = get_micros_since_boot(),
.data = data,
.data_len = data_len,
};
mutex_lock(&poll_subscriptions_mutex.handle);
for (SystemEventSubscription* sub = poll_subscriptions; sub != nullptr; sub = sub->next) {
if (sub->type == type) {
sub->timestamp = timestamp;
if (data_len > 0) {
std::memcpy(sub->data, data, std::min(data_len, static_cast<size_t>(SYSTEM_EVENT_MAX_DATA_SIZE)));
}
sub->data_len = data_len;
sub->sequence++;
xTaskNotifyGive(sub->task);
}
}
mutex_unlock(&poll_subscriptions_mutex.handle);
}
static error_t notify_listeners(
SystemEvent& event
) {
// Snapshot matching subscriptions under the lock, then invoke after unlocking: a
// callback calling system_event_subscribe(), system_event_unsubscribe() or
// callback calling system_event_callback_add(), system_event_callback_remove() or
// system_event_emit() would otherwise deadlock against this same (non-recursive)
// mutex, and a slow callback would block every other thread's subscribe/unsubscribe
// for the duration of this emit.
@ -86,7 +111,7 @@ error_t system_event_emit(
size_t match_count = 0;
for (const auto& subscription : subscriptions) {
if (subscription.type == type) {
if (subscription.type == event.type) {
match_count++;
}
}
@ -101,7 +126,7 @@ error_t system_event_emit(
size_t matched_count = 0;
for (const auto& subscription : subscriptions) {
if (subscription.type == type) {
if (subscription.type == event.type) {
matching[matched_count++] = subscription;
}
}
@ -117,4 +142,66 @@ error_t system_event_emit(
return ERROR_NONE;
}
error_t system_event_emit(
SystemEventType type,
const void* data,
size_t data_len
) {
SystemEvent event = {
.type = type,
.timestamp = get_micros_since_boot(),
.data = data,
.data_len = data_len,
};
notify_poll_subscribers(type, event.timestamp, data, data_len);
auto error = notify_listeners(event);
if (error != ERROR_NONE) { return error; }
return ERROR_NONE;
}
error_t system_event_subscribe(SystemEventSubscription* sub) {
sub->task = xTaskGetCurrentTaskHandle();
sub->sequence = 0;
sub->consumed_sequence = 0;
sub->data_len = 0;
mutex_lock(&poll_subscriptions_mutex.handle);
sub->next = poll_subscriptions;
poll_subscriptions = sub;
mutex_unlock(&poll_subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t system_event_unsubscribe(SystemEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
mutex_lock(&poll_subscriptions_mutex.handle);
for (SystemEventSubscription** link = &poll_subscriptions; *link != nullptr; link = &(*link)->next) {
if (*link == sub) {
*link = sub->next;
result = ERROR_NONE;
break;
}
}
mutex_unlock(&poll_subscriptions_mutex.handle);
return result;
}
error_t system_event_await(SystemEventSubscription* sub, TickType_t timeout) {
uint32_t old_sequence = sub->sequence;
while (sub->sequence == old_sequence) {
if (ulTaskNotifyTake(pdTRUE, timeout) == 0) {
return ERROR_TIMEOUT;
}
}
sub->consumed_sequence = sub->sequence;
return ERROR_NONE;
}
} // extern "C"

View File

@ -1,13 +1,15 @@
#include "doctest.h"
#include <tactility/concurrent/thread.h>
#include <tactility/delay.h>
#include <tactility/system_event.h>
#include <tactility/time.h>
#include <vector>
// system_event_emit() snapshots matching subscriptions under the lock, then invokes them
// after unlocking (see the @warning on system_event_subscribe() in system_event.h), so a
// callback calling system_event_subscribe()/_unsubscribe()/_emit() must not deadlock -
// after unlocking (see the @warning on system_event_callback_add() in system_event.h), so a
// callback calling system_event_callback_add()/_unsubscribe()/_emit() must not deadlock -
// covered below, mirroring DeviceListenerTest.cpp's reentrancy test.
struct RecordedCall {
@ -39,8 +41,8 @@ TEST_CASE("system_event_emit invokes every subscriber registered for that type")
int context_a = 1;
int context_b = 2;
CHECK_EQ(system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a), ERROR_NONE);
CHECK_EQ(system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b), ERROR_NONE);
CHECK_EQ(system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a), ERROR_NONE);
CHECK_EQ(system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b), ERROR_NONE);
CHECK_EQ(system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0), ERROR_NONE);
@ -51,14 +53,14 @@ TEST_CASE("system_event_emit invokes every subscriber registered for that type")
REQUIRE_EQ(calls_b.size(), 1);
CHECK_EQ(calls_b[0].context, &context_b);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
}
TEST_CASE("system_event_emit only invokes subscribers registered for the emitted type") {
reset_calls();
int context_a = 1;
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
CHECK_EQ(calls_a.size(), 0);
@ -66,7 +68,7 @@ TEST_CASE("system_event_emit only invokes subscribers registered for the emitted
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
CHECK_EQ(calls_a.size(), 1);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
}
TEST_CASE("system_event_emit passes the data pointer and length through unchanged") {
@ -74,7 +76,7 @@ TEST_CASE("system_event_emit passes the data pointer and length through unchange
int context_a = 1;
struct Payload { int value; } payload { 42 };
system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a);
system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a);
system_event_emit(KERNEL_EVENT_TIME_CHANGED, &payload, sizeof(payload));
REQUIRE_EQ(calls_a.size(), 1);
@ -82,13 +84,13 @@ TEST_CASE("system_event_emit passes the data pointer and length through unchange
CHECK_EQ(calls_a[0].data_len, sizeof(payload));
CHECK_EQ(static_cast<const Payload*>(calls_a[0].data)->value, 42);
system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, listener_a);
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_a);
}
TEST_CASE("system_event_emit with no data passes a null pointer and zero length") {
reset_calls();
int context_a = 1;
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
@ -96,50 +98,50 @@ TEST_CASE("system_event_emit with no data passes a null pointer and zero length"
CHECK_EQ(calls_a[0].data, nullptr);
CHECK_EQ(calls_a[0].data_len, 0);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
}
TEST_CASE("system_event_unsubscribe stops further notifications for that callback only") {
TEST_CASE("system_event_callback_remove stops further notifications for that callback only") {
reset_calls();
int context_a = 1;
int context_b = 2;
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b);
CHECK_EQ(system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a), ERROR_NONE);
CHECK_EQ(system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a), ERROR_NONE);
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
CHECK_EQ(calls_a.size(), 0);
CHECK_EQ(calls_b.size(), 1);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
}
TEST_CASE("system_event_unsubscribe on an unregistered callback returns ERROR_NOT_FOUND and is a no-op") {
TEST_CASE("system_event_callback_remove on an unregistered callback returns ERROR_NOT_FOUND and is a no-op") {
reset_calls();
int context_b = 2;
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b);
// listener_a was never added for this type, so removing it must not disturb listener_b.
CHECK_EQ(system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a), ERROR_NOT_FOUND);
CHECK_EQ(system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a), ERROR_NOT_FOUND);
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
CHECK_EQ(calls_b.size(), 1);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
}
TEST_CASE("system_event_unsubscribe matches on (type, callback), not the callback alone") {
TEST_CASE("system_event_callback_remove matches on (type, callback), not the callback alone") {
reset_calls();
int context_a = 1;
// Same callback subscribed for two different event types.
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
CHECK_EQ(calls_a.size(), 0);
@ -147,7 +149,7 @@ TEST_CASE("system_event_unsubscribe matches on (type, callback), not the callbac
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
CHECK_EQ(calls_a.size(), 1);
system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, listener_a);
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_a);
}
TEST_CASE("system_event_emit with no subscribers for that type returns ERROR_NONE") {
@ -157,7 +159,7 @@ TEST_CASE("system_event_emit with no subscribers for that type returns ERROR_NON
TEST_CASE("system_event_emit stamps the event with the current boot-relative time") {
reset_calls();
int context_a = 1;
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
auto before = static_cast<uint64_t>(get_micros_since_boot());
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
@ -167,7 +169,7 @@ TEST_CASE("system_event_emit stamps the event with the current boot-relative tim
CHECK_GE(calls_a[0].timestamp, before);
CHECK_LE(calls_a[0].timestamp, after);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
}
static bool reentrant_add_triggered = false;
@ -179,10 +181,10 @@ static void reentrant_listener(SystemEvent* event, void* context) {
// Subscribing from within a notification must not deadlock: emit() releases the
// lock before invoking callbacks, so this only blocks briefly on the (already
// unlocked) mutex.
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b, context);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_b, context);
// Also exercise unsubscribe() and a nested emit() of a different type from within
// a callback - all must complete without deadlocking.
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, reentrant_listener);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, reentrant_listener);
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
}
}
@ -192,8 +194,8 @@ TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes an
reentrant_add_triggered = false;
int context_a = 1;
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, reentrant_listener, &context_a);
system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, listener_b, &context_a);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, reentrant_listener, &context_a);
system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, listener_b, &context_a);
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
@ -210,6 +212,68 @@ TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes an
CHECK_EQ(calls_a.size(), 1);
CHECK_EQ(calls_b.size(), 2);
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, listener_b);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_b);
}
// gps.h-style poll subscription: system_event_subscribe()/_await()/_unsubscribe().
//
// system_event_await() only detects sequence increments that happen *after* it starts
// waiting (same as gps_api_event_await()), so the emit must be started from another task
// while this one is already blocked in await() - emitting first and awaiting after would
// race the notification the same way it would with any FreeRTOS task-notify consumer.
TEST_CASE("system_event_subscribe/_await deliver the event payload by value") {
SystemEventSubscription sub {};
sub.type = KERNEL_EVENT_NETWORK_CONNECTED;
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
auto* thread = thread_alloc_full(
"system-event-emitter",
4096,
[](void* context) {
delay_millis(20);
auto* connected_ptr = static_cast<NetworkConnectedEvent*>(context);
system_event_emit(KERNEL_EVENT_NETWORK_CONNECTED, connected_ptr, sizeof(*connected_ptr));
return 0;
},
&connected,
-1
);
CHECK_EQ(thread_start(thread), ERROR_NONE);
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
const auto* received = reinterpret_cast<const NetworkConnectedEvent*>(sub.data);
CHECK_EQ(received->ipv4_addr, connected.ipv4_addr);
CHECK_EQ(received->gateway, connected.gateway);
CHECK_EQ(sub.data_len, sizeof(connected));
CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE);
thread_free(thread);
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
}
TEST_CASE("system_event_await times out when no matching event has arrived") {
SystemEventSubscription sub {};
sub.type = KERNEL_EVENT_TIME_CHANGED;
system_event_subscribe(&sub);
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
system_event_unsubscribe(&sub);
}
TEST_CASE("system_event_emit does not notify a poll subscriber of a different type") {
SystemEventSubscription sub {};
sub.type = KERNEL_EVENT_BOOT_COMPLETED;
system_event_subscribe(&sub);
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
system_event_unsubscribe(&sub);
}