Skip to content

BLE ATT notification can-send callback gets stuck after reconnect on Raspberry Pi Pico W / CYW43 #740

Description

@savejeff

Hi. I think i found a Bug with Pico W (2) BLE Notify. it seems the connection state is not fully cleared/reset and causes later connections to break.
im using att_server_register_can_send_now_callback to send notify packages. after disconnect and reconnect these callbacks start to not be called back.

disclaimer: as my left arm is currently in a cast, i had to use Codex to write the issue report.

here also a repo that demonstrates the issue reliably on the second connect: Demo
the Demo also explains how to reproduce the issue

Summary

On Raspberry Pi Pico W / Pico 2 W using BTstack through the Arduino-Pico core, ATT notification transmission can reliably stall after a BLE reconnect.

The failure appears around att_server_register_can_send_now_callback(). On the first connection, the callback is usually called immediately from inside att_server_register_can_send_now_callback() when ACL send credit is available. On the second connection, after a small number of successful notifications, one registration no longer invokes the callback immediately and the callback is never delivered later. All following calls to att_server_register_can_send_now_callback() return ERROR_CODE_COMMAND_DISALLOWED (0x0c, rc=12), suggesting that the callback registration remains queued in att_server->notification_requests.

This is highly reproducible.

Environment

Hardware:

  • Raspberry Pi Pico W / Pico 2 W
  • CYW43 Bluetooth controller

Software:

  • PlatformIO
  • Arduino-Pico core
  • BTstack from Arduino-Pico / Pico SDK package

Observed with:

  • platform-raspberrypi 1.20.0+sha.aa70b80 (latest)
  • framework-arduinopico 1.50601.0+sha.832f2c06 (latest)
  • BTstack dependency reported as BTstack @ 1.0

The same behavior was also observed on an older Arduino-Pico core before updating.

Reproducer Shape

The firmware exposes a simple Nordic-UART-like GATT service:

  • TX characteristic: notify
  • RX characteristic: write / write without response (optional)

The peripheral sends dummy notification data only through the ATT can-send callback path:

att_server_register_can_send_now_callback(&tx_ready_callback_registration, connection_handle);

When the callback fires, the firmware sends exactly one notification:

att_server_notify(connection_handle, notify_char_handle, payload, payload_len);

Then it re-arms later from the main loop, not recursively from inside the callback.

The test traffic is intentionally modest:

  • Requested ATT MTU: 517
  • Max notification payload: 514 bytes
  • Offered notification payload rate: about 1000 B/s
  • Pending TX debt capped to one ATT payload
  • One notification per can-send callback

So this is not an unbounded notification flood.

Reliable Trigger

  1. Start advertising.
  2. Connect from a BLE central.
  3. Enable notifications on the TX characteristic.
  4. Send notifications using att_server_register_can_send_now_callback() / att_server_notify().
  5. Disconnect from the central.
  6. Reconnect from the central.
  7. Enable notifications again.
  8. Continue the same notification flow.

On the second connection, the bug occurs consistently after a few successful sends.

Typical observed sequence:

cycle 1:
  connect
  MTU exchange
  notifications enabled
  notification stream works
  central disconnects
  firmware receives notify disabled
  firmware receives device disconnected / ATT disconnected

cycle 2:
  reconnect
  MTU exchange
  notifications enabled
  first few notifications work
  then can-send callback registration stops producing a callback
  every later registration returns rc=12
  no more notifications
  firmware remains in stale conn=1/sub=1 state
  later reconnect attempts time out

Example log excerpt:

device: connected #2, handle=0x0040
mtu: notify_payload=514, requested_by_test=517
notify: enabled

... first few notifications succeed ...

tx: ready_request_failed rc=12, pending=514

status: conn=1, sub=1, handle=0x0040, payload=514,
        pending=514, ready_pending=0, ready_events=4,
        sent_bytes=1572, sent_packets=4,
        failures=0, ready_failures=10

tx: ready_request_failed rc=12, pending=514
tx: ready_request_failed rc=12, pending=514
...

After this point, ready_failures keeps increasing. No later ATT_EVENT_DISCONNECTED / device disconnected event is observed for the stalled connection.

