Kotlin Multiplatform BLE library for iOS, Android, macos, windows and javascript
Blue Falcon 3.0βs plugin system allows you to extend BLE functionality without modifying core library code. Plugins use an interceptor pattern to wrap BLE operations, enabling cross-cutting concerns like:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Code β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β call scan(), connect(), read(), etc.
βΌ
ββββββββββββββββββββββ
β BlueFalcon Core β
ββββββββββ¬ββββββββββββ
β intercept operations
βΌ
ββββββββββββββββββββββ
β Plugin Registry ββββββ Installed Plugins
β - LoggingPlugin β
β - RetryPlugin β
β - CachingPlugin β
β - YourPlugin β
ββββββββββ¬ββββββββββββ
β call chain
βΌ
ββββββββββββββββββββββ
β BlueFalconEngine β
β (Android, iOS...) β
ββββββββββββββββββββββ
When you call blueFalcon.scan(), the plugin system creates an interceptor chain:
Application β Plugin 1 β Plugin 2 β ... β Engine β Result
β β β
onBeforeScan onBeforeScan actual scan
β β β
onAfterScan onAfterScan scan completed
Each plugin can:
Letβs build a simple performance monitoring plugin that measures BLE operation duration.
package com.example.plugins.performance
import dev.bluefalcon.core.plugin.*
import dev.bluefalcon.core.*
import kotlin.time.TimeSource
import kotlin.time.Duration
class PerformancePlugin(private val config: Config) : BlueFalconPlugin {
class Config : PluginConfig() {
var onMeasure: (operation: String, duration: Duration) -> Unit = { _, _ -> }
var logSlowOperations: Boolean = true
var slowThreshold: Duration = 1.seconds
}
override fun install(client: BlueFalconClient, config: PluginConfig) {
println("PerformancePlugin installed")
}
override suspend fun onBeforeScan(call: ScanCall): ScanCall {
// Called before scan starts
return call
}
override suspend fun onAfterScan(call: ScanCall) {
// Called after scan completes
}
override suspend fun onBeforeConnect(call: ConnectCall): ConnectCall {
// Track start time (store in call metadata if needed)
return call
}
override suspend fun onAfterConnect(call: ConnectCall, result: Result<Unit>) {
// Measure duration and report
}
override suspend fun onBeforeRead(call: ReadCall): ReadCall {
return call
}
override suspend fun onAfterRead(call: ReadCall, result: Result<ByteArray?>) {
// Measure read duration
}
override suspend fun onBeforeWrite(call: WriteCall): WriteCall {
return call
}
override suspend fun onAfterWrite(call: WriteCall, result: Result<Unit>) {
// Measure write duration
}
}
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
install(PerformancePlugin(PerformancePlugin.Config().apply {
onMeasure = { operation, duration ->
println("$operation took ${duration.inWholeMilliseconds}ms")
}
slowThreshold = 500.milliseconds
}))
}
lifecycleScope.launch {
blueFalcon.scan() // Plugin logs: "scan took 245ms"
blueFalcon.connect(peripheral) // Plugin logs: "connect took 1523ms"
}
class MyPlugin : BlueFalconPlugin {
override fun install(client: BlueFalconClient, config: PluginConfig) {
// Called once when plugin is installed
// Use this to:
// - Validate configuration
// - Initialize resources
// - Set up listeners
println("Plugin installed with config: $config")
}
}
Plugins intercept these BLE operations:
| Operation | Before Hook | After Hook |
|---|---|---|
| Scan | onBeforeScan(call) |
onAfterScan(call) |
| Connect | onBeforeConnect(call) |
onAfterConnect(call, result) |
| Read | onBeforeRead(call) |
onAfterRead(call, result) |
| Write | onBeforeWrite(call) |
onAfterWrite(call, result) |
// Application calls
blueFalcon.connect(peripheral)
// Execution order:
1. Plugin1.onBeforeConnect(call)
2. Plugin2.onBeforeConnect(call)
3. Engine.connect(peripheral) β Actual BLE operation
4. Plugin2.onAfterConnect(call, result)
5. Plugin1.onAfterConnect(call, result)
Change operation parameters before they reach the engine:
class FilterPlugin(private val config: Config) : BlueFalconPlugin {
class Config : PluginConfig() {
var allowedDevices: Set<String> = emptySet()
}
override suspend fun onBeforeConnect(call: ConnectCall): ConnectCall {
// Only allow connections to whitelisted devices
if (call.peripheral.name !in config.allowedDevices) {
throw SecurityException("Device not whitelisted: ${call.peripheral.name}")
}
return call
}
}
Modify data after operations complete:
class DecryptionPlugin : BlueFalconPlugin {
override suspend fun onAfterRead(call: ReadCall, result: Result<ByteArray?>) {
result.onSuccess { encryptedData ->
val decrypted = decrypt(encryptedData ?: return@onSuccess)
// Update characteristic value with decrypted data
call.characteristic.value = decrypted
}
}
private fun decrypt(data: ByteArray): ByteArray {
// Your decryption logic
return data // simplified
}
}
Catch and handle errors:
class ErrorReportingPlugin(private val config: Config) : BlueFalconPlugin {
class Config : PluginConfig() {
var errorReporter: (Throwable) -> Unit = { println(it) }
}
override suspend fun onAfterConnect(call: ConnectCall, result: Result<Unit>) {
result.onFailure { error ->
config.errorReporter(error)
// Optionally send to analytics, crash reporting, etc.
}
}
}
Prevent too many operations in a short time:
package com.example.plugins.ratelimit
import dev.bluefalcon.core.plugin.*
import dev.bluefalcon.core.*
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource
class RateLimitPlugin(private val config: Config) : BlueFalconPlugin {
class Config : PluginConfig() {
var minDelay: Duration = 100.milliseconds
var maxOperationsPerSecond: Int = 10
}
private var lastOperationTime = TimeSource.Monotonic.markNow()
private var operationCount = 0
private var windowStart = TimeSource.Monotonic.markNow()
override fun install(client: BlueFalconClient, config: PluginConfig) {
println("RateLimitPlugin installed: ${this.config.maxOperationsPerSecond} ops/sec")
}
override suspend fun onBeforeRead(call: ReadCall): ReadCall {
enforceRateLimit()
return call
}
override suspend fun onBeforeWrite(call: WriteCall): WriteCall {
enforceRateLimit()
return call
}
private suspend fun enforceRateLimit() {
val now = TimeSource.Monotonic.markNow()
// Reset counter every second
if ((now - windowStart) > 1.seconds) {
operationCount = 0
windowStart = now
}
// Check rate limit
if (operationCount >= config.maxOperationsPerSecond) {
val waitTime = 1.seconds - (now - windowStart)
delay(waitTime)
operationCount = 0
windowStart = TimeSource.Monotonic.markNow()
}
// Enforce minimum delay between operations
val timeSinceLastOp = now - lastOperationTime
if (timeSinceLastOp < config.minDelay) {
delay(config.minDelay - timeSinceLastOp)
}
operationCount++
lastOperationTime = TimeSource.Monotonic.markNow()
}
}
// Usage
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
install(RateLimitPlugin(RateLimitPlugin.Config().apply {
minDelay = 50.milliseconds
maxOperationsPerSecond = 20
}))
}
Encrypt writes, decrypt reads:
package com.example.plugins.encryption
import dev.bluefalcon.core.plugin.*
import dev.bluefalcon.core.*
class EncryptionPlugin(private val config: Config) : BlueFalconPlugin {
class Config : PluginConfig() {
lateinit var encryptionKey: ByteArray
var algorithm: String = "AES"
}
override fun install(client: BlueFalconClient, config: PluginConfig) {
require(this.config.encryptionKey.isNotEmpty()) {
"Encryption key must be provided"
}
}
override suspend fun onBeforeWrite(call: WriteCall): WriteCall {
// Encrypt data before writing
val encrypted = encrypt(call.value)
return call.copy(value = encrypted)
}
override suspend fun onAfterRead(call: ReadCall, result: Result<ByteArray?>) {
// Decrypt data after reading
result.onSuccess { encryptedData ->
encryptedData?.let {
val decrypted = decrypt(it)
call.characteristic.value = decrypted
}
}
}
private fun encrypt(data: ByteArray): ByteArray {
// Simple XOR encryption (use proper crypto in production!)
return data.mapIndexed { i, byte ->
(byte.toInt() xor config.encryptionKey[i % config.encryptionKey.size].toInt()).toByte()
}.toByteArray()
}
private fun decrypt(data: ByteArray): ByteArray {
// XOR is symmetric
return encrypt(data)
}
}
// Usage
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
install(EncryptionPlugin(EncryptionPlugin.Config().apply {
encryptionKey = "my-secret-key-1234".encodeToByteArray()
}))
}
// All writes are encrypted, all reads are decrypted automatically!
blueFalcon.writeCharacteristic(peripheral, characteristic, "sensitive data")
Track BLE usage for analytics:
package com.example.plugins.analytics
import dev.bluefalcon.core.plugin.*
import dev.bluefalcon.core.*
class AnalyticsPlugin(private val config: Config) : BlueFalconPlugin {
class Config : PluginConfig() {
var trackScans: Boolean = true
var trackConnections: Boolean = true
var trackGattOperations: Boolean = true
var analyticsEndpoint: String = ""
}
private var scanCount = 0
private var connectionCount = 0
private var readCount = 0
private var writeCount = 0
override fun install(client: BlueFalconClient, config: PluginConfig) {
println("AnalyticsPlugin installed")
}
override suspend fun onBeforeScan(call: ScanCall): ScanCall {
if (config.trackScans) {
scanCount++
trackEvent("scan_started", mapOf("filters" to call.filters.size))
}
return call
}
override suspend fun onAfterConnect(call: ConnectCall, result: Result<Unit>) {
if (config.trackConnections) {
connectionCount++
val status = if (result.isSuccess) "success" else "failure"
trackEvent("connection_attempt", mapOf(
"device" to call.peripheral.name,
"status" to status,
"auto_connect" to call.autoConnect
))
}
}
override suspend fun onAfterRead(call: ReadCall, result: Result<ByteArray?>) {
if (config.trackGattOperations) {
readCount++
trackEvent("characteristic_read", mapOf(
"characteristic" to call.characteristic.uuid.toString(),
"success" to result.isSuccess,
"bytes" to (result.getOrNull()?.size ?: 0)
))
}
}
override suspend fun onAfterWrite(call: WriteCall, result: Result<Unit>) {
if (config.trackGattOperations) {
writeCount++
trackEvent("characteristic_write", mapOf(
"characteristic" to call.characteristic.uuid.toString(),
"success" to result.isSuccess,
"bytes" to call.value.size
))
}
}
private fun trackEvent(eventName: String, properties: Map<String, Any>) {
println("π Analytics: $eventName - $properties")
// Send to your analytics service
// analytics.track(eventName, properties)
}
fun getStats(): Map<String, Int> = mapOf(
"scans" to scanCount,
"connections" to connectionCount,
"reads" to readCount,
"writes" to writeCount
)
}
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
class PerformancePluginTest {
@Test
fun `should measure operation duration`() = runTest {
var measuredOperation: String? = null
var measuredDuration: Duration? = null
val plugin = PerformancePlugin(PerformancePlugin.Config().apply {
onMeasure = { op, duration ->
measuredOperation = op
measuredDuration = duration
}
})
val call = ReadCall(
peripheral = mockPeripheral(),
characteristic = mockCharacteristic()
)
plugin.onBeforeRead(call)
delay(100) // Simulate operation
plugin.onAfterRead(call, Result.success(byteArrayOf(0x01)))
assertEquals("read", measuredOperation)
assertTrue(measuredDuration!! > 90.milliseconds)
}
}
class PluginIntegrationTest {
@Test
fun `should install and use plugin`() = runTest {
val context = mockContext()
var loggedMessage: String? = null
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
install(LoggingPlugin(LoggingPlugin.Config().apply {
logger = object : Logger {
override fun log(level: LogLevel, message: String) {
loggedMessage = message
}
}
}))
}
blueFalcon.scan()
assertNotNull(loggedMessage)
assertTrue(loggedMessage!!.contains("scan"))
}
}
// library/plugins/my-plugin/build.gradle.kts
plugins {
kotlin("multiplatform")
id("maven-publish")
}
kotlin {
jvm()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
api(project(":core"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}
}
}
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = "dev.bluefalcon"
artifactId = "blue-falcon-plugin-my-plugin"
version = "3.0.0"
}
}
}
Create a README.md:
# Blue Falcon My Plugin
## Installation
```kotlin
commonMain.dependencies {
implementation("dev.bluefalcon:blue-falcon-plugin-my-plugin:3.0.0")
}
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
install(MyPlugin(MyPlugin.Config().apply {
// configuration
}))
}
option1: Descriptionoption2: Description
```./gradlew :plugins:my-plugin:publishToMavenLocal
# or publish to Maven Central
./gradlew :plugins:my-plugin:publish
β Good: Single responsibility
class LoggingPlugin // Only handles logging
class RetryPlugin // Only handles retries
β Bad: Multiple responsibilities
class LoggingAndRetryPlugin // Does too much
class MyPlugin(private val config: Config = Config()) : BlueFalconPlugin {
class Config : PluginConfig() {
var enabled: Boolean = true // Sensible defaults
var level: Int = 1
}
}
β Good: Use suspend functions
override suspend fun onBeforeRead(call: ReadCall): ReadCall {
delay(100) // Non-blocking
return call
}
β Bad: Block threads
override suspend fun onBeforeRead(call: ReadCall): ReadCall {
Thread.sleep(100) // Blocks thread!
return call
}
override suspend fun onAfterRead(call: ReadCall, result: Result<ByteArray?>) {
result.fold(
onSuccess = { data ->
// Handle success
},
onFailure = { error ->
// Log error, don't throw unless critical
logger.error("Read failed: ${error.message}")
}
)
}
Include:
Extend BlueFalconPlugin for custom hooks:
interface AdvancedPlugin : BlueFalconPlugin {
suspend fun onServiceDiscovered(service: BluetoothService) {}
suspend fun onNotificationReceived(characteristic: BluetoothCharacteristic) {}
}
One plugin can depend on another:
class MetricsPlugin(
private val loggingPlugin: LoggingPlugin
) : BlueFalconPlugin {
override fun install(client: BlueFalconClient, config: PluginConfig) {
// Use loggingPlugin functionality
loggingPlugin.log("MetricsPlugin installed")
}
}
val blueFalcon = BlueFalcon {
engine = AndroidBlueFalconEngine(context)
if (BuildConfig.DEBUG) {
install(LoggingPlugin(LoggingPlugin.Config().apply {
level = LogLevel.DEBUG
}))
}
install(RetryPlugin(RetryPlugin.Config()))
}
Plugins can communicate through shared state:
object PluginState {
var connectionAttempts = 0
}
class TrackerPlugin : BlueFalconPlugin {
override suspend fun onBeforeConnect(call: ConnectCall): ConnectCall {
PluginState.connectionAttempts++
return call
}
}
class ReporterPlugin : BlueFalconPlugin {
override suspend fun onAfterConnect(call: ConnectCall, result: Result<Unit>) {
println("Total attempts: ${PluginState.connectionAttempts}")
}
}
library/plugins/Happy Plugin Development! π
Need help? Open an issue or discussion on GitHub.