Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,6 @@
[submodule "frozen/CircuitPython_edupico2_paj7620"]
path = frozen/CircuitPython_edupico2_paj7620
url = https://github.com/CytronTechnologies/CircuitPython_edupico2_paj7620.git
[submodule "frozen/Adafruit_CircuitPython_BLE_File_Transfer"]
path = frozen/Adafruit_CircuitPython_BLE_File_Transfer
url = https://github.com/adafruit/Adafruit_CircuitPython_BLE_File_Transfer.git
8 changes: 8 additions & 0 deletions ports/zephyr-cp/boards/nrf54lm20bsim_nrf54lm20a_cpuapp.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
# Override Kconfig default
CONFIG_BT_BUF_CMD_TX_COUNT=2

# The bsim native_sim does not drive the BLE controller ticker time-slot
# mechanism used for radio-synchronized flash operations. With the default
# SOC_FLASH_NRF_RADIO_SYNC_TICKER, flash erase/write waits on a semaphore
# that is never given, hanging for ~34s (FLASH_TIMEOUT_MS) per operation.
# Disable radio sync so flash operations run directly, matching nrf5340bsim
# (which uses an IPC-based controller and so defaults to _NONE).
CONFIG_SOC_FLASH_NRF_RADIO_SYNC_NONE=y

CONFIG_TRACING=y
CONFIG_TRACING_PERFETTO=y
CONFIG_TRACING_SYNC=y
Expand Down
9 changes: 9 additions & 0 deletions ports/zephyr-cp/boards/nrf54lm20dk_nrf54lm20a_cpuapp.conf
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y

# Work around a latent bug in soc_flash_nrf_ticker.c: with the default
# NRF_RRAM_WRITE_BUFFER_SIZE=1, FLASH_SLOT_WRITE (500 us) <
# FLASH_SYNC_SWITCHING_TIME (1700 us), so `interval = duration -
# FLASH_SYNC_SWITCHING_TIME` underflows and the next flash slot is scheduled
# ~71 minutes out. Any write larger than a single 16-byte slot then times out
# (-ETIMEDOUT / -116), e.g. persisting bond keys right after pairing.
# See zephyr/tests/boards/nrf/rram/overlay-radio_sync.conf.
CONFIG_NRF_RRAM_WRITE_BUFFER_SIZE=32
203 changes: 172 additions & 31 deletions ports/zephyr-cp/common-hal/_bleio/Adapter.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <zephyr/bluetooth/addr.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/hci_vs.h>
#include <zephyr/settings/settings.h>
Expand All @@ -20,18 +21,41 @@
#include "py/runtime.h"
#include "bindings/zephyr_kernel/__init__.h"
#include "shared-bindings/_bleio/__init__.h"
#include "common-hal/_bleio/__init__.h"
#include "shared-bindings/_bleio/Adapter.h"
#include "shared-bindings/_bleio/Address.h"
#include "shared-module/_bleio/Address.h"
#include "shared-module/_bleio/ScanResults.h"
#include "supervisor/background_callback.h"
#include "supervisor/shared/bluetooth/bluetooth.h"
#include "supervisor/shared/tick.h"

bleio_connection_internal_t bleio_connections[BLEIO_TOTAL_CONNECTION_COUNT];

// Background pump: drains the BLE file-transfer / serial PacketBuffers by
// calling supervisor_bluetooth_background(). Queued from GATT write callbacks
// and connection events so the VM processes incoming data promptly.
static background_callback_t bluetooth_background_cb = {NULL, NULL};

static void bluetooth_adapter_background(void *data) {
(void)data;
supervisor_bluetooth_background();
}

void bleio_request_bluetooth_background(void) {
if (bluetooth_background_cb.fun != NULL) {
background_callback_add_core(&bluetooth_background_cb);
}
}

static bool scan_callbacks_registered = false;
static bleio_scanresults_obj_t *active_scan_results = NULL;
static struct bt_le_scan_cb scan_callbacks;
static bool ble_advertising = false;
// True when advertising was started by the BLE workflow (supervisor) rather
// 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;

#define BLEIO_ADV_MAX_FIELDS 16
Expand Down Expand Up @@ -142,27 +166,65 @@ static void bleio_connection_release(bleio_connection_internal_t *connection, ui
common_hal_bleio_adapter_obj.connection_objs = NULL;
}

// Per-connection ATT MTU exchange parameters. The params struct must persist
// until the exchange callback fires, so it lives for the lifetime of the slot.
static struct bt_gatt_exchange_params mtu_exchange_params[BLEIO_TOTAL_CONNECTION_COUNT];

static void on_mtu_exchanged(struct bt_conn *conn, uint8_t err,
struct bt_gatt_exchange_params *params) {
(void)conn;
(void)params;
if (err == 0) {
// Wake the workflow so outgoing_packet_length is recomputed with the
// now-larger negotiated MTU.
bleio_request_bluetooth_background();
}
}

static void bleio_connected_cb(struct bt_conn *conn, uint8_t err) {
if (err != 0) {
return;
}

if (bleio_connection_track(conn) == NULL) {
bleio_connection_internal_t *connection = bleio_connection_track(conn);
if (connection == NULL) {
bt_conn_disconnect(conn, BT_HCI_ERR_CONN_LIMIT_EXCEEDED);
return;
}

// Initiate an ATT MTU exchange so the negotiated MTU reflects the larger
// payload our stack supports (CONFIG_BT_L2CAP_TX_MTU). Many centrals do
// this themselves, but if they don't we'd be stuck at the default 23-byte
// MTU (20-byte payload). That forces the file-transfer workflow to split
// protocol messages across notifications in ways peers can't reassemble
// (e.g. a listdir_entry whose second fragment begins with a 0x00 flags
// byte is misread as "unknown command 0x00"). bt_gatt_exchange_mtu returns
// -EALREADY if the peer already initiated, so this is safe either way.
size_t idx = (size_t)(connection - bleio_connections);
mtu_exchange_params[idx].func = on_mtu_exchanged;
int mtu_err = bt_gatt_exchange_mtu(conn, &mtu_exchange_params[idx]);
(void)mtu_err;

// 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
// thread context).
ble_advertising = false;
ble_advertising_internal = false;

common_hal_bleio_adapter_obj.connection_objs = NULL;

// Pump the workflow once now, and arm the recurring background callback
// so future GATT writes / events get drained by the VM.
bluetooth_background_cb.fun = bluetooth_adapter_background;
bluetooth_background_cb.data = NULL;
bluetooth_adapter_background(NULL);
}

static void bleio_disconnected_cb(struct bt_conn *conn, uint8_t reason) {
bleio_connection_discovery_abort();
bleio_connection_release(bleio_connection_find_by_conn(conn), reason);
bleio_request_bluetooth_background();
}

static void bleio_security_changed_cb(struct bt_conn *conn, bt_security_t level,
Expand Down Expand Up @@ -240,7 +302,7 @@ static size_t bleio_parse_adv_data(const uint8_t *raw, size_t raw_len, struct bt
if (offset + field_len + 1 > raw_len ||
count >= out_len ||
storage_offset + data_len > storage_len) {
mp_raise_ValueError(MP_ERROR_TEXT("Invalid advertising data"));
return 0;
}
uint8_t type = raw[offset + 1];
memcpy(storage + storage_offset, raw + offset + 2, data_len);
Expand Down Expand Up @@ -328,13 +390,15 @@ mp_int_t common_hal_bleio_adapter_get_tx_power(bleio_adapter_obj_t *self) {
return power;
}

void common_hal_bleio_adapter_set_tx_power(bleio_adapter_obj_t *self, mp_int_t tx_power) {
// Non-raising variant of common_hal_bleio_adapter_set_tx_power for use from the
// BLE workflow, which runs outside the VM. Returns 0 on success.
static int bleio_adapter_set_tx_power_noraise(mp_int_t tx_power) {
struct bt_hci_cp_vs_write_tx_power_level *cp;
struct net_buf *buf, *rsp = NULL;

buf = bt_hci_cmd_alloc(K_MSEC(3000));
if (!buf) {
mp_raise_msg(&mp_type_MemoryError, NULL);
return -ENOMEM;
}
cp = net_buf_add(buf, sizeof(*cp));
cp->handle_type = BT_HCI_VS_LL_HANDLE_TYPE_ADV;
Expand All @@ -343,10 +407,18 @@ void common_hal_bleio_adapter_set_tx_power(bleio_adapter_obj_t *self, mp_int_t t

int err = bt_hci_cmd_send_sync(BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, buf, &rsp);
if (err) {
raise_zephyr_error(err);
return err;
}

net_buf_unref(rsp);
return 0;
}

void common_hal_bleio_adapter_set_tx_power(bleio_adapter_obj_t *self, mp_int_t tx_power) {
int err = bleio_adapter_set_tx_power_noraise(tx_power);
if (err) {
raise_zephyr_error(err);
}
}

bleio_address_obj_t *common_hal_bleio_adapter_get_address(bleio_adapter_obj_t *self) {
Expand Down Expand Up @@ -380,25 +452,31 @@ void common_hal_bleio_adapter_set_name(bleio_adapter_obj_t *self, const char *na
}
}

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,
// Internal start_advertising used by the BLE workflow (file transfer + serial
// services). Runs outside the VM, so it must not raise. Returns 0 on success or
// a positive errno on failure. A timeout of 0 means advertise indefinitely.
// This is the core implementation; common_hal_bleio_adapter_start_advertising()
// delegates here and translates errors into exceptions.
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) {
(void)directed_to;
(void)interval;
(void)anonymous;
(void)timeout;

if (advertising_data_bufinfo->len > BLEIO_ADV_MAX_DATA_LEN ||
scan_response_data_bufinfo->len > BLEIO_ADV_MAX_DATA_LEN) {
mp_raise_NotImplementedError(NULL);
}

if (timeout != 0) {
mp_raise_NotImplementedError(NULL);
if (advertising_data_len > BLEIO_ADV_MAX_DATA_LEN ||
scan_response_data_len > BLEIO_ADV_MAX_DATA_LEN) {
return (uint32_t)EINVAL;
}

// Don't disturb advertising that is already active (either user code or the
// workflow's own previous advert). The caller is responsible for stopping
// first if a restart is desired.
if (ble_advertising) {
raise_zephyr_error(-EALREADY);
return (uint32_t)EBUSY;
}

bt_addr_le_t id_addrs[CONFIG_BT_ID_MAX];
Expand All @@ -407,30 +485,31 @@ void common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self,
if (id_count == 0 || bt_addr_le_eq(&id_addrs[BT_ID_DEFAULT], BT_ADDR_LE_ANY)) {
int id = bt_id_create(NULL, NULL);
if (id < 0) {
printk("Failed to create identity address: %d\n", id);
raise_zephyr_error(id);
return (uint32_t)(-id);
}
}

size_t adv_count = bleio_parse_adv_data(advertising_data_bufinfo->buf,
advertising_data_bufinfo->len,
size_t adv_count = bleio_parse_adv_data(advertising_data,
advertising_data_len,
adv_data,
BLEIO_ADV_MAX_FIELDS,
adv_data_storage,
sizeof(adv_data_storage));
if (adv_count == 0) {
return (uint32_t)EINVAL;
}

size_t scan_resp_count = 0;
if (scan_response_data_bufinfo->len > 0) {
scan_resp_count = bleio_parse_adv_data(scan_response_data_bufinfo->buf,
scan_response_data_bufinfo->len,
if (scan_response_data_len > 0) {
scan_resp_count = bleio_parse_adv_data(scan_response_data,
scan_response_data_len,
scan_resp_data,
BLEIO_ADV_MAX_FIELDS,
scan_resp_storage,
sizeof(scan_resp_storage));
}

if (anonymous) {
mp_raise_NotImplementedError(NULL);
if (scan_resp_count == 0) {
return (uint32_t)EINVAL;
}
}

struct bt_le_adv_param adv_params;
Expand All @@ -454,15 +533,75 @@ void common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self,
NULL);
}

common_hal_bleio_adapter_set_tx_power(self, tx_power);
// Best-effort TX power: the vendor HCI command may not exist on all
// controllers, so ignore failures here.
(void)bleio_adapter_set_tx_power_noraise(tx_power);

raise_zephyr_error(bt_le_adv_start(&adv_params,
int err = bt_le_adv_start(&adv_params,
adv_data,
adv_count,
scan_resp_count > 0 ? scan_resp_data : NULL,
scan_resp_count));
scan_resp_count);
if (err) {
return (uint32_t)(-err);
}

ble_advertising = true;
// Default to workflow-owned; the public wrapper overrides this for user code.
ble_advertising_internal = true;
return 0;
}

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) {
(void)directed_to;
(void)interval;

if (advertising_data_bufinfo->len > BLEIO_ADV_MAX_DATA_LEN ||
scan_response_data_bufinfo->len > BLEIO_ADV_MAX_DATA_LEN) {
mp_raise_NotImplementedError(NULL);
}

if (timeout != 0) {
mp_raise_NotImplementedError(NULL);
}

if (anonymous) {
mp_raise_NotImplementedError(NULL);
}

if (ble_advertising) {
if (!ble_advertising_internal) {
// User code is already advertising.
raise_zephyr_error(-EALREADY);
}
// The workflow is advertising. Stop it so user code can take over.
common_hal_bleio_adapter_stop_advertising(self);
}

uint32_t status = _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);
if (status == (uint32_t)EINVAL) {
mp_raise_ValueError(MP_ERROR_TEXT("Invalid advertising data"));
}
if (status != 0) {
raise_zephyr_error(-(int)status);
}

// Mark as user-owned so the workflow won't clobber it.
ble_advertising_internal = false;
}

void common_hal_bleio_adapter_stop_advertising(bleio_adapter_obj_t *self) {
Expand All @@ -472,6 +611,7 @@ void common_hal_bleio_adapter_stop_advertising(bleio_adapter_obj_t *self) {
}
bt_le_adv_stop();
ble_advertising = false;
ble_advertising_internal = false;
}

bool common_hal_bleio_adapter_get_advertising(bleio_adapter_obj_t *self) {
Expand Down Expand Up @@ -737,6 +877,7 @@ void bleio_adapter_reset(bleio_adapter_obj_t *adapter) {
adapter->connection_objs = NULL;
active_scan_results = NULL;
ble_advertising = false;
ble_advertising_internal = false;
ble_adapter_enabled = bt_is_ready();
}

Expand Down
4 changes: 4 additions & 0 deletions ports/zephyr-cp/common-hal/_bleio/Adapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ typedef struct {

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.
void bleio_request_bluetooth_background(void);
Loading
Loading