Cannot complete a bt connection after last update to v2.3.0 #3106

Open
opened 2026-07-31 10:53:07 -05:00 by elrir01 · 8 comments
elrir01 commented 2026-07-31 10:53:07 -05:00 (Migrated from github.com)

Hello,

I am building a device that streams Bluetooth audio using the A2DP protocol over I2S to a DAC, in order to play music from my phone to a set of speakers. I am using the Pimoroni Pico 2 W Plus.

For this project, I based my code on the btstack example a2dp_sink_demo.c found in pico-examples. I am using FreeRTOS to manage the various tasks, and I have a dedicated task for the btstack loop.

Anyway, the issue I am experiencing can also be reproduced with just the aforementioned example; in fact, the Bluetooth handling code in my project is almost identical.

The problem is that I cannot connect to the phone after updating to the latest version of the pico-sdk (v2.3.0), and consequently to btstack version 1.8.2. (The btstack changelog mentions an increased security level in this release.) Let me explain further: the pairing between the device and the phone, when initiating the request from the phone, is successful and I am able to connect; however, after a reboot using the following function

a2dp_sink_establish_stream(device_addr, &a2dp_sink_demo_a2dp_connection.a2dp_cid); 

where device_addr is the MAC address of my phone a Pixel 8a,
as you can see in the log AudioPico_Pixel_connection_failed.pklg, it returns:

357	-31622388.284000    0.000000	PKTLOG	59	A2DP  Sink : Streaming connection failed, status 0x6a

After several tests and attempts, my guess is that the process might be failing after authentication when requesting to raise the security level at the following line:

306	-31622392.284000	0.009000			PKTLOG	91	hci.c.9154: gap_request_security_level requested level 2, planned level 0, current level 0

Looking at the log from the phone's perspective in the Pixel_AudioPico_connection_failed.pcapng file, after the line below, the Pico sends the disconnect command. I assume because the security levels do not match, meaning level 2 is requested but the current level is 0:

57	0.970892000	0.049205000	controller	host	HCI_EVT	12	Rcvd Encryption Change [v2]

The same code works with pico-sdk 2.2.0.
I noticed another thing: when I compile with the latest SDK version and pair, after rebooting I initially get error 0x66 which is L2CAP_CONNECTION_RESPONSE_RESULT_REFUSED_SECURITY,I noticed from the logs that the Link Key was reversed; by executing a function to reverse it and saving it, the error goes back to being 0x6a or L2CAP_CONNECTION_BASEBAND_DISCONNECT. I know the key is correct reversed because if I flash the code compiled with the old sdk it works without need to pair, in fact the keys are saved in flash via the TLV functions.

This is my function to initialize the various bluetooth protocols:

int setup_demo(void){
    // init protocols
    l2cap_init();
    sdp_init();
    gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_ONLY);
#ifdef ENABLE_BLE
    // Initialize LE Security Manager. Needed for cross-transport key derivation
    sm_init();
    sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
#endif
    goep_client_init();
    avrcp_cover_art_client_init();

    // Init profiles
    a2dp_sink_init();
    avrcp_init();
    avrcp_controller_init();
    avrcp_target_init();

    // Configure A2DP Sink
    a2dp_sink_register_packet_handler(&a2dp_sink_packet_handler);
    a2dp_sink_register_media_handler(&handle_l2cap_media_data_packet);
    a2dp_sink_demo_stream_endpoint_t* stream_endpoint = &a2dp_sink_demo_stream_endpoint;
    avdtp_stream_endpoint_t* local_stream_endpoint = a2dp_sink_create_stream_endpoint(AVDTP_AUDIO,
        AVDTP_CODEC_SBC, media_sbc_codec_capabilities, sizeof(media_sbc_codec_capabilities),
        stream_endpoint->media_sbc_codec_configuration, sizeof(stream_endpoint->media_sbc_codec_configuration));
    btstack_assert(local_stream_endpoint != NULL);
    // - Store stream enpoint's SEP ID, as it is used by A2DP API to identify the stream endpoint
    stream_endpoint->a2dp_local_seid = avdtp_local_seid(local_stream_endpoint);


    // Configure AVRCP Controller + Target
    avrcp_register_packet_handler(&avrcp_packet_handler);
    avrcp_controller_register_packet_handler(&avrcp_controller_packet_handler);
    avrcp_target_register_packet_handler(&avrcp_target_packet_handler);

    // Configure SDP

    // - Create and register A2DP Sink service record
    memset(sdp_avdtp_sink_service_buffer, 0, sizeof(sdp_avdtp_sink_service_buffer));
    a2dp_sink_create_sdp_record(sdp_avdtp_sink_service_buffer, sdp_create_service_record_handle(),
                                AVDTP_SINK_FEATURE_MASK_HEADPHONE, NULL, NULL);
    btstack_assert(de_get_len( sdp_avdtp_sink_service_buffer) <= sizeof(sdp_avdtp_sink_service_buffer));
    sdp_register_service(sdp_avdtp_sink_service_buffer);

    // - Create AVRCP Controller service record and register it with SDP. We send Category 1 commands to the media player, e.g. play/pause
    memset(sdp_avrcp_controller_service_buffer, 0, sizeof(sdp_avrcp_controller_service_buffer));
    uint16_t controller_supported_features = 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_CATEGORY_PLAYER_OR_RECORDER;
