circuitpython/ports/espressif/common-hal/_bleio/Adapter.c

668 lines
23 KiB
C

/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2018 Dan Halbert for Adafruit Industries
* Copyright (c) 2016 Glenn Ruben Bakke
* Copyright (c) 2018 Artur Pacholec
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "py/gc.h"
#include "py/objstr.h"
#include "py/runtime.h"
#include "supervisor/shared/bluetooth/bluetooth.h"
#include "supervisor/shared/safe_mode.h"
#include "supervisor/shared/tick.h"
#include "supervisor/usb.h"
#include "shared-bindings/_bleio/__init__.h"
#include "shared-bindings/_bleio/Adapter.h"
#include "shared-bindings/_bleio/Address.h"
#include "shared-bindings/nvm/ByteArray.h"
#include "shared-bindings/_bleio/Connection.h"
#include "shared-bindings/_bleio/ScanEntry.h"
#include "shared-bindings/time/__init__.h"
#include "controller/ble_ll_adv.h"
#include "nimble/hci_common.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_gap.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
#include "common-hal/_bleio/Connection.h"
#include "esp_bt.h"
#include "esp_nimble_hci.h"
bleio_connection_internal_t bleio_connections[BLEIO_TOTAL_CONNECTION_COUNT];
// static void bluetooth_adapter_background(void *data) {
// supervisor_bluetooth_background();
// bleio_background();
// }
bool ble_active = false;
static void nimble_host_task(void *param) {
nimble_port_run();
nimble_port_freertos_deinit();
}
static TaskHandle_t cp_task = NULL;
static void _on_sync(void) {
int rc = ble_hs_util_ensure_addr(0);
assert(rc == 0);
xTaskNotifyGive(cp_task);
}
void common_hal_bleio_adapter_set_enabled(bleio_adapter_obj_t *self, bool enabled) {
const bool is_enabled = common_hal_bleio_adapter_get_enabled(self);
// Don't enable or disable twice
if (is_enabled == enabled) {
return;
}
if (enabled) {
esp_nimble_hci_and_controller_init();
nimble_port_init();
// ble_hs_cfg.reset_cb = blecent_on_reset;
ble_hs_cfg.sync_cb = _on_sync;
// ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
ble_svc_gap_device_name_set("CIRCUITPY");
// Clear all of the internal connection objects.
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
bleio_connection_internal_t *connection = &bleio_connections[i];
// Reset connection.
connection->conn_handle = BLEIO_HANDLE_INVALID;
}
cp_task = xTaskGetCurrentTaskHandle();
nimble_port_freertos_init(nimble_host_task);
// Wait for sync.
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(200));
} else {
nimble_port_stop();
}
}
bool common_hal_bleio_adapter_get_enabled(bleio_adapter_obj_t *self) {
return xTaskGetHandle("ble") != NULL;
}
bleio_address_obj_t *common_hal_bleio_adapter_get_address(bleio_adapter_obj_t *self) {
uint8_t address_bytes[6];
uint8_t address_type = BLE_ADDR_RANDOM;
ble_hs_id_infer_auto(0, &address_type);
int result = ble_hs_id_copy_addr(address_type, address_bytes, NULL);
if (result != 0) {
return NULL;
}
bleio_address_obj_t *address = m_new_obj(bleio_address_obj_t);
address->base.type = &bleio_address_type;
common_hal_bleio_address_construct(address, address_bytes, BLEIO_ADDRESS_TYPE_RANDOM_STATIC);
return address;
}
bool common_hal_bleio_adapter_set_address(bleio_adapter_obj_t *self, bleio_address_obj_t *address) {
if (address->type != BLEIO_ADDRESS_TYPE_RANDOM_STATIC) {
return false;
}
mp_buffer_info_t bufinfo;
if (!mp_get_buffer(address->bytes, &bufinfo, MP_BUFFER_READ)) {
return false;
}
int result = ble_hs_id_set_rnd(bufinfo.buf);
return result == 0;
}
mp_obj_str_t *common_hal_bleio_adapter_get_name(bleio_adapter_obj_t *self) {
const char *name = ble_svc_gap_device_name();
return mp_obj_new_str(name, strlen(name));
}
void common_hal_bleio_adapter_set_name(bleio_adapter_obj_t *self, const char *name) {
ble_svc_gap_device_name_set(name);
}
static int _scan_event(struct ble_gap_event *event, void *scan_results_in) {
bleio_scanresults_obj_t *scan_results = (bleio_scanresults_obj_t *)scan_results_in;
if (event->type == BLE_GAP_EVENT_DISC_COMPLETE) {
shared_module_bleio_scanresults_set_done(scan_results, true);
return 0;
}
if (event->type != BLE_GAP_EVENT_DISC && event->type != BLE_GAP_EVENT_EXT_DISC) {
#if CIRCUITPY_VERBOSE_BLE
mp_printf(&mp_plat_print, "Unsupported scan event %d\n", event->type);
#endif
return 0;
}
if (event->type == BLE_GAP_EVENT_DISC) {
// Legacy advertisement
struct ble_gap_disc_desc *disc = &event->disc;
bool connectable = disc->event_type == BLE_HCI_ADV_RPT_EVTYPE_ADV_IND ||
disc->event_type == BLE_HCI_ADV_RPT_EVTYPE_DIR_IND;
shared_module_bleio_scanresults_append(scan_results,
supervisor_ticks_ms64(),
connectable,
disc->event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP,
disc->rssi,
disc->addr.val,
disc->addr.type,
disc->data,
disc->length_data);
} else {
// Extended advertisement
struct ble_gap_ext_disc_desc *disc = &event->ext_disc;
shared_module_bleio_scanresults_append(scan_results,
supervisor_ticks_ms64(),
(disc->props & BLE_HCI_ADV_CONN_MASK) != 0,
(disc->props & BLE_HCI_ADV_SCAN_MASK) != 0,
disc->rssi,
disc->addr.val,
disc->addr.type,
disc->data,
disc->length_data);
}
return 0;
}
mp_obj_t common_hal_bleio_adapter_start_scan(bleio_adapter_obj_t *self, uint8_t *prefixes,
size_t prefix_length, bool extended, mp_int_t buffer_size, mp_float_t timeout,
mp_float_t interval, mp_float_t window, mp_int_t minimum_rssi, bool active) {
if (self->scan_results != NULL) {
if (!shared_module_bleio_scanresults_get_done(self->scan_results)) {
mp_raise_bleio_BluetoothError(translate("Scan already in progess. Stop with stop_scan."));
}
self->scan_results = NULL;
}
self->scan_results = shared_module_bleio_new_scanresults(buffer_size, prefixes, prefix_length, minimum_rssi);
// size_t max_packet_size = extended ? BLE_HCI_MAX_EXT_ADV_DATA_LEN : BLE_HCI_MAX_ADV_DATA_LEN;
uint8_t own_addr_type;
struct ble_gap_disc_params disc_params;
/* Figure out address to use while advertising (no privacy for now) */
int privacy = 0;
CHECK_NIMBLE_ERROR(ble_hs_id_infer_auto(privacy, &own_addr_type));
disc_params.filter_duplicates = 0;
disc_params.passive = !active;
disc_params.itvl = interval / 0.625;
disc_params.window = window / 0.625;
disc_params.filter_policy = 0;
disc_params.limited = 0;
size_t duration_ms = timeout * 1000;
if (duration_ms == 0) {
duration_ms = BLE_HS_FOREVER;
}
CHECK_NIMBLE_ERROR(ble_gap_disc(own_addr_type, duration_ms, &disc_params,
_scan_event, self->scan_results));
return MP_OBJ_FROM_PTR(self->scan_results);
}
void common_hal_bleio_adapter_stop_scan(bleio_adapter_obj_t *self) {
if (self->scan_results == NULL) {
return;
}
ble_gap_disc_cancel();
shared_module_bleio_scanresults_set_done(self->scan_results, true);
self->scan_results = NULL;
}
STATIC void _convert_address(const bleio_address_obj_t *address, ble_addr_t *nimble_address) {
nimble_address->type = address->type;
mp_buffer_info_t address_buf_info;
mp_get_buffer_raise(address->bytes, &address_buf_info, MP_BUFFER_READ);
memcpy(nimble_address->val, (uint8_t *)address_buf_info.buf, NUM_BLEIO_ADDRESS_BYTES);
}
STATIC int _mtu_reply(uint16_t conn_handle,
const struct ble_gatt_error *error,
uint16_t mtu, void *arg) {
bleio_connection_internal_t *connection = (bleio_connection_internal_t *)arg;
if (conn_handle != connection->conn_handle || error->status != 0) {
return 0;
}
connection->mtu = mtu;
return 0;
}
STATIC void _new_connection(uint16_t conn_handle) {
// Set the tx_power for the connection higher than the advertisement.
esp_ble_tx_power_set(conn_handle, ESP_PWR_LVL_N0);
// Find an empty connection. One must always be available because the SD has the same
// total connection limit.
bleio_connection_internal_t *connection;
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
connection = &bleio_connections[i];
if (connection->conn_handle == BLEIO_HANDLE_INVALID) {
break;
}
}
connection->conn_handle = conn_handle;
connection->connection_obj = mp_const_none;
connection->pair_status = PAIR_NOT_PAIRED;
connection->mtu = 0;
ble_gattc_exchange_mtu(conn_handle, _mtu_reply, connection);
// Change the callback for the connection.
ble_gap_set_event_cb(conn_handle, bleio_connection_event_cb, connection);
}
static int _connect_event(struct ble_gap_event *event, void *self_in) {
bleio_adapter_obj_t *self = (bleio_adapter_obj_t *)self_in;
#if CIRCUITPY_VERBOSE_BLE
mp_printf(&mp_plat_print, "Connect event: %d\n", event->type);
#endif
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
_new_connection(event->connect.conn_handle);
// Set connections objs back to NULL since we have a new
// connection and need a new tuple.
self->connection_objs = NULL;
xTaskNotify(cp_task, event->connect.conn_handle, eSetValueWithOverwrite);
} else {
xTaskNotify(cp_task, -event->connect.status, eSetValueWithOverwrite);
}
break;
default:
#if CIRCUITPY_VERBOSE_BLE
// For debugging.
mp_printf(&mp_plat_print, "Unhandled connect event: %d\n", event->type);
#endif
break;
}
return 0;
}
mp_obj_t common_hal_bleio_adapter_connect(bleio_adapter_obj_t *self, bleio_address_obj_t *address, mp_float_t timeout) {
// Stop any active scan.
if (self->scan_results != NULL) {
common_hal_bleio_adapter_stop_scan(self);
}
struct ble_gap_conn_params conn_params = {
.scan_itvl = MSEC_TO_UNITS(100, UNIT_0_625_MS),
.scan_window = MSEC_TO_UNITS(100, UNIT_0_625_MS),
.itvl_min = MSEC_TO_UNITS(15, UNIT_1_25_MS),
.itvl_max = MSEC_TO_UNITS(300, UNIT_1_25_MS),
.latency = 0,
.supervision_timeout = MSEC_TO_UNITS(4000, UNIT_10_MS),
.min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN,
.max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN
};
uint8_t own_addr_type;
// TODO: Use a resolvable address if the peer has our key.
CHECK_NIMBLE_ERROR(ble_hs_id_infer_auto(false, &own_addr_type));
ble_addr_t addr;
_convert_address(address, &addr);
cp_task = xTaskGetCurrentTaskHandle();
// Make sure we don't have a pending notification from a previous time. This
// can happen if a previous wait timed out before the notification was given.
xTaskNotifyStateClear(cp_task);
CHECK_NIMBLE_ERROR(
ble_gap_connect(own_addr_type, &addr,
SEC_TO_UNITS(timeout, UNIT_1_MS) + 0.5f,
&conn_params,
_connect_event, self));
int error_code;
CHECK_NOTIFY(xTaskNotifyWait(0, 0, (uint32_t *)&error_code, 200));
// Negative values are error codes, connection handle otherwise.
if (error_code < 0) {
CHECK_BLE_ERROR(-error_code);
}
uint16_t conn_handle = error_code;
// TODO: If we have keys, then try and encrypt the connection.
// TODO: Negotiate for better PHY and data lengths since we are the central. These are
// nice-to-haves so ignore any errors.
// Make the connection object and return it.
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
bleio_connection_internal_t *connection = &bleio_connections[i];
if (connection->conn_handle == conn_handle) {
connection->is_central = true;
return bleio_connection_new_from_internal(connection);
}
}
mp_raise_bleio_BluetoothError(translate("Failed to connect: internal error"));
return mp_const_none;
}
typedef struct {
struct os_mbuf mbuf;
struct os_mbuf_pkthdr hdr;
} os_mbuf_t;
STATIC void _wrap_in_mbuf(const uint8_t *data, uint16_t len, os_mbuf_t *buf) {
struct os_mbuf *mbuf = &buf->mbuf;
mbuf->om_data = (uint8_t *)data,
mbuf->om_flags = 0;
mbuf->om_pkthdr_len = 0;
mbuf->om_len = len;
mbuf->om_next.sle_next = NULL;
buf->hdr.omp_len = len;
// Setting the pool to NULL will cause frees to fail. Hopefully that failure
// is ignored.
mbuf->om_omp = NULL;
}
static int _advertising_event(struct ble_gap_event *event, void *self_in) {
bleio_adapter_obj_t *self = (bleio_adapter_obj_t *)self_in;
#if CIRCUITPY_VERBOSE_BLE
mp_printf(&mp_plat_print, "Advertising event: %d\n", event->type);
#endif
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
// Spurious connect events can happen.
break;
case BLE_GAP_EVENT_ADV_COMPLETE:
if (event->adv_complete.reason == NIMBLE_OK) {
_new_connection(event->adv_complete.conn_handle);
// Set connections objs back to NULL since we have a new
// connection and need a new tuple.
self->connection_objs = NULL;
}
// Other statuses indicate timeout or preemption.
common_hal_bleio_adapter_stop_advertising(self);
break;
default:
#if CIRCUITPY_VERBOSE_BLE
// For debugging.
mp_printf(&mp_plat_print, "Unhandled advertising event: %d\n", event->type);
#endif
break;
}
return 0;
}
uint32_t _common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self,
bool connectable, bool anonymous, uint32_t timeout, float interval,
const uint8_t *advertising_data, uint16_t advertising_data_len,
const uint8_t *scan_response_data, uint16_t scan_response_data_len,
mp_int_t tx_power, const bleio_address_obj_t *directed_to) {
if (ble_gap_adv_active() && !self->user_advertising) {
return BLE_HS_EBUSY;
}
uint32_t rc;
bool extended = advertising_data_len > BLE_ADV_LEGACY_DATA_MAX_LEN ||
scan_response_data_len > BLE_ADV_LEGACY_DATA_MAX_LEN;
ble_addr_t peer;
if (directed_to != NULL) {
_convert_address(directed_to, &peer);
}
uint8_t own_addr_type;
// Anonymous addresses are still resolvable. (Following what the NRF
// implementation does.)
rc = ble_hs_id_infer_auto(anonymous, &own_addr_type);
if (rc != NIMBLE_OK) {
return rc;
}
bool high_duty_directed = directed_to != NULL && interval <= 3.5 && timeout <= 1.3;
struct ble_gap_ext_adv_params adv_params = {
.connectable = connectable,
.scannable = scan_response_data_len > 0,
.directed = directed_to != NULL,
.high_duty_directed = high_duty_directed,
.legacy_pdu = !extended,
.anonymous = anonymous,
.include_tx_power = extended,
.scan_req_notif = false,
.itvl_min = SEC_TO_UNITS(interval, UNIT_0_625_MS) + 0.5f,
.itvl_max = SEC_TO_UNITS(interval, UNIT_0_625_MS) + 0.5f,
.channel_map = 0,
.own_addr_type = own_addr_type,
.peer = peer,
.filter_policy = BLE_HCI_CONN_FILT_NO_WL,
.primary_phy = BLE_HCI_LE_PHY_1M,
.secondary_phy = BLE_HCI_LE_PHY_1M,
.tx_power = tx_power,
.sid = 0,
};
// Configure must come before setting payloads.
rc = ble_gap_ext_adv_configure(0,
&adv_params,
NULL,
_advertising_event, self);
if (rc != NIMBLE_OK) {
return rc;
}
os_mbuf_t buf;
_wrap_in_mbuf(advertising_data, advertising_data_len, &buf);
// This copies the advertising data into buffers to send to the controller.
// Therefore, we don't need to worry about the lifetime of our copy.
rc = ble_gap_ext_adv_set_data(0, &buf.mbuf);
if (rc != NIMBLE_OK) {
return rc;
}
if (scan_response_data_len > 0) {
_wrap_in_mbuf(scan_response_data, scan_response_data_len, &buf);
// This copies the advertising data into buffers to send to the controller.
// Therefore, we don't need to worry about the lifetime of our copy.
rc = ble_gap_ext_adv_rsp_set_data(0, &buf.mbuf);
if (rc != NIMBLE_OK) {
return rc;
}
}
rc = ble_gap_ext_adv_start(0, timeout / 10, 0);
if (rc != NIMBLE_OK) {
return rc;
}
return NIMBLE_OK;
}
STATIC void check_data_fit(size_t data_len, bool connectable) {
if (data_len > MYNEWT_VAL(BLE_EXT_ADV_MAX_SIZE) ||
(connectable && data_len > MYNEWT_VAL(BLE_EXT_ADV_MAX_SIZE))) {
mp_raise_ValueError(translate("Data too large for advertisement packet"));
}
}
void common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, bool connectable,
bool anonymous, uint32_t timeout, mp_float_t interval,
mp_buffer_info_t *advertising_data_bufinfo, mp_buffer_info_t *scan_response_data_bufinfo,
mp_int_t tx_power, const bleio_address_obj_t *directed_to) {
if (self->user_advertising) {
mp_raise_bleio_BluetoothError(translate("Already advertising."));
} else {
// If the user isn't advertising, then the background is. So, stop the
// background advertising so the user can.
common_hal_bleio_adapter_stop_advertising(self);
}
// interval value has already been validated.
check_data_fit(advertising_data_bufinfo->len, connectable);
check_data_fit(scan_response_data_bufinfo->len, connectable);
if (advertising_data_bufinfo->len > 31 && scan_response_data_bufinfo->len > 0) {
mp_raise_bleio_BluetoothError(translate("Extended advertisements with scan response not supported."));
}
if (advertising_data_bufinfo->len > 0 && directed_to != NULL) {
mp_raise_bleio_BluetoothError(translate("Data not supported with directed advertising"));
}
if (anonymous) {
mp_raise_NotImplementedError(NULL);
}
if (!timeout) {
timeout = BLE_HS_FOREVER;
} else if (timeout > INT32_MAX) {
mp_raise_bleio_BluetoothError(translate("Timeout is too long: Maximum timeout length is %d seconds"),
INT32_MAX / 1000);
}
CHECK_NIMBLE_ERROR(_common_hal_bleio_adapter_start_advertising(self, connectable, anonymous, timeout, interval,
advertising_data_bufinfo->buf,
advertising_data_bufinfo->len,
scan_response_data_bufinfo->buf,
scan_response_data_bufinfo->len,
tx_power,
directed_to));
self->user_advertising = true;
}
void common_hal_bleio_adapter_stop_advertising(bleio_adapter_obj_t *self) {
int err_code = ble_gap_ext_adv_stop(0);
self->user_advertising = false;
if ((err_code != NIMBLE_OK) &&
(err_code != BLE_HS_EALREADY) &&
(err_code != BLE_HS_EINVAL)) {
CHECK_NIMBLE_ERROR(err_code);
}
}
bool common_hal_bleio_adapter_get_advertising(bleio_adapter_obj_t *self) {
return ble_gap_adv_active();
}
bool common_hal_bleio_adapter_get_connected(bleio_adapter_obj_t *self) {
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
bleio_connection_internal_t *connection = &bleio_connections[i];
if (connection->conn_handle != BLEIO_HANDLE_INVALID) {
return true;
}
}
return false;
}
mp_obj_t common_hal_bleio_adapter_get_connections(bleio_adapter_obj_t *self) {
if (self->connection_objs != NULL) {
return self->connection_objs;
}
size_t total_connected = 0;
mp_obj_t items[BLEIO_TOTAL_CONNECTION_COUNT];
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
bleio_connection_internal_t *connection = &bleio_connections[i];
if (connection->conn_handle != BLEIO_HANDLE_INVALID) {
if (connection->connection_obj == mp_const_none) {
connection->connection_obj = bleio_connection_new_from_internal(connection);
}
items[total_connected] = connection->connection_obj;
total_connected++;
}
}
self->connection_objs = mp_obj_new_tuple(total_connected, items);
return self->connection_objs;
}
void common_hal_bleio_adapter_erase_bonding(bleio_adapter_obj_t *self) {
mp_raise_NotImplementedError(NULL);
// bonding_erase_storage();
}
bool common_hal_bleio_adapter_is_bonded_to_central(bleio_adapter_obj_t *self) {
mp_raise_NotImplementedError(NULL);
// return bonding_peripheral_bond_count() > 0;
return false;
}
void bleio_adapter_gc_collect(bleio_adapter_obj_t *adapter) {
// We divide by size_t so that we can scan each 32-bit aligned value to see
// if it is a pointer. This allows us to change the structs without worrying
// about collecting new pointers.
gc_collect_root((void **)adapter, sizeof(bleio_adapter_obj_t) / (sizeof(size_t)));
gc_collect_root((void **)bleio_connections, sizeof(bleio_connections) / (sizeof(size_t)));
}
void bleio_adapter_reset(bleio_adapter_obj_t *adapter) {
common_hal_bleio_adapter_stop_scan(adapter);
if (common_hal_bleio_adapter_get_advertising(adapter)) {
common_hal_bleio_adapter_stop_advertising(adapter);
}
adapter->connection_objs = NULL;
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
bleio_connection_internal_t *connection = &bleio_connections[i];
// Disconnect all connections cleanly.
if (connection->conn_handle != BLEIO_HANDLE_INVALID) {
common_hal_bleio_connection_disconnect(connection);
}
connection->connection_obj = mp_const_none;
}
// Wait up to 125 ms (128 ticks) for disconnect to complete. This should be
// greater than most connection intervals.
bool any_connected = false;
uint64_t start_ticks = supervisor_ticks_ms64();
while (any_connected && supervisor_ticks_ms64() - start_ticks < 128) {
any_connected = false;
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
bleio_connection_internal_t *connection = &bleio_connections[i];
any_connected |= connection->conn_handle != BLEIO_HANDLE_INVALID;
}
}
}