From a8bb681bd939693a2c25d3d64d26b5150798d999 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sun, 20 Sep 2026 21:08:34 -0400 Subject: [PATCH 1/3] _bleio HCI: use extended advertising commands on 5.x controllers A controller that supports LE Extended Advertising refuses legacy advertising commands with Command Disallowed once any extended command has been issued (Core Spec Vol 4 Part E 3.1.1). The adapter sent `LE Read Maximum Advertising Data Length` at enable time and then legacy `LE Set Advertising Parameters`, so `start_advertising()` failed with HCI status 0x0C on the ESP32-C6 AirLift (Fruit Jam). On such controllers, use the extended command set for all advertising, with legacy PDUs so the 31-byte limit and 4.x central compatibility are unchanged. Also complete the extended path: send advertising and scan response data, fill in `prim_adv_phy`, `filter_policy` and the peer address type, and disable via the extended enable command. Fixes #11322 Co-Authored-By: Claude Sonnet 5 --- devices/ble_hci/common-hal/_bleio/Adapter.c | 98 ++++++++++++--------- devices/ble_hci/common-hal/_bleio/Adapter.h | 2 - devices/ble_hci/common-hal/_bleio/hci.c | 15 ++++ devices/ble_hci/common-hal/_bleio/hci.h | 3 + 4 files changed, 76 insertions(+), 42 deletions(-) diff --git a/devices/ble_hci/common-hal/_bleio/Adapter.c b/devices/ble_hci/common-hal/_bleio/Adapter.c index 005dd0aeb03..5a47d09bb9e 100644 --- a/devices/ble_hci/common-hal/_bleio/Adapter.c +++ b/devices/ble_hci/common-hal/_bleio/Adapter.c @@ -160,6 +160,13 @@ static void check_enabled(bleio_adapter_obj_t *adapter) { } } +// A controller that supports extended advertising refuses legacy advertising +// commands (Command Disallowed) once any extended command has been sent, so +// use the extended command set exclusively on such controllers. +static bool adapter_uses_extended_advertising(bleio_adapter_obj_t *adapter) { + return BT_FEAT_LE_EXT_ADV(adapter->features); +} + // static bool adapter_on_ble_evt(ble_evt_t *ble_evt, void *self_in) { // bleio_adapter_obj_t *self = (bleio_adapter_obj_t*)self_in; @@ -296,17 +303,6 @@ static void bleio_adapter_hci_init(bleio_adapter_obj_t *self) { self->max_acl_buffer_len = acl_max_len; self->max_acl_num_buffers = acl_max_num; } - - // Get max advertising length if extended advertising is supported. - if (BT_FEAT_LE_EXT_ADV(self->features)) { - uint16_t max_adv_data_len; - if (hci_le_read_maximum_advertising_data_length(&max_adv_data_len) != HCI_OK) { - mp_raise_bleio_BluetoothError(MP_ERROR_TEXT("Could not get max advertising length")); - } - self->max_adv_data_len = max_adv_data_len; - } else { - self->max_adv_data_len = MAX_ADVERTISEMENT_SIZE; - } } void common_hal_bleio_adapter_construct_hci_uart(bleio_adapter_obj_t *self, busio_uart_obj_t *uart, digitalio_digitalinout_obj_t *rts, digitalio_digitalinout_obj_t *cts) { @@ -343,7 +339,6 @@ void common_hal_bleio_adapter_set_enabled(bleio_adapter_obj_t *self, bool enable // Enabling or disabling: stop any current activity; reset to known state. hci_reset(); self->now_advertising = false; - self->extended_advertising = false; self->circuitpython_advertising = false; self->advertising_timeout_msecs = 0; @@ -633,19 +628,17 @@ uint32_t _common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, memcpy(&peer_addr.a.val, directed_to->bytes, sizeof(peer_addr.a.val)); } - bool extended = - advertising_data_len > self->max_adv_data_len || scan_response_data_len > self->max_adv_data_len; - - if (extended) { - if (!BT_FEAT_LE_EXT_ADV(self->features)) { - mp_raise_bleio_BluetoothError(MP_ERROR_TEXT("Data length needs extended advertising, but this adapter does not support it")); - } - - uint16_t props = 0; + if (adapter_uses_extended_advertising(self)) { + // Use legacy PDUs (ADV_IND etc.) so that 4.x centrals still see us and the + // 31-byte data limit applies unchanged; only the HCI command set is extended. + uint16_t props = BT_HCI_LE_ADV_PROP_LEGACY; if (connectable) { - props |= BT_HCI_LE_ADV_PROP_CONN; - } - if (scan_response_data_len > 0) { + if (directed_to) { + props |= BT_HCI_LE_ADV_PROP_CONN | BT_HCI_LE_ADV_PROP_DIRECT; + } else { + props |= BT_HCI_LE_ADV_PROP_CONN | BT_HCI_LE_ADV_PROP_SCAN; + } + } else if (scan_response_data_len > 0) { props |= BT_HCI_LE_ADV_PROP_SCAN; } @@ -660,21 +653,43 @@ uint32_t _common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, interval_units, // max interval 0b111, // channel map: channels 37, 38, 39 anonymous ? BT_ADDR_LE_RANDOM : BT_ADDR_LE_PUBLIC, - &peer_addr, // peer_addr, + &peer_addr, 0x00, // filter policy: no filter - DEFAULT_TX_POWER, - BT_HCI_LE_EXT_SCAN_PHY_1M, // Secondary PHY to use - 0x00, // AUX_ADV_IND shall be sent prior to next adv event - BT_HCI_LE_EXT_SCAN_PHY_1M, // Secondary PHY to use - 0x00, // Advertising SID - 0x00 // Scan req notify disable + BT_HCI_LE_ADV_TX_POWER_NO_PREF, + BT_HCI_LE_EXT_SCAN_PHY_1M, // primary PHY + 0x00, // secondary max skip (unused with legacy PDUs) + BT_HCI_LE_EXT_SCAN_PHY_1M, // secondary PHY (unused with legacy PDUs) + 0x00, // advertising SID + 0x00 // scan request notification disabled )); - // We can use the duration mechanism provided, instead of our own. - self->advertising_timeout_msecs = 0; + hci_check_error( + hci_le_set_extended_advertising_data( + 0, // handle + BT_HCI_LE_EXT_ADV_OP_COMPLETE_DATA, + BT_HCI_LE_EXT_ADV_FRAG_DISABLED, + advertising_data_len, + (uint8_t *)advertising_data)); + + // A scannable set must have scan response data defined before it is enabled, + // even if that data is empty. + if (props & BT_HCI_LE_ADV_PROP_SCAN) { + hci_check_error( + hci_le_set_extended_scan_response_data( + 0, // handle + BT_HCI_LE_EXT_ADV_OP_COMPLETE_DATA, + BT_HCI_LE_EXT_ADV_FRAG_DISABLED, + scan_response_data_len, + (uint8_t *)scan_response_data)); + } + + // Use our own timeout rather than the controller's duration, so that we don't + // depend on receiving the LE Advertising Set Terminated event. + self->advertising_timeout_msecs = timeout * 1000; + self->advertising_start_ticks = supervisor_ticks_ms64(); uint8_t handle[1] = { 0 }; - uint16_t duration_10msec[1] = { timeout * 100 }; + uint16_t duration_10msec[1] = { 0 }; uint8_t max_ext_adv_evts[1] = { 0 }; hci_check_error( hci_le_set_extended_advertising_enable( @@ -684,8 +699,6 @@ uint32_t _common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, duration_10msec, max_ext_adv_evts )); - - self->extended_advertising = true; } else { // Legacy advertising (not extended). @@ -732,7 +745,6 @@ uint32_t _common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, // Start advertising. hci_check_error(hci_le_set_advertising_enable(BT_HCI_LE_ADV_ENABLE)); - self->extended_advertising = false; } // end legacy advertising setup vm_used_ble = true; @@ -790,10 +802,17 @@ void common_hal_bleio_adapter_stop_advertising(bleio_adapter_obj_t *self) { check_enabled(self); self->now_advertising = false; - self->extended_advertising = false; self->circuitpython_advertising = false; - int result = hci_le_set_advertising_enable(BT_HCI_LE_ADV_DISABLE); + int result; + if (adapter_uses_extended_advertising(self)) { + uint8_t handle[1] = { 0 }; + uint16_t duration_10msec[1] = { 0 }; + uint8_t max_ext_adv_evts[1] = { 0 }; + result = hci_le_set_extended_advertising_enable(BT_HCI_LE_ADV_DISABLE, 1, handle, duration_10msec, max_ext_adv_evts); + } else { + result = hci_le_set_advertising_enable(BT_HCI_LE_ADV_DISABLE); + } // OK if we're already stopped. There seems to be an ESP32 HCI bug: // If advertising is already off, then LE_SET_ADV_ENABLE does not return a response. if (result != HCI_RESPONSE_TIMEOUT) { @@ -807,7 +826,6 @@ void common_hal_bleio_adapter_stop_advertising(bleio_adapter_obj_t *self) { // Don't ask the adapter to stop. void bleio_adapter_advertising_was_stopped(bleio_adapter_obj_t *self) { self->now_advertising = false; - self->extended_advertising = false; self->circuitpython_advertising = false; } diff --git a/devices/ble_hci/common-hal/_bleio/Adapter.h b/devices/ble_hci/common-hal/_bleio/Adapter.h index c018c676171..ee7672649dd 100644 --- a/devices/ble_hci/common-hal/_bleio/Adapter.h +++ b/devices/ble_hci/common-hal/_bleio/Adapter.h @@ -34,7 +34,6 @@ typedef struct _bleio_adapter_obj_t { digitalio_digitalinout_obj_t *cts_digitalinout; bool allocated; // True when in use. bool now_advertising; - bool extended_advertising; bool circuitpython_advertising; bool enabled; @@ -56,7 +55,6 @@ typedef struct _bleio_adapter_obj_t { uint16_t max_acl_buffer_len; uint16_t max_acl_num_buffers; - uint16_t max_adv_data_len; uint8_t features[8]; // Supported BLE features. // All the local attributes for this device. The index into the list diff --git a/devices/ble_hci/common-hal/_bleio/hci.c b/devices/ble_hci/common-hal/_bleio/hci.c index 126f05fbae2..0caa8a1f189 100644 --- a/devices/ble_hci/common-hal/_bleio/hci.c +++ b/devices/ble_hci/common-hal/_bleio/hci.c @@ -608,7 +608,9 @@ hci_result_t hci_le_set_extended_advertising_parameters(uint8_t handle, uint16_t .prim_channel_map = prim_channel_map, .own_addr_type = own_addr_type, // .peer_addr set below. + .filter_policy = filter_policy, .tx_power = tx_power, + .prim_adv_phy = prim_adv_phy, .sec_adv_max_skip = sec_adv_max_skip, .sec_adv_phy = sec_adv_phy, .sid = sid, @@ -619,6 +621,7 @@ hci_result_t hci_le_set_extended_advertising_parameters(uint8_t handle, uint16_t sizeof_field(struct bt_hci_cp_le_set_ext_adv_param, prim_min_interval)); memcpy(params.prim_max_interval, (void *)&prim_max_interval, sizeof_field(struct bt_hci_cp_le_set_ext_adv_param, prim_max_interval)); + params.peer_addr.type = peer_addr->type; memcpy(params.peer_addr.a.val, peer_addr->a.val, sizeof_field(bt_addr_le_t, a.val)); return send_command(BT_HCI_OP_LE_SET_EXT_ADV_PARAM, sizeof(params), ¶ms); } @@ -672,6 +675,18 @@ hci_result_t hci_le_set_extended_advertising_data(uint8_t handle, uint8_t op, ui return send_command(BT_HCI_OP_LE_SET_EXT_ADV_DATA, sizeof(params) - (max_len - valid_len), ¶ms); } +hci_result_t hci_le_set_extended_scan_response_data(uint8_t handle, uint8_t op, uint8_t frag_pref, uint8_t len, uint8_t data[]) { + const uint8_t max_len = sizeof_field(struct bt_hci_cp_le_set_ext_scan_rsp_data, data); + uint8_t valid_len = MIN(len, max_len); + struct bt_hci_cp_le_set_ext_scan_rsp_data params = { + .handle = handle, + .op = op, + .frag_pref = frag_pref, + .len = valid_len, + }; + memcpy(params.data, data, valid_len); + return send_command(BT_HCI_OP_LE_SET_EXT_SCAN_RSP_DATA, sizeof(params) - (max_len - valid_len), ¶ms); +} hci_result_t hci_le_set_scan_response_data(uint8_t len, uint8_t data[]) { struct bt_hci_cp_le_set_scan_rsp_data params = { diff --git a/devices/ble_hci/common-hal/_bleio/hci.h b/devices/ble_hci/common-hal/_bleio/hci.h index fffed7ee151..52da0e246a0 100644 --- a/devices/ble_hci/common-hal/_bleio/hci.h +++ b/devices/ble_hci/common-hal/_bleio/hci.h @@ -68,6 +68,9 @@ hci_result_t hci_le_set_advertising_parameters(uint16_t min_interval, uint16_t m hci_result_t hci_le_set_extended_advertising_data(uint8_t handle, uint8_t op, uint8_t frag_pref, uint8_t len, uint8_t data[]); hci_result_t hci_le_set_extended_advertising_enable(uint8_t enable, uint8_t set_num, uint8_t handle[], uint16_t duration[], uint8_t max_ext_adv_evts[]); hci_result_t hci_le_set_extended_advertising_parameters(uint8_t handle, uint16_t props, uint32_t prim_min_interval, uint32_t prim_max_interval, uint8_t prim_channel_map, uint8_t own_addr_type, bt_addr_le_t *peer_addr, uint8_t filter_policy, int8_t tx_power, uint8_t prim_adv_phy, uint8_t sec_adv_max_skip, uint8_t sec_adv_phy, uint8_t sid, uint8_t scan_req_notify_enable); +// Sends at most one fragment (251 bytes); longer data is truncated. +// Returns HCI_OK or the HCI error status; no output values. +hci_result_t hci_le_set_extended_scan_response_data(uint8_t handle, uint8_t op, uint8_t frag_pref, uint8_t len, uint8_t data[]); hci_result_t hci_le_set_random_address(uint8_t addr[6]); hci_result_t hci_le_set_scan_enable(uint8_t enable, uint8_t filter_dup); From 1af5d49a1879b1775ea39fa3ce5d106c55380c31 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sun, 20 Sep 2026 21:08:59 -0400 Subject: [PATCH 2/3] _bleio HCI: send ACL data with the non-flushable start PB flag `hci_send_acl_pkt()` tagged host-to-controller ACL data with PB flag 0b10 (first automatically flushable). NimBLE controllers such as the ESP32-C6 treat any PB above 0b01 as a bad packet and drop it silently, so every ATT response was lost and peers hung in service discovery. Use 0b00 (first non-flushable), as BlueZ and Zephyr do. The ESP32 BTDM controller accepted either value. Co-Authored-By: Claude Sonnet 5 --- devices/ble_hci/common-hal/_bleio/hci.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/devices/ble_hci/common-hal/_bleio/hci.c b/devices/ble_hci/common-hal/_bleio/hci.c index 0caa8a1f189..a5d63e9015c 100644 --- a/devices/ble_hci/common-hal/_bleio/hci.c +++ b/devices/ble_hci/common-hal/_bleio/hci.c @@ -489,7 +489,9 @@ hci_result_t hci_send_acl_pkt(uint16_t handle, uint8_t cid, uint16_t data_len, u acl_data_t *acl_data = (acl_data_t *)acl_pkt->data; acl_pkt->pkt_type = H4_ACL; acl_pkt->handle = handle; - acl_pkt->pb = ACL_DATA_PB_FIRST_FLUSH; + // Host-to-controller LE data must use the non-flushable start flag; NimBLE + // controllers (ESP32-C6) silently discard packets with ACL_DATA_PB_FIRST_FLUSH. + acl_pkt->pb = ACL_DATA_PB_FIRST_NON_FLUSH; acl_pkt->bc = 0; acl_pkt->data_len = (uint16_t)(sizeof(acl_data_t) + data_len); acl_data->acl_data_len = data_len; From fd039bdf49e513795f8e8abd6209892721012de6 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 21 Sep 2026 17:37:22 -0400 Subject: [PATCH 3/3] _bleio HCI: explain the extended advertising enable duration values Co-Authored-By: Claude Sonnet 5 --- devices/ble_hci/common-hal/_bleio/Adapter.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/devices/ble_hci/common-hal/_bleio/Adapter.c b/devices/ble_hci/common-hal/_bleio/Adapter.c index 5a47d09bb9e..0ea05c44e5c 100644 --- a/devices/ble_hci/common-hal/_bleio/Adapter.c +++ b/devices/ble_hci/common-hal/_bleio/Adapter.c @@ -688,6 +688,7 @@ uint32_t _common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, self->advertising_timeout_msecs = timeout * 1000; self->advertising_start_ticks = supervisor_ticks_ms64(); + // Duration 0 and max events 0 mean "advertise until disabled by the host". uint8_t handle[1] = { 0 }; uint16_t duration_10msec[1] = { 0 }; uint8_t max_ext_adv_evts[1] = { 0 }; @@ -806,6 +807,7 @@ void common_hal_bleio_adapter_stop_advertising(bleio_adapter_obj_t *self) { int result; if (adapter_uses_extended_advertising(self)) { + // Duration and max events are ignored when disabling. uint8_t handle[1] = { 0 }; uint16_t duration_10msec[1] = { 0 }; uint8_t max_ext_adv_evts[1] = { 0 };