Refactored events (#621)

Event handling:

- Added unified event handling for application, system, and Wi‑Fi events.
- Updated all apps to reflect event handling changes

Wi-Fi:

- Refactored event handling from listener interface to task & event group.
- Added direct Wi‑Fi radio controls and improved event subscriptions.
- Wi‑Fi screens now refresh asynchronously and handle unavailable devices more gracefully.
- Enabled Wi‑Fi by default on in dts files, but radio on/off is still done by code. The main reason was reliable event subscription and consistent devicetree states.
- Improved Wi‑Fi shutdown cleanup and radio-state handling.

Other:
- Renamed the Kernel Display app to Display.
This commit is contained in:
Ken Van Hoeylandt
2026-08-25 17:44:20 +02:00
committed by GitHub
parent db48dfe812
commit ab75d2022d
121 changed files with 2416 additions and 1381 deletions
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/concurrent/event_group.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Usable bits per FreeRTOS event group when configUSE_16_BIT_TICKS is 0
* (This project's setting on every target. See Devices/simulator/Source/FreeRTOSConfig.h). */
#define TASK_EVENT_GROUP_MAX_BITS 24
/**
* Lets a task block on several independent event sources (e.g. app_event and system_event) with
* a single wait call, instead of each source claiming the task's own FreeRTOS notification value
* or its own semaphore - primitives that can't be combined into one blocking call.
*
* Caller-owned storage, constructed/destructed like struct Mutex/EventGroupHandle_t - no heap
* allocation. Each event source claims its own bit at subscribe time via
* task_event_group_claim_bit(), so sources at any layer (kernel, firmware, app) can share one
* group without knowing about each other's bit assignments.
*
* @warning `bit_mutex`/`claimed_bits` are implementation-only; do not read or write directly.
*/
struct TaskEventGroup {
uint32_t claimed_bits;
struct {
EventGroupHandle_t handle;
struct Mutex bit_mutex;
} internal;
};
/** @warning Does not work in ISR context. */
void task_event_group_construct(struct TaskEventGroup* group);
/** @warning Does not work in ISR context. */
void task_event_group_destruct(struct TaskEventGroup* group);
/**
* Claim an unused bit in @a group for exclusive use by one event source. Always starts clear
* (unsignalled), whether this bit has ever been claimed before or not.
* @param[out] out_bit set to the claimed bit (a single-bit mask, not a bit index) on success
* @retval ERROR_NONE on success
* @retval ERROR_RESOURCE all TASK_EVENT_GROUP_MAX_BITS bits are already claimed
*/
error_t task_event_group_claim_bit(struct TaskEventGroup* group, uint32_t* out_bit);
/**
* Release a bit previously returned by task_event_group_claim_bit(), making it available for
* reuse. Also clears it, so a signal nobody drained before releasing can't be handed to the next
* claimant as a false "already fired" wake.
*/
void task_event_group_release_bit(struct TaskEventGroup* group, uint32_t bit);
/** Signal @a bit, waking any task blocked in task_event_group_wait() for it. ISR-safe. */
error_t task_event_group_signal(struct TaskEventGroup* group, uint32_t bit);
/**
* Block until one or more bits in @a bits_mask are signalled, or @a timeout elapses. Matched
* bits are cleared on exit.
* @param[in] await_all if true, wait for every bit in @a bits_mask; otherwise wait for any of them
* @param[out] out_flags if non-NULL, set to the matched bits on ERROR_NONE
*/
error_t task_event_group_wait(
struct TaskEventGroup* group,
uint32_t bits_mask,
bool await_all,
uint32_t* out_flags,
TickType_t timeout
);
/**
* Like task_event_group_wait(), but waits on every bit currently claimed in @a group (snapshotted
* at call time) instead of a caller-supplied mask - so the caller doesn't need to track/OR
* together each subscription's bit by hand. A bit claimed by a new subscription *during* the wait
* isn't included until the next call.
* @warning Meant for the common case where every subscription sharing @a group is set up before
* the wait loop starts (matching app_event/system_event/wifi_event's own usage patterns) - not
* for a group a subscriber can join mid-wait and expect to wake immediately.
* @retval ERROR_TIMEOUT returned immediately, without blocking, if @a group currently has no
* claimed bits (FreeRTOS asserts on a zero-bit wait mask, so this is handled before it gets there)
*/
error_t task_event_group_wait_any(struct TaskEventGroup* group, uint32_t* out_flags, TickType_t timeout);
#ifdef __cplusplus
}
#endif
+109 -14
View File
@@ -4,6 +4,8 @@
#include <stddef.h>
#include <stdint.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.h>
#include <tactility/firmware/firmware.h>
@@ -90,9 +92,59 @@ struct WifiEvent {
};
};
typedef void (*WifiEventCallback)(struct Device* device, void* callback_context, struct WifiEvent event);
/** Number of events a WifiEventSubscription can hold before wifi_event_emit() starts dropping
* the newest event for it (still delivered to any other matching subscription). Generous: a
* device fires these serially, one at a time, not in genuinely concurrent bursts. */
#define WIFI_EVENT_QUEUE_CAPACITY 4
/**
* Caller-owned subscription node, registered with wifi_event_subscribe() and polled with
* wifi_event_poll(). Like app_event, this queues events by value (FIFO) rather than
* coalescing to the latest one: a burst of distinct WifiEventTypes (e.g.
* WIFI_EVENT_TYPE_STATION_STATE_CHANGED immediately followed by
* WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT) must each be delivered, not just "something
* changed."
* @warning Fields other than `bit` are for internal use only; do not read or write them
* directly.
*/
struct WifiEventSubscription {
/** Set by wifi_event_subscribe(). Read-only for the caller: OR it into a
* task_event_group_wait() mask (alongside other subscriptions sharing the same
* `event_group`) to block on this subscription and other event sources with one call. */
uint32_t bit;
struct {
/** Caller-owned, borrowed; set by wifi_event_subscribe(). */
struct TaskEventGroup* event_group;
/** Guards `queue`/`head`/`count` between wifi_event_emit() (driver thread) and
* wifi_event_poll() (caller's thread) - the two live in different translation units with
* no other shared lock. */
struct Mutex ring_mutex;
struct WifiEvent queue[WIFI_EVENT_QUEUE_CAPACITY];
uint8_t head;
uint8_t count;
struct WifiEventSubscription* next;
} internal;
};
struct WifiApi {
/**
* Turn the radio on. Unlike start_device()/stop_device() (which only allocate/free the
* driver's bookkeeping, so event subscribers can stay subscribed across radio toggles),
* this is what actually brings the hardware up.
* @param[in] device the wifi device
* @return ERROR_NONE on success, or if the radio is already on
*/
error_t (*set_radio_on)(struct Device* device);
/**
* Turn the radio off. See set_radio_on().
* @param[in] device the wifi device
* @return ERROR_NONE on success, or if the radio is already off
*/
error_t (*set_radio_off)(struct Device* device);
/**
* Get the radio state of the device.
* @param[in] device the wifi device
@@ -182,21 +234,30 @@ struct WifiApi {
error_t (*station_get_rssi)(struct Device* device, int32_t* rssi);
/**
* Add a WifiEvent callback.
* Register a subscription for this device's WifiEvents.
* @warning Does not work in ISR context.
* @param[in] device the wifi device
* @param[in] callback_context the context to pass to the callback
* @param[in] callback the callback function
* @return ERROR_NONE on success
* @param[in,out] sub subscription to register; owns the storage, must stay alive (and
* stationary) until unsubscribed
* @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be
* destructed only after event_unsubscribe()). To block for an event, call
* task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit into the
* mask, or use _wait_any() to include every subscription sharing it), then drain with
* wifi_event_poll().
* @retval ERROR_NONE on success
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered
* @retval ERROR_INVALID_STATE @a sub is already registered
*/
error_t (*add_event_callback)(struct Device* device, void* callback_context, WifiEventCallback callback);
error_t (*event_subscribe)(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group);
/**
* Remove a WifiEvent callback.
* Remove a previously registered subscription.
* @warning Does not work in ISR context.
* @param[in] device the wifi device
* @param[in] callback the callback function
* @return ERROR_NONE on success
* @param[in] sub subscription to remove, as passed to event_subscribe()
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t (*remove_event_callback)(struct Device* device, WifiEventCallback callback);
error_t (*event_unsubscribe)(struct Device* device, struct WifiEventSubscription* sub);
/**
* Get this device's co-processor firmware update interface, if it has one.
@@ -210,8 +271,10 @@ struct WifiApi {
extern const struct DeviceType WIFI_TYPE;
/** @return the first registered WiFi device, regardless of started state, or NULL if none exists */
struct Device* wifi_find_first_registered_device(void);
/** Turn the radio on. See WifiApi::set_radio_on(). Requires the device to be started (device_start()). */
error_t wifi_set_radio_on(struct Device* device);
/** Turn the radio off. See WifiApi::set_radio_off(). Requires the device to be started (device_start()). */
error_t wifi_set_radio_off(struct Device* device);
error_t wifi_get_radio_state(struct Device* device, enum WifiRadioState* state);
error_t wifi_get_station_state(struct Device* device, enum WifiStationState* state);
@@ -224,8 +287,40 @@ error_t wifi_station_get_target_ssid(struct Device* device, char* ssid);
error_t wifi_station_connect(struct Device* device, const char* ssid, const char* password, int32_t channel);
error_t wifi_station_disconnect(struct Device* device);
error_t wifi_station_get_rssi(struct Device* device, int32_t* rssi);
error_t wifi_add_event_callback(struct Device* device, void* callback_context, WifiEventCallback callback);
error_t wifi_remove_event_callback(struct Device* device, WifiEventCallback callback);
/**
* Register a subscription for @a device's WifiEvents.
* @warning Does not work in ISR context.
* @param[in] device the wifi device
* @param[in,out] sub subscription to register; owns the storage, must stay alive (and
* stationary) until unsubscribed
* @param[in] event_group caller-owned group to wait on; must outlive @a sub. To block for an
* event, call task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit
* into the mask, or use _wait_any() to include every subscription sharing it), then drain with
* wifi_event_poll().
* @retval ERROR_NONE on success
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered
* @retval ERROR_INVALID_STATE @a sub is already registered
*/
error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group);
/**
* Remove a previously registered subscription.
* @warning Does not work in ISR context.
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t wifi_event_unsubscribe(struct Device* device, struct WifiEventSubscription* sub);
/**
* Non-blocking: pop the next event for @a sub if one is already queued.
* @warning Never blocks. To wait for an event, block in task_event_group_wait()/
* task_event_group_wait_any() on @a sub's event group first (see wifi_event_subscribe()), then
* drain with this in a loop.
* @retval ERROR_NONE @a out_event was filled
* @retval ERROR_TIMEOUT nothing queued right now
*/
error_t wifi_event_poll(struct WifiEventSubscription* sub, struct WifiEvent* out_event);
error_t wifi_get_firmware_ops(struct Device* device, const struct FirmwareOps** ops, void** ctx);
#ifdef __cplusplus
@@ -5,10 +5,8 @@
#include <stddef.h>
#include <stdint.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h>
#include <tactility/freertos/task.h>
#ifdef __cplusplus
extern "C" {
@@ -141,17 +139,9 @@ error_t system_event_emit(
/**
* 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
* and polled with system_event_poll(). 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
@@ -159,36 +149,26 @@ struct SystemEventSubscription {
* each matching system_event_emit() - see the @warning above. */
struct SystemEvent event;
/** Set by system_event_subscribe(). Read-only for the caller: OR it into a
* task_event_group_wait() mask (alongside other subscriptions sharing the same
* `internal.event_group`) to block on this subscription and other event sources with one
* call. */
uint32_t bit;
/** 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;
/** Caller-owned, borrowed; set by system_event_subscribe(). A task with more than one
* poll subscription shares one group across them - each subscription claims its own
* bit, so an event for one can't wake (and consume the signal meant for) another. */
struct TaskEventGroup* event_group;
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. */
/** Set by system_event_unsubscribe(). Diagnostic only: lets a subsequent
* system_event_poll() call report ERROR_INVALID_STATE instead of ERROR_TIMEOUT. Not a
* safety mechanism - see system_event_unsubscribe()'s @warning. */
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;
@@ -197,52 +177,50 @@ struct SystemEventSubscription {
/**
* 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
* @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be
* destructed only after system_event_unsubscribe()). To block for an event, call
* task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit into the mask,
* or use _wait_any() to include every subscription sharing it), then poll with
* system_event_poll().
* @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_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered
* @retval ERROR_INVALID_STATE @a sub is already registered
*/
error_t system_event_subscribe(struct SystemEventSubscription* sub);
error_t system_event_subscribe(struct SystemEventSubscription* sub, struct TaskEventGroup* event_group);
/**
* 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.
* @warning Does not wait for a task concurrently blocked in task_event_group_wait()/
* task_event_group_wait_any() on @a sub's bit to leave before releasing that bit - the caller
* must ensure no other task is still waiting on @a sub before unsubscribing it. (A blocked task
* is still nudged awake as a best-effort courtesy - its subsequent system_event_poll() call will
* report ERROR_INVALID_STATE - but this is diagnostic, not a guarantee.)
* @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.
* Non-blocking: check whether a new event has arrived for @a sub since the last call.
* @warning Never blocks. To wait, block in task_event_group_wait()/task_event_group_wait_any()
* on @a sub's bit first (see system_event_subscribe()), then call this.
* @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_poll() 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
* @warning Cannot be called concurrently from different tasks. Each task must have its own subscription.
* @param[in,out] sub subscription to poll, as passed to system_event_subscribe()
* @retval ERROR_NONE a new event arrived - read it via @a sub->event or system_event_get_data()
* @retval ERROR_TIMEOUT no new event since the last call
* @retval ERROR_INVALID_STATE @a sub was unsubscribed (best-effort diagnostic, not guaranteed -
* see system_event_unsubscribe()'s @warning)
*/
error_t system_event_await(struct SystemEventSubscription* sub, TickType_t timeout);
error_t system_event_poll(struct SystemEventSubscription* sub);
/**
* Copies @a sub's current event payload (the data from the most recent system_event_emit()