Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 50 additions & 11 deletions Drivers/mock-drivers-module/source/mock_bluetooth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ struct MockBluetoothCtx {
BtRadioState radioState = BT_RADIO_STATE_OFF;
bool scanning = false;
char deviceName[BLE_DEVICE_NAME_MAX + 1] = {};
/** Empty means "use deviceName". */
char identity[BLE_IDENTITY_MAX + 1] = {};
MockPeerState peers[MOCK_PEER_COUNT];

Mutex subscriptionsMutex {};
Expand Down Expand Up @@ -205,6 +207,14 @@ bool apiIsScanning(Device* device) {
return scanning;
}

bool apiIsPeripheralActive(Device* device) {
auto* ctx = GET_CTX(device);
if (ctx == nullptr) return false;
// The mock has no peripheral profiles, so nothing is ever serving.
(void)ctx;
return false;
}

error_t apiPair(Device* device, const BtAddr addr) {
auto* ctx = GET_CTX(device);
if (ctx == nullptr || addr == nullptr) return ERROR_INVALID_ARGUMENT;
Expand Down Expand Up @@ -267,7 +277,7 @@ error_t apiGetPairedPeers(Device* device, BtPeerRecord* out, size_t* count) {
return ERROR_NONE;
}

error_t apiConnect(Device* device, const BtAddr addr, BtProfileId profile) {
error_t apiHostConnect(Device* device, const BtAddr addr) {
auto* ctx = GET_CTX(device);
if (ctx == nullptr || addr == nullptr) return ERROR_INVALID_ARGUMENT;

Expand All @@ -282,31 +292,31 @@ error_t apiConnect(Device* device, const BtAddr addr, BtProfileId profile) {
return ERROR_NOT_FOUND;
}
ctx->peers[index].paired = true;
ctx->peers[index].connectedProfiles |= 1 << profile;
ctx->peers[index].connectedProfiles |= 1 << BT_PROFILE_HID_HOST;
mutex_unlock(&ctx->mutex);

// No real peer to negotiate with, so the mock always succeeds instantly.
fireProfileState(ctx, index, profile, BT_PROFILE_STATE_CONNECTING);
fireProfileState(ctx, index, profile, BT_PROFILE_STATE_CONNECTED);
fireProfileState(ctx, index, BT_PROFILE_HID_HOST, BT_PROFILE_STATE_CONNECTING);
fireProfileState(ctx, index, BT_PROFILE_HID_HOST, BT_PROFILE_STATE_CONNECTED);
fireConnectState(ctx, index);
return ERROR_NONE;
}

error_t apiDisconnect(Device* device, const BtAddr addr, BtProfileId profile) {
error_t apiHostDisconnect(Device* device, const BtAddr addr) {
auto* ctx = GET_CTX(device);
if (ctx == nullptr || addr == nullptr) return ERROR_INVALID_ARGUMENT;

const size_t index = findPeer(addr);
if (index == MOCK_PEER_COUNT) return ERROR_NOT_FOUND;

mutex_lock(&ctx->mutex);
const bool was_connected = (ctx->peers[index].connectedProfiles & (1 << profile)) != 0;
ctx->peers[index].connectedProfiles &= ~(1 << profile);
const bool was_connected = (ctx->peers[index].connectedProfiles & (1 << BT_PROFILE_HID_HOST)) != 0;
ctx->peers[index].connectedProfiles &= ~(1 << BT_PROFILE_HID_HOST);
mutex_unlock(&ctx->mutex);
if (!was_connected) return ERROR_NONE;

fireProfileState(ctx, index, profile, BT_PROFILE_STATE_DISCONNECTING);
fireProfileState(ctx, index, profile, BT_PROFILE_STATE_IDLE);
fireProfileState(ctx, index, BT_PROFILE_HID_HOST, BT_PROFILE_STATE_DISCONNECTING);
fireProfileState(ctx, index, BT_PROFILE_HID_HOST, BT_PROFILE_STATE_IDLE);
fireConnectState(ctx, index);
return ERROR_NONE;
}
Expand Down Expand Up @@ -381,21 +391,50 @@ error_t apiGetDeviceName(Device* device, char* buf, size_t buf_len) {
return ERROR_NONE;
}

error_t apiSetIdentity(Device* device, const char* identity) {
auto* ctx = GET_CTX(device);
if (ctx == nullptr) return ERROR_INVALID_ARGUMENT;
if (identity != nullptr && strlen(identity) > BLE_IDENTITY_MAX) return ERROR_INVALID_ARGUMENT;
mutex_lock(&ctx->mutex);
if (identity == nullptr || identity[0] == '\0') {
ctx->identity[0] = '\0';
} else {
strncpy(ctx->identity, identity, BLE_IDENTITY_MAX);
ctx->identity[BLE_IDENTITY_MAX] = '\0';
}
mutex_unlock(&ctx->mutex);
return ERROR_NONE;
}

error_t apiGetIdentity(Device* device, char* buf, size_t buf_len) {
auto* ctx = GET_CTX(device);
if (ctx == nullptr || buf == nullptr || buf_len == 0) return ERROR_INVALID_ARGUMENT;
mutex_lock(&ctx->mutex);
const char* identity = (ctx->identity[0] != '\0') ? ctx->identity : ctx->deviceName;
strncpy(buf, identity, buf_len - 1);
buf[buf_len - 1] = '\0';
mutex_unlock(&ctx->mutex);
return ERROR_NONE;
}

const BluetoothApi mock_bluetooth_api = {
.get_radio_state = apiGetRadioState,
.set_radio_enabled = apiSetRadioEnabled,
.scan_start = apiScanStart,
.scan_stop = apiScanStop,
.is_scanning = apiIsScanning,
.is_peripheral_active = apiIsPeripheralActive,
.pair = apiPair,
.unpair = apiUnpair,
.get_paired_peers = apiGetPairedPeers,
.connect = apiConnect,
.disconnect = apiDisconnect,
.host_connect = apiHostConnect,
.host_disconnect = apiHostDisconnect,
.event_subscribe = apiEventSubscribe,
.event_unsubscribe = apiEventUnsubscribe,
.set_device_name = apiSetDeviceName,
.get_device_name = apiGetDeviceName,
.set_identity = apiSetIdentity,
.get_identity = apiGetIdentity,
};

// ---- Driver lifecycle ----
Expand Down
49 changes: 34 additions & 15 deletions Modules/bluetooth-module/source/bluetooth_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,11 @@ static void auto_start_profiles(BluetoothService* data) {
if (query.hid_host) {
LOG_I(TAG, "HID host auto-connect peer found, starting scan");
bluetooth_scan_start(data->device);
} else if (query.hid_device) {
LOG_I(TAG, "HID device auto-start (bonded peer found)");
if (Device* hid_device = bluetooth_hid_device_get()) {
bluetooth_hid_device_start(hid_device, BT_HID_DEVICE_MODE_KEYBOARD);
device_put(hid_device);
}
} else {
// HID device profiles are not auto-started. The identity, address and bond set belong
// to the app that starts the profile, so a service start would expose the current
// identity as a keyboard with no app behind it. A paired host reconnects once an app
// starts the profile.
if (bluetooth_settings_get_spp_auto_start()) {
LOG_I(TAG, "Auto-starting SPP server");
if (Device* serial = bluetooth_serial_get()) {
Expand All @@ -182,6 +180,12 @@ static void auto_start_profiles(BluetoothService* data) {
// Connects to the first saved HID host peer that appeared in the last scan.
// When none appeared, scans again so the search continues until the device powers on.
static void auto_connect_hid_host(BluetoothService* data) {
// A peripheral profile owns the radio and its identity, and bonds are stored per identity,
// so a host connect here would authenticate against the wrong bond set.
if (bluetooth_is_peripheral_active(data->device)) {
return;
}

mutex_lock(&data->mutex);
bool connected = data->hid_host_connected;
tt::std::vector<BluetoothPeer> scanned = data->scan_results;
Expand All @@ -196,7 +200,7 @@ static void auto_connect_hid_host(BluetoothService* data) {
char addr_hex[BLUETOOTH_ADDR_HEX_LENGTH + 1];
bluetooth_addr_to_hex(peer.addr, addr_hex);
LOG_I(TAG, "Auto-connecting HID host to %s", addr_hex);
bluetooth_connect(data->device, peer.addr, BT_PROFILE_HID_HOST);
bluetooth_host_connect(data->device, peer.addr);
return;
}
}
Expand Down Expand Up @@ -521,7 +525,7 @@ void bluetooth_service_connect(const BtAddr addr, enum BtProfileId profile) {
LOG_I(TAG, "connect(profile=%d)", static_cast<int>(profile));
switch (profile) {
case BT_PROFILE_HID_HOST:
bluetooth_connect(service->device, addr, profile);
bluetooth_host_connect(service->device, addr);
break;
case BT_PROFILE_HID_DEVICE:
if (Device* hid_device = bluetooth_hid_device_get()) {
Expand Down Expand Up @@ -551,13 +555,28 @@ void bluetooth_service_disconnect(const BtAddr addr, enum BtProfileId profile) {
return;
}
LOG_I(TAG, "disconnect(profile=%d)", static_cast<int>(profile));
if (profile == BT_PROFILE_HID_DEVICE) {
if (Device* hid_device = bluetooth_hid_device_get()) {
bluetooth_hid_device_stop(hid_device);
device_put(hid_device);
}
} else {
bluetooth_disconnect(service->device, addr, profile);
switch (profile) {
case BT_PROFILE_HID_HOST:
bluetooth_host_disconnect(service->device, addr);
break;
case BT_PROFILE_HID_DEVICE:
if (Device* hid_device = bluetooth_hid_device_get()) {
bluetooth_hid_device_stop(hid_device);
device_put(hid_device);
}
break;
case BT_PROFILE_SPP:
if (Device* serial = bluetooth_serial_get()) {
bluetooth_serial_stop(serial);
device_put(serial);
}
break;
case BT_PROFILE_MIDI:
if (Device* midi = bluetooth_midi_get()) {
bluetooth_midi_stop(midi);
device_put(midi);
}
break;
}
}

Expand Down
73 changes: 72 additions & 1 deletion Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ struct BleCtx {
std::atomic<bool> midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows)
std::atomic<bool> hid_active;
std::atomic<bool> link_encrypted;
// Established links in either role, maintained from the GAP connect and disconnect events.
// Tells an idle radio from one in use before swapping the bond set.
std::atomic<int> conn_count;
// Blocks the background scan's name resolution from starting another central connection.
std::atomic<bool> name_res_abort;
// Link opened by name resolution to read a peer's GAP name, NONE when no read is in flight.
std::atomic<uint16_t> name_res_conn_handle;
std::atomic<int> pending_reset_count;

// Timers
Expand Down Expand Up @@ -93,6 +100,19 @@ struct BleCtx {
// BLE device name (set before or after radio enable; applied in dispatch_enable)
char device_name[BLE_DEVICE_NAME_MAX + 1];

// Explicit identity (bluetooth_set_identity()). Empty means "use device_name".
char identity[BLE_IDENTITY_MAX + 1];
// Identity string that was resolved when the radio came up. A later rename does not change
// it, so this (not the possibly-newer device_name) is what bluetooth_get_identity() reports
// while the radio is on.
char identity_resolved[BLE_IDENTITY_MAX + 1];
// Resolved at radio enable: hash of identity (or device_name). Selects the bond store
// slot and is folded into the advertising address. 0 while the radio has never been on.
uint32_t identity_hash;
// Address type used for advertising: BLE_OWN_ADDR_RANDOM once the identity address was
// installed in on_sync(), BLE_OWN_ADDR_PUBLIC if that failed (or never ran).
std::atomic<uint8_t> own_addr_type;

// Scan data (guarded by scan_mutex), allocated only while the radio is on
SemaphoreHandle_t scan_mutex;
BtPeerRecord* scan_results;
Expand Down Expand Up @@ -123,14 +143,65 @@ void ble_set_scan_active(struct Device* device, bool v);
// ---- Scan data management (defined in esp32_ble_scan.cpp) ----
void ble_scan_clear_results(struct Device* device);

/**
* Stop the background scan and its name-resolution chain and cancel a connection attempt in
* flight. A profile start needs an idle GAP, and name resolution keeps opening short-lived
* central connections. Does not wait for the cancel, see ble_prepare_profile_start().
*/
void ble_scan_abort(struct Device* device);

/**
* Prepare the radio for a peripheral profile start. Aborts the background scan and name
* resolution, then waits off the host task until the GAP is idle.
* @return false when the GAP is still busy after the timeout, the caller may still try.
*/
bool ble_prepare_profile_start(struct Device* device);

// ---- Event publishing ----
void ble_publish_event(struct Device* device, struct BtEvent event);

// ---- Advertising helpers (defined in esp32_ble.cpp) ----
void ble_start_advertising(struct Device* device, const ble_uuid128_t* svc_uuid); // svc_uuid=nullptr → name-only
void ble_start_advertising(struct Device* device, const ble_uuid128_t* svc_uuid);
void ble_start_advertising_hid(struct Device* device, uint16_t appearance);
void ble_schedule_adv_restart(struct Device* device, uint64_t delay_us);

// ---- Per-identity bond store + advertising address (defined in esp32_ble_identity.cpp) ----

/** FNV-1a hash of the identity string, never returns 0 or OWNER_TRANSITION. */
uint32_t ble_identity_hash(const char* identity);

/**
* Derive a static random BLE address from the controller's public address and the identity
* hash. The same controller and identity always yield the same address, different identities
* yield different addresses.
*/
bool ble_identity_derive_addr(uint32_t identity_hash, const uint8_t base[BT_ADDR_LEN], uint8_t out[BT_ADDR_LEN]);

/**
* Make @a identity_hash's bonds the active set. NimBLE keys bond records by peer address only,
* so identities sharing one namespace would collide on the same peer. This swaps whole sets
* between "nimble_bond" and one "btid_<hash>" archive namespace per identity, crash-safely.
*
* Touches NVS only. dispatch_enable() calls it with the host down,
* ble_identity_apply_pending() on an idle running host.
*
* @return false when the swap could not be committed, the previous set stays active.
*/
bool ble_identity_bond_slot_activate(uint32_t identity_hash);

/**
* Apply the pending identity to a radio that is already on, so a profile start does not
* advertise under another app's identity. Called by the peripheral profile start functions
* before they take gap_mutex.
*
* The bond set and the identity address only change together and only while the host is idle.
* A busy radio keeps its identity for the session.
*
* @return true when the active identity afterwards is the pending one. Also true when there is
* nothing to do or when the radio is off and dispatch_enable() will resolve it.
*/
bool ble_identity_apply_pending(struct Device* device);

// ---- GAP scan callback (defined in esp32_ble_scan.cpp) ----
int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg);
void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx);
Expand Down
Loading
Loading