From 28f68382dde21a0f1e67411132094347e5e7daf5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 30 Mar 2026 02:18:24 +0000 Subject: [PATCH] feat: add BleNostrMesh facade for simpler developer API Before (4 steps, manual wiring): val transport = AndroidBleTransport(context) val mesh = BleMeshManager(transport, object : BleMeshListener { ... }) transport.setListener(mesh) mesh.start() After (2 steps, auto-wired): val mesh = BleNostrMesh(AndroidBleTransport(context)) mesh.onEvent { event, peer -> saveEvent(event) } mesh.start() The facade uses lambda callbacks instead of interface implementation, and auto-wires the transport listener via AndroidBleTransportContract. BleMeshManager remains available for advanced use cases. https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT --- .../quartz/nipBEBle/AndroidBleTransport.kt | 11 +- .../quartz/nipBEBle/BleNostrMesh.kt | 166 ++++++++++++++++++ 2 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrMesh.kt diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/AndroidBleTransport.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/AndroidBleTransport.kt index 982d74d91..8b646ddcf 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/AndroidBleTransport.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/AndroidBleTransport.kt @@ -58,8 +58,8 @@ import java.util.UUID * * Usage: * ```kotlin - * val transport = AndroidBleTransport(context) - * val mesh = BleMeshManager(transport, myListener) + * val mesh = BleNostrMesh(AndroidBleTransport(context)) + * mesh.onEvent { event, peer -> saveEvent(event) } * mesh.start() * ``` */ @@ -67,7 +67,8 @@ import java.util.UUID class AndroidBleTransport( private val context: Context, override val deviceUuid: String = UUID.randomUUID().toString().uppercase(), -) : BleTransport { +) : BleTransport, + AndroidBleTransportContract { private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter @@ -91,6 +92,10 @@ class AndroidBleTransport( this.listener = listener } + override fun setTransportListener(listener: BleTransportListener) { + this.listener = listener + } + override fun startAdvertising() { val advertiser = bluetoothAdapter?.bluetoothLeAdvertiser ?: run { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrMesh.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrMesh.kt new file mode 100644 index 000000000..edcd8bd9f --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrMesh.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipBEBle + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command + +/** + * Easy-to-use facade for NIP-BE Nostr BLE mesh networking. + * + * Wraps all the BLE protocol complexity behind a simple lambda-based API. + * Handles transport wiring, role assignment, message chunking, and + * event routing automatically. + * + * ## Quick start (Android) + * ```kotlin + * val mesh = BleNostrMesh(AndroidBleTransport(context)) + * + * mesh.onEvent { event, peer -> + * Log.d("BLE", "Got ${event.kind} from ${peer.deviceUuid}") + * saveToDatabase(event) + * } + * + * mesh.onPeerConnected { peer -> + * Log.d("BLE", "Peer connected: ${peer.deviceUuid}") + * } + * + * mesh.start() + * + * // Broadcast a signed event to all connected peers + * mesh.broadcast(myEvent) + * + * // Clean up + * mesh.stop() + * ``` + * + * ## Advanced usage + * For relay-level protocol access (REQ, filters, subscriptions), + * use [getRelayClient] to get an [IRelayClient] for a specific peer, + * or set [onMessage] / [onCommand] for raw protocol callbacks. + */ +class BleNostrMesh( + transport: BleTransport, +) { + /** Called when a Nostr event is received from any peer. */ + var onEvent: ((event: Event, fromPeer: BlePeer) -> Unit)? = null + + /** Called when a new peer connects. */ + var onPeerConnected: ((peer: BlePeer) -> Unit)? = null + + /** Called when a peer disconnects. */ + var onPeerDisconnected: ((peer: BlePeer) -> Unit)? = null + + /** Called when a new peer is discovered (before connection). */ + var onPeerDiscovered: ((peer: BlePeer) -> Unit)? = null + + /** Called on BLE errors. */ + var onError: ((error: String) -> Unit)? = null + + /** Called for raw Nostr messages (when acting as client). */ + var onMessage: ((relay: IRelayClient, msg: Message) -> Unit)? = null + + /** Called for raw Nostr commands (when acting as server). */ + var onCommand: ((server: BleNostrServer, cmd: Command) -> Unit)? = null + + private val manager = + BleMeshManager( + transport = transport, + listener = + object : BleMeshListener { + override fun onEventReceived( + event: Event, + fromPeer: BlePeer, + ) { + onEvent?.invoke(event, fromPeer) + } + + override fun onPeerConnected( + peer: BlePeer, + role: BleRole, + ) { + this@BleNostrMesh.onPeerConnected?.invoke(peer) + } + + override fun onPeerDisconnected(peer: BlePeer) { + this@BleNostrMesh.onPeerDisconnected?.invoke(peer) + } + + override fun onPeerDiscovered(peer: BlePeer) { + this@BleNostrMesh.onPeerDiscovered?.invoke(peer) + } + + override fun onMessageReceived( + relay: IRelayClient, + msgStr: String, + msg: Message, + ) { + onMessage?.invoke(relay, msg) + } + + override fun onCommandReceived( + server: BleNostrServer, + cmdStr: String, + cmd: Command, + ) { + onCommand?.invoke(server, cmd) + } + + override fun onError( + peer: BlePeer?, + error: String, + ) { + this@BleNostrMesh.onError?.invoke(error) + } + }, + ) + + init { + if (transport is AndroidBleTransportContract) { + transport.setTransportListener(manager) + } + } + + /** Starts BLE advertising and scanning for peers. */ + fun start() = manager.start() + + /** Stops all BLE operations and disconnects from all peers. */ + fun stop() = manager.stop() + + /** Broadcasts an event to all connected peers. */ + fun broadcast(event: Event) = manager.broadcastEvent(event) + + /** Returns all currently connected peers. */ + fun connectedPeers(): List = manager.connectedPeers() + + /** Returns the [IRelayClient] for a specific peer (for advanced protocol use). */ + fun getRelayClient(peerUuid: String): IRelayClient? = manager.getRelayClient(peerUuid) +} + +/** + * Contract for Android BLE transports that support automatic listener wiring. + * Implemented by [AndroidBleTransport] so that [BleNostrMesh] can self-configure. + */ +interface AndroidBleTransportContract { + fun setTransportListener(listener: BleTransportListener) +}