Kotlin Multiplatform BLE library for iOS, Android, macos, windows and javascript
The main client class for interacting with Bluetooth Low Energy devices.
class BlueFalcon(
val engine: BlueFalconEngine
)
fun BlueFalcon(block: BlueFalconConfig.() -> Unit): BlueFalcon
Example:
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
install(LoggingPlugin) { /* config */ }
}
| Property | Type | Description |
|---|---|---|
engine |
BlueFalconEngine |
Underlying platform engine |
plugins |
PluginRegistry |
Installed plugins |
peripherals |
StateFlow<Set<BluetoothPeripheral>> |
Discovered devices |
managerState |
StateFlow<BluetoothManagerState> |
Bluetooth adapter state |
isScanning |
Boolean |
Whether currently scanning |
centralCapabilities |
CentralCapabilities |
Typed central/client features implemented by the selected engine |
characteristicWriteCapabilities |
StateFlow<Map<CharacteristicWriteKey, CharacteristicWriteCapability>> |
Per-connection write limits and durable readiness |
characteristicWriteReady |
SharedFlow<CharacteristicWriteReady> |
Edge-triggered hint to retry a backpressured write |
notificationSubscriptionUpdates |
SharedFlow<NotificationSubscriptionUpdate> |
Completed typed notification-subscription outcomes |
suspend fun scan(filters: List<ServiceFilter> = emptyList())
Start scanning for BLE devices.
Parameters:
filters: Optional list of service UUIDs to filter devicesExample:
// Scan for all devices
blueFalcon.scan()
// Scan for devices with specific service
blueFalcon.scan(filters = listOf(
ServiceFilter(Uuid("0000180D-0000-1000-8000-00805F9B34FB"))
))
suspend fun stopScanning()
Stop scanning for devices.
fun clearPeripherals()
Clear all discovered peripherals from cache.
suspend fun connect(
peripheral: BluetoothPeripheral,
autoConnect: Boolean = false
)
Connect to a BLE peripheral.
Parameters:
peripheral: The device to connect toautoConnect: Auto-reconnect on disconnection (Android only)Throws:
BluetoothException: If connection failsExample:
try {
blueFalcon.connect(peripheral, autoConnect = true)
println("Connected!")
} catch (e: BluetoothException) {
println("Failed: ${e.message}")
}
suspend fun disconnect(peripheral: BluetoothPeripheral)
Disconnect from a peripheral.
fun connectionState(peripheral: BluetoothPeripheral): BluetoothPeripheralState
Get current connection state.
Returns: Connected, Connecting, Disconnected, Disconnecting
fun retrievePeripheral(identifier: String): BluetoothPeripheral?
Retrieve a peripheral by platform-specific identifier.
Parameters:
identifier: MAC address (Android) or UUID string (iOS/macOS)Returns: Peripheral if found, null otherwise
fun requestConnectionPriority(
peripheral: BluetoothPeripheral,
priority: ConnectionPriority
)
Request connection priority change (Android only, no-op on other platforms).
Priority levels: Balanced, High, LowPower
suspend fun discoverServices(
peripheral: BluetoothPeripheral,
serviceUUIDs: List<Uuid> = emptyList()
)
Discover GATT services on a connected peripheral.
Parameters:
peripheral: Connected deviceserviceUUIDs: Optional filter for specific servicesExample:
blueFalcon.connect(peripheral)
blueFalcon.discoverServices(peripheral)
peripheral.services.forEach { service ->
println("Service: ${service.uuid}")
}
suspend fun discoverCharacteristics(
peripheral: BluetoothPeripheral,
service: BluetoothService,
characteristicUUIDs: List<Uuid> = emptyList()
)
Discover characteristics for a service.
suspend fun readCharacteristic(
peripheral: BluetoothPeripheral,
characteristic: BluetoothCharacteristic
)
Read a characteristic value. Result is stored in characteristic.value.
Example:
blueFalcon.readCharacteristic(peripheral, characteristic)
val value = characteristic.value
println("Read: ${value?.decodeToString()}")
suspend fun writeCharacteristic(
peripheral: BluetoothPeripheral,
characteristic: BluetoothCharacteristic,
value: String,
writeType: Int? = null
)
suspend fun writeCharacteristic(
peripheral: BluetoothPeripheral,
characteristic: BluetoothCharacteristic,
value: ByteArray,
writeType: Int? = null
)
suspend fun writeCharacteristic(
peripheral: BluetoothPeripheral,
characteristic: BluetoothCharacteristic,
value: ByteArray,
writeType: CharacteristicWriteType,
): CharacteristicWriteResult
The Int? overloads are compatibility APIs and do not expose a delivery outcome. The typed overload
requires an explicit CharacteristicWriteType so existing source code cannot silently change
semantics during the compatibility window.
CharacteristicWriteResult has these outcomes:
| Result | Meaning |
|---|---|
Sent |
Android received a successful matching GATT callback; Apple WithResponse received a successful CoreBluetooth callback; Apple WithoutResponse was accepted by CoreBluetooth without remote acknowledgement |
Backpressured |
The engine did not retain the payload. Retry the same payload only after readiness changes |
PayloadTooLarge(maximumLength) |
The payload exceeded the current per-connection limit |
Disconnected |
No active connection generation owns the operation |
Unsupported |
The selected engine does not implement typed central writes |
Failed(cause) |
The platform rejected or failed the operation |
Readiness is a hint, not a reservation: another operation may consume capacity before the caller
retries. maximumLength == null means the limit is unknown, not unlimited. Android publishes a
20-byte default and replaces it with negotiatedMtu - 3 after a successful MTU callback. Apple
queries CoreBluetooth separately for each write type.
Example:
while (offset < payload.size) {
when (
val result = blueFalcon.writeCharacteristic(
peripheral,
characteristic,
nextFragment(),
CharacteristicWriteType.WithoutResponse,
)
) {
CharacteristicWriteResult.Sent -> advance()
CharacteristicWriteResult.Backpressured -> {
val key = CharacteristicWriteKey(
peripheral.uuid,
CharacteristicWriteType.WithoutResponse,
)
blueFalcon.characteristicWriteCapabilities.first { capabilities ->
capabilities[key]?.ready == true
}
}
else -> handleFailure(result)
}
}
Continue submitting while the result is Sent; suspend only after Backpressured. Blue Falcon does
not own framing, fragmentation, retries, or a durable application queue. Use the durable
characteristicWriteCapabilities snapshot for correctness. characteristicWriteReady is an
edge-triggered latency optimization for collectors installed before the edge; it is not a replaying
source of truth.
suspend fun setNotificationSubscription(
peripheral: BluetoothPeripheral,
characteristic: BluetoothCharacteristic,
enabled: Boolean,
): NotificationSubscriptionResult
suspend fun notifyCharacteristic(
peripheral: BluetoothPeripheral,
characteristic: BluetoothCharacteristic,
notify: Boolean
)
setNotificationSubscription is the typed notification API. The subscription is ready only after
Updated(enabled); Android waits for the matching CCCD write callback and Apple waits for
didUpdateNotificationStateForCharacteristic with matching isNotifying state. Disconnected,
Unsupported, and Failed are terminal outcomes. The compatibility notifyCharacteristic method
does not return this confirmation.
Example:
when (
val result = blueFalcon.setNotificationSubscription(
peripheral,
characteristic,
enabled = true,
)
) {
is NotificationSubscriptionResult.Updated -> receiveNotifications()
else -> handleSubscriptionFailure(result)
}
Typed central writes and subscriptions are currently implemented by Android, iOS, and native macOS.
Other engines use the default Unsupported contract. Apple central restoration is intentionally
deferred to a subsequent change.
suspend fun indicateCharacteristic(
peripheral: BluetoothPeripheral,
characteristic: BluetoothCharacteristic,
indicate: Boolean
)
Enable/disable indications for a characteristic.
suspend fun readDescriptor(
peripheral: BluetoothPeripheral,
characteristic: BluetoothCharacteristic,
descriptor: BluetoothCharacteristicDescriptor
)
Read a characteristic descriptor.
suspend fun writeDescriptor(
peripheral: BluetoothPeripheral,
descriptor: BluetoothCharacteristicDescriptor,
value: ByteArray
)
Write to a characteristic descriptor.
suspend fun changeMTU(peripheral: BluetoothPeripheral, mtuSize: Int)
Request MTU (Maximum Transmission Unit) size change.
fun refreshGattCache(peripheral: BluetoothPeripheral): Boolean
Refresh GATT cache (Android only).
suspend fun openL2capChannel(peripheral: BluetoothPeripheral, psm: Int)
Open L2CAP channel (platform-dependent).
suspend fun createBond(peripheral: BluetoothPeripheral)
Create a bond (pair) with the peripheral.
suspend fun removeBond(peripheral: BluetoothPeripheral)
Remove bond (unpair) with the peripheral.
Base interface for creating plugins.
interface BlueFalconPlugin {
fun install(client: BlueFalconClient, config: PluginConfig)
suspend fun onBeforeScan(call: ScanCall): ScanCall = call
suspend fun onAfterScan(call: ScanCall) {}
suspend fun onBeforeConnect(call: ConnectCall): ConnectCall = call
suspend fun onAfterConnect(call: ConnectCall, result: Result<Unit>) {}
suspend fun onBeforeRead(call: ReadCall): ReadCall = call
suspend fun onAfterRead(call: ReadCall, result: Result<ByteArray?>) {}
suspend fun onBeforeWrite(call: WriteCall): WriteCall = call
suspend fun onAfterWrite(call: WriteCall, result: Result<Unit>) {}
}
Hook Methods:
| Method | Description | When Called |
|---|---|---|
install() |
Plugin initialization | Once on installation |
onBeforeScan() |
Intercept scan | Before scan starts |
onAfterScan() |
Post-scan hook | After scan completes |
onBeforeConnect() |
Intercept connect | Before connection attempt |
onAfterConnect() |
Post-connect hook | After connection result |
onBeforeRead() |
Intercept read | Before reading characteristic |
onAfterRead() |
Post-read hook | After read completes |
onBeforeWrite() |
Intercept write | Before writing characteristic |
onAfterWrite() |
Post-write hook | After write completes |
Example:
class MyPlugin : BlueFalconPlugin {
override fun install(client: BlueFalconClient, config: PluginConfig) {
println("Plugin installed")
}
override suspend fun onBeforeRead(call: ReadCall): ReadCall {
println("About to read ${call.characteristic.uuid}")
return call
}
override suspend fun onAfterRead(call: ReadCall, result: Result<ByteArray?>) {
result.onSuccess { data ->
println("Read ${data?.size ?: 0} bytes")
}
}
}
Manages installed plugins.
class PluginRegistry {
fun install(plugin: BlueFalconPlugin, configure: PluginConfig.() -> Unit = {})
fun uninstall(plugin: BlueFalconPlugin)
}
Example:
val plugins = blueFalcon.plugins
// Install a plugin
plugins.install(LoggingPlugin(LoggingPlugin.Config()))
// Uninstall a plugin
plugins.uninstall(loggingPlugin)
Base class for plugin configurations.
open class PluginConfig
Example:
class MyPluginConfig : PluginConfig() {
var enabled: Boolean = true
var timeout: Duration = 5.seconds
}
data class ScanCall(val filters: List<ServiceFilter>)
data class ConnectCall(
val peripheral: BluetoothPeripheral,
val autoConnect: Boolean
)
data class ReadCall(
val peripheral: BluetoothPeripheral,
val characteristic: BluetoothCharacteristic
)
data class WriteCall(
val peripheral: BluetoothPeripheral,
val characteristic: BluetoothCharacteristic,
val value: ByteArray,
val writeType: Int?
)
Represents a BLE device.
interface BluetoothPeripheral {
val name: String? // Device name
val uuid: String // Platform-specific ID
val rssi: Float? // Signal strength (dBm)
val mtuSize: Int? // MTU size
val services: List<BluetoothService> // GATT services
val characteristics: List<BluetoothCharacteristic>
}
Example:
blueFalcon.peripherals.collect { devices ->
devices.forEach { device ->
println("${device.name} (${device.uuid})")
println("RSSI: ${device.rssi} dBm")
}
}
Represents a GATT service.
interface BluetoothService {
val uuid: Uuid // Service UUID
val name: String? // Human-readable name
val characteristics: List<BluetoothCharacteristic>
}
Example:
peripheral.services.forEach { service ->
println("Service: ${service.uuid}")
service.characteristics.forEach { char ->
println(" Characteristic: ${char.uuid}")
}
}
Represents a GATT characteristic.
interface BluetoothCharacteristic {
val uuid: Uuid // Characteristic UUID
val name: String? // Human-readable name
val value: ByteArray? // Current value
val descriptors: List<BluetoothCharacteristicDescriptor>
val isNotifying: Boolean // Notifications enabled?
val service: BluetoothService? // Parent service
}
Example:
blueFalcon.readCharacteristic(peripheral, characteristic)
val text = characteristic.value?.decodeToString()
println("Value: $text")
Represents a GATT descriptor.
interface BluetoothCharacteristicDescriptor {
val uuid: Uuid // Descriptor UUID
val name: String? // Human-readable name
val value: ByteArray? // Current value
val characteristic: BluetoothCharacteristic? // Parent characteristic
}
UUID wrapper for BLE UUIDs.
class Uuid(val value: String) {
override fun toString(): String = value
}
Common UUIDs:
// Standard service UUIDs
val HEART_RATE_SERVICE = Uuid("0000180D-0000-1000-8000-00805F9B34FB")
val BATTERY_SERVICE = Uuid("0000180F-0000-1000-8000-00805F9B34FB")
val DEVICE_INFO = Uuid("0000180A-0000-1000-8000-00805F9B34FB")
// Standard characteristic UUIDs
val HEART_RATE_MEASUREMENT = Uuid("00002A37-0000-1000-8000-00805F9B34FB")
val BATTERY_LEVEL = Uuid("00002A19-0000-1000-8000-00805F9B34FB")
Filter for scanning operations.
class ServiceFilter(val uuid: Uuid)
Example:
val heartRateFilter = ServiceFilter(
Uuid("0000180D-0000-1000-8000-00805F9B34FB")
)
blueFalcon.scan(filters = listOf(heartRateFilter))
Connection state enum.
enum class BluetoothPeripheralState {
Connected,
Connecting,
Disconnected,
Disconnecting
}
Bluetooth adapter state.
enum class BluetoothManagerState {
Unknown,
Resetting,
Unsupported,
Unauthorized,
PoweredOff,
PoweredOn
}
Connection priority levels (Android).
enum class ConnectionPriority {
Balanced,
High,
LowPower
}
Core interface that all platform engines implement.
interface BlueFalconEngine {
val scope: CoroutineScope
val peripherals: StateFlow<Set<BluetoothPeripheral>>
val managerState: StateFlow<BluetoothManagerState>
val isScanning: Boolean
suspend fun scan(filters: List<ServiceFilter> = emptyList())
suspend fun stopScanning()
// ... all BlueFalcon methods
}
class AndroidBlueFalconEngine(
context: Context,
autoDiscoverServices: Boolean = true
) : BlueFalconEngine
Dependencies:
androidMain.dependencies {
implementation("dev.bluefalcon:blue-falcon-engine-android:3.0.0")
}
Example:
val engine = AndroidBlueFalconEngine(
context = applicationContext,
autoDiscoverServices = true
)
class IosBlueFalconEngine : BlueFalconEngine
Dependencies:
iosMain.dependencies {
implementation("dev.bluefalcon:blue-falcon-engine-ios:3.0.0")
}
Example:
val engine = IosBlueFalconEngine()
class MacOSBlueFalconEngine : BlueFalconEngine
Dependencies:
macosMain.dependencies {
implementation("dev.bluefalcon:blue-falcon-engine-macos:3.0.0")
}
class JsBlueFalconEngine : BlueFalconEngine
Dependencies:
jsMain.dependencies {
implementation("dev.bluefalcon:blue-falcon-engine-js:3.0.0")
}
Browser support: Chrome, Edge, Opera
class WindowsBlueFalconEngine : BlueFalconEngine
Dependencies:
windowsMain.dependencies {
implementation("dev.bluefalcon:blue-falcon-engine-windows:3.0.0")
}
Requirements: Windows 10 version 1803+
class RpiBlueFalconEngine : BlueFalconEngine
Dependencies:
rpiMain.dependencies {
implementation("dev.bluefalcon:blue-falcon-engine-rpi:3.0.0")
}
Configuration builder for DSL-based initialization.
class BlueFalconConfig {
lateinit var engine: BlueFalconEngine
fun <T : BlueFalconPlugin> install(
plugin: T,
configure: PluginConfig.() -> Unit = {}
)
}
Example:
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
install(LoggingPlugin) {
level = LogLevel.DEBUG
logger = PrintLnLogger
}
install(RetryPlugin) {
maxRetries = 3
initialDelay = 500.milliseconds
}
}
sealed class BluetoothException : Exception()
class BluetoothUnknownException : BluetoothException()
class BluetoothResettingException : BluetoothException()
class BluetoothUnsupportedException : BluetoothException()
class BluetoothPermissionException : BluetoothException()
class BluetoothNotEnabledException : BluetoothException()
try {
blueFalcon.connect(peripheral)
} catch (e: BluetoothPermissionException) {
// Request permissions
} catch (e: BluetoothNotEnabledException) {
// Prompt user to enable Bluetooth
} catch (e: BluetoothException) {
// Handle other BLE errors
}
Plugin hooks receive Result<T> for operations:
override suspend fun onAfterConnect(call: ConnectCall, result: Result<Unit>) {
result.fold(
onSuccess = { println("Connected!") },
onFailure = { error -> println("Failed: $error") }
)
}
val peripherals: StateFlow<Set<BluetoothPeripheral>>
Example:
lifecycleScope.launch {
blueFalcon.peripherals.collect { devices ->
println("Found ${devices.size} devices")
updateUI(devices)
}
}
val managerState: StateFlow<BluetoothManagerState>
Example:
lifecycleScope.launch {
blueFalcon.managerState.collect { state ->
when (state) {
BluetoothManagerState.PoweredOn -> startScanning()
BluetoothManagerState.PoweredOff -> showBluetoothDisabledMessage()
BluetoothManagerState.Unauthorized -> requestPermissions()
else -> {}
}
}
}
lifecycleScope.launch {
combine(
blueFalcon.peripherals,
blueFalcon.managerState
) { devices, state ->
Pair(devices, state)
}.collect { (devices, state) ->
if (state == BluetoothManagerState.PoweredOn) {
updateDeviceList(devices)
}
}
}
// Initialize
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
}
// Scan
blueFalcon.scan()
// Monitor discovered devices
blueFalcon.peripherals.collect { devices -> }
// Connect
blueFalcon.connect(peripheral)
// Discover services
blueFalcon.discoverServices(peripheral)
// Read characteristic
blueFalcon.readCharacteristic(peripheral, characteristic)
val value = characteristic.value
// Write characteristic
blueFalcon.writeCharacteristic(peripheral, characteristic, "data")
// Enable notifications
blueFalcon.notifyCharacteristic(peripheral, characteristic, true)
// Disconnect
blueFalcon.disconnect(peripheral)
| Platform | Engine | Context Required |
|---|---|---|
| Android | AndroidBlueFalconEngine |
✅ Android Context |
| iOS | IosBlueFalconEngine |
❌ None |
| macOS | MacOSBlueFalconEngine |
❌ None |
| JavaScript | JsBlueFalconEngine |
❌ None |
| Windows | WindowsBlueFalconEngine |
❌ None |
| Raspberry Pi | RpiBlueFalconEngine |
❌ None |
Need help? Open an issue on GitHub or check the examples in the examples/ directory.