#ifdef AVRCP_BROWSING_ENABLED
    controller_supported_features |= 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_BROWSING;
#endif
    controller_supported_features |= 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_COVER_ART_GET_LINKED_THUMBNAIL;
    avrcp_controller_create_sdp_record(sdp_avrcp_controller_service_buffer, sdp_create_service_record_handle(),
                                       controller_supported_features, NULL, NULL);
    sdp_register_service(sdp_avrcp_controller_service_buffer);

    // - Create and register A2DP Sink service record
    //   -  We receive Category 2 commands from the media player, e.g. volume up/down
    memset(sdp_avrcp_target_service_buffer, 0, sizeof(sdp_avrcp_target_service_buffer));
    uint16_t target_supported_features = 1 << AVRCP_TARGET_SUPPORTED_FEATURE_CATEGORY_MONITOR_OR_AMPLIFIER;
    avrcp_target_create_sdp_record(sdp_avrcp_target_service_buffer,
                                   sdp_create_service_record_handle(), target_supported_features, NULL, NULL);
    sdp_register_service(sdp_avrcp_target_service_buffer);

    // - Create and register Device ID (PnP) service record
    memset(device_id_sdp_service_buffer, 0, sizeof(device_id_sdp_service_buffer));
    device_id_create_sdp_record(device_id_sdp_service_buffer,
                                sdp_create_service_record_handle(), DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH,
                                BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
    sdp_register_service(device_id_sdp_service_buffer);


    // Configure GAP - discovery / connection

    // - Set local name with a template Bluetooth address, that will be automatically
    //   replaced with an actual address once it is available, i.e. when BTstack boots
    //   up and starts talking to a Bluetooth module.
    gap_set_local_name(BLUETOOTH_DEVICE_NAME);
    // - Allow to show up in Bluetooth inquiry
    gap_discoverable_control(1);
    // - Set Class of Device - Service Class: Audio, Major Device Class: Audio, Minor: Loudspeaker
    gap_set_class_of_device(0x200414);
    // - Allow for role switch in general and sniff mode
    gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE);
    // - Allow for role switch on outgoing connections
    //   - This allows A2DP Source, e.g. smartphone, to become master when we re-connect to it.
    gap_set_allow_role_switch(true);

    // Register for HCI events
    hci_event_callback_registration.callback = &hci_packet_handler;
    hci_add_event_handler(&hci_event_callback_registration);

    return 0;
}

This is my btstack_config.h

#ifndef _PICO_BTSTACK_BTSTACK_CONFIG_H
#define _PICO_BTSTACK_BTSTACK_CONFIG_H
// BTstack features that can be enabled
#define ENABLE_AVRCP_COVER_ART
#define ENABLE_GOEP_L2CAP
#define ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE
#define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE
#define ENABLE_LE_PERIPHERAL
#define ENABLE_LE_CENTRAL
#define ENABLE_LOG_INFO
#define ENABLE_LOG_ERROR
#define ENABLE_PRINTF_HEXDUMP
#define ENABLE_SCO_OVER_HCI
#define ENABLE_CROSS_TRANSPORT_KEY_DERIVATION
#define ENABLE_LE_SECURE_CONNECTIONS
// BTstack configuration. buffers, sizes, ...
#define HCI_OUTGOING_PRE_BUFFER_SIZE 4
#define HCI_ACL_PAYLOAD_SIZE (1691 + 4)
#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4
#define MAX_NR_AVDTP_CONNECTIONS 1
#define MAX_NR_AVDTP_STREAM_ENDPOINTS 1
#define MAX_NR_AVRCP_CONNECTIONS 2
#define MAX_NR_BNEP_CHANNELS 1
#define MAX_NR_BNEP_SERVICES 1
#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES  2
#define MAX_NR_GATT_CLIENTS 1
#define MAX_NR_HCI_CONNECTIONS 2
#define MAX_NR_HID_HOST_CONNECTIONS 1
#define MAX_NR_HIDS_CLIENTS 1
#define MAX_NR_HFP_CONNECTIONS 1
#define MAX_NR_L2CAP_CHANNELS  5
#define MAX_NR_L2CAP_SERVICES  3
#define MAX_NR_RFCOMM_CHANNELS 1
#define MAX_NR_RFCOMM_MULTIPLEXERS 1
#define MAX_NR_RFCOMM_SERVICES 1
#define MAX_NR_SERVICE_RECORD_ITEMS 4
#define MAX_NR_SM_LOOKUP_ENTRIES 3
#define MAX_NR_WHITELIST_ENTRIES 16
#define MAX_NR_LE_DEVICE_DB_ENTRIES 16
// Limit number of ACL/SCO Buffer to use by stack to avoid cyw43 shared bus overrun
#define MAX_NR_CONTROLLER_ACL_BUFFERS 3
#define MAX_NR_CONTROLLER_SCO_PACKETS 3
// Enable and configure HCI Controller to Host Flow Control to avoid cyw43 shared bus overrun
#define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL
#define HCI_HOST_ACL_PACKET_LEN 1024
#define HCI_HOST_ACL_PACKET_NUM 3
#define HCI_HOST_SCO_PACKET_LEN 120
#define HCI_HOST_SCO_PACKET_NUM 3
// Link Key DB and LE Device DB using TLV on top of Flash Sector interface
#define NVM_NUM_DEVICE_DB_ENTRIES 16
#define NVM_NUM_LINK_KEYS 16
// We don't give btstack a malloc, so use a fixed-size ATT DB.
#define MAX_ATT_DB_SIZE 512
// BTstack HAL configuration
//#define HAVE_EMBEDDED_TIME_MS
// map btstack_assert onto Pico SDK assert()
#define HAVE_ASSERT
// Some USB dongles take longer to respond to HCI reset (e.g. BCM20702A).
#define HCI_RESET_RESEND_TIMEOUT_MS 1000
#define ENABLE_SOFTWARE_AES128
#define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS

