Add L2CAP channel support - #1231
Open
torfinnberset wants to merge 1 commit into
Open
Conversation
torfinnberset
force-pushed
the
feature/l2cap-support
branch
from
July 20, 2026 12:50
578ea5d to
974c8d9
Compare
Adds `L2CapSocket`, a connection-oriented channel opened from a connected peripheral: `AndroidPeripheral.openL2CapChannel` / `openInsecureL2CapChannel` and `CoreBluetoothPeripheral.openL2CapChannel`. The socket is a bidirectional byte stream (`read` into a buffer, `write` a packet, `close`) exposing `isConnected` / `hasReachedEof` state; failures surface as `L2CapException`. Android wraps `BluetoothDevice.createL2capChannel`, connecting on `Dispatchers.IO`. Apple confines the `CBL2CAPChannel` NSStreams to a single dedicated run-loop thread (they are not safe to drive from a rotating dispatcher pool) and serializes opens through the connection's guard, since CoreBluetooth's `didOpenL2CAPChannel` callback carries no PSM and so cannot be correlated to a request; a channel delivered to a cancelled or disconnected open is torn down rather than leaked. Closing (or abandoning) a socket must actually free the PSM: CoreBluetooth only disconnects a channel when the CBL2CAPChannel deallocates — closing its streams is not enough — and Kotlin/Native releases Obj-C references at collection time, so teardown drops the channel reference and close() forces a collection. Without that, reopening the same PSM fails with "L2CAP PSM already connected" until an incidental GC runs.
torfinnberset
force-pushed
the
feature/l2cap-support
branch
from
July 27, 2026 06:16
974c8d9 to
58ed1f4
Compare
Author
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 58ed1f4. Configure here.
Member
|
Thanks for the PR! I'll try to find time to review this soon. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adapted from #1023, and completing the implementation for both iOS and Android. Tested on both platforms with real devices, and also from macOS to iOS.
Adds
L2CapSocket, a connection-oriented channel opened from a connected peripheral:AndroidPeripheral.openL2CapChannel/openInsecureL2CapChannelandCoreBluetoothPeripheral.openL2CapChannel. The socket is a bidirectional byte stream (readinto a buffer,writea packet,close) exposingisConnected/hasReachedEofstate; failures surface asL2CapException.Android wraps
BluetoothDevice.createL2capChannel, connecting onDispatchers.IO. Apple confines theCBL2CAPChannelNSStreams to a single dedicated run-loop thread (they are not safe to drive from a rotating dispatcher pool) and serializes opens through the connection's guard, since CoreBluetooth'sdidOpenL2CAPChannelcallback carries no PSM and so cannot be correlated to a request; a channel delivered to a cancelled or disconnected open is torn down rather than leaked.Closes #810 #588 #1023.
Note
Medium Risk
New BLE transport path with substantial Apple concurrency/GC lifecycle logic; failures are mostly additive but incorrect teardown could leave PSMs stuck or leak channels until GC.
Overview
Adds L2CAP CoC alongside GATT: a new
L2CapSocketbyte-stream API andL2CapException, opened from connected platform peripherals after a GATT link exists.On Android (API 29+),
AndroidPeripheralexposes secure and insecure opens viaBluetoothDeviceL2CAP sockets, with I/O onDispatchers.IOand socket cleanup on failed/cancelled connect.On Apple,
CoreBluetoothPeripheral.openL2CapChanneldrivesCBPeripheral.openL2CAPChannel; opens are single-flight under the connection guard becausedidOpenL2CAPChannelhas no PSM.AppleL2CapSocketruns allNSStreamwork on a dedicated run-loop thread, retains/releasesCBL2CAPChannelfor lifecycle (includingGC.collect()onclose()), and abandons channels delivered after cancel/disconnect to avoid leaks or “PSM already connected”.README documents usage; JVM API surface is updated in
kable-core.api.Reviewed by Cursor Bugbot for commit 58ed1f4. Configure here.