Refactored events (#621)

Event handling:

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

Wi-Fi:

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

Other:
- Renamed the Kernel Display app to Display.
This commit is contained in:
Ken Van Hoeylandt
2026-08-25 17:44:20 +02:00
committed by GitHub
parent db48dfe812
commit ab75d2022d
121 changed files with 2416 additions and 1381 deletions
@@ -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;
}
}
+97 -62
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();
@@ -189,18 +229,7 @@ void dispatchConnect() {
LOG_I(TAG, "Connecting to %s", target.ssid.c_str());
error_t result = wifi_station_connect(state.device, target.ssid.c_str(), target.password.c_str(), target.channel);
if (result != ERROR_NONE) {
LOG_E(TAG, "Failed to connect to %s (%s)", target.ssid.c_str(), error_to_string(result));
WifiEvent event = {};
event.type = WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT;
// The driver couldn't even initiate the connection attempt; there's no
// more specific WifiStationConnectionError for that.
event.connection_error = WIFI_STATION_CONNECTION_ERROR_TIMEOUT;
publish(event);
}
// On success, WIFI_EVENT_TYPE_STATION_STATE_CHANGED / _CONNECTION_RESULT arrive
// asynchronously via onWifiDeviceEvent().
wifi_station_connect(state.device, target.ssid.c_str(), target.password.c_str(), target.channel);
}
void dispatchDisconnect() {
@@ -233,12 +262,14 @@ bool findAutoConnectAp(settings::WifiApSettings& out) {
void dispatchAutoConnect() {
LOG_I(TAG, "dispatchAutoConnect()");
if (state.pauseAutoConnect || state.externalScanPause.load()) {
if (state.pauseAutoConnect || state.externalScanPause.load() || !isRadioOn()) {
// A manual disconnect() or an in-progress manual connect() has paused
// auto-connect, or a caller (e.g. AutoScanPauseGuard) has externally paused it.
// This is called on every SCAN_FINISHED, not just the auto-connect timer's own
// scans (e.g. WifiManage re-scans on show), so it must honor the pause instead of
// reconnecting unconditionally.
// reconnecting unconditionally. The radio-off check matters because a scan that was
// already in flight can finish after the user turns the radio off. Without it,
// connect() would call dispatchSetEnabled(true) and turn the radio back on.
return;
}
RadioState radio_state = getRadioState();
@@ -278,7 +309,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(); });
@@ -333,10 +364,6 @@ void onWifiDeviceEvent(Device* device, void* /*context*/, ::WifiEvent event) {
default:
break;
}
// Forward the event as-is: subscribers inspect event.type and the
// relevant union field directly, same as this function does.
publish(event);
}
void autoScanSetPaused(bool paused) {
@@ -348,12 +375,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 +426,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 +528,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 +566,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;