#define HAVE_BTSTACK_STDIN

// To get the audio demos working even with HCI dump at 115200, this truncates long ACL packetws
//#define HCI_DUMP_STDOUT_MAX_SIZE_ACL 100

#ifdef ENABLE_CLASSIC
#define ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE
#endif

#endif

Here it's the Drive folder with all the logs1, since Wireshark captures are not supported in GitHub.

As I mentioned, I also tried using only the example code compiled with the latest SDK, and the behavior is the same. I am attaching logs named Example_Pixel or Pixel_Example which refer to the format described in the notes.
The file Pixel_AudioPico_Pairing contains the log of a successful pairing.
The file Pixel_AudioPico_connection_successful_old contains the log of a successful connection between the Pixel and the Pico (also initiated by the Pico) using version 2.2.0.

My final question is: how can I solve this issue? I have also tried all the various security settings in the new version of BTstack, but nothing seems to work.

I tried changing devices by using an iPad running iOS 18.7.9 and a MacBook Air running macOS 26, but the behavior remains the same and the Pico log is practically identical.

Thanks, any help would be greatly appreciated,
Mattia.


  1. The file naming follows this format: Source_Destination_type_of_log e.g. AudioPico_Pixel_connection_failed indicates the log extracted from the Pico trying to connect to the phone, while Pixel_AudioPico indicates the log extracted from the phone trying to connect to the Pico. ↩︎