Observed Callback Behavior

On the first connection, att_server_register_can_send_now_callback() often calls the registered callback synchronously before returning.

The path appears to be:

att_server_register_can_send_now_callback()
  -> att_server_request_to_send_notification()
    -> btstack_linked_list_add_tail(&att_server->notification_requests, callback)
    -> att_server_request_can_send_now()
      -> att_dispatch_server_request_can_send_now_event()
        -> l2cap_request_can_send_fix_channel_now_event()
          -> l2cap_notify_channel_can_send()
            -> l2cap_channel_ready_to_send()
            -> l2cap_channel_trigger_send()
              -> L2CAP_EVENT_CAN_SEND_NOW
                -> att_server_handle_can_send_now()
                  -> remove callback from notification_requests
                  -> callback()

That immediate callback behavior seems expected when LE ACL send credit is available.

After reconnect, the first few notifications still work. Then one registration appears to be queued but not delivered:

att_server_register_can_send_now_callback()
  -> callback is added to notification_requests
  -> no immediate callback, presumably no ACL credit at that instant
  -> expected later L2CAP_EVENT_CAN_SEND_NOW never arrives
  -> callback remains queued
  -> next registration with same callback object returns rc=12

rc=12 is ERROR_CODE_COMMAND_DISALLOWED. In att_server_request_to_send_notification(), this appears to happen when btstack_linked_list_add_tail() refuses to add the same callback object because it is already present in att_server->notification_requests.

Suspected Cause

The initial failure is probably not rc=12 itself. rc=12 looks like the aftermath.

The suspected root issue is that after reconnect, a queued ATT notification can-send request is not later released by the expected L2CAP/HCI can-send path.

Possible causes:

  1. HCI completed-packets / ACL credit recovery is missing or ignored

    • hci_can_send_acl_le_packet_now() becomes false after a few sends.
    • BTstack queues the notification callback.
    • The expected HCI_EVENT_NUMBER_OF_COMPLETED_PACKETS or equivalent event does not drive L2CAP again.
    • The queued callback remains stuck.
  2. L2CAP fixed-channel can-send state gets stuck

    • l2cap_request_can_send_fix_channel_now_event() sets fixed ATT channel waiting state.
    • The later l2cap_notify_channel_can_send() path does not emit L2CAP_EVENT_CAN_SEND_NOW.
  3. ATT dispatch global pending state becomes inconsistent after reconnect

    • For fixed ATT, att_dispatch.c uses global waiting/pending state.
    • After reconnect, this state may prevent another can-send event from being requested or delivered.
  4. Stale ATT notification request survives in current connection state

    • The callback remains in att_server->notification_requests.
    • Subsequent registration attempts fail with ERROR_CODE_COMMAND_DISALLOWED.

Connection Handle Note

The connection handle is always 0x0040 in the logs. This appears to be assigned by the controller in the HCI LE Connection Complete event and simply stored by BTstack.

Repeated 0x0040 after a clean disconnect is probably valid controller behavior, since HCI connection handles only need to be unique among active connections.

However, the repeated handle makes it important to confirm that all per-connection ATT/L2CAP/HCI state is fully reset on disconnect/reconnect.

Expected Behavior

If att_server_register_can_send_now_callback() successfully queues a callback and the connection remains valid, BTstack should eventually invoke the callback once ACL send credit becomes available.

If the connection is disconnected before the callback can be invoked, the queued request should be cleaned up as part of connection teardown, and later reconnects should not inherit stale notification request state.

Actual Behavior

After reconnect:

  • att_server_register_can_send_now_callback() succeeds a few times.
  • Then one callback registration remains queued.
  • The callback is never invoked.
  • Further registration attempts return ERROR_CODE_COMMAND_DISALLOWED (rc=12).
  • No more notifications are sent.
  • Firmware continues to report connected/subscribed state.
  • Later central reconnect attempts time out.

Minimal Reproducer

I have a reduced single-file repro that uses direct Arduino-Pico / BTstack calls. It creates a simple notify/write GATT service and drives notifications only through the ATT can-send callback mechanism.

I can provide the complete repro and logs if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions