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
This commit is contained in:
Claude
2026-03-30 02:18:24 +00:00
parent 57aec95e8d
commit 28f68382dd
2 changed files with 174 additions and 3 deletions
@@ -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 {
@@ -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<BlePeer> = 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)
}