Blue-Falcon

Kotlin Multiplatform BLE library for iOS, Android, macos, windows and javascript

View the Project on GitHub Reedyuk/blue-falcon

Blue Falcon Plugin Development Guide

Table of Contents

  1. Introduction
  2. How the Plugin System Works
  3. Creating Your First Plugin
  4. Plugin Lifecycle
  5. Interceptor Pattern
  6. Complete Plugin Examples
  7. Testing Plugins
  8. Publishing Plugins
  9. Best Practices
  10. Advanced Topics

Introduction

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:

Why Build a Plugin?


How the Plugin System Works

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Application Code                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚ call scan(), connect(), read(), etc.
                  β–Ό
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   BlueFalcon Core  β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚ intercept operations
                  β–Ό
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  Plugin Registry   │◄──── Installed Plugins
         β”‚  - LoggingPlugin   β”‚
         β”‚  - RetryPlugin     β”‚
         β”‚  - CachingPlugin   β”‚
         β”‚  - YourPlugin      β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚ call chain
                  β–Ό
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚ BlueFalconEngine   β”‚
         β”‚  (Android, iOS...) β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Interceptor Chain

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:

  1. Modify the request before it reaches the engine
  2. Execute logic before/after the operation
  3. Transform the response before returning to the app
  4. Handle errors and retry failed operations

Creating Your First Plugin

Let’s build a simple performance monitoring plugin that measures BLE operation duration.

Step 1: Implement BlueFalconPlugin

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
    }
}

Step 2: Install the Plugin

val blueFalcon = BlueFalcon {
    engine = AndroidBlueFalconEngine(context)
    
    install(PerformancePlugin(PerformancePlugin.Config().apply {
        onMeasure = { operation, duration ->
            println("$operation took ${duration.inWholeMilliseconds}ms")
        }
        slowThreshold = 500.milliseconds
    }))
}

Step 3: Use It!

lifecycleScope.launch {
    blueFalcon.scan() // Plugin logs: "scan took 245ms"
    blueFalcon.connect(peripheral) // Plugin logs: "connect took 1523ms"
}

Plugin Lifecycle

Installation Phase

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")
    }
}

Operation Interception

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)

Hook Execution Order

// 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)

Interceptor Pattern

Modifying Requests

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
    }
}

Transforming Responses

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
    }
}

Error Handling

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.
        }
    }
}

Complete Plugin Examples

Example 1: Rate Limiting Plugin

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
    }))
}

Example 2: Encryption Plugin

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")

Example 3: Analytics Plugin

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
    )
}

Testing Plugins

Unit Testing

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)
    }
}

Integration Testing

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"))
    }
}

Publishing Plugins

Step 1: Create Gradle Module

// 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"
        }
    }
}

Step 2: Document Your Plugin

Create a README.md:

# Blue Falcon My Plugin

## Installation

```kotlin
commonMain.dependencies {
    implementation("dev.bluefalcon:blue-falcon-plugin-my-plugin:3.0.0")
}

Usage

val blueFalcon = BlueFalcon {
    engine = AndroidBlueFalconEngine(context)
    
    install(MyPlugin(MyPlugin.Config().apply {
        // configuration
    }))
}

Configuration Options

Step 3: Publish to Maven

./gradlew :plugins:my-plugin:publishToMavenLocal
# or publish to Maven Central
./gradlew :plugins:my-plugin:publish

Best Practices

1. Keep Plugins Focused

βœ… Good: Single responsibility

class LoggingPlugin // Only handles logging
class RetryPlugin   // Only handles retries

❌ Bad: Multiple responsibilities

class LoggingAndRetryPlugin // Does too much

2. Make Configuration Optional

class MyPlugin(private val config: Config = Config()) : BlueFalconPlugin {
    class Config : PluginConfig() {
        var enabled: Boolean = true  // Sensible defaults
        var level: Int = 1
    }
}

3. Avoid Blocking Operations

βœ… 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
}

4. Handle Errors Gracefully

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}")
        }
    )
}

5. Document Your Plugin

Include:


Advanced Topics

Custom Plugin Interfaces

Extend BlueFalconPlugin for custom hooks:

interface AdvancedPlugin : BlueFalconPlugin {
    suspend fun onServiceDiscovered(service: BluetoothService) {}
    suspend fun onNotificationReceived(characteristic: BluetoothCharacteristic) {}
}

Plugin Dependencies

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")
    }
}

Conditional Plugin Loading

val blueFalcon = BlueFalcon {
    engine = AndroidBlueFalconEngine(context)
    
    if (BuildConfig.DEBUG) {
        install(LoggingPlugin(LoggingPlugin.Config().apply {
            level = LogLevel.DEBUG
        }))
    }
    
    install(RetryPlugin(RetryPlugin.Config()))
}

Plugin Communication

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}")
    }
}

Resources


Happy Plugin Development! πŸ”Œ

Need help? Open an issue or discussion on GitHub.