SkillAgentSearch skills...

Kable

Kotlin Asynchronous Bluetooth Low-Energy

Install / Use

/learn @JuulLabs/Kable

README

![badge][badge-android] ![badge][badge-ios] ![badge][badge-js] ![badge][badge-mac] ![badge][badge-jvm] Slack

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.Service filters (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 scanSettings property 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 via close:

peripheral.close()

Once a [Peripheral] is disposed (via close) it can no longer be used (e.g. calling connect will throw IllegalStateException).

[!TIP] launched coroutines from a Peripheral object's scope are permitted to run until Peripheral.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), launch via the CoroutineScope returned from Peripheral.connect (or [Connected] state scope property) 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
View on GitHub
GitHub Stars1.1k
CategoryDevelopment
Updated7h ago
Forks103

Languages

Kotlin

Security Score

100/100

Audited on Mar 20, 2026

No findings