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
- Start advertising.
- Connect from a BLE central.
- Enable notifications on the TX characteristic.
- Send notifications using
att_server_register_can_send_now_callback() / att_server_notify().
- Disconnect from the central.
- Reconnect from the central.
- Enable notifications again.
- 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:
-
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.
-
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.
-
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.
-
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.
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 insideatt_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 toatt_server_register_can_send_now_callback()returnERROR_CODE_COMMAND_DISALLOWED(0x0c,rc=12), suggesting that the callback registration remains queued inatt_server->notification_requests.This is highly reproducible.
Environment
Hardware:
Software:
Observed with:
platform-raspberrypi 1.20.0+sha.aa70b80(latest)framework-arduinopico 1.50601.0+sha.832f2c06(latest)BTstack @ 1.0The 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:
The peripheral sends dummy notification data only through the ATT can-send callback path:
When the callback fires, the firmware sends exactly one notification:
Then it re-arms later from the main loop, not recursively from inside the callback.
The test traffic is intentionally modest:
517514bytes1000 B/sSo this is not an unbounded notification flood.
Reliable Trigger
att_server_register_can_send_now_callback()/att_server_notify().On the second connection, the bug occurs consistently after a few successful sends.
Typical observed sequence:
Example log excerpt:
After this point,
ready_failureskeeps increasing. No laterATT_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:
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:
rc=12isERROR_CODE_COMMAND_DISALLOWED. Inatt_server_request_to_send_notification(), this appears to happen whenbtstack_linked_list_add_tail()refuses to add the same callback object because it is already present inatt_server->notification_requests.Suspected Cause
The initial failure is probably not
rc=12itself.rc=12looks 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:
HCI completed-packets / ACL credit recovery is missing or ignored
hci_can_send_acl_le_packet_now()becomes false after a few sends.HCI_EVENT_NUMBER_OF_COMPLETED_PACKETSor equivalent event does not drive L2CAP again.L2CAP fixed-channel can-send state gets stuck
l2cap_request_can_send_fix_channel_now_event()sets fixed ATT channel waiting state.l2cap_notify_channel_can_send()path does not emitL2CAP_EVENT_CAN_SEND_NOW.ATT dispatch global pending state becomes inconsistent after reconnect
att_dispatch.cuses global waiting/pending state.Stale ATT notification request survives in current connection state
att_server->notification_requests.ERROR_CODE_COMMAND_DISALLOWED.Connection Handle Note
The connection handle is always
0x0040in the logs. This appears to be assigned by the controller in the HCI LE Connection Complete event and simply stored by BTstack.Repeated
0x0040after 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.ERROR_CODE_COMMAND_DISALLOWED(rc=12).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.