Refactored events

This commit is contained in:
Ken Van Hoeylandt 2026-08-24 01:18:48 +02:00
parent db48dfe812
commit f15154392f
69 changed files with 2063 additions and 1129 deletions

View File

@ -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`):
- `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()`.
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`).

View File

@ -11,11 +11,10 @@
## 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.
- 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.
When that's changed reduce LVGL callstack size in Tactility.cpp run()
- Get rid of WiFi service (Wifi.cpp/h) in Tactility.cpp
- 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)
- Wrap file operations like fopen/fclose with file_mutex

View File

@ -6,15 +6,14 @@
#include <stddef.h>
#include <stdint.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/task.h>
#ifdef __cplusplus
extern "C" {
#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 {
APP_EVENT_RESULT, // struct AppResultEventData
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
* (which coalesces to the latest value), this queues events by value (FIFO) since dropping an
* APP_EVENT_RESULT would be unacceptable.
* @warning Fields other than `app_instance_id` are for internal use only; do not read or write
* them directly.
* @warning Fields other than `app_instance_id` and `bit` are for internal use only; do not read
* or write them directly.
*/
struct AppEventSubscription {
/** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */
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 AppEvent queue[APP_EVENT_QUEUE_CAPACITY];
uint8_t head;
uint8_t count;
struct {
/** Caller-owned, borrowed; set by app_event_subscribe(). */
struct TaskEventGroup* event_group;
struct AppEventSubscription* next;
struct AppEvent queue[APP_EVENT_QUEUE_CAPACITY];
uint8_t head;
uint8_t count;
struct AppEventSubscription* next;
} internal;
};
/**
@ -72,9 +79,14 @@ struct AppEventSubscription {
* @warning Does not work in ISR context.
* @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
* @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.
@ -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);
/**
* 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_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
}

View File

@ -7,7 +7,7 @@
/**
* 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
* (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;
@ -21,13 +21,20 @@ static AppEventMutex subscriptions_mutex;
extern "C" {
error_t app_event_subscribe(AppEventSubscription* sub) {
sub->task = xTaskGetCurrentTaskHandle();
sub->head = 0;
sub->count = 0;
error_t app_event_subscribe(AppEventSubscription* sub, TaskEventGroup* event_group) {
uint32_t bit;
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
if (claim_result != ERROR_NONE) {
return claim_result;
}
sub->bit = bit;
sub->internal.event_group = event_group;
sub->internal.head = 0;
sub->internal.count = 0;
mutex_lock(&subscriptions_mutex.handle);
sub->next = subscriptions;
sub->internal.next = subscriptions;
subscriptions = sub;
mutex_unlock(&subscriptions_mutex.handle);
@ -38,15 +45,19 @@ error_t app_event_unsubscribe(AppEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
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) {
*link = sub->next;
*link = sub->internal.next;
result = ERROR_NONE;
break;
}
}
mutex_unlock(&subscriptions_mutex.handle);
if (result == ERROR_NONE) {
task_event_group_release_bit(sub->internal.event_group, sub->bit);
}
return result;
}
@ -57,23 +68,23 @@ error_t app_event_emit(AppInstanceId app_instance_id, const AppEvent* event) {
error_t result = ERROR_NOT_FOUND;
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) {
continue;
}
if (sub->count >= APP_EVENT_QUEUE_CAPACITY) {
if (sub->internal.count >= APP_EVENT_QUEUE_CAPACITY) {
result = ERROR_RESOURCE;
continue;
}
uint8_t tail = (sub->head + sub->count) % APP_EVENT_QUEUE_CAPACITY;
sub->queue[tail] = stamped_event;
sub->count++;
uint8_t tail = (sub->internal.head + sub->internal.count) % APP_EVENT_QUEUE_CAPACITY;
sub->internal.queue[tail] = stamped_event;
sub->internal.count++;
if (result != ERROR_RESOURCE) {
result = ERROR_NONE;
}
xTaskNotifyGive(sub->task);
task_event_group_signal(sub->internal.event_group, sub->bit);
}
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) {
mutex_lock(&subscriptions_mutex.handle);
bool has_event = sub->count > 0;
bool has_event = sub->internal.count > 0;
if (has_event) {
*out_event = sub->queue[sub->head];
sub->head = (sub->head + 1) % APP_EVENT_QUEUE_CAPACITY;
sub->count--;
*out_event = sub->internal.queue[sub->internal.head];
sub->internal.head = (sub->internal.head + 1) % APP_EVENT_QUEUE_CAPACITY;
sub->internal.count--;
}
mutex_unlock(&subscriptions_mutex.handle);
return has_event;
}
error_t app_event_await(AppEventSubscription* sub, AppEvent* out_event, TickType_t timeout) {
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.
error_t app_event_poll(AppEventSubscription* sub, AppEvent* out_event) {
return try_pop(sub, out_event) ? ERROR_NONE : ERROR_TIMEOUT;
}

View File

@ -8,6 +8,7 @@
#include <service/manager.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.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_unsubscribe),
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
DEFINE_MODULE_SYMBOL(app_get_install_path),
DEFINE_MODULE_SYMBOL(app_install),

View File

@ -6,10 +6,13 @@
#include <tactility/delay.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 {};
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++) {
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++) {
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.result.launch_id, i);
}
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
}
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 {};
sub.app_instance_id = 10;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
CHECK_EQ(app_event_emit(11, &event), ERROR_NOT_FOUND);
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);
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") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
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++) {
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.
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
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);
}
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);
task_event_group_destruct(&event_group);
}
TEST_CASE("app_event_unsubscribe stops further delivery") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
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_NOT_FOUND);
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
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 {};
sub.app_instance_id = 40;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
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);
task_event_group_destruct(&event_group);
}
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 {};
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());
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());
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_LE(out.timestamp, after);
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 {};
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(
"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(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
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(thread_join(thread, pdMS_TO_TICKS(2000), 1), ERROR_NONE);
thread_free(thread);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
}

View File

@ -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));
}
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = app_instance_id;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
while (true) {
AppEvent event {};
if (app_event_await(&sub, &event, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
break; // safety net so a bug here can't hang the test suite
}
if (event.type == APP_EVENT_CLOSE) {
break;
bool done = false;
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
done = true;
break;
}
}
if (done) break;
}
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
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);
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 {};
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" };
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.
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 {};
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.result.launch_id, child_id);
CHECK_EQ(event.result.result, 42);
app_event_unsubscribe(&parent_sub);
task_event_group_destruct(&parent_event_group);
app_manager_stop(child_id);
app_manager_stop(parent_id);
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);
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 {};
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;
// 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
REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
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.result.launch_id, child_id);
CHECK_EQ(event.result.result, 0); // fake_run's CLOSE loop always returns 0
app_event_unsubscribe(&parent_sub);
task_event_group_destruct(&parent_event_group);
app_manager_stop(parent_id);
app_manager_remove("test.app.parent2");
app_manager_remove("test.app.child2");

View 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);
}

View File

@ -14,7 +14,7 @@ idf_component_register(
SRCS ${SOURCES}
INCLUDE_DIRS "include/"
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}
)

View File

@ -18,6 +18,8 @@
#include <tactility/log.h>
#include <tactility/time.h>
#include <TactilityCpp/Allocator.h>
#if defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
#include <tactility/drivers/esp32_esp_hosted_ota.h>
#endif
@ -33,14 +35,8 @@
namespace {
constexpr size_t WIFI_MAX_CALLBACKS = 4;
constexpr uint16_t WIFI_SCAN_RECORD_LIMIT = 32;
struct WifiCallbackEntry {
WifiEventCallback fn = nullptr;
void* ctx = nullptr;
};
struct Esp32WifiCtx {
Device* device = nullptr;
@ -66,9 +62,8 @@ struct Esp32WifiCtx {
int32_t lastEventId = -1;
TickType_t lastEventTick = 0;
Mutex callbackMutex{};
WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {};
size_t callbackCount = 0;
Mutex subscriptionsMutex{};
WifiEventSubscription* subscriptions = nullptr;
};
#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) {
WifiCallbackEntry local[WIFI_MAX_CALLBACKS];
size_t count;
mutex_lock(&ctx->callbackMutex);
count = ctx->callbackCount;
memcpy(local, ctx->callbacks, count * sizeof(WifiCallbackEntry));
mutex_unlock(&ctx->callbackMutex);
for (size_t i = 0; i < count; i++) {
local[i].fn(ctx->device, local[i].ctx, event);
mutex_lock(&ctx->subscriptionsMutex);
for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr; sub = sub->internal.next) {
mutex_lock(&sub->internal.ring_mutex);
if (sub->internal.count < WIFI_EVENT_QUEUE_CAPACITY) {
uint8_t tail = (sub->internal.head + sub->internal.count) % WIFI_EVENT_QUEUE_CAPACITY;
sub->internal.queue[tail] = event;
sub->internal.count++;
}
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) ----
@ -257,6 +254,11 @@ error_t bring_up_wifi(Esp32WifiCtx* ctx) {
ctx->radioState = WIFI_RADIO_STATE_ON;
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");
return ERROR_NONE;
}
@ -306,6 +308,11 @@ void bring_down_wifi(Esp32WifiCtx* ctx) {
ctx->radioState = WIFI_RADIO_STATE_OFF;
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");
}
@ -497,38 +504,69 @@ error_t api_station_get_rssi(Device* device, int32_t* rssi) {
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);
if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT;
if (ctx == nullptr) return ERROR_INVALID_STATE;
mutex_lock(&ctx->callbackMutex);
if (ctx->callbackCount >= WIFI_MAX_CALLBACKS) {
mutex_unlock(&ctx->callbackMutex);
return ERROR_OUT_OF_MEMORY;
}
ctx->callbacks[ctx->callbackCount] = { .fn = callback, .ctx = callback_context };
ctx->callbackCount++;
mutex_unlock(&ctx->callbackMutex);
mutex_lock(&ctx->mutex);
bool already_on = ctx->radioState == WIFI_RADIO_STATE_ON;
mutex_unlock(&ctx->mutex);
if (already_on) return ERROR_NONE;
return bring_up_wifi(ctx);
}
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;
}
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);
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);
for (size_t i = 0; i < ctx->callbackCount; i++) {
if (ctx->callbacks[i].fn == callback) {
for (size_t j = i; j + 1 < ctx->callbackCount; j++) {
ctx->callbacks[j] = ctx->callbacks[j + 1];
}
ctx->callbackCount--;
mutex_unlock(&ctx->callbackMutex);
return ERROR_NONE;
uint32_t bit;
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
if (claim_result != ERROR_NONE) {
return claim_result;
}
sub->internal.event_group = event_group;
sub->bit = bit;
mutex_lock(&ctx->subscriptionsMutex);
sub->internal.next = ctx->subscriptions;
ctx->subscriptions = sub;
mutex_unlock(&ctx->subscriptionsMutex);
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);
return ERROR_NOT_FOUND;
mutex_unlock(&ctx->subscriptionsMutex);
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) {
@ -550,6 +588,8 @@ error_t api_get_firmware_ops(Device* /*device*/, const FirmwareOps** ops, void**
}
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_station_state = api_get_station_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_disconnect = api_station_disconnect,
.station_get_rssi = api_station_get_rssi,
.add_event_callback = api_add_event_callback,
.remove_event_callback = api_remove_event_callback,
.event_subscribe = api_event_subscribe,
.event_unsubscribe = api_event_unsubscribe,
.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) {
auto* ctx = new(std::nothrow) Esp32WifiCtx();
if (ctx == nullptr) return ERROR_OUT_OF_MEMORY;
// Prefers PSRAM/SPIRAM, falling back to internal RAM when unavailable - aborts on true OOM
// (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;
mutex_construct(&ctx->mutex);
mutex_construct(&ctx->callbackMutex);
mutex_construct(&ctx->subscriptionsMutex);
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;
}
@ -596,12 +625,15 @@ error_t stop_device(Device* device) {
auto* ctx = GET_CTX(device);
if (ctx == nullptr) return ERROR_NONE;
bring_down_wifi(ctx);
if (ctx->radioState == WIFI_RADIO_STATE_ON) {
bring_down_wifi(ctx);
}
device_set_driver_data(device, nullptr);
mutex_destruct(&ctx->callbackMutex);
mutex_destruct(&ctx->subscriptionsMutex);
mutex_destruct(&ctx->mutex);
delete ctx;
ctx->~Esp32WifiCtx();
tt::OptExternalAllocator<Esp32WifiCtx>().deallocate(ctx, 1);
return ERROR_NONE;
}

View File

@ -41,6 +41,12 @@ struct Esp32WifiPinnedCtx {
};
#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 {
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);
}
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);
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);
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 ----
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) {
auto* ctx = GET_CTX(device);
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 = {
.set_radio_on = api_set_radio_on,
.set_radio_off = api_set_radio_off,
.get_radio_state = api_get_radio_state,
.get_station_state = api_get_station_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_disconnect = api_station_disconnect,
.station_get_rssi = api_station_get_rssi,
.add_event_callback = api_add_event_callback,
.remove_event_callback = api_remove_event_callback
.event_subscribe = api_event_subscribe,
.event_unsubscribe = api_event_unsubscribe
};
// ---- Driver lifecycle ----
// Starting/stopping the child (which brings the ESP-IDF WiFi stack up/down)
// also happens on the pinned thread, since esp_wifi_init/start/stop/deinit
// are subject to the same core-affinity requirement as the state-changing
// WifiApi calls above.
// Starting/stopping the child (allocating/freeing its bookkeeping - see esp32_wifi.cpp's own
// start_device()/stop_device() comment) happens on the pinned thread for the same core-affinity
// reason as the state-changing WifiApi calls above; enable()/disable() (which actually bring the
// ESP-IDF WiFi stack up/down) are marshalled the same way.
error_t start_device(Device* device) {
auto* ctx = new(std::nothrow) Esp32WifiPinnedCtx();

View File

@ -19,8 +19,6 @@
namespace {
constexpr size_t WIFI_MAX_CALLBACKS = 4;
struct MockApRecord {
const char* ssid;
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 const char* MOCK_IPV4_ADDRESS = "192.168.1.2";
struct WifiCallbackEntry {
WifiEventCallback fn = nullptr;
void* ctx = nullptr;
};
struct PosixWifiCtx {
Device* device = nullptr;
@ -54,24 +47,25 @@ struct PosixWifiCtx {
bool scanning = false;
char targetSsid[33] = {};
Mutex callbackMutex {};
WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {};
size_t callbackCount = 0;
Mutex subscriptionsMutex {};
WifiEventSubscription* subscriptions = nullptr;
};
#define GET_CTX(device) (static_cast<PosixWifiCtx*>(device_get_driver_data(device)))
void fireEvent(PosixWifiCtx* ctx, WifiEvent event) {
WifiCallbackEntry local[WIFI_MAX_CALLBACKS];
size_t count;
mutex_lock(&ctx->callbackMutex);
count = ctx->callbackCount;
memcpy(local, ctx->callbacks, count * sizeof(WifiCallbackEntry));
mutex_unlock(&ctx->callbackMutex);
for (size_t i = 0; i < count; i++) {
local[i].fn(ctx->device, local[i].ctx, event);
mutex_lock(&ctx->subscriptionsMutex);
for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr; sub = sub->next) {
mutex_lock(&sub->ring_mutex);
if (sub->count < WIFI_EVENT_QUEUE_CAPACITY) {
uint8_t tail = (sub->head + sub->count) % WIFI_EVENT_QUEUE_CAPACITY;
sub->queue[tail] = event;
sub->count++;
}
mutex_unlock(&sub->ring_mutex);
task_event_group_signal(sub->event_group, sub->bit);
}
mutex_unlock(&ctx->subscriptionsMutex);
}
// ---- WifiApi ----
@ -247,41 +241,89 @@ error_t apiStationGetRssi(Device* device, int32_t* rssi) {
return ERROR_NONE;
}
error_t apiAddEventCallback(Device* device, void* callback_context, WifiEventCallback callback) {
error_t apiSetRadioOn(Device* 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);
if (ctx->callbackCount >= WIFI_MAX_CALLBACKS) {
mutex_unlock(&ctx->callbackMutex);
return ERROR_OUT_OF_MEMORY;
}
ctx->callbacks[ctx->callbackCount] = { .fn = callback, .ctx = callback_context };
ctx->callbackCount++;
mutex_unlock(&ctx->callbackMutex);
mutex_lock(&ctx->mutex);
bool already_on = ctx->radioState == WIFI_RADIO_STATE_ON;
ctx->radioState = WIFI_RADIO_STATE_ON;
mutex_unlock(&ctx->mutex);
if (already_on) return ERROR_NONE;
WifiEvent radio_event = {};
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;
}
error_t apiRemoveEventCallback(Device* device, WifiEventCallback callback) {
error_t apiSetRadioOff(Device* 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);
for (size_t i = 0; i < ctx->callbackCount; i++) {
if (ctx->callbacks[i].fn == callback) {
for (size_t j = i; j + 1 < ctx->callbackCount; j++) {
ctx->callbacks[j] = ctx->callbacks[j + 1];
}
ctx->callbackCount--;
mutex_unlock(&ctx->callbackMutex);
return ERROR_NONE;
mutex_lock(&ctx->mutex);
bool already_off = ctx->radioState == WIFI_RADIO_STATE_OFF;
ctx->radioState = WIFI_RADIO_STATE_OFF;
ctx->stationState = WIFI_STATION_STATE_DISCONNECTED;
ctx->scanning = false;
mutex_unlock(&ctx->mutex);
if (already_off) return ERROR_NONE;
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;
}
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);
return ERROR_NOT_FOUND;
mutex_unlock(&ctx->subscriptionsMutex);
if (result == ERROR_NONE) {
task_event_group_release_bit(sub->event_group, sub->bit);
}
return result;
}
const WifiApi posix_wifi_api = {
.set_radio_on = apiSetRadioOn,
.set_radio_off = apiSetRadioOff,
.get_radio_state = apiGetRadioState,
.get_station_state = apiGetStationState,
.get_access_point_state = apiGetAccessPointState,
@ -293,13 +335,14 @@ const WifiApi posix_wifi_api = {
.station_connect = apiStationConnect,
.station_disconnect = apiStationDisconnect,
.station_get_rssi = apiStationGetRssi,
.add_event_callback = apiAddEventCallback,
.remove_event_callback = apiRemoveEventCallback
.event_subscribe = apiEventSubscribe,
.event_unsubscribe = apiEventUnsubscribe
};
// ---- Driver lifecycle ----
// Unlike the real esp32 driver, there's no hardware to bring up: the radio
// simply reports itself as ON as soon as the device is started.
// startDevice()/stopDevice() only allocate/free this driver's bookkeeping (subscriber list
// 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) {
auto* ctx = new(std::nothrow) PosixWifiCtx();
@ -307,12 +350,9 @@ error_t startDevice(Device* device) {
ctx->device = device;
mutex_construct(&ctx->mutex);
mutex_construct(&ctx->callbackMutex);
ctx->radioState = WIFI_RADIO_STATE_ON;
mutex_construct(&ctx->subscriptionsMutex);
device_set_driver_data(device, ctx);
LOG_I(TAG, "WiFi radio on (mock)");
return ERROR_NONE;
}
@ -320,12 +360,15 @@ error_t stopDevice(Device* device) {
auto* ctx = GET_CTX(device);
if (ctx == nullptr) return ERROR_NONE;
if (ctx->radioState == WIFI_RADIO_STATE_ON) {
apiSetRadioOff(device);
}
device_set_driver_data(device, nullptr);
mutex_destruct(&ctx->callbackMutex);
mutex_destruct(&ctx->subscriptionsMutex);
mutex_destruct(&ctx->mutex);
delete ctx;
LOG_I(TAG, "WiFi radio off (mock)");
return ERROR_NONE;
}

View File

@ -26,12 +26,6 @@ enum class RadioState {
Off,
};
/**
* @brief Get wifi pubsub that broadcasts Event objects
* @return PubSub
*/
std::shared_ptr<PubSub<WifiEvent>> getPubsub();
/** @return Get the current radio state */
RadioState getRadioState();

View File

@ -519,7 +519,6 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) {
.on_start = onLvglStarted,
.on_stop = onLvglStopped,
.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,
#ifdef ESP_PLATFORM
.task_affinity = getCpuAffinityConfiguration().graphics

View File

@ -203,32 +203,38 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
app_manager_stop(event.result.launch_id);
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -102,24 +102,32 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.argc = argc;
ctx.argv = argv;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return ctx.result;
}

View File

@ -114,38 +114,44 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
return 0;
}
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingUninstallDialogId) {
if (event.result.result == 0) { // 0 = Yes
app_uninstall(ctx.targetManifest.id);
shouldClose = true;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingUninstallDialogId) {
if (event.result.result == 0) { // 0 = Yes
app_uninstall(ctx.targetManifest.id);
shouldClose = true;
}
app_manager_stop(event.result.launch_id);
}
app_manager_stop(event.result.launch_id);
}
break;
default:
break;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -49,7 +49,7 @@ void refresh(Context* ctx);
void onBackPressed(lv_event_t* 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);
}
@ -201,29 +201,35 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx;
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
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);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}
@ -234,7 +240,7 @@ extern const ::AppManifest manifest = {
.id = "tactility.apphub",
.name = "App Hub",
.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

View File

@ -230,41 +230,47 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
}
}
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT: {
bool confirmed = event.result.result == CONFIRMATION_BUTTON_INDEX;
if (event.result.launch_id == ctx.installDialogId && confirmed) {
installApp(&ctx);
} else if (event.result.launch_id == ctx.uninstallDialogId && confirmed) {
uninstallApp(&ctx);
} else if (event.result.launch_id == ctx.updateDialogId && confirmed) {
updateApp(&ctx);
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT: {
bool confirmed = event.result.result == CONFIRMATION_BUTTON_INDEX;
if (event.result.launch_id == ctx.installDialogId && confirmed) {
installApp(&ctx);
} else if (event.result.launch_id == ctx.uninstallDialogId && confirmed) {
uninstallApp(&ctx);
} else if (event.result.launch_id == ctx.updateDialogId && confirmed) {
updateApp(&ctx);
}
app_manager_stop(event.result.launch_id);
break;
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
default:
break;
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -81,24 +81,32 @@ void createWidgets(lv_obj_t* parent, void*) {
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
appListInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -90,29 +90,35 @@ void createWidgets(lv_obj_t* parent, void*) {
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
appSettingsInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -126,24 +126,29 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.appInstanceId = appInstanceId;
ctx.wsSettings = settings::webserver::loadOrGetDefault();
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
@ -168,6 +173,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -215,24 +215,29 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
@ -243,6 +248,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -292,9 +292,12 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
isUsbBootSplash = hal::usb::isUsbBootMode();
sdCardMissing = false;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
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
// app" policy, unless sdCardMissing halted before it.
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
if (bootWindowId != 0) {
window_manager_remove(bootWindowId);
}
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -220,9 +220,12 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.state.updateScanResults();
ctx.state.updatePairedPeers();
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
@ -243,16 +246,18 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
@ -271,6 +276,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -199,38 +199,43 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
device_put(btDevice);
}
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.result == 0) { // 0 = Yes
if (isCurrentlyConnected(&ctx)) {
if (ctx.profileId == BT_PROFILE_HID_HOST) {
bluetooth::hidHostDisconnect();
} else {
bluetooth::disconnect(ctx.addr, ctx.profileId);
}
}
bluetooth::unpair(ctx.addr);
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
break;
case APP_EVENT_RESULT:
if (event.result.result == 0) { // 0 = Yes
if (isCurrentlyConnected(&ctx)) {
if (ctx.profileId == BT_PROFILE_HID_HOST) {
bluetooth::hidHostDisconnect();
} else {
bluetooth::disconnect(ctx.addr, ctx.profileId);
}
}
bluetooth::unpair(ctx.addr);
shouldClose = true;
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
}
@ -241,6 +246,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -185,29 +185,35 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
);
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
service::espnow::unsubscribeReceiver(ctx.receiveSubscription);
disableEspNow(&ctx);

View File

@ -153,31 +153,37 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
if (!ctx.hasFatalError) {
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
bool continuePressed = ctx.continuePressed;

View File

@ -174,9 +174,12 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
return 0;
}
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
@ -197,16 +200,18 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
@ -217,6 +222,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -30,34 +30,40 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
View view(state);
CreateContext createContext { &view, appInstanceId };
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &createContext);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
view.onResult(event.result.launch_id, event.result.result);
app_manager_stop(event.result.launch_id);
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
view.onResult(event.result.launch_id, event.result.result);
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
}
view.deinit();
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -62,24 +62,32 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
app_event_emit(appInstanceId, &closeEvent);
});
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return ctx.result;
}

