Bluetooth Classic¶
Bluetooth Classic (BR/EDR) is still the right answer for high-throughput links: RFCOMM streams, L2CAP channels, SPP devices, older audio accessories and the long tail of industrial equipment that will never speak GATT.
blueLib.discoverClassic(includeRssi = true)
.filterIsInstance<ClassicDiscoveryEvent.DeviceFound>()
.collect { render(it.device, it.rssi) }
blueLib.bond(deviceId).getOrThrow()
val connection = blueLib.connectRfcomm(deviceId, SPP_UUID).getOrThrow()
connection.incoming.collect { bytes -> parse(bytes) }
connection.write(payload)
Discovery¶
- Discovery is exclusive: one at a time, and it monopolises the radio. A collecting coroutine owns it โ
cancelling the collection calls
cancelDiscovery(). ClassicDiscoveryEventreportsStarted,DeviceFound(withrssion Android 11+) andFinished. Below API 30 the RSSI extra is not public API, so BlueLib reportsnullrather than reading a hidden field.- Starting discovery needs
BluetoothOperation.CLASSIC_DISCOVERY, which on Android 12+ isBLUETOOTH_CONNECTand below that alsoACCESS_FINE_LOCATION. - Discovery and an active BR/EDR connection interfere; a link that is throughput-sensitive should be opened after discovery stops.
Bonding¶
bond() waits for the platform's BOND_BONDED state, so a returned success means the bond exists โ not
merely that the request was accepted. See
Permissions and pairing for the Android 16.1/17 bond-loss flow,
BondLossReason and the systemRepairInProgress flag.
Bonded devices are available as a flow:
blueLib.bondedDevices().collect { devices -> render(devices) } // ClassicDevice(name, bondState, uuids, deviceClass)
Reading the list requires BLUETOOTH_CONNECT on Android 12+; a failure is reported as
PermissionMissing in diagnostics rather than an empty list, because an empty list is indistinguishable
from "no devices paired".
RFCOMM sockets¶
val connection = blueLib.connectRfcomm(
deviceId = deviceId,
serviceUuid = BluetoothUuid.fromShort(0x1101), // Serial Port Profile
settings = SocketSettings(serviceName = "BlueLib SPP", secure = true),
).getOrThrow()
secure = trueusescreateRfcommSocketToServiceRecord, which authenticates and encrypts using the link key โ but "secure" has historically meant different things on different stacks.- On Android 16+ (
API 36) you can state the requirement explicitly, and BlueLib usesBluetoothSocketSettingswhen you do:
Below API 36 that combination fails with FeatureUnsupported naming the API level, because silently
downgrading an explicit security requirement is exactly the kind of thing a library must not do.
* connection.incoming is a flow of byte chunks; it stops when the peer closes the link or read fails,
and the failure is reported through diagnostics.
* connection.write() runs on the IO dispatcher and flushes; a broken pipe comes back as Unexpected
with the original IOException in cause.
* connection.close() closes the streams first, then the socket, so a blocking read returns instead of
hanging the reader thread.
L2CAP channels¶
- Needs Android 10 (API 29) for connection-oriented channels; below that you get
FeatureUnsupported. - PSM values outside
1..65535are rejected up front (Android reserves the low range for LE CoC). - Same
SocketSettingsrules as RFCOMM for encryption/authentication on Android 16+.
Which to choose¶
| Requirement | Use |
|---|---|
| Low power, small payloads, background-friendly | BLE + GATT |
| Streams with high throughput (audio, file transfer, serial) | Classic RFCOMM |
| A specific PSM level protocol | Classic L2CAP |
| Both, on one device (dual-mode) | both, but never connect BR/EDR and LE to the same device simultaneously unless the device documents dual-mode support |