Refactor app loading and window management (#609)

This commit is contained in:
Ken Van Hoeylandt
2026-08-11 23:40:59 +02:00
committed by GitHub
parent dc3f6104b8
commit 37c507544b
243 changed files with 16034 additions and 10865 deletions
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
/**
* @brief key-value storage for general purpose.
* Maps strings on a fixed set of data types.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* A dictionary that maps keys (strings) onto several atomary types.
* Opaque handle - allocate with bundle_alloc(), release with bundle_free().
*/
typedef struct Bundle Bundle;
Bundle* bundle_alloc(void);
Bundle* bundle_clone(const Bundle* bundle);
void bundle_free(Bundle* bundle);
/** @warning Undefined if @a key is absent or not a bool - check with bundle_has_bool()/bundle_opt_bool() first. */
bool bundle_get_bool(const Bundle* bundle, const char* key);
/** @warning Undefined if @a key is absent or not an int32 - check with bundle_has_int32()/bundle_opt_int32() first. */
int32_t bundle_get_int32(const Bundle* bundle, const char* key);
/** @warning Undefined if @a key is absent or not an int64 - check with bundle_has_int64()/bundle_opt_int64() first. */
int64_t bundle_get_int64(const Bundle* bundle, const char* key);
/**
* @warning Undefined if @a key is absent or not a string - check with bundle_has_string()/bundle_opt_string() first.
* @retval ERROR_BUFFER_OVERFLOW out_value_size is too small
* @retval ERROR_NONE on success
*/
error_t bundle_get_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size);
bool bundle_has_bool(const Bundle* bundle, const char* key);
bool bundle_has_int32(const Bundle* bundle, const char* key);
bool bundle_has_int64(const Bundle* bundle, const char* key);
bool bundle_has_string(const Bundle* bundle, const char* key);
bool bundle_opt_bool(const Bundle* bundle, const char* key, bool* out_value);
bool bundle_opt_int32(const Bundle* bundle, const char* key, int32_t* out_value);
bool bundle_opt_int64(const Bundle* bundle, const char* key, int64_t* out_value);
/**
* @retval ERROR_NOT_FOUND @a key is absent or not a string - @a out_value is left untouched
* @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched
* @retval ERROR_NONE on success
*/
error_t bundle_opt_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size);
void bundle_put_bool(Bundle* bundle, const char* key, bool value);
void bundle_put_int32(Bundle* bundle, const char* key, int32_t value);
void bundle_put_int64(Bundle* bundle, const char* key, int64_t value);
void bundle_put_string(Bundle* bundle, const char* key, const char* value);
#ifdef __cplusplus
}
#endif
@@ -66,6 +66,17 @@ struct KeyboardApi {
* @retval ERROR_NOT_SUPPORTED when this device has no backlight
*/
error_t (*get_backlight)(struct Device* device, struct Device** backlight_device);
/**
* @brief Optional: reports whether the keyboard is physically present right now. Only
* meaningful for hot-pluggable/detachable keyboards (e.g. a removable accessory) whose
* kernel device is constructed and started once at boot regardless of physical attachment -
* leave NULL for a keyboard that's always physically present whenever its device is active
* (the common case; callers must treat NULL the same as "always present").
* @param[in] device the keyboard device
* @return true if physically attached/present
*/
bool (*is_present)(struct Device* device);
};
/**
@@ -83,6 +94,14 @@ error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data);
*/
error_t keyboard_get_backlight(struct Device* device, struct Device** backlight_device);
/**
* @brief Whether the keyboard device is physically present right now. True when the driver
* doesn't implement KeyboardApi::is_present (i.e. it's always physically present whenever its
* device is active) - see that field's doc comment.
* @param[in] device the keyboard device
*/
bool keyboard_is_present(struct Device* device);
extern const struct DeviceType KEYBOARD_TYPE;
#ifdef __cplusplus
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
/**
* @brief Key-value settings, persisted as a .properties file on disk (instead of NVS/in-memory).
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Opaque handle - open with preferences_open(), release with preferences_close().
*/
typedef struct Preferences Preferences;
/**
* Open (or create) a preferences store backed by the properties file at @a path. The parent
* directory is created (recursively, like mkdir -p) if it doesn't already exist. The file is
* read into memory now; changes made with preferences_put_*() are only written back to disk by
* preferences_close().
* @param[in] path absolute or relative file path (e.g. "/data/settings.properties")
* @return the new instance, or NULL if the parent directory couldn't be created, or on
* allocation failure
*/
Preferences* preferences_open(const char* path);
/** Writes any pending preferences_put_*() changes to the backing file, then releases the
* instance. */
void preferences_close(Preferences* preferences);
bool preferences_has_bool(const Preferences* preferences, const char* key);
bool preferences_has_int32(const Preferences* preferences, const char* key);
bool preferences_has_int64(const Preferences* preferences, const char* key);
bool preferences_has_string(const Preferences* preferences, const char* key);
bool preferences_opt_bool(const Preferences* preferences, const char* key, bool* out_value);
bool preferences_opt_int32(const Preferences* preferences, const char* key, int32_t* out_value);
bool preferences_opt_int64(const Preferences* preferences, const char* key, int64_t* out_value);
/**
* @retval ERROR_NOT_FOUND @a key is absent or not a string - @a out_value is left untouched
* @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched
* @retval ERROR_NONE on success
*/
error_t preferences_opt_string(const Preferences* preferences, const char* key, char* out_value, size_t out_value_size);
/** Sets the value in the in-memory cache; only persisted to the backing file by
* preferences_close(). */
void preferences_put_bool(Preferences* preferences, const char* key, bool value);
void preferences_put_int32(Preferences* preferences, const char* key, int32_t value);
void preferences_put_int64(Preferences* preferences, const char* key, int64_t value);
void preferences_put_string(Preferences* preferences, const char* key, const char* value);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
/**
* @brief Generic string key-value ".properties" file.
* @note Safely acquires/releases the filesystem mutex registered for the file's path (see
* tactility/filesystem/file_mutex.h) - manual locking isn't needed.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Opaque handle - open with properties_file_open(), release with properties_file_close().
*/
typedef struct PropertiesFile PropertiesFile;
/**
* Open (or create) a properties file at @a path. The file is read into memory now; changes
* made with properties_file_set() are only written back to disk by properties_file_close().
* @param[in] path absolute or relative file path (e.g. "/data/settings.properties") - the
* parent directory must already exist
* @return the new instance, or NULL on allocation failure, or NULL if @a path exists but a
* genuine error prevented opening or reading it, e.g. a permissions error (a missing file is
* not an error - the instance starts out empty in that case)
*/
PropertiesFile* properties_file_open(const char* path);
/**
* Writes any pending properties_file_set() changes to the backing file (atomically - via a
* temporary file in the same directory, renamed over the real path - so a write failure leaves
* the previous on-disk content untouched rather than a truncated/partial file), then releases
* the instance either way.
* @retval ERROR_NONE the backing file was fully updated
* @retval ERROR_RESOURCE writing failed (full filesystem, I/O error, ...) - the previous
* on-disk content, if any, is unchanged; the in-memory changes are lost along with the instance
*/
error_t properties_file_close(PropertiesFile* file);
bool properties_file_has(const PropertiesFile* file, const char* key);
/**
* @retval ERROR_NOT_FOUND @a key is absent - @a out_value is left untouched
* @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched
* @retval ERROR_NONE on success
*/
error_t properties_file_get(const PropertiesFile* file, const char* key, char* out_value, size_t out_value_size);
/** Sets the value in the in-memory cache; only persisted to the backing file by
* properties_file_close(). */
void properties_file_set(PropertiesFile* file, const char* key, const char* value);
typedef void (*PropertiesFileVisitorFn)(const char* key, const char* value, void* context);
/** Invokes @a visitor for every key currently cached, in unspecified order. */
void properties_file_for_each(const PropertiesFile* file, PropertiesFileVisitorFn visitor, void* context);
#ifdef __cplusplus
}
#endif
+151 -20
View File
@@ -1,10 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h>
#include <tactility/freertos/task.h>
#ifdef __cplusplus
extern "C" {
@@ -25,20 +29,6 @@ enum SystemEventType {
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;
@@ -71,9 +61,29 @@ struct ServiceStoppedEvent {
const char* id;
};
/** Size of the largest type-specific event struct documented in SystemEventType, i.e. the
* embedded buffer size needed by SystemEvent/SystemEventSubscription to hold any event's
* payload by value. */
#define SYSTEM_EVENT_MAX_DATA_SIZE (sizeof(struct NetworkConnectedEvent))
/**
* A system-wide event as delivered to a system_event_callback_t.
* `data` (up to `data_len` bytes, `SYSTEM_EVENT_MAX_DATA_SIZE` max) is a by-value copy of the
* type-specific struct documented next to `type`'s enum value in SystemEventType (or unused,
* with `data_len` 0, when none is documented) - like SystemEventSubscription's `data`, but only
* valid for the duration of the callback rather than for the subscription's lifetime.
*/
struct SystemEvent {
enum SystemEventType type;
/** Microseconds since boot, from get_micros_since_boot(). */
uint64_t timestamp;
uint8_t data[SYSTEM_EVENT_MAX_DATA_SIZE];
size_t data_len;
};
/**
* @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 +92,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 +101,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 +110,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 +135,127 @@ 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))
/**
* Poll subscription: caller-owned node, registered with system_event_subscribe()
* and polled with system_event_await(). Unlike system_event_callback_t, `event` is a by-value
* copy that remains valid for the subscription's lifetime (until the next matching event
* overwrites it), not just for the duration of a callback.
* @warning Must be zero-initialized before the first system_event_subscribe() call (e.g.
* `SystemEventSubscription sub = {};` in C++, `SystemEventSubscription sub = {0};` in C, or
* static/global storage) - system_event_subscribe() reads `internal.unsubscribe_in_progress`
* before it writes it, to detect reuse of a node still being torn down by a concurrent
* system_event_unsubscribe() call; on indeterminate (non-zeroed) storage that read is undefined
* behavior. Not required again for a later system_event_subscribe() reusing the same node after
* system_event_unsubscribe() - the fields it depends on are fully owned/maintained by this API
* from the first successful registration onward.
*/
struct SystemEventSubscription {
/** `event.type` is the event type to subscribe to; set by the caller before
* system_event_subscribe(). The rest of `event` (timestamp/data/data_len) is populated by
* each matching system_event_emit() - see the @warning above. */
struct SystemEvent event;
/** Implementation-only bookkeeping; do not read or write directly. */
struct {
/** Own wakeup signal, not the subscribing task's shared default notification value - a
* task with more than one poll subscription would otherwise have events for one
* subscription wake (and consume the notification meant for) system_event_await()
* calls on another. */
SemaphoreHandle_t semaphore;
uint32_t sequence;
uint32_t consumed_sequence;
/** Number of tasks currently blocked in system_event_await() on `semaphore` -
* system_event_unsubscribe() waits for this to reach 0 before deleting it, since
* FreeRTOS requires no task be blocked on a semaphore when it's deleted. */
int waiter_count;
/** Set by system_event_unsubscribe() before it gives `semaphore` and waits, so a task
* already blocked in system_event_await() bails out (ERROR_INVALID_STATE) instead of
* waiting out its full timeout. Reset once system_event_unsubscribe() finishes
* draining old awaiters (see unsubscribe_in_progress) - not simply "on the next
* system_event_subscribe()", so a fresh registration can never observe a stale `true`
* left over from an unsubscribe that hasn't returned yet. */
bool cancelled;
/** True from the moment system_event_unsubscribe() unlinks `sub` until it has finished
* draining old awaiters and deleted the old semaphore. system_event_subscribe() spins
* until this clears before reusing `sub` - otherwise a new registration could reset
* waiter_count/cancelled (both shared with the old registration, there being only one
* `sub`) out from under the old system_event_unsubscribe() call still relying on them,
* or hand out a new semaphore for that same call to then promptly delete instead of the
* old one, while an old awaiter is still blocked on the real old semaphore. */
bool unsubscribe_in_progress;
struct SystemEventSubscription* next;
} internal;
};
/**
* Register a poll subscription for events of @a sub->type.
* @warning Does not work in ISR context.
* @warning On its very first call for a given @a sub, @a sub must have been zero-initialized -
* see SystemEventSubscription's @warning.
* @warning If @a sub was just passed to system_event_unsubscribe() (e.g. reusing a node for a
* new registration) and that call hasn't returned yet on another task, this call blocks
* (briefly - not for the full duration of anyone's timeout) until it does, before registering -
* see SystemEventSubscription::internal.unsubscribe_in_progress.
* @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
* @retval ERROR_NONE on success
* @retval ERROR_OUT_OF_MEMORY failed to allocate the subscription's wakeup semaphore; @a sub
* was not registered
* @retval ERROR_INVALID_STATE @a sub is already registered
*/
error_t system_event_subscribe(struct SystemEventSubscription* sub);
/**
* Remove a previously registered poll subscription.
* @warning Does not work in ISR context.
* @warning Blocks (briefly - not for the full duration of anyone's timeout) until any task
* currently blocked in system_event_await() on @a sub has woken up and left, so it's safe to
* delete the subscription's semaphore before this call returns. A blocked awaiter is woken
* (with ERROR_INVALID_STATE) as part of this call rather than left to time out on its own.
* A concurrent system_event_subscribe() reusing the same @a sub waits out this same window
* (see system_event_subscribe()'s @warning) rather than racing it.
* @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.
* @warning Poll subscriptions coalesce to the latest event, they are not a queue: if
* system_event_emit() is called more than once for @a sub->event.type between two
* system_event_await() calls, only the most recent event's data/timestamp is visible via
* system_event_get_data()/system_event_get_timestamp() afterward - intermediate events are
* silently overwritten, never delivered. Use system_event_callback_add() instead if every
* individual event matters.
* @warning Cannot be called concurrently from different tasks. Each tasks must have its own subscription.
* @param[in,out] sub subscription to wait on, as passed to system_event_subscribe()
* @param[in] timeout max ticks to wait
* @retval ERROR_NONE an event arrived
* @retval ERROR_TIMEOUT @a timeout elapsed first
* @retval ERROR_INVALID_STATE another task called system_event_unsubscribe() on @a sub while
* this call was blocked
*/
error_t system_event_await(struct SystemEventSubscription* sub, TickType_t timeout);
/**
* Copies @a sub's current event payload (the data from the most recent system_event_emit()
* that reached it) into @a data.
* @param[in] sub subscription to read the payload from, as passed to system_event_subscribe()
* @param[out] data buffer to copy the payload into
* @param[in] data_len size of @a data
* @retval ERROR_NONE on success
* @retval ERROR_BUFFER_OVERFLOW @a data_len is smaller than the stored payload - @a data is
* left untouched
*/
error_t system_event_get_data(struct SystemEventSubscription* sub, uint8_t* data, size_t data_len);
#ifdef __cplusplus
}
#endif
+152
View File
@@ -0,0 +1,152 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/bundle.h>
#include <cstring>
#include <new>
#include <string>
#include <unordered_map>
namespace {
enum class Type {
Bool,
Int32,
Int64,
String,
};
struct Value {
Type type;
union {
bool value_bool;
int32_t value_int32;
int64_t value_int64;
};
std::string value_string;
};
} // namespace
// Definition of the opaque handle declared in tactility/bundle.h - C callers only ever see it
// through a Bundle* pointer, never its members.
struct Bundle {
std::unordered_map<std::string, Value> entries;
};
extern "C" {
Bundle* bundle_alloc(void) {
return new (std::nothrow) Bundle();
}
Bundle* bundle_clone(const Bundle* bundle) {
auto* clone = new (std::nothrow) Bundle();
if (clone == nullptr) {
return nullptr;
}
clone->entries = bundle->entries;
return clone;
}
void bundle_free(Bundle* bundle) {
delete bundle;
}
bool bundle_get_bool(const Bundle* bundle, const char* key) {
return bundle->entries.find(key)->second.value_bool;
}
int32_t bundle_get_int32(const Bundle* bundle, const char* key) {
return bundle->entries.find(key)->second.value_int32;
}
int64_t bundle_get_int64(const Bundle* bundle, const char* key) {
return bundle->entries.find(key)->second.value_int64;
}
error_t bundle_get_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size) {
const std::string& value = bundle->entries.find(key)->second.value_string;
if (value.size() + 1 > out_value_size) {
return ERROR_BUFFER_OVERFLOW;
}
std::memcpy(out_value, value.c_str(), value.size() + 1);
return ERROR_NONE;
}
bool bundle_has_bool(const Bundle* bundle, const char* key) {
auto entry = bundle->entries.find(key);
return entry != bundle->entries.end() && entry->second.type == Type::Bool;
}
bool bundle_has_int32(const Bundle* bundle, const char* key) {
auto entry = bundle->entries.find(key);
return entry != bundle->entries.end() && entry->second.type == Type::Int32;
}
bool bundle_has_int64(const Bundle* bundle, const char* key) {
auto entry = bundle->entries.find(key);
return entry != bundle->entries.end() && entry->second.type == Type::Int64;
}
bool bundle_has_string(const Bundle* bundle, const char* key) {
auto entry = bundle->entries.find(key);
return entry != bundle->entries.end() && entry->second.type == Type::String;
}
bool bundle_opt_bool(const Bundle* bundle, const char* key, bool* out_value) {
auto entry = bundle->entries.find(key);
if (entry != bundle->entries.end() && entry->second.type == Type::Bool) {
*out_value = entry->second.value_bool;
return true;
}
return false;
}
bool bundle_opt_int32(const Bundle* bundle, const char* key, int32_t* out_value) {
auto entry = bundle->entries.find(key);
if (entry != bundle->entries.end() && entry->second.type == Type::Int32) {
*out_value = entry->second.value_int32;
return true;
}
return false;
}
bool bundle_opt_int64(const Bundle* bundle, const char* key, int64_t* out_value) {
auto entry = bundle->entries.find(key);
if (entry != bundle->entries.end() && entry->second.type == Type::Int64) {
*out_value = entry->second.value_int64;
return true;
}
return false;
}
error_t bundle_opt_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size) {
auto entry = bundle->entries.find(key);
if (entry == bundle->entries.end() || entry->second.type != Type::String) {
return ERROR_NOT_FOUND;
}
const std::string& value = entry->second.value_string;
if (value.size() + 1 > out_value_size) {
return ERROR_BUFFER_OVERFLOW;
}
std::memcpy(out_value, value.c_str(), value.size() + 1);
return ERROR_NONE;
}
void bundle_put_bool(Bundle* bundle, const char* key, bool value) {
bundle->entries[key] = Value { .type = Type::Bool, .value_bool = value, .value_string = "" };
}
void bundle_put_int32(Bundle* bundle, const char* key, int32_t value) {
bundle->entries[key] = Value { .type = Type::Int32, .value_int32 = value, .value_string = "" };
}
void bundle_put_int64(Bundle* bundle, const char* key, int64_t value) {
bundle->entries[key] = Value { .type = Type::Int64, .value_int64 = value, .value_string = "" };
}
void bundle_put_string(Bundle* bundle, const char* key, const char* value) {
bundle->entries[key] = Value { .type = Type::String, .value_bool = false, .value_string = value };
}
} // extern "C"
@@ -28,6 +28,16 @@ error_t keyboard_get_backlight(Device* device, Device** backlight_device) {
return KEYBOARD_DRIVER_API(driver)->get_backlight(device, backlight_device);
}
bool keyboard_is_present(Device* device) {
const auto* driver = device_get_driver(device);
if (KEYBOARD_DRIVER_API(driver)->is_present == nullptr) {
return true;
}
return KEYBOARD_DRIVER_API(driver)->is_present(device);
}
const DeviceType KEYBOARD_TYPE {
.name = "keyboard"
};
+3 -3
View File
@@ -8,7 +8,7 @@
#include <cstdio>
#include <cstring>
static error_t get_user_data_root_path(char* out_path, size_t out_path_size) {
static error_t paths_get_user_data_root_path(char* out_path, size_t out_path_size) {
#if defined(CONFIG_TT_USER_DATA_LOCATION_INTERNAL)
#ifdef ESP_PLATFORM
const char* mount_point = "/data";
@@ -21,7 +21,7 @@ static error_t get_user_data_root_path(char* out_path, size_t out_path_size) {
std::strcpy(out_path, mount_point);
return ERROR_NONE;
#elif defined(CONFIG_TT_USER_DATA_LOCATION_SD)
struct FileSystem* found = nullptr;
FileSystem* found = nullptr;
file_system_for_each(&found, [](FileSystem* fs, void* context) {
auto* owner = file_system_get_owner(fs);
if (owner == nullptr || device_get_type(owner) != &SDCARD_TYPE) {
@@ -44,7 +44,7 @@ extern "C" {
error_t paths_get_user_data_path(char* out_path, size_t out_path_size) {
#ifdef ESP_PLATFORM
char root[64];
error_t error = get_user_data_root_path(root, sizeof(root));
error_t error = paths_get_user_data_root_path(root, sizeof(root));
if (error != ERROR_NONE) {
return error;
}
+288
View File
@@ -0,0 +1,288 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/preferences.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <tactility/properties_file.h>
#include <cerrno>
#include <cinttypes>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <new>
#include <string>
#include <sys/stat.h>
#include <vector>
namespace {
constexpr auto* TAG = "preferences";
// Escapes '\\' and '\n' so a string value can never break properties_file's one-entry-per-line
// on-disk format, regardless of its content.
std::string escape(const std::string& value) {
std::string result;
result.reserve(value.size());
for (char c : value) {
if (c == '\\') {
result += "\\\\";
} else if (c == '\n') {
result += "\\n";
} else {
result += c;
}
}
return result;
}
std::string unescape(const std::string& value) {
std::string result;
result.reserve(value.size());
for (size_t i = 0; i < value.size(); i++) {
if (value[i] == '\\' && i + 1 < value.size()) {
i++;
result += (value[i] == 'n') ? '\n' : value[i];
} else {
result += value[i];
}
}
return result;
}
// Splits a tagged value ("b:1", "i32:42", "i64:123", "s:escaped text") into its type tag and
// raw payload. Returns false if there's no ':' separator (malformed/missing).
bool split_tag(const std::string& tagged_value, std::string& tag, std::string& raw_value) {
size_t colon = tagged_value.find(':');
if (colon == std::string::npos) {
return false;
}
tag = tagged_value.substr(0, colon);
raw_value = tagged_value.substr(colon + 1);
return true;
}
// Rejects anything but an exact "0" or "1" - a manually edited or corrupted properties file
// could otherwise have e.g. "b:garbage" silently read back as false.
bool parse_bool(const std::string& raw_value, bool& out) {
if (raw_value == "0") {
out = false;
return true;
}
if (raw_value == "1") {
out = true;
return true;
}
return false;
}
// strtol()'s own error signaling (errno/end pointer) is easy to get wrong by omission: called
// naively, it silently accepts trailing garbage ("42abc"), out-of-range input (clamped to
// LONG_MIN/LONG_MAX instead of failing), and - since `long` can be wider than int32_t (e.g. on
// the posix simulator, where `long` is 64-bit) - a value that overflows int32_t but not `long`
// would silently truncate on the narrowing cast instead of being rejected.
bool parse_int32(const std::string& raw_value, int32_t& out) {
if (raw_value.empty()) {
return false;
}
errno = 0;
char* end = nullptr;
long parsed = std::strtol(raw_value.c_str(), &end, 10);
if (errno == ERANGE || end != raw_value.c_str() + raw_value.size()) {
return false;
}
if (parsed < INT32_MIN || parsed > INT32_MAX) {
return false;
}
out = static_cast<int32_t>(parsed);
return true;
}
// See parse_int32() - same reasoning, with strtoll()/`long long`/int64_t.
bool parse_int64(const std::string& raw_value, int64_t& out) {
if (raw_value.empty()) {
return false;
}
errno = 0;
char* end = nullptr;
long long parsed = std::strtoll(raw_value.c_str(), &end, 10);
if (errno == ERANGE || end != raw_value.c_str() + raw_value.size()) {
return false;
}
if (parsed < INT64_MIN || parsed > INT64_MAX) {
return false;
}
out = static_cast<int64_t>(parsed);
return true;
}
bool ensure_directory(const std::string& path) {
struct stat info {};
if (stat(path.c_str(), &info) == 0) {
return (info.st_mode & S_IFMT) == S_IFDIR;
}
return mkdir(path.c_str(), 0777) == 0;
}
// mkdir -p.
bool ensure_directory_recursive(const std::string& path) {
for (size_t index = path.find('/', 1); index != std::string::npos; index = path.find('/', index + 1)) {
if (!ensure_directory(path.substr(0, index))) {
return false;
}
}
return ensure_directory(path);
}
// "" if @a path has no directory component (e.g. a bare filename) - nothing to create in that
// case, the current/root directory already exists.
std::string parent_directory(const std::string& path) {
size_t slash = path.find_last_of('/');
if (slash == std::string::npos) {
return "";
}
return path.substr(0, slash);
}
} // namespace
// Definition of the opaque handle declared in tactility/preferences.h - C callers only ever
// see it through a Preferences* pointer, never its members. Backed by a PropertiesFile
// (tactility/properties_file.h) rather than its own file I/O - each value is stored as a
// tagged string ("b:1", "i32:42", "i64:123", "s:escaped text") since PropertiesFile only knows
// about plain strings.
struct Preferences {
PropertiesFile* file;
};
namespace {
// Grow-and-retry: properties_file_get() needs a bounded buffer, and a string preference's
// value (unlike bool/int32/int64's short encodings) can be arbitrarily long.
bool try_get_tagged(const PropertiesFile* file, const char* key, std::string& tag, std::string& raw_value) {
std::vector<char> buffer(32);
while (true) {
error_t error = properties_file_get(file, key, buffer.data(), buffer.size());
if (error == ERROR_NONE) {
return split_tag(std::string(buffer.data()), tag, raw_value);
}
if (error == ERROR_NOT_FOUND) {
return false;
}
buffer.resize(buffer.size() * 2);
}
}
} // namespace
extern "C" {
Preferences* preferences_open(const char* path) {
std::string directory = parent_directory(path);
if (!directory.empty() && !ensure_directory_recursive(directory)) {
LOG_E(TAG, "Directory not found: %s", directory.c_str());
return nullptr;
}
PropertiesFile* file = properties_file_open(path);
if (file == nullptr) {
LOG_E(TAG, "Failed to open %s", path);
return nullptr;
}
auto* preferences = new (std::nothrow) Preferences { file };
if (preferences == nullptr) {
LOG_E(TAG, "Out of memory");
properties_file_close(file);
return nullptr;
}
return preferences;
}
void preferences_close(Preferences* preferences) {
properties_file_close(preferences->file);
delete preferences;
}
bool preferences_has_bool(const Preferences* preferences, const char* key) {
std::string tag, raw_value;
bool value;
return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "b" && parse_bool(raw_value, value);
}
bool preferences_has_int32(const Preferences* preferences, const char* key) {
std::string tag, raw_value;
int32_t value;
return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "i32" && parse_int32(raw_value, value);
}
bool preferences_has_int64(const Preferences* preferences, const char* key) {
std::string tag, raw_value;
int64_t value;
return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "i64" && parse_int64(raw_value, value);
}
bool preferences_has_string(const Preferences* preferences, const char* key) {
std::string tag, raw_value;
return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "s";
}
bool preferences_opt_bool(const Preferences* preferences, const char* key, bool* out_value) {
std::string tag, raw_value;
if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "b") {
return false;
}
return parse_bool(raw_value, *out_value);
}
bool preferences_opt_int32(const Preferences* preferences, const char* key, int32_t* out_value) {
std::string tag, raw_value;
if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "i32") {
return false;
}
return parse_int32(raw_value, *out_value);
}
bool preferences_opt_int64(const Preferences* preferences, const char* key, int64_t* out_value) {
std::string tag, raw_value;
if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "i64") {
return false;
}
return parse_int64(raw_value, *out_value);
}
error_t preferences_opt_string(const Preferences* preferences, const char* key, char* out_value, size_t out_value_size) {
std::string tag, raw_value;
if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "s") {
return ERROR_NOT_FOUND;
}
std::string value = unescape(raw_value);
if (value.size() + 1 > out_value_size) {
return ERROR_BUFFER_OVERFLOW;
}
std::memcpy(out_value, value.c_str(), value.size() + 1);
return ERROR_NONE;
}
void preferences_put_bool(Preferences* preferences, const char* key, bool value) {
properties_file_set(preferences->file, key, value ? "b:1" : "b:0");
}
void preferences_put_int32(Preferences* preferences, const char* key, int32_t value) {
char buffer[32];
std::snprintf(buffer, sizeof(buffer), "i32:%" PRId32, value);
properties_file_set(preferences->file, key, buffer);
}
void preferences_put_int64(Preferences* preferences, const char* key, int64_t value) {
char buffer[40];
std::snprintf(buffer, sizeof(buffer), "i64:%" PRId64, value);
properties_file_set(preferences->file, key, buffer);
}
void preferences_put_string(Preferences* preferences, const char* key, const char* value) {
std::string tagged = "s:" + escape(value);
properties_file_set(preferences->file, key, tagged.c_str());
}
} // extern "C"
+218
View File
@@ -0,0 +1,218 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/properties_file.h>
#include <tactility/filesystem/file_mutex.h>
#include <tactility/log.h>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <new>
#include <string>
#include <unordered_map>
constexpr auto* TAG = "properties_file";
namespace {
std::string trim(const std::string& value, const char* chars) {
size_t start = value.find_first_not_of(chars);
if (start == std::string::npos) {
return "";
}
size_t end = value.find_last_not_of(chars);
return value.substr(start, end - start + 1);
}
bool split_key_value(const std::string& line, std::string& key, std::string& value) {
size_t index = line.find('=');
if (index == std::string::npos) {
return false;
}
key = line.substr(0, index);
value = line.substr(index + 1);
return true;
}
} // namespace
// Definition of the opaque handle declared in tactility/properties_file.h - C callers only
// ever see it through a PropertiesFile* pointer, never its members.
struct PropertiesFile {
std::string path;
std::unordered_map<std::string, std::string> entries;
};
namespace {
// Missing file is not an error - a fresh instance just starts out empty and gets created on
// close(). Mirrors Tactility's loadPropertiesFile(): "#"-prefixed and blank lines are skipped;
// a "[section]" line becomes a literal prefix (verbatim, brackets included) prepended to every
// subsequent key, until the next "[section]" line replaces it.
// @return false if the file exists but a genuine I/O error interrupted opening or reading it
// (fgetc()'s EOF return doesn't by itself distinguish clean end-of-file from a read error -
// ferror() after the loop does); true otherwise, including for a missing file (ENOENT).
bool load_from_file(PropertiesFile* file) {
FileMutex mutex {};
file_mutex_get(&mutex, file->path.c_str());
file_mutex_lock(&mutex);
FILE* handle = std::fopen(file->path.c_str(), "r");
if (handle == nullptr) {
const int open_error = errno;
file_mutex_unlock(&mutex);
if (open_error == ENOENT) {
return true;
}
LOG_E(TAG, "Failed to open %s", file->path.c_str());
return false;
}
std::string key_prefix;
std::string raw_line;
uint32_t line_number = 0;
auto flush_line = [&]() {
line_number++;
std::string trimmed_line = trim(raw_line, " \t\r\n");
raw_line.clear();
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
return;
}
if (trimmed_line.starts_with("[")) {
key_prefix = trimmed_line;
return;
}
std::string key, value;
if (!split_key_value(trimmed_line, key, value)) {
LOG_E(TAG, "Failed to parse line %u of %s (skipped)", line_number, file->path.c_str());
return;
}
file->entries[key_prefix + trim(key, " \t")] = trim(value, " \t");
};
int c;
while ((c = std::fgetc(handle)) != EOF) {
if (c == '\n') {
flush_line();
} else {
raw_line += static_cast<char>(c);
}
}
flush_line();
bool read_ok = std::ferror(handle) == 0;
std::fclose(handle);
file_mutex_unlock(&mutex);
if (!read_ok) {
LOG_E(TAG, "Failed to read %s", file->path.c_str());
}
return read_ok;
}
// Writes to a temporary file in the same directory, then atomically replaces the real path -
// opening the real path directly with "w" would truncate it immediately, so any failure
// partway through (full filesystem, I/O error, a reset before close) would discard the
// previously-good content instead of leaving it intact. Same directory so rename() stays on one
// filesystem, which is what makes it atomic.
// @return true if the backing file was fully replaced with the current entries; false (leaving
// the previous on-disk content untouched) if any step failed.
bool save_to_file(const PropertiesFile* file) {
FileMutex mutex {};
file_mutex_get(&mutex, file->path.c_str());
file_mutex_lock(&mutex);
std::string temp_path = file->path + ".tmp";
FILE* handle = std::fopen(temp_path.c_str(), "w");
if (handle == nullptr) {
LOG_E(TAG, "Failed to open %s", temp_path.c_str());
file_mutex_unlock(&mutex);
return false;
}
for (const auto& [key, value] : file->entries) {
std::fprintf(handle, "%s=%s\n", key.c_str(), value.c_str());
}
// Order matters: ferror()/fflush() need the still-open handle, fclose() consumes it.
bool write_ok = std::ferror(handle) == 0;
bool flush_ok = std::fflush(handle) == 0;
bool close_ok = std::fclose(handle) == 0;
if (!write_ok || !flush_ok || !close_ok) {
LOG_E(TAG, "Failed to write %s", temp_path.c_str());
std::remove(temp_path.c_str());
file_mutex_unlock(&mutex);
return false;
}
// rename() may not overwrite an existing destination on some filesystems (e.g. FAT on
// ESP32), so remove it first; this is best-effort and ignored if the path doesn't exist yet.
std::remove(file->path.c_str());
if (std::rename(temp_path.c_str(), file->path.c_str()) != 0) {
LOG_E(TAG, "Failed to replace %s", file->path.c_str());
std::remove(temp_path.c_str());
file_mutex_unlock(&mutex);
return false;
}
file_mutex_unlock(&mutex);
return true;
}
} // namespace
extern "C" {
PropertiesFile* properties_file_open(const char* path) {
auto* file = new (std::nothrow) PropertiesFile();
if (file == nullptr) {
return nullptr;
}
file->path = path;
if (!load_from_file(file)) {
delete file;
return nullptr;
}
return file;
}
error_t properties_file_close(PropertiesFile* file) {
bool saved = save_to_file(file);
delete file;
return saved ? ERROR_NONE : ERROR_RESOURCE;
}
bool properties_file_has(const PropertiesFile* file, const char* key) {
return file->entries.contains(key);
}
error_t properties_file_get(const PropertiesFile* file, const char* key, char* out_value, size_t out_value_size) {
auto entry = file->entries.find(key);
if (entry == file->entries.end()) {
return ERROR_NOT_FOUND;
}
const std::string& value = entry->second;
if (value.size() + 1 > out_value_size) {
return ERROR_BUFFER_OVERFLOW;
}
std::memcpy(out_value, value.c_str(), value.size() + 1);
return ERROR_NONE;
}
void properties_file_set(PropertiesFile* file, const char* key, const char* value) {
file->entries[key] = value;
}
void properties_file_for_each(const PropertiesFile* file, PropertiesFileVisitorFn visitor, void* context) {
for (const auto& [key, value] : file->entries) {
visitor(key.c_str(), value.c_str(), context);
}
}
} // extern "C"
+48
View File
@@ -1,3 +1,4 @@
#include <tactility/bundle.h>
#include <tactility/concurrent/dispatcher.h>
#include <tactility/concurrent/event_group.h>
#include <tactility/concurrent/thread.h>
@@ -41,6 +42,9 @@
#include <tactility/filesystem/file_system.h>
#include <tactility/memory.h>
#include <tactility/module.h>
#include <tactility/paths.h>
#include <tactility/preferences.h>
#include <tactility/properties_file.h>
#include <tactility/wifi_auto_scan.h>
#ifndef ESP_PLATFORM
@@ -240,6 +244,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
// drivers/keyboard
DEFINE_MODULE_SYMBOL(keyboard_read_key),
DEFINE_MODULE_SYMBOL(KEYBOARD_TYPE),
// drivers/paths
DEFINE_MODULE_SYMBOL(paths_get_user_data_path),
// drivers/pointer
DEFINE_MODULE_SYMBOL(pointer_enter_sleep),
DEFINE_MODULE_SYMBOL(pointer_exit_sleep),
@@ -471,6 +477,48 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(module_is_started),
DEFINE_MODULE_SYMBOL(module_resolve_symbol),
DEFINE_MODULE_SYMBOL(module_resolve_symbol_global),
// bundle
DEFINE_MODULE_SYMBOL(bundle_alloc),
DEFINE_MODULE_SYMBOL(bundle_clone),
DEFINE_MODULE_SYMBOL(bundle_free),
DEFINE_MODULE_SYMBOL(bundle_get_bool),
DEFINE_MODULE_SYMBOL(bundle_get_int32),
DEFINE_MODULE_SYMBOL(bundle_get_int64),
DEFINE_MODULE_SYMBOL(bundle_get_string),
DEFINE_MODULE_SYMBOL(bundle_has_bool),
DEFINE_MODULE_SYMBOL(bundle_has_int32),
DEFINE_MODULE_SYMBOL(bundle_has_int64),
DEFINE_MODULE_SYMBOL(bundle_has_string),
DEFINE_MODULE_SYMBOL(bundle_opt_bool),
DEFINE_MODULE_SYMBOL(bundle_opt_int32),
DEFINE_MODULE_SYMBOL(bundle_opt_int64),
DEFINE_MODULE_SYMBOL(bundle_opt_string),
DEFINE_MODULE_SYMBOL(bundle_put_bool),
DEFINE_MODULE_SYMBOL(bundle_put_int32),
DEFINE_MODULE_SYMBOL(bundle_put_int64),
DEFINE_MODULE_SYMBOL(bundle_put_string),
// preferences
DEFINE_MODULE_SYMBOL(preferences_open),
DEFINE_MODULE_SYMBOL(preferences_close),
DEFINE_MODULE_SYMBOL(preferences_has_bool),
DEFINE_MODULE_SYMBOL(preferences_has_int32),
DEFINE_MODULE_SYMBOL(preferences_has_int64),
DEFINE_MODULE_SYMBOL(preferences_has_string),
DEFINE_MODULE_SYMBOL(preferences_opt_bool),
DEFINE_MODULE_SYMBOL(preferences_opt_int32),
DEFINE_MODULE_SYMBOL(preferences_opt_int64),
DEFINE_MODULE_SYMBOL(preferences_opt_string),
DEFINE_MODULE_SYMBOL(preferences_put_bool),
DEFINE_MODULE_SYMBOL(preferences_put_int32),
DEFINE_MODULE_SYMBOL(preferences_put_int64),
DEFINE_MODULE_SYMBOL(preferences_put_string),
// properties_file
DEFINE_MODULE_SYMBOL(properties_file_open),
DEFINE_MODULE_SYMBOL(properties_file_close),
DEFINE_MODULE_SYMBOL(properties_file_has),
DEFINE_MODULE_SYMBOL(properties_file_get),
DEFINE_MODULE_SYMBOL(properties_file_set),
DEFINE_MODULE_SYMBOL(properties_file_for_each),
// terminator
MODULE_SYMBOL_TERMINATOR
};
+235 -13
View File
@@ -1,10 +1,12 @@
#include <tactility/system_event.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/delay.h>
#include <tactility/error.h>
#include <tactility/time.h>
#include <algorithm>
#include <cstring>
#include <new>
#include <vector>
@@ -27,9 +29,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 +50,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 +68,38 @@ 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 signals its wakeup semaphore.
// Held entirely under the lock: unlike the callback path, this never invokes caller code
// (just a memcpy and a semaphore give), 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->internal.next) {
if (sub->event.type == type) {
sub->event.timestamp = timestamp;
const size_t copied_len = std::min(data_len, SYSTEM_EVENT_MAX_DATA_SIZE);
if (copied_len > 0) {
std::memcpy(sub->event.data, data, copied_len);
}
sub->event.data_len = copied_len;
sub->internal.sequence++;
xSemaphoreGive(sub->internal.semaphore);
}
}
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 +113,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 +128,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 +144,199 @@ error_t system_event_emit(
return ERROR_NONE;
}
error_t system_event_emit(
SystemEventType type,
const void* data,
size_t data_len
) {
SystemEvent event {};
event.type = type;
event.timestamp = get_micros_since_boot();
const size_t copied_len = std::min(data_len, SYSTEM_EVENT_MAX_DATA_SIZE);
if (copied_len > 0) {
std::memcpy(event.data, data, copied_len);
}
event.data_len = copied_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) {
// Wait out any system_event_unsubscribe() call still draining old awaiters for this same
// `sub` on another task (see internal.unsubscribe_in_progress). waiter_count/cancelled
// belong to `sub` itself, not to a given registration - reusing `sub` before that call
// finishes would reset them out from under it, and could hand out a fresh semaphore for it
// to then promptly delete instead of the old one, while an old awaiter is still blocked on
// the real old semaphore.
while (true) {
mutex_lock(&poll_subscriptions_mutex.handle);
bool busy = sub->internal.unsubscribe_in_progress;
mutex_unlock(&poll_subscriptions_mutex.handle);
if (!busy) {
break;
}
delay_ticks(pdMS_TO_TICKS(10));
}
SemaphoreHandle_t semaphore = xSemaphoreCreateBinary();
if (semaphore == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
mutex_lock(&poll_subscriptions_mutex.handle);
// Check-and-insert in one critical section: registering the same `sub` twice would link
// it into a list that already contains it, creating a cycle that notify_poll_subscribers()
// would then traverse forever while holding this same mutex.
for (SystemEventSubscription* existing = poll_subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&poll_subscriptions_mutex.handle);
vSemaphoreDelete(semaphore);
return ERROR_INVALID_STATE;
}
}
sub->internal.semaphore = semaphore;
sub->internal.sequence = 0;
sub->internal.consumed_sequence = 0;
sub->internal.waiter_count = 0;
sub->internal.cancelled = false;
sub->event.data_len = 0;
sub->internal.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;
SemaphoreHandle_t semaphore_to_delete = nullptr;
mutex_lock(&poll_subscriptions_mutex.handle);
for (SystemEventSubscription** link = &poll_subscriptions; *link != nullptr; link = &(*link)->internal.next) {
if (*link == sub) {
*link = sub->internal.next;
result = ERROR_NONE;
break;
}
}
if (result == ERROR_NONE) {
// Unlinked first, so notify_poll_subscribers() can no longer reach this subscription.
// Mark it cancelled (checked by system_event_await()'s loop) and capture the semaphore
// handle into a local variable rather than deleting it via sub->internal.semaphore
// directly - a concurrent system_event_subscribe() re-registering this same `sub` after
// this point would overwrite that field with a freshly created semaphore, and we must
// not delete the wrong (newly active) one.
sub->internal.cancelled = true;
// Blocks a concurrent system_event_subscribe() from reusing `sub` until this whole
// call returns - see internal.unsubscribe_in_progress and system_event_subscribe().
sub->internal.unsubscribe_in_progress = true;
semaphore_to_delete = sub->internal.semaphore;
sub->internal.semaphore = nullptr;
}
mutex_unlock(&poll_subscriptions_mutex.handle);
if (result != ERROR_NONE) {
return result;
}
// Nudge any task already blocked in system_event_await() (it captured its own local copy
// of this same semaphore handle before this point, so it's unaffected by the field having
// just been cleared above) so it re-checks `cancelled` and bails out now instead of waiting
// out its full timeout, then wait for it to actually leave the semaphore before deleting it
// - FreeRTOS requires no task be blocked on a semaphore when it's deleted.
xSemaphoreGive(semaphore_to_delete);
while (true) {
mutex_lock(&poll_subscriptions_mutex.handle);
bool still_waiting = sub->internal.waiter_count > 0;
mutex_unlock(&poll_subscriptions_mutex.handle);
if (!still_waiting) {
break;
}
delay_ticks(pdMS_TO_TICKS(10));
}
vSemaphoreDelete(semaphore_to_delete);
// Reset under the lock, together, as the last step - only past this point is `sub` safe
// for system_event_subscribe() to reuse (see internal.unsubscribe_in_progress and the
// busy-wait at the top of system_event_subscribe()).
mutex_lock(&poll_subscriptions_mutex.handle);
sub->internal.cancelled = false;
sub->internal.unsubscribe_in_progress = false;
mutex_unlock(&poll_subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t system_event_await(SystemEventSubscription* sub, TickType_t timeout) {
mutex_lock(&poll_subscriptions_mutex.handle);
SemaphoreHandle_t semaphore = sub->internal.semaphore;
sub->internal.waiter_count++;
mutex_unlock(&poll_subscriptions_mutex.handle);
error_t result = ERROR_NONE;
// sequence/consumed_sequence are written by notify_poll_subscribers() under
// poll_subscriptions_mutex - read (and, on a match, updated) under the same lock each
// iteration, rather than compared lock-free, so a concurrent emit can't land between an
// unlocked read and this loop acting on it.
//
// Compare against consumed_sequence, not a sequence snapshot taken now - an emit that
// landed between system_event_subscribe() and this call already incremented sequence and
// gave the semaphore, so that event is pending but unconsumed. Snapshotting "now" would
// make the loop wait for yet another event instead of returning this already-pending one.
while (true) {
mutex_lock(&poll_subscriptions_mutex.handle);
bool pending = sub->internal.sequence != sub->internal.consumed_sequence;
bool cancelled = sub->internal.cancelled;
if (pending) {
sub->internal.consumed_sequence = sub->internal.sequence;
}
mutex_unlock(&poll_subscriptions_mutex.handle);
if (pending) {
break;
}
if (cancelled) {
result = ERROR_INVALID_STATE;
break;
}
if (xSemaphoreTake(semaphore, timeout) == pdFALSE) {
result = ERROR_TIMEOUT;
break;
}
}
mutex_lock(&poll_subscriptions_mutex.handle);
sub->internal.waiter_count--;
mutex_unlock(&poll_subscriptions_mutex.handle);
return result;
}
error_t system_event_get_data(SystemEventSubscription* sub, uint8_t* data, size_t data_len) {
// sub->event.* is written by notify_poll_subscribers() under poll_subscriptions_mutex -
// the length check and the copy must happen as one snapshot under the same lock, otherwise
// a concurrent emit could grow data_len (or overwrite data) between the check and the
// memcpy below.
mutex_lock(&poll_subscriptions_mutex.handle);
error_t result = ERROR_NONE;
if (data_len < sub->event.data_len) {
result = ERROR_BUFFER_OVERFLOW;
} else {
std::memcpy(data, sub->event.data, sub->event.data_len);
}
mutex_unlock(&poll_subscriptions_mutex.handle);
return result;
}
} // extern "C"