View File

@ -267,51 +267,57 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
updateDeviceStates(&ctx);
});
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
ctx.timer->start();
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (ctx.hasPendingDelete) {
ctx.hasPendingDelete = false;
if (event.result.result == 0) { // 0 = Yes
lvgl_lock();
std::erase_if(ctx.deviceRows, [&ctx](const DeviceRow& row) {
return row.device == ctx.pendingDeleteDevice;
});
lvgl_unlock();
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (ctx.hasPendingDelete) {
ctx.hasPendingDelete = false;
if (event.result.result == 0) { // 0 = Yes
lvgl_lock();
std::erase_if(ctx.deviceRows, [&ctx](const DeviceRow& row) {
return row.device == ctx.pendingDeleteDevice;
});
lvgl_unlock();
gps_settings_remove_configuration_at(ctx.pendingDeleteIndex);
ctx.pendingDeleteDevice = nullptr;
gps_settings_remove_configuration_at(ctx.pendingDeleteIndex);
ctx.pendingDeleteDevice = nullptr;
lvgl_lock();
rebuildDeviceList(&ctx);
lvgl_unlock();
lvgl_lock();
rebuildDeviceList(&ctx);
lvgl_unlock();
}
}
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
}
ctx.timer->stop();
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -93,29 +93,35 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -388,30 +388,36 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx;
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
stopScanningIfRunning(&ctx);
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
stopScanningIfRunning(&ctx);
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -89,29 +89,35 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.appInstanceId = appInstanceId;
ctx.filePath = argv[0];
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -111,24 +111,32 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.argc = argc;
ctx.argv = argv;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return ctx.result;
}

View File

@ -308,30 +308,36 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
persistIfUpdated(ctx);
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
persistIfUpdated(ctx);
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -209,30 +209,36 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
persistIfUpdated(ctx);
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
persistIfUpdated(ctx);
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -234,26 +234,34 @@ void runAutoStart() {
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
runAutoStart();
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
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
// thread when app-module's scheduler asks it to (e.g. another new-model app is started).
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -145,29 +145,35 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx;
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -190,51 +190,57 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.filePath = argv[0];
}
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
LOG_I(TAG, "Result for launch id %u", event.result.launch_id);
if (event.result.launch_id == ctx.loadFileLaunchId) {
ctx.loadFileLaunchId = 0;
if (event.result.result == 0 /* Ok */) {
auto path = fileselection::getLastPath();
if (!path.empty()) {
openFile(&ctx, path);
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
LOG_I(TAG, "Result for launch id %u", event.result.launch_id);
if (event.result.launch_id == ctx.loadFileLaunchId) {
ctx.loadFileLaunchId = 0;
if (event.result.result == 0 /* Ok */) {
auto path = fileselection::getLastPath();
if (!path.empty()) {
openFile(&ctx, path);
}
}
} else if (event.result.launch_id == ctx.saveFileLaunchId) {
ctx.saveFileLaunchId = 0;
if (event.result.result == 0 /* Ok */) {
auto path = fileselection::getLastPath();
// Must re-open file, because the UI was cleared after opening the dialog.
if (!path.empty() && saveFile(&ctx, path)) {
openFile(&ctx, path);
}
}
}
} else if (event.result.launch_id == ctx.saveFileLaunchId) {
ctx.saveFileLaunchId = 0;
if (event.result.result == 0 /* Ok */) {
auto path = fileselection::getLastPath();
// Must re-open file, because the UI was cleared after opening the dialog.
if (!path.empty() && saveFile(&ctx, path)) {
openFile(&ctx, path);
}
}
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -240,31 +240,37 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
updateUi(&ctx);
});
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
ctx.timer->start();
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
ctx.timer->stop();
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -136,18 +136,21 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
@ -155,10 +158,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -250,24 +250,29 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
}
});
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
@ -277,6 +282,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -104,24 +104,32 @@ int32_t appMain(AppInstanceId appInstanceId, int argc, char* argv[]) {
ctx.argc = argc;
ctx.argv = argv;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return ctx.result;
}

View File

@ -79,24 +79,32 @@ void createWidgets(lv_obj_t* parent, void*) {
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
settingsInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -159,7 +159,7 @@ void onContinueClicked(lv_event_t* event) {
markCompleted();
// 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 (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 = {} };
app_event_emit(ctx->appInstanceId, &closeEvent);
break;
@ -229,36 +229,42 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
}
};
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingStepDialogId) {
ctx.pendingStepDialogId = 0;
advanceTo(&ctx, ctx.stepIndex + 1);
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingStepDialogId) {
ctx.pendingStepDialogId = 0;
advanceTo(&ctx, ctx.stepIndex + 1);
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -417,9 +417,12 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
lvgl_unlock();
});
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
ctx.memoryTimer->start(); // Memory: every 10s
@ -427,16 +430,18 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
@ -444,6 +449,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.tasksTimer->stop();
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -157,44 +157,50 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingTimeZoneDialogId) {
ctx.pendingTimeZoneDialogId = 0;
if (event.result.result == 0 /* Ok */) {
const auto name = timezone::getLastName();
LOG_I(TAG, "Result name=%s code=%s", name.c_str(), timezone::getLastCode().c_str());
if (!name.empty()) {
lvgl_lock();
lv_label_set_text(ctx.timeZoneLabel, name.c_str());
lvgl_unlock();
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingTimeZoneDialogId) {
ctx.pendingTimeZoneDialogId = 0;
if (event.result.result == 0 /* Ok */) {
const auto name = timezone::getLastName();
LOG_I(TAG, "Result name=%s code=%s", name.c_str(), timezone::getLastCode().c_str());
if (!name.empty()) {
lvgl_lock();
lv_label_set_text(ctx.timeZoneLabel, name.c_str());
lvgl_unlock();
}
}
}
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -240,26 +240,34 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
updateList(&ctx);
});
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
ctx.updateTimer->start();
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
ctx.updateTimer->stop();
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return ctx.result;
}

View File

@ -174,7 +174,7 @@ void onPress(lv_event_t* event) {
// 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
// (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.
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
app_event_emit(ctx->appInstanceId, &closeEvent);
@ -233,29 +233,35 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
}
lvgl_unlock();
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
// finishCalibration() already applied a new calibration on success. On cancel/failure,
// restore whatever calibration was on disk before the block above cleared it.

View File