Hello, I am building a device that streams Bluetooth audio using the A2DP protocol over I2S to a DAC, in order to play music from my phone to a set of speakers. I am using the **Pimoroni Pico 2 W Plus**. For this project, I based my code on the btstack example a2dp_sink_demo.c found in pico-examples. I am using FreeRTOS to manage the various tasks, and I have a dedicated task for the btstack loop. Anyway, the issue I am experiencing can also be reproduced with just the aforementioned example; in fact, the Bluetooth handling code in my project is almost identical. The problem is that I cannot connect to the phone after updating to the latest version of the pico-sdk (v2.3.0), and consequently to btstack version 1.8.2. (*The btstack changelog mentions an increased security level in this release.*) Let me explain further: the pairing between the device and the phone, when initiating the request from the phone, is successful and I am able to connect; however, after a reboot using the following function ```c a2dp_sink_establish_stream(device_addr, &a2dp_sink_demo_a2dp_connection.a2dp_cid); ``` where `device_addr` is the MAC address of my phone a Pixel 8a, as you can see in the log **AudioPico_Pixel_connection_failed.pklg**, it returns: ``` 357 -31622388.284000 0.000000 PKTLOG 59 A2DP Sink : Streaming connection failed, status 0x6a ``` After several tests and attempts, my guess is that the process might be failing after authentication when requesting to raise the security level at the following line: ``` 306 -31622392.284000 0.009000 PKTLOG 91 hci.c.9154: gap_request_security_level requested level 2, planned level 0, current level 0 ``` Looking at the log from the phone's perspective in the **Pixel_AudioPico_connection_failed.pcapng** file, after the line below, the Pico sends the disconnect command. I assume because the security levels do not match, meaning level 2 is requested but the current level is 0: ``` 57 0.970892000 0.049205000 controller host HCI_EVT 12 Rcvd Encryption Change [v2] ``` The same code works with pico-sdk 2.2.0. I noticed another thing: when I compile with the latest SDK version and pair, after rebooting I initially get error **0x66** which is `L2CAP_CONNECTION_RESPONSE_RESULT_REFUSED_SECURITY`,I noticed from the logs that the Link Key was reversed; by executing a function to reverse it and saving it, the error goes back to being **0x6a** or `L2CAP_CONNECTION_BASEBAND_DISCONNECT`. I know the key is correct reversed because if I flash the code compiled with the old sdk it works without need to pair, in fact the keys are saved in flash via the TLV functions. This is my function to initialize the various bluetooth protocols: ```c int setup_demo(void){ // init protocols l2cap_init(); sdp_init(); gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_ONLY); #ifdef ENABLE_BLE // Initialize LE Security Manager. Needed for cross-transport key derivation sm_init(); sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY); #endif goep_client_init(); avrcp_cover_art_client_init(); // Init profiles a2dp_sink_init(); avrcp_init(); avrcp_controller_init(); avrcp_target_init(); // Configure A2DP Sink a2dp_sink_register_packet_handler(&a2dp_sink_packet_handler); a2dp_sink_register_media_handler(&handle_l2cap_media_data_packet); a2dp_sink_demo_stream_endpoint_t* stream_endpoint = &a2dp_sink_demo_stream_endpoint; avdtp_stream_endpoint_t* local_stream_endpoint = a2dp_sink_create_stream_endpoint(AVDTP_AUDIO, AVDTP_CODEC_SBC, media_sbc_codec_capabilities, sizeof(media_sbc_codec_capabilities), stream_endpoint->media_sbc_codec_configuration, sizeof(stream_endpoint->media_sbc_codec_configuration)); btstack_assert(local_stream_endpoint != NULL); // - Store stream enpoint's SEP ID, as it is used by A2DP API to identify the stream endpoint stream_endpoint->a2dp_local_seid = avdtp_local_seid(local_stream_endpoint); // Configure AVRCP Controller + Target avrcp_register_packet_handler(&avrcp_packet_handler); avrcp_controller_register_packet_handler(&avrcp_controller_packet_handler); avrcp_target_register_packet_handler(&avrcp_target_packet_handler); // Configure SDP // - Create and register A2DP Sink service record memset(sdp_avdtp_sink_service_buffer, 0, sizeof(sdp_avdtp_sink_service_buffer)); a2dp_sink_create_sdp_record(sdp_avdtp_sink_service_buffer, sdp_create_service_record_handle(), AVDTP_SINK_FEATURE_MASK_HEADPHONE, NULL, NULL); btstack_assert(de_get_len( sdp_avdtp_sink_service_buffer) <= sizeof(sdp_avdtp_sink_service_buffer)); sdp_register_service(sdp_avdtp_sink_service_buffer); // - Create AVRCP Controller service record and register it with SDP. We send Category 1 commands to the media player, e.g. play/pause memset(sdp_avrcp_controller_service_buffer, 0, sizeof(sdp_avrcp_controller_service_buffer)); uint16_t controller_supported_features = 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_CATEGORY_PLAYER_OR_RECORDER; #ifdef AVRCP_BROWSING_ENABLED controller_supported_features |= 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_BROWSING; #endif controller_supported_features |= 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_COVER_ART_GET_LINKED_THUMBNAIL; avrcp_controller_create_sdp_record(sdp_avrcp_controller_service_buffer, sdp_create_service_record_handle(), controller_supported_features, NULL, NULL); sdp_register_service(sdp_avrcp_controller_service_buffer); // - Create and register A2DP Sink service record // - We receive Category 2 commands from the media player, e.g. volume up/down memset(sdp_avrcp_target_service_buffer, 0, sizeof(sdp_avrcp_target_service_buffer)); uint16_t target_supported_features = 1 << AVRCP_TARGET_SUPPORTED_FEATURE_CATEGORY_MONITOR_OR_AMPLIFIER; avrcp_target_create_sdp_record(sdp_avrcp_target_service_buffer, sdp_create_service_record_handle(), target_supported_features, NULL, NULL); sdp_register_service(sdp_avrcp_target_service_buffer); // - Create and register Device ID (PnP) service record memset(device_id_sdp_service_buffer, 0, sizeof(device_id_sdp_service_buffer)); device_id_create_sdp_record(device_id_sdp_service_buffer, sdp_create_service_record_handle(), DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1); sdp_register_service(device_id_sdp_service_buffer); // Configure GAP - discovery / connection // - Set local name with a template Bluetooth address, that will be automatically // replaced with an actual address once it is available, i.e. when BTstack boots // up and starts talking to a Bluetooth module. gap_set_local_name(BLUETOOTH_DEVICE_NAME); // - Allow to show up in Bluetooth inquiry gap_discoverable_control(1); // - Set Class of Device - Service Class: Audio, Major Device Class: Audio, Minor: Loudspeaker gap_set_class_of_device(0x200414); // - Allow for role switch in general and sniff mode gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE); // - Allow for role switch on outgoing connections // - This allows A2DP Source, e.g. smartphone, to become master when we re-connect to it. gap_set_allow_role_switch(true); // Register for HCI events hci_event_callback_registration.callback = &hci_packet_handler; hci_add_event_handler(&hci_event_callback_registration); return 0; } ``` This is my btstack_config.h ```c #ifndef _PICO_BTSTACK_BTSTACK_CONFIG_H #define _PICO_BTSTACK_BTSTACK_CONFIG_H // BTstack features that can be enabled #define ENABLE_AVRCP_COVER_ART #define ENABLE_GOEP_L2CAP #define ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE #define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE #define ENABLE_LE_PERIPHERAL #define ENABLE_LE_CENTRAL #define ENABLE_LOG_INFO #define ENABLE_LOG_ERROR #define ENABLE_PRINTF_HEXDUMP #define ENABLE_SCO_OVER_HCI #define ENABLE_CROSS_TRANSPORT_KEY_DERIVATION #define ENABLE_LE_SECURE_CONNECTIONS // BTstack configuration. buffers, sizes, ... #define HCI_OUTGOING_PRE_BUFFER_SIZE 4 #define HCI_ACL_PAYLOAD_SIZE (1691 + 4) #define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4 #define MAX_NR_AVDTP_CONNECTIONS 1 #define MAX_NR_AVDTP_STREAM_ENDPOINTS 1 #define MAX_NR_AVRCP_CONNECTIONS 2 #define MAX_NR_BNEP_CHANNELS 1 #define MAX_NR_BNEP_SERVICES 1 #define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2 #define MAX_NR_GATT_CLIENTS 1 #define MAX_NR_HCI_CONNECTIONS 2 #define MAX_NR_HID_HOST_CONNECTIONS 1 #define MAX_NR_HIDS_CLIENTS 1 #define MAX_NR_HFP_CONNECTIONS 1 #define MAX_NR_L2CAP_CHANNELS 5 #define MAX_NR_L2CAP_SERVICES 3 #define MAX_NR_RFCOMM_CHANNELS 1 #define MAX_NR_RFCOMM_MULTIPLEXERS 1 #define MAX_NR_RFCOMM_SERVICES 1 #define MAX_NR_SERVICE_RECORD_ITEMS 4 #define MAX_NR_SM_LOOKUP_ENTRIES 3 #define MAX_NR_WHITELIST_ENTRIES 16 #define MAX_NR_LE_DEVICE_DB_ENTRIES 16 // Limit number of ACL/SCO Buffer to use by stack to avoid cyw43 shared bus overrun #define MAX_NR_CONTROLLER_ACL_BUFFERS 3 #define MAX_NR_CONTROLLER_SCO_PACKETS 3 // Enable and configure HCI Controller to Host Flow Control to avoid cyw43 shared bus overrun #define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL #define HCI_HOST_ACL_PACKET_LEN 1024 #define HCI_HOST_ACL_PACKET_NUM 3 #define HCI_HOST_SCO_PACKET_LEN 120 #define HCI_HOST_SCO_PACKET_NUM 3 // Link Key DB and LE Device DB using TLV on top of Flash Sector interface #define NVM_NUM_DEVICE_DB_ENTRIES 16 #define NVM_NUM_LINK_KEYS 16 // We don't give btstack a malloc, so use a fixed-size ATT DB. #define MAX_ATT_DB_SIZE 512 // BTstack HAL configuration //#define HAVE_EMBEDDED_TIME_MS // map btstack_assert onto Pico SDK assert() #define HAVE_ASSERT // Some USB dongles take longer to respond to HCI reset (e.g. BCM20702A). #define HCI_RESET_RESEND_TIMEOUT_MS 1000 #define ENABLE_SOFTWARE_AES128 #define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS #define HAVE_BTSTACK_STDIN // To get the audio demos working even with HCI dump at 115200, this truncates long ACL packetws //#define HCI_DUMP_STDOUT_MAX_SIZE_ACL 100 #ifdef ENABLE_CLASSIC #define ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE #endif #endif ``` Here it's the [Drive folder](https://drive.google.com/drive/folders/1fha7-_ftXaalwWh8BOi2tbfpZmhLbwtt?usp=sharing) with all the logs[^1], since Wireshark captures are not supported in GitHub. As I mentioned, I also tried using only the example code compiled with the latest SDK, and the behavior is the same. I am attaching logs named **Example_Pixel** or **Pixel_Example** which refer to the format described in the notes. The file **Pixel_AudioPico_Pairing** contains the log of a successful pairing. The file **Pixel_AudioPico_connection_successful_old** contains the log of a successful connection between the Pixel and the Pico (also initiated by the Pico) using version 2.2.0. **My final question is: how can I solve this issue?** I have also tried all the various security settings in the new version of BTstack, but nothing seems to work. I tried changing devices by using an iPad running iOS 18.7.9 and a MacBook Air running macOS 26, but the behavior remains the same and the Pico log is practically identical. Thanks, any help would be greatly appreciated, Mattia. [^1]:*The file naming follows this format: Source_Destination_type_of_log e.g. AudioPico_Pixel_connection_failed indicates the log extracted from the Pico trying to connect to the phone, while Pixel_AudioPico indicates the log extracted from the phone trying to connect to the Pico.*
hiromorii commented 2026-08-16 12:12:29 -05:00 (Migrated from github.com)

Hello,

I've hit the same issue after updating to pico-sdk 2.3.0.
The issue appears to stem from two independent problems, both in btstack.

The first cause is btstack hci_stack->ssp_auto_accept in btstack/src/hci.c defaulting to 0 as of version 1.8.2 - the SSP confirmation reply gets blocked without setting it to 1 explicitly. The default flipped from 1 to 0 specifically between v1.8.1 and v1.8.2.

gap_ssp_set_auto_accept(1);

The second cause appears to lie in btstack's HCI dispatch design: static void hci_run() in btstack/src/hci.c dispatches at most one pending item per call and relies on being re-invoked externally to drain anything further. pico-sdk PR #3042 removed a self-emitted HCI_EVENT_TRANSPORT_PACKET_SENT that had been incidentally providing that re-invocation immediately after every send. The PR itself is a correct fix.

The regression is a side effect: with the self-emit gone, a second pending item that becomes ready right after the first dispatch now has to wait for some other real packet to arrive before hci_run() gets re-invoked. This bites outgoing connections in particular because btstack/src/classic/hid_host.c doesn't request the security level until well after the ACL link is up - so the authenticate -> encrypt -> read-key-size chain kicks off late, in a quiet window with little other packet traffic to accidentally trigger hci_run() again.

I was able to get my Pico working with either of the following approaches:

