diff --git a/Devices/lilygo-tlora-pager/source/module.cpp b/Devices/lilygo-tlora-pager/source/module.cpp index 4f4b27aa3..ca93edd98 100644 --- a/Devices/lilygo-tlora-pager/source/module.cpp +++ b/Devices/lilygo-tlora-pager/source/module.cpp @@ -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; } diff --git a/Documentation/ideas.md b/Documentation/ideas.md index f92dbe67f..b2910b85f 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -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() diff --git a/Tactility/Source/lvgl/Statusbar.cpp b/Tactility/Source/lvgl/Statusbar.cpp index 92016eda6..9f192ff0f 100644 --- a/Tactility/Source/lvgl/Statusbar.cpp +++ b/Tactility/Source/lvgl/Statusbar.cpp @@ -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); } } diff --git a/Tactility/Source/service/rtctime/RtcTimeService.cpp b/Tactility/Source/service/rtctime/RtcTimeService.cpp index d8f677364..4f430b500 100644 --- a/Tactility/Source/service/rtctime/RtcTimeService.cpp +++ b/Tactility/Source/service/rtctime/RtcTimeService.cpp @@ -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; } diff --git a/Tactility/Source/service/wifi/Wifi.cpp b/Tactility/Source/service/wifi/Wifi.cpp index 2aa57f3f7..28e0f9d19 100644 --- a/Tactility/Source/service/wifi/Wifi.cpp +++ b/Tactility/Source/service/wifi/Wifi.cpp @@ -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; } diff --git a/TactilityKernel/include/tactility/system_event.h b/TactilityKernel/include/tactility/system_event.h index 65c83c467..5b0b82c15 100644 --- a/TactilityKernel/include/tactility/system_event.h +++ b/TactilityKernel/include/tactility/system_event.h @@ -5,6 +5,8 @@ #include #include +#include +#include #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 diff --git a/TactilityKernel/source/system_event.cpp b/TactilityKernel/source/system_event.cpp index 5038e11a2..ae649d2f0 100644 --- a/TactilityKernel/source/system_event.cpp +++ b/TactilityKernel/source/system_event.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -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(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" diff --git a/Tests/TactilityKernel/Source/SystemEventTest.cpp b/Tests/TactilityKernel/Source/SystemEventTest.cpp index c19f8759b..0ea1b7157 100644 --- a/Tests/TactilityKernel/Source/SystemEventTest.cpp +++ b/Tests/TactilityKernel/Source/SystemEventTest.cpp @@ -1,13 +1,15 @@ #include "doctest.h" +#include +#include #include #include #include // 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(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(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(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(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); }