Kable
Kotlin Asynchronous Bluetooth Low-Energy
Install / Use
/learn @JuulLabs/KableREADME
![badge][badge-android]
![badge][badge-ios]
![badge][badge-js]
![badge][badge-mac]
![badge][badge-jvm]
Kable
Kotlin Asynchronous Bluetooth Low Energy provides a simple Coroutines-powered API for interacting with Bluetooth Low Energy devices.
Usage is demonstrated with the [SensorTag sample app].
UUIDs
UUIDs (Universally Unique Identifiers) are used to uniquely identify various components of a
Bluetooth Low Energy device. The Bluetooth Base UUID (00000000-0000-1000-8000-00805F9B34FB) allows
for short form (16-bit or 32-bit) UUIDs which are reserved for standard, predefined components
(e.g. 0x180D for "Heart Rate Service", or 0x2A37 for "Heart Rate Measurement").
128-bit UUIDs outside of the Bluetooth Base UUID are typically used for custom applications.
The Bluetooth.BaseUuid is provided to simplify defining 16-bit or 32-bit UUIDs. Simply add (+)
a 16-bit or 32-bit UUID (in [Int] or [Long] form) to the Bluetooth Base UUID to get a "full"
[Uuid] representation; for example:
val uuid16bit = 0x180D
val heartRateServiceUuid = Bluetooth.BaseUuid + uuid16bit
println(heartRateServiceUuid) // Output: 0000180d-0000-1000-8000-00805f9b34fb
Web Bluetooth named UUIDs may also be used to acquire [Uuid]s via the following [Uuid] extension
functions:
Uuid.service(name: String)Uuid.characteristic(name: String)Uuid.descriptor(name: String)
For example:
val heartRateServiceUuid = Uuid.service("heart_rate")
println(heartRateServiceUuid) // Output: 0000180d-0000-1000-8000-00805f9b34fb
[!NOTE] List of known UUID names can be found in
Uuid.kt.
Additional example shorthand notations:
| Shorthand | Canonical UUID |
|-----------------------------------|----------------------------------------|
| Bluetooth.BaseUuid + 0x180D | 0000180D-0000-1000-8000-00805F9B34FB |
| Bluetooth.BaseUuid + 0x12345678 | 12345678-0000-1000-8000-00805F9B34FB |
| Uuid.service("blood_pressure") | 00001810-0000-1000-8000-00805F9B34FB |
| Uuid.characteristic("altitude") | 00002AB3-0000-1000-8000-00805F9B34FB |
| Uuid.descriptor("valid_range") | 00002906-0000-1000-8000-00805F9B34FB |
Scanning
To scan for nearby peripherals, the [Scanner] provides an [advertisements] [Flow] which is a stream of
[Advertisement] objects representing advertisements seen from nearby peripherals. [Advertisement] objects contain
information such as the peripheral's name and RSSI (signal strength).
The [Scanner] may be configured via the following DSL (shown are defaults, when not specified):
val scanner = Scanner {
filters {
match {
name = Filter.Name.Exact("My device")
}
}
logging {
engine = SystemLogEngine
level = Warnings
format = Multiline
}
}
Scan results can be filtered by providing a list of [Filter]s via the filters DSL.
The following filters are supported:
| Filter | Android | Apple | JavaScript | JVM |
|--------------------|:-------------:|:-------------:|:----------:|:-------------:|
| Service | ✓ | ✓<sup>2</sup> | ✓ | ✓<sup>1</sup> |
| Name | ✓ | ✓<sup>1</sup> | ✓ | ✓<sup>1</sup> |
| NamePrefix | ✓<sup>1</sup> | ✓<sup>1</sup> | ✓ | ✓<sup>1</sup> |
| Address | ✓ | | | |
| ManufacturerData | ✓ | ✓<sup>1</sup> | ✓ | ✓<sup>1</sup> |
✓ Supported natively
✓<sup>1</sup> Support provided by Kable via flow filter
✓<sup>2</sup> Supported natively if the only filter type used, otherwise falls back to flow filter
[!TIP] When a filter is supported natively, the system will often be able to perform scan optimizations. If feasible, it is recommended to provide only
Filter.Servicefilters (and at least one) — as it is natively supported on all platforms.
When filters are specified, only [Advertisement]s that match at least one [Filter] will be emitted. For example, if
you had the following peripherals nearby when performing a scan:
| ID | Name | Services |
|:--:|-------------|------------------------------------------------------------------------------------|
| D1 | "SensorTag" | 0000aa80-0000-1000-8000-00805f9b34fb |
| D2 | | f484e2db-2efa-4b58-96be-f89372a3ef82 |
| D3 | "Example" | 8d7798c7-15bd-493f-a935-785305946870,<br/>67bebb9e-6372-4de6-a7bf-e0384583929e |
To have peripherals D1 and D3 emitted during a scan, you could use the following filters:
val scanner = Scanner {
filters {
match {
services = listOf(Bluetooth.BaseUuid + 0xaa80) // SensorTag
}
match {
name = Filter.Name.Prefix("Ex")
}
}
}
Scanning begins when the [advertisements] [Flow] is collected and stops when the [Flow] collection is terminated.
A [Flow] terminal operator (such as [first]) may be used to scan until (for example) the first advertisement is
found matching the specified filters:
val advertisement = Scanner {
filters {
match {
name = Filter.Name.Exact("Example")
}
}
}.advertisements.first()
Android
Android offers additional settings to customize scanning. They are available via the scanSettings property in the
[Scanner] builder DSL. Simply set scanSettings property to an Android [ScanSettings] object, for example:
val scanner = Scanner {
scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
}
[!NOTE] The
scanSettingsproperty is only available on Android and is considered a Kable obsolete API, meaning it will be removed when a DSL specific API becomes available.
JavaScript
Scanning for nearby peripherals is supported, but only available on Chrome 79+ with "Experimental Web Platform
features" enabled via: chrome://flags/#enable-experimental-web-platform-features
Peripheral
Once an [Advertisement] is obtained, it can be converted to a [Peripheral] via the Peripheral builder function:
val peripheral = Peripheral(advertisement) {
// Configure peripheral.
}
[Peripheral] objects represent actions that can be performed against a remote peripheral, such as connection
handling and I/O operations. [Peripheral] objects provide a [CoroutineScope] scope property, and coroutines
can be launched from it:
peripheral.scope.launch {
// Long running task that will be cancelled when peripheral is disposed
// (i.e. `peripheral.close()` is called).
}
[!IMPORTANT] When a [
Peripheral] is no longer needed, it should be disposed viaclose:peripheral.close()Once a [
Peripheral] is disposed (viaclose) it can no longer be used (e.g. callingconnectwill throwIllegalStateException).
[!TIP]
launched coroutines from aPeripheralobject'sscopeare permitted to run untilPeripheral.dispose()is called (i.e. can span across reconnects); for tasks that should only run for the duration of a single connection (i.e. shutdown on disconnect),launchvia theCoroutineScopereturned fromPeripheral.connect(or [Connected] statescopeproperty) instead.
Configuration
Logging
By default, Kable only logs a small number of warnings when unexpected failures occur. To aid in debugging, additional
logging may be enabled and configured via the logging DSL, for example:
val peripheral = Peripheral(advertisement) {
logging {
level = Events // or Data
}
}
The available log levels are:
Warnings: Logs warnings when unexpected failures occur (default)Events: Sam