1. Proactively request security LEVEL_2 when a Pico-initiated connection completes.

case HCI_EVENT_CONNECTION_COMPLETE: {
    if (connection_was_initiated_by_pico) {
        if (hci_event_connection_complete_get_status(packet) == ERROR_CODE_SUCCESS) {
            hci_con_handle_t con_handle = hci_event_connection_complete_get_connection_handle(packet);
            if (gap_security_level(con_handle) < LEVEL_2) {
                gap_request_security_level(con_handle, LEVEL_2);
            }
        }
    }
    break;
}

2. Patch btstack version 1.8.2 hci_run() directly.

This addresses the root cause: rather than depending on external re-invocation, hci_run() drains everything it can in a single call. Apply the diff from within the Pico SDK checkout's lib/btstack directory:

diff --git a/src/hci.c b/src/hci.c
index 9b16d90..5fd4643 100644
--- a/src/hci.c
+++ b/src/hci.c
@@ -8326,49 +8326,53 @@ static void hci_run(void){
 
     bool done;
 
-    // send continuation fragments first, as they block the prepared packet buffer
-    done = hci_run_acl_fragments();
-    if (done) return;
+    while (true) {
+        // send continuation fragments first, as they block the prepared packet buffer
+        done = hci_run_acl_fragments();
+        if (done) continue;
 
 #ifdef ENABLE_LE_ISOCHRONOUS_STREAMS
-    done = hci_run_iso_fragments();
-    if (done) return;
+        done = hci_run_iso_fragments();
+        if (done) continue;
 #endif
 
 #ifdef ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL
-    // send host num completed packets next as they don't require num_cmd_packets > 0
-    if (!hci_can_send_command_packet_transport()) return;
-    if (hci_stack->host_completed_packets){
-        hci_host_num_completed_packets();        
-        return;
-    }
+        // send host num completed packets next as they don't require num_cmd_packets > 0
+        if (!hci_can_send_command_packet_transport()) return;
+        if (hci_stack->host_completed_packets){
+            hci_host_num_completed_packets();        
+            continue;
+        }
 #endif
 
-    if (!hci_can_send_command_packet_now()) return;
-
-    // global/non-connection oriented commands
+        if (!hci_can_send_command_packet_now()) return;
 
+        // global/non-connection oriented commands
 
 #ifdef ENABLE_CLASSIC
-    // general gap classic
-    done = hci_run_general_gap_classic();
-    if (done) return;
+        // general gap classic
+        done = hci_run_general_gap_classic();
+        if (done) continue;
 #endif
 
 #ifdef ENABLE_BLE
-    // general gap le
-    done = hci_run_general_gap_le();
-    if (done) return;
+        // general gap le
+        done = hci_run_general_gap_le();
+        if (done) continue;
 
 #ifdef ENABLE_LE_ISOCHRONOUS_STREAMS
-    // ISO related tasks, e.g. BIG create/terminate/sync
-    done = hci_run_iso_tasks();
-    if (done) return;
+        // ISO related tasks, e.g. BIG create/terminate/sync
+        done = hci_run_iso_tasks();
+        if (done) continue;
 #endif
 #endif
 
-    // send pending HCI commands
-    hci_run_general_pending_commands();
+        // send pending HCI commands
+        done = hci_run_general_pending_commands();
+        if (done) continue;
+
+        return;
+    }
 }
 
 #ifdef ENABLE_CLASSIC

Hope this helps,
- Hiroshi

