mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-20 12:25:06 +00:00
Refactored events
This commit is contained in:
parent
db48dfe812
commit
f15154392f
@ -4,7 +4,7 @@ Apps are event-driven, C API (`app-module`, `<app/*.h>`), not a C++ class. Each
|
|||||||
|
|
||||||
Lifecycle and inter-app communication go through `app_manager_*()` (`app/manager.h`) and `app_event_*()` (`app/event.h`):
|
Lifecycle and inter-app communication go through `app_manager_*()` (`app/manager.h`) and `app_event_*()` (`app/event.h`):
|
||||||
- `app_manager_start()`/`app_manager_start_with_parameters()` launch a plain instance; `app_manager_start_for_result()` launches a modal child that reports back to a parent instance.
|
- `app_manager_start()`/`app_manager_start_with_parameters()` launch a plain instance; `app_manager_start_for_result()` launches a modal child that reports back to a parent instance.
|
||||||
- An app subscribes with `app_event_subscribe()`/`app_event_await()` and reacts to `APP_EVENT_CLOSE` (terminate now) and `APP_EVENT_RESULT` (a child it started reported back).
|
- An app subscribes with `app_event_subscribe()`, blocks via `task_event_group_wait()`/`task_event_group_wait_any()`, and drains with `app_event_poll()`, reacting to `APP_EVENT_CLOSE` (terminate now) and `APP_EVENT_RESULT` (a child it started reported back).
|
||||||
- An app closes itself by calling `app_manager_finish()` right before returning from `main()`; another instance is closed via `app_manager_stop()`.
|
- An app closes itself by calling `app_manager_finish()` right before returning from `main()`; another instance is closed via `app_manager_stop()`.
|
||||||
|
|
||||||
Apps are registered at startup via `app_manager_add()`. External apps can be loaded from SD card via `manifest.properties` files, or side-loaded as ELF binaries on ESP32 (see `app/loader.h`'s `AppLoaderApi`).
|
Apps are registered at startup via `app_manager_add()`. External apps can be loaded from SD card via `manifest.properties` files, or side-loaded as ELF binaries on ESP32 (see `app/loader.h`'s `AppLoaderApi`).
|
||||||
|
|||||||
@ -11,11 +11,10 @@
|
|||||||
|
|
||||||
## Higher Priority
|
## Higher Priority
|
||||||
|
|
||||||
- AppEventSubscription, SystemEventSubscription: should use TaskHandle_t notification. That way, there can be a single wait event for a task instead of X separate ones with each their timeout.
|
- Warn if file operations are done from prohibited tasks (e.g. lvgl task)
|
||||||
- AppHubApp: Prevent download callbacks from accessing a destroyed view.
|
- AppHubApp: Prevent download callbacks from accessing a destroyed view.
|
||||||
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
|
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
|
||||||
- wifi: wifi_add_event_callback() and wifi_remove_event_callback() should be replaced by a subscribe/await pattern like system events.
|
- Get rid of WiFi service (Wifi.cpp/h) in Tactility.cpp
|
||||||
When that's changed reduce LVGL callstack size in Tactility.cpp run()
|
|
||||||
- Make it more clear to end-users that an SD card is required to run Tactility
|
- Make it more clear to end-users that an SD card is required to run Tactility
|
||||||
- Make it possible to override stack size for an app via config file (loaded at boot), and make it possible to set preferred memory location (e.g. internal/external)
|
- Make it possible to override stack size for an app via config file (loaded at boot), and make it possible to set preferred memory location (e.g. internal/external)
|
||||||
- Wrap file operations like fopen/fclose with file_mutex
|
- Wrap file operations like fopen/fclose with file_mutex
|
||||||
|
|||||||
@ -6,15 +6,14 @@
|
|||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
#include <tactility/freertos/freertos.h>
|
|
||||||
#include <tactility/freertos/task.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/** Identifies the kind of app-lifecycle event delivered through app_event_await(). */
|
/** Identifies the kind of app-lifecycle event delivered through app_event_poll(). */
|
||||||
enum AppEventType {
|
enum AppEventType {
|
||||||
APP_EVENT_RESULT, // struct AppResultEventData
|
APP_EVENT_RESULT, // struct AppResultEventData
|
||||||
APP_EVENT_CLOSE, // no data - terminate now, permanently
|
APP_EVENT_CLOSE, // no data - terminate now, permanently
|
||||||
@ -51,20 +50,28 @@ struct AppEvent {
|
|||||||
* Caller-owned subscription node. Unlike TactilityKernel's system_event poll subscription
|
* Caller-owned subscription node. Unlike TactilityKernel's system_event poll subscription
|
||||||
* (which coalesces to the latest value), this queues events by value (FIFO) since dropping an
|
* (which coalesces to the latest value), this queues events by value (FIFO) since dropping an
|
||||||
* APP_EVENT_RESULT would be unacceptable.
|
* APP_EVENT_RESULT would be unacceptable.
|
||||||
* @warning Fields other than `app_instance_id` are for internal use only; do not read or write
|
* @warning Fields other than `app_instance_id` and `bit` are for internal use only; do not read
|
||||||
* them directly.
|
* or write them directly.
|
||||||
*/
|
*/
|
||||||
struct AppEventSubscription {
|
struct AppEventSubscription {
|
||||||
/** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */
|
/** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */
|
||||||
AppInstanceId app_instance_id;
|
AppInstanceId app_instance_id;
|
||||||
|
|
||||||
TaskHandle_t task;
|
/** Set by app_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 app_event_subscribe(). */
|
||||||
|
struct TaskEventGroup* event_group;
|
||||||
|
|
||||||
struct AppEvent queue[APP_EVENT_QUEUE_CAPACITY];
|
struct AppEvent queue[APP_EVENT_QUEUE_CAPACITY];
|
||||||
uint8_t head;
|
uint8_t head;
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
|
|
||||||
struct AppEventSubscription* next;
|
struct AppEventSubscription* next;
|
||||||
|
} internal;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,9 +79,14 @@ struct AppEventSubscription {
|
|||||||
* @warning Does not work in ISR context.
|
* @warning Does not work in ISR context.
|
||||||
* @param[in,out] sub subscription to register; caller sets @a sub->app_instance_id beforehand,
|
* @param[in,out] sub subscription to register; caller sets @a sub->app_instance_id beforehand,
|
||||||
* owns the storage, and must keep it alive (and stationary) until unsubscribed
|
* owns the storage, and must keep it alive (and stationary) until unsubscribed
|
||||||
* @return ERROR_NONE on success
|
* @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be
|
||||||
|
* destructed only after app_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 app_event_poll().
|
||||||
|
* @retval ERROR_NONE on success
|
||||||
|
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim
|
||||||
*/
|
*/
|
||||||
error_t app_event_subscribe(struct AppEventSubscription* sub);
|
error_t app_event_subscribe(struct AppEventSubscription* sub, struct TaskEventGroup* event_group);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a previously registered subscription.
|
* Remove a previously registered subscription.
|
||||||
@ -94,11 +106,14 @@ error_t app_event_unsubscribe(struct AppEventSubscription* sub);
|
|||||||
error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
|
error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pop the next event for @a sub, blocking up to @a timeout if the queue is currently empty.
|
* 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 app_event_subscribe()), then
|
||||||
|
* drain with this in a loop.
|
||||||
* @retval ERROR_NONE @a out_event was filled
|
* @retval ERROR_NONE @a out_event was filled
|
||||||
* @retval ERROR_TIMEOUT no event arrived before the timeout elapsed
|
* @retval ERROR_TIMEOUT nothing queued right now
|
||||||
*/
|
*/
|
||||||
error_t app_event_await(struct AppEventSubscription* sub, struct AppEvent* out_event, TickType_t timeout);
|
error_t app_event_poll(struct AppEventSubscription* sub, struct AppEvent* out_event);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
/**
|
/**
|
||||||
* Intrusive singly-linked list of subscriptions, keyed by app_instance_id.
|
* Intrusive singly-linked list of subscriptions, keyed by app_instance_id.
|
||||||
* Guarded by a single coarse-grained mutex, notifying a subscriber here never invokes caller code
|
* Guarded by a single coarse-grained mutex, notifying a subscriber here never invokes caller code
|
||||||
* (just a struct copy and an xTaskNotifyGive), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
|
* (just a struct copy and a task_event_group_signal), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
|
||||||
*/
|
*/
|
||||||
static AppEventSubscription* subscriptions = nullptr;
|
static AppEventSubscription* subscriptions = nullptr;
|
||||||
|
|
||||||
@ -21,13 +21,20 @@ static AppEventMutex subscriptions_mutex;
|
|||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
error_t app_event_subscribe(AppEventSubscription* sub) {
|
error_t app_event_subscribe(AppEventSubscription* sub, TaskEventGroup* event_group) {
|
||||||
sub->task = xTaskGetCurrentTaskHandle();
|
uint32_t bit;
|
||||||
sub->head = 0;
|
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
|
||||||
sub->count = 0;
|
if (claim_result != ERROR_NONE) {
|
||||||
|
return claim_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub->bit = bit;
|
||||||
|
sub->internal.event_group = event_group;
|
||||||
|
sub->internal.head = 0;
|
||||||
|
sub->internal.count = 0;
|
||||||
|
|
||||||
mutex_lock(&subscriptions_mutex.handle);
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
sub->next = subscriptions;
|
sub->internal.next = subscriptions;
|
||||||
subscriptions = sub;
|
subscriptions = sub;
|
||||||
mutex_unlock(&subscriptions_mutex.handle);
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
|
|
||||||
@ -38,15 +45,19 @@ error_t app_event_unsubscribe(AppEventSubscription* sub) {
|
|||||||
error_t result = ERROR_NOT_FOUND;
|
error_t result = ERROR_NOT_FOUND;
|
||||||
|
|
||||||
mutex_lock(&subscriptions_mutex.handle);
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
for (AppEventSubscription** link = &subscriptions; *link != nullptr; link = &(*link)->next) {
|
for (AppEventSubscription** link = &subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
||||||
if (*link == sub) {
|
if (*link == sub) {
|
||||||
*link = sub->next;
|
*link = sub->internal.next;
|
||||||
result = ERROR_NONE;
|
result = ERROR_NONE;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutex_unlock(&subscriptions_mutex.handle);
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
|
|
||||||
|
if (result == ERROR_NONE) {
|
||||||
|
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,23 +68,23 @@ error_t app_event_emit(AppInstanceId app_instance_id, const AppEvent* event) {
|
|||||||
error_t result = ERROR_NOT_FOUND;
|
error_t result = ERROR_NOT_FOUND;
|
||||||
|
|
||||||
mutex_lock(&subscriptions_mutex.handle);
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
for (AppEventSubscription* sub = subscriptions; sub != nullptr; sub = sub->next) {
|
for (AppEventSubscription* sub = subscriptions; sub != nullptr; sub = sub->internal.next) {
|
||||||
if (sub->app_instance_id != app_instance_id) {
|
if (sub->app_instance_id != app_instance_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub->count >= APP_EVENT_QUEUE_CAPACITY) {
|
if (sub->internal.count >= APP_EVENT_QUEUE_CAPACITY) {
|
||||||
result = ERROR_RESOURCE;
|
result = ERROR_RESOURCE;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t tail = (sub->head + sub->count) % APP_EVENT_QUEUE_CAPACITY;
|
uint8_t tail = (sub->internal.head + sub->internal.count) % APP_EVENT_QUEUE_CAPACITY;
|
||||||
sub->queue[tail] = stamped_event;
|
sub->internal.queue[tail] = stamped_event;
|
||||||
sub->count++;
|
sub->internal.count++;
|
||||||
if (result != ERROR_RESOURCE) {
|
if (result != ERROR_RESOURCE) {
|
||||||
result = ERROR_NONE;
|
result = ERROR_NONE;
|
||||||
}
|
}
|
||||||
xTaskNotifyGive(sub->task);
|
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||||
}
|
}
|
||||||
mutex_unlock(&subscriptions_mutex.handle);
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
|
|
||||||
@ -82,33 +93,17 @@ error_t app_event_emit(AppInstanceId app_instance_id, const AppEvent* event) {
|
|||||||
|
|
||||||
static bool try_pop(AppEventSubscription* sub, AppEvent* out_event) {
|
static bool try_pop(AppEventSubscription* sub, AppEvent* out_event) {
|
||||||
mutex_lock(&subscriptions_mutex.handle);
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
bool has_event = sub->count > 0;
|
bool has_event = sub->internal.count > 0;
|
||||||
if (has_event) {
|
if (has_event) {
|
||||||
*out_event = sub->queue[sub->head];
|
*out_event = sub->internal.queue[sub->internal.head];
|
||||||
sub->head = (sub->head + 1) % APP_EVENT_QUEUE_CAPACITY;
|
sub->internal.head = (sub->internal.head + 1) % APP_EVENT_QUEUE_CAPACITY;
|
||||||
sub->count--;
|
sub->internal.count--;
|
||||||
}
|
}
|
||||||
mutex_unlock(&subscriptions_mutex.handle);
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
return has_event;
|
return has_event;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t app_event_await(AppEventSubscription* sub, AppEvent* out_event, TickType_t timeout) {
|
error_t app_event_poll(AppEventSubscription* sub, AppEvent* out_event) {
|
||||||
if (try_pop(sub, out_event)) {
|
|
||||||
// Drain any notification credit this (or an earlier) push accumulated on this task's
|
|
||||||
// FreeRTOS notification value: each app_event_emit() calls xTaskNotifyGive() regardless
|
|
||||||
// of whether the consumer takes this fast path or the blocking path below, so without
|
|
||||||
// this the credit would carry over and cause a future ulTaskNotifyTake() below to
|
|
||||||
// return immediately for a notification that was already accounted for here.
|
|
||||||
ulTaskNotifyTake(pdTRUE, 0);
|
|
||||||
return ERROR_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ulTaskNotifyTake(pdTRUE, timeout) == 0) {
|
|
||||||
return ERROR_TIMEOUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single-consumer by design (one task per subscription), so a wakeup implies the event
|
|
||||||
// this call was notified for is still there for us to pop.
|
|
||||||
return try_pop(sub, out_event) ? ERROR_NONE : ERROR_TIMEOUT;
|
return try_pop(sub, out_event) ? ERROR_NONE : ERROR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <service/manager.h>
|
#include <service/manager.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
#include <tactility/module.h>
|
#include <tactility/module.h>
|
||||||
|
|
||||||
@ -20,7 +21,15 @@ const ModuleSymbol app_module_symbols[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(app_event_subscribe),
|
DEFINE_MODULE_SYMBOL(app_event_subscribe),
|
||||||
DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
|
DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
|
||||||
DEFINE_MODULE_SYMBOL(app_event_emit),
|
DEFINE_MODULE_SYMBOL(app_event_emit),
|
||||||
DEFINE_MODULE_SYMBOL(app_event_await),
|
DEFINE_MODULE_SYMBOL(app_event_poll),
|
||||||
|
// tactility/concurrent/task_event_group - needed to construct the group app_event_subscribe() takes
|
||||||
|
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),
|
||||||
// app/install
|
// app/install
|
||||||
DEFINE_MODULE_SYMBOL(app_get_install_path),
|
DEFINE_MODULE_SYMBOL(app_get_install_path),
|
||||||
DEFINE_MODULE_SYMBOL(app_install),
|
DEFINE_MODULE_SYMBOL(app_install),
|
||||||
|
|||||||
@ -6,10 +6,13 @@
|
|||||||
#include <tactility/delay.h>
|
#include <tactility/delay.h>
|
||||||
#include <tactility/time.h>
|
#include <tactility/time.h>
|
||||||
|
|
||||||
TEST_CASE("app_event_subscribe/_await deliver events in FIFO order") {
|
TEST_CASE("app_event_subscribe/_poll deliver events in FIFO order") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = 1;
|
sub.app_instance_id = 1;
|
||||||
CHECK_EQ(app_event_subscribe(&sub), ERROR_NONE);
|
CHECK_EQ(app_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||||
|
|
||||||
for (uint32_t i = 0; i < 3; i++) {
|
for (uint32_t i = 0; i < 3; i++) {
|
||||||
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } };
|
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } };
|
||||||
@ -18,32 +21,40 @@ TEST_CASE("app_event_subscribe/_await deliver events in FIFO order") {
|
|||||||
|
|
||||||
for (uint32_t i = 0; i < 3; i++) {
|
for (uint32_t i = 0; i < 3; i++) {
|
||||||
AppEvent out {};
|
AppEvent out {};
|
||||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
CHECK_EQ(app_event_poll(&sub, &out), ERROR_NONE);
|
||||||
CHECK_EQ(out.type, APP_EVENT_RESULT);
|
CHECK_EQ(out.type, APP_EVENT_RESULT);
|
||||||
CHECK_EQ(out.result.launch_id, i);
|
CHECK_EQ(out.result.launch_id, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("app_event_emit only delivers to subscriptions for that app_instance_id") {
|
TEST_CASE("app_event_emit only delivers to subscriptions for that app_instance_id") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = 10;
|
sub.app_instance_id = 10;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
CHECK_EQ(app_event_emit(11, &event), ERROR_NOT_FOUND);
|
CHECK_EQ(app_event_emit(11, &event), ERROR_NOT_FOUND);
|
||||||
|
|
||||||
AppEvent out {};
|
AppEvent out {};
|
||||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
CHECK_EQ(app_event_poll(&sub, &out), ERROR_TIMEOUT);
|
||||||
|
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("app_event_emit returns ERROR_RESOURCE and drops the newest event once a subscription's queue is full") {
|
TEST_CASE("app_event_emit returns ERROR_RESOURCE and drops the newest event once a subscription's queue is full") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = 20;
|
sub.app_instance_id = 20;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
|
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
|
||||||
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } };
|
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } };
|
||||||
@ -57,42 +68,55 @@ TEST_CASE("app_event_emit returns ERROR_RESOURCE and drops the newest event once
|
|||||||
// The already-queued events survive, in order, and the dropped one never arrives.
|
// The already-queued events survive, in order, and the dropped one never arrives.
|
||||||
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
|
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
|
||||||
AppEvent out {};
|
AppEvent out {};
|
||||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
CHECK_EQ(app_event_poll(&sub, &out), ERROR_NONE);
|
||||||
CHECK_EQ(out.result.launch_id, i);
|
CHECK_EQ(out.result.launch_id, i);
|
||||||
}
|
}
|
||||||
AppEvent out {};
|
AppEvent out {};
|
||||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
CHECK_EQ(app_event_poll(&sub, &out), ERROR_TIMEOUT);
|
||||||
|
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("app_event_unsubscribe stops further delivery") {
|
TEST_CASE("app_event_unsubscribe stops further delivery") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = 30;
|
sub.app_instance_id = 30;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NONE);
|
CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NONE);
|
||||||
CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||||
|
|
||||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
CHECK_EQ(app_event_emit(30, &event), ERROR_NOT_FOUND);
|
CHECK_EQ(app_event_emit(30, &event), ERROR_NOT_FOUND);
|
||||||
|
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("app_event_await times out when no event has arrived") {
|
TEST_CASE("app_event_poll times out when no event has arrived") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = 40;
|
sub.app_instance_id = 40;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
AppEvent out {};
|
AppEvent out {};
|
||||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
CHECK_EQ(app_event_poll(&sub, &out), ERROR_TIMEOUT);
|
||||||
|
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("app_event_emit stamps the event with the current boot-relative time") {
|
TEST_CASE("app_event_emit stamps the event with the current boot-relative time") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = 50;
|
sub.app_instance_id = 50;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
auto before = static_cast<uint64_t>(get_micros_since_boot());
|
auto before = static_cast<uint64_t>(get_micros_since_boot());
|
||||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
@ -100,17 +124,21 @@ TEST_CASE("app_event_emit stamps the event with the current boot-relative time")
|
|||||||
auto after = static_cast<uint64_t>(get_micros_since_boot());
|
auto after = static_cast<uint64_t>(get_micros_since_boot());
|
||||||
|
|
||||||
AppEvent out {};
|
AppEvent out {};
|
||||||
REQUIRE_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
REQUIRE_EQ(app_event_poll(&sub, &out), ERROR_NONE);
|
||||||
CHECK_GE(out.timestamp, before);
|
CHECK_GE(out.timestamp, before);
|
||||||
CHECK_LE(out.timestamp, after);
|
CHECK_LE(out.timestamp, after);
|
||||||
|
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("app_event_await wakes when the event is emitted from another task") {
|
TEST_CASE("task_event_group_wait wakes when the event is emitted from another task") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = 60;
|
sub.app_instance_id = 60;
|
||||||
CHECK_EQ(app_event_subscribe(&sub), ERROR_NONE);
|
CHECK_EQ(app_event_subscribe(&sub, &event_group), ERROR_NONE);
|
||||||
|
|
||||||
auto* thread = thread_alloc_full(
|
auto* thread = thread_alloc_full(
|
||||||
"app-event-emitter",
|
"app-event-emitter",
|
||||||
@ -126,12 +154,14 @@ TEST_CASE("app_event_await wakes when the event is emitted from another task") {
|
|||||||
);
|
);
|
||||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||||
|
|
||||||
|
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||||
AppEvent out {};
|
AppEvent out {};
|
||||||
CHECK_EQ(app_event_await(&sub, &out, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
CHECK_EQ(app_event_poll(&sub, &out), ERROR_NONE);
|
||||||
CHECK_EQ(out.type, APP_EVENT_CLOSE);
|
CHECK_EQ(out.type, APP_EVENT_CLOSE);
|
||||||
|
|
||||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), 1), ERROR_NONE);
|
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), 1), ERROR_NONE);
|
||||||
thread_free(thread);
|
thread_free(thread);
|
||||||
|
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,21 +66,31 @@ int32_t fake_run(void*, uint32_t app_instance_id, int argc, char* argv[]) {
|
|||||||
return static_cast<int32_t>(strtol(argv[0], nullptr, 10));
|
return static_cast<int32_t>(strtol(argv[0], nullptr, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = app_instance_id;
|
sub.app_instance_id = app_instance_id;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
AppEvent event {};
|
if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
|
||||||
if (app_event_await(&sub, &event, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
|
|
||||||
break; // safety net so a bug here can't hang the test suite
|
break; // safety net so a bug here can't hang the test suite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool done = false;
|
||||||
|
AppEvent event {};
|
||||||
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
done = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (done) break;
|
||||||
|
}
|
||||||
|
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,9 +339,12 @@ TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent,
|
|||||||
REQUIRE_EQ(app_manager_start("test.app.parent", &parent_id), ERROR_NONE);
|
REQUIRE_EQ(app_manager_start("test.app.parent", &parent_id), ERROR_NONE);
|
||||||
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||||
|
|
||||||
|
TaskEventGroup parent_event_group {};
|
||||||
|
task_event_group_construct(&parent_event_group);
|
||||||
|
|
||||||
AppEventSubscription parent_sub {};
|
AppEventSubscription parent_sub {};
|
||||||
parent_sub.app_instance_id = parent_id;
|
parent_sub.app_instance_id = parent_id;
|
||||||
REQUIRE_EQ(app_event_subscribe(&parent_sub), ERROR_NONE);
|
REQUIRE_EQ(app_event_subscribe(&parent_sub, &parent_event_group), ERROR_NONE);
|
||||||
|
|
||||||
const char* argv[] = { "42" };
|
const char* argv[] = { "42" };
|
||||||
uint32_t child_id = 0;
|
uint32_t child_id = 0;
|
||||||
@ -340,13 +353,15 @@ TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent,
|
|||||||
// Launching a modal child never touches the parent's own task/state.
|
// Launching a modal child never touches the parent's own task/state.
|
||||||
CHECK_EQ(app_manager_get_state(parent_id), APP_INSTANCE_STATE_ACTIVE);
|
CHECK_EQ(app_manager_get_state(parent_id), APP_INSTANCE_STATE_ACTIVE);
|
||||||
|
|
||||||
|
REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
REQUIRE_EQ(app_event_await(&parent_sub, &event, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE);
|
||||||
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
||||||
CHECK_EQ(event.result.launch_id, child_id);
|
CHECK_EQ(event.result.launch_id, child_id);
|
||||||
CHECK_EQ(event.result.result, 42);
|
CHECK_EQ(event.result.result, 42);
|
||||||
|
|
||||||
app_event_unsubscribe(&parent_sub);
|
app_event_unsubscribe(&parent_sub);
|
||||||
|
task_event_group_destruct(&parent_event_group);
|
||||||
app_manager_stop(child_id);
|
app_manager_stop(child_id);
|
||||||
app_manager_stop(parent_id);
|
app_manager_stop(parent_id);
|
||||||
app_manager_remove("test.app.parent");
|
app_manager_remove("test.app.parent");
|
||||||
@ -365,9 +380,12 @@ TEST_CASE("app_manager_start_for_result delivers the child's own return value as
|
|||||||
REQUIRE_EQ(app_manager_start("test.app.parent2", &parent_id), ERROR_NONE);
|
REQUIRE_EQ(app_manager_start("test.app.parent2", &parent_id), ERROR_NONE);
|
||||||
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||||
|
|
||||||
|
TaskEventGroup parent_event_group {};
|
||||||
|
task_event_group_construct(&parent_event_group);
|
||||||
|
|
||||||
AppEventSubscription parent_sub {};
|
AppEventSubscription parent_sub {};
|
||||||
parent_sub.app_instance_id = parent_id;
|
parent_sub.app_instance_id = parent_id;
|
||||||
REQUIRE_EQ(app_event_subscribe(&parent_sub), ERROR_NONE);
|
REQUIRE_EQ(app_event_subscribe(&parent_sub, &parent_event_group), ERROR_NONE);
|
||||||
|
|
||||||
uint32_t child_id = 0;
|
uint32_t child_id = 0;
|
||||||
// No parameters - fake_run falls through to its normal CLOSE loop instead of acting as a
|
// No parameters - fake_run falls through to its normal CLOSE loop instead of acting as a
|
||||||
@ -377,13 +395,15 @@ TEST_CASE("app_manager_start_for_result delivers the child's own return value as
|
|||||||
|
|
||||||
app_manager_stop(child_id); // force-close
|
app_manager_stop(child_id); // force-close
|
||||||
|
|
||||||
|
REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
REQUIRE_EQ(app_event_await(&parent_sub, &event, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE);
|
||||||
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
||||||
CHECK_EQ(event.result.launch_id, child_id);
|
CHECK_EQ(event.result.launch_id, child_id);
|
||||||
CHECK_EQ(event.result.result, 0); // fake_run's CLOSE loop always returns 0
|
CHECK_EQ(event.result.result, 0); // fake_run's CLOSE loop always returns 0
|
||||||
|
|
||||||
app_event_unsubscribe(&parent_sub);
|
app_event_unsubscribe(&parent_sub);
|
||||||
|
task_event_group_destruct(&parent_event_group);
|
||||||
app_manager_stop(parent_id);
|
app_manager_stop(parent_id);
|
||||||
app_manager_remove("test.app.parent2");
|
app_manager_remove("test.app.parent2");
|
||||||
app_manager_remove("test.app.child2");
|
app_manager_remove("test.app.child2");
|
||||||
|
|||||||
72
Modules/app-module/tests/source/combined_event_test.cpp
Normal file
72
Modules/app-module/tests/source/combined_event_test.cpp
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#include "doctest.h"
|
||||||
|
|
||||||
|
#include <app/event.h>
|
||||||
|
|
||||||
|
#include <tactility/system_event.h>
|
||||||
|
|
||||||
|
// Regression coverage for the primary motivation behind TaskEventGroup: a task subscribed to
|
||||||
|
// both an app_event and a system_event must be able to block once and wake for either, without
|
||||||
|
// losing an event or regressing either subsystem's own delivery semantics (FIFO for app_event,
|
||||||
|
// coalescing for system_event).
|
||||||
|
|
||||||
|
TEST_CASE("a task can wait on app_event and system_event together via one TaskEventGroup") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
|
AppEventSubscription app_sub {};
|
||||||
|
app_sub.app_instance_id = 100;
|
||||||
|
CHECK_EQ(app_event_subscribe(&app_sub, &event_group), ERROR_NONE);
|
||||||
|
|
||||||
|
SystemEventSubscription sys_sub {};
|
||||||
|
sys_sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
|
||||||
|
CHECK_EQ(system_event_subscribe(&sys_sub, &event_group), ERROR_NONE);
|
||||||
|
|
||||||
|
// Distinct bits, so a combined wait can tell (via out_flags) which source(s) fired.
|
||||||
|
CHECK_NE(app_sub.bit, sys_sub.bit);
|
||||||
|
|
||||||
|
uint32_t both_bits = app_sub.bit | sys_sub.bit;
|
||||||
|
|
||||||
|
// Only app_event fires: combined wait matches just that bit, and only app_event_poll()
|
||||||
|
// finds something to pop.
|
||||||
|
AppEvent emitted { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
|
CHECK_EQ(app_event_emit(100, &emitted), ERROR_NONE);
|
||||||
|
|
||||||
|
uint32_t out_flags = 0;
|
||||||
|
CHECK_EQ(task_event_group_wait(&event_group, both_bits, false, &out_flags, 0), ERROR_NONE);
|
||||||
|
CHECK_EQ(out_flags, app_sub.bit);
|
||||||
|
|
||||||
|
AppEvent app_out {};
|
||||||
|
CHECK_EQ(app_event_poll(&app_sub, &app_out), ERROR_NONE);
|
||||||
|
CHECK_EQ(app_out.type, APP_EVENT_CLOSE);
|
||||||
|
CHECK_EQ(system_event_poll(&sys_sub), ERROR_TIMEOUT);
|
||||||
|
|
||||||
|
// Only system_event fires: combined wait matches just that bit, and only
|
||||||
|
// system_event_poll() finds something pending.
|
||||||
|
CHECK_EQ(system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0), ERROR_NONE);
|
||||||
|
|
||||||
|
out_flags = 0;
|
||||||
|
CHECK_EQ(task_event_group_wait(&event_group, both_bits, false, &out_flags, 0), ERROR_NONE);
|
||||||
|
CHECK_EQ(out_flags, sys_sub.bit);
|
||||||
|
|
||||||
|
CHECK_EQ(system_event_poll(&sys_sub), ERROR_NONE);
|
||||||
|
AppEvent app_out2 {};
|
||||||
|
CHECK_EQ(app_event_poll(&app_sub, &app_out2), ERROR_TIMEOUT);
|
||||||
|
|
||||||
|
// Both fire before the wait: combined wait matches both bits, and both subsystems' own
|
||||||
|
// polls (which re-check their own state rather than trusting the bit) still deliver.
|
||||||
|
CHECK_EQ(app_event_emit(100, &emitted), ERROR_NONE);
|
||||||
|
CHECK_EQ(system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0), ERROR_NONE);
|
||||||
|
|
||||||
|
out_flags = 0;
|
||||||
|
CHECK_EQ(task_event_group_wait(&event_group, both_bits, false, &out_flags, 0), ERROR_NONE);
|
||||||
|
CHECK_EQ(out_flags, both_bits);
|
||||||
|
|
||||||
|
CHECK_EQ(app_event_poll(&app_sub, &app_out), ERROR_NONE);
|
||||||
|
CHECK_EQ(system_event_poll(&sys_sub), ERROR_NONE);
|
||||||
|
|
||||||
|
// Unsubscribe order (system_event before app_event) must not matter - the group outlives
|
||||||
|
// both and is destructed last.
|
||||||
|
system_event_unsubscribe(&sys_sub);
|
||||||
|
app_event_unsubscribe(&app_sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ idf_component_register(
|
|||||||
SRCS ${SOURCES}
|
SRCS ${SOURCES}
|
||||||
INCLUDE_DIRS "include/"
|
INCLUDE_DIRS "include/"
|
||||||
PRIV_INCLUDE_DIRS "private/"
|
PRIV_INCLUDE_DIRS "private/"
|
||||||
REQUIRES TactilityKernel driver esp_adc esp_driver_i2c esp_lcd vfs fatfs esp_wifi esp_netif esp_event
|
REQUIRES TactilityKernel TactilityKernelCpp driver esp_adc esp_driver_i2c esp_lcd vfs fatfs esp_wifi esp_netif esp_event
|
||||||
PRIV_REQUIRES ${PRIV_REQUIRES_LIST}
|
PRIV_REQUIRES ${PRIV_REQUIRES_LIST}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,8 @@
|
|||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
#include <tactility/time.h>
|
#include <tactility/time.h>
|
||||||
|
|
||||||
|
#include <TactilityCpp/Allocator.h>
|
||||||
|
|
||||||
#if defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
#if defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
||||||
#include <tactility/drivers/esp32_esp_hosted_ota.h>
|
#include <tactility/drivers/esp32_esp_hosted_ota.h>
|
||||||
#endif
|
#endif
|
||||||
@ -33,14 +35,8 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr size_t WIFI_MAX_CALLBACKS = 4;
|
|
||||||
constexpr uint16_t WIFI_SCAN_RECORD_LIMIT = 32;
|
constexpr uint16_t WIFI_SCAN_RECORD_LIMIT = 32;
|
||||||
|
|
||||||
struct WifiCallbackEntry {
|
|
||||||
WifiEventCallback fn = nullptr;
|
|
||||||
void* ctx = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Esp32WifiCtx {
|
struct Esp32WifiCtx {
|
||||||
Device* device = nullptr;
|
Device* device = nullptr;
|
||||||
|
|
||||||
@ -66,9 +62,8 @@ struct Esp32WifiCtx {
|
|||||||
int32_t lastEventId = -1;
|
int32_t lastEventId = -1;
|
||||||
TickType_t lastEventTick = 0;
|
TickType_t lastEventTick = 0;
|
||||||
|
|
||||||
Mutex callbackMutex{};
|
Mutex subscriptionsMutex{};
|
||||||
WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {};
|
WifiEventSubscription* subscriptions = nullptr;
|
||||||
size_t callbackCount = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#define GET_CTX(device) (static_cast<Esp32WifiCtx*>(device_get_driver_data(device)))
|
#define GET_CTX(device) (static_cast<Esp32WifiCtx*>(device_get_driver_data(device)))
|
||||||
@ -93,16 +88,18 @@ WifiAuthenticationType to_wifi_authentication_type(wifi_auth_mode_t mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void fire_event(Esp32WifiCtx* ctx, WifiEvent event) {
|
void fire_event(Esp32WifiCtx* ctx, WifiEvent event) {
|
||||||
WifiCallbackEntry local[WIFI_MAX_CALLBACKS];
|
mutex_lock(&ctx->subscriptionsMutex);
|
||||||
size_t count;
|
for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr; sub = sub->internal.next) {
|
||||||
mutex_lock(&ctx->callbackMutex);
|
mutex_lock(&sub->internal.ring_mutex);
|
||||||
count = ctx->callbackCount;
|
if (sub->internal.count < WIFI_EVENT_QUEUE_CAPACITY) {
|
||||||
memcpy(local, ctx->callbacks, count * sizeof(WifiCallbackEntry));
|
uint8_t tail = (sub->internal.head + sub->internal.count) % WIFI_EVENT_QUEUE_CAPACITY;
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
sub->internal.queue[tail] = event;
|
||||||
|
sub->internal.count++;
|
||||||
for (size_t i = 0; i < count; i++) {
|
|
||||||
local[i].fn(ctx->device, local[i].ctx, event);
|
|
||||||
}
|
}
|
||||||
|
mutex_unlock(&sub->internal.ring_mutex);
|
||||||
|
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||||
|
}
|
||||||
|
mutex_unlock(&ctx->subscriptionsMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- ESP-IDF event handling (runs on the esp_event task) ----
|
// ---- ESP-IDF event handling (runs on the esp_event task) ----
|
||||||
@ -257,6 +254,11 @@ error_t bring_up_wifi(Esp32WifiCtx* ctx) {
|
|||||||
ctx->radioState = WIFI_RADIO_STATE_ON;
|
ctx->radioState = WIFI_RADIO_STATE_ON;
|
||||||
mutex_unlock(&ctx->mutex);
|
mutex_unlock(&ctx->mutex);
|
||||||
|
|
||||||
|
WifiEvent radio_event = {};
|
||||||
|
radio_event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED;
|
||||||
|
radio_event.radio_state = WIFI_RADIO_STATE_ON;
|
||||||
|
fire_event(ctx, radio_event);
|
||||||
|
|
||||||
LOG_I(TAG, "WiFi radio on");
|
LOG_I(TAG, "WiFi radio on");
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
@ -306,6 +308,11 @@ void bring_down_wifi(Esp32WifiCtx* ctx) {
|
|||||||
ctx->radioState = WIFI_RADIO_STATE_OFF;
|
ctx->radioState = WIFI_RADIO_STATE_OFF;
|
||||||
mutex_unlock(&ctx->mutex);
|
mutex_unlock(&ctx->mutex);
|
||||||
|
|
||||||
|
WifiEvent radio_event = {};
|
||||||
|
radio_event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED;
|
||||||
|
radio_event.radio_state = WIFI_RADIO_STATE_OFF;
|
||||||
|
fire_event(ctx, radio_event);
|
||||||
|
|
||||||
LOG_I(TAG, "WiFi radio off");
|
LOG_I(TAG, "WiFi radio off");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,38 +504,69 @@ error_t api_station_get_rssi(Device* device, int32_t* rssi) {
|
|||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t api_add_event_callback(Device* device, void* callback_context, WifiEventCallback callback) {
|
error_t api_set_radio_on(Device* device) {
|
||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT;
|
if (ctx == nullptr) return ERROR_INVALID_STATE;
|
||||||
|
|
||||||
mutex_lock(&ctx->callbackMutex);
|
mutex_lock(&ctx->mutex);
|
||||||
if (ctx->callbackCount >= WIFI_MAX_CALLBACKS) {
|
bool already_on = ctx->radioState == WIFI_RADIO_STATE_ON;
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
mutex_unlock(&ctx->mutex);
|
||||||
return ERROR_OUT_OF_MEMORY;
|
if (already_on) return ERROR_NONE;
|
||||||
}
|
|
||||||
ctx->callbacks[ctx->callbackCount] = { .fn = callback, .ctx = callback_context };
|
return bring_up_wifi(ctx);
|
||||||
ctx->callbackCount++;
|
}
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
|
||||||
|
error_t api_set_radio_off(Device* device) {
|
||||||
|
auto* ctx = GET_CTX(device);
|
||||||
|
if (ctx == nullptr) return ERROR_INVALID_STATE;
|
||||||
|
|
||||||
|
mutex_lock(&ctx->mutex);
|
||||||
|
bool already_off = ctx->radioState == WIFI_RADIO_STATE_OFF;
|
||||||
|
mutex_unlock(&ctx->mutex);
|
||||||
|
if (already_off) return ERROR_NONE;
|
||||||
|
|
||||||
|
bring_down_wifi(ctx);
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t api_remove_event_callback(Device* device, WifiEventCallback callback) {
|
error_t api_event_subscribe(Device* device, WifiEventSubscription* sub, TaskEventGroup* event_group) {
|
||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT;
|
if (ctx == nullptr || sub == nullptr || event_group == nullptr) return ERROR_INVALID_ARGUMENT;
|
||||||
|
|
||||||
mutex_lock(&ctx->callbackMutex);
|
uint32_t bit;
|
||||||
for (size_t i = 0; i < ctx->callbackCount; i++) {
|
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
|
||||||
if (ctx->callbacks[i].fn == callback) {
|
if (claim_result != ERROR_NONE) {
|
||||||
for (size_t j = i; j + 1 < ctx->callbackCount; j++) {
|
return claim_result;
|
||||||
ctx->callbacks[j] = ctx->callbacks[j + 1];
|
|
||||||
}
|
}
|
||||||
ctx->callbackCount--;
|
sub->internal.event_group = event_group;
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
sub->bit = bit;
|
||||||
|
|
||||||
|
mutex_lock(&ctx->subscriptionsMutex);
|
||||||
|
sub->internal.next = ctx->subscriptions;
|
||||||
|
ctx->subscriptions = sub;
|
||||||
|
mutex_unlock(&ctx->subscriptionsMutex);
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t api_event_unsubscribe(Device* device, WifiEventSubscription* sub) {
|
||||||
|
auto* ctx = GET_CTX(device);
|
||||||
|
if (ctx == nullptr || sub == nullptr) return ERROR_INVALID_ARGUMENT;
|
||||||
|
|
||||||
|
error_t result = ERROR_NOT_FOUND;
|
||||||
|
mutex_lock(&ctx->subscriptionsMutex);
|
||||||
|
for (WifiEventSubscription** link = &ctx->subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
||||||
|
if (*link == sub) {
|
||||||
|
*link = sub->internal.next;
|
||||||
|
result = ERROR_NONE;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
mutex_unlock(&ctx->subscriptionsMutex);
|
||||||
return ERROR_NOT_FOUND;
|
|
||||||
|
if (result == ERROR_NONE) {
|
||||||
|
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t api_get_firmware_ops(Device* /*device*/, const FirmwareOps** ops, void** ctx) {
|
error_t api_get_firmware_ops(Device* /*device*/, const FirmwareOps** ops, void** ctx) {
|
||||||
@ -550,6 +588,8 @@ error_t api_get_firmware_ops(Device* /*device*/, const FirmwareOps** ops, void**
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WifiApi esp32_wifi_api = {
|
const WifiApi esp32_wifi_api = {
|
||||||
|
.set_radio_on = api_set_radio_on,
|
||||||
|
.set_radio_off = api_set_radio_off,
|
||||||
.get_radio_state = api_get_radio_state,
|
.get_radio_state = api_get_radio_state,
|
||||||
.get_station_state = api_get_station_state,
|
.get_station_state = api_get_station_state,
|
||||||
.get_access_point_state = api_get_access_point_state,
|
.get_access_point_state = api_get_access_point_state,
|
||||||
@ -561,34 +601,23 @@ const WifiApi esp32_wifi_api = {
|
|||||||
.station_connect = api_station_connect,
|
.station_connect = api_station_connect,
|
||||||
.station_disconnect = api_station_disconnect,
|
.station_disconnect = api_station_disconnect,
|
||||||
.station_get_rssi = api_station_get_rssi,
|
.station_get_rssi = api_station_get_rssi,
|
||||||
.add_event_callback = api_add_event_callback,
|
.event_subscribe = api_event_subscribe,
|
||||||
.remove_event_callback = api_remove_event_callback,
|
.event_unsubscribe = api_event_unsubscribe,
|
||||||
.get_firmware_ops = api_get_firmware_ops
|
.get_firmware_ops = api_get_firmware_ops
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- Driver lifecycle ----
|
|
||||||
// The ESP-IDF WiFi stack isn't touched until the device is actually started:
|
|
||||||
// registering this driver (module start()) only makes it available for
|
|
||||||
// binding, it doesn't spin up any resources.
|
|
||||||
|
|
||||||
error_t start_device(Device* device) {
|
error_t start_device(Device* device) {
|
||||||
auto* ctx = new(std::nothrow) Esp32WifiCtx();
|
// Prefers PSRAM/SPIRAM, falling back to internal RAM when unavailable - aborts on true OOM
|
||||||
if (ctx == nullptr) return ERROR_OUT_OF_MEMORY;
|
// (see OptExternalAllocator's own doc), so no null check here.
|
||||||
|
tt::OptExternalAllocator<Esp32WifiCtx> allocator;
|
||||||
|
auto* ctx = allocator.allocate(1);
|
||||||
|
new (ctx) Esp32WifiCtx();
|
||||||
|
|
||||||
ctx->device = device;
|
ctx->device = device;
|
||||||
mutex_construct(&ctx->mutex);
|
mutex_construct(&ctx->mutex);
|
||||||
mutex_construct(&ctx->callbackMutex);
|
mutex_construct(&ctx->subscriptionsMutex);
|
||||||
device_set_driver_data(device, ctx);
|
device_set_driver_data(device, ctx);
|
||||||
|
|
||||||
error_t result = bring_up_wifi(ctx);
|
|
||||||
if (result != ERROR_NONE) {
|
|
||||||
device_set_driver_data(device, nullptr);
|
|
||||||
mutex_destruct(&ctx->callbackMutex);
|
|
||||||
mutex_destruct(&ctx->mutex);
|
|
||||||
delete ctx;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,12 +625,15 @@ error_t stop_device(Device* device) {
|
|||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr) return ERROR_NONE;
|
if (ctx == nullptr) return ERROR_NONE;
|
||||||
|
|
||||||
|
if (ctx->radioState == WIFI_RADIO_STATE_ON) {
|
||||||
bring_down_wifi(ctx);
|
bring_down_wifi(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
device_set_driver_data(device, nullptr);
|
device_set_driver_data(device, nullptr);
|
||||||
mutex_destruct(&ctx->callbackMutex);
|
mutex_destruct(&ctx->subscriptionsMutex);
|
||||||
mutex_destruct(&ctx->mutex);
|
mutex_destruct(&ctx->mutex);
|
||||||
delete ctx;
|
ctx->~Esp32WifiCtx();
|
||||||
|
tt::OptExternalAllocator<Esp32WifiCtx>().deallocate(ctx, 1);
|
||||||
|
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,12 @@ struct Esp32WifiPinnedCtx {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#define GET_CTX(device) (static_cast<Esp32WifiPinnedCtx*>(device_get_driver_data(device)))
|
#define GET_CTX(device) (static_cast<Esp32WifiPinnedCtx*>(device_get_driver_data(device)))
|
||||||
|
// Only for event_subscribe()/event_unsubscribe(): unlike the rest of WifiApi, have
|
||||||
|
// construct/destruct side effects on sub->ring_mutex at the wifi_event_subscribe()/
|
||||||
|
// wifi_event_unsubscribe() kernel-wrapper layer (see wifi.cpp) - calling that wrapper AGAIN here
|
||||||
|
// (instead of the child driver's raw vtable entry) double-runs those side effects, corrupting the
|
||||||
|
// mutex. Every other WifiApi call below is a plain pass-through.
|
||||||
|
#define CHILD_WIFI_API(child) ((const struct WifiApi*)device_get_driver(child)->api)
|
||||||
|
|
||||||
struct MarshalledCall {
|
struct MarshalledCall {
|
||||||
const std::function<void()>* work;
|
const std::function<void()>* work;
|
||||||
@ -176,20 +182,44 @@ error_t api_station_get_rssi(Device* device, int32_t* rssi) {
|
|||||||
return wifi_station_get_rssi(ctx->child, rssi);
|
return wifi_station_get_rssi(ctx->child, rssi);
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t api_add_event_callback(Device* device, void* callback_context, WifiEventCallback callback) {
|
error_t api_event_subscribe(Device* device, WifiEventSubscription* sub, TaskEventGroup* event_group) {
|
||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
||||||
return wifi_add_event_callback(ctx->child, callback_context, callback);
|
return CHILD_WIFI_API(ctx->child)->event_subscribe(ctx->child, sub, event_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t api_remove_event_callback(Device* device, WifiEventCallback callback) {
|
error_t api_event_unsubscribe(Device* device, WifiEventSubscription* sub) {
|
||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
||||||
return wifi_remove_event_callback(ctx->child, callback);
|
return CHILD_WIFI_API(ctx->child)->event_unsubscribe(ctx->child, sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- WifiApi: state-changing calls are marshalled onto the pinned WiFi task ----
|
// ---- WifiApi: state-changing calls are marshalled onto the pinned WiFi task ----
|
||||||
|
|
||||||
|
error_t api_set_radio_on(Device* device) {
|
||||||
|
auto* ctx = GET_CTX(device);
|
||||||
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
||||||
|
|
||||||
|
error_t result = ERROR_NONE;
|
||||||
|
Device* child = ctx->child;
|
||||||
|
error_t err = run_on_pinned_thread(ctx, [child, &result]() {
|
||||||
|
result = wifi_set_radio_on(child);
|
||||||
|
});
|
||||||
|
return err != ERROR_NONE ? err : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t api_set_radio_off(Device* device) {
|
||||||
|
auto* ctx = GET_CTX(device);
|
||||||
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
||||||
|
|
||||||
|
error_t result = ERROR_NONE;
|
||||||
|
Device* child = ctx->child;
|
||||||
|
error_t err = run_on_pinned_thread(ctx, [child, &result]() {
|
||||||
|
result = wifi_set_radio_off(child);
|
||||||
|
});
|
||||||
|
return err != ERROR_NONE ? err : result;
|
||||||
|
}
|
||||||
|
|
||||||
error_t api_scan(Device* device) {
|
error_t api_scan(Device* device) {
|
||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
||||||
@ -227,6 +257,8 @@ error_t api_station_disconnect(Device* device) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WifiApi esp32_wifi_pinned_api = {
|
const WifiApi esp32_wifi_pinned_api = {
|
||||||
|
.set_radio_on = api_set_radio_on,
|
||||||
|
.set_radio_off = api_set_radio_off,
|
||||||
.get_radio_state = api_get_radio_state,
|
.get_radio_state = api_get_radio_state,
|
||||||
.get_station_state = api_get_station_state,
|
.get_station_state = api_get_station_state,
|
||||||
.get_access_point_state = api_get_access_point_state,
|
.get_access_point_state = api_get_access_point_state,
|
||||||
@ -238,15 +270,15 @@ const WifiApi esp32_wifi_pinned_api = {
|
|||||||
.station_connect = api_station_connect,
|
.station_connect = api_station_connect,
|
||||||
.station_disconnect = api_station_disconnect,
|
.station_disconnect = api_station_disconnect,
|
||||||
.station_get_rssi = api_station_get_rssi,
|
.station_get_rssi = api_station_get_rssi,
|
||||||
.add_event_callback = api_add_event_callback,
|
.event_subscribe = api_event_subscribe,
|
||||||
.remove_event_callback = api_remove_event_callback
|
.event_unsubscribe = api_event_unsubscribe
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- Driver lifecycle ----
|
// ---- Driver lifecycle ----
|
||||||
// Starting/stopping the child (which brings the ESP-IDF WiFi stack up/down)
|
// Starting/stopping the child (allocating/freeing its bookkeeping - see esp32_wifi.cpp's own
|
||||||
// also happens on the pinned thread, since esp_wifi_init/start/stop/deinit
|
// start_device()/stop_device() comment) happens on the pinned thread for the same core-affinity
|
||||||
// are subject to the same core-affinity requirement as the state-changing
|
// reason as the state-changing WifiApi calls above; enable()/disable() (which actually bring the
|
||||||
// WifiApi calls above.
|
// ESP-IDF WiFi stack up/down) are marshalled the same way.
|
||||||
|
|
||||||
error_t start_device(Device* device) {
|
error_t start_device(Device* device) {
|
||||||
auto* ctx = new(std::nothrow) Esp32WifiPinnedCtx();
|
auto* ctx = new(std::nothrow) Esp32WifiPinnedCtx();
|
||||||
|
|||||||
@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr size_t WIFI_MAX_CALLBACKS = 4;
|
|
||||||
|
|
||||||
struct MockApRecord {
|
struct MockApRecord {
|
||||||
const char* ssid;
|
const char* ssid;
|
||||||
int8_t rssi;
|
int8_t rssi;
|
||||||
@ -40,11 +38,6 @@ constexpr size_t MOCK_SCAN_RESULT_COUNT = sizeof(MOCK_SCAN_RESULTS) / sizeof(MOC
|
|||||||
constexpr int8_t MOCK_CONNECTED_RSSI = -30;
|
constexpr int8_t MOCK_CONNECTED_RSSI = -30;
|
||||||
constexpr const char* MOCK_IPV4_ADDRESS = "192.168.1.2";
|
constexpr const char* MOCK_IPV4_ADDRESS = "192.168.1.2";
|
||||||
|
|
||||||
struct WifiCallbackEntry {
|
|
||||||
WifiEventCallback fn = nullptr;
|
|
||||||
void* ctx = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PosixWifiCtx {
|
struct PosixWifiCtx {
|
||||||
Device* device = nullptr;
|
Device* device = nullptr;
|
||||||
|
|
||||||
@ -54,24 +47,25 @@ struct PosixWifiCtx {
|
|||||||
bool scanning = false;
|
bool scanning = false;
|
||||||
char targetSsid[33] = {};
|
char targetSsid[33] = {};
|
||||||
|
|
||||||
Mutex callbackMutex {};
|
Mutex subscriptionsMutex {};
|
||||||
WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {};
|
WifiEventSubscription* subscriptions = nullptr;
|
||||||
size_t callbackCount = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#define GET_CTX(device) (static_cast<PosixWifiCtx*>(device_get_driver_data(device)))
|
#define GET_CTX(device) (static_cast<PosixWifiCtx*>(device_get_driver_data(device)))
|
||||||
|
|
||||||
void fireEvent(PosixWifiCtx* ctx, WifiEvent event) {
|
void fireEvent(PosixWifiCtx* ctx, WifiEvent event) {
|
||||||
WifiCallbackEntry local[WIFI_MAX_CALLBACKS];
|
mutex_lock(&ctx->subscriptionsMutex);
|
||||||
size_t count;
|
for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr; sub = sub->next) {
|
||||||
mutex_lock(&ctx->callbackMutex);
|
mutex_lock(&sub->ring_mutex);
|
||||||
count = ctx->callbackCount;
|
if (sub->count < WIFI_EVENT_QUEUE_CAPACITY) {
|
||||||
memcpy(local, ctx->callbacks, count * sizeof(WifiCallbackEntry));
|
uint8_t tail = (sub->head + sub->count) % WIFI_EVENT_QUEUE_CAPACITY;
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
sub->queue[tail] = event;
|
||||||
|
sub->count++;
|
||||||
for (size_t i = 0; i < count; i++) {
|
|
||||||
local[i].fn(ctx->device, local[i].ctx, event);
|
|
||||||
}
|
}
|
||||||
|
mutex_unlock(&sub->ring_mutex);
|
||||||
|
task_event_group_signal(sub->event_group, sub->bit);
|
||||||
|
}
|
||||||
|
mutex_unlock(&ctx->subscriptionsMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- WifiApi ----
|
// ---- WifiApi ----
|
||||||
@ -247,41 +241,89 @@ error_t apiStationGetRssi(Device* device, int32_t* rssi) {
|
|||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t apiAddEventCallback(Device* device, void* callback_context, WifiEventCallback callback) {
|
error_t apiSetRadioOn(Device* device) {
|
||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT;
|
if (ctx == nullptr) return ERROR_INVALID_STATE;
|
||||||
|
|
||||||
mutex_lock(&ctx->callbackMutex);
|
mutex_lock(&ctx->mutex);
|
||||||
if (ctx->callbackCount >= WIFI_MAX_CALLBACKS) {
|
bool already_on = ctx->radioState == WIFI_RADIO_STATE_ON;
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
ctx->radioState = WIFI_RADIO_STATE_ON;
|
||||||
return ERROR_OUT_OF_MEMORY;
|
mutex_unlock(&ctx->mutex);
|
||||||
}
|
if (already_on) return ERROR_NONE;
|
||||||
ctx->callbacks[ctx->callbackCount] = { .fn = callback, .ctx = callback_context };
|
|
||||||
ctx->callbackCount++;
|
WifiEvent radio_event = {};
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
radio_event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED;
|
||||||
|
radio_event.radio_state = WIFI_RADIO_STATE_ON;
|
||||||
|
fireEvent(ctx, radio_event);
|
||||||
|
|
||||||
|
LOG_I(TAG, "WiFi radio on (mock)");
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t apiRemoveEventCallback(Device* device, WifiEventCallback callback) {
|
error_t apiSetRadioOff(Device* device) {
|
||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT;
|
if (ctx == nullptr) return ERROR_INVALID_STATE;
|
||||||
|
|
||||||
mutex_lock(&ctx->callbackMutex);
|
mutex_lock(&ctx->mutex);
|
||||||
for (size_t i = 0; i < ctx->callbackCount; i++) {
|
bool already_off = ctx->radioState == WIFI_RADIO_STATE_OFF;
|
||||||
if (ctx->callbacks[i].fn == callback) {
|
ctx->radioState = WIFI_RADIO_STATE_OFF;
|
||||||
for (size_t j = i; j + 1 < ctx->callbackCount; j++) {
|
ctx->stationState = WIFI_STATION_STATE_DISCONNECTED;
|
||||||
ctx->callbacks[j] = ctx->callbacks[j + 1];
|
ctx->scanning = false;
|
||||||
}
|
mutex_unlock(&ctx->mutex);
|
||||||
ctx->callbackCount--;
|
if (already_off) return ERROR_NONE;
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
|
||||||
|
WifiEvent radio_event = {};
|
||||||
|
radio_event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED;
|
||||||
|
radio_event.radio_state = WIFI_RADIO_STATE_OFF;
|
||||||
|
fireEvent(ctx, radio_event);
|
||||||
|
|
||||||
|
LOG_I(TAG, "WiFi radio off (mock)");
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t apiEventSubscribe(Device* device, WifiEventSubscription* sub, TaskEventGroup* event_group) {
|
||||||
|
auto* ctx = GET_CTX(device);
|
||||||
|
if (ctx == nullptr || sub == nullptr || event_group == nullptr) return ERROR_INVALID_ARGUMENT;
|
||||||
|
|
||||||
|
uint32_t bit;
|
||||||
|
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
|
||||||
|
if (claim_result != ERROR_NONE) {
|
||||||
|
return claim_result;
|
||||||
|
}
|
||||||
|
sub->event_group = event_group;
|
||||||
|
sub->bit = bit;
|
||||||
|
|
||||||
|
mutex_lock(&ctx->subscriptionsMutex);
|
||||||
|
sub->next = ctx->subscriptions;
|
||||||
|
ctx->subscriptions = sub;
|
||||||
|
mutex_unlock(&ctx->subscriptionsMutex);
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t apiEventUnsubscribe(Device* device, WifiEventSubscription* sub) {
|
||||||
|
auto* ctx = GET_CTX(device);
|
||||||
|
if (ctx == nullptr || sub == nullptr) return ERROR_INVALID_ARGUMENT;
|
||||||
|
|
||||||
|
error_t result = ERROR_NOT_FOUND;
|
||||||
|
mutex_lock(&ctx->subscriptionsMutex);
|
||||||
|
for (WifiEventSubscription** link = &ctx->subscriptions; *link != nullptr; link = &(*link)->next) {
|
||||||
|
if (*link == sub) {
|
||||||
|
*link = sub->next;
|
||||||
|
result = ERROR_NONE;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutex_unlock(&ctx->callbackMutex);
|
mutex_unlock(&ctx->subscriptionsMutex);
|
||||||
return ERROR_NOT_FOUND;
|
|
||||||
|
if (result == ERROR_NONE) {
|
||||||
|
task_event_group_release_bit(sub->event_group, sub->bit);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WifiApi posix_wifi_api = {
|
const WifiApi posix_wifi_api = {
|
||||||
|
.set_radio_on = apiSetRadioOn,
|
||||||
|
.set_radio_off = apiSetRadioOff,
|
||||||
.get_radio_state = apiGetRadioState,
|
.get_radio_state = apiGetRadioState,
|
||||||
.get_station_state = apiGetStationState,
|
.get_station_state = apiGetStationState,
|
||||||
.get_access_point_state = apiGetAccessPointState,
|
.get_access_point_state = apiGetAccessPointState,
|
||||||
@ -293,13 +335,14 @@ const WifiApi posix_wifi_api = {
|
|||||||
.station_connect = apiStationConnect,
|
.station_connect = apiStationConnect,
|
||||||
.station_disconnect = apiStationDisconnect,
|
.station_disconnect = apiStationDisconnect,
|
||||||
.station_get_rssi = apiStationGetRssi,
|
.station_get_rssi = apiStationGetRssi,
|
||||||
.add_event_callback = apiAddEventCallback,
|
.event_subscribe = apiEventSubscribe,
|
||||||
.remove_event_callback = apiRemoveEventCallback
|
.event_unsubscribe = apiEventUnsubscribe
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- Driver lifecycle ----
|
// ---- Driver lifecycle ----
|
||||||
// Unlike the real esp32 driver, there's no hardware to bring up: the radio
|
// startDevice()/stopDevice() only allocate/free this driver's bookkeeping (subscriber list
|
||||||
// simply reports itself as ON as soon as the device is started.
|
// included), so event subscribers can stay subscribed across radio on/off toggles - the radio
|
||||||
|
// itself is only touched by apiSetRadioOn()/apiSetRadioOff().
|
||||||
|
|
||||||
error_t startDevice(Device* device) {
|
error_t startDevice(Device* device) {
|
||||||
auto* ctx = new(std::nothrow) PosixWifiCtx();
|
auto* ctx = new(std::nothrow) PosixWifiCtx();
|
||||||
@ -307,12 +350,9 @@ error_t startDevice(Device* device) {
|
|||||||
|
|
||||||
ctx->device = device;
|
ctx->device = device;
|
||||||
mutex_construct(&ctx->mutex);
|
mutex_construct(&ctx->mutex);
|
||||||
mutex_construct(&ctx->callbackMutex);
|
mutex_construct(&ctx->subscriptionsMutex);
|
||||||
ctx->radioState = WIFI_RADIO_STATE_ON;
|
|
||||||
|
|
||||||
device_set_driver_data(device, ctx);
|
device_set_driver_data(device, ctx);
|
||||||
|
|
||||||
LOG_I(TAG, "WiFi radio on (mock)");
|
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,12 +360,15 @@ error_t stopDevice(Device* device) {
|
|||||||
auto* ctx = GET_CTX(device);
|
auto* ctx = GET_CTX(device);
|
||||||
if (ctx == nullptr) return ERROR_NONE;
|
if (ctx == nullptr) return ERROR_NONE;
|
||||||
|
|
||||||
|
if (ctx->radioState == WIFI_RADIO_STATE_ON) {
|
||||||
|
apiSetRadioOff(device);
|
||||||
|
}
|
||||||
|
|
||||||
device_set_driver_data(device, nullptr);
|
device_set_driver_data(device, nullptr);
|
||||||
mutex_destruct(&ctx->callbackMutex);
|
mutex_destruct(&ctx->subscriptionsMutex);
|
||||||
mutex_destruct(&ctx->mutex);
|
mutex_destruct(&ctx->mutex);
|
||||||
delete ctx;
|
delete ctx;
|
||||||
|
|
||||||
LOG_I(TAG, "WiFi radio off (mock)");
|
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,12 +26,6 @@ enum class RadioState {
|
|||||||
Off,
|
Off,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get wifi pubsub that broadcasts Event objects
|
|
||||||
* @return PubSub
|
|
||||||
*/
|
|
||||||
std::shared_ptr<PubSub<WifiEvent>> getPubsub();
|
|
||||||
|
|
||||||
/** @return Get the current radio state */
|
/** @return Get the current radio state */
|
||||||
RadioState getRadioState();
|
RadioState getRadioState();
|
||||||
|
|
||||||
|
|||||||
@ -519,7 +519,6 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) {
|
|||||||
.on_start = onLvglStarted,
|
.on_start = onLvglStarted,
|
||||||
.on_stop = onLvglStopped,
|
.on_stop = onLvglStopped,
|
||||||
.task_priority = THREAD_PRIORITY_HIGHER,
|
.task_priority = THREAD_PRIORITY_HIGHER,
|
||||||
// TODO: Remove Wi-Fi driver callback mechanism and use subscribe/await from wifi app to be able to reduce callstack
|
|
||||||
.task_stack_size = 9120,
|
.task_stack_size = 9120,
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
.task_affinity = getCpuAffinityConfiguration().graphics
|
.task_affinity = getCpuAffinityConfiguration().graphics
|
||||||
|
|||||||
@ -203,18 +203,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -225,10 +228,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,24 +102,32 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.argc = argc;
|
ctx.argc = argc;
|
||||||
ctx.argv = argv;
|
ctx.argv = argv;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return ctx.result;
|
return ctx.result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,18 +114,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -142,10 +145,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ void refresh(Context* ctx);
|
|||||||
|
|
||||||
void onBackPressed(lv_event_t* event) {
|
void onBackPressed(lv_event_t* event) {
|
||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent closeEvent {.type = APP_EVENT_CLOSE, .timestamp = 0, .result = {}};
|
||||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,18 +201,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx;
|
Context ctx;
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -220,10 +223,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -234,7 +240,7 @@ extern const ::AppManifest manifest = {
|
|||||||
.id = "tactility.apphub",
|
.id = "tactility.apphub",
|
||||||
.name = "App Hub",
|
.name = "App Hub",
|
||||||
.category = APP_CATEGORY_SYSTEM,
|
.category = APP_CATEGORY_SYSTEM,
|
||||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }
|
.location = {APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain)}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace tt::app::apphub
|
||||||
|
|||||||
@ -230,18 +230,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -261,10 +264,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,24 +81,32 @@ void createWidgets(lv_obj_t* parent, void*) {
|
|||||||
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||||
appListInstanceId = appInstanceId;
|
appListInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -90,18 +90,21 @@ void createWidgets(lv_obj_t* parent, void*) {
|
|||||||
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||||
appSettingsInstanceId = appInstanceId;
|
appSettingsInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -109,10 +112,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -126,18 +126,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
ctx.wsSettings = settings::webserver::loadOrGetDefault();
|
ctx.wsSettings = settings::webserver::loadOrGetDefault();
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -145,6 +148,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equivalent of the old model's onHide(): persist the ORIGINAL settings (as loaded at
|
// Equivalent of the old model's onHide(): persist the ORIGINAL settings (as loaded at
|
||||||
@ -168,6 +173,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -215,18 +215,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -234,6 +237,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.audioSubscription != nullptr) {
|
if (ctx.audioSubscription != nullptr) {
|
||||||
@ -243,6 +248,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -292,9 +292,12 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
isUsbBootSplash = hal::usb::isUsbBootMode();
|
isUsbBootSplash = hal::usb::isUsbBootMode();
|
||||||
sdCardMissing = false;
|
sdCardMissing = false;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
bootWindowId = window_manager_create(appInstanceId, createSplashWidgets, nullptr);
|
bootWindowId = window_manager_create(appInstanceId, createSplashWidgets, nullptr);
|
||||||
|
|
||||||
@ -304,19 +307,24 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
// startNextApp() above is what triggers that, via app-module's "save the previously active
|
// startNextApp() above is what triggers that, via app-module's "save the previously active
|
||||||
// app" policy, unless sdCardMissing halted before it.
|
// app" policy, unless sdCardMissing halted before it.
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
if (bootWindowId != 0) {
|
if (bootWindowId != 0) {
|
||||||
window_manager_remove(bootWindowId);
|
window_manager_remove(bootWindowId);
|
||||||
}
|
}
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -220,9 +220,12 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.state.updateScanResults();
|
ctx.state.updateScanResults();
|
||||||
ctx.state.updatePairedPeers();
|
ctx.state.updatePairedPeers();
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
@ -243,10 +246,10 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -254,6 +257,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate any BT event dispatched-but-not-yet-run for this instance before doing
|
// Invalidate any BT event dispatched-but-not-yet-run for this instance before doing
|
||||||
@ -271,6 +276,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -199,18 +199,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
device_put(btDevice);
|
device_put(btDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -232,6 +235,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device_get_first_active_by_type(&BLUETOOTH_TYPE, &btDevice) == ERROR_NONE) {
|
if (device_get_first_active_by_type(&BLUETOOTH_TYPE, &btDevice) == ERROR_NONE) {
|
||||||
@ -241,6 +246,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -185,18 +185,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -204,10 +207,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
service::espnow::unsubscribeReceiver(ctx.receiveSubscription);
|
service::espnow::unsubscribeReceiver(ctx.receiveSubscription);
|
||||||
disableEspNow(&ctx);
|
disableEspNow(&ctx);
|
||||||
|
|||||||
@ -153,19 +153,22 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
if (!ctx.hasFatalError) {
|
if (!ctx.hasFatalError) {
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -173,11 +176,14 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
bool continuePressed = ctx.continuePressed;
|
bool continuePressed = ctx.continuePressed;
|
||||||
|
|
||||||
|
|||||||
@ -174,9 +174,12 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
@ -197,10 +200,10 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -208,6 +211,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equivalent of the old model's onHide(): ensure the periodic update isn't already happening.
|
// Equivalent of the old model's onHide(): ensure the periodic update isn't already happening.
|
||||||
@ -217,6 +222,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,18 +30,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
View view(state);
|
View view(state);
|
||||||
CreateContext createContext { &view, appInstanceId };
|
CreateContext createContext { &view, appInstanceId };
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &createContext);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &createContext);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -53,11 +56,14 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.deinit();
|
view.deinit();
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,24 +62,32 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
app_event_emit(appInstanceId, &closeEvent);
|
app_event_emit(appInstanceId, &closeEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return ctx.result;
|
return ctx.result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -267,19 +267,22 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
updateDeviceStates(&ctx);
|
updateDeviceStates(&ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
ctx.timer->start();
|
ctx.timer->start();
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -307,11 +310,14 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.timer->stop();
|
ctx.timer->stop();
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,18 +93,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -112,10 +115,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -388,18 +388,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx;
|
Context ctx;
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
stopScanningIfRunning(&ctx);
|
stopScanningIfRunning(&ctx);
|
||||||
@ -408,10 +411,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,18 +89,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
ctx.filePath = argv[0];
|
ctx.filePath = argv[0];
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -108,10 +111,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,24 +111,32 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.argc = argc;
|
ctx.argc = argc;
|
||||||
ctx.argv = argv;
|
ctx.argv = argv;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return ctx.result;
|
return ctx.result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -308,18 +308,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
persistIfUpdated(ctx);
|
persistIfUpdated(ctx);
|
||||||
@ -328,10 +331,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -209,18 +209,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
persistIfUpdated(ctx);
|
persistIfUpdated(ctx);
|
||||||
@ -229,10 +232,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -234,26 +234,34 @@ void runAutoStart() {
|
|||||||
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||||
runAutoStart();
|
runAutoStart();
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
||||||
|
|
||||||
// The launcher is meant to stay resident (it's the home screen) - it only gives up its
|
// The launcher is meant to stay resident (it's the home screen) - it only gives up its
|
||||||
// thread when app-module's scheduler asks it to (e.g. another new-model app is started).
|
// thread when app-module's scheduler asks it to (e.g. another new-model app is started).
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -145,18 +145,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx;
|
Context ctx;
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -164,10 +167,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -190,18 +190,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.filePath = argv[0];
|
ctx.filePath = argv[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -231,10 +234,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -240,19 +240,22 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
updateUi(&ctx);
|
updateUi(&ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
ctx.timer->start();
|
ctx.timer->start();
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -260,11 +263,14 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.timer->stop();
|
ctx.timer->stop();
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,18 +136,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -155,10 +158,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -250,18 +250,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -269,6 +272,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.updateTimer->isRunning()) {
|
if (ctx.updateTimer->isRunning()) {
|
||||||
@ -277,6 +282,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,24 +104,32 @@ int32_t appMain(AppInstanceId appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.argc = argc;
|
ctx.argc = argc;
|
||||||
ctx.argv = argv;
|
ctx.argv = argv;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return ctx.result;
|
return ctx.result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,24 +79,32 @@ void createWidgets(lv_obj_t* parent, void*) {
|
|||||||
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||||
settingsInstanceId = appInstanceId;
|
settingsInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -159,7 +159,7 @@ void onContinueClicked(lv_event_t* event) {
|
|||||||
markCompleted();
|
markCompleted();
|
||||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: this
|
// Async, non-blocking - must NOT call app_manager_stop() directly here: this
|
||||||
// callback runs ON the LVGL task, and app-lifecycle transitions must happen on this
|
// callback runs ON the LVGL task, and app-lifecycle transitions must happen on this
|
||||||
// app's own thread (woken via app_event_await()), which closes by returning.
|
// app's own thread (woken via app_event_poll()), which closes by returning.
|
||||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||||
break;
|
break;
|
||||||
@ -229,18 +229,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -255,10 +258,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -417,9 +417,12 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
ctx.memoryTimer->start(); // Memory: every 10s
|
ctx.memoryTimer->start(); // Memory: every 10s
|
||||||
@ -427,10 +430,10 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -438,12 +441,15 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.memoryTimer->stop();
|
ctx.memoryTimer->stop();
|
||||||
ctx.tasksTimer->stop();
|
ctx.tasksTimer->stop();
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -157,18 +157,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -191,10 +194,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -240,26 +240,34 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
updateList(&ctx);
|
updateList(&ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
ctx.updateTimer->start();
|
ctx.updateTimer->start();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.updateTimer->stop();
|
ctx.updateTimer->stop();
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return ctx.result;
|
return ctx.result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,7 +174,7 @@ void onPress(lv_event_t* event) {
|
|||||||
|
|
||||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: this callback runs
|
// Async, non-blocking - must NOT call app_manager_stop() directly here: this callback runs
|
||||||
// ON the LVGL task, and app-lifecycle transitions must happen on this app's own thread
|
// ON the LVGL task, and app-lifecycle transitions must happen on this app's own thread
|
||||||
// (woken up via app_event_await() below), which closes by returning. The result (Ok/Error)
|
// (woken up via app_event_poll() below), which closes by returning. The result (Ok/Error)
|
||||||
// is reported by appMain() itself when it returns, based on ctx.calibrationApplied.
|
// is reported by appMain() itself when it returns, based on ctx.calibrationApplied.
|
||||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||||
@ -233,18 +233,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -252,10 +255,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
// finishCalibration() already applied a new calibration on success. On cancel/failure,
|
// finishCalibration() already applied a new calibration on success. On cancel/failure,
|
||||||
// restore whatever calibration was on disk before the block above cleared it.
|
// restore whatever calibration was on disk before the block above cleared it.
|
||||||
|
|||||||
@ -272,18 +272,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
persistIfUpdated(ctx);
|
persistIfUpdated(ctx);
|
||||||
@ -292,10 +295,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,18 +85,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
Context ctx {};
|
Context ctx {};
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -104,10 +107,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -362,18 +362,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.wsSettings.webServerEnabled = service::webserver::isWebServerEnabled();
|
ctx.wsSettings.webServerEnabled = service::webserver::isWebServerEnabled();
|
||||||
ctx.originalSettings = ctx.wsSettings;
|
ctx.originalSettings = ctx.wsSettings;
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
|
|
||||||
AppEvent event {};
|
AppEvent event {};
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -381,6 +384,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equivalent of the old model's onHide().
|
// Equivalent of the old model's onHide().
|
||||||
@ -423,6 +428,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
#include <lvgl/lvgl.h>
|
#include <lvgl/lvgl.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <lvgl/widgets/toolbar.h>
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
|
|
||||||
namespace tt::app::wifiapsettings {
|
namespace tt::app::wifiapsettings {
|
||||||
@ -29,9 +30,14 @@ struct Context {
|
|||||||
lv_obj_t* busySpinner = nullptr;
|
lv_obj_t* busySpinner = nullptr;
|
||||||
lv_obj_t* connectButton = nullptr;
|
lv_obj_t* connectButton = nullptr;
|
||||||
lv_obj_t* disconnectButton = nullptr;
|
lv_obj_t* disconnectButton = nullptr;
|
||||||
|
lv_obj_t* autoConnectSwitch = nullptr;
|
||||||
|
lv_obj_t* forgetButton = nullptr;
|
||||||
|
lv_obj_t* autoConnectWrapper = nullptr;
|
||||||
|
|
||||||
uint32_t forgetDialogId = 0;
|
uint32_t forgetDialogId = 0;
|
||||||
PubSub<service::wifi::WifiEvent>::SubscriptionHandle wifiSubscription = nullptr;
|
// Set once in appMain() before subscribing, left null if this device has no WiFi driver
|
||||||
|
Device* wifiDevice = nullptr;
|
||||||
|
WifiEventSubscription wifiEventSub {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -41,8 +47,7 @@ void onBackPressed(lv_event_t* event) {
|
|||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
// but this callback runs ON the LVGL task, which would deadlock against itself.
|
||||||
// deadlock against itself.
|
|
||||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||||
}
|
}
|
||||||
@ -104,13 +109,34 @@ void updateBusySpinner(Context* ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Touches the filesystem (service::wifi::settings::load()) - callers must not run this on the
|
||||||
|
// LVGL task (see updateViews()'s callers).
|
||||||
|
void updateAutoConnectSection(Context* ctx) {
|
||||||
|
service::wifi::settings::WifiApSettings settings;
|
||||||
|
if (service::wifi::settings::load(ctx->ssid.c_str(), settings)) {
|
||||||
|
if (settings.autoConnect) {
|
||||||
|
lv_obj_add_state(ctx->autoConnectSwitch, LV_STATE_CHECKED);
|
||||||
|
} else {
|
||||||
|
lv_obj_remove_state(ctx->autoConnectSwitch, LV_STATE_CHECKED);
|
||||||
|
}
|
||||||
|
lv_obj_remove_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lv_obj_remove_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "No settings found");
|
||||||
|
lv_obj_add_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lv_obj_add_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs on whichever thread calls it, not necessarily the LVGL task
|
||||||
void updateViews(Context* ctx) {
|
void updateViews(Context* ctx) {
|
||||||
if (ctx->connectButton == nullptr) {
|
if (ctx->connectButton == nullptr) {
|
||||||
// Buried (e.g. the forget confirmation dialog opened on top) - see destroyWidgets().
|
// Buried (e.g. the forget confirmation dialog opened on top)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateConnectButton(ctx);
|
updateConnectButton(ctx);
|
||||||
updateBusySpinner(ctx);
|
updateBusySpinner(ctx);
|
||||||
|
updateAutoConnectSection(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
void requestViewUpdate(Context* ctx) {
|
void requestViewUpdate(Context* ctx) {
|
||||||
@ -119,13 +145,14 @@ void requestViewUpdate(Context* ctx) {
|
|||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs with the LVGL lock already held, possibly on another app's thread - see
|
|
||||||
// WindowDestroyWidgetsFn's warnings. Must stay lock-free: only nulls pointers.
|
|
||||||
void destroyWidgets(void* userData) {
|
void destroyWidgets(void* userData) {
|
||||||
auto* ctx = static_cast<Context*>(userData);
|
auto* ctx = static_cast<Context*>(userData);
|
||||||
ctx->busySpinner = nullptr;
|
ctx->busySpinner = nullptr;
|
||||||
ctx->connectButton = nullptr;
|
ctx->connectButton = nullptr;
|
||||||
ctx->disconnectButton = nullptr;
|
ctx->disconnectButton = nullptr;
|
||||||
|
ctx->autoConnectSwitch = nullptr;
|
||||||
|
ctx->forgetButton = nullptr;
|
||||||
|
ctx->autoConnectWrapper = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||||
@ -162,43 +189,30 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
|||||||
|
|
||||||
// Forget
|
// Forget
|
||||||
|
|
||||||
auto* forget_button = lv_button_create(wrapper);
|
ctx->forgetButton = lv_button_create(wrapper);
|
||||||
lv_obj_set_width(forget_button, LV_PCT(100));
|
lv_obj_set_width(ctx->forgetButton, LV_PCT(100));
|
||||||
lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, ctx);
|
lv_obj_add_event_cb(ctx->forgetButton, onPressForget, LV_EVENT_SHORT_CLICKED, ctx);
|
||||||
auto* forget_button_label = lv_label_create(forget_button);
|
auto* forget_button_label = lv_label_create(ctx->forgetButton);
|
||||||
lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
|
lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
|
||||||
lv_label_set_text(forget_button_label, "Forget");
|
lv_label_set_text(forget_button_label, "Forget");
|
||||||
|
lv_obj_add_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN); // shown by updateAutoConnectSection()
|
||||||
|
|
||||||
// Auto-connect
|
// Auto-connect
|
||||||
|
|
||||||
auto* auto_connect_wrapper = lv_obj_create(wrapper);
|
ctx->autoConnectWrapper = lv_obj_create(wrapper);
|
||||||
lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
lv_obj_set_size(ctx->autoConnectWrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
lvgl::obj_set_style_bg_invisible(auto_connect_wrapper);
|
lvgl::obj_set_style_bg_invisible(ctx->autoConnectWrapper);
|
||||||
lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT);
|
lv_obj_set_style_pad_all(ctx->autoConnectWrapper, 0, LV_STATE_DEFAULT);
|
||||||
lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT);
|
lv_obj_set_style_border_width(ctx->autoConnectWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_add_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN); // shown by updateAutoConnectSection()
|
||||||
|
|
||||||
auto* auto_connect_label = lv_label_create(auto_connect_wrapper);
|
auto* auto_connect_label = lv_label_create(ctx->autoConnectWrapper);
|
||||||
lv_label_set_text(auto_connect_label, "Auto-connect");
|
lv_label_set_text(auto_connect_label, "Auto-connect");
|
||||||
lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0);
|
lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||||
|
|
||||||
auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper);
|
ctx->autoConnectSwitch = lv_switch_create(ctx->autoConnectWrapper);
|
||||||
lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx);
|
lv_obj_add_event_cb(ctx->autoConnectSwitch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx);
|
||||||
lv_obj_align(auto_connect_switch, LV_ALIGN_RIGHT_MID, 0, 0);
|
lv_obj_align(ctx->autoConnectSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||||
|
|
||||||
service::wifi::settings::WifiApSettings settings;
|
|
||||||
if (service::wifi::settings::load(ctx->ssid.c_str(), settings)) {
|
|
||||||
if (settings.autoConnect) {
|
|
||||||
lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED);
|
|
||||||
} else {
|
|
||||||
lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_W(TAG, "No settings found");
|
|
||||||
lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN);
|
|
||||||
lv_obj_add_flag(auto_connect_wrapper, LV_OBJ_FLAG_HIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateViews(ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||||
@ -207,25 +221,49 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
ctx.ssid = (argc > 0) ? argv[0] : std::string();
|
ctx.ssid = (argc > 0) ? argv[0] : std::string();
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
// Subscribed once here, not in createWidgets(): that callback re-runs on every
|
Device* wifi_device = nullptr;
|
||||||
// burial/resurface rebuild, and re-subscribing there would leak the previous subscription
|
if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) {
|
||||||
// (and its captured ctx pointer) every time, only the last of which shutdown ever cleans up.
|
if (wifi_event_subscribe(wifi_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) {
|
||||||
ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto) {
|
ctx.wifiDevice = wifi_device;
|
||||||
requestViewUpdate(&ctx);
|
} else {
|
||||||
});
|
LOG_W(TAG, "Failed to subscribe to WiFi events");
|
||||||
|
device_put(wifi_device);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "No WiFi device found");
|
||||||
|
}
|
||||||
|
|
||||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||||
|
|
||||||
|
// The file-I/O-touching part of the view (updateAutoConnectSection()'s settings::load())
|
||||||
|
// runs here, on this app's own task, not the LVGL task createWidgets()
|
||||||
|
requestViewUpdate(&ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
AppEvent event {};
|
TickType_t wait_timeout = (ctx.wifiDevice == nullptr) ? pdMS_TO_TICKS(500) : portMAX_DELAY;
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
task_event_group_wait_any(&event_group, nullptr, wait_timeout);
|
||||||
break;
|
|
||||||
|
if (ctx.wifiDevice == nullptr) {
|
||||||
|
Device* retry_device = nullptr;
|
||||||
|
if (device_get_first_by_type(&WIFI_TYPE, &retry_device) == ERROR_NONE) {
|
||||||
|
if (wifi_event_subscribe(retry_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) {
|
||||||
|
ctx.wifiDevice = retry_device;
|
||||||
|
} else {
|
||||||
|
device_put(retry_device);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppEvent event {};
|
||||||
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -250,13 +288,28 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.wifiSubscription != nullptr) {
|
if (ctx.wifiDevice != nullptr) {
|
||||||
service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription);
|
WifiEvent wifi_event {};
|
||||||
|
bool wifi_event_received = false;
|
||||||
|
while (wifi_event_poll(&ctx.wifiEventSub, &wifi_event) == ERROR_NONE) {
|
||||||
|
wifi_event_received = true;
|
||||||
|
}
|
||||||
|
if (wifi_event_received) {
|
||||||
|
requestViewUpdate(&ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.wifiDevice != nullptr) {
|
||||||
|
wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub);
|
||||||
|
device_put(ctx.wifiDevice);
|
||||||
}
|
}
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
#include <lvgl/widgets/spinner.h>
|
#include <lvgl/widgets/spinner.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <lvgl/widgets/toolbar.h>
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
|
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
@ -33,10 +34,6 @@ struct Context {
|
|||||||
std::string initialSsid;
|
std::string initialSsid;
|
||||||
std::string initialPassword;
|
std::string initialPassword;
|
||||||
|
|
||||||
// Touched only from the LVGL task: directly by onConnectPressed() (an LVGL event callback,
|
|
||||||
// which already runs with the LVGL lock held), and by onWifiEvent() (a wifi-pubsub
|
|
||||||
// callback running on some other thread) which explicitly wraps its touches in
|
|
||||||
// lvgl_lock()/lvgl_unlock() - see WifiApSettings.cpp for the same convention.
|
|
||||||
bool connecting = false;
|
bool connecting = false;
|
||||||
bool connectionError = false;
|
bool connectionError = false;
|
||||||
|
|
||||||
@ -49,7 +46,9 @@ struct Context {
|
|||||||
lv_obj_t* connecting_spinner = nullptr;
|
lv_obj_t* connecting_spinner = nullptr;
|
||||||
lv_obj_t* connection_error = nullptr;
|
lv_obj_t* connection_error = nullptr;
|
||||||
|
|
||||||
PubSub<service::wifi::WifiEvent>::SubscriptionHandle wifiSubscription = nullptr;
|
// Set once in appMain() before subscribing, left null if this device has no WiFi driver
|
||||||
|
Device* wifiDevice = nullptr;
|
||||||
|
WifiEventSubscription wifiEventSub {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -61,14 +60,12 @@ void onBackPressed(lv_event_t* event) {
|
|||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
// but this callback runs ON the LVGL task, which would deadlock against itself.
|
||||||
// deadlock against itself.
|
|
||||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs on the wifi service's pubsub thread, not the LVGL task or this app's own thread.
|
void onWifiEvent(Context* ctx, WifiEvent event) {
|
||||||
void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) {
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
|
|
||||||
lvgl_lock();
|
lvgl_lock();
|
||||||
@ -90,8 +87,6 @@ void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) {
|
|||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
|
|
||||||
if (shouldClose) {
|
if (shouldClose) {
|
||||||
// Async, non-blocking - same reasoning as onBackPressed() (must not call
|
|
||||||
// app_manager_stop() on ourselves); safe to call from any thread.
|
|
||||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||||
}
|
}
|
||||||
@ -121,8 +116,7 @@ void setLoading(Context* ctx, bool loading) {
|
|||||||
|
|
||||||
void updateView(Context* ctx) {
|
void updateView(Context* ctx) {
|
||||||
if (ctx->connect_button == nullptr) {
|
if (ctx->connect_button == nullptr) {
|
||||||
// Buried (e.g. this window's own connecting state closed it, or a future dialog opens
|
// Buried (e.g. this window's own connecting state closed it, or a future dialog opens on top)
|
||||||
// on top) - see destroyWidgets().
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ctx->connectionError) {
|
if (ctx->connectionError) {
|
||||||
@ -313,25 +307,45 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
ctx.initialSsid = (argc > 0) ? argv[0] : std::string();
|
ctx.initialSsid = (argc > 0) ? argv[0] : std::string();
|
||||||
ctx.initialPassword = (argc > 1) ? argv[1] : std::string();
|
ctx.initialPassword = (argc > 1) ? argv[1] : std::string();
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
// Subscribed once here, not in createWidgets(): that callback re-runs on every
|
Device* wifi_device = nullptr;
|
||||||
// burial/resurface rebuild, and re-subscribing there would leak the previous subscription
|
if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) {
|
||||||
// (and its captured ctx pointer) every time, only the last of which shutdown ever cleans up.
|
if (wifi_event_subscribe(wifi_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) {
|
||||||
ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto event) {
|
ctx.wifiDevice = wifi_device;
|
||||||
onWifiEvent(&ctx, event);
|
} else {
|
||||||
});
|
LOG_W(TAG, "Failed to subscribe to WiFi events");
|
||||||
|
device_put(wifi_device);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "No WiFi device found");
|
||||||
|
}
|
||||||
|
|
||||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
AppEvent event {};
|
TickType_t wait_timeout = (ctx.wifiDevice == nullptr) ? pdMS_TO_TICKS(500) : portMAX_DELAY;
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
task_event_group_wait_any(&event_group, nullptr, wait_timeout);
|
||||||
break;
|
|
||||||
|
if (ctx.wifiDevice == nullptr) {
|
||||||
|
Device* retry_device = nullptr;
|
||||||
|
if (device_get_first_by_type(&WIFI_TYPE, &retry_device) == ERROR_NONE) {
|
||||||
|
if (wifi_event_subscribe(retry_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) {
|
||||||
|
ctx.wifiDevice = retry_device;
|
||||||
|
} else {
|
||||||
|
device_put(retry_device);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppEvent event {};
|
||||||
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -339,13 +353,24 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.wifiSubscription != nullptr) {
|
if (ctx.wifiDevice != nullptr) {
|
||||||
service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription);
|
WifiEvent wifi_event {};
|
||||||
|
while (wifi_event_poll(&ctx.wifiEventSub, &wifi_event) == ERROR_NONE) {
|
||||||
|
onWifiEvent(&ctx, wifi_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.wifiDevice != nullptr) {
|
||||||
|
wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub);
|
||||||
|
device_put(ctx.wifiDevice);
|
||||||
}
|
}
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,10 +10,13 @@
|
|||||||
|
|
||||||
#include <lvgl_window_manager/window_manager.h>
|
#include <lvgl_window_manager/window_manager.h>
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
|
|
||||||
#include <lvgl/lvgl.h>
|
#include <lvgl/lvgl.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
namespace tt::app::wifimanage {
|
namespace tt::app::wifimanage {
|
||||||
|
|
||||||
constexpr auto* TAG = "WifiManage";
|
constexpr auto* TAG = "WifiManage";
|
||||||
@ -24,12 +27,18 @@ namespace {
|
|||||||
|
|
||||||
struct Context {
|
struct Context {
|
||||||
uint32_t appInstanceId;
|
uint32_t appInstanceId;
|
||||||
PubSub<service::wifi::WifiEvent>::SubscriptionHandle wifiSubscription = nullptr;
|
// Set once in appMain() before subscribing, left null if this device has no WiFi driver
|
||||||
|
Device* wifiDevice = nullptr;
|
||||||
|
WifiEventSubscription wifiEventSub {};
|
||||||
Mutex mutex;
|
Mutex mutex;
|
||||||
Bindings bindings {};
|
Bindings bindings {};
|
||||||
State state;
|
State state;
|
||||||
View view = View(&bindings, &state);
|
View view = View(&bindings, &state);
|
||||||
|
|
||||||
|
TaskEventGroup* eventGroup = nullptr;
|
||||||
|
uint32_t refreshBit = 0;
|
||||||
|
std::atomic<bool> needsRefresh {false};
|
||||||
|
|
||||||
void lock() { mutex.lock(); }
|
void lock() { mutex.lock(); }
|
||||||
void unlock() { mutex.unlock(); }
|
void unlock() { mutex.unlock(); }
|
||||||
};
|
};
|
||||||
@ -62,17 +71,15 @@ static void onConnectToHidden() {
|
|||||||
wificonnect::start();
|
wificonnect::start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void requestViewUpdate(Context* ctx) {
|
void updateView(Context* ctx) {
|
||||||
ctx->lock();
|
ctx->lock();
|
||||||
lvgl_lock();
|
lvgl_lock();
|
||||||
// Safe even while buried (e.g. WifiApSettings/WifiConnect opened on top): destroyWidgets()
|
|
||||||
// nulls the view's widget pointers before they're deleted, and update() no-ops on that.
|
|
||||||
ctx->view.update();
|
ctx->view.update();
|
||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
ctx->unlock();
|
ctx->unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) {
|
void onWifiEvent(Context* ctx, WifiEvent event) {
|
||||||
auto radio_state = service::wifi::getRadioState();
|
auto radio_state = service::wifi::getRadioState();
|
||||||
LOG_I(TAG, "Update with state %s", service::wifi::radioStateToString(radio_state));
|
LOG_I(TAG, "Update with state %s", service::wifi::radioStateToString(radio_state));
|
||||||
ctx->state.setRadioState(radio_state);
|
ctx->state.setRadioState(radio_state);
|
||||||
@ -93,7 +100,7 @@ void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
requestViewUpdate(ctx);
|
updateView(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||||
@ -101,12 +108,11 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
|||||||
ctx->lock();
|
ctx->lock();
|
||||||
ctx->state.setConnectSsid("Connected"); // TODO update with proper SSID
|
ctx->state.setConnectSsid("Connected"); // TODO update with proper SSID
|
||||||
ctx->view.init(ctx->appInstanceId, parent);
|
ctx->view.init(ctx->appInstanceId, parent);
|
||||||
ctx->view.update();
|
|
||||||
ctx->unlock();
|
ctx->unlock();
|
||||||
|
ctx->needsRefresh = true;
|
||||||
|
task_event_group_signal(ctx->eventGroup, ctx->refreshBit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs with the LVGL lock already held, possibly on another app's thread - see
|
|
||||||
// WindowDestroyWidgetsFn's warnings. Must stay lock-free: View::reset() only nulls pointers.
|
|
||||||
void destroyWidgets(void* userData) {
|
void destroyWidgets(void* userData) {
|
||||||
auto* ctx = static_cast<Context*>(userData);
|
auto* ctx = static_cast<Context*>(userData);
|
||||||
ctx->view.reset();
|
ctx->view.reset();
|
||||||
@ -123,18 +129,33 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
.onConnectToHidden = onConnectToHidden
|
.onConnectToHidden = onConnectToHidden
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto event) {
|
|
||||||
onWifiEvent(&ctx, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
// State update (it has its own locking)
|
// State update (it has its own locking)
|
||||||
ctx.state.setRadioState(service::wifi::getRadioState());
|
ctx.state.setRadioState(service::wifi::getRadioState());
|
||||||
ctx.state.setScanning(service::wifi::isScanning());
|
ctx.state.setScanning(service::wifi::isScanning());
|
||||||
ctx.state.updateApRecords();
|
ctx.state.updateApRecords();
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
ctx.eventGroup = &event_group;
|
||||||
|
if (task_event_group_claim_bit(&event_group, &ctx.refreshBit) != ERROR_NONE) {
|
||||||
|
LOG_W(TAG, "Failed to claim a refresh bit; resurfacing after burial won't repopulate the view");
|
||||||
|
}
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
sub.app_instance_id = appInstanceId;
|
sub.app_instance_id = appInstanceId;
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
|
Device* wifi_device = nullptr;
|
||||||
|
if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) {
|
||||||
|
if (wifi_event_subscribe(wifi_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) {
|
||||||
|
ctx.wifiDevice = wifi_device;
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "Failed to subscribe to WiFi events");
|
||||||
|
device_put(wifi_device);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "No WiFi device found");
|
||||||
|
}
|
||||||
|
|
||||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||||
|
|
||||||
@ -154,10 +175,28 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
|
|
||||||
bool shouldClose = false;
|
bool shouldClose = false;
|
||||||
while (!shouldClose) {
|
while (!shouldClose) {
|
||||||
AppEvent event {};
|
// If the wifi device wasn't started yet when this app opened (boot-order race),
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
// ctx.wifiDevice is still null and there's no wifi-driven wake source to learn
|
||||||
break;
|
// "it's ready now" from, so poll for it on a bounded timeout instead of blocking
|
||||||
|
// indefinitely. Once subscribed, this reverts to
|
||||||
|
// portMAX_DELAY - task_event_group_wait_any() still returns immediately for app_event
|
||||||
|
// and (once live) wifi_event, this timeout only matters while neither has fired yet.
|
||||||
|
TickType_t wait_timeout = (ctx.wifiDevice == nullptr) ? pdMS_TO_TICKS(500) : portMAX_DELAY;
|
||||||
|
task_event_group_wait_any(&event_group, nullptr, wait_timeout);
|
||||||
|
|
||||||
|
if (ctx.wifiDevice == nullptr) {
|
||||||
|
Device* retry_device = nullptr;
|
||||||
|
if (device_get_first_by_type(&WIFI_TYPE, &retry_device) == ERROR_NONE) {
|
||||||
|
if (wifi_event_subscribe(retry_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) {
|
||||||
|
ctx.wifiDevice = retry_device;
|
||||||
|
} else {
|
||||||
|
device_put(retry_device);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppEvent event {};
|
||||||
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case APP_EVENT_CLOSE:
|
case APP_EVENT_CLOSE:
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
@ -165,15 +204,29 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (shouldClose) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.lock();
|
if (ctx.wifiDevice != nullptr) {
|
||||||
service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription);
|
WifiEvent wifi_event {};
|
||||||
ctx.wifiSubscription = nullptr;
|
while (wifi_event_poll(&ctx.wifiEventSub, &wifi_event) == ERROR_NONE) {
|
||||||
ctx.unlock();
|
onWifiEvent(&ctx, wifi_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.needsRefresh.exchange(false)) {
|
||||||
|
updateView(&ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.wifiDevice != nullptr) {
|
||||||
|
wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub);
|
||||||
|
device_put(ctx.wifiDevice);
|
||||||
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <Tactility/service/espnow/EspNowHostedTransport.h>
|
#include <Tactility/service/espnow/EspNowHostedTransport.h>
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
#include <tactility/drivers/wifi.h>
|
#include <tactility/drivers/wifi.h>
|
||||||
|
|
||||||
namespace tt::service::espnow::backend {
|
namespace tt::service::espnow::backend {
|
||||||
@ -16,16 +17,19 @@ namespace tt::service::espnow::backend {
|
|||||||
// truth shared with any other caller (e.g. an external OTA app), instead of being duplicated
|
// truth shared with any other caller (e.g. an external OTA app), instead of being duplicated
|
||||||
// here.
|
// here.
|
||||||
bool waitForHostedTransport(uint32_t timeoutMs) {
|
bool waitForHostedTransport(uint32_t timeoutMs) {
|
||||||
Device* device = wifi_find_first_registered_device();
|
Device* device = nullptr;
|
||||||
if (device == nullptr) {
|
if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const FirmwareOps* ops = nullptr;
|
const FirmwareOps* ops = nullptr;
|
||||||
void* ctx = nullptr;
|
void* ctx = nullptr;
|
||||||
if (wifi_get_firmware_ops(device, &ops, &ctx) != ERROR_NONE) {
|
if (wifi_get_firmware_ops(device, &ops, &ctx) != ERROR_NONE) {
|
||||||
|
device_put(device);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return ops->wait_ready(ctx, timeoutMs);
|
bool ready = ops->wait_ready(ctx, timeoutMs);
|
||||||
|
device_put(device);
|
||||||
|
return ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <Tactility/LogMessages.h>
|
#include <Tactility/LogMessages.h>
|
||||||
#include <Tactility/RecursiveMutex.h>
|
#include <Tactility/RecursiveMutex.h>
|
||||||
#include <Tactility/Tactility.h>
|
#include <Tactility/Tactility.h>
|
||||||
|
#include <Tactility/Thread.h>
|
||||||
#include <Tactility/Timer.h>
|
#include <Tactility/Timer.h>
|
||||||
#include <Tactility/service/Service.h>
|
#include <Tactility/service/Service.h>
|
||||||
#include <Tactility/service/ServiceManifest.h>
|
#include <Tactility/service/ServiceManifest.h>
|
||||||
@ -62,7 +63,6 @@ namespace {
|
|||||||
/** State lives for the entire process; only ever (re)initialized by onStart(). */
|
/** State lives for the entire process; only ever (re)initialized by onStart(). */
|
||||||
struct WifiServiceState {
|
struct WifiServiceState {
|
||||||
Device* device = nullptr;
|
Device* device = nullptr;
|
||||||
std::shared_ptr<PubSub<WifiEvent>> pubsub = std::make_shared<PubSub<WifiEvent>>();
|
|
||||||
RecursiveMutex mutex;
|
RecursiveMutex mutex;
|
||||||
bool secureConnection = false;
|
bool secureConnection = false;
|
||||||
// Internal: set by connect()/disconnect() while a manual attempt is in flight, cleared on
|
// Internal: set by connect()/disconnect() while a manual attempt is in flight, cleared on
|
||||||
@ -79,24 +79,32 @@ struct WifiServiceState {
|
|||||||
TickType_t lastScanTime = MAX_TICKS;
|
TickType_t lastScanTime = MAX_TICKS;
|
||||||
std::unique_ptr<Timer> autoConnectTimer;
|
std::unique_ptr<Timer> autoConnectTimer;
|
||||||
bool bootEventSubscribed = false;
|
bool bootEventSubscribed = false;
|
||||||
|
|
||||||
|
// Dedicated consumer for WifiEvents, alive for the service's whole lifetime (started in
|
||||||
|
// onStart(), stopped in onStop() - see dispatchSetEnabled()'s comment on why this outlives
|
||||||
|
// radio on/off toggles): runs onWifiDeviceEvent() on its own stack instead of the ESP-IDF
|
||||||
|
// esp_event task's, by blocking in task_event_group_wait_any() rather than being called back
|
||||||
|
// directly from fire_event().
|
||||||
|
TaskEventGroup wifiEventGroup {};
|
||||||
|
WifiEventSubscription wifiEventSub {};
|
||||||
|
Thread* wifiEventThread = nullptr;
|
||||||
|
std::atomic<bool> wifiEventThreadRunning {false};
|
||||||
};
|
};
|
||||||
|
|
||||||
WifiServiceState state;
|
WifiServiceState state;
|
||||||
bool started = false;
|
bool started = false;
|
||||||
|
|
||||||
void onWifiDeviceEvent(Device* device, void* context, ::WifiEvent event);
|
void onWifiDeviceEvent(Device* device, ::WifiEvent event);
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
void publish(WifiEvent event) {
|
// state.device is started (bookkeeping allocated) for the service's entire lifetime now - see
|
||||||
state.pubsub->publish(event);
|
// dispatchSetEnabled()'s comment - so device_is_ready() no longer tracks radio-on state; query
|
||||||
}
|
// the driver directly instead.
|
||||||
|
bool isRadioOn() {
|
||||||
void publishRadioState(WifiRadioState radio_state) {
|
if (state.device == nullptr) return false;
|
||||||
WifiEvent event = {};
|
WifiRadioState radio = WIFI_RADIO_STATE_OFF;
|
||||||
event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED;
|
return wifi_get_radio_state(state.device, &radio) == ERROR_NONE && radio == WIFI_RADIO_STATE_ON;
|
||||||
event.radio_state = radio_state;
|
|
||||||
publish(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioState combineRadioState(WifiRadioState radio, WifiStationState station) {
|
RadioState combineRadioState(WifiRadioState radio, WifiStationState station) {
|
||||||
@ -114,56 +122,88 @@ RadioState combineRadioState(WifiRadioState radio, WifiStationState station) {
|
|||||||
return RadioState::Off;
|
return RadioState::Off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- WifiEvent consumer thread ----
|
||||||
|
// Runs onWifiDeviceEvent() on its own stack (see WifiServiceState::wifiEventGroup's comment).
|
||||||
|
|
||||||
|
constexpr configSTACK_DEPTH_TYPE WIFI_EVENT_THREAD_STACK_SIZE = 4096;
|
||||||
|
|
||||||
|
int32_t wifiEventThreadMain() {
|
||||||
|
// The 250ms timeout only bounds how promptly a stop request (wifiEventThreadRunning going
|
||||||
|
// false) is noticed; a real event still wakes this immediately regardless, since
|
||||||
|
// task_event_group_wait_any() returns as soon as the bit is signalled, whichever comes first.
|
||||||
|
while (state.wifiEventThreadRunning.load()) {
|
||||||
|
task_event_group_wait_any(&state.wifiEventGroup, nullptr, pdMS_TO_TICKS(250));
|
||||||
|
|
||||||
|
WifiEvent event {};
|
||||||
|
while (wifi_event_poll(&state.wifiEventSub, &event) == ERROR_NONE) {
|
||||||
|
onWifiDeviceEvent(state.device, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool startWifiEventThread() {
|
||||||
|
task_event_group_construct(&state.wifiEventGroup);
|
||||||
|
if (wifi_event_subscribe(state.device, &state.wifiEventSub, &state.wifiEventGroup) != ERROR_NONE) {
|
||||||
|
task_event_group_destruct(&state.wifiEventGroup);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.wifiEventThreadRunning = true;
|
||||||
|
state.wifiEventThread = new Thread("wifi-events", WIFI_EVENT_THREAD_STACK_SIZE, [] { return wifiEventThreadMain(); });
|
||||||
|
state.wifiEventThread->start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopWifiEventThread() {
|
||||||
|
if (state.wifiEventThread == nullptr) return;
|
||||||
|
|
||||||
|
state.wifiEventThreadRunning = false;
|
||||||
|
state.wifiEventThread->join();
|
||||||
|
delete state.wifiEventThread;
|
||||||
|
state.wifiEventThread = nullptr;
|
||||||
|
|
||||||
|
wifi_event_unsubscribe(state.device, &state.wifiEventSub);
|
||||||
|
task_event_group_destruct(&state.wifiEventGroup);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Dispatched work (runs on the main task) ----
|
// ---- Dispatched work (runs on the main task) ----
|
||||||
|
|
||||||
|
// state.device is started (device_start()) once, in onStart(), and never stopped until onStop() -
|
||||||
|
// this only toggles the radio itself, so the wifi-events thread (and any app subscribed directly
|
||||||
|
// to the driver) stays subscribed across on/off toggles instead of having to resubscribe.
|
||||||
void dispatchSetEnabled(bool enabled) {
|
void dispatchSetEnabled(bool enabled) {
|
||||||
LOG_I(TAG, "dispatchSetEnabled(%d)", (int)enabled);
|
LOG_I(TAG, "dispatchSetEnabled(%d)", (int)enabled);
|
||||||
if (!started || state.device == nullptr) return;
|
if (!started || state.device == nullptr) return;
|
||||||
|
|
||||||
bool ready = device_is_ready(state.device);
|
if (enabled == isRadioOn()) {
|
||||||
if (enabled == ready) {
|
|
||||||
LOG_W(TAG, "Can't enable/disable from current state");
|
LOG_W(TAG, "Can't enable/disable from current state");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
publishRadioState(WIFI_RADIO_STATE_ON_PENDING);
|
|
||||||
|
|
||||||
if (device_start(state.device) != ERROR_NONE) {
|
if (wifi_set_radio_on(state.device) != ERROR_NONE) {
|
||||||
LOG_E(TAG, "Failed to start WiFi device");
|
LOG_E(TAG, "Failed to enable WiFi radio");
|
||||||
publishRadioState(WIFI_RADIO_STATE_OFF);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wifi_add_event_callback(state.device, nullptr, onWifiDeviceEvent) != ERROR_NONE) {
|
|
||||||
LOG_E(TAG, "Failed to register WiFi event callback");
|
|
||||||
device_stop(state.device);
|
|
||||||
publishRadioState(WIFI_RADIO_STATE_OFF);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.pauseAutoConnect = false;
|
state.pauseAutoConnect = false;
|
||||||
state.lastScanTime = 0;
|
state.lastScanTime = 0;
|
||||||
publishRadioState(WIFI_RADIO_STATE_ON);
|
|
||||||
} else {
|
} else {
|
||||||
publishRadioState(WIFI_RADIO_STATE_OFF_PENDING);
|
|
||||||
|
|
||||||
if (device_stop(state.device) != ERROR_NONE) {
|
if (wifi_set_radio_off(state.device) != ERROR_NONE) {
|
||||||
LOG_E(TAG, "Failed to stop WiFi device");
|
LOG_E(TAG, "Failed to disable WiFi radio");
|
||||||
publishRadioState(WIFI_RADIO_STATE_ON);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wifi_remove_event_callback(state.device, onWifiDeviceEvent);
|
|
||||||
|
|
||||||
state.secureConnection = false;
|
state.secureConnection = false;
|
||||||
publishRadioState(WIFI_RADIO_STATE_OFF);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispatchScan() {
|
void dispatchScan() {
|
||||||
LOG_I(TAG, "dispatchScan()");
|
LOG_I(TAG, "dispatchScan()");
|
||||||
if (!started || state.device == nullptr || !device_is_ready(state.device)) return;
|
if (!started || state.device == nullptr || !isRadioOn()) return;
|
||||||
|
|
||||||
state.lastScanTime = get_ticks();
|
state.lastScanTime = get_ticks();
|
||||||
|
|
||||||
@ -278,7 +318,7 @@ void onAutoConnectTimer() {
|
|||||||
|
|
||||||
// ---- Kernel driver event bridge ----
|
// ---- Kernel driver event bridge ----
|
||||||
|
|
||||||
void onWifiDeviceEvent(Device* device, void* /*context*/, ::WifiEvent event) {
|
void onWifiDeviceEvent(Device* device, ::WifiEvent event) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case WIFI_EVENT_TYPE_SCAN_FINISHED:
|
case WIFI_EVENT_TYPE_SCAN_FINISHED:
|
||||||
getMainDispatcher().dispatch([] { dispatchAutoConnect(); });
|
getMainDispatcher().dispatch([] { dispatchAutoConnect(); });
|
||||||
@ -348,12 +388,8 @@ void autoScanSetPaused(bool paused) {
|
|||||||
|
|
||||||
// region Public functions
|
// region Public functions
|
||||||
|
|
||||||
std::shared_ptr<PubSub<WifiEvent>> getPubsub() {
|
|
||||||
return state.pubsub;
|
|
||||||
}
|
|
||||||
|
|
||||||
RadioState getRadioState() {
|
RadioState getRadioState() {
|
||||||
if (!started || state.device == nullptr || !device_is_ready(state.device)) {
|
if (!started || state.device == nullptr) {
|
||||||
return RadioState::Off;
|
return RadioState::Off;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,7 +439,7 @@ void connect(const settings::WifiApSettings& ap, bool remember) {
|
|||||||
state.pauseAutoConnect = true;
|
state.pauseAutoConnect = true;
|
||||||
state.connectionTarget = ap;
|
state.connectionTarget = ap;
|
||||||
state.connectionTargetRemember = remember;
|
state.connectionTargetRemember = remember;
|
||||||
radio_off = !device_is_ready(state.device);
|
radio_off = !isRadioOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
getMainDispatcher().dispatch([radio_off] {
|
getMainDispatcher().dispatch([radio_off] {
|
||||||
@ -505,9 +541,17 @@ public:
|
|||||||
|
|
||||||
wifi_auto_scan_set_paused_function(autoScanSetPaused);
|
wifi_auto_scan_set_paused_function(autoScanSetPaused);
|
||||||
|
|
||||||
state.device = wifi_find_first_registered_device();
|
Device* wifi_device = nullptr;
|
||||||
if (state.device == nullptr) {
|
if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) {
|
||||||
LOG_W(TAG, "No WiFi device found");
|
LOG_W(TAG, "No WiFi device found");
|
||||||
|
} else if (device_start(wifi_device) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to start WiFi device");
|
||||||
|
device_put(wifi_device);
|
||||||
|
} else {
|
||||||
|
state.device = wifi_device;
|
||||||
|
if (!startWifiEventThread()) {
|
||||||
|
LOG_E(TAG, "Failed to subscribe to WiFi events");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) {
|
if (system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) {
|
||||||
@ -535,9 +579,13 @@ public:
|
|||||||
state.bootEventSubscribed = false;
|
state.bootEventSubscribed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.device != nullptr && device_is_ready(state.device)) {
|
if (state.device != nullptr) {
|
||||||
wifi_remove_event_callback(state.device, onWifiDeviceEvent);
|
if (isRadioOn()) {
|
||||||
|
wifi_set_radio_off(state.device);
|
||||||
|
}
|
||||||
|
stopWifiEventThread();
|
||||||
device_stop(state.device);
|
device_stop(state.device);
|
||||||
|
device_put(state.device);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.secureConnection = false;
|
state.secureConnection = false;
|
||||||
|
|||||||
@ -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 <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/mutex.h>
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
#include <tactility/firmware/firmware.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 {
|
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.
|
* Get the radio state of the device.
|
||||||
* @param[in] device the wifi device
|
* @param[in] device the wifi device
|
||||||
@ -182,21 +234,29 @@ struct WifiApi {
|
|||||||
error_t (*station_get_rssi)(struct Device* device, int32_t* rssi);
|
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] device the wifi device
|
||||||
* @param[in] callback_context the context to pass to the callback
|
* @param[in,out] sub subscription to register; owns the storage, must stay alive (and
|
||||||
* @param[in] callback the callback function
|
* stationary) until unsubscribed
|
||||||
* @return ERROR_NONE on success
|
* @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
|
||||||
*/
|
*/
|
||||||
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] device the wifi device
|
||||||
* @param[in] callback the callback function
|
* @param[in] sub subscription to remove, as passed to event_subscribe()
|
||||||
* @return ERROR_NONE on success
|
* @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.
|
* Get this device's co-processor firmware update interface, if it has one.
|
||||||
@ -210,8 +270,10 @@ struct WifiApi {
|
|||||||
|
|
||||||
extern const struct DeviceType WIFI_TYPE;
|
extern const struct DeviceType WIFI_TYPE;
|
||||||
|
|
||||||
/** @return the first registered WiFi device, regardless of started state, or NULL if none exists */
|
/** Turn the radio on. See WifiApi::set_radio_on(). Requires the device to be started (device_start()). */
|
||||||
struct Device* wifi_find_first_registered_device(void);
|
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_radio_state(struct Device* device, enum WifiRadioState* state);
|
||||||
error_t wifi_get_station_state(struct Device* device, enum WifiStationState* state);
|
error_t wifi_get_station_state(struct Device* device, enum WifiStationState* state);
|
||||||
@ -224,8 +286,39 @@ 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_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_disconnect(struct Device* device);
|
||||||
error_t wifi_station_get_rssi(struct Device* device, int32_t* rssi);
|
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
|
||||||
|
*/
|
||||||
|
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);
|
error_t wifi_get_firmware_ops(struct Device* device, const struct FirmwareOps** ops, void** ctx);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@ -5,10 +5,8 @@
|
|||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
#include <tactility/freertos/freertos.h>
|
|
||||||
#include <tactility/freertos/semphr.h>
|
|
||||||
#include <tactility/freertos/task.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@ -141,17 +139,9 @@ error_t system_event_emit(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll subscription: caller-owned node, registered with system_event_subscribe()
|
* 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
|
* copy that remains valid for the subscription's lifetime (until the next matching event
|
||||||
* overwrites it), not just for the duration of a callback.
|
* 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 {
|
struct SystemEventSubscription {
|
||||||
/** `event.type` is the event type to subscribe to; set by the caller before
|
/** `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. */
|
* each matching system_event_emit() - see the @warning above. */
|
||||||
struct SystemEvent event;
|
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. */
|
/** Implementation-only bookkeeping; do not read or write directly. */
|
||||||
struct {
|
struct {
|
||||||
/** Own wakeup signal, not the subscribing task's shared default notification value - a
|
/** Caller-owned, borrowed; set by system_event_subscribe(). A task with more than one
|
||||||
* task with more than one poll subscription would otherwise have events for one
|
* poll subscription shares one group across them - each subscription claims its own
|
||||||
* subscription wake (and consume the notification meant for) system_event_await()
|
* bit, so an event for one can't wake (and consume the signal meant for) another. */
|
||||||
* calls on another. */
|
struct TaskEventGroup* event_group;
|
||||||
SemaphoreHandle_t semaphore;
|
|
||||||
|
|
||||||
uint32_t sequence;
|
uint32_t sequence;
|
||||||
uint32_t consumed_sequence;
|
uint32_t consumed_sequence;
|
||||||
|
|
||||||
/** Number of tasks currently blocked in system_event_await() on `semaphore` -
|
/** Set by system_event_unsubscribe(). Diagnostic only: lets a subsequent
|
||||||
* system_event_unsubscribe() waits for this to reach 0 before deleting it, since
|
* system_event_poll() call report ERROR_INVALID_STATE instead of ERROR_TIMEOUT. Not a
|
||||||
* FreeRTOS requires no task be blocked on a semaphore when it's deleted. */
|
* safety mechanism - see system_event_unsubscribe()'s @warning. */
|
||||||
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;
|
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;
|
struct SystemEventSubscription* next;
|
||||||
} internal;
|
} internal;
|
||||||
@ -197,52 +177,50 @@ struct SystemEventSubscription {
|
|||||||
/**
|
/**
|
||||||
* Register a poll subscription for events of @a sub->type.
|
* Register a poll subscription for events of @a sub->type.
|
||||||
* @warning Does not work in ISR context.
|
* @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
|
* @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
|
* 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_NONE on success
|
||||||
* @retval ERROR_OUT_OF_MEMORY failed to allocate the subscription's wakeup semaphore; @a sub
|
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered
|
||||||
* was not registered
|
|
||||||
* @retval ERROR_INVALID_STATE @a sub is already 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.
|
* Remove a previously registered poll subscription.
|
||||||
* @warning Does not work in ISR context.
|
* @warning Does not work in ISR context.
|
||||||
* @warning Blocks (briefly - not for the full duration of anyone's timeout) until any task
|
* @warning Does not wait for a task concurrently blocked in task_event_group_wait()/
|
||||||
* currently blocked in system_event_await() on @a sub has woken up and left, so it's safe to
|
* task_event_group_wait_any() on @a sub's bit to leave before releasing that bit - the caller
|
||||||
* delete the subscription's semaphore before this call returns. A blocked awaiter is woken
|
* must ensure no other task is still waiting on @a sub before unsubscribing it. (A blocked task
|
||||||
* (with ERROR_INVALID_STATE) as part of this call rather than left to time out on its own.
|
* is still nudged awake as a best-effort courtesy - its subsequent system_event_poll() call will
|
||||||
* A concurrent system_event_subscribe() reusing the same @a sub waits out this same window
|
* report ERROR_INVALID_STATE - but this is diagnostic, not a guarantee.)
|
||||||
* (see system_event_subscribe()'s @warning) rather than racing it.
|
|
||||||
* @param[in] sub subscription to remove, as passed to system_event_subscribe()
|
* @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
|
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
|
||||||
*/
|
*/
|
||||||
error_t system_event_unsubscribe(struct SystemEventSubscription* sub);
|
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
|
* @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_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
|
* system_event_get_data()/system_event_get_timestamp() afterward - intermediate events are
|
||||||
* silently overwritten, never delivered. Use system_event_callback_add() instead if every
|
* silently overwritten, never delivered. Use system_event_callback_add() instead if every
|
||||||
* individual event matters.
|
* individual event matters.
|
||||||
* @warning Cannot be called concurrently from different tasks. Each tasks must have its own subscription.
|
* @warning Cannot be called concurrently from different tasks. Each task must have its own subscription.
|
||||||
* @param[in,out] sub subscription to wait on, as passed to system_event_subscribe()
|
* @param[in,out] sub subscription to poll, as passed to system_event_subscribe()
|
||||||
* @param[in] timeout max ticks to wait
|
* @retval ERROR_NONE a new event arrived - read it via @a sub->event or system_event_get_data()
|
||||||
* @retval ERROR_NONE an event arrived
|
* @retval ERROR_TIMEOUT no new event since the last call
|
||||||
* @retval ERROR_TIMEOUT @a timeout elapsed first
|
* @retval ERROR_INVALID_STATE @a sub was unsubscribed (best-effort diagnostic, not guaranteed -
|
||||||
* @retval ERROR_INVALID_STATE another task called system_event_unsubscribe() on @a sub while
|
* see system_event_unsubscribe()'s @warning)
|
||||||
* this call was blocked
|
|
||||||
*/
|
*/
|
||||||
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()
|
* Copies @a sub's current event payload (the data from the most recent system_event_emit()
|
||||||
|
|||||||
76
TactilityKernel/source/concurrent/task_event_group.cpp
Normal file
76
TactilityKernel/source/concurrent/task_event_group.cpp
Normal file
@ -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" {
|
extern "C" {
|
||||||
|
|
||||||
struct Device* wifi_find_first_registered_device() {
|
error_t wifi_set_radio_on(struct Device* device) {
|
||||||
struct Device* found = nullptr;
|
return WIFI_API(device)->set_radio_on(device);
|
||||||
device_for_each_of_type(&WIFI_TYPE, &found, [](struct Device* dev, void* ctx) -> bool {
|
}
|
||||||
*static_cast<struct Device**>(ctx) = dev;
|
|
||||||
return false;
|
error_t wifi_set_radio_off(struct Device* device) {
|
||||||
});
|
return WIFI_API(device)->set_radio_off(device);
|
||||||
return found;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t wifi_get_radio_state(struct Device* device, enum WifiRadioState* state) {
|
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);
|
return WIFI_API(device)->station_get_rssi(device, rssi);
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t wifi_add_event_callback(struct Device* device, void* callback_context, WifiEventCallback callback) {
|
error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group) {
|
||||||
return WIFI_API(device)->add_event_callback(device, callback_context, callback);
|
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) {
|
error_t wifi_event_unsubscribe(struct Device* device, struct WifiEventSubscription* sub) {
|
||||||
return WIFI_API(device)->remove_event_callback(device, callback);
|
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) {
|
error_t wifi_get_firmware_ops(struct Device* device, const struct FirmwareOps** ops, void** ctx) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include <tactility/bundle.h>
|
#include <tactility/bundle.h>
|
||||||
#include <tactility/concurrent/dispatcher.h>
|
#include <tactility/concurrent/dispatcher.h>
|
||||||
#include <tactility/concurrent/event_group.h>
|
#include <tactility/concurrent/event_group.h>
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
#include <tactility/concurrent/thread.h>
|
#include <tactility/concurrent/thread.h>
|
||||||
#include <tactility/concurrent/timer.h>
|
#include <tactility/concurrent/timer.h>
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
@ -379,7 +380,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(camera_capture_jpeg),
|
DEFINE_MODULE_SYMBOL(camera_capture_jpeg),
|
||||||
DEFINE_MODULE_SYMBOL(CAMERA_TYPE),
|
DEFINE_MODULE_SYMBOL(CAMERA_TYPE),
|
||||||
// drivers/wifi
|
// 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_radio_state),
|
||||||
DEFINE_MODULE_SYMBOL(wifi_get_station_state),
|
DEFINE_MODULE_SYMBOL(wifi_get_station_state),
|
||||||
DEFINE_MODULE_SYMBOL(wifi_get_access_point_state),
|
DEFINE_MODULE_SYMBOL(wifi_get_access_point_state),
|
||||||
@ -391,8 +393,9 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(wifi_station_connect),
|
DEFINE_MODULE_SYMBOL(wifi_station_connect),
|
||||||
DEFINE_MODULE_SYMBOL(wifi_station_disconnect),
|
DEFINE_MODULE_SYMBOL(wifi_station_disconnect),
|
||||||
DEFINE_MODULE_SYMBOL(wifi_station_get_rssi),
|
DEFINE_MODULE_SYMBOL(wifi_station_get_rssi),
|
||||||
DEFINE_MODULE_SYMBOL(wifi_add_event_callback),
|
DEFINE_MODULE_SYMBOL(wifi_event_subscribe),
|
||||||
DEFINE_MODULE_SYMBOL(wifi_remove_event_callback),
|
DEFINE_MODULE_SYMBOL(wifi_event_unsubscribe),
|
||||||
|
DEFINE_MODULE_SYMBOL(wifi_event_poll),
|
||||||
DEFINE_MODULE_SYMBOL(wifi_get_firmware_ops),
|
DEFINE_MODULE_SYMBOL(wifi_get_firmware_ops),
|
||||||
DEFINE_MODULE_SYMBOL(WIFI_TYPE),
|
DEFINE_MODULE_SYMBOL(WIFI_TYPE),
|
||||||
// wifi_auto_scan
|
// wifi_auto_scan
|
||||||
@ -471,6 +474,14 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(event_group_clear),
|
DEFINE_MODULE_SYMBOL(event_group_clear),
|
||||||
DEFINE_MODULE_SYMBOL(event_group_get),
|
DEFINE_MODULE_SYMBOL(event_group_get),
|
||||||
DEFINE_MODULE_SYMBOL(event_group_wait),
|
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
|
// concurrent/thread
|
||||||
DEFINE_MODULE_SYMBOL(thread_alloc),
|
DEFINE_MODULE_SYMBOL(thread_alloc),
|
||||||
DEFINE_MODULE_SYMBOL(thread_alloc_full),
|
DEFINE_MODULE_SYMBOL(thread_alloc_full),
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
#include <tactility/system_event.h>
|
#include <tactility/system_event.h>
|
||||||
|
|
||||||
#include <tactility/concurrent/mutex.h>
|
#include <tactility/concurrent/mutex.h>
|
||||||
#include <tactility/delay.h>
|
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
#include <tactility/time.h>
|
#include <tactility/time.h>
|
||||||
|
|
||||||
@ -30,9 +29,9 @@ struct KernelEventMutex {
|
|||||||
static KernelEventMutex subscriptions_mutex;
|
static KernelEventMutex subscriptions_mutex;
|
||||||
|
|
||||||
// Intrusive singly-linked list of poll subscriptions (system_event_subscribe()/_unsubscribe()/
|
// Intrusive singly-linked list of poll subscriptions (system_event_subscribe()/_unsubscribe()/
|
||||||
// _await()), separate from the callback-based `subscriptions` vector above. Guarded by its own
|
// _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 an
|
// mutex since notifying a poll subscriber never invokes caller code (just a memcpy and a
|
||||||
// xTaskNotifyGive), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
|
// task_event_group_signal), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
|
||||||
static SystemEventSubscription* poll_subscriptions = nullptr;
|
static SystemEventSubscription* poll_subscriptions = nullptr;
|
||||||
static KernelEventMutex poll_subscriptions_mutex;
|
static KernelEventMutex poll_subscriptions_mutex;
|
||||||
|
|
||||||
@ -68,9 +67,9 @@ error_t system_event_callback_remove(
|
|||||||
return result;
|
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
|
// 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(
|
static void notify_poll_subscribers(
|
||||||
SystemEventType type,
|
SystemEventType type,
|
||||||
uint64_t timestamp,
|
uint64_t timestamp,
|
||||||
@ -88,7 +87,7 @@ static void notify_poll_subscribers(
|
|||||||
}
|
}
|
||||||
sub->event.data_len = copied_len;
|
sub->event.data_len = copied_len;
|
||||||
sub->internal.sequence++;
|
sub->internal.sequence++;
|
||||||
xSemaphoreGive(sub->internal.semaphore);
|
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,27 +164,11 @@ error_t system_event_emit(
|
|||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t system_event_subscribe(SystemEventSubscription* sub) {
|
error_t system_event_subscribe(SystemEventSubscription* sub, TaskEventGroup* event_group) {
|
||||||
// Wait out any system_event_unsubscribe() call still draining old awaiters for this same
|
uint32_t bit;
|
||||||
// `sub` on another task (see internal.unsubscribe_in_progress). waiter_count/cancelled
|
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
|
||||||
// belong to `sub` itself, not to a given registration - reusing `sub` before that call
|
if (claim_result != ERROR_NONE) {
|
||||||
// finishes would reset them out from under it, and could hand out a fresh semaphore for it
|
return claim_result;
|
||||||
// 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);
|
mutex_lock(&poll_subscriptions_mutex.handle);
|
||||||
@ -196,15 +179,15 @@ error_t system_event_subscribe(SystemEventSubscription* sub) {
|
|||||||
for (SystemEventSubscription* existing = poll_subscriptions; existing != nullptr; existing = existing->internal.next) {
|
for (SystemEventSubscription* existing = poll_subscriptions; existing != nullptr; existing = existing->internal.next) {
|
||||||
if (existing == sub) {
|
if (existing == sub) {
|
||||||
mutex_unlock(&poll_subscriptions_mutex.handle);
|
mutex_unlock(&poll_subscriptions_mutex.handle);
|
||||||
vSemaphoreDelete(semaphore);
|
task_event_group_release_bit(event_group, bit);
|
||||||
return ERROR_INVALID_STATE;
|
return ERROR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub->internal.semaphore = semaphore;
|
sub->internal.event_group = event_group;
|
||||||
|
sub->bit = bit;
|
||||||
sub->internal.sequence = 0;
|
sub->internal.sequence = 0;
|
||||||
sub->internal.consumed_sequence = 0;
|
sub->internal.consumed_sequence = 0;
|
||||||
sub->internal.waiter_count = 0;
|
|
||||||
sub->internal.cancelled = false;
|
sub->internal.cancelled = false;
|
||||||
sub->event.data_len = 0;
|
sub->event.data_len = 0;
|
||||||
sub->internal.next = poll_subscriptions;
|
sub->internal.next = poll_subscriptions;
|
||||||
@ -217,7 +200,6 @@ error_t system_event_subscribe(SystemEventSubscription* sub) {
|
|||||||
|
|
||||||
error_t system_event_unsubscribe(SystemEventSubscription* sub) {
|
error_t system_event_unsubscribe(SystemEventSubscription* sub) {
|
||||||
error_t result = ERROR_NOT_FOUND;
|
error_t result = ERROR_NOT_FOUND;
|
||||||
SemaphoreHandle_t semaphore_to_delete = nullptr;
|
|
||||||
|
|
||||||
mutex_lock(&poll_subscriptions_mutex.handle);
|
mutex_lock(&poll_subscriptions_mutex.handle);
|
||||||
for (SystemEventSubscription** link = &poll_subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
for (SystemEventSubscription** link = &poll_subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
||||||
@ -228,73 +210,19 @@ error_t system_event_unsubscribe(SystemEventSubscription* sub) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result == ERROR_NONE) {
|
if (result == ERROR_NONE) {
|
||||||
// Unlinked first, so notify_poll_subscribers() can no longer reach this subscription.
|
// Diagnostic only now (see @warning) - not relied on for safety. Best-effort nudge for a
|
||||||
// Mark it cancelled (checked by system_event_await()'s loop) and capture the semaphore
|
// task that might still be blocked in task_event_group_wait() on this bit; unlike before,
|
||||||
// handle into a local variable rather than deleting it via sub->internal.semaphore
|
// this does not wait for it to leave before the bit is released.
|
||||||
// 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;
|
sub->internal.cancelled = true;
|
||||||
// Blocks a concurrent system_event_subscribe() from reusing `sub` until this whole
|
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||||
// call returns - see internal.unsubscribe_in_progress and system_event_subscribe().
|
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
||||||
sub->internal.unsubscribe_in_progress = true;
|
|
||||||
semaphore_to_delete = sub->internal.semaphore;
|
|
||||||
sub->internal.semaphore = nullptr;
|
|
||||||
}
|
}
|
||||||
mutex_unlock(&poll_subscriptions_mutex.handle);
|
mutex_unlock(&poll_subscriptions_mutex.handle);
|
||||||
|
|
||||||
if (result != ERROR_NONE) {
|
|
||||||
return result;
|
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) {
|
error_t system_event_poll(SystemEventSubscription* sub) {
|
||||||
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);
|
mutex_lock(&poll_subscriptions_mutex.handle);
|
||||||
bool pending = sub->internal.sequence != sub->internal.consumed_sequence;
|
bool pending = sub->internal.sequence != sub->internal.consumed_sequence;
|
||||||
bool cancelled = sub->internal.cancelled;
|
bool cancelled = sub->internal.cancelled;
|
||||||
@ -304,23 +232,12 @@ error_t system_event_await(SystemEventSubscription* sub, TickType_t timeout) {
|
|||||||
mutex_unlock(&poll_subscriptions_mutex.handle);
|
mutex_unlock(&poll_subscriptions_mutex.handle);
|
||||||
|
|
||||||
if (pending) {
|
if (pending) {
|
||||||
break;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
result = ERROR_INVALID_STATE;
|
return ERROR_INVALID_STATE;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (xSemaphoreTake(semaphore, timeout) == pdFALSE) {
|
return ERROR_TIMEOUT;
|
||||||
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) {
|
error_t system_event_get_data(SystemEventSubscription* sub, uint8_t* data, size_t data_len) {
|
||||||
|
|||||||
@ -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);
|
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
|
// system_event_poll() only detects sequence increments that happened before it's called (same
|
||||||
// waiting (same as gps_api_event_await()), so the emit must be started from another task
|
// as gps_api_event_await()), so tests that need to observe an emit from another task block via
|
||||||
// while this one is already blocked in await() - emitting first and awaiting after would
|
// task_event_group_wait() first - emitting before that wait started would race the notification
|
||||||
// race the notification the same way it would with any FreeRTOS task-notify consumer.
|
// 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 {};
|
SystemEventSubscription sub {};
|
||||||
sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
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 };
|
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
|
||||||
auto* thread = thread_alloc_full(
|
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(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 {};
|
NetworkConnectedEvent received {};
|
||||||
CHECK_EQ(system_event_get_data(&sub, reinterpret_cast<uint8_t*>(&received), sizeof(received)), ERROR_NONE);
|
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_NONE);
|
||||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
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 {};
|
SystemEventSubscription sub {};
|
||||||
sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
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
|
// Same-thread emit, no background thread or wait needed: unlike the "detects a change after
|
||||||
// starts waiting" tests above, this is exactly the case system_event_await() must handle -
|
// it starts waiting" tests above, this is exactly the case system_event_poll() must handle -
|
||||||
// sequence already moved ahead of consumed_sequence before await() is even called.
|
// sequence already moved ahead of consumed_sequence before poll() is even called.
|
||||||
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
|
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_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 {};
|
NetworkConnectedEvent received {};
|
||||||
CHECK_EQ(system_event_get_data(&sub, reinterpret_cast<uint8_t*>(&received), sizeof(received)), ERROR_NONE);
|
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.ipv4_addr, connected.ipv4_addr);
|
||||||
CHECK_EQ(received.gateway, connected.gateway);
|
CHECK_EQ(received.gateway, connected.gateway);
|
||||||
|
|
||||||
// The pending event was consumed by the call above - a second await() with no further
|
// The pending event was consumed by the call above - a second poll() with no further emit
|
||||||
// emit must time out rather than returning the same event again.
|
// must time out rather than returning the same event again.
|
||||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
|
CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT);
|
||||||
|
|
||||||
system_event_unsubscribe(&sub);
|
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 {};
|
SystemEventSubscription sub {};
|
||||||
sub.event.type = KERNEL_EVENT_TIME_CHANGED;
|
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);
|
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") {
|
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 {};
|
SystemEventSubscription sub {};
|
||||||
sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
|
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);
|
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);
|
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") {
|
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 {};
|
SystemEventSubscription sub {};
|
||||||
sub.event.type = KERNEL_EVENT_NETWORK_DISCONNECTED;
|
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
|
// Background emit + wait, same pattern as the payload-delivery test above - see the comment
|
||||||
// waiting (see the comment above), so the emit must come from another task while this one
|
// near the top of the file.
|
||||||
// is already blocked in await() - same pattern as the payload-delivery test above.
|
|
||||||
NetworkDisconnectedEvent disconnected { .device = nullptr };
|
NetworkDisconnectedEvent disconnected { .device = nullptr };
|
||||||
auto* thread = thread_alloc_full(
|
auto* thread = thread_alloc_full(
|
||||||
"system-event-emitter",
|
"system-event-emitter",
|
||||||
@ -325,7 +345,8 @@ TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the bu
|
|||||||
-1
|
-1
|
||||||
);
|
);
|
||||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
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);
|
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||||
thread_free(thread);
|
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);
|
CHECK_EQ(system_event_get_data(&sub, exact, sizeof(exact)), ERROR_NONE);
|
||||||
|
|
||||||
system_event_unsubscribe(&sub);
|
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") {
|
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 {};
|
SystemEventSubscription sub {};
|
||||||
sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
|
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(
|
auto* thread = thread_alloc_full(
|
||||||
"system-event-emitter",
|
"system-event-emitter",
|
||||||
@ -356,7 +381,8 @@ TEST_CASE("system_event_get_data on a subscription with no payload copies nothin
|
|||||||
-1
|
-1
|
||||||
);
|
);
|
||||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
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);
|
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||||
thread_free(thread);
|
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
|
CHECK_EQ(buffer[0], 0x42); // untouched - nothing to copy
|
||||||
|
|
||||||
system_event_unsubscribe(&sub);
|
system_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regression coverage for system_event_unsubscribe() racing a task blocked in
|
// Regression coverage for system_event_unsubscribe()'s (now best-effort, not guaranteed - see
|
||||||
// system_event_await() on the same subscription, and for reusing a subscription node after
|
// its @warning in system_event.h) diagnostic signal, and for reusing a subscription node after
|
||||||
// unsubscribing it - see the @warning on system_event_unsubscribe() in system_event.h.
|
// 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 {};
|
SystemEventSubscription sub {};
|
||||||
sub.event.type = KERNEL_EVENT_SERVICE_STARTED;
|
sub.event.type = KERNEL_EVENT_SERVICE_STARTED;
|
||||||
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-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_unsubscribe(&sub), 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);
|
// `sub` is still caller-owned storage after unsubscribe - polling it directly (rather than
|
||||||
CHECK_EQ(thread_get_return_code(thread), ERROR_INVALID_STATE);
|
// still being blocked in task_event_group_wait() on it, which system_event_unsubscribe()'s
|
||||||
thread_free(thread);
|
// @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.
|
task_event_group_destruct(&event_group);
|
||||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscribe") {
|
TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscribe") {
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
SystemEventSubscription sub {};
|
SystemEventSubscription sub {};
|
||||||
sub.event.type = KERNEL_EVENT_SERVICE_STOPPED;
|
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);
|
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||||
|
|
||||||
// Re-registering the same node (same storage, not a fresh SystemEventSubscription) must
|
// 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.
|
// 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(
|
auto* thread = thread_alloc_full(
|
||||||
"system-event-emitter",
|
"system-event-emitter",
|
||||||
@ -431,9 +442,12 @@ TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscri
|
|||||||
-1
|
-1
|
||||||
);
|
);
|
||||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
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);
|
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||||
thread_free(thread);
|
thread_free(thread);
|
||||||
|
|
||||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||||
|
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
}
|
}
|
||||||
|
|||||||
42
TactilityKernel/tests/source/task_event_group_test.cpp
Normal file
42
TactilityKernel/tests/source/task_event_group_test.cpp
Normal file
@ -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);
|
||||||
|
}
|
||||||
@ -21,24 +21,30 @@ static void create_widgets(lv_obj_t* parent, void* userData) {
|
|||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
AppInstanceId app_instance_id = app_scheduler_current_app_id();
|
AppInstanceId app_instance_id = app_scheduler_current_app_id();
|
||||||
|
|
||||||
|
struct TaskEventGroup event_group = {0};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
struct AppEventSubscription sub = { .app_instance_id = app_instance_id };
|
struct AppEventSubscription sub = { .app_instance_id = app_instance_id };
|
||||||
app_event_subscribe(&sub);
|
app_event_subscribe(&sub, &event_group);
|
||||||
|
|
||||||
WindowId window = window_manager_create(app_instance_id, create_widgets, NULL);
|
WindowId window = window_manager_create(app_instance_id, create_widgets, NULL);
|
||||||
|
|
||||||
bool should_close = false;
|
bool should_close = false;
|
||||||
while (!should_close) {
|
while (!should_close) {
|
||||||
|
task_event_group_wait_any(&event_group, NULL, portMAX_DELAY);
|
||||||
|
|
||||||
struct AppEvent event;
|
struct AppEvent event;
|
||||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.type == APP_EVENT_CLOSE) {
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
should_close = true;
|
should_close = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
app_event_unsubscribe(&sub);
|
app_event_unsubscribe(&sub);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user