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