@ -272,30 +272,36 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
persistIfUpdated(ctx);
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
persistIfUpdated(ctx);
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -85,29 +85,35 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -362,24 +362,29 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.wsSettings.webServerEnabled = service::webserver::isWebServerEnabled();
ctx.originalSettings = ctx.wsSettings;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
}
@ -423,6 +428,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -12,6 +12,7 @@
#include <lvgl/lvgl.h>
#include <lvgl/widgets/toolbar.h>
#include <tactility/device.h>
#include <tactility/log.h>
namespace tt::app::wifiapsettings {
@ -29,9 +30,14 @@ struct Context {
lv_obj_t* busySpinner = nullptr;
lv_obj_t* connectButton = 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;
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));
// 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
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
// deadlock against itself.
// but this callback runs ON the LVGL task, which would deadlock against itself.
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
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) {
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;
}
updateConnectButton(ctx);
updateBusySpinner(ctx);
updateAutoConnectSection(ctx);
}
void requestViewUpdate(Context* ctx) {
@ -119,13 +145,14 @@ void requestViewUpdate(Context* ctx) {
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) {
auto* ctx = static_cast<Context*>(userData);
ctx->busySpinner = nullptr;
ctx->connectButton = nullptr;
ctx->disconnectButton = nullptr;
ctx->autoConnectSwitch = nullptr;
ctx->forgetButton = nullptr;
ctx->autoConnectWrapper = nullptr;
}
void createWidgets(lv_obj_t* parent, void* userData) {
@ -162,43 +189,30 @@ void createWidgets(lv_obj_t* parent, void* userData) {
// Forget
auto* forget_button = lv_button_create(wrapper);
lv_obj_set_width(forget_button, LV_PCT(100));
lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, ctx);
auto* forget_button_label = lv_label_create(forget_button);
ctx->forgetButton = lv_button_create(wrapper);
lv_obj_set_width(ctx->forgetButton, LV_PCT(100));
lv_obj_add_event_cb(ctx->forgetButton, onPressForget, LV_EVENT_SHORT_CLICKED, ctx);
auto* forget_button_label = lv_label_create(ctx->forgetButton);
lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(forget_button_label, "Forget");
lv_obj_add_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN); // shown by updateAutoConnectSection()
// Auto-connect
auto* auto_connect_wrapper = lv_obj_create(wrapper);
lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lvgl::obj_set_style_bg_invisible(auto_connect_wrapper);
lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT);
ctx->autoConnectWrapper = lv_obj_create(wrapper);
lv_obj_set_size(ctx->autoConnectWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lvgl::obj_set_style_bg_invisible(ctx->autoConnectWrapper);
lv_obj_set_style_pad_all(ctx->autoConnectWrapper, 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_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper);
lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx);
lv_obj_align(auto_connect_switch, 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);
ctx->autoConnectSwitch = lv_switch_create(ctx->autoConnectWrapper);
lv_obj_add_event_cb(ctx->autoConnectSwitch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx);
lv_obj_align(ctx->autoConnectSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
}
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
@ -207,56 +221,95 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.appInstanceId = appInstanceId;
ctx.ssid = (argc > 0) ? argv[0] : std::string();
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
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
// burial/resurface rebuild, and re-subscribing there would leak the previous subscription
// (and its captured ctx pointer) every time, only the last of which shutdown ever cleans up.
ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto) {
requestViewUpdate(&ctx);
});
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);
// 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;
while (!shouldClose) {
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.forgetDialogId && event.result.result == 0) { // 0 = Yes
if (!service::wifi::settings::remove(ctx.ssid.c_str())) {
LOG_E(TAG, "Failed to remove SSID");
} else {
LOG_I(TAG, "Removed SSID");
if (
service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive &&
service::wifi::getConnectionTarget() == ctx.ssid
) {
service::wifi::disconnect();
}
shouldClose = true;
}
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);
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
}
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.forgetDialogId && event.result.result == 0) { // 0 = Yes
if (!service::wifi::settings::remove(ctx.ssid.c_str())) {
LOG_E(TAG, "Failed to remove SSID");
} else {
LOG_I(TAG, "Removed SSID");
if (
service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive &&
service::wifi::getConnectionTarget() == ctx.ssid
) {
service::wifi::disconnect();
}
shouldClose = true;
}
}
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
if (ctx.wifiDevice != nullptr) {
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.wifiSubscription != nullptr) {
service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription);
if (ctx.wifiDevice != nullptr) {
wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub);
device_put(ctx.wifiDevice);
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -14,6 +14,7 @@
#include <lvgl/widgets/spinner.h>
#include <lvgl/widgets/toolbar.h>
#include <tactility/device.h>
#include <tactility/log.h>
#include <lvgl.h>
@ -33,10 +34,6 @@ struct Context {
std::string initialSsid;
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 connectionError = false;
@ -49,7 +46,9 @@ struct Context {
lv_obj_t* connecting_spinner = 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));
// 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
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
// deadlock against itself.
// but this callback runs ON the LVGL task, which would deadlock against itself.
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
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, service::wifi::WifiEvent event) {
void onWifiEvent(Context* ctx, WifiEvent event) {
bool shouldClose = false;
lvgl_lock();
@ -90,8 +87,6 @@ void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) {
lvgl_unlock();
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 = {} };
app_event_emit(ctx->appInstanceId, &closeEvent);
}
@ -121,8 +116,7 @@ void setLoading(Context* ctx, bool loading) {
void updateView(Context* ctx) {
if (ctx->connect_button == nullptr) {
// Buried (e.g. this window's own connecting state closed it, or a future dialog opens
// on top) - see destroyWidgets().
// Buried (e.g. this window's own connecting state closed it, or a future dialog opens on top)
return;
}
if (ctx->connectionError) {
@ -313,39 +307,70 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
ctx.initialSsid = (argc > 0) ? argv[0] : std::string();
ctx.initialPassword = (argc > 1) ? argv[1] : std::string();
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
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
// burial/resurface rebuild, and re-subscribing there would leak the previous subscription
// (and its captured ctx pointer) every time, only the last of which shutdown ever cleans up.
ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto event) {
onWifiEvent(&ctx, event);
});
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);
bool shouldClose = false;
while (!shouldClose) {
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
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);
}
}
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
if (ctx.wifiDevice != nullptr) {
WifiEvent wifi_event {};
while (wifi_event_poll(&ctx.wifiEventSub, &wifi_event) == ERROR_NONE) {
onWifiEvent(&ctx, wifi_event);
}
}
}
if (ctx.wifiSubscription != nullptr) {
service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription);
if (ctx.wifiDevice != nullptr) {
wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub);
device_put(ctx.wifiDevice);
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -10,10 +10,13 @@
#include <lvgl_window_manager/window_manager.h>
#include <tactility/device.h>
#include <tactility/log.h>
#include <lvgl/lvgl.h>
#include <atomic>
namespace tt::app::wifimanage {
constexpr auto* TAG = "WifiManage";
@ -24,12 +27,18 @@ namespace {
struct Context {
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;
Bindings bindings {};
State state;
View view = View(&bindings, &state);
TaskEventGroup* eventGroup = nullptr;
uint32_t refreshBit = 0;
std::atomic<bool> needsRefresh {false};
void lock() { mutex.lock(); }
void unlock() { mutex.unlock(); }
};
@ -62,17 +71,15 @@ static void onConnectToHidden() {
wificonnect::start();
}
void requestViewUpdate(Context* ctx) {
void updateView(Context* ctx) {
ctx->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();
lvgl_unlock();
ctx->unlock();
}
void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) {
void onWifiEvent(Context* ctx, WifiEvent event) {
auto radio_state = service::wifi::getRadioState();
LOG_I(TAG, "Update with state %s", service::wifi::radioStateToString(radio_state));
ctx->state.setRadioState(radio_state);
@ -93,7 +100,7 @@ void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) {
break;
}
requestViewUpdate(ctx);
updateView(ctx);
}
void createWidgets(lv_obj_t* parent, void* userData) {
@ -101,12 +108,11 @@ void createWidgets(lv_obj_t* parent, void* userData) {
ctx->lock();
ctx->state.setConnectSsid("Connected"); // TODO update with proper SSID
ctx->view.init(ctx->appInstanceId, parent);
ctx->view.update();
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) {
auto* ctx = static_cast<Context*>(userData);
ctx->view.reset();
@ -123,18 +129,33 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
.onConnectToHidden = onConnectToHidden
};
ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto event) {
onWifiEvent(&ctx, event);
});
// State update (it has its own locking)
ctx.state.setRadioState(service::wifi::getRadioState());
ctx.state.setScanning(service::wifi::isScanning());
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 {};
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);
@ -154,26 +175,58 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
bool shouldClose = false;
while (!shouldClose) {
AppEvent event {};
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
// If the wifi device wasn't started yet when this app opened (boot-order race),
// ctx.wifiDevice is still null and there's no wifi-driven wake source to learn
// "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);
}
}
}
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
default:
break;
}
if (shouldClose) break;
}
if (ctx.wifiDevice != nullptr) {
WifiEvent wifi_event {};
while (wifi_event_poll(&ctx.wifiEventSub, &wifi_event) == ERROR_NONE) {
onWifiEvent(&ctx, wifi_event);
}
}
if (ctx.needsRefresh.exchange(false)) {
updateView(&ctx);
}
}
ctx.lock();
service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription);
ctx.wifiSubscription = nullptr;
ctx.unlock();
if (ctx.wifiDevice != nullptr) {
wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub);
device_put(ctx.wifiDevice);
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}

View File

@ -6,6 +6,7 @@
#include <Tactility/service/espnow/EspNowHostedTransport.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
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
// here.
bool waitForHostedTransport(uint32_t timeoutMs) {
Device* device = wifi_find_first_registered_device();
if (device == nullptr) {
Device* device = nullptr;
if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) {
return false;
}
const FirmwareOps* ops = nullptr;
void* ctx = nullptr;
if (wifi_get_firmware_ops(device, &ops, &ctx) != ERROR_NONE) {
device_put(device);
return false;
}
return ops->wait_ready(ctx, timeoutMs);
bool ready = ops->wait_ready(ctx, timeoutMs);
device_put(device);
return ready;
}
}

View File

@ -4,6 +4,7 @@
#include <Tactility/LogMessages.h>
#include <Tactility/RecursiveMutex.h>
#include <Tactility/Tactility.h>
#include <Tactility/Thread.h>
#include <Tactility/Timer.h>
#include <Tactility/service/Service.h>
#include <Tactility/service/ServiceManifest.h>
@ -62,7 +63,6 @@ namespace {
/** State lives for the entire process; only ever (re)initialized by onStart(). */
struct WifiServiceState {
Device* device = nullptr;
std::shared_ptr<PubSub<WifiEvent>> pubsub = std::make_shared<PubSub<WifiEvent>>();
RecursiveMutex mutex;
bool secureConnection = false;
// 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;
std::unique_ptr<Timer> autoConnectTimer;
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;
bool started = false;
void onWifiDeviceEvent(Device* device, void* context, ::WifiEvent event);
void onWifiDeviceEvent(Device* device, ::WifiEvent event);
// ---- Helpers ----
void publish(WifiEvent event) {
state.pubsub->publish(event);
}
void publishRadioState(WifiRadioState radio_state) {
WifiEvent event = {};
event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED;
event.radio_state = radio_state;
publish(event);
// state.device is started (bookkeeping allocated) for the service's entire lifetime now - see
// dispatchSetEnabled()'s comment - so device_is_ready() no longer tracks radio-on state; query
// the driver directly instead.
bool isRadioOn() {
if (state.device == nullptr) return false;
WifiRadioState radio = WIFI_RADIO_STATE_OFF;
return wifi_get_radio_state(state.device, &radio) == ERROR_NONE && radio == WIFI_RADIO_STATE_ON;
}
RadioState combineRadioState(WifiRadioState radio, WifiStationState station) {
@ -114,56 +122,88 @@ RadioState combineRadioState(WifiRadioState radio, WifiStationState station) {
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) ----
// 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) {
LOG_I(TAG, "dispatchSetEnabled(%d)", (int)enabled);
if (!started || state.device == nullptr) return;
bool ready = device_is_ready(state.device);
if (enabled == ready) {
if (enabled == isRadioOn()) {
LOG_W(TAG, "Can't enable/disable from current state");
return;
}
if (enabled) {
publishRadioState(WIFI_RADIO_STATE_ON_PENDING);
if (device_start(state.device) != ERROR_NONE) {
LOG_E(TAG, "Failed to start WiFi device");
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);
if (wifi_set_radio_on(state.device) != ERROR_NONE) {
LOG_E(TAG, "Failed to enable WiFi radio");
return;
}
state.pauseAutoConnect = false;
state.lastScanTime = 0;
publishRadioState(WIFI_RADIO_STATE_ON);
} else {
publishRadioState(WIFI_RADIO_STATE_OFF_PENDING);
if (device_stop(state.device) != ERROR_NONE) {
LOG_E(TAG, "Failed to stop WiFi device");
publishRadioState(WIFI_RADIO_STATE_ON);
if (wifi_set_radio_off(state.device) != ERROR_NONE) {
LOG_E(TAG, "Failed to disable WiFi radio");
return;
}
wifi_remove_event_callback(state.device, onWifiDeviceEvent);
state.secureConnection = false;
publishRadioState(WIFI_RADIO_STATE_OFF);
}
}
void 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();
@ -278,7 +318,7 @@ void onAutoConnectTimer() {
// ---- Kernel driver event bridge ----
void onWifiDeviceEvent(Device* device, void* /*context*/, ::WifiEvent event) {
void onWifiDeviceEvent(Device* device, ::WifiEvent event) {
switch (event.type) {
case WIFI_EVENT_TYPE_SCAN_FINISHED:
getMainDispatcher().dispatch([] { dispatchAutoConnect(); });
@ -348,12 +388,8 @@ void autoScanSetPaused(bool paused) {
// region Public functions
std::shared_ptr<PubSub<WifiEvent>> getPubsub() {
return state.pubsub;
}
RadioState getRadioState() {
if (!started || state.device == nullptr || !device_is_ready(state.device)) {
if (!started || state.device == nullptr) {
return RadioState::Off;
}
@ -403,7 +439,7 @@ void connect(const settings::WifiApSettings& ap, bool remember) {
state.pauseAutoConnect = true;
state.connectionTarget = ap;
state.connectionTargetRemember = remember;
radio_off = !device_is_ready(state.device);
radio_off = !isRadioOn();
}
getMainDispatcher().dispatch([radio_off] {
@ -505,9 +541,17 @@ public:
wifi_auto_scan_set_paused_function(autoScanSetPaused);
state.device = wifi_find_first_registered_device();
if (state.device == nullptr) {
Device* wifi_device = nullptr;
if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) {
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) {
@ -535,9 +579,13 @@ public:
state.bootEventSubscribed = false;
}
if (state.device != nullptr && device_is_ready(state.device)) {
wifi_remove_event_callback(state.device, onWifiDeviceEvent);
if (state.device != nullptr) {
if (isRadioOn()) {
wifi_set_radio_off(state.device);
}
stopWifiEventThread();
device_stop(state.device);
device_put(state.device);
}
state.secureConnection = false;

View File

@ -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

View File

@ -4,6 +4,8 @@
#include <stddef.h>
#include <stdint.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.h>
#include <tactility/firmware/firmware.h>
@ -90,9 +92,59 @@ struct WifiEvent {
};
};
typedef void (*WifiEventCallback)(struct Device* device, void* callback_context, struct WifiEvent event);
/** Number of events a WifiEventSubscription can hold before wifi_event_emit() starts dropping
* the newest event for it (still delivered to any other matching subscription). Generous: a
* device fires these serially, one at a time, not in genuinely concurrent bursts. */
#define WIFI_EVENT_QUEUE_CAPACITY 4
/**
* Caller-owned subscription node, registered with wifi_event_subscribe() and polled with
* wifi_event_poll(). Like app_event, this queues events by value (FIFO) rather than
* coalescing to the latest one: a burst of distinct WifiEventTypes (e.g.
* WIFI_EVENT_TYPE_STATION_STATE_CHANGED immediately followed by
* WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT) must each be delivered, not just "something
* changed."
* @warning Fields other than `bit` are for internal use only; do not read or write them
* directly.
*/
struct WifiEventSubscription {
/** Set by wifi_event_subscribe(). Read-only for the caller: OR it into a
* task_event_group_wait() mask (alongside other subscriptions sharing the same
* `event_group`) to block on this subscription and other event sources with one call. */
uint32_t bit;
struct {
/** Caller-owned, borrowed; set by wifi_event_subscribe(). */
struct TaskEventGroup* event_group;
/** Guards `queue`/`head`/`count` between wifi_event_emit() (driver thread) and
* wifi_event_poll() (caller's thread) - the two live in different translation units with
* no other shared lock. */
struct Mutex ring_mutex;
struct WifiEvent queue[WIFI_EVENT_QUEUE_CAPACITY];
uint8_t head;
uint8_t count;
struct WifiEventSubscription* next;
} internal;
};
struct WifiApi {
/**
* Turn the radio on. Unlike start_device()/stop_device() (which only allocate/free the
* driver's bookkeeping, so event subscribers can stay subscribed across radio toggles),
* this is what actually brings the hardware up.
* @param[in] device the wifi device
* @return ERROR_NONE on success, or if the radio is already on
*/
error_t (*set_radio_on)(struct Device* device);
/**
* Turn the radio off. See set_radio_on().
* @param[in] device the wifi device
* @return ERROR_NONE on success, or if the radio is already off
*/
error_t (*set_radio_off)(struct Device* device);
/**
* Get the radio state of the device.
* @param[in] device the wifi device
@ -182,21 +234,29 @@ struct WifiApi {
error_t (*station_get_rssi)(struct Device* device, int32_t* rssi);
/**
* Add a WifiEvent callback.
* Register a subscription for this device's WifiEvents.
* @warning Does not work in ISR context.
* @param[in] device the wifi device
* @param[in] callback_context the context to pass to the callback
* @param[in] callback the callback function
* @return ERROR_NONE on success
* @param[in,out] sub subscription to register; owns the storage, must stay alive (and
* stationary) until unsubscribed
* @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be
* destructed only after event_unsubscribe()). To block for an event, call
* task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit into the
* mask, or use _wait_any() to include every subscription sharing it), then drain with
* wifi_event_poll().
* @retval ERROR_NONE on success
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim
*/
error_t (*add_event_callback)(struct Device* device, void* callback_context, WifiEventCallback callback);
error_t (*event_subscribe)(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group);
/**
* Remove a WifiEvent callback.
* Remove a previously registered subscription.
* @warning Does not work in ISR context.
* @param[in] device the wifi device
* @param[in] callback the callback function
* @return ERROR_NONE on success
* @param[in] sub subscription to remove, as passed to event_subscribe()
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t (*remove_event_callback)(struct Device* device, WifiEventCallback callback);
error_t (*event_unsubscribe)(struct Device* device, struct WifiEventSubscription* sub);
/**
* Get this device's co-processor firmware update interface, if it has one.
@ -210,8 +270,10 @@ struct WifiApi {
extern const struct DeviceType WIFI_TYPE;
/** @return the first registered WiFi device, regardless of started state, or NULL if none exists */
struct Device* wifi_find_first_registered_device(void);
/** Turn the radio on. See WifiApi::set_radio_on(). Requires the device to be started (device_start()). */
error_t wifi_set_radio_on(struct Device* device);
/** Turn the radio off. See WifiApi::set_radio_off(). Requires the device to be started (device_start()). */
error_t wifi_set_radio_off(struct Device* device);
error_t wifi_get_radio_state(struct Device* device, enum WifiRadioState* state);
error_t wifi_get_station_state(struct Device* device, enum WifiStationState* state);
@ -224,8 +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_disconnect(struct Device* device);
error_t wifi_station_get_rssi(struct Device* device, int32_t* rssi);
error_t wifi_add_event_callback(struct Device* device, void* callback_context, WifiEventCallback callback);
error_t wifi_remove_event_callback(struct Device* device, WifiEventCallback callback);
/**
* Register a subscription for @a device's WifiEvents.
* @warning Does not work in ISR context.
* @param[in] device the wifi device
* @param[in,out] sub subscription to register; owns the storage, must stay alive (and
* stationary) until unsubscribed
* @param[in] event_group caller-owned group to wait on; must outlive @a sub. To block for an
* event, call task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit
* into the mask, or use _wait_any() to include every subscription sharing it), then drain with
* wifi_event_poll().
* @retval ERROR_NONE on success
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim
*/
error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group);
/**
* Remove a previously registered subscription.
* @warning Does not work in ISR context.
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t wifi_event_unsubscribe(struct Device* device, struct WifiEventSubscription* sub);
/**
* Non-blocking: pop the next event for @a sub if one is already queued.
* @warning Never blocks. To wait for an event, block in task_event_group_wait()/
* task_event_group_wait_any() on @a sub's event group first (see wifi_event_subscribe()), then
* drain with this in a loop.
* @retval ERROR_NONE @a out_event was filled
* @retval ERROR_TIMEOUT nothing queued right now
*/
error_t wifi_event_poll(struct WifiEventSubscription* sub, struct WifiEvent* out_event);
error_t wifi_get_firmware_ops(struct Device* device, const struct FirmwareOps** ops, void** ctx);
#ifdef __cplusplus

View File

@ -5,10 +5,8 @@
#include <stddef.h>
#include <stdint.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h>
#include <tactility/freertos/task.h>
#ifdef __cplusplus
extern "C" {
@ -141,17 +139,9 @@ error_t system_event_emit(
/**
* Poll subscription: caller-owned node, registered with system_event_subscribe()
* and polled with system_event_await(). Unlike system_event_callback_t, `event` is a by-value
* and polled with system_event_poll(). Unlike system_event_callback_t, `event` is a by-value
* copy that remains valid for the subscription's lifetime (until the next matching event
* overwrites it), not just for the duration of a callback.
* @warning Must be zero-initialized before the first system_event_subscribe() call (e.g.
* `SystemEventSubscription sub = {};` in C++, `SystemEventSubscription sub = {0};` in C, or
* static/global storage) - system_event_subscribe() reads `internal.unsubscribe_in_progress`
* before it writes it, to detect reuse of a node still being torn down by a concurrent
* system_event_unsubscribe() call; on indeterminate (non-zeroed) storage that read is undefined
* behavior. Not required again for a later system_event_subscribe() reusing the same node after
* system_event_unsubscribe() - the fields it depends on are fully owned/maintained by this API
* from the first successful registration onward.
*/
struct SystemEventSubscription {
/** `event.type` is the event type to subscribe to; set by the caller before
@ -159,36 +149,26 @@ struct SystemEventSubscription {
* each matching system_event_emit() - see the @warning above. */
struct SystemEvent event;
/** Set by system_event_subscribe(). Read-only for the caller: OR it into a
* task_event_group_wait() mask (alongside other subscriptions sharing the same
* `internal.event_group`) to block on this subscription and other event sources with one
* call. */
uint32_t bit;
/** Implementation-only bookkeeping; do not read or write directly. */
struct {
/** Own wakeup signal, not the subscribing task's shared default notification value - a
* task with more than one poll subscription would otherwise have events for one
* subscription wake (and consume the notification meant for) system_event_await()
* calls on another. */
SemaphoreHandle_t semaphore;
/** Caller-owned, borrowed; set by system_event_subscribe(). A task with more than one
* poll subscription shares one group across them - each subscription claims its own
* bit, so an event for one can't wake (and consume the signal meant for) another. */
struct TaskEventGroup* event_group;
uint32_t sequence;
uint32_t consumed_sequence;
/** Number of tasks currently blocked in system_event_await() on `semaphore` -
* system_event_unsubscribe() waits for this to reach 0 before deleting it, since
* FreeRTOS requires no task be blocked on a semaphore when it's deleted. */
int waiter_count;
/** Set by system_event_unsubscribe() before it gives `semaphore` and waits, so a task
* already blocked in system_event_await() bails out (ERROR_INVALID_STATE) instead of
* waiting out its full timeout. Reset once system_event_unsubscribe() finishes
* draining old awaiters (see unsubscribe_in_progress) - not simply "on the next
* system_event_subscribe()", so a fresh registration can never observe a stale `true`
* left over from an unsubscribe that hasn't returned yet. */
/** Set by system_event_unsubscribe(). Diagnostic only: lets a subsequent
* system_event_poll() call report ERROR_INVALID_STATE instead of ERROR_TIMEOUT. Not a
* safety mechanism - see system_event_unsubscribe()'s @warning. */
bool cancelled;
/** True from the moment system_event_unsubscribe() unlinks `sub` until it has finished
* draining old awaiters and deleted the old semaphore. system_event_subscribe() spins
* until this clears before reusing `sub` - otherwise a new registration could reset
* waiter_count/cancelled (both shared with the old registration, there being only one
* `sub`) out from under the old system_event_unsubscribe() call still relying on them,
* or hand out a new semaphore for that same call to then promptly delete instead of the
* old one, while an old awaiter is still blocked on the real old semaphore. */
bool unsubscribe_in_progress;
struct SystemEventSubscription* next;
} internal;
@ -197,52 +177,50 @@ struct SystemEventSubscription {
/**
* Register a poll subscription for events of @a sub->type.
* @warning Does not work in ISR context.
* @warning On its very first call for a given @a sub, @a sub must have been zero-initialized -
* see SystemEventSubscription's @warning.
* @warning If @a sub was just passed to system_event_unsubscribe() (e.g. reusing a node for a
* new registration) and that call hasn't returned yet on another task, this call blocks
* (briefly - not for the full duration of anyone's timeout) until it does, before registering -
* see SystemEventSubscription::internal.unsubscribe_in_progress.
* @param[in,out] sub subscription to register; caller sets @a sub->type beforehand, owns the
* storage, and must keep it alive (and stationary) until unsubscribed
* @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be
* destructed only after system_event_unsubscribe()). To block for an event, call
* task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit into the mask,
* or use _wait_any() to include every subscription sharing it), then poll with
* system_event_poll().
* @retval ERROR_NONE on success
* @retval ERROR_OUT_OF_MEMORY failed to allocate the subscription's wakeup semaphore; @a sub
* was not registered
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered
* @retval ERROR_INVALID_STATE @a sub is already registered
*/
error_t system_event_subscribe(struct SystemEventSubscription* sub);
error_t system_event_subscribe(struct SystemEventSubscription* sub, struct TaskEventGroup* event_group);
/**
* Remove a previously registered poll subscription.
* @warning Does not work in ISR context.
* @warning Blocks (briefly - not for the full duration of anyone's timeout) until any task
* currently blocked in system_event_await() on @a sub has woken up and left, so it's safe to
* delete the subscription's semaphore before this call returns. A blocked awaiter is woken
* (with ERROR_INVALID_STATE) as part of this call rather than left to time out on its own.
* A concurrent system_event_subscribe() reusing the same @a sub waits out this same window
* (see system_event_subscribe()'s @warning) rather than racing it.
* @warning Does not wait for a task concurrently blocked in task_event_group_wait()/
* task_event_group_wait_any() on @a sub's bit to leave before releasing that bit - the caller
* must ensure no other task is still waiting on @a sub before unsubscribing it. (A blocked task
* is still nudged awake as a best-effort courtesy - its subsequent system_event_poll() call will
* report ERROR_INVALID_STATE - but this is diagnostic, not a guarantee.)
* @param[in] sub subscription to remove, as passed to system_event_subscribe()
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t system_event_unsubscribe(struct SystemEventSubscription* sub);
/**
* Blocks the calling task until a new event arrives for @a sub, or timeout elapses.
* Non-blocking: check whether a new event has arrived for @a sub since the last call.
* @warning Never blocks. To wait, block in task_event_group_wait()/task_event_group_wait_any()
* on @a sub's bit first (see system_event_subscribe()), then call this.
* @warning Poll subscriptions coalesce to the latest event, they are not a queue: if
* system_event_emit() is called more than once for @a sub->event.type between two
* system_event_await() calls, only the most recent event's data/timestamp is visible via
* system_event_poll() calls, only the most recent event's data/timestamp is visible via
* system_event_get_data()/system_event_get_timestamp() afterward - intermediate events are
* silently overwritten, never delivered. Use system_event_callback_add() instead if every
* individual event matters.
* @warning Cannot be called concurrently from different tasks. Each tasks must have its own subscription.
* @param[in,out] sub subscription to wait on, as passed to system_event_subscribe()
* @param[in] timeout max ticks to wait
* @retval ERROR_NONE an event arrived
* @retval ERROR_TIMEOUT @a timeout elapsed first
* @retval ERROR_INVALID_STATE another task called system_event_unsubscribe() on @a sub while
* this call was blocked
* @warning Cannot be called concurrently from different tasks. Each task must have its own subscription.
* @param[in,out] sub subscription to poll, as passed to system_event_subscribe()
* @retval ERROR_NONE a new event arrived - read it via @a sub->event or system_event_get_data()
* @retval ERROR_TIMEOUT no new event since the last call
* @retval ERROR_INVALID_STATE @a sub was unsubscribed (best-effort diagnostic, not guaranteed -
* see system_event_unsubscribe()'s @warning)
*/
error_t system_event_await(struct SystemEventSubscription* sub, TickType_t timeout);
error_t system_event_poll(struct SystemEventSubscription* sub);
/**
* Copies @a sub's current event payload (the data from the most recent system_event_emit()

View 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"

View File

@ -7,13 +7,12 @@
extern "C" {
struct Device* wifi_find_first_registered_device() {
struct Device* found = nullptr;
device_for_each_of_type(&WIFI_TYPE, &found, [](struct Device* dev, void* ctx) -> bool {
*static_cast<struct Device**>(ctx) = dev;
return false;
});
return found;
error_t wifi_set_radio_on(struct Device* device) {
return WIFI_API(device)->set_radio_on(device);
}
error_t wifi_set_radio_off(struct Device* device) {
return WIFI_API(device)->set_radio_off(device);
}
error_t wifi_get_radio_state(struct Device* device, enum WifiRadioState* state) {
@ -60,12 +59,36 @@ error_t wifi_station_get_rssi(struct Device* device, int32_t* rssi) {
return WIFI_API(device)->station_get_rssi(device, rssi);
}
error_t wifi_add_event_callback(struct Device* device, void* callback_context, WifiEventCallback callback) {
return WIFI_API(device)->add_event_callback(device, callback_context, callback);
error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group) {
mutex_construct(&sub->internal.ring_mutex);
sub->internal.head = 0;
sub->internal.count = 0;
error_t result = WIFI_API(device)->event_subscribe(device, sub, event_group);
if (result != ERROR_NONE) {
mutex_destruct(&sub->internal.ring_mutex);
}
return result;
}
error_t wifi_remove_event_callback(struct Device* device, WifiEventCallback callback) {
return WIFI_API(device)->remove_event_callback(device, callback);
error_t wifi_event_unsubscribe(struct Device* device, struct WifiEventSubscription* sub) {
error_t result = WIFI_API(device)->event_unsubscribe(device, sub);
if (result == ERROR_NONE) {
mutex_destruct(&sub->internal.ring_mutex);
}
return result;
}
error_t wifi_event_poll(struct WifiEventSubscription* sub, struct WifiEvent* out_event) {
mutex_lock(&sub->internal.ring_mutex);
bool has_event = sub->internal.count > 0;
if (has_event) {
*out_event = sub->internal.queue[sub->internal.head];
sub->internal.head = (sub->internal.head + 1) % WIFI_EVENT_QUEUE_CAPACITY;
sub->internal.count--;
}
mutex_unlock(&sub->internal.ring_mutex);
return has_event ? ERROR_NONE : ERROR_TIMEOUT;
}
error_t wifi_get_firmware_ops(struct Device* device, const struct FirmwareOps** ops, void** ctx) {

View File

@ -1,6 +1,7 @@
#include <tactility/bundle.h>
#include <tactility/concurrent/dispatcher.h>
#include <tactility/concurrent/event_group.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/concurrent/thread.h>
#include <tactility/concurrent/timer.h>
#include <tactility/device.h>
@ -379,7 +380,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(camera_capture_jpeg),
DEFINE_MODULE_SYMBOL(CAMERA_TYPE),
// drivers/wifi
DEFINE_MODULE_SYMBOL(wifi_find_first_registered_device),
DEFINE_MODULE_SYMBOL(wifi_set_radio_on),
DEFINE_MODULE_SYMBOL(wifi_set_radio_off),
DEFINE_MODULE_SYMBOL(wifi_get_radio_state),
DEFINE_MODULE_SYMBOL(wifi_get_station_state),
DEFINE_MODULE_SYMBOL(wifi_get_access_point_state),
@ -391,8 +393,9 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(wifi_station_connect),
DEFINE_MODULE_SYMBOL(wifi_station_disconnect),
DEFINE_MODULE_SYMBOL(wifi_station_get_rssi),
DEFINE_MODULE_SYMBOL(wifi_add_event_callback),
DEFINE_MODULE_SYMBOL(wifi_remove_event_callback),
DEFINE_MODULE_SYMBOL(wifi_event_subscribe),
DEFINE_MODULE_SYMBOL(wifi_event_unsubscribe),
DEFINE_MODULE_SYMBOL(wifi_event_poll),
DEFINE_MODULE_SYMBOL(wifi_get_firmware_ops),
DEFINE_MODULE_SYMBOL(WIFI_TYPE),
// wifi_auto_scan
@ -471,6 +474,14 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(event_group_clear),
DEFINE_MODULE_SYMBOL(event_group_get),
DEFINE_MODULE_SYMBOL(event_group_wait),
// concurrent/task_event_group
DEFINE_MODULE_SYMBOL(task_event_group_construct),
DEFINE_MODULE_SYMBOL(task_event_group_destruct),
DEFINE_MODULE_SYMBOL(task_event_group_claim_bit),
DEFINE_MODULE_SYMBOL(task_event_group_release_bit),
DEFINE_MODULE_SYMBOL(task_event_group_signal),
DEFINE_MODULE_SYMBOL(task_event_group_wait),
DEFINE_MODULE_SYMBOL(task_event_group_wait_any),
// concurrent/thread
DEFINE_MODULE_SYMBOL(thread_alloc),
DEFINE_MODULE_SYMBOL(thread_alloc_full),

View File

@ -1,7 +1,6 @@
#include <tactility/system_event.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/delay.h>
#include <tactility/error.h>
#include <tactility/time.h>
@ -30,9 +29,9 @@ struct KernelEventMutex {
static KernelEventMutex subscriptions_mutex;
// Intrusive singly-linked list of poll subscriptions (system_event_subscribe()/_unsubscribe()/
// _await()), separate from the callback-based `subscriptions` vector above. Guarded by its own
// mutex since notifying a poll subscriber never invokes caller code (just a memcpy and an
// xTaskNotifyGive), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
// _poll()), separate from the callback-based `subscriptions` vector above. Guarded by its own
// mutex since notifying a poll subscriber never invokes caller code (just a memcpy and a
// task_event_group_signal), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
static SystemEventSubscription* poll_subscriptions = nullptr;
static KernelEventMutex poll_subscriptions_mutex;
@ -68,9 +67,9 @@ error_t system_event_callback_remove(
return result;
}
// Copies `data` into every current poll subscriber of `type` and signals its wakeup semaphore.
// Copies `data` into every current poll subscriber of `type` and signals its wakeup bit.
// Held entirely under the lock: unlike the callback path, this never invokes caller code
// (just a memcpy and a semaphore give), so there is nothing that could reenter and deadlock.
// (just a memcpy and a signal), so there is nothing that could reenter and deadlock.
static void notify_poll_subscribers(
SystemEventType type,
uint64_t timestamp,
@ -88,7 +87,7 @@ static void notify_poll_subscribers(
}
sub->event.data_len = copied_len;
sub->internal.sequence++;
xSemaphoreGive(sub->internal.semaphore);
task_event_group_signal(sub->internal.event_group, sub->bit);
}
}
@ -165,27 +164,11 @@ error_t system_event_emit(
return ERROR_NONE;
}
error_t system_event_subscribe(SystemEventSubscription* sub) {
// Wait out any system_event_unsubscribe() call still draining old awaiters for this same
// `sub` on another task (see internal.unsubscribe_in_progress). waiter_count/cancelled
// belong to `sub` itself, not to a given registration - reusing `sub` before that call
// finishes would reset them out from under it, and could hand out a fresh semaphore for it
// to then promptly delete instead of the old one, while an old awaiter is still blocked on
// the real old semaphore.
while (true) {
mutex_lock(&poll_subscriptions_mutex.handle);
bool busy = sub->internal.unsubscribe_in_progress;
mutex_unlock(&poll_subscriptions_mutex.handle);
if (!busy) {
break;
}
delay_ticks(pdMS_TO_TICKS(10));
}
SemaphoreHandle_t semaphore = xSemaphoreCreateBinary();
if (semaphore == nullptr) {
return ERROR_OUT_OF_MEMORY;
error_t system_event_subscribe(SystemEventSubscription* sub, TaskEventGroup* event_group) {
uint32_t bit;
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
if (claim_result != ERROR_NONE) {
return claim_result;
}
mutex_lock(&poll_subscriptions_mutex.handle);
@ -196,15 +179,15 @@ error_t system_event_subscribe(SystemEventSubscription* sub) {
for (SystemEventSubscription* existing = poll_subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&poll_subscriptions_mutex.handle);
vSemaphoreDelete(semaphore);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
}
}
sub->internal.semaphore = semaphore;
sub->internal.event_group = event_group;
sub->bit = bit;
sub->internal.sequence = 0;
sub->internal.consumed_sequence = 0;
sub->internal.waiter_count = 0;
sub->internal.cancelled = false;
sub->event.data_len = 0;
sub->internal.next = poll_subscriptions;
@ -217,7 +200,6 @@ error_t system_event_subscribe(SystemEventSubscription* sub) {
error_t system_event_unsubscribe(SystemEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
SemaphoreHandle_t semaphore_to_delete = nullptr;
mutex_lock(&poll_subscriptions_mutex.handle);
for (SystemEventSubscription** link = &poll_subscriptions; *link != nullptr; link = &(*link)->internal.next) {
@ -228,101 +210,36 @@ error_t system_event_unsubscribe(SystemEventSubscription* sub) {
}
}
if (result == ERROR_NONE) {
// Unlinked first, so notify_poll_subscribers() can no longer reach this subscription.
// Mark it cancelled (checked by system_event_await()'s loop) and capture the semaphore
// handle into a local variable rather than deleting it via sub->internal.semaphore
// directly - a concurrent system_event_subscribe() re-registering this same `sub` after
// this point would overwrite that field with a freshly created semaphore, and we must
// not delete the wrong (newly active) one.
// Diagnostic only now (see @warning) - not relied on for safety. Best-effort nudge for a
// task that might still be blocked in task_event_group_wait() on this bit; unlike before,
// this does not wait for it to leave before the bit is released.
sub->internal.cancelled = true;
// Blocks a concurrent system_event_subscribe() from reusing `sub` until this whole
// call returns - see internal.unsubscribe_in_progress and system_event_subscribe().
sub->internal.unsubscribe_in_progress = true;
semaphore_to_delete = sub->internal.semaphore;
sub->internal.semaphore = nullptr;
task_event_group_signal(sub->internal.event_group, sub->bit);
task_event_group_release_bit(sub->internal.event_group, sub->bit);
}
mutex_unlock(&poll_subscriptions_mutex.handle);
if (result != ERROR_NONE) {
return result;
}
// Nudge any task already blocked in system_event_await() (it captured its own local copy
// of this same semaphore handle before this point, so it's unaffected by the field having
// just been cleared above) so it re-checks `cancelled` and bails out now instead of waiting
// out its full timeout, then wait for it to actually leave the semaphore before deleting it
// - FreeRTOS requires no task be blocked on a semaphore when it's deleted.
xSemaphoreGive(semaphore_to_delete);
while (true) {
mutex_lock(&poll_subscriptions_mutex.handle);
bool still_waiting = sub->internal.waiter_count > 0;
mutex_unlock(&poll_subscriptions_mutex.handle);
if (!still_waiting) {
break;
}
delay_ticks(pdMS_TO_TICKS(10));
}
vSemaphoreDelete(semaphore_to_delete);
// Reset under the lock, together, as the last step - only past this point is `sub` safe
// for system_event_subscribe() to reuse (see internal.unsubscribe_in_progress and the
// busy-wait at the top of system_event_subscribe()).
mutex_lock(&poll_subscriptions_mutex.handle);
sub->internal.cancelled = false;
sub->internal.unsubscribe_in_progress = false;
mutex_unlock(&poll_subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t system_event_await(SystemEventSubscription* sub, TickType_t timeout) {
mutex_lock(&poll_subscriptions_mutex.handle);
SemaphoreHandle_t semaphore = sub->internal.semaphore;
sub->internal.waiter_count++;
mutex_unlock(&poll_subscriptions_mutex.handle);
error_t result = ERROR_NONE;
// sequence/consumed_sequence are written by notify_poll_subscribers() under
// poll_subscriptions_mutex - read (and, on a match, updated) under the same lock each
// iteration, rather than compared lock-free, so a concurrent emit can't land between an
// unlocked read and this loop acting on it.
//
// Compare against consumed_sequence, not a sequence snapshot taken now - an emit that
// landed between system_event_subscribe() and this call already incremented sequence and
// gave the semaphore, so that event is pending but unconsumed. Snapshotting "now" would
// make the loop wait for yet another event instead of returning this already-pending one.
while (true) {
mutex_lock(&poll_subscriptions_mutex.handle);
bool pending = sub->internal.sequence != sub->internal.consumed_sequence;
bool cancelled = sub->internal.cancelled;
if (pending) {
sub->internal.consumed_sequence = sub->internal.sequence;
}
mutex_unlock(&poll_subscriptions_mutex.handle);
if (pending) {
break;
}
if (cancelled) {
result = ERROR_INVALID_STATE;
break;
}
if (xSemaphoreTake(semaphore, timeout) == pdFALSE) {
result = ERROR_TIMEOUT;
break;
}
}
mutex_lock(&poll_subscriptions_mutex.handle);
sub->internal.waiter_count--;
mutex_unlock(&poll_subscriptions_mutex.handle);
return result;
}
error_t system_event_poll(SystemEventSubscription* sub) {
mutex_lock(&poll_subscriptions_mutex.handle);
bool pending = sub->internal.sequence != sub->internal.consumed_sequence;
bool cancelled = sub->internal.cancelled;
if (pending) {
sub->internal.consumed_sequence = sub->internal.sequence;
}
mutex_unlock(&poll_subscriptions_mutex.handle);
if (pending) {
return ERROR_NONE;
}
if (cancelled) {
return ERROR_INVALID_STATE;
}
return ERROR_TIMEOUT;
}
error_t system_event_get_data(SystemEventSubscription* sub, uint8_t* data, size_t data_len) {
// sub->event.* is written by notify_poll_subscribers() under poll_subscriptions_mutex -
// the length check and the copy must happen as one snapshot under the same lock, otherwise

View File

@ -216,17 +216,20 @@ TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes an
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_b);
}
// gps.h-style poll subscription: system_event_subscribe()/_await()/_unsubscribe().
// gps.h-style poll subscription: system_event_subscribe()/_poll()/_unsubscribe().
//
// system_event_await() only detects sequence increments that happen *after* it starts
// waiting (same as gps_api_event_await()), so the emit must be started from another task
// while this one is already blocked in await() - emitting first and awaiting after would
// race the notification the same way it would with any FreeRTOS task-notify consumer.
// system_event_poll() only detects sequence increments that happened before it's called (same
// as gps_api_event_await()), so tests that need to observe an emit from another task block via
// task_event_group_wait() first - emitting before that wait started would race the notification
// the same way it would with any FreeRTOS task-notify consumer.
TEST_CASE("system_event_subscribe/_poll deliver the event payload by value") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
TEST_CASE("system_event_subscribe/_await deliver the event payload by value") {
SystemEventSubscription sub {};
sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
auto* thread = thread_alloc_full(
@ -243,7 +246,8 @@ TEST_CASE("system_event_subscribe/_await deliver the event payload by value") {
);
CHECK_EQ(thread_start(thread), ERROR_NONE);
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
NetworkConnectedEvent received {};
CHECK_EQ(system_event_get_data(&sub, reinterpret_cast<uint8_t*>(&received), sizeof(received)), ERROR_NONE);
@ -255,62 +259,78 @@ TEST_CASE("system_event_subscribe/_await deliver the event payload by value") {
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
task_event_group_destruct(&event_group);
}
TEST_CASE("system_event_await returns a matching event that arrived before it started waiting") {
TEST_CASE("system_event_poll returns a matching event that arrived before it was called") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
SystemEventSubscription sub {};
sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
// Same-thread emit, no background thread needed: unlike the "detects a change after it
// starts waiting" tests above, this is exactly the case system_event_await() must handle -
// sequence already moved ahead of consumed_sequence before await() is even called.
// Same-thread emit, no background thread or wait needed: unlike the "detects a change after
// it starts waiting" tests above, this is exactly the case system_event_poll() must handle -
// sequence already moved ahead of consumed_sequence before poll() is even called.
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
CHECK_EQ(system_event_emit(KERNEL_EVENT_NETWORK_CONNECTED, &connected, sizeof(connected)), ERROR_NONE);
CHECK_EQ(system_event_await(&sub, 0), ERROR_NONE);
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
NetworkConnectedEvent received {};
CHECK_EQ(system_event_get_data(&sub, reinterpret_cast<uint8_t*>(&received), sizeof(received)), ERROR_NONE);
CHECK_EQ(received.ipv4_addr, connected.ipv4_addr);
CHECK_EQ(received.gateway, connected.gateway);
// The pending event was consumed by the call above - a second await() with no further
// emit must time out rather than returning the same event again.
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
// The pending event was consumed by the call above - a second poll() with no further emit
// must time out rather than returning the same event again.
CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT);
system_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
}
TEST_CASE("system_event_await times out when no matching event has arrived") {
TEST_CASE("system_event_poll times out when no matching event has arrived") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
SystemEventSubscription sub {};
sub.event.type = KERNEL_EVENT_TIME_CHANGED;
system_event_subscribe(&sub);
system_event_subscribe(&sub, &event_group);
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT);
system_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
}
TEST_CASE("system_event_emit does not notify a poll subscriber of a different type") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
SystemEventSubscription sub {};
sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
system_event_subscribe(&sub);
system_event_subscribe(&sub, &event_group);
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT);
system_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
}
TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the buffer untouched") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
SystemEventSubscription sub {};
sub.event.type = KERNEL_EVENT_NETWORK_DISCONNECTED;
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
// system_event_await() only detects sequence increments that happen *after* it starts
// waiting (see the comment above), so the emit must come from another task while this one
// is already blocked in await() - same pattern as the payload-delivery test above.
// Background emit + wait, same pattern as the payload-delivery test above - see the comment
// near the top of the file.
NetworkDisconnectedEvent disconnected { .device = nullptr };
auto* thread = thread_alloc_full(
"system-event-emitter",
@ -325,7 +345,8 @@ TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the bu
-1
);
CHECK_EQ(thread_start(thread), ERROR_NONE);
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
thread_free(thread);
@ -337,12 +358,16 @@ TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the bu
CHECK_EQ(system_event_get_data(&sub, exact, sizeof(exact)), ERROR_NONE);
system_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
}
TEST_CASE("system_event_get_data on a subscription with no payload copies nothing and succeeds") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
SystemEventSubscription sub {};
sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
auto* thread = thread_alloc_full(
"system-event-emitter",
@ -356,7 +381,8 @@ TEST_CASE("system_event_get_data on a subscription with no payload copies nothin
-1
);
CHECK_EQ(thread_start(thread), ERROR_NONE);
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
thread_free(thread);
@ -365,59 +391,44 @@ TEST_CASE("system_event_get_data on a subscription with no payload copies nothin
CHECK_EQ(buffer[0], 0x42); // untouched - nothing to copy
system_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
}
// Regression coverage for system_event_unsubscribe() racing a task blocked in
// system_event_await() on the same subscription, and for reusing a subscription node after
// unsubscribing it - see the @warning on system_event_unsubscribe() in system_event.h.
// Regression coverage for system_event_unsubscribe()'s (now best-effort, not guaranteed - see
// its @warning in system_event.h) diagnostic signal, and for reusing a subscription node after
// unsubscribing it.
TEST_CASE("system_event_poll returns ERROR_INVALID_STATE after unsubscribe (diagnostic, best-effort)") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
TEST_CASE("system_event_unsubscribe wakes a task blocked in system_event_await with ERROR_INVALID_STATE") {
SystemEventSubscription sub {};
sub.event.type = KERNEL_EVENT_SERVICE_STARTED;
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
auto* thread = thread_alloc_full(
"system-event-awaiter",
4096,
[](void* context) {
auto* awaited_sub = static_cast<SystemEventSubscription*>(context);
// Long timeout - the point is that unsubscribe() wakes this early, not that it
// eventually times out on its own.
return static_cast<int32_t>(system_event_await(awaited_sub, pdMS_TO_TICKS(5000)));
},
&sub,
-1
);
CHECK_EQ(thread_start(thread), ERROR_NONE);
// Give the awaiter task a moment to actually reach xSemaphoreTake() before unsubscribing -
// otherwise this test wouldn't exercise the "already blocked" race at all.
delay_millis(20);
// Must return promptly (nudging the blocked awaiter awake), not by waiting out its timeout.
TickType_t before = get_ticks();
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
CHECK_LT(get_ticks() - before, pdMS_TO_TICKS(1000));
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
CHECK_EQ(thread_get_return_code(thread), ERROR_INVALID_STATE);
thread_free(thread);
// `sub` is still caller-owned storage after unsubscribe - polling it directly (rather than
// still being blocked in task_event_group_wait() on it, which system_event_unsubscribe()'s
// @warning now says not to do) is the one remaining diagnostic case `cancelled` covers.
CHECK_EQ(system_event_poll(&sub), ERROR_INVALID_STATE);
// A second unsubscribe() has nothing left to do.
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
task_event_group_destruct(&event_group);
}
TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscribe") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
SystemEventSubscription sub {};
sub.event.type = KERNEL_EVENT_SERVICE_STOPPED;
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
// Re-registering the same node (same storage, not a fresh SystemEventSubscription) must
// work as if it were new - a fresh semaphore, and no leftover `cancelled` state from the
// work as if it were new - a fresh bit, and no leftover `cancelled` state from the
// unsubscribe() above causing an immediate spurious ERROR_INVALID_STATE below.
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE);
auto* thread = thread_alloc_full(
"system-event-emitter",
@ -431,9 +442,12 @@ TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscri
-1
);
CHECK_EQ(thread_start(thread), ERROR_NONE);
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
CHECK_EQ(system_event_poll(&sub), ERROR_NONE);
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
thread_free(thread);
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
task_event_group_destruct(&event_group);
}

View 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);
}

View File

@ -21,24 +21,30 @@ static void create_widgets(lv_obj_t* parent, void* userData) {
int main(int argc, char* argv[]) {
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 };
app_event_subscribe(&sub);
app_event_subscribe(&sub, &event_group);
WindowId window = window_manager_create(app_instance_id, create_widgets, NULL);
bool should_close = false;
while (!should_close) {
task_event_group_wait_any(&event_group, NULL, portMAX_DELAY);
struct AppEvent event;
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
break;
}
if (event.type == APP_EVENT_CLOSE) {
should_close = true;
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
should_close = true;
break;
}
}
}
window_manager_remove(window);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}