Kotlin Asynchronous Bluetooth Low-Energy
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.
To scan for nearby peripherals, the
Scannerprovides an
advertisementswhich is a stream of
Advertisementobjects representing advertisements seen from nearby peripherals.
Advertisementobjects contain information such as the peripheral's name and RSSI (signal strength).
advertisementsis collected and stops when the
Flowcollection is terminated. A
Flowterminal operator (such as
first) may be used to scan until an advertisement is found that matches a desired predicate.
val advertisement = Scanner() .advertisements .first { it.name?.startsWith("Example") }
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
Advertisementis obtained, it can be converted to a
Peripheralvia the
CoroutineScope.peripheralextension function.
Peripheralobjects represent actions that can be performed against a remote peripheral, such as connection handling and I/O operations.
val peripheral = scope.peripheral(advertisement)
On JavaScript, rather than processing a stream of advertisements, a specific peripheral can be requested using the
CoroutineScope.requestPeripheralextension function. Criteria (
Options) such as expected service UUIDs on the peripheral and/or the peripheral's name may be specified. When
requestPeripheralis called with the specified options, the browser shows the user a list of peripherals matching the criteria. The peripheral chosen by the user is then returned (as a
Peripheralobject).
val options = Options( optionalServices = arrayOf( "f000aa80-0451-4000-b000-000000000000", "f000aa81-0451-4000-b000-000000000000" ), filters = arrayOf( NamePrefix("Example") ) ) val peripheral = scope.requestPeripheral(options).await()
Peripheralobject is acquired, a connection can be established via the
connectfunction. The
connectmethod suspends until a connection is established and ready (or a failure occurs). A connection is considered ready when connected, services have been discovered, and observations (if any) have been re-wired. Service discovery occurs automatically upon connection.
connectwill all suspend until connection is ready.
peripheral.connect()
disconnectfunction will disconnect an active connection, or cancel an in-flight connection attempt. The
disconnectfunction suspends until the peripheral has settled on a disconnected state.
peripheral.disconnect()
If the underlying subsystem fails to deliver the disconnected state then the
disconnectcall could potentially stall indefinitely. To prevent this (and ensure underlying resources are cleaned up in a timely manner) it is recommended that
disconnectbe wrapped with a timeout, for example:
// Allow 5 seconds for graceful disconnect before forcefully closing `Peripheral`. withTimeoutOrNull(5_000L) { peripheral.disconnect() }
Peripheralcan be monitored via its
state.
peripheral.state.collect { state -> // Display and/or process the connection state. }
statewill typically transition through the following
States:
Disconnectingstate only occurs on Android platform. JavaScript and Apple-based platforms transition directly from
Connectedto
Disconnected.
Bluetooth Low Energy devices are organized into a tree-like structure of services, characteristics and descriptors; whereas characteristics and descriptors have the capability of being read from, or written to.
For example, a peripheral might have the following structure:
00001815-0000-1000-8000-00805f9b34fb)
00002a56-0000-1000-8000-00805f9b34fb)
00002902-0000-1000-8000-00805f9b34fb)
To access a characteristic or descriptor, use the
charactisticOfor
descriptorOffunctions, respectively.
In the above example, to access "Descriptor D2":
val descriptor = descriptorOf( service = "00001815-0000-1000-8000-00805f9b34fb", characteristic = "00002a56-0000-1000-8000-00805f9b34fb", descriptor = "00002902-0000-1000-8000-00805f9b34fb" )
Once connected, data can be read from, or written to, characteristics and/or descriptors via
readand
writefunctions.
readand
writefunctions throw
NotReadyExceptionuntil a connection is established.
val data = peripheral.read(characteristic)peripheral.write(descriptor, byteArrayOf(1, 2, 3))
Bluetooth Low Energy provides the capability of subscribing to characteristic changes by means of notifications, whereas a characteristic change on a connected peripheral is "pushed" to the central via a characteristic notification which carries the new value of the characteristic.
Characteristic change notifications can be observed/subscribed to via the
observefunction which returns a
Flowof the new characteristic data.
val observation = peripheral.observe(characteristic) observation.collect { data -> // Process data. }
observefunction can be called (and its returned
Flowcan be collected) prior to a connection being established. Once a connection is established then characteristic changes will stream from the
Flow. If the connection drops, the
Flowwill remain active, and upon reconnecting it will resume streaming characteristic changes.
Failures related to notifications are propagated via
connectif the
observeis collected prior to a connection being established. If a connection is already established when an
observeis beginning to be collected, then notification failures are propagated via the
observe.
Peripheral objects/connections are scoped to a Coroutine scope. When creating a
Peripheral, the
CoroutineScope.peripheralextension function is used, which scopes the returned
Peripheralto the
CoroutineScopereceiver. If the
CoroutineScopereceiver is cancelled then the
Peripheralwill disconnect and be disposed.
Scanner() .advertisements .filter { advertisement -> advertisement.name?.startsWith("Example") } .map { advertisement -> scope.peripheral(advertisement) } .onEach { peripheral -> peripheral.connect() } .launchIn(scope)delay(60_000L) scope.cancel() // All
peripherals
will implicitly disconnect and be disposed.
Peripheral.disconnectis the preferred method of disconnecting peripherals, but disposal via Coroutine scope cancellation is provided to prevent connection leaks.
Kable can be configured via Gradle Kotlin DSL as follows:
plugins { id("com.android.application") // or id("com.android.library") kotlin("multiplatform") }repositories { jcenter() // or mavenCentral() }
kotlin { android() js().browser() // and/or js().node() macosX64()
sourceSets { val commonMain by getting { dependencies { implementation("com.juul.kable:core:$version") } } }
}
android { // ... }
Note that Apple-based targets (e.g.
macosX64) require Coroutines with multithread support for Kotlin/Native. Kable is configured to use
-native-mtas a transitive dependency for Apple-based targets.
repositories { jcenter() // or mavenCentral() }dependencies { implementation("com.juul.kable:core-$platform:$version") }
Where
$platformrepresents (should be replaced with) the desired platform dependency (e.g.
android).
Copyright 2020 JUUL Labs, Inc.Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.