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.
This commit is contained in:
Ken Van Hoeylandt
2026-07-29 17:31:20 +02:00
committed by GitHub
parent 6e55e71e67
commit acb2f1d4c7
19 changed files with 589 additions and 190 deletions
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#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
+3 -3
View File
@@ -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
}
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
#include <algorithm>
#include <tactility/device.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/concurrent/recursive_mutex.h>
#include <tactility/device.h>
#include <tactility/filesystem/file_system.h>
#include <tactility/system_event.h>
#include <vector>
// 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) {
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/service/service_manager.h>
#include "tactility/system_event.h"
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
@@ -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;
}
+120
View File
@@ -0,0 +1,120 @@
#include <tactility/system_event.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
#include <tactility/time.h>
#include <algorithm>
#include <new>
#include <vector>
struct KernelEventSubscription {
SystemEventType type;
system_event_callback_t callback;
void* callback_context;
};
static std::vector<KernelEventSubscription> 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"