# Peripheral Echo Server Example This standalone common Kotlin example hosts a small GATT echo server with the production `BlueFalconPeripheral` API. It demonstrates application-owned lifecycle, explicit ATT request routing, and notification backpressure through `QueuePlugin`. The current peripheral backends support Android, iOS, and macOS. Other Blue Falcon targets can add peripheral backends later; this example does not claim JVM server support. ## Dependencies Use matching versions of the peripheral module and queue plugin in `commonMain`: ```kotlin commonMain.dependencies { implementation("dev.bluefalcon:blue-falcon-peripheral:") implementation("dev.bluefalcon:blue-falcon-plugin-queue:") } ``` Copy [`src/PeripheralEchoServer.kt`](src/PeripheralEchoServer.kt) into your common source set, then create exactly one server for each application-owned peripheral manager: ```kotlin import dev.bluefalcon.example.peripheral.PeripheralEchoServer import dev.bluefalcon.peripheral.android.createBlueFalconPeripheral import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch // Android application startup val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) val peripheral = createBlueFalconPeripheral(applicationContext) val server = PeripheralEchoServer(peripheral, applicationScope) applicationScope.launch { server.start() } ``` ```kotlin import dev.bluefalcon.example.peripheral.PeripheralEchoServer import dev.bluefalcon.peripheral.apple.createBlueFalconPeripheral import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch // iOS/macOS application startup val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) val peripheral = createBlueFalconPeripheral() val server = PeripheralEchoServer(peripheral, applicationScope) applicationScope.launch(start = CoroutineStart.UNDISPATCHED) { server.start() } ``` ## Platform startup and ownership ### Android For Android 11 (API 30) and lower, declare the legacy `BLUETOOTH` and `BLUETOOTH_ADMIN` permissions for the peripheral role. For Android 12 (API 31) and higher, declare and request `BLUETOOTH_ADVERTISE` and `BLUETOOTH_CONNECT` at runtime. `BLUETOOTH_SCAN` and location permissions are central-scanning concerns rather than GATT-server requirements; whether scanning requires location depends on the Android version and how the application declares its scan usage. Create the manager with the application context. If the server must remain available while the app is not visible, own its lifecycle from a foreground service rather than a screen or short-lived view model. That paragraph is architectural guidance, not a complete manifest recipe: the target SDK and current Android platform rules determine the required connected-device foreground-service type, permissions, and background-start eligibility. ### Apple Create the peripheral manager and launch `server.start()` during `application(_:didFinishLaunchingWithOptions:)` or equivalent early application startup. Constructing the factory alone is insufficient: the CoreBluetooth peripheral manager and its restoration options are opened by `start()`. CoreBluetooth restoration requires this to happen immediately with the same stable restoration identifier: ```kotlin const val restorationIdentifier = "dev.bluefalcon.example.echo-peripheral" ``` `CoroutineStart.UNDISPATCHED` begins `server.start()` inline on the application-owned main scope, reaching CoreBluetooth initialization before the first suspension and before the startup callback returns. The scope must remain alive for the server's lifetime. Do not wait for lazy UI or view-model initialization when restoration is required. Keep the application-owned manager, scope, and `PeripheralEchoServer` alive for the application's BLE lifetime on both iOS and macOS. ## Exercise the server The server advertises as `Blue Falcon Echo` and exposes: - Service: `84f7e120-63fd-4f79-8b08-5b9780a36a94` - Characteristic: `84f7e121-63fd-4f79-8b08-5b9780a36a94` - Properties: read, write, write without response, notify, and indicate - Initial value: `Hello from Blue Falcon` Using a BLE client: 1. Connect to `Blue Falcon Echo` and discover the service and characteristic above. 2. Write bytes to the characteristic. A non-zero offset overlays the current value. 3. Read the characteristic to receive the updated echo value. 4. Subscribe to notifications or indications. 5. From the application, call `server.notifySubscribers(payload)`. `notifySubscribers` returns one typed result per subscribed session: - `QueueSendResult.Sent` - `QueueSendResult.QueueFull` - `QueueSendResult.PayloadTooLarge` - `QueueSendResult.Disconnected` - `QueueSendResult.Unsupported` - `QueueSendResult.Failed(cause)` `Sent` means the local platform or its bounded queue accepted/sent the update. It is not an application-level acknowledgement from the remote peer. The server snapshots the currently subscribed sessions and enqueues for them concurrently, so one slow peer does not prevent the other peers from being offered the payload. ## Lifecycle and transport boundary `start()` can follow `stop()`; stopping advertising and the GATT server is restartable. `close()` is terminal and idempotent: it cancels the request collector and closes the peripheral manager. Calls to `start()` or `stop()` after `close()` are rejected; create a new manager and server instead. The caller-provided scope must be active at construction and must outlive the server. Call `server.close()` before cancelling `applicationScope`. Cancelling the scope stops request routing, and a later `start()` is rejected rather than advertising without a request handler. Lifecycle operations are serialized. `notifySubscribers` does not hold the lifecycle lock; sends racing with `close()` complete with the queue's typed result, including `Disconnected` or `Failed(cause)`. QueuePlugin provides bounded per-session queuing and platform-readiness handling. It intentionally does not implement application-layer fragmentation, acknowledgements, retries, or persistence. The stored echo attribute is limited to 512 bytes, and larger writes receive `InvalidAttributeValueLength` without changing the stored value. This attribute limit is separate from notifications: payloads passed to `notifySubscribers` must also fit each individual session's `maximumUpdateValueLength`; larger notification payloads produce `PayloadTooLarge`. ## Verification The Compose Multiplatform example permanently includes this tutorial's `src` and `test` directories in its `commonTest` source set. This keeps the tutorial out of every production source set while making its tests part of the normal shared-module test lifecycle: ```bash cd ../ComposeMultiplatform-3.0-Example ./gradlew :shared:jvmTest --tests '*PeripheralEchoServerTest' ```