Skip to content

Add timeout for write-without-response#1186

Merged
twyatt merged 4 commits into
JuulLabs:mainfrom
LuoPeiQin:fix_ios_peripheralIsReady_error
Jul 8, 2026
Merged

Add timeout for write-without-response#1186
twyatt merged 4 commits into
JuulLabs:mainfrom
LuoPeiQin:fix_ios_peripheralIsReady_error

Conversation

@LuoPeiQin

@LuoPeiQin LuoPeiQin commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

On some iOS versions/chipsets, the callback may be delayed or never fired, causing the write pipeline to stall indefinitely. Also reset canSendWriteWithoutResponse on disconnect to avoid stale state.

Closes #1185


Note

Medium Risk
Changes timing and fallback behavior for BLE writes without response; timeout proceeds with writes that might have been deferred, though that is the intended tradeoff to avoid permanent stalls.

Overview
Fixes indefinite stalls when issuing write-without-response on Apple targets if Core Bluetooth never invokes peripheralIsReadyToSendWriteWithoutResponse (reported on some iOS versions/chipsets).

When the peripheral is not ready, the code still waits on canSendWriteWithoutResponse, but now wraps that wait in a 2 second withTimeout. On timeout it logs a warning and continues with the write instead of blocking forever.

On disconnect, PeripheralDelegate.close() now sets canSendWriteWithoutResponse back to true so a stale “not ready” flag does not carry over to the next connection.

Reviewed by Cursor Bugbot for commit 75588c2. Configure here.

…ReadyToSendWriteWithoutResponse is not called。

On some iOS versions/chipsets, the callback may be delayed or never fired, causing the write pipeline
to stall indefinitely. Also reset canSendWriteWithoutResponse on disconnect to avoid stale state.
@LuoPeiQin LuoPeiQin requested review from a team and twyatt as code owners June 30, 2026 07:05
@LuoPeiQin LuoPeiQin requested a review from sdonn3 June 30, 2026 07:05

@twyatt twyatt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LuoPeiQin do you happen to know if there is any official documentation around this as an issue with Core Bluetooth? I'm worried this approach will cause loss of data for some consumers of Kable, and I'd love to see more information about why this happens and if it is a known bug with Core Bluetooth.

@LuoPeiQin

LuoPeiQin commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@LuoPeiQin do you happen to know if there is any official documentation around this as an issue with Core Bluetooth? I'm worried this approach will cause loss of data for some consumers of Kable, and I'd love to see more information about why this happens and if it is a known bug with Core Bluetooth.

@twyatt Yes — this is a well-documented, long-standing CoreBluetooth issue. Here are the three most relevant references:


1. Apple Developer Forums — Apple engineer confirmed callback is not reliably delivered

An Apple engineer confirmed that the system will not wake up an app to deliver peripheralIsReady(toSendWriteWithoutResponse:), even when the app has BLE background mode enabled and other delegate callbacks (like didUpdateValue) continue to fire normally.

"This is by design. The system will not wake up an app in order to deliver peripheralIsReady(toSendWriteWithoutResponse:)."

The reporter also observed that writing directly (ignoring canSendWriteWithoutResponse) works fine — proving the flag and callback are unreliable while the underlying transport is still functional.

2. NordicSemiconductor/IOS-nRF-Connect-Device-Manager — Official workaround in production

Nordic's official iOS BLE library (McuMgrBleTransport) uses a blocking delay loop on canSendWriteWithoutResponse as standard practice, and explicitly overrides the "peripheral not ready" state:

PR #354: "Fix: Override Peripheral Is Not Ready For Write Without Response"
PR #348: Extended timeout from 5s to 10s to handle delayed/missing callbacks.

If the industry's leading BLE chip vendor treats this callback as unreliable in their own production library, it validates the need for a timeout fallback.

3. flutter_blue_plus Issue #1331 (May 2026) — Bug confirmed on latest iOS

On iOS 26.4.2 and iOS 26.5, canSendWriteWithoutResponse stays false after the first successful write. Small payloads (≤20 bytes), low frequency (1 packet/sec) — not a flooding issue. The write pipeline stalls until the 25s timeout fires.

This confirms the problem still exists on the latest iOS versions and is not a legacy-only issue.


Regarding data loss

The timeout fallback does not introduce new data loss risk:

  1. Write-without-response does not guarantee delivery at the BLE protocol level. CoreBluetooth silently drops packets when the internal buffer is full — no error, no callback (documented by tinygo-org/bluetooth PR #386).

  2. The timeout only activates when the callback is broken. In the normal case, peripheralIsReadyToSendWriteWithoutResponse fires within 1-2 connection intervals (15-30ms) and the write proceeds via the callback path. The 30ms timeout is approximately two connection intervals — long enough for the normal callback to arrive, short enough to avoid stalling the pipeline.

  3. Without the timeout, the write pipeline stalls indefinitely. This is strictly worse — the entire BLE channel becomes permanently blocked until the app is killed or the device reconnects.

  4. Resetting canSendWriteWithoutResponse on disconnect prevents stale false state from blocking writes on the next connection — a scenario confirmed by RxBluetoothKit #388 (still open, last activity Jan 2026).

@LuoPeiQin

Copy link
Copy Markdown
Contributor Author

@twyatt @sdonn3 Hello, this is a very good repository, thank you for your contributions, I would also like to contribute to it. Is the information I have provided sufficient, or is there anything I need to add?

@twyatt twyatt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, we should just make the timeout configurable.

Comment thread kable-core/src/appleMain/kotlin/CBPeripheralCoreBluetoothPeripheral.kt Outdated
@twyatt twyatt changed the title Fix #1185:Add timeout fallback for write-without-response when peripheralIs… Add timeout for write-without-response Jul 7, 2026
@twyatt twyatt added apple minor Changes that should bump the MINOR version number labels Jul 7, 2026
Defaults to 30ms (via defaultWriteWithoutResponseTimeout). To disable,
set a sufficiently large value (e.g. Duration.INFINITE). Apple only.

@twyatt twyatt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks for the contribution!

@twyatt twyatt added this to the 0.44.0 milestone Jul 8, 2026
@sdonn3

sdonn3 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

LGTM

@twyatt twyatt enabled auto-merge (squash) July 8, 2026 21:57
@twyatt twyatt merged commit a8e32c0 into JuulLabs:main Jul 8, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apple minor Changes that should bump the MINOR version number

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS writeWithoutResponse gets stuck after first successful write, canSendWriteWithoutResponse stays false

3 participants