Kotlin Multiplatform BLE library for iOS, Android, macos, windows and javascript
Status: ✅ Implemented (All Phases Complete)
Date: 2026-04-10
Implementation Started: 2026-04-10
Implementation Completed: 2026-04-11
Deciders: Blue Falcon maintainers, community contributors
Technical Story: As Blue Falcon grows with more platforms and custom use cases, the monolithic architecture becomes increasingly difficult to maintain and extend. Users want to add custom functionality and platform support without forking the entire library.
Blue Falcon currently uses a monolithic Kotlin Multiplatform architecture where platform implementations are developed and published together under a single library coordinate (dev.bluefalcon:blue-falcon:2.x.x). This design has served the project well for initial development but faces several challenges:
Ktor’s HTTP client successfully uses an engine-based architecture:
ktor-client-core - Common API and abstractionsktor-client-android, ktor-client-ios, ktor-client-js - Platform engines as separate dependenciesThis architecture enables:
We need similar benefits:
We will refactor Blue Falcon into a plugin-based engine architecture with three layers:
blue-falcon-core)Purpose: Common API, abstractions, and engine management
Responsibilities:
BlueFalconEngine, BluetoothPeripheral, BluetoothService, etc.)API Design:
// Core API - platform-agnostic
interface BlueFalconEngine {
val scope: CoroutineScope
val peripherals: StateFlow<Set<BluetoothPeripheral>>
val managerState: StateFlow<BluetoothManagerState>
suspend fun scan(filters: List<ServiceFilter> = emptyList())
suspend fun stopScanning()
suspend fun connect(peripheral: BluetoothPeripheral, autoConnect: Boolean = false)
suspend fun disconnect(peripheral: BluetoothPeripheral)
// ... other BLE operations
}
// Core client with engine + plugins
class BlueFalcon(
val engine: BlueFalconEngine
) {
val plugins: PluginRegistry = PluginRegistry()
// Delegates to engine
suspend fun scan(filters: List<ServiceFilter> = emptyList()) = engine.scan(filters)
// ...
}
// DSL for configuration
fun BlueFalcon(
block: BlueFalconConfig.() -> Unit
): BlueFalcon {
val config = BlueFalconConfig().apply(block)
return BlueFalcon(config.engine)
}
Each platform becomes an independent module published as separate artifacts:
blue-falcon-engine-android - Android BLE implementationblue-falcon-engine-ios - iOS CoreBluetooth implementationblue-falcon-engine-macos - macOS CoreBluetooth implementationblue-falcon-engine-js - JavaScript Web Bluetooth implementationblue-falcon-engine-windows - Windows WinRT implementationblue-falcon-engine-rpi - Raspberry Pi implementationblue-falcon-engine-linux, blue-falcon-engine-custom, etc.Monorepo Structure: All modules remain under library/ directory with dedicated folders for engines and plugins:
library/
├── settings.gradle.kts # Include all modules
├── core/ # blue-falcon-core
│ ├── build.gradle.kts # Publishes: dev.bluefalcon:blue-falcon-core
│ └── src/
│ └── commonMain/
├── engines/
│ ├── android/ # blue-falcon-engine-android
│ │ ├── build.gradle.kts # Publishes: dev.bluefalcon:blue-falcon-engine-android
│ │ └── src/
│ │ └── androidMain/
│ ├── ios/ # blue-falcon-engine-ios
│ │ ├── build.gradle.kts # Publishes: dev.bluefalcon:blue-falcon-engine-ios
│ │ └── src/
│ │ ├── iosMain/
│ │ └── nativeMain/
│ ├── macos/ # blue-falcon-engine-macos
│ │ ├── build.gradle.kts
│ │ └── src/macosMain/
│ ├── js/ # blue-falcon-engine-js
│ │ ├── build.gradle.kts
│ │ └── src/jsMain/
│ ├── windows/ # blue-falcon-engine-windows
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── windowsMain/
│ │ └── cpp/ # Native Windows code
│ └── rpi/ # blue-falcon-engine-rpi
│ ├── build.gradle.kts
│ └── src/rpiMain/
├── plugins/
│ ├── logging/ # blue-falcon-plugin-logging
│ │ ├── build.gradle.kts # Publishes: dev.bluefalcon:blue-falcon-plugin-logging
│ │ └── src/commonMain/
│ ├── retry/ # blue-falcon-plugin-retry
│ │ ├── build.gradle.kts
│ │ └── src/commonMain/
│ └── caching/ # blue-falcon-plugin-caching
│ ├── build.gradle.kts
│ └── src/commonMain/
└── legacy/ # blue-falcon-legacy (compatibility layer)
├── build.gradle.kts
└── src/
Each subfolder is a Gradle module with its own build.gradle.kts and can be:
Usage Example:
// In build.gradle.kts
dependencies {
implementation("dev.bluefalcon:blue-falcon-core:3.0.0")
implementation("dev.bluefalcon:blue-falcon-engine-android:3.0.0") // Only Android
}
// In code
val blueFalcon = BlueFalcon {
engine = AndroidEngine(context)
install(LoggingPlugin) {
level = LogLevel.DEBUG
}
}
Plugins provide cross-cutting functionality and are published as separate artifacts from library/plugins/:
Core Plugins (maintained in monorepo):
blue-falcon-plugin-logging (library/plugins/logging/) - Structured loggingblue-falcon-plugin-retry (library/plugins/retry/) - Automatic retry on transient failuresblue-falcon-plugin-caching (library/plugins/caching/) - Cache GATT service/characteristic metadatablue-falcon-plugin-metrics (library/plugins/metrics/) - Performance and usage metricsCommunity Plugins (examples, external repositories):
blue-falcon-plugin-device-profiles - High-level abstractions for common device types (heart rate monitors, thermometers, glucose meters)blue-falcon-plugin-security - Additional encryption/authentication layersblue-falcon-plugin-simulator - Mock BLE devices for testingblue-falcon-plugin-nordic-ota - Over-the-air firmware updates for Nordic chipsets (nRF52, nRF53, etc.)blue-falcon-plugin-texas-instruments-ota - OTA updates for Texas Instruments BLE devicesblue-falcon-plugin-analytics - Usage analytics and telemetryPlugin API:
interface BlueFalconPlugin {
fun install(client: BlueFalcon, config: PluginConfig)
suspend fun onScan(call: ScanCall, next: suspend (ScanCall) -> Unit)
suspend fun onConnect(call: ConnectCall, next: suspend (ConnectCall) -> Unit)
suspend fun onRead(call: ReadCall, next: suspend (ReadCall) -> Unit)
suspend fun onWrite(call: WriteCall, next: suspend (WriteCall) -> Unit)
// ... interceptors for all operations
}
// Example: Nordic OTA Plugin
class NordicOTAPlugin(
private val config: NordicOTAConfig
) : BlueFalconPlugin {
suspend fun updateFirmware(
peripheral: BluetoothPeripheral,
firmwareData: ByteArray,
onProgress: (Int) -> Unit
) {
// Nordic DFU protocol implementation
// - Enter bootloader mode
// - Send firmware packets
// - Verify and reboot
}
override suspend fun onConnect(call: ConnectCall, next: suspend (ConnectCall) -> Unit) {
next(call)
// Detect Nordic bootloader service UUID if present
if (call.peripheral.hasService(NORDIC_DFU_SERVICE_UUID)) {
// Mark peripheral as OTA-capable
}
}
}
// Usage
val blueFalcon = BlueFalcon {
engine = AndroidEngine(context)
install(NordicOTAPlugin) {
enableAutoBootloaderDetection = true
packetSize = 20
}
}
Legacy API (blue-falcon-legacy or within blue-falcon-core):
Maintain the existing expect/actual BlueFalcon API but mark as deprecated:
@Deprecated(
message = "Use the new engine-based API. See migration guide.",
replaceWith = ReplaceWith("BlueFalcon { engine = AndroidEngine(context) }"),
level = DeprecationLevel.WARNING
)
expect class BlueFalcon(
log: Logger?,
context: ApplicationContext,
autoDiscoverAllServicesAndCharacteristics: Boolean = true
)
The deprecated API internally delegates to the new engine system, ensuring existing code continues to work.
library/ for easier development and testingMaintain the existing expect/actual pattern with all platforms in one artifact.
Pros:
Cons:
Why not chosen: Does not solve the core extensibility and modularity problems driving this proposal.
Split into multiple artifacts but keep platform-specific APIs:
blue-falcon-androidblue-falcon-iosPros:
Cons:
Why not chosen: Loses the key benefit of a unified API. Users want one API that works everywhere.
Create BlueFalconEngine interface but use direct constructor injection instead of DSL:
val engine = AndroidEngine(context)
val blueFalcon = BlueFalcon(engine)
Pros:
Cons:
Why not chosen: While simpler, the DSL provides better developer experience and aligns with Kotlin ecosystem conventions (Ktor, Koin, etc.). We can provide both approaches.
Add hooks/callbacks to current architecture without full refactor:
expect class BlueFalcon {
var extensionHandler: BlueFalconExtension?
}
Pros:
Cons:
Why not chosen: Doesn’t provide sufficient long-term value. If we’re going to break things, do it right once.
All modules remain under the library/ directory as a monorepo:
library/
├── settings.gradle.kts # Include all modules
├── core/
│ ├── build.gradle.kts # Publishes as blue-falcon-core
│ └── src/commonMain/
├── engines/
│ ├── android/
│ │ ├── build.gradle.kts # Publishes as blue-falcon-engine-android
│ │ └── src/androidMain/
│ ├── ios/
│ │ ├── build.gradle.kts # Publishes as blue-falcon-engine-ios
│ │ └── src/{iosMain,nativeMain}/
│ ├── macos/
│ │ └── src/macosMain/
│ ├── js/
│ │ └── src/jsMain/
│ ├── windows/
│ │ └── src/{windowsMain,cpp}/
│ └── rpi/
│ └── src/rpiMain/
├── plugins/
│ ├── logging/
│ │ └── src/commonMain/
│ ├── retry/
│ └── caching/
└── legacy/ # Compatibility layer
└── src/
Gradle Configuration:
// library/settings.gradle.kts
include(
":core",
":engines:android",
":engines:ios",
":engines:macos",
":engines:js",
":engines:windows",
":engines:rpi",
":plugins:logging",
":plugins:retry",
":plugins:caching",
":legacy"
)
Each module has its own build.gradle.kts with independent:
library/core/ module structureBlueFalconEngine interface in core/src/commonMain/library/settings.gradle.kts to include :corelibrary/engines/ directory and engine module directories:
library/engines/android/library/engines/ios/library/engines/macos/library/engines/js/library/engines/windows/library/src/*Main/ to respective engine modulesbuild.gradle.kts for independent publishinglibrary/settings.gradle.kts to include all engines (:engines:android, :engines:ios, etc.)library/legacy/ modulelibrary/plugins/ directory structurelibrary/plugins/:
library/plugins/logging/library/plugins/retry/library/plugins/caching/build.gradle.kts for independent publishingBefore (2.x):
dependencies {
implementation("dev.bluefalcon:blue-falcon:2.5.4")
}
val blueFalcon = BlueFalcon(PrintLnLogger, ApplicationContext())
blueFalcon.scan()
After (3.0) - New API:
dependencies {
implementation("dev.bluefalcon:blue-falcon-core:3.0.0")
implementation("dev.bluefalcon:blue-falcon-engine-android:3.0.0")
}
val blueFalcon = BlueFalcon {
engine = AndroidEngine(context)
install(LoggingPlugin)
}
blueFalcon.scan()
After (3.0) - Compatibility API:
dependencies {
implementation("dev.bluefalcon:blue-falcon-core:3.0.0")
implementation("dev.bluefalcon:blue-falcon-engine-android:3.0.0")
implementation("dev.bluefalcon:blue-falcon-legacy:3.0.0") // compat layer
}
// Works unchanged, but shows deprecation warnings
val blueFalcon = BlueFalcon(PrintLnLogger, ApplicationContext())
blueFalcon.scan()
dev.bluefalcon → dev.bluefalcon.core, dev.bluefalcon.engine.*library/src/*Main/ to library/engines/*/src/*Main/Keeping all modules under library/ with dedicated engines/ and plugins/ folders provides:
library/ organizationlibrary/engines/ directory/library/src/commonMain/kotlin/dev/bluefalcon/Successfully created the core module with all foundational components:
Created Files (16 files, ~1,800 lines of code):
library/core/ - Complete core module
BlueFalconEngine.kt - Main engine interfaceBlueFalcon.kt - Client class with DSL APIBluetoothTypes.kt - Core data interfacesBluetoothStates.kt - State enumsLogger.kt, Exceptions.kt, Uuid.kt, etc.plugin/BlueFalconPlugin.kt - Plugin systemplugin/PluginRegistry.kt - Plugin managementStatus: ✅ Core module compiles successfully on all platforms (JVM, JS, Native)
What Works:
BlueFalconEngine interface with all BLE operationsBlueFalcon { engine = ... })Successfully migrated all 6 platform implementations to the new engine architecture:
Created Engines (43 files, ~4,024 lines of code):
library/engines/android/) - ✅ Complete
dev.bluefalcon:blue-falcon-engine-android:3.0.0library/engines/ios/) - ✅ Complete
dev.bluefalcon:blue-falcon-engine-ios:3.0.0library/engines/macos/) - ✅ Complete
dev.bluefalcon:blue-falcon-engine-macos:3.0.0library/engines/js/) - ✅ Complete
dev.bluefalcon:blue-falcon-engine-js:3.0.0library/engines/windows/) - ✅ Complete
dev.bluefalcon:blue-falcon-engine-windows:3.0.0library/engines/rpi/) - ✅ Complete
dev.bluefalcon:blue-falcon-engine-rpi:3.0.0Build Status: ✅ All engines compile successfully with ./gradlew build
What Works:
BlueFalconEngine interfacelibrary/settings.gradle.ktsNext Steps: Implement core plugins
Successfully created a compatibility layer that allows existing 2.x code to work with 3.0 engines:
Created Module (library/legacy/) - 15 files:
BlueFalconDelegate.kt - Complete 2.x delegate interface (14 callback methods)BlueFalcon.kt (expect) - Matches 2.x API signatureApplicationContext.kt (expect) - Platform-specific contextLogger.kt - Simple logging interfaceNativeFlow.kt - Flow wrapper for native platformsKey Features:
delegates: MutableSet<BlueFalconDelegate>dev.bluefalcon:blue-falcon:3.0.0 (main artifact)Build Status: ✅ Compiles successfully with ./gradlew :legacy:build
Migration Path:
Successfully implemented three production-ready core plugins demonstrating the plugin system:
Created Modules (library/plugins/) - 4 files, ~809 LOC:
plugins/logging/):
[BlueFalcon] [LEVEL] messagedev.bluefalcon:blue-falcon-plugin-logging:3.0.0plugins/retry/):
dev.bluefalcon:blue-falcon-plugin-retry:3.0.0plugins/caching/):
dev.bluefalcon:blue-falcon-plugin-caching:3.0.0Usage Example:
val blueFalcon = BlueFalcon {
engine = AndroidEngine(context)
install(LoggingPlugin) {
level = LogLevel.DEBUG
logGattOperations = true
}
install(RetryPlugin) {
maxRetries = 3
initialDelay = 500.milliseconds
}
install(CachingPlugin) {
cacheDuration = 5.minutes
invalidateOnDisconnect = true
}
}
Build Status: ✅ All plugins compile successfully on all platforms (JVM, JS, iOS, macOS)
Key Features:
Estimated Completion: 4-6 weeks of focused development
For detailed implementation status, see session checkpoints.