From acb2f1d4c75e77c736cc889827ff5efbb23fedb6 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 29 Jul 2026 17:31:20 +0200 Subject: [PATCH] Replace Tactility SystemEvents with new kernel implementation (#598) TactilityKernel now has a system event API to replace the one from the Tactility subproject. It also implements several tests for it. --- Devices/lilygo-tdeck-plus/source/module.cpp | 16 +- Devices/lilygo-tdeck/source/module.cpp | 31 +-- Devices/lilygo-tlora-pager/source/module.cpp | 22 +- .../source/drivers/esp32_wifi.cpp | 14 ++ Tactility/Include/Tactility/SystemEvents.h | 28 --- .../service/rtctime/RtcTimeService.h | 7 +- Tactility/Source/SystemEvents.cpp | 81 ------- Tactility/Source/app/boot/Boot.cpp | 6 +- Tactility/Source/lvgl/Statusbar.cpp | 10 +- Tactility/Source/network/Ntp.cpp | 4 +- .../Source/service/rtctime/RtcTimeService.cpp | 32 +-- Tactility/Source/service/wifi/Wifi.cpp | 26 ++- Tactility/Source/settings/time.cpp | 5 +- .../include/tactility/system_event.h | 130 +++++++++++ TactilityKernel/include/tactility/time.h | 6 +- .../source/filesystem/file_system.cpp | 18 +- .../source/service/service_manager.cpp | 8 + TactilityKernel/source/system_event.cpp | 120 ++++++++++ .../Source/SystemEventTest.cpp | 215 ++++++++++++++++++ 19 files changed, 589 insertions(+), 190 deletions(-) delete mode 100644 Tactility/Include/Tactility/SystemEvents.h delete mode 100644 Tactility/Source/SystemEvents.cpp create mode 100644 TactilityKernel/include/tactility/system_event.h create mode 100644 TactilityKernel/source/system_event.cpp create mode 100644 Tests/TactilityKernel/Source/SystemEventTest.cpp diff --git a/Devices/lilygo-tdeck-plus/source/module.cpp b/Devices/lilygo-tdeck-plus/source/module.cpp index 7460fe6cb..50e0dc27a 100644 --- a/Devices/lilygo-tdeck-plus/source/module.cpp +++ b/Devices/lilygo-tdeck-plus/source/module.cpp @@ -4,7 +4,8 @@ #include #include -#include +#include + #include #include @@ -15,8 +16,6 @@ constexpr auto* TAG = "tdeck-plus"; extern "C" { -static tt::kernel::SystemEventSubscription tdeck_boot_splash_subscription = tt::kernel::NoSystemEventSubscription; - void init_trackball() { auto tbSettings = tt::settings::trackball::loadOrGetDefault(); lvgl_lock(); @@ -31,6 +30,10 @@ void init_trackball() { lvgl_unlock(); } +static void on_boot_completed(struct SystemEvent* /*event*/, void* /*context*/) { + init_trackball(); +} + static error_t start() { LOG_I(TAG, LOG_MESSAGE_POWER_ON_START); @@ -42,16 +45,13 @@ static error_t start() { // Avoids crash when no SD card is inserted. It's unknown why, but likely is related to power draw. delay_millis(100); - tdeck_boot_splash_subscription = tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { - init_trackball(); - }); + system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr); return ERROR_NONE; } static error_t stop() { - tt::kernel::unsubscribeSystemEvent(tdeck_boot_splash_subscription); - tdeck_boot_splash_subscription = tt::kernel::NoSystemEventSubscription; + system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed); return ERROR_NONE; } diff --git a/Devices/lilygo-tdeck/source/module.cpp b/Devices/lilygo-tdeck/source/module.cpp index 9087099f1..e9863403f 100644 --- a/Devices/lilygo-tdeck/source/module.cpp +++ b/Devices/lilygo-tdeck/source/module.cpp @@ -5,7 +5,8 @@ #include -#include +#include + #include #include #include @@ -17,22 +18,24 @@ constexpr auto* TAG = "tdeck"; extern "C" { +static void on_boot_completed(struct SystemEvent* /*event*/, void* /*context*/) { + auto tbSettings = tt::settings::trackball::loadOrGetDefault(); + lvgl_lock(); + if (trackball::init() != nullptr) { + trackball::setMode(tbSettings.trackballMode == tt::settings::trackball::TrackballMode::Pointer + ? trackball::Mode::Pointer + : trackball::Mode::Encoder); + trackball::setEncoderSensitivity(tbSettings.encoderSensitivity); + trackball::setPointerSensitivity(tbSettings.pointerSensitivity); + trackball::setEnabled(tbSettings.trackballEnabled); + } + lvgl_unlock(); +} + void subscribe_events() { // The kernel trackball device is already started by kernel_init(); this just registers it as an // LVGL input device and applies persisted settings, both of which require LVGL to be up first. - tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { - auto tbSettings = tt::settings::trackball::loadOrGetDefault(); - lvgl_lock(); - if (trackball::init() != nullptr) { - trackball::setMode(tbSettings.trackballMode == tt::settings::trackball::TrackballMode::Pointer - ? trackball::Mode::Pointer - : trackball::Mode::Encoder); - trackball::setEncoderSensitivity(tbSettings.encoderSensitivity); - trackball::setPointerSensitivity(tbSettings.pointerSensitivity); - trackball::setEnabled(tbSettings.trackballEnabled); - } - lvgl_unlock(); - }); + system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr); } static error_t start() { diff --git a/Devices/lilygo-tlora-pager/source/module.cpp b/Devices/lilygo-tlora-pager/source/module.cpp index b24dd91a3..f690156d7 100644 --- a/Devices/lilygo-tlora-pager/source/module.cpp +++ b/Devices/lilygo-tlora-pager/source/module.cpp @@ -1,7 +1,8 @@ #include #include -#include +#include + #include #include @@ -10,22 +11,21 @@ constexpr auto* TAG = "T-Lora Pager"; extern "C" { -tt::kernel::SystemEventSubscription event_subscription = tt::kernel::NoSystemEventSubscription; +static void on_boot_completed(struct SystemEvent* /*event*/, void* /*context*/) { + // The kernel tpager_encoder device is already started by kernel_init(); this just + // registers it as an LVGL input device, which requires LVGL to be up first. + lvgl_lock(); + tpager_encoder::init(); + lvgl_unlock(); +} static error_t start() { - event_subscription = tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent) { - // The kernel tpager_encoder device is already started by kernel_init(); this just - // registers it as an LVGL input device, which requires LVGL to be up first. - lvgl_lock(); - tpager_encoder::init(); - lvgl_unlock(); - }); + system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr); return ERROR_NONE; } static error_t stop() { - tt::kernel::unsubscribeSystemEvent(event_subscription); - event_subscription = tt::kernel::NoSystemEventSubscription; + system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed); return ERROR_NONE; } diff --git a/Platforms/platform-esp32/source/drivers/esp32_wifi.cpp b/Platforms/platform-esp32/source/drivers/esp32_wifi.cpp index af5325b48..dfcddeb63 100644 --- a/Platforms/platform-esp32/source/drivers/esp32_wifi.cpp +++ b/Platforms/platform-esp32/source/drivers/esp32_wifi.cpp @@ -21,6 +21,9 @@ #include #endif +#include "tactility/system_event.h" + + #include #include #include @@ -114,6 +117,10 @@ void on_wifi_or_ip_event(void* arg, esp_event_base_t event_base, int32_t event_i result_event.type = WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT; result_event.connection_error = WIFI_STATION_CONNECTION_ERROR_TARGET_NOT_FOUND; fire_event(ctx, result_event); + NetworkDisconnectedEvent disconnected_event = { + .device = ctx->device + }; + system_event_emit(KERNEL_EVENT_NETWORK_DISCONNECTED, &disconnected_event, sizeof(disconnected_event)); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { auto* got_ip = static_cast(event_data); @@ -132,6 +139,13 @@ void on_wifi_or_ip_event(void* arg, esp_event_base_t event_base, int32_t event_i result_event.type = WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT; result_event.connection_error = WIFI_STATION_CONNECTION_ERROR_NONE; fire_event(ctx, result_event); + + NetworkConnectedEvent connected_event = { + .device = ctx->device, + .ipv4_addr = ctx->ipInfo.ip.addr, + .gateway = ctx->ipInfo.gw.addr, + }; + system_event_emit(KERNEL_EVENT_NETWORK_CONNECTED, &connected_event, sizeof(connected_event)); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { mutex_lock(&ctx->mutex); ctx->scanning = false; diff --git a/Tactility/Include/Tactility/SystemEvents.h b/Tactility/Include/Tactility/SystemEvents.h deleted file mode 100644 index 3265e0f75..000000000 --- a/Tactility/Include/Tactility/SystemEvents.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -namespace tt::kernel { - -enum class SystemEvent { - BootSplash, - /** Gained IP address */ - NetworkConnected, - NetworkDisconnected, - Time, -}; - -/** Value 0 mean "no subscription" */ -typedef uint32_t SystemEventSubscription; -constexpr SystemEventSubscription NoSystemEventSubscription = 0U; - -typedef std::function OnSystemEvent; - -void publishSystemEvent(SystemEvent event); - -SystemEventSubscription subscribeSystemEvent(SystemEvent event, OnSystemEvent handler); - -void unsubscribeSystemEvent(SystemEventSubscription subscription); - -} \ No newline at end of file diff --git a/Tactility/Include/Tactility/service/rtctime/RtcTimeService.h b/Tactility/Include/Tactility/service/rtctime/RtcTimeService.h index ab502db82..701d4f9da 100644 --- a/Tactility/Include/Tactility/service/rtctime/RtcTimeService.h +++ b/Tactility/Include/Tactility/service/rtctime/RtcTimeService.h @@ -1,21 +1,22 @@ #pragma once -#include #include #include struct Device; +struct SystemEvent; namespace tt::service::rtctime { class RtcTimeService final : public Service { - kernel::SystemEventSubscription timeEventSubscription = 0; + bool timeEventSubscribed = false; Device* rtcDevice = nullptr; Device* findRtcDevice(); - void onTimeChanged(kernel::SystemEvent event); + void onTimeChanged(); + static void onTimeChangedTrampoline(struct SystemEvent* event, void* context); public: diff --git a/Tactility/Source/SystemEvents.cpp b/Tactility/Source/SystemEvents.cpp deleted file mode 100644 index 2f2834072..000000000 --- a/Tactility/Source/SystemEvents.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -#include - -namespace tt::kernel { - -constexpr auto* TAG = "SystemEvents"; - -struct SubscriptionData { - SystemEventSubscription id; - SystemEvent event; - OnSystemEvent handler; -}; - -static Mutex mutex; -static SystemEventSubscription subscriptionCounter = 0; -static std::list subscriptions; - -static const char* getEventName(SystemEvent event) { - switch (event) { - using enum SystemEvent; - case BootSplash: - return TT_STRINGIFY(BootSplash); - case NetworkConnected: - return TT_STRINGIFY(NetworkConnected); - case NetworkDisconnected: - return TT_STRINGIFY(NetworkDisconnected); - case Time: - return TT_STRINGIFY(Time); - } - - check(false); // Missing case above -} - -void publishSystemEvent(SystemEvent event) { - LOG_I(TAG, "%s", getEventName(event)); - - if (mutex.lock(MAX_TICKS)) { - for (auto& subscription : subscriptions) { - if (subscription.event == event) { - subscription.handler(event); - } - } - - mutex.unlock(); - } -} - -SystemEventSubscription subscribeSystemEvent(SystemEvent event, OnSystemEvent handler) { - if (mutex.lock(MAX_TICKS)) { - auto id = ++subscriptionCounter; - - subscriptions.push_back({ - .id = id, - .event = event, - .handler = handler - }); - - mutex.unlock(); - return id; - } else { - check(false); - } -} - -void unsubscribeSystemEvent(SystemEventSubscription subscription) { - if (mutex.lock(MAX_TICKS)) { - std::erase_if(subscriptions, [subscription](auto& item) { - return (item.id == subscription); - }); - mutex.unlock(); - } -} - -} diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 706266f9a..bc904d9b0 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -1,3 +1,6 @@ +#include "tactility/system_event.h" + + #include #include #include @@ -6,7 +9,6 @@ #include #include -#include #include #include #include @@ -154,7 +156,7 @@ class BootApp : public App { // This event will likely block as other systems are initialized // e.g. Wi-Fi reads AP configs from SD card LOG_I(TAG, "Publish event"); - kernel::publishSystemEvent(kernel::SystemEvent::BootSplash); + system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); return 0; } diff --git a/Tactility/Source/lvgl/Statusbar.cpp b/Tactility/Source/lvgl/Statusbar.cpp index 7d254129b..92016eda6 100644 --- a/Tactility/Source/lvgl/Statusbar.cpp +++ b/Tactility/Source/lvgl/Statusbar.cpp @@ -1,6 +1,5 @@ #define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration -#include #include #include #include @@ -11,6 +10,7 @@ #include #include +#include #include #include @@ -38,7 +38,6 @@ struct StatusbarData { uint8_t time_hours = 0; uint8_t time_minutes = 0; bool time_set = false; - kernel::SystemEventSubscription systemEventSubscription = 0; }; static StatusbarData statusbar_data; @@ -115,7 +114,7 @@ static void statusbar_pubsub_event(Statusbar* statusbar) { } } -static void onTimeChanged(kernel::SystemEvent event) { +static void onTimeChanged(struct SystemEvent* /*event*/, void* /*context*/) { if (statusbar_data.mutex.lock()) { statusbar_data.time_update_timer->reset(5); statusbar_data.mutex.unlock(); @@ -134,10 +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(); - statusbar_data.systemEventSubscription = kernel::subscribeSystemEvent( - kernel::SystemEvent::Time, - onTimeChanged - ); + system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, onTimeChanged, nullptr); } } diff --git a/Tactility/Source/network/Ntp.cpp b/Tactility/Source/network/Ntp.cpp index fa7506a1a..d7d43da25 100644 --- a/Tactility/Source/network/Ntp.cpp +++ b/Tactility/Source/network/Ntp.cpp @@ -6,8 +6,8 @@ #include #ifdef ESP_PLATFORM -#include #include +#include #include #include #endif @@ -45,7 +45,7 @@ static void onTimeSynced(timeval* tv) { processedSyncEvent = true; esp_netif_sntp_deinit(); storeTimeInNvs(); - kernel::publishSystemEvent(kernel::SystemEvent::Time); + system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0); } void init() { diff --git a/Tactility/Source/service/rtctime/RtcTimeService.cpp b/Tactility/Source/service/rtctime/RtcTimeService.cpp index 44a026ddf..d8f677364 100644 --- a/Tactility/Source/service/rtctime/RtcTimeService.cpp +++ b/Tactility/Source/service/rtctime/RtcTimeService.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -96,15 +97,17 @@ static void writeRtcFromSystemTime(Device* rtc) { } } -void RtcTimeService::onTimeChanged(kernel::SystemEvent event) { - if (event == kernel::SystemEvent::Time) { - Device* rtc = findRtcDevice(); - if (rtc) { - writeRtcFromSystemTime(rtc); - } +void RtcTimeService::onTimeChanged() { + Device* rtc = findRtcDevice(); + if (rtc) { + writeRtcFromSystemTime(rtc); } } +void RtcTimeService::onTimeChangedTrampoline(struct SystemEvent* /*event*/, void* context) { + static_cast(context)->onTimeChanged(); +} + bool RtcTimeService::onStart(ServiceContext& serviceContext) { Device* rtc = findRtcDevice(); if (!rtc) { @@ -113,22 +116,21 @@ bool RtcTimeService::onStart(ServiceContext& serviceContext) { } if (setSystemTimeFromRtc(rtc)) { - // Publish time event so other components know time is now valid - kernel::publishSystemEvent(kernel::SystemEvent::Time); + // Emit time event so other components know time is now valid + system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0); } - timeEventSubscription = kernel::subscribeSystemEvent( - kernel::SystemEvent::Time, - [this](kernel::SystemEvent event) { onTimeChanged(event); } - ); + if (system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline, this) == ERROR_NONE) { + timeEventSubscribed = true; + } return true; } void RtcTimeService::onStop(ServiceContext& serviceContext) { - if (timeEventSubscription != 0) { - kernel::unsubscribeSystemEvent(timeEventSubscription); - timeEventSubscription = 0; + if (timeEventSubscribed) { + system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline); + timeEventSubscribed = false; } if (rtcDevice) { diff --git a/Tactility/Source/service/wifi/Wifi.cpp b/Tactility/Source/service/wifi/Wifi.cpp index 64781f3ec..2aa57f3f7 100644 --- a/Tactility/Source/service/wifi/Wifi.cpp +++ b/Tactility/Source/service/wifi/Wifi.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -16,6 +15,7 @@ #include #include #include +#include #include #include @@ -78,7 +78,7 @@ struct WifiServiceState { uint16_t scanRecordLimit = TT_WIFI_SCAN_RECORD_LIMIT; TickType_t lastScanTime = MAX_TICKS; std::unique_ptr autoConnectTimer; - kernel::SystemEventSubscription bootEventSubscription = kernel::NoSystemEventSubscription; + bool bootEventSubscribed = false; }; WifiServiceState state; @@ -278,7 +278,7 @@ void onAutoConnectTimer() { // ---- Kernel driver event bridge ---- -void onWifiDeviceEvent(Device* /*device*/, void* /*context*/, ::WifiEvent event) { +void onWifiDeviceEvent(Device* device, void* /*context*/, ::WifiEvent event) { switch (event.type) { case WIFI_EVENT_TYPE_SCAN_FINISHED: getMainDispatcher().dispatch([] { dispatchAutoConnect(); }); @@ -291,7 +291,8 @@ void onWifiDeviceEvent(Device* /*device*/, void* /*context*/, ::WifiEvent event) // Resetting it on every disconnect (including deliberate ones) would // let auto-connect immediately reconnect the user. Attempts that fail // while pending are unpaused via WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT below. - kernel::publishSystemEvent(kernel::SystemEvent::NetworkDisconnected); + NetworkDisconnectedEvent disconnected_event = { .device = device }; + system_event_emit(KERNEL_EVENT_NETWORK_DISCONNECTED, &disconnected_event, sizeof(disconnected_event)); } break; @@ -319,7 +320,6 @@ void onWifiDeviceEvent(Device* /*device*/, void* /*context*/, ::WifiEvent event) if (remember && !settings::save(target)) { LOG_E(TAG, "Failed to store credentials"); } - kernel::publishSystemEvent(kernel::SystemEvent::NetworkConnected); } else { // The pending connection attempt (which paused auto-connect via connect()) // failed; unpause so auto-connect can try other saved APs. @@ -492,6 +492,10 @@ std::string getIp() { namespace { +void onBootCompleted(struct SystemEvent* /*event*/, void* /*context*/) { + bootSplashInit(); +} + class WifiService final : public Service { public: @@ -506,9 +510,9 @@ public: LOG_W(TAG, "No WiFi device found"); } - state.bootEventSubscription = kernel::subscribeSystemEvent(kernel::SystemEvent::BootSplash, [](auto) { - bootSplashInit(); - }); + if (system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) { + state.bootEventSubscribed = true; + } auto timer_interval = std::min(2000, AUTO_SCAN_INTERVAL); state.autoConnectTimer = std::make_unique(Timer::Type::Periodic, timer_interval, [] { onAutoConnectTimer(); }); @@ -526,8 +530,10 @@ public: state.autoConnectTimer->stop(); state.autoConnectTimer = nullptr; // Must release as it holds a reference via its callback. - kernel::unsubscribeSystemEvent(state.bootEventSubscription); - state.bootEventSubscription = kernel::NoSystemEventSubscription; + if (state.bootEventSubscribed) { + system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted); + state.bootEventSubscribed = false; + } if (state.device != nullptr && device_is_ready(state.device)) { wifi_remove_event_callback(state.device, onWifiDeviceEvent); diff --git a/Tactility/Source/settings/time.cpp b/Tactility/Source/settings/time.cpp index 073a3ad7d..435c080f8 100644 --- a/Tactility/Source/settings/time.cpp +++ b/Tactility/Source/settings/time.cpp @@ -1,9 +1,10 @@ #include -#include #include #include +#include + #ifdef ESP_PLATFORM #include #endif @@ -36,7 +37,7 @@ void setTimeZone(const std::string& name, const std::string& code) { tzset(); #endif - kernel::publishSystemEvent(kernel::SystemEvent::Time); + system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0); } std::string getTimeZoneName() { diff --git a/TactilityKernel/include/tactility/system_event.h b/TactilityKernel/include/tactility/system_event.h new file mode 100644 index 000000000..65c83c467 --- /dev/null +++ b/TactilityKernel/include/tactility/system_event.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct FileSystem; + +/** Identifies a system-wide event */ +enum SystemEventType { + KERNEL_EVENT_BOOT_COMPLETED, // No data + KERNEL_EVENT_NETWORK_CONNECTED, // struct NetworkConnectedEvent + KERNEL_EVENT_NETWORK_DISCONNECTED, // struct NetworkDisconnectedEvent + KERNEL_EVENT_FILE_SYSTEM_MOUNTED, // struct FileSystemMountedEvent + KERNEL_EVENT_FILE_SYSTEM_UNMOUNTED, // struct FileSystemUnmountedEvent + KERNEL_EVENT_SERVICE_STARTED, // ServiceStartedEvent + KERNEL_EVENT_SERVICE_STOPPED, // ServiceStoppedEvent + KERNEL_EVENT_TIME_CHANGED, // No data - fired whenever system time is set (NTP sync, RTC restore, manual change) +}; + +/** + * A system-wide event as delivered to a system_event_callback_t. + * `data` points at the type-specific struct documented next to `type`'s enum value + * in SystemEventType (or is NULL when none is documented). + * It is only valid for the duration of the callback. + */ +struct SystemEvent { + enum SystemEventType type; + /** Microseconds since boot, from get_micros_since_boot(). */ + uint64_t timestamp; + const void *data; + size_t data_len; +}; + +/** Data for KERNEL_EVENT_NETWORK_CONNECTED. */ +struct NetworkConnectedEvent { + struct Device* device; + uint32_t ipv4_addr; + uint32_t gateway; +}; + +/** Data for KERNEL_EVENT_NETWORK_DISCONNECTED. */ +struct NetworkDisconnectedEvent { + struct Device* device; +}; + +/** Data for KERNEL_EVENT_FILE_SYSTEM_MOUNTED. */ +struct FileSystemMountedEvent { + struct FileSystem* file_system; +}; + +/** Data for KERNEL_EVENT_FILE_SYSTEM_UNMOUNTED. */ +struct FileSystemUnmountedEvent { + struct FileSystem* file_system; +}; + +/** Data for KERNEL_EVENT_SERVICE_STARTED. */ +struct ServiceStartedEvent { + const char* id; +}; + +/** Data for KERNEL_EVENT_SERVICE_STOPPED. */ +struct ServiceStoppedEvent { + const char* id; +}; + +/** + * @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() + */ +typedef void (*system_event_callback_t)(struct SystemEvent* event, void* context); + +/** + * Subscribe to system events of a given type. + * @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 + * 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. + * @param[in] type the event type to subscribe to + * @param[in] callback the callback to invoke when a matching event is emitted + * @param[in] context an opaque pointer passed back to @a callback unmodified + * @return ERROR_NONE on success + */ +error_t system_event_subscribe( + enum SystemEventType type, + system_event_callback_t callback, + void *context +); + +/** + * 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 + * @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists + */ +error_t system_event_unsubscribe( + enum SystemEventType type, + system_event_callback_t callback +); + +/** + * Emit a system event, synchronously invoking every subscription registered for @a type + * (in subscription order) on the calling task before returning. + * @warning Does not work in ISR context. + * @param[in] type the event type + * @param[in] data optional pointer to the type-specific event struct (see SystemEventType); + * only valid for the duration of this call, subscribers must not retain it + * @param[in] data_len size of @a data in bytes (0 if @a data is NULL) + * @return ERROR_NONE on success + */ +error_t system_event_emit( + enum SystemEventType type, + const void* data, + size_t data_len +); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/time.h b/TactilityKernel/include/tactility/time.h index 94f24c0fa..58834354f 100644 --- a/TactilityKernel/include/tactility/time.h +++ b/TactilityKernel/include/tactility/time.h @@ -64,17 +64,17 @@ static inline TickType_t get_timeout_remaining_ticks(TickType_t timeout, TickTyp uint32_t kernel_get_tick_frequency(); /** @return the microseconds that have passed since boot */ -static inline int64_t get_micros_since_boot() { +static inline uint64_t get_micros_since_boot() { #ifdef ESP_PLATFORM return esp_timer_get_time(); #else struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) { - return ((int64_t)ts.tv_sec * 1000000LL) + (ts.tv_nsec / 1000); + return ((uint64_t)ts.tv_sec * 1000000LL) + (ts.tv_nsec / 1000); } struct timeval tv; gettimeofday(&tv, NULL); - return ((int64_t)tv.tv_sec * 1000000LL) + tv.tv_usec; + return ((uint64_t)tv.tv_sec * 1000000LL) + tv.tv_usec; #endif } diff --git a/TactilityKernel/source/filesystem/file_system.cpp b/TactilityKernel/source/filesystem/file_system.cpp index 453105abb..c97e0aeaf 100644 --- a/TactilityKernel/source/filesystem/file_system.cpp +++ b/TactilityKernel/source/filesystem/file_system.cpp @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 #include -#include -#include #include +#include #include +#include #include // Define the internal FileSystem structure @@ -78,11 +78,21 @@ void file_system_for_each(void* callback_context, bool (*callback)(FileSystem* f error_t file_system_mount(FileSystem* fs) { // Assuming 'device' is accessible or passed via a different mechanism // as it's required by the FileSystemApi signatures. - return fs->api->mount(fs->data); + auto result = fs->api->mount(fs->data); + if (result == ERROR_NONE) { + FileSystemMountedEvent mounted_event = { .file_system = fs }; + system_event_emit(KERNEL_EVENT_FILE_SYSTEM_MOUNTED, &mounted_event, sizeof(mounted_event)); + } + return result; } error_t file_system_unmount(FileSystem* fs) { - return fs->api->unmount(fs->data); + auto result = fs->api->unmount(fs->data); + if (result == ERROR_NONE) { + FileSystemUnmountedEvent unmounted_event = { .file_system = fs }; + system_event_emit(KERNEL_EVENT_FILE_SYSTEM_UNMOUNTED, &unmounted_event, sizeof(unmounted_event)); + } + return result; } bool file_system_is_mounted(FileSystem* fs) { diff --git a/TactilityKernel/source/service/service_manager.cpp b/TactilityKernel/source/service/service_manager.cpp index e0bb58186..648190b25 100644 --- a/TactilityKernel/source/service/service_manager.cpp +++ b/TactilityKernel/source/service/service_manager.cpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #include + +#include "tactility/system_event.h" + #include #include @@ -122,6 +125,8 @@ error_t service_manager_start(const char* id) { if (error == ERROR_NONE) { service_instance_set_state(instance, SERVICE_STATE_STARTED); + ServiceStartedEvent start_event = { .id = id }; + system_event_emit(KERNEL_EVENT_SERVICE_STARTED, &start_event, sizeof(start_event)); return ERROR_NONE; } @@ -165,6 +170,9 @@ error_t service_manager_stop(const char* id) { service_instance_destruct(instance); delete instance; + ServiceStoppedEvent stop_event = { .id = id }; + system_event_emit(KERNEL_EVENT_SERVICE_STOPPED, &stop_event, sizeof(stop_event)); + return ERROR_NONE; } diff --git a/TactilityKernel/source/system_event.cpp b/TactilityKernel/source/system_event.cpp new file mode 100644 index 000000000..5038e11a2 --- /dev/null +++ b/TactilityKernel/source/system_event.cpp @@ -0,0 +1,120 @@ +#include + +#include +#include +#include + +#include +#include +#include + +struct KernelEventSubscription { + SystemEventType type; + system_event_callback_t callback; + void* callback_context; +}; + +static std::vector subscriptions; + +// Mutex is constructed/destructed via a static-lifetime wrapper because struct Mutex +// itself has no constructor: mutex_lock() on an unconstructed handle is undefined +// behaviour (the raw QueueHandle_t would be null). +struct KernelEventMutex { + Mutex handle {}; + KernelEventMutex() { mutex_construct(&handle); } + ~KernelEventMutex() { mutex_destruct(&handle); } +}; + +static KernelEventMutex subscriptions_mutex; + +extern "C" { + +error_t system_event_subscribe( + SystemEventType type, + system_event_callback_t callback, + void* context +) { + mutex_lock(&subscriptions_mutex.handle); + subscriptions.push_back(KernelEventSubscription { type, callback, context }); + mutex_unlock(&subscriptions_mutex.handle); + + return ERROR_NONE; +} + +error_t system_event_unsubscribe( + SystemEventType type, + system_event_callback_t callback +) { + mutex_lock(&subscriptions_mutex.handle); + const auto iterator = std::ranges::find_if(subscriptions, [type, callback](const KernelEventSubscription& subscription) { + return subscription.type == type && subscription.callback == callback; + }); + error_t result = ERROR_NOT_FOUND; + if (iterator != subscriptions.end()) { + subscriptions.erase(iterator); + result = ERROR_NONE; + } + mutex_unlock(&subscriptions_mutex.handle); + + return result; +} + +error_t system_event_emit( + enum SystemEventType type, + const void* data, + size_t data_len +) { + SystemEvent event = { + .type = type, + .timestamp = get_micros_since_boot(), + .data = data, + .data_len = data_len, + }; + + // Snapshot matching subscriptions under the lock, then invoke after unlocking: a + // callback calling system_event_subscribe(), system_event_unsubscribe() 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. + // + // Nothing between mutex_lock() and mutex_unlock() below may throw. + // Count first, then use new(std::nothrow) to allocate the exact size and report + // failure through the return value instead; the fill loop below is then a plain + // assignment of a trivially-copyable struct, which cannot throw or reallocate. + + mutex_lock(&subscriptions_mutex.handle); + + size_t match_count = 0; + for (const auto& subscription : subscriptions) { + if (subscription.type == type) { + match_count++; + } + } + + KernelEventSubscription* matching = (match_count > 0) + ? new (std::nothrow) KernelEventSubscription[match_count] + : nullptr; + if (match_count > 0 && matching == nullptr) { + mutex_unlock(&subscriptions_mutex.handle); + return ERROR_OUT_OF_MEMORY; + } + + size_t matched_count = 0; + for (const auto& subscription : subscriptions) { + if (subscription.type == type) { + matching[matched_count++] = subscription; + } + } + + mutex_unlock(&subscriptions_mutex.handle); + + for (size_t i = 0; i < matched_count; i++) { + matching[i].callback(&event, matching[i].callback_context); + } + + delete[] matching; + + return ERROR_NONE; +} + +} // extern "C" diff --git a/Tests/TactilityKernel/Source/SystemEventTest.cpp b/Tests/TactilityKernel/Source/SystemEventTest.cpp new file mode 100644 index 000000000..c19f8759b --- /dev/null +++ b/Tests/TactilityKernel/Source/SystemEventTest.cpp @@ -0,0 +1,215 @@ +#include "doctest.h" + +#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 - +// covered below, mirroring DeviceListenerTest.cpp's reentrancy test. + +struct RecordedCall { + void* context; + SystemEventType type; + const void* data; + size_t data_len; + uint64_t timestamp; +}; + +static std::vector calls_a; +static std::vector calls_b; + +static void listener_a(SystemEvent* event, void* context) { + calls_a.push_back({ context, event->type, event->data, event->data_len, event->timestamp }); +} + +static void listener_b(SystemEvent* event, void* context) { + calls_b.push_back({ context, event->type, event->data, event->data_len, event->timestamp }); +} + +static void reset_calls() { + calls_a.clear(); + calls_b.clear(); +} + +TEST_CASE("system_event_emit invokes every subscriber registered for that type") { + reset_calls(); + 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_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0), ERROR_NONE); + + REQUIRE_EQ(calls_a.size(), 1); + CHECK_EQ(calls_a[0].context, &context_a); + CHECK_EQ(calls_a[0].type, KERNEL_EVENT_BOOT_COMPLETED); + + 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); +} + +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_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0); + CHECK_EQ(calls_a.size(), 0); + + system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); + CHECK_EQ(calls_a.size(), 1); + + system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a); +} + +TEST_CASE("system_event_emit passes the data pointer and length through unchanged") { + reset_calls(); + int context_a = 1; + struct Payload { int value; } payload { 42 }; + + system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a); + system_event_emit(KERNEL_EVENT_TIME_CHANGED, &payload, sizeof(payload)); + + REQUIRE_EQ(calls_a.size(), 1); + CHECK_EQ(calls_a[0].data, &payload); + 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); +} + +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_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); + + REQUIRE_EQ(calls_a.size(), 1); + CHECK_EQ(calls_a[0].data, nullptr); + CHECK_EQ(calls_a[0].data_len, 0); + + system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a); +} + +TEST_CASE("system_event_unsubscribe 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); + + CHECK_EQ(system_event_unsubscribe(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); +} + +TEST_CASE("system_event_unsubscribe 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); + + // 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); + + system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); + CHECK_EQ(calls_b.size(), 1); + + system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b); +} + +TEST_CASE("system_event_unsubscribe 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_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a); + + system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); + CHECK_EQ(calls_a.size(), 0); + + system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0); + CHECK_EQ(calls_a.size(), 1); + + system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, listener_a); +} + +TEST_CASE("system_event_emit with no subscribers for that type returns ERROR_NONE") { + CHECK_EQ(system_event_emit(KERNEL_EVENT_SERVICE_STOPPED, nullptr, 0), ERROR_NONE); +} + +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); + + auto before = static_cast(get_micros_since_boot()); + system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); + auto after = static_cast(get_micros_since_boot()); + + REQUIRE_EQ(calls_a.size(), 1); + CHECK_GE(calls_a[0].timestamp, before); + CHECK_LE(calls_a[0].timestamp, after); + + system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a); +} + +static bool reentrant_add_triggered = false; + +static void reentrant_listener(SystemEvent* event, void* context) { + calls_a.push_back({ context, event->type, event->data, event->data_len, event->timestamp }); + if (!reentrant_add_triggered) { + reentrant_add_triggered = true; + // 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); + // 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_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0); + } +} + +TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes and emits during notification") { + reset_calls(); + 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_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); + + // reentrant_listener unsubscribed itself and triggered a nested TIME_CHANGED emit, + // which the pre-existing listener_b subscription picks up. The listener_b + // subscription added *during* this round wasn't part of this round's snapshot, so it + // wasn't invoked for BOOT_COMPLETED yet. + CHECK_EQ(calls_a.size(), 1); + CHECK_EQ(calls_b.size(), 1); + + // A second BOOT_COMPLETED emit must not reach reentrant_listener again (it + // unsubscribed itself), but must reach the listener_b subscription added last round. + system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); + 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); +}