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:
committed by
GitHub
parent
db48dfe812
commit
ab75d2022d
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
void task_event_group_construct(TaskEventGroup* group) {
|
||||
event_group_construct(&group->internal.handle);
|
||||
mutex_construct(&group->internal.bit_mutex);
|
||||
group->claimed_bits = 0;
|
||||
}
|
||||
|
||||
void task_event_group_destruct(TaskEventGroup* group) {
|
||||
event_group_destruct(&group->internal.handle);
|
||||
mutex_destruct(&group->internal.bit_mutex);
|
||||
group->claimed_bits = 0;
|
||||
}
|
||||
|
||||
error_t task_event_group_claim_bit(TaskEventGroup* group, uint32_t* out_bit) {
|
||||
mutex_lock(&group->internal.bit_mutex);
|
||||
|
||||
error_t result = ERROR_RESOURCE;
|
||||
for (uint32_t i = 0; i < TASK_EVENT_GROUP_MAX_BITS; i++) {
|
||||
uint32_t bit = 1u << i;
|
||||
if ((group->claimed_bits & bit) == 0) {
|
||||
group->claimed_bits |= bit;
|
||||
*out_bit = bit;
|
||||
result = ERROR_NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mutex_unlock(&group->internal.bit_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
void task_event_group_release_bit(TaskEventGroup* group, uint32_t bit) {
|
||||
// Clear the real flag, not just the claim bookkeeping: a signal nobody drained before
|
||||
// releasing (e.g. a courtesy nudge on unsubscribe with no one currently waiting) would
|
||||
// otherwise sit set in the underlying event group and hand the next claimant of this same
|
||||
// bit a false "already fired" wake.
|
||||
event_group_clear(group->internal.handle, bit);
|
||||
|
||||
mutex_lock(&group->internal.bit_mutex);
|
||||
group->claimed_bits &= ~bit;
|
||||
mutex_unlock(&group->internal.bit_mutex);
|
||||
}
|
||||
|
||||
error_t task_event_group_signal(TaskEventGroup* group, uint32_t bit) {
|
||||
return event_group_set(group->internal.handle, bit);
|
||||
}
|
||||
|
||||
error_t task_event_group_wait(
|
||||
TaskEventGroup* group,
|
||||
uint32_t bits_mask,
|
||||
bool await_all,
|
||||
uint32_t* out_flags,
|
||||
TickType_t timeout
|
||||
) {
|
||||
return event_group_wait(group->internal.handle, bits_mask, await_all, true, out_flags, timeout);
|
||||
}
|
||||
|
||||
error_t task_event_group_wait_any(TaskEventGroup* group, uint32_t* out_flags, TickType_t timeout) {
|
||||
mutex_lock(&group->internal.bit_mutex);
|
||||
uint32_t mask = group->claimed_bits;
|
||||
mutex_unlock(&group->internal.bit_mutex);
|
||||
|
||||
if (mask == 0) {
|
||||
// xEventGroupWaitBits() asserts on a zero mask - nothing claimed means nothing to wait
|
||||
// for, so this is the correct outcome anyway.
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
|
||||
return task_event_group_wait(group, mask, false, out_flags, timeout);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -7,13 +7,12 @@
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct Device* wifi_find_first_registered_device() {
|
||||
struct Device* found = nullptr;
|
||||
device_for_each_of_type(&WIFI_TYPE, &found, [](struct Device* dev, void* ctx) -> bool {
|
||||
*static_cast<struct Device**>(ctx) = dev;
|
||||
return false;
|
||||
});
|
||||
return found;
|
||||
error_t wifi_set_radio_on(struct Device* device) {
|
||||
return WIFI_API(device)->set_radio_on(device);
|
||||
}
|
||||
|
||||
error_t wifi_set_radio_off(struct Device* device) {
|
||||
return WIFI_API(device)->set_radio_off(device);
|
||||
}
|
||||
|
||||
error_t wifi_get_radio_state(struct Device* device, enum WifiRadioState* state) {
|
||||
@@ -60,12 +59,36 @@ error_t wifi_station_get_rssi(struct Device* device, int32_t* rssi) {
|
||||
return WIFI_API(device)->station_get_rssi(device, rssi);
|
||||
}
|
||||
|
||||
error_t wifi_add_event_callback(struct Device* device, void* callback_context, WifiEventCallback callback) {
|
||||
return WIFI_API(device)->add_event_callback(device, callback_context, callback);
|
||||
error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group) {
|
||||
mutex_construct(&sub->internal.ring_mutex);
|
||||
sub->internal.head = 0;
|
||||
sub->internal.count = 0;
|
||||
|
||||
error_t result = WIFI_API(device)->event_subscribe(device, sub, event_group);
|
||||
if (result != ERROR_NONE) {
|
||||
mutex_destruct(&sub->internal.ring_mutex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
error_t wifi_remove_event_callback(struct Device* device, WifiEventCallback callback) {
|
||||
return WIFI_API(device)->remove_event_callback(device, callback);
|
||||
error_t wifi_event_unsubscribe(struct Device* device, struct WifiEventSubscription* sub) {
|
||||
error_t result = WIFI_API(device)->event_unsubscribe(device, sub);
|
||||
if (result == ERROR_NONE) {
|
||||
mutex_destruct(&sub->internal.ring_mutex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
error_t wifi_event_poll(struct WifiEventSubscription* sub, struct WifiEvent* out_event) {
|
||||
mutex_lock(&sub->internal.ring_mutex);
|
||||
bool has_event = sub->internal.count > 0;
|
||||
if (has_event) {
|
||||
*out_event = sub->internal.queue[sub->internal.head];
|
||||
sub->internal.head = (sub->internal.head + 1) % WIFI_EVENT_QUEUE_CAPACITY;
|
||||
sub->internal.count--;
|
||||
}
|
||||
mutex_unlock(&sub->internal.ring_mutex);
|
||||
return has_event ? ERROR_NONE : ERROR_TIMEOUT;
|
||||
}
|
||||
|
||||
error_t wifi_get_firmware_ops(struct Device* device, const struct FirmwareOps** ops, void** ctx) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <tactility/bundle.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/concurrent/dispatcher.h>
|
||||
#include <tactility/concurrent/event_group.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/concurrent/timer.h>
|
||||
#include <tactility/device.h>
|
||||
@@ -379,7 +381,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(camera_capture_jpeg),
|
||||
DEFINE_MODULE_SYMBOL(CAMERA_TYPE),
|
||||
// drivers/wifi
|
||||
DEFINE_MODULE_SYMBOL(wifi_find_first_registered_device),
|
||||
DEFINE_MODULE_SYMBOL(wifi_set_radio_on),
|
||||
DEFINE_MODULE_SYMBOL(wifi_set_radio_off),
|
||||
DEFINE_MODULE_SYMBOL(wifi_get_radio_state),
|
||||
DEFINE_MODULE_SYMBOL(wifi_get_station_state),
|
||||
DEFINE_MODULE_SYMBOL(wifi_get_access_point_state),
|
||||
@@ -391,8 +394,9 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(wifi_station_connect),
|
||||
DEFINE_MODULE_SYMBOL(wifi_station_disconnect),
|
||||
DEFINE_MODULE_SYMBOL(wifi_station_get_rssi),
|
||||
DEFINE_MODULE_SYMBOL(wifi_add_event_callback),
|
||||
DEFINE_MODULE_SYMBOL(wifi_remove_event_callback),
|
||||
DEFINE_MODULE_SYMBOL(wifi_event_subscribe),
|
||||
DEFINE_MODULE_SYMBOL(wifi_event_unsubscribe),
|
||||
DEFINE_MODULE_SYMBOL(wifi_event_poll),
|
||||
DEFINE_MODULE_SYMBOL(wifi_get_firmware_ops),
|
||||
DEFINE_MODULE_SYMBOL(WIFI_TYPE),
|
||||
// wifi_auto_scan
|
||||
@@ -471,6 +475,14 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(event_group_clear),
|
||||
DEFINE_MODULE_SYMBOL(event_group_get),
|
||||
DEFINE_MODULE_SYMBOL(event_group_wait),
|
||||
// concurrent/task_event_group
|
||||
DEFINE_MODULE_SYMBOL(task_event_group_construct),
|
||||
DEFINE_MODULE_SYMBOL(task_event_group_destruct),
|
||||
DEFINE_MODULE_SYMBOL(task_event_group_claim_bit),
|
||||
DEFINE_MODULE_SYMBOL(task_event_group_release_bit),
|
||||
DEFINE_MODULE_SYMBOL(task_event_group_signal),
|
||||
DEFINE_MODULE_SYMBOL(task_event_group_wait),
|
||||
DEFINE_MODULE_SYMBOL(task_event_group_wait_any),
|
||||
// concurrent/thread
|
||||
DEFINE_MODULE_SYMBOL(thread_alloc),
|
||||
DEFINE_MODULE_SYMBOL(thread_alloc_full),
|
||||
@@ -501,6 +513,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(timer_set_callback_priority),
|
||||
// error
|
||||
DEFINE_MODULE_SYMBOL(error_to_string),
|
||||
// check
|
||||
DEFINE_MODULE_SYMBOL(__crash),
|
||||
// log
|
||||
#ifndef ESP_PLATFORM
|
||||
DEFINE_MODULE_SYMBOL(log_generic),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <tactility/system_event.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
@@ -30,9 +29,9 @@ 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.
|
||||
// _poll()), 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 a
|
||||
// task_event_group_signal), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
|
||||
static SystemEventSubscription* poll_subscriptions = nullptr;
|
||||
static KernelEventMutex poll_subscriptions_mutex;
|
||||
|
||||
@@ -68,9 +67,9 @@ error_t system_event_callback_remove(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Copies `data` into every current poll subscriber of `type` and signals its wakeup semaphore.
|
||||
// Copies `data` into every current poll subscriber of `type` and signals its wakeup bit.
|
||||
// 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.
|
||||
// (just a memcpy and a signal), so there is nothing that could reenter and deadlock.
|
||||
static void notify_poll_subscribers(
|
||||
SystemEventType type,
|
||||
uint64_t timestamp,
|
||||
@@ -88,7 +87,7 @@ static void notify_poll_subscribers(
|
||||
}
|
||||
sub->event.data_len = copied_len;
|
||||
sub->internal.sequence++;
|
||||
xSemaphoreGive(sub->internal.semaphore);
|
||||
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,46 +164,26 @@ error_t system_event_emit(
|
||||
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;
|
||||
error_t system_event_subscribe(SystemEventSubscription* sub, TaskEventGroup* event_group) {
|
||||
uint32_t bit;
|
||||
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
|
||||
if (claim_result != ERROR_NONE) {
|
||||
return claim_result;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// Avoid cyclic subscription list that would loop forever
|
||||
if (poll_subscriptions == sub) {
|
||||
mutex_unlock(&poll_subscriptions_mutex.handle);
|
||||
task_event_group_release_bit(event_group, bit);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
sub->internal.semaphore = semaphore;
|
||||
sub->internal.event_group = event_group;
|
||||
sub->bit = bit;
|
||||
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;
|
||||
@@ -217,7 +196,6 @@ error_t system_event_subscribe(SystemEventSubscription* sub) {
|
||||
|
||||
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) {
|
||||
@@ -228,101 +206,36 @@ error_t system_event_unsubscribe(SystemEventSubscription* sub) {
|
||||
}
|
||||
}
|
||||
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.
|
||||
// Diagnostic only now (see @warning) - not relied on for safety. Best-effort nudge for a
|
||||
// task that might still be blocked in task_event_group_wait() on this bit; unlike before,
|
||||
// this does not wait for it to leave before the bit is released.
|
||||
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;
|
||||
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
||||
}
|
||||
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_poll(SystemEventSubscription* sub) {
|
||||
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) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
if (cancelled) {
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -216,17 +216,20 @@ TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes an
|
||||
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_b);
|
||||
}
|
||||
|
||||
// gps.h-style poll subscription: system_event_subscribe()/_await()/_unsubscribe().
|
||||
// gps.h-style poll subscription: system_event_subscribe()/_poll()/_unsubscribe().
|
||||
//
|
||||
// system_event_await() only detects sequence increments that happen *after* it starts
|
||||
// waiting (same as gps_api_event_await()), so the emit must be started from another task
|
||||
// while this one is already blocked in await() - emitting first and awaiting after would
|
||||
// race the notification the same way it would with any FreeRTOS task-notify consumer.
|
||||
// system_event_poll() only detects sequence increments that happened before it's called (same
|
||||
// as gps_api_event_await()), so tests that need to observe an emit from another task block via
|
||||
// task_event_group_wait() first - emitting before that wait started would race the notification
|
||||
// the same way it would with any FreeRTOS task-notify consumer.
|
||||
|
||||
TEST_CASE("system_event_subscribe/_poll deliver the event payload by value") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
TEST_CASE("system_event_subscribe/_await deliver the event payload by value") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
|
||||
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
|
||||
auto* thread = thread_alloc_full(
|
||||
@@ -243,7 +246,8 @@ TEST_CASE("system_event_subscribe/_await deliver the event payload by value") {
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
|
||||
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
|
||||
|
||||
NetworkConnectedEvent received {};
|
||||
CHECK_EQ(system_event_get_data(&sub, reinterpret_cast<uint8_t*>(&received), sizeof(received)), ERROR_NONE);
|
||||
@@ -255,62 +259,78 @@ TEST_CASE("system_event_subscribe/_await deliver the event payload by value") {
|
||||
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_await returns a matching event that arrived before it started waiting") {
|
||||
TEST_CASE("system_event_poll returns a matching event that arrived before it was called") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
|
||||
// Same-thread emit, no background thread needed: unlike the "detects a change after it
|
||||
// starts waiting" tests above, this is exactly the case system_event_await() must handle -
|
||||
// sequence already moved ahead of consumed_sequence before await() is even called.
|
||||
// Same-thread emit, no background thread or wait needed: unlike the "detects a change after
|
||||
// it starts waiting" tests above, this is exactly the case system_event_poll() must handle -
|
||||
// sequence already moved ahead of consumed_sequence before poll() is even called.
|
||||
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
|
||||
CHECK_EQ(system_event_emit(KERNEL_EVENT_NETWORK_CONNECTED, &connected, sizeof(connected)), ERROR_NONE);
|
||||
|
||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_NONE);
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
|
||||
|
||||
NetworkConnectedEvent received {};
|
||||
CHECK_EQ(system_event_get_data(&sub, reinterpret_cast<uint8_t*>(&received), sizeof(received)), ERROR_NONE);
|
||||
CHECK_EQ(received.ipv4_addr, connected.ipv4_addr);
|
||||
CHECK_EQ(received.gateway, connected.gateway);
|
||||
|
||||
// The pending event was consumed by the call above - a second await() with no further
|
||||
// emit must time out rather than returning the same event again.
|
||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
|
||||
// The pending event was consumed by the call above - a second poll() with no further emit
|
||||
// must time out rather than returning the same event again.
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT);
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_await times out when no matching event has arrived") {
|
||||
TEST_CASE("system_event_poll times out when no matching event has arrived") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_TIME_CHANGED;
|
||||
system_event_subscribe(&sub);
|
||||
system_event_subscribe(&sub, &event_group);
|
||||
|
||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT);
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_emit does not notify a poll subscriber of a different type") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
|
||||
system_event_subscribe(&sub);
|
||||
system_event_subscribe(&sub, &event_group);
|
||||
|
||||
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
|
||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT);
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the buffer untouched") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_NETWORK_DISCONNECTED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
|
||||
// system_event_await() only detects sequence increments that happen *after* it starts
|
||||
// waiting (see the comment above), so the emit must come from another task while this one
|
||||
// is already blocked in await() - same pattern as the payload-delivery test above.
|
||||
// Background emit + wait, same pattern as the payload-delivery test above - see the comment
|
||||
// near the top of the file.
|
||||
NetworkDisconnectedEvent disconnected { .device = nullptr };
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-emitter",
|
||||
@@ -325,7 +345,8 @@ TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the bu
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
|
||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
@@ -337,12 +358,16 @@ TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the bu
|
||||
CHECK_EQ(system_event_get_data(&sub, exact, sizeof(exact)), ERROR_NONE);
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_get_data on a subscription with no payload copies nothing and succeeds") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-emitter",
|
||||
@@ -356,7 +381,8 @@ TEST_CASE("system_event_get_data on a subscription with no payload copies nothin
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
|
||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
@@ -365,59 +391,44 @@ TEST_CASE("system_event_get_data on a subscription with no payload copies nothin
|
||||
CHECK_EQ(buffer[0], 0x42); // untouched - nothing to copy
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
// Regression coverage for system_event_unsubscribe() racing a task blocked in
|
||||
// system_event_await() on the same subscription, and for reusing a subscription node after
|
||||
// unsubscribing it - see the @warning on system_event_unsubscribe() in system_event.h.
|
||||
// Regression coverage for system_event_unsubscribe()'s (now best-effort, not guaranteed - see
|
||||
// its @warning in system_event.h) diagnostic signal, and for reusing a subscription node after
|
||||
// unsubscribing it.
|
||||
|
||||
TEST_CASE("system_event_poll returns ERROR_INVALID_STATE after unsubscribe (diagnostic, best-effort)") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
TEST_CASE("system_event_unsubscribe wakes a task blocked in system_event_await with ERROR_INVALID_STATE") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_SERVICE_STARTED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-awaiter",
|
||||
4096,
|
||||
[](void* context) {
|
||||
auto* awaited_sub = static_cast<SystemEventSubscription*>(context);
|
||||
// Long timeout - the point is that unsubscribe() wakes this early, not that it
|
||||
// eventually times out on its own.
|
||||
return static_cast<int32_t>(system_event_await(awaited_sub, pdMS_TO_TICKS(5000)));
|
||||
},
|
||||
&sub,
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
|
||||
// Give the awaiter task a moment to actually reach xSemaphoreTake() before unsubscribing -
|
||||
// otherwise this test wouldn't exercise the "already blocked" race at all.
|
||||
delay_millis(20);
|
||||
|
||||
// Must return promptly (nudging the blocked awaiter awake), not by waiting out its timeout.
|
||||
TickType_t before = get_ticks();
|
||||
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||
CHECK_LT(get_ticks() - before, pdMS_TO_TICKS(1000));
|
||||
|
||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||
CHECK_EQ(thread_get_return_code(thread), ERROR_INVALID_STATE);
|
||||
thread_free(thread);
|
||||
// `sub` is still caller-owned storage after unsubscribe - polling it directly (rather than
|
||||
// still being blocked in task_event_group_wait() on it, which system_event_unsubscribe()'s
|
||||
// @warning now says not to do) is the one remaining diagnostic case `cancelled` covers.
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_INVALID_STATE);
|
||||
|
||||
// A second unsubscribe() has nothing left to do.
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscribe") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_SERVICE_STOPPED;
|
||||
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||
|
||||
// Re-registering the same node (same storage, not a fresh SystemEventSubscription) must
|
||||
// work as if it were new - a fresh semaphore, and no leftover `cancelled` state from the
|
||||
// work as if it were new - a fresh bit, and no leftover `cancelled` state from the
|
||||
// unsubscribe() above causing an immediate spurious ERROR_INVALID_STATE below.
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-emitter",
|
||||
@@ -431,9 +442,12 @@ TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscri
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
|
||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
|
||||
TEST_CASE("task_event_group_wait_any wakes on any bit currently claimed in the group") {
|
||||
TaskEventGroup group {};
|
||||
task_event_group_construct(&group);
|
||||
|
||||
uint32_t bit_a, bit_b;
|
||||
CHECK_EQ(task_event_group_claim_bit(&group, &bit_a), ERROR_NONE);
|
||||
CHECK_EQ(task_event_group_claim_bit(&group, &bit_b), ERROR_NONE);
|
||||
CHECK_NE(bit_a, bit_b);
|
||||
|
||||
CHECK_EQ(task_event_group_signal(&group, bit_b), ERROR_NONE);
|
||||
|
||||
uint32_t out_flags = 0;
|
||||
CHECK_EQ(task_event_group_wait_any(&group, &out_flags, 0), ERROR_NONE);
|
||||
CHECK_EQ(out_flags, bit_b);
|
||||
|
||||
task_event_group_destruct(&group);
|
||||
}
|
||||
|
||||
TEST_CASE("task_event_group_wait_any times out immediately when the group has no claimed bits") {
|
||||
TaskEventGroup group {};
|
||||
task_event_group_construct(&group);
|
||||
|
||||
CHECK_EQ(task_event_group_wait_any(&group, nullptr, 0), ERROR_TIMEOUT);
|
||||
|
||||
task_event_group_destruct(&group);
|
||||
}
|
||||
|
||||
TEST_CASE("task_event_group_wait_any times out when claimed bits exist but none are signalled") {
|
||||
TaskEventGroup group {};
|
||||
task_event_group_construct(&group);
|
||||
|
||||
uint32_t bit;
|
||||
CHECK_EQ(task_event_group_claim_bit(&group, &bit), ERROR_NONE);
|
||||
|
||||
CHECK_EQ(task_event_group_wait_any(&group, nullptr, 0), ERROR_TIMEOUT);
|
||||
|
||||
task_event_group_destruct(&group);
|
||||
}
|
||||
Reference in New Issue
Block a user