Skip to content
Open
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
103 changes: 60 additions & 43 deletions ports/zephyr-cp/common-hal/_bleio/Adapter.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ static bool ble_advertising = false;
// than user code. Lets the workflow restart its own adverts without disturbing
// user-initiated advertising.
static bool ble_advertising_internal = false;
static bool ble_adapter_enabled = true;
// Set to true when common_hal_bleio_adapter_set_enabled(true) brings the stack up.
// set_enabled(false) clears this flag and stops advertising and scanning, but
// the controller keeps running.
static bool ble_adapter_enabled = false;

#define BLEIO_ADV_MAX_FIELDS 16
#define BLEIO_ADV_MAX_DATA_LEN 31
Expand Down Expand Up @@ -149,6 +152,7 @@ static void bleio_connection_clear(bleio_connection_internal_t *self) {
self->connection_obj = mp_const_none;
self->pair_status = PAIR_NOT_PAIRED;
self->sec_err = 0;
self->user_owned = false;
}

static void bleio_connection_release(bleio_connection_internal_t *connection, uint8_t reason) {
Expand Down Expand Up @@ -205,6 +209,11 @@ static void bleio_connected_cb(struct bt_conn *conn, uint8_t err) {
int mtu_err = bt_gatt_exchange_mtu(conn, &mtu_exchange_params[idx]);
(void)mtu_err;

// A peripheral connection belongs to whoever started the advertising it
// answered: the BLE workflow (internal) or user code. Central connections
// are marked in common_hal_bleio_adapter_connect().
connection->user_owned = !ble_advertising_internal;

// When connectable advertising results in a connection, the controller
// auto-stops advertising. Clear our flag to match (we cannot call
// stop_advertising() here because this callback runs in Zephyr's BT
Expand Down Expand Up @@ -330,6 +339,32 @@ static uint16_t bleio_validate_and_convert_timeout(mp_float_t timeout) {
return (uint16_t)timeout_units;
}

// Start the Zephyr Bluetooth host and load its settings (identity, bond keys,
// CCC state) if that hasn't happened yet. Returns 0 or a negative Zephyr errno.
//
// This is separate from set_enabled() because the BLE workflow asks about
// bonds (is_bonded_to_central(), erase_bonding()) in supervisor_bluetooth_init(),
// before it enables the adapter in supervisor_start_bluetooth(). Bond keys live
// in the settings subsystem and are only in RAM after settings_load(), so those
// calls must be able to bring the stack up on their own. Nothing here powers the
// controller down later: set_enabled(false) leaves it running.
static int bleio_adapter_ensure_stack_ready(void) {
if (bt_is_ready()) {
return 0;
}
int err = bt_enable(NULL);
if (err != 0 && err != -EALREADY) {
return err;
}

// bt_init() returns early without setting BT_DEV_READY when
// CONFIG_BT_SETTINGS=y and no identity is loaded yet.
// Load settings so the BT settings handler fires and calls
// bt_finalize_init() which sets BT_DEV_READY.
settings_load();
return 0;
}

void common_hal_bleio_adapter_set_enabled(bleio_adapter_obj_t *self, bool enabled) {
if (enabled == ble_adapter_enabled) {
return;
Expand All @@ -338,17 +373,9 @@ void common_hal_bleio_adapter_set_enabled(bleio_adapter_obj_t *self, bool enable
for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
bleio_connection_clear(&bleio_connections[i]);
}
if (!bt_is_ready()) {
int err = bt_enable(NULL);
if (err != 0 && err != -EALREADY) {
raise_zephyr_error(err);
}

// bt_init() returns early without setting BT_DEV_READY when
// CONFIG_BT_SETTINGS=y and no identity is loaded yet.
// Load settings so the BT settings handler fires and calls
// bt_finalize_init() which sets BT_DEV_READY.
settings_load();
int err = bleio_adapter_ensure_stack_ready();
if (err != 0) {
raise_zephyr_error(err);
}
// Ensure a local identity exists so advertising/connections work and the
// name is stable across reboots. bt_id_create persists the identity when
Expand Down Expand Up @@ -815,6 +842,9 @@ mp_obj_t common_hal_bleio_adapter_connect(bleio_adapter_obj_t *self, bleio_addre
// ref via bleio_connection_track(). Drop the create ref now.
bt_conn_unref(conn);

// Only user code connects in the central role.
connection->user_owned = true;

self->connection_objs = NULL;
return bleio_connection_new_from_internal(connection);
}
Expand All @@ -840,6 +870,16 @@ static void bond_iterator_check(const struct bt_bond_info *info, void *user_data
}

void common_hal_bleio_adapter_erase_bonding(bleio_adapter_obj_t *self) {
// Can be called from Python as _bleio.adapter.erase_bonding(), and from
// supervisor_bluetooth_init() on a discovery-mode boot, before the adapter is
// enabled. Bond keys are only visible once the stack is up and settings are
// loaded; without this the loop below finds nothing and the bond reappears
// when the workflow starts. The workflow call runs before the VM, so a failure
// to start the stack is ignored here rather than raised.
if (bleio_adapter_ensure_stack_ready() != 0) {
return;
}

// Unpair all bonded devices for all local identities.
for (uint8_t id = 0; id < CONFIG_BT_ID_MAX; id++) {
// bt_unpair takes an addr; use bt_foreach_bond to iterate and unpair.
Expand All @@ -866,7 +906,14 @@ void common_hal_bleio_adapter_erase_bonding(bleio_adapter_obj_t *self) {
}

bool common_hal_bleio_adapter_is_bonded_to_central(bleio_adapter_obj_t *self) {
// Check if any bond exists for identity 0
// Called only by the BLE workflow, from supervisor_bluetooth_init() before
// the adapter is enabled and from supervisor_bluetooth_background(); see
// erase_bonding() above. Runs before the VM, so don't raise.
if (bleio_adapter_ensure_stack_ready() != 0) {
return false;
}

// Check if any bond exists for any local identity.
for (uint8_t id = 0; id < CONFIG_BT_ID_MAX; id++) {
bool has_bonds = false;
bt_foreach_bond(id, bond_iterator_check, &has_bonds);
Expand All @@ -882,36 +929,6 @@ void bleio_adapter_gc_collect(bleio_adapter_obj_t *adapter) {
gc_collect_root((void **)bleio_connections, sizeof(bleio_connections) / sizeof(size_t));
}

void bleio_adapter_reset(bleio_adapter_obj_t *adapter) {
if (adapter == NULL) {
return;
}

common_hal_bleio_adapter_stop_scan(adapter);
common_hal_bleio_adapter_stop_advertising(adapter);

for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
bleio_connection_internal_t *connection = &bleio_connections[i];
if (connection->conn != NULL) {
bt_conn_disconnect(connection->conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
if (connection->connection_obj != MP_OBJ_NULL &&
connection->connection_obj != mp_const_none) {
bleio_connection_obj_t *connection_obj = MP_OBJ_TO_PTR(connection->connection_obj);
connection_obj->connection = NULL;
connection_obj->disconnect_reason = BT_HCI_ERR_REMOTE_USER_TERM_CONN;
}
bleio_connection_clear(connection);
}

adapter->scan_results = NULL;
adapter->connection_objs = NULL;
active_scan_results = NULL;
ble_advertising = false;
ble_advertising_internal = false;
ble_adapter_enabled = bt_is_ready();
}

bleio_adapter_obj_t *common_hal_bleio_allocate_adapter_or_raise(void) {
return &common_hal_bleio_adapter_obj;
}
Expand Down
1 change: 0 additions & 1 deletion ports/zephyr-cp/common-hal/_bleio/Adapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ typedef struct {
} bleio_adapter_obj_t;

void bleio_adapter_gc_collect(bleio_adapter_obj_t *adapter);
void bleio_adapter_reset(bleio_adapter_obj_t *adapter);

// Queue a background run of supervisor_bluetooth_background() so the VM drains
// incoming BLE PacketBuffer data. Safe to call from Zephyr BT/workqueue context.
Expand Down
4 changes: 4 additions & 0 deletions ports/zephyr-cp/common-hal/_bleio/Connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ typedef struct {
mp_obj_t connection_obj;
volatile pair_status_t pair_status;
uint8_t sec_err; // Security error code from pairing attempt
// True if user code initiated this connection or accepted it with its own
// advertising. bleio_user_reset() disconnects only these; the BLE workflow
// connection is not user-owned and survives VM restarts.
bool user_owned;
} bleio_connection_internal_t;

typedef struct {
Expand Down
73 changes: 44 additions & 29 deletions ports/zephyr-cp/common-hal/_bleio/PacketBuffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -134,28 +134,36 @@ static void packet_buffer_send_work_handler(struct k_work *work) {
bleio_packet_buffer_obj_t *self = CONTAINER_OF(
k_work_delayable_from_work(work), bleio_packet_buffer_obj_t, send_work);

// A notification is awaiting its completion callback; it will resubmit us.
if (self->packet_queued) {
return;
}
if (self->pending_size == 0) {
return; // nothing staged
}

// Server-side notify path only; clients write directly. characteristic is
// NULL after deinit, so bail before touching it (a completion callback can
// resubmit us after teardown).
// NULL after deinit, so bail before touching it (notify_complete_cb() can
// reschedule send_work after teardown).
bleio_characteristic_obj_t *c = self->characteristic;
if (c == NULL || self->client) {
return;
}
if (!conn_is_valid(self)) {
// Stale connection: drop everything staged.
// The peer is gone: drop everything staged, and clear packet_queued even
// if a notification was handed to bt_gatt_notify_cb() already and its
// notify_complete_cb() has not run. It never will: Zephyr does not call
// the ATT callback for a PDU destroyed by a disconnect (att.c
// att_on_sent_cb(): "Bearer not connected, dropping ATT cb"). If
// packet_queued stayed set, the early return below would skip every
// later send on this buffer, even after the peer reconnects.
self->pending_size = 0;
self->packet_queued = false;
return;
}

// A notification has been handed to bt_gatt_notify_cb() and its
// notify_complete_cb() has not run yet. That callback reschedules send_work,
// so this handler will run again once the controller has sent the PDU.
if (self->packet_queued) {
return;
}
if (self->pending_size == 0) {
return; // nothing staged
}

// Staging keeps pending_size <= the negotiated ATT MTU payload, so the
// whole staged packet fits in one notification.
struct bt_gatt_notify_params params = {
Expand All @@ -176,27 +184,34 @@ static void packet_buffer_send_work_handler(struct k_work *work) {
self->pending_index ^= 1; // VM fills the other buffer next
return;
}
if (err == -ENOMEM || err == -EAGAIN) {
// No ATT TX buffer right now. The ATT TX pool is shared across all ATT
// traffic on all connections, so it can be full from other
// notifies/indications/responses even when we have nothing in flight.
// Running on the workqueue makes the allocator use K_NO_WAIT: it returns
// NULL and the stack returns -ENOMEM *before* copying or queueing a PDU —
// nothing sent, nothing dropped; the bytes are still in
// outgoing[pending_index]. Leave the data staged and reschedule ourselves
// after a short delay so we retry even when the VM is idle (no write/flush
// to drive us). The delay — not an immediate resubmit — keeps the workqueue
// from busy-looping against a full pool.
k_work_reschedule(&self->send_work, K_MSEC(2));
return;
}
if (err == -ENOTCONN) {
// Peer disconnected — discard everything pending and cancel any
// pending delayed retry. (We're here only when packet_queued is clear,
// so no in-flight completion is owed.)
// Peer disconnected — forget the connection too. (We're here only when
// packet_queued is clear, so no in-flight completion is owed.)
self->conn = NULL;
self->pending_size = 0;
self->packet_queued = false;
k_work_cancel_delayable(&self->send_work);
return;
}
// -ENOMEM / -EAGAIN: no ATT TX buffer right now. The ATT TX pool is shared
// across all ATT traffic on all connections, so it can be full from other
// notifies/indications/responses even when we have nothing in flight.
// Running on the workqueue makes the allocator use K_NO_WAIT: it returns
// NULL and the stack returns -ENOMEM *before* copying or queueing a PDU —
// nothing sent, nothing dropped; the bytes are still in
// outgoing[pending_index]. Leave the data staged and reschedule ourselves
// after a short delay so we retry even when the VM is idle (no write/flush
// to drive us). The delay — not an immediate resubmit — keeps the workqueue
// from busy-looping against a full pool.
k_work_reschedule(&self->send_work, K_MSEC(2));
// Any other error is not transient: -ENOTCONN (peer gone), -EPERM (link not
// encrypted and the characteristic requires it), -EINVAL (peer not
// subscribed; CONFIG_BT_GATT_ENFORCE_SUBSCRIPTION), -ENOENT (attribute not
// registered). Retrying would spin forever and wedge this buffer. Drop the
// staged packet, as nordic and espressif do on non-resource errors, and
// cancel any pending delayed retry.
self->pending_size = 0;
self->packet_queued = false;
k_work_cancel_delayable(&self->send_work);
}

// Shared core for both the Python-facing (allocating) and workflow
Expand Down
Loading
Loading