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;