Hello, I've hit the same issue after updating to pico-sdk 2.3.0. The issue appears to stem from two independent problems, both in btstack. The first cause is btstack `hci_stack->ssp_auto_accept` in `btstack/src/hci.c` defaulting to 0 as of version 1.8.2 - the SSP confirmation reply gets blocked without setting it to 1 explicitly. The default flipped from 1 to 0 specifically between v1.8.1 and v1.8.2. ``` gap_ssp_set_auto_accept(1); ``` The second cause appears to lie in btstack's HCI dispatch design: `static void hci_run()` in `btstack/src/hci.c` dispatches at most one pending item per call and relies on being re-invoked externally to drain anything further. pico-sdk PR #3042 removed a self-emitted HCI_EVENT_TRANSPORT_PACKET_SENT that had been incidentally providing that re-invocation immediately after every send. The PR itself is a correct fix. The regression is a side effect: with the self-emit gone, a second pending item that becomes ready right after the first dispatch now has to wait for some other real packet to arrive before hci_run() gets re-invoked. This bites outgoing connections in particular because `btstack/src/classic/hid_host.c` doesn't request the security level until well after the ACL link is up - so the authenticate -> encrypt -> read-key-size chain kicks off late, in a quiet window with little other packet traffic to accidentally trigger hci_run() again. I was able to get my Pico working with either of the following approaches: **1. Proactively request security LEVEL_2 when a Pico-initiated connection completes.** ``` case HCI_EVENT_CONNECTION_COMPLETE: { if (connection_was_initiated_by_pico) { if (hci_event_connection_complete_get_status(packet) == ERROR_CODE_SUCCESS) { hci_con_handle_t con_handle = hci_event_connection_complete_get_connection_handle(packet); if (gap_security_level(con_handle) < LEVEL_2) { gap_request_security_level(con_handle, LEVEL_2); } } } break; } ``` **2. Patch btstack version 1.8.2 `hci_run()` directly.** This addresses the root cause: rather than depending on external re-invocation, `hci_run()` drains everything it can in a single call. Apply the diff from within the Pico SDK checkout's `lib/btstack` directory: ``` diff --git a/src/hci.c b/src/hci.c index 9b16d90..5fd4643 100644 --- a/src/hci.c +++ b/src/hci.c @@ -8326,49 +8326,53 @@ static void hci_run(void){ bool done; - // send continuation fragments first, as they block the prepared packet buffer - done = hci_run_acl_fragments(); - if (done) return; + while (true) { + // send continuation fragments first, as they block the prepared packet buffer + done = hci_run_acl_fragments(); + if (done) continue; #ifdef ENABLE_LE_ISOCHRONOUS_STREAMS - done = hci_run_iso_fragments(); - if (done) return; + done = hci_run_iso_fragments(); + if (done) continue; #endif #ifdef ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL - // send host num completed packets next as they don't require num_cmd_packets > 0 - if (!hci_can_send_command_packet_transport()) return; - if (hci_stack->host_completed_packets){ - hci_host_num_completed_packets(); - return; - } + // send host num completed packets next as they don't require num_cmd_packets > 0 + if (!hci_can_send_command_packet_transport()) return; + if (hci_stack->host_completed_packets){ + hci_host_num_completed_packets(); + continue; + } #endif - if (!hci_can_send_command_packet_now()) return; - - // global/non-connection oriented commands + if (!hci_can_send_command_packet_now()) return; + // global/non-connection oriented commands #ifdef ENABLE_CLASSIC - // general gap classic - done = hci_run_general_gap_classic(); - if (done) return; + // general gap classic + done = hci_run_general_gap_classic(); + if (done) continue; #endif #ifdef ENABLE_BLE - // general gap le - done = hci_run_general_gap_le(); - if (done) return; + // general gap le + done = hci_run_general_gap_le(); + if (done) continue; #ifdef ENABLE_LE_ISOCHRONOUS_STREAMS - // ISO related tasks, e.g. BIG create/terminate/sync - done = hci_run_iso_tasks(); - if (done) return; + // ISO related tasks, e.g. BIG create/terminate/sync + done = hci_run_iso_tasks(); + if (done) continue; #endif #endif - // send pending HCI commands - hci_run_general_pending_commands(); + // send pending HCI commands + done = hci_run_general_pending_commands(); + if (done) continue; + + return; + } } #ifdef ENABLE_CLASSIC ``` Hope this helps, \- Hiroshi
elrir01 commented 2026-08-16 15:04:46 -05:00 (Migrated from github.com)

Thanks a lot, it works flawlessly!! 😃 I just tested your first approach and it connects right away!
Anyway the definitive solution is to ask to modify btstack or is it intended behaviour to proactively request higher security level?
Mattia.

Thanks a lot, it works flawlessly!! 😃 I just tested your first approach and it connects right away! Anyway the definitive solution is to ask to modify btstack or is it intended behaviour to proactively request higher security level? Mattia.
peterharperuk commented 2026-08-17 06:11:24 -05:00 (Migrated from github.com)

Can you test with develop which has a newer vesion of btstack? https://github.com/raspberrypi/pico-sdk/pull/3123
It looks like this change in 2.3.0 caused a regression https://github.com/raspberrypi/pico-sdk/pull/3042

