Bluetooth and Wi-Fi improvements (#622)

- Enable BT by default in DTS, and create separate radio on/of functionality (like recent Wi-Fi changes)
- Bluetooth event handling now uses queued subscriptions, improving event delivery and application responsiveness.
- Improved synchronization of Bluetooth advertising, connections, HID, MIDI, and SPP operations.
- Enhanced cleanup during Bluetooth shutdown and subscription removal.
- Clarified Bluetooth start and stop behavior.
- Improved wifi driver subscription stability to prevent endless loops
This commit is contained in:
Ken Van Hoeylandt
2026-08-26 00:30:03 +02:00
committed by GitHub
parent ab75d2022d
commit 0ee2415f3b
50 changed files with 458 additions and 270 deletions
@@ -22,7 +22,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
-1
View File
@@ -22,7 +22,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
-1
View File
@@ -23,7 +23,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -24,7 +24,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -24,7 +24,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -24,7 +24,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -24,7 +24,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
-1
View File
@@ -28,7 +28,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -34,7 +34,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -25,7 +25,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -22,7 +22,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -52,7 +52,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
-1
View File
@@ -50,7 +50,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -23,7 +23,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -23,7 +23,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
-1
View File
@@ -27,7 +27,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -31,7 +31,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -31,7 +31,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -32,7 +32,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -35,7 +35,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -27,7 +27,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -35,7 +35,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -28,7 +28,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
-1
View File
@@ -33,7 +33,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
-1
View File
@@ -28,7 +28,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
-1
View File
@@ -26,7 +26,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -25,7 +25,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -24,7 +24,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -25,7 +25,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -25,7 +25,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -24,7 +24,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
@@ -24,7 +24,6 @@
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
+1 -1
View File
@@ -28,7 +28,7 @@
- Improve Setup: Show "Step done" screen
- Improve Setup: Add keyboard/keypad navigation explanation
- display.h API: get_backlight does not change ref counting, but it should
- bluetooth: various getters for child devices do not change ref counting, but they should
- bluetooth: various getters for child devices do not change ref counting, but they should (e.g. bluetooth_hid_device_get_device())
- Improve kernel_init.cpp (and other modules): create driver_ensure_added() and driver_ensure_destructed()
- Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module.
- LilyGO T-Dongle S3: 1 button control, stop auto-launching web server
@@ -30,17 +30,16 @@
#include <bluetooth/esp32_ble_midi.h>
#include <bluetooth/esp32_ble_hid.h>
#define BLE_MAX_CALLBACKS 8
struct BleCallbackEntry {
BtEventCallback fn;
void* ctx;
};
struct BleCtx {
// Mutexes
SemaphoreHandle_t radio_mutex; // guards radio state transitions
SemaphoreHandle_t cb_mutex; // guards callbacks array
// Guards GAP/GATT advertising & profile-switch calls (on_sync()'s advertising decision vs.
// hid/spp/midi's _start()/_stop()). Deliberately separate from radio_mutex: radio_mutex can
// be held for the duration of nimble_port_stop(), which blocks waiting for the NimBLE host
// task to exit its run loop - taking that same mutex from code that can run ON the host task
// (on_sync()) would deadlock disable against it. Nothing held under gap_mutex ever waits on
// host-task progress, so it's safe to take from the host task itself.
SemaphoreHandle_t gap_mutex;
// Radio / scan state (atomic — read from multiple tasks)
std::atomic<BtRadioState> radio_state;
@@ -48,9 +47,9 @@ struct BleCtx {
// Set by Tactility HID host to prevent simultaneous central connection during name resolution
std::atomic<bool> hid_host_active;
// Event callbacks (guarded by cb_mutex)
BleCallbackEntry callbacks[BLE_MAX_CALLBACKS];
size_t callback_count;
// Event subscriptions (guarded by subscriptionsMutex)
Mutex subscriptionsMutex;
BtEventSubscription* subscriptions;
// Connection handles + active flags (atomic — accessed from multiple tasks)
std::atomic<uint16_t> spp_conn_handle;
@@ -98,7 +97,7 @@ struct BleCtx {
ble_addr_t scan_addrs[64];
size_t scan_count;
// Device reference (passed to BtEventCallback)
// Device reference (passed to subscribers via BtEvent delivery)
struct Device* device;
// Child devices (created by esp32_ble_start_device, destroyed by stop_device)
@@ -96,34 +96,47 @@ void ble_set_scan_active(struct Device* device, bool v) {
void ble_publish_event(struct Device* device, struct BtEvent event) {
BleCtx* ctx = ble_get_ctx(device);
if (!ctx) return;
// Copy under mutex so callbacks can safely call add/remove_event_callback
BleCallbackEntry local[BLE_MAX_CALLBACKS];
size_t count;
xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY);
count = ctx->callback_count;
memcpy(local, ctx->callbacks, count * sizeof(BleCallbackEntry));
xSemaphoreGive(ctx->cb_mutex);
for (size_t i = 0; i < count; i++) {
local[i].fn(ctx->device, local[i].ctx, event);
mutex_lock(&ctx->subscriptionsMutex);
for (BtEventSubscription* sub = ctx->subscriptions; sub != nullptr; sub = sub->internal.next) {
mutex_lock(&sub->internal.ring_mutex);
if (sub->internal.count < BT_EVENT_QUEUE_CAPACITY) {
uint8_t tail = (sub->internal.head + sub->internal.count) % BT_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);
}
// ---- Advertising restart helper ----
//TODO: Fix Restart name-only advertising after a rename.
// Delay before on_sync() falls back to name-only advertising - see its call site.
constexpr uint64_t ADV_ON_SYNC_DELAY_US = 100'000;
static void adv_restart_callback(void* arg) {
struct Device* device = (struct Device*)arg;
BleCtx* ctx = ble_get_ctx(device);
if (ctx->radio_state.load() != BT_RADIO_STATE_ON) return;
// gap_mutex (not radio_mutex - this can run on the host task and must never wait on a mutex
// disable can hold across nimble_port_stop()) serializes this against hid/spp/midi_device_
// start()/_stop(), which touch the same GAP/GATT state from another task.
xSemaphoreTake(ctx->gap_mutex, portMAX_DELAY);
uint16_t hid_conn = ble_hid_get_conn_handle(ctx->hid_device_child);
if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising(device, &MIDI_SVC_UUID);
} else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising(device, &NUS_SVC_UUID);
} else if (ctx->hid_active.load() && hid_conn == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising_hid(device, hid_appearance);
} else if (!ctx->hid_active.load() && !ctx->spp_active.load() && !ctx->midi_active.load()) {
// Nothing profile-specific active - name-only keeps the radio discoverable/connectable.
ble_start_advertising(device, nullptr);
}
xSemaphoreGive(ctx->gap_mutex);
}
void ble_schedule_adv_restart(struct Device* device, uint64_t delay_us) {
@@ -441,17 +454,13 @@ static void on_sync() {
e.radio_state = BT_RADIO_STATE_ON;
ble_publish_event(ctx->device, e);
// The Tactility bridge handles auto-start (SPP/MIDI/HID) in response to
// BT_EVENT_RADIO_STATE_CHANGED(ON), dispatched to the main task above.
// On a multi-core ESP32 the main task can preempt the NimBLE host task
// between ble_publish_event() and here, call hid/spp/midi_device_start(),
// set hid/spp/midi_active, and already start profile-specific advertising.
// Only start name-only advertising if no profile has been activated yet;
// otherwise we would overwrite the correct profile advertising with name-only,
// causing Windows to connect without seeing the HID/SPP/MIDI service UUID.
if (!ctx->hid_active.load() && !ctx->spp_active.load() && !ctx->midi_active.load()) {
ble_start_advertising(ctx->device, nullptr);
}
// Deferred, not synchronous: an app reacting to the event just published above (e.g.
// starting HID) races this to set hid/spp/midi_active, and immediately starting name-only
// advertising only to tear it down again a few ms later for a profile switch was observed to
// corrupt the NimBLE host's HCI event pool (crash in os_memblock_get, called from
// lld_adv_end_ind_handler_hack). The delay gives that app time to react first;
// adv_restart_callback() re-checks the active flags, so this is a no-op if one did.
ble_schedule_adv_restart(ctx->device, ADV_ON_SYNC_DELAY_US);
}
static void dispatch_disable_timer_cb(void* arg) {
@@ -995,38 +1004,54 @@ static error_t api_disconnect(struct Device* device, const BtAddr addr, enum BtP
return ERROR_NOT_SUPPORTED;
}
static error_t api_add_event_callback(struct Device* device, void* cb_ctx, BtEventCallback fn) {
static error_t api_event_subscribe(struct Device* device, BtEventSubscription* sub, TaskEventGroup* event_group) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx || !fn) return ERROR_INVALID_ARGUMENT;
xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY);
if (ctx->callback_count >= BLE_MAX_CALLBACKS) {
xSemaphoreGive(ctx->cb_mutex);
return ERROR_OUT_OF_MEMORY;
if (!ctx || !sub || !event_group) 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;
}
ctx->callbacks[ctx->callback_count].fn = fn;
ctx->callbacks[ctx->callback_count].ctx = cb_ctx;
ctx->callback_count++;
xSemaphoreGive(ctx->cb_mutex);
mutex_lock(&ctx->subscriptionsMutex);
// Avoid cyclic subscription list that would loop forever
for (BtEventSubscription* existing = ctx->subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&ctx->subscriptionsMutex);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
}
}
sub->internal.event_group = event_group;
sub->bit = bit;
sub->internal.next = ctx->subscriptions;
ctx->subscriptions = sub;
mutex_unlock(&ctx->subscriptionsMutex);
return ERROR_NONE;
}
static error_t api_remove_event_callback(struct Device* device, BtEventCallback fn) {
static error_t api_event_unsubscribe(struct Device* device, BtEventSubscription* sub) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx || !fn) return ERROR_INVALID_ARGUMENT;
xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY);
for (size_t i = 0; i < ctx->callback_count; i++) {
if (ctx->callbacks[i].fn == fn) {
// Shift remaining entries down
for (size_t j = i; j + 1 < ctx->callback_count; j++) {
ctx->callbacks[j] = ctx->callbacks[j + 1];
}
ctx->callback_count--;
xSemaphoreGive(ctx->cb_mutex);
return ERROR_NONE;
if (!ctx || !sub) return ERROR_INVALID_ARGUMENT;
error_t result = ERROR_NOT_FOUND;
mutex_lock(&ctx->subscriptionsMutex);
for (BtEventSubscription** link = &ctx->subscriptions; *link != nullptr; link = &(*link)->internal.next) {
if (*link == sub) {
*link = sub->internal.next;
result = ERROR_NONE;
break;
}
}
xSemaphoreGive(ctx->cb_mutex);
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;
}
static error_t api_set_device_name(struct Device* device, const char* name) {
@@ -1079,8 +1104,8 @@ const BluetoothApi nimble_bluetooth_api = {
.get_paired_peers = api_get_paired_peers,
.connect = api_connect,
.disconnect = api_disconnect,
.add_event_callback = api_add_event_callback,
.remove_event_callback = api_remove_event_callback,
.event_subscribe = api_event_subscribe,
.event_unsubscribe = api_event_unsubscribe,
.set_device_name = api_set_device_name,
.get_device_name = api_get_device_name,
.set_hid_host_active = api_set_hid_host_active,
@@ -1116,11 +1141,12 @@ static void destroy_child_device(struct Device*& child) {
static error_t esp32_ble_start_device(struct Device* device) {
BleCtx* ctx = new BleCtx();
ctx->radio_mutex = xSemaphoreCreateMutex();
ctx->cb_mutex = xSemaphoreCreateMutex();
ctx->gap_mutex = xSemaphoreCreateMutex();
mutex_construct(&ctx->subscriptionsMutex);
ctx->subscriptions = nullptr;
ctx->radio_state.store(BT_RADIO_STATE_OFF);
ctx->scan_active.store(false);
ctx->hid_host_active.store(false);
ctx->callback_count = 0;
ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
ctx->spp_active.store(false);
ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
@@ -1206,6 +1232,29 @@ static error_t esp32_ble_stop_device(struct Device* device) {
ctx->disable_timer = nullptr;
}
if (ctx->gap_mutex != nullptr) {
vSemaphoreDelete(ctx->gap_mutex);
ctx->gap_mutex = nullptr;
}
// Release any subscribers that never unsubscribed: device_stop() doesn't wait for apps still
// using this device, so a later bluetooth_event_unsubscribe() would find no ctx and skip
// releasing the bit.
mutex_lock(&ctx->subscriptionsMutex);
for (BtEventSubscription* sub = ctx->subscriptions; sub != nullptr;) {
BtEventSubscription* next = sub->internal.next;
// Force-close, don't destruct: a concurrent bluetooth_event_poll() may hold/await this
// same lock (see BtEventSubscription::internal::closed).
mutex_lock(&sub->internal.ring_mutex);
sub->internal.closed = true;
mutex_unlock(&sub->internal.ring_mutex);
task_event_group_release_bit(sub->internal.event_group, sub->bit);
sub = next;
}
ctx->subscriptions = nullptr;
mutex_unlock(&ctx->subscriptionsMutex);
mutex_destruct(&ctx->subscriptionsMutex);
s_ctx = nullptr;
device_set_driver_data(device, nullptr);
delete ctx;
@@ -388,17 +388,27 @@ static error_t hid_device_start(struct Device* device, enum BtHidDeviceMode mode
}
hid_appearance = appearance;
if (!ble_hid_switch_profile(device, profile)) {
// gap_mutex (not radio_mutex - see BleCtx::gap_mutex's comment) serializes this GAP/GATT
// sequence against on_sync()'s own advertising decision and spp/midi's start/stop.
BleCtx* root_ctx = ble_get_ctx(device);
xSemaphoreTake(root_ctx->gap_mutex, portMAX_DELAY);
bool switched = ble_hid_switch_profile(device, profile);
if (switched) {
ble_hid_set_active(device, true);
ble_start_advertising_hid(device, hid_appearance);
}
xSemaphoreGive(root_ctx->gap_mutex);
if (!switched) {
delete (BleHidDeviceCtx*)device_get_driver_data(device);
device_set_driver_data(device, nullptr);
return ERROR_INVALID_STATE;
}
ble_hid_set_active(device, true);
ble_start_advertising_hid(device, hid_appearance);
return ERROR_NONE;
}
static error_t hid_device_stop(struct Device* device) {
BleCtx* root_ctx = ble_get_ctx(device);
xSemaphoreTake(root_ctx->gap_mutex, portMAX_DELAY);
ble_hid_set_active(device, false);
ble_gap_adv_stop();
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
@@ -421,6 +431,7 @@ static error_t hid_device_stop(struct Device* device) {
delete hid_ctx;
device_set_driver_data(device, nullptr);
}
xSemaphoreGive(root_ctx->gap_mutex);
return ERROR_NONE;
}
@@ -167,13 +167,19 @@ static void midi_keepalive_cb(void* arg) {
// All functions receive the midi child Device*.
static error_t midi_start(struct Device* device) {
// gap_mutex (not radio_mutex - see BleCtx::gap_mutex's comment) serializes this GAP sequence
// against on_sync()'s own advertising decision and hid/spp's start/stop.
BleCtx* root_ctx = ble_get_ctx(device);
xSemaphoreTake(root_ctx->gap_mutex, portMAX_DELAY);
ble_midi_set_active(device, true);
// Create 2-second periodic Active Sensing timer to prevent Windows BLE MIDI
// driver from declaring the connection idle and disconnecting (~8-10 s timeout).
error_t rc = ble_midi_ensure_keepalive(device, midi_keepalive_cb, 2'000'000);
if (rc != ERROR_NONE) return rc;
ble_start_advertising(device, &MIDI_SVC_UUID);
return ERROR_NONE;
if (rc == ERROR_NONE) {
ble_start_advertising(device, &MIDI_SVC_UUID);
}
xSemaphoreGive(root_ctx->gap_mutex);
return rc;
}
error_t ble_midi_start_internal(struct Device* midi_child) {
@@ -181,6 +187,8 @@ error_t ble_midi_start_internal(struct Device* midi_child) {
}
static error_t midi_stop(struct Device* device) {
BleCtx* root_ctx = ble_get_ctx(device);
xSemaphoreTake(root_ctx->gap_mutex, portMAX_DELAY);
ble_midi_set_active(device, false);
ble_midi_stop_keepalive(device);
uint16_t conn = ble_midi_get_conn_handle(device);
@@ -191,6 +199,7 @@ static error_t midi_stop(struct Device* device) {
if (!ble_spp_get_active(device) && !ble_hid_get_active(device)) {
ble_gap_adv_stop();
}
xSemaphoreGive(root_ctx->gap_mutex);
return ERROR_NONE;
}
@@ -126,8 +126,13 @@ void ble_spp_set_conn_handle(struct Device* device, uint16_t h) {
// All functions receive the serial child Device*.
static error_t spp_start(struct Device* device) {
// gap_mutex (not radio_mutex - see BleCtx::gap_mutex's comment) serializes this GAP sequence
// against on_sync()'s own advertising decision and hid/midi's start/stop.
BleCtx* root_ctx = ble_get_ctx(device);
xSemaphoreTake(root_ctx->gap_mutex, portMAX_DELAY);
ble_spp_set_active(device, true);
ble_start_advertising(device, &NUS_SVC_UUID);
xSemaphoreGive(root_ctx->gap_mutex);
return ERROR_NONE;
}
@@ -136,6 +141,8 @@ error_t ble_spp_start_internal(struct Device* serial_child) {
}
static error_t spp_stop(struct Device* device) {
BleCtx* root_ctx = ble_get_ctx(device);
xSemaphoreTake(root_ctx->gap_mutex, portMAX_DELAY);
ble_spp_set_active(device, false);
uint16_t conn = ble_spp_get_conn_handle(device);
if (conn != BLE_HS_CONN_HANDLE_NONE) {
@@ -147,6 +154,7 @@ static error_t spp_stop(struct Device* device) {
if (!ble_midi_get_active(device) && !ble_hid_get_active(device)) {
ble_gap_adv_stop();
}
xSemaphoreGive(root_ctx->gap_mutex);
return ERROR_NONE;
}
@@ -542,10 +542,12 @@ error_t api_event_subscribe(Device* device, WifiEventSubscription* sub, TaskEven
mutex_lock(&ctx->subscriptionsMutex);
// Avoid cyclic subscription list that would loop forever
if (ctx->subscriptions == sub) {
mutex_unlock(&ctx->subscriptionsMutex);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
for (WifiEventSubscription* existing = ctx->subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&ctx->subscriptionsMutex);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
}
}
sub->internal.event_group = event_group;
@@ -639,12 +641,16 @@ error_t stop_device(Device* device) {
// Release any subscribers that never unsubscribed: device_stop() doesn't wait for apps still
// using this device, so a later wifi_event_unsubscribe() would find no ctx and skip releasing
// the bit and destructing sub->internal.ring_mutex.
// the bit.
mutex_lock(&ctx->subscriptionsMutex);
for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr;) {
WifiEventSubscription* next = sub->internal.next;
// Force-close, don't destruct: a concurrent wifi_event_poll() may hold/await this same
// lock (see WifiEventSubscription::internal::closed).
mutex_lock(&sub->internal.ring_mutex);
sub->internal.closed = true;
mutex_unlock(&sub->internal.ring_mutex);
task_event_group_release_bit(sub->internal.event_group, sub->bit);
mutex_destruct(&sub->internal.ring_mutex);
sub = next;
}
ctx->subscriptions = nullptr;
@@ -302,10 +302,12 @@ error_t apiEventSubscribe(Device* device, WifiEventSubscription* sub, TaskEventG
mutex_lock(&ctx->subscriptionsMutex);
// Avoid cyclic subscription list that would loop forever
if (ctx->subscriptions == sub) {
mutex_unlock(&ctx->subscriptionsMutex);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
for (WifiEventSubscription* existing = ctx->subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&ctx->subscriptionsMutex);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
}
}
sub->internal.event_group = event_group;
@@ -382,12 +384,16 @@ error_t stopDevice(Device* device) {
// Release any subscribers that never unsubscribed: device_stop() doesn't wait for apps still
// using this device, so a later wifi_event_unsubscribe() would find no ctx and skip releasing
// the bit and destructing sub->internal.ring_mutex.
// the bit.
mutex_lock(&ctx->subscriptionsMutex);
for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr;) {
WifiEventSubscription* next = sub->internal.next;
// Force-close, don't destruct: a concurrent wifi_event_poll() may hold/await this same
// lock (see WifiEventSubscription::internal::closed).
mutex_lock(&sub->internal.ring_mutex);
sub->internal.closed = true;
mutex_unlock(&sub->internal.ring_mutex);
task_event_group_release_bit(sub->internal.event_group, sub->bit);
mutex_destruct(&sub->internal.ring_mutex);
sub = next;
}
ctx->subscriptions = nullptr;
@@ -26,10 +26,10 @@ struct PeerRecord {
int profileId = 0;
};
// Wrapper around device start & radio on
// Turns the BT radio on (dev is already started - see ble0's devicetree status)
bool start(Device* dev);
// Wrapper around device stop & radio off
// Turns the BT radio off (dev stays started)
bool stop(Device* dev);
bool isRadioOnOrPending(Device* dev);
@@ -7,9 +7,6 @@
#include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h>
#include <atomic>
#include <memory>
namespace tt::app::btmanage {
struct Context {
@@ -19,13 +16,7 @@ struct Context {
State state;
View view = View(&bindings, &state);
Device* btDevice = nullptr;
bool callbackRegistered = false;
// Bumped right before the BT event callback is unregistered at the end of appMain(), to
// invalidate any BT event already dispatched to the main task for this instance. Kept in
// its own heap allocation, independent of Context's (stack-local) lifetime, so a dispatched
// callback can check it without touching a possibly already-destroyed Context.
std::shared_ptr<std::atomic<int>> generation = std::make_shared<std::atomic<int>>(0);
BtEventSubscription btEventSub {};
void lock() { mutex.lock(); }
void unlock() { mutex.unlock(); }
@@ -34,15 +25,4 @@ struct Context {
void onBtEvent(Context* ctx, const struct BtEvent& event);
void requestViewUpdate(Context* ctx);
// Re-attempts registering the device event callback. Needed because the BLE driver only
// allocates its callback list while the device is started/on: a registration attempted while
// the radio is off silently no-ops, so this must be called again right after a successful
// bluetooth::start(). Idempotent: no-ops if already registered for this device.
void registerDeviceCallback(Context* ctx, Device* dev);
// Call after bluetooth::stop(): the driver frees its callback list on stop, so the
// registration state must be cleared here too, without touching the (now-dangling) driver-side
// list.
void forgetCallbackRegistration(Context* ctx);
} // namespace tt::app::btmanage
+26 -74
View File
@@ -3,8 +3,6 @@
#include <Tactility/app/btmanage/BtManagePrivate.h>
#include <Tactility/app/btmanage/View.h>
#include <Tactility/Tactility.h>
#include <app/event.h>
#include <app/manager.h>
#include <app/manifest.h>
@@ -13,6 +11,7 @@
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/log.h>
namespace tt::app::btmanage {
@@ -22,26 +21,17 @@ constexpr auto* TAG = "BtManage";
extern const ::AppManifest manifest;
static void onBtToggled(void* context, bool requestOn) {
static void onBtToggled(void* /*context*/, bool requestOn) {
#if defined(CONFIG_BT_NIMBLE_ENABLED)
auto* ctx = static_cast<Context*>(context);
Device* dev;
if (device_get_first_by_type(&BLUETOOTH_TYPE, &dev) == ERROR_NONE) {
bool radio_on = bluetooth::isRadioOnOrPending(dev);
if (requestOn && !radio_on) {
LOG_I(TAG, "Turning on");
if (bluetooth::start(dev)) {
// The driver only allocates its callback list once the device is started,
// so the registration attempted at startup (while radio was off) was a
// no-op. Register again now that the device is actually up.
registerDeviceCallback(ctx, dev);
}
bluetooth::start(dev);
} else if (!requestOn && radio_on) {
LOG_I(TAG, "Turning off");
if (bluetooth::stop(dev)) {
// A completed stop frees the driver's callback list.
forgetCallbackRegistration(ctx);
}
bluetooth::stop(dev);
}
device_put(dev);
} else {
@@ -85,8 +75,6 @@ static void onForgetPeer(const std::array<uint8_t, 6>& addr) {
bluetooth::unpair(addr);
}
static void onKernelBtEvent(Device* /*device*/, void* context, BtEvent event);
void requestViewUpdate(Context* ctx) {
// Lock order must match appMain()'s setup/teardown: both run under the LVGL lock
// and then take `ctx->mutex` internally. Taking `mutex` before lvgl_lock() here would
@@ -143,50 +131,6 @@ void onBtEvent(Context* ctx, const BtEvent& event) {
requestViewUpdate(ctx);
}
static void onKernelBtEvent(Device* /*device*/, void* context, BtEvent event) {
// BT event callbacks can fire from the NimBLE host task (e.g. DISCONNECT during
// nimble_port_stop shutdown). Calling onBtEvent() synchronously from the NimBLE
// task would block it on the LVGL mutex (held by the LVGL task waiting in
// nimble_port_stop), creating a permanent deadlock. Dispatch to the main task so
// the NimBLE host task is never blocked by BtManage's state updates or LVGL lock.
auto* ctx = static_cast<Context*>(context);
// Captured while `ctx` is still guaranteed valid (the callback is only invoked while
// registered, i.e. before appMain()'s cleanup removes it). Comparing this later -
// without dereferencing `ctx` - lets the dispatched lambda detect a stale event from an
// instance that has since closed (and had its Context destroyed) without a UAF: the
// generation bump in appMain()'s cleanup always happens before window_manager_remove()
// destroys ctx's widgets, and this dispatched lambda always re-reads the live generation
// at run time (not at dispatch time), so a bump landing anywhere before this lambda
// actually runs is enough to make it skip touching ctx.
auto generation = ctx->generation;
int expectedGeneration = generation->load();
getMainDispatcher().dispatch([ctx, generation, expectedGeneration, event] {
if (generation->load() != expectedGeneration) {
return;
}
onBtEvent(ctx, event);
});
}
void registerDeviceCallback(Context* ctx, Device* dev) {
ctx->lock();
if (ctx->btDevice == dev && !ctx->callbackRegistered) {
// Only latch the flag on success: while the radio is off the driver has no
// callback list yet, so this add is a silent no-op and must be retried once
// bluetooth::start() actually brings the device up.
if (bluetooth_add_event_callback(dev, ctx, onKernelBtEvent) == ERROR_NONE) {
ctx->callbackRegistered = true;
}
}
ctx->unlock();
}
void forgetCallbackRegistration(Context* ctx) {
ctx->lock();
ctx->callbackRegistered = false;
ctx->unlock();
}
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 = {} };
@@ -229,13 +173,18 @@ int32_t appMain(int argc, char* argv[]) {
AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
ctx.btDevice = dev;
if (ctx.btDevice) {
registerDeviceCallback(&ctx, ctx.btDevice);
// dev is started for the process lifetime once ble0 is enabled in the devicetree -
// subscribe once here rather than resubscribing on every bluetooth::start()/stop() toggle.
if (dev != nullptr) {
if (bluetooth_event_subscribe(dev, &ctx.btEventSub, &event_group) == ERROR_NONE) {
ctx.btDevice = dev;
} else {
LOG_W(TAG, "Failed to subscribe to BT events");
}
}
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
auto radio_state = bluetooth::getRadioState();
bool can_scan = radio_state == bluetooth::RadioState::On;
LOG_I(TAG, "Radio: %s, Scanning: %d, Can scan: %d",
@@ -261,21 +210,24 @@ int32_t appMain(int argc, char* argv[]) {
}
if (shouldClose) break;
}
if (ctx.btDevice != nullptr) {
BtEvent bt_event {};
while (bluetooth_event_poll(&ctx.btEventSub, &bt_event) == ERROR_NONE) {
onBtEvent(&ctx, bt_event);
}
}
}
// Invalidate any BT event dispatched-but-not-yet-run for this instance before doing
// anything else, so it can't race the teardown below (see onKernelBtEvent()).
ctx.generation->fetch_add(1);
if (ctx.btDevice) {
if (ctx.callbackRegistered) {
bluetooth_remove_event_callback(ctx.btDevice, onKernelBtEvent);
ctx.callbackRegistered = false;
}
device_put(ctx.btDevice);
bluetooth_event_unsubscribe(ctx.btDevice, &ctx.btEventSub);
ctx.btDevice = nullptr;
}
if (dev != nullptr) {
device_put(dev);
}
window_manager_remove(window);
check(app_event_unsubscribe(&sub) == ERROR_NONE);
task_event_group_destruct(&event_group);
@@ -58,8 +58,7 @@ void updateViews(const Context* ctx) {
}
}
void onKernelBtEvent(struct Device* /*device*/, void* context, struct BtEvent /*event*/) {
auto* ctx = static_cast<Context*>(context);
void onBtEvent(Context* ctx) {
lvgl_lock();
updateViews(ctx);
lvgl_unlock();
@@ -196,18 +195,21 @@ int32_t appMain(int argc, char* argv[]) {
}
Device* btDevice = nullptr;
if (device_get_first_active_by_type(&BLUETOOTH_TYPE, &btDevice) == ERROR_NONE) {
bluetooth_add_event_callback(btDevice, &ctx, onKernelBtEvent);
device_put(btDevice);
}
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
Device* btDevice = nullptr;
BtEventSubscription btSub {};
if (device_get_first_active_by_type(&BLUETOOTH_TYPE, &btDevice) == ERROR_NONE) {
if (bluetooth_event_subscribe(btDevice, &btSub, &event_group) != ERROR_NONE) {
device_put(btDevice);
btDevice = nullptr;
}
}
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
@@ -239,10 +241,17 @@ int32_t appMain(int argc, char* argv[]) {
}
if (shouldClose) break;
}
if (btDevice != nullptr) {
BtEvent bt_event {};
while (bluetooth_event_poll(&btSub, &bt_event) == ERROR_NONE) {
onBtEvent(&ctx);
}
}
}
if (device_get_first_active_by_type(&BLUETOOTH_TYPE, &btDevice) == ERROR_NONE) {
bluetooth_remove_event_callback(btDevice, onKernelBtEvent);
if (btDevice != nullptr) {
bluetooth_event_unsubscribe(btDevice, &btSub);
device_put(btDevice);
}
+74 -29
View File
@@ -11,6 +11,7 @@
#include <Tactility/Mutex.h>
#include <Tactility/Tactility.h>
#include <Tactility/Thread.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/bluetooth.h>
@@ -20,6 +21,7 @@
#include <tactility/log.h>
#include <array>
#include <atomic>
#include <cstring>
#include <vector>
@@ -103,12 +105,17 @@ static void cachePeerRecord(const BtPeerRecord& krecord) {
scan_results_cache.push_back(std::move(rec));
}
// ---- Bridge callback (registered with kernel driver) ----
// This callback listens to platform driver events to perform auto-start logic
// and settings management. Consumers should register their own callbacks via
// bluetooth_add_event_callback() to receive events directly.
// ---- Bridge thread (subscribed to the kernel driver) ----
// This thread listens to platform driver events to perform auto-start logic and settings
// management. Consumers should subscribe directly via bluetooth_event_subscribe() to receive
// events themselves.
static void bt_event_bridge(Device*, void* /*context*/, BtEvent event) {
TaskEventGroup btEventGroup {};
BtEventSubscription btEventSub {};
Thread* btEventThread = nullptr;
std::atomic<bool> btEventThreadRunning {false};
static void bt_event_bridge(BtEvent event) {
switch (event.type) {
case BT_EVENT_RADIO_STATE_CHANGED:
switch (event.radio_state) {
@@ -250,6 +257,57 @@ static void bt_event_bridge(Device*, void* /*context*/, BtEvent event) {
}
}
// ---- Bridge thread lifecycle ----
// Runs bt_event_bridge() on its own stack instead of whichever thread published the event, by
// blocking in task_event_group_wait_any() rather than being called back directly.
constexpr configSTACK_DEPTH_TYPE BT_EVENT_THREAD_STACK_SIZE = 4096;
Device* btEventDevice = nullptr;
int32_t btEventThreadMain() {
while (btEventThreadRunning.load()) {
task_event_group_wait_any(&btEventGroup, nullptr, pdMS_TO_TICKS(250));
BtEvent event {};
while (bluetooth_event_poll(&btEventSub, &event) == ERROR_NONE) {
bt_event_bridge(event);
}
}
return 0;
}
bool startBtEventThread(Device* dev) {
if (btEventThread != nullptr) {
return true; // already running
}
task_event_group_construct(&btEventGroup);
if (bluetooth_event_subscribe(dev, &btEventSub, &btEventGroup) != ERROR_NONE) {
task_event_group_destruct(&btEventGroup);
return false;
}
btEventDevice = dev;
btEventThreadRunning = true;
btEventThread = new Thread("bt-events", BT_EVENT_THREAD_STACK_SIZE, [] { return btEventThreadMain(); });
btEventThread->start();
return true;
}
void stopBtEventThread() {
if (btEventThread == nullptr) return;
btEventThreadRunning = false;
btEventThread->join();
delete btEventThread;
btEventThread = nullptr;
bluetooth_event_unsubscribe(btEventDevice, &btEventSub);
task_event_group_destruct(&btEventGroup);
btEventDevice = nullptr;
}
// ---- systemStart ----
void systemStart() {
@@ -271,26 +329,20 @@ bool isRadioOnOrPending(Device* dev) {
return state == BT_RADIO_STATE_ON || state == BT_RADIO_STATE_ON_PENDING;
}
// dev is started (device_start()) once, at kernel_init (see ble0's devicetree status) and never
// stopped for the process lifetime - this only toggles the radio itself, so callers subscribed
// directly to the driver (e.g. BtManage) stay subscribed across on/off toggles instead of
// having to resubscribe.
bool start(Device* dev) {
LOG_I(TAG, "Auto-enabling BLE on boot");
if (!device_is_ready(dev)) {
LOG_I(TAG, "Starting BLE device");
if (device_start(dev) != ERROR_NONE) {
LOG_E(TAG, "Failed to start BLE device");
return false;
}
}
// TODO: Fix bug where repeatedly calling start would add this callback multiple times
if (bluetooth_add_event_callback(dev, nullptr, bt_event_bridge) != ERROR_NONE) {
LOG_E(TAG, "Failed to set BLE callback");
// TODO: Fix bug where repeatedly calling start would try to subscribe the bridge thread twice
if (!startBtEventThread(dev)) {
LOG_E(TAG, "Failed to subscribe to BLE events");
}
LOG_I(TAG, "Enabling BT radio");
if (bluetooth_set_radio_enabled(dev, true) != ERROR_NONE) {
LOG_E(TAG, "Failed to enable BLE radio");
// Add bridge again
bluetooth_remove_event_callback(dev, bt_event_bridge);
stopBtEventThread();
return false;
}
@@ -308,19 +360,12 @@ bool stop(Device* dev) {
return true;
}
if (bluetooth_remove_event_callback(dev, bt_event_bridge) != ERROR_NONE) {
LOG_E(TAG, "Failed to remove BLE callback");
}
stopBtEventThread();
if (bluetooth_set_radio_enabled(dev, false) != ERROR_NONE) {
LOG_E(TAG, "Failed to disable BT radio");
// Re-register bridge
bluetooth_add_event_callback(dev, nullptr, bt_event_bridge);
return false;
}
if (device_stop(dev) != ERROR_NONE) {
LOG_E(TAG, "Failed to stop BT device");
// Re-subscribe bridge
startBtEventThread(dev);
return false;
}
@@ -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>
#ifdef __cplusplus
@@ -134,7 +136,45 @@ struct BtEvent {
};
};
typedef void (*BtEventCallback)(struct Device* device, void* context, struct BtEvent event);
/** Number of events a BtEventSubscription can hold before bluetooth_fire_event() 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 BT_EVENT_QUEUE_CAPACITY 4
/**
* Caller-owned subscription node, registered with bluetooth_event_subscribe() and polled with
* bluetooth_event_poll(). Like wifi_event, queues events by value (FIFO) rather than coalescing
* to the latest one.
* @warning Fields other than `bit` are for internal use only; do not read or write them
* directly.
*/
struct BtEventSubscription {
/** Set by bluetooth_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 bluetooth_event_subscribe(). */
struct TaskEventGroup* event_group;
/** Guards `queue`/`head`/`count`/`closed` between bluetooth_fire_event() (driver thread)
* and bluetooth_event_poll() (caller's thread) - the two live in different translation
* units with no other shared lock. */
struct Mutex ring_mutex;
struct BtEvent queue[BT_EVENT_QUEUE_CAPACITY];
uint8_t head;
uint8_t count;
/** Set under `ring_mutex` when the device is torn down while still subscribed (see
* esp32_ble_stop_device()). `ring_mutex` stays constructed - only the subscription's own
* bluetooth_event_unsubscribe() destructs it, since that can't race its own polling. */
bool closed;
/** True while `ring_mutex` is constructed. Read without locking `ring_mutex`: it's what
* decides whether locking it is safe. */
bool constructed;
struct BtEventSubscription* next;
} internal;
};
// ---- Top-level Bluetooth API ----
@@ -219,21 +259,30 @@ struct BluetoothApi {
error_t (*disconnect)(struct Device* device, const BtAddr addr, enum BtProfileId profile);
/**
* Add an event callback.
* Register a subscription for this device's BtEvents.
* @warning Does not work in ISR context.
* @param[in] device the bluetooth device
* @param[in] context context pointer passed 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
* bluetooth_event_poll().
* @retval ERROR_NONE on success
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered
* @retval ERROR_INVALID_STATE @a sub is already registered
*/
error_t (*add_event_callback)(struct Device* device, void* context, BtEventCallback callback);
error_t (*event_subscribe)(struct Device* device, struct BtEventSubscription* sub, struct TaskEventGroup* event_group);
/**
* Remove a previously added event callback.
* Remove a previously registered subscription.
* @warning Does not work in ISR context.
* @param[in] device the bluetooth device
* @param[in] callback the callback to remove
* @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, BtEventCallback callback);
error_t (*event_unsubscribe)(struct Device* device, struct BtEventSubscription* sub);
/**
* Set the BLE device name used in advertising and the GAP service.
@@ -264,7 +313,7 @@ struct BluetoothApi {
void (*set_hid_host_active)(struct Device* device, bool active);
/**
* Fire an event through all registered event callbacks.
* Fire an event to all registered subscriptions.
* Used by the Tactility HID host module to inject profile-state events that
* originate outside the platform driver (e.g. HID host connect/disconnect).
*/
@@ -295,13 +344,44 @@ error_t bluetooth_unpair(struct Device* device, const BtAddr addr);
error_t bluetooth_get_paired_peers(struct Device* device, struct BtPeerRecord* out, size_t* count);
error_t bluetooth_connect(struct Device* device, const BtAddr addr, enum BtProfileId profile);
error_t bluetooth_disconnect(struct Device* device, const BtAddr addr, enum BtProfileId profile);
error_t bluetooth_add_event_callback(struct Device* device, void* context, BtEventCallback callback);
error_t bluetooth_remove_event_callback(struct Device* device, BtEventCallback callback);
error_t bluetooth_set_device_name(struct Device* device, const char* name);
error_t bluetooth_get_device_name(struct Device* device, char* buf, size_t buf_len);
void bluetooth_set_hid_host_active(struct Device* device, bool active);
void bluetooth_fire_event(struct Device* device, struct BtEvent event);
/**
* Register a subscription for @a device's BtEvents.
* @warning Does not work in ISR context.
* @param[in] device the bluetooth 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
* bluetooth_event_poll().
* @retval ERROR_NONE on success
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered
* @retval ERROR_INVALID_STATE @a sub is already registered
*/
error_t bluetooth_event_subscribe(struct Device* device, struct BtEventSubscription* 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 bluetooth_event_unsubscribe(struct Device* device, struct BtEventSubscription* 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 bluetooth_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 bluetooth_event_poll(struct BtEventSubscription* sub, struct BtEvent* out_event);
#ifdef __cplusplus
}
#endif
@@ -116,13 +116,20 @@ struct WifiEventSubscription {
struct {
/** Caller-owned, borrowed; set by wifi_event_subscribe(). */
struct TaskEventGroup* event_group;
/** Guards `queue`/`head`/`count` between wifi_event_emit() (driver thread) and
/** Guards `queue`/`head`/`count`/`closed` 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;
/** Set under `ring_mutex` when the device is torn down while still subscribed (see
* esp32_wifi's stop_device()). `ring_mutex` stays constructed - only the subscription's
* own wifi_event_unsubscribe() destructs it, since that can't race its own polling. */
bool closed;
/** True while `ring_mutex` is constructed. Read without locking `ring_mutex`: it's what
* decides whether locking it is safe. */
bool constructed;
struct WifiEventSubscription* next;
} internal;
+46 -5
View File
@@ -66,14 +66,55 @@ error_t bluetooth_disconnect(struct Device* device, const BtAddr addr, enum BtPr
return BT_API(device)->disconnect(device, addr, profile);
}
// ---- Event callbacks ----
// ---- Event subscription ----
error_t bluetooth_add_event_callback(struct Device* device, void* context, BtEventCallback callback) {
return BT_API(device)->add_event_callback(device, context, callback);
error_t bluetooth_event_subscribe(struct Device* device, struct BtEventSubscription* sub, struct TaskEventGroup* event_group) {
mutex_construct(&sub->internal.ring_mutex);
sub->internal.head = 0;
sub->internal.count = 0;
sub->internal.closed = false;
sub->internal.constructed = true;
error_t result = BT_API(device)->event_subscribe(device, sub, event_group);
if (result != ERROR_NONE) {
sub->internal.constructed = false;
mutex_destruct(&sub->internal.ring_mutex);
}
return result;
}
error_t bluetooth_remove_event_callback(struct Device* device, BtEventCallback callback) {
return BT_API(device)->remove_event_callback(device, callback);
error_t bluetooth_event_unsubscribe(struct Device* device, struct BtEventSubscription* sub) {
error_t result = BT_API(device)->event_unsubscribe(device, sub);
// sub was never subscribed, or was already unsubscribed. ring_mutex was never constructed,
// unsafe to lock or destruct.
if (!sub->internal.constructed) {
return result;
}
mutex_lock(&sub->internal.ring_mutex);
bool was_closed = sub->internal.closed;
mutex_unlock(&sub->internal.ring_mutex);
// Destruct on success or force-close.
if (result == ERROR_NONE || was_closed) {
sub->internal.constructed = false;
mutex_destruct(&sub->internal.ring_mutex);
}
return result;
}
error_t bluetooth_event_poll(struct BtEventSubscription* sub, struct BtEvent* out_event) {
mutex_lock(&sub->internal.ring_mutex);
if (sub->internal.closed) {
mutex_unlock(&sub->internal.ring_mutex);
return ERROR_TIMEOUT;
}
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) % BT_EVENT_QUEUE_CAPACITY;
sub->internal.count--;
}
mutex_unlock(&sub->internal.ring_mutex);
return has_event ? ERROR_NONE : ERROR_TIMEOUT;
}
error_t bluetooth_set_device_name(struct Device* device, const char* name) {
+18 -1
View File
@@ -63,9 +63,12 @@ error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription
mutex_construct(&sub->internal.ring_mutex);
sub->internal.head = 0;
sub->internal.count = 0;
sub->internal.closed = false;
sub->internal.constructed = true;
error_t result = WIFI_API(device)->event_subscribe(device, sub, event_group);
if (result != ERROR_NONE) {
sub->internal.constructed = false;
mutex_destruct(&sub->internal.ring_mutex);
}
return result;
@@ -73,7 +76,17 @@ error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription
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) {
// sub was never subscribed, or was already unsubscribed. ring_mutex was never constructed,
// unsafe to lock or destruct.
if (!sub->internal.constructed) {
return result;
}
mutex_lock(&sub->internal.ring_mutex);
bool was_closed = sub->internal.closed;
mutex_unlock(&sub->internal.ring_mutex);
// Destruct on success or force-close.
if (result == ERROR_NONE || was_closed) {
sub->internal.constructed = false;
mutex_destruct(&sub->internal.ring_mutex);
}
return result;
@@ -81,6 +94,10 @@ error_t wifi_event_unsubscribe(struct Device* device, struct WifiEventSubscripti
error_t wifi_event_poll(struct WifiEventSubscription* sub, struct WifiEvent* out_event) {
mutex_lock(&sub->internal.ring_mutex);
if (sub->internal.closed) {
mutex_unlock(&sub->internal.ring_mutex);
return ERROR_TIMEOUT;
}
bool has_event = sub->internal.count > 0;
if (has_event) {
*out_event = sub->internal.queue[sub->internal.head];
+3 -2
View File
@@ -337,8 +337,9 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(bluetooth_get_paired_peers),
DEFINE_MODULE_SYMBOL(bluetooth_connect),
DEFINE_MODULE_SYMBOL(bluetooth_disconnect),
DEFINE_MODULE_SYMBOL(bluetooth_add_event_callback),
DEFINE_MODULE_SYMBOL(bluetooth_remove_event_callback),
DEFINE_MODULE_SYMBOL(bluetooth_event_subscribe),
DEFINE_MODULE_SYMBOL(bluetooth_event_unsubscribe),
DEFINE_MODULE_SYMBOL(bluetooth_event_poll),
DEFINE_MODULE_SYMBOL(bluetooth_set_device_name),
DEFINE_MODULE_SYMBOL(bluetooth_get_device_name),
DEFINE_MODULE_SYMBOL(bluetooth_set_hid_host_active),