Kable Documentation
repository·main·Indexed 22 days ago
https://github.com/juullabs/kableA Kotlin-first, Coroutines-powered library for interacting with Bluetooth Low Energy (BLE) devices across multiple platforms, including Android, iOS, JavaScript, macOS, and JVM. Kable provides a unified API for scanning peripherals, managing connections, and performing I/O operations on services, characteristics, and descriptors.
What's inside Kable
- Kable (Kotlin Asynchronous Bluetooth Low Energy) is a library providing a Coroutines-powered API for interacting with Bluetooth Low Energy (BLE) devices. It is designed to work across multiple platforms including Android, iOS, JavaScript, macOS, and JVM.
How to work with UUIDs in Kable
mainBluetooth Low Energy uses UUIDs to identify services, characteristics, and descriptors. Kable provides several ways to handle them:
- Short-form UUIDs: You can create a full 128-bit
Uuidby adding a 16-bit or 32-bit integer toBluetooth.BaseUuid. - Web Bluetooth Names: You can use extension functions to resolve UUIDs from human-readable strings (e.g.,
Uuid.service("heart_rate")).
Shorthand Reference Table:
Shorthand Canonical UUID Bluetooth.BaseUuid + 0x180D0000180D-0000-1000-8000-00805F9B34FBBluetooth.BaseUuid + 0x1234567812345678-0000-1000-8000-00805F9B34FBUuid.service("blood_pressure")00001810-0000-1000-8000-00805F9B34FBUuid.characteristic("altitude")00002AB3-0000-1000-8000-00805F9B34FBUuid.descriptor("valid_range")00002906-0000-1000-8000-00805F9B34FBval uuid16bit = 0x180D val heartRateServiceUuid = Bluetooth.BaseUuid + uuid16bit println(heartRateServiceUuid) // Output: 0000180d-0000-1000-8000-00805f9b34fb val heartRateServiceUuidByName = Uuid.service("heart_rate") println(heartRateServiceUuidByName) // Output: 0000180d-0000-1000-8000-00805f9b34fb- Short-form UUIDs: You can create a full 128-bit
How Peripheral connection states work
mainA
Peripheraltransitions through various connection states which can be observed via thestateFlow.Important Note on Apple/JavaScript: The
Disconnectingstate is skipped on Apple and JavaScript platforms when the connection closure is initiated by the peripheral itself or if the peripheral goes out of range.Create and manage a Peripheral
mainA
Peripheralrepresents a remote Bluetooth device and provides methods for connection handling and I/O operations. You create aPeripheralfrom anAdvertisementusing thePeripheralbuilder function.Lifecycle and Coroutines
- Scope: A
Peripheralprovides aCoroutineScopevia itsscopeproperty. Use this for long-running tasks that should persist across reconnections but be cancelled when the peripheral is disposed. - Connection-specific tasks: For tasks that should only run while a single connection is active (and shut down on disconnect), use the
CoroutineScopereturned fromPeripheral.connect()or thescopeproperty of theConnectedstate. - Disposal: You must call
peripheral.close()when the peripheral is no longer needed. Once closed, the peripheral cannot be reused (e.g., callingconnectwill throw anIllegalStateException).
val peripheral = Peripheral(advertisement) { // Configure peripheral } // Long running task tied to peripheral lifecycle peripheral.scope.launch { // ... } // Cleanup peripheral.close()- Scope: A
Access characteristics and descriptors
mainBLE devices use a tree structure of Services, Characteristics, and Descriptors. You can access these in two ways:
- Lazy Search (
characteristicOf/descriptorOf): These functions search the GATT profile for the first match based on the provided UUIDs. When performing I/O, the search also respects the required properties (e.g., searching for a characteristic that supportswriteWithoutResponse). - Manual Traversal (
Peripheral.services): You can traverse the discovered services tree. This is useful if multiple items share the same UUID. Warning: Objects obtained viaPeripheral.serviceshold strong references to platform types; ensure you remove references when they are no longer needed to avoid leaks.
Note:
readandwriteoperations will throwNotConnectedExceptionif the peripheral is not connected.// Lazy access to a descriptor val descriptor = descriptorOf( service = Bluetooth.BaseUuid + 0x1815, characteristic = Bluetooth.BaseUuid + 0x2A56, descriptor = Uuid.descriptor("gatt.client_characteristic_configuration"), ) // Manual traversal val services = peripheral.services.value ?: error("Services not discovered") val descriptor = services .first { it.serviceUuid == Bluetooth.BaseUuid + 0x1815 } .characteristics .first { it.characteristicUuid == Bluetooth.BaseUuid + 0x2A56 } .descriptors .first { it.descriptorUuid == Uuid.descriptor("gatt.client_characteristic_configuration") }- Lazy Search (
Build and run SensorTag for Desktop JVM (Linux, Mac, Windows)
mainThe Desktop JVM version can be run on Linux, Mac, or Windows using the following commands:
# Linux or Mac ./gradlew :shared:run # Windows .\gradlew.bat :shared:runBuild and run SensorTag for MacOS
mainThe MacOS console app automatically scans for nearby [SensorTag] peripherals upon launch. It connects to the first one found and subscribes to the gyroscope, printing values to the console.
Choose the command based on your Mac's CPU architecture:
# For Intel Macs ./gradlew :shared:runDebugExecutableMacosX64 # For Apple Silicon (e.g., M1, M2) ./gradlew :shared:runDebugExecutableMacosArm64Install Kable via Gradle
mainAdd Kable to your Kotlin Multiplatform project. For Android, it is recommended to use the
kable-default-permissionsartifact to simplify permission handling.kotlin { sourceSets { commonMain.dependencies { api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") implementation("com.juul.kable:kable-core:${kableVersion}") } androidMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}") implementation("com.juul.kable:kable-default-permissions:${kableVersion}") // Optional } } }Build and install the SensorTag Android app
mainThe SensorTag sample app for Android can be built and installed using Android Studio or via the command line using the Gradle wrapper.
./gradlew :android:installDebugScan for peripherals using Scanner
mainThe
Scannerprovides anadvertisementsFlowthat emitsAdvertisementobjects (containing name and RSSI) as they are seen.Configuration
You can configure the
Scannerusing a DSL to definefiltersandloggingsettings.Filtering
Scan results are filtered by providing
Filters. OnlyAdvertisements matching at least one filter will be emitted. Supported filters include:Service(Recommended for optimization)NameNamePrefixAddress(Android only)ManufacturerData
Note: Providing
Filter.Serviceis recommended as it is natively supported on all platforms and allows for system-level scan optimizations.Lifecycle
Scanning begins when the
advertisementsFlowis collected and stops when the collection is terminated.// Scan until the first advertisement matching a specific name is found val advertisement = Scanner { filters { match { name = Filter.Name.Exact("Example") } } }.advertisements.first()Request a Peripheral in JavaScript
mainIn JavaScript environments, instead of processing a stream of advertisements, you can use
requestPeripheral(options)to trigger a browser-native device picker.Usage Requirements
- The browser will show a list of peripherals matching your
filters. - Crucial: If you use name filters, you must also include any services you intend to access in the
optionalServiceslist. Otherwise, the origin will not have permission to access those services after the user selects the device. - If the user cancels the dialog,
requestPeripheralreturnsnull.
val options = Options { filters { match { name = Filter.Name.Prefix("Example") } } optionalServices = listOf( Uuid.parse("f000aa80-0451-4000-b000-000000000000"), Uuid.parse("f000aa81-0451-4000-b000-000000000000"), ) } val peripheral = requestPeripheral(options)- The browser will show a list of peripherals matching your
Generate and run the SensorTag iOS project
mainTo use the SensorTag sample on iOS, you must first generate the Xcode project using Gradle. After generation, you must configure signing within Xcode before running the app.
Use
./gradlew :ios:generateXcodeProjectto generate the project, or./gradlew :ios:openXcodeto both generate the project and open it in Xcode immediately../gradlew :ios:generateXcodeProject # OR ./gradlew :ios:openXcode