the key is correct reversed

This looks like a btstack bug. Investigating.

Can you test with develop which has a newer vesion of btstack? https://github.com/raspberrypi/pico-sdk/pull/3123 It looks like this change in 2.3.0 caused a regression https://github.com/raspberrypi/pico-sdk/pull/3042 > the key is correct reversed This looks like a btstack bug. Investigating.
hiromorii commented 2026-08-17 06:39:20 -05:00 (Migrated from github.com)

@Elrir94
Glad to hear it worked for you!

Normally, apps shouldn't need to call gap_request_security_level() themselves. hid_host.c already calls it internally via l2cap_create_channel(..., sec_level=2) when it opens the HID Control L2CAP channel, and from there btstack transitions the security level automatically: it kicks off the authenticate -> encrypt -> read-key-size chain without further app involvement.

On pico-sdk 2.3.0, though, that automatic transition stalls right after encryption completes, specifically at the BONDING_SEND_READ_ENCRYPTION_KEY_SIZE step. The flag gets set, but nothing re-invokes hci_run() to actually dispatch the Read_Encryption_Key_Size command.

I'm not sure whether hci_run()'s external-re-invocation-reliant design was ever meant to be a hard guarantee, but it does appear to be original and unchanged. The "dispatch one item, then return" pattern predates the 2020 refactor and is applied uniformly across the whole function, not something specific to this bonding step. Given that, patching hci_run() to loop instead of return seems like the more logical fix: it addresses the dispatch mechanism itself, not just the bonding step that happened to expose it, so it should also cover any other pending item that becomes ready right after another dispatch, not just BONDING_SEND_READ_ENCRYPTION_KEY_SIZE.

- Hiroshi

@Elrir94 Glad to hear it worked for you! Normally, apps shouldn't need to call `gap_request_security_level()` themselves. `hid_host.c` already calls it internally via `l2cap_create_channel(..., sec_level=2)` when it opens the HID Control L2CAP channel, and from there btstack transitions the security level automatically: it kicks off the authenticate -> encrypt -> read-key-size chain without further app involvement. On pico-sdk 2.3.0, though, that automatic transition stalls right after encryption completes, specifically at the BONDING_SEND_READ_ENCRYPTION_KEY_SIZE step. The flag gets set, but nothing re-invokes `hci_run()` to actually dispatch the Read_Encryption_Key_Size command. I'm not sure whether `hci_run()`'s external-re-invocation-reliant design was ever meant to be a hard guarantee, but it does appear to be original and unchanged. The "dispatch one item, then return" pattern predates the 2020 refactor and is applied uniformly across the whole function, not something specific to this bonding step. Given that, patching `hci_run()` to loop instead of return seems like the more logical fix: it addresses the dispatch mechanism itself, not just the bonding step that happened to expose it, so it should also cover any other pending item that becomes ready right after another dispatch, not just BONDING_SEND_READ_ENCRYPTION_KEY_SIZE. \- Hiroshi
hiromorii commented 2026-08-17 07:53:38 -05:00 (Migrated from github.com)

@peterharperuk
Thank you! The develop fixes the bonding issue. Confirmed that hci.c's hci_schedule_transport_packet_sent() re-emits HCI_EVENT_TRANSPORT_PACKET_SENT by registering a callback via btstack_run_loop_execute_on_main_thread(), which on the Pico gets queued through the async_context worker.

- Hiroshi

@peterharperuk Thank you! The develop fixes the bonding issue. Confirmed that `hci.c`'s `hci_schedule_transport_packet_sent()` re-emits `HCI_EVENT_TRANSPORT_PACKET_SENT` by registering a callback via `btstack_run_loop_execute_on_main_thread()`, which on the Pico gets queued through the async_context worker. \- Hiroshi
peterharperuk commented 2026-08-17 08:00:10 -05:00 (Migrated from github.com)

This fixes the "key is stored reversed" problem for me. https://github.com/bluekitchen/btstack/pull/745

This fixes the "key is stored reversed" problem for me. https://github.com/bluekitchen/btstack/pull/745
elrir01 commented 2026-08-18 03:00:42 -05:00 (Migrated from github.com)

@peterharperuk I just tried the develop branch and,as @hiromorii said, it fixes the bonding problem, without the need for proactively requesting security level 2. However the "reversed key problem" persists and i have to manually reverse it and save again to make it work.

@peterharperuk I just tried the develop branch and,as @hiromorii said, it fixes the bonding problem, without the need for proactively requesting security level 2. However the "reversed key problem" persists and i have to manually reverse it and save again to make it work.
peterharperuk commented 2026-08-18 03:58:51 -05:00 (Migrated from github.com)

You need this fix https://github.com/bluekitchen/btstack/pull/745 It has not been merged yet

You need this fix https://github.com/bluekitchen/btstack/pull/745 It has not been merged yet
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-raspberrypi/pico-sdk#3106
No description provided.