diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/AndroidBleTransport.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/AndroidBleTransport.kt similarity index 98% rename from quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/AndroidBleTransport.kt rename to quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/AndroidBleTransport.kt index 8b646ddcf..640670baa 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/AndroidBleTransport.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/AndroidBleTransport.kt @@ -18,7 +18,7 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.transport import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter @@ -41,6 +41,10 @@ import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.Context import android.os.ParcelUuid +import com.vitorpamplona.quartz.nipBEBle.AndroidBleTransportContract +import com.vitorpamplona.quartz.nipBEBle.BleConfig +import com.vitorpamplona.quartz.nipBEBle.BlePeer +import com.vitorpamplona.quartz.nipBEBle.BleRole import com.vitorpamplona.quartz.utils.Log import java.util.UUID diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrMesh.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrMesh.kt index edcd8bd9f..7641c0af1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrMesh.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrMesh.kt @@ -24,6 +24,11 @@ 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 +import com.vitorpamplona.quartz.nipBEBle.relay.BleMeshListener +import com.vitorpamplona.quartz.nipBEBle.relay.BleMeshManager +import com.vitorpamplona.quartz.nipBEBle.relay.BleNostrServer +import com.vitorpamplona.quartz.nipBEBle.transport.BleTransport +import com.vitorpamplona.quartz.nipBEBle.transport.BleTransportListener /** * Easy-to-use facade for NIP-BE Nostr BLE mesh networking. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/README.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/README.md new file mode 100644 index 000000000..dad393f67 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/README.md @@ -0,0 +1,145 @@ +# NIP-BE: Nostr BLE Communications Protocol + +Kotlin Multiplatform implementation of [NIP-BE](https://github.com/nostr-protocol/nips/blob/master/BE.md) +for peer-to-peer Nostr event synchronization over Bluetooth Low Energy. + +Compatible with [KoalaSat/samiz](https://github.com/KoalaSat/samiz). + +## Quick Start (Android) + +```kotlin +// 1. Create the mesh (auto-wires transport) +val mesh = BleNostrMesh(AndroidBleTransport(context)) + +// 2. Listen for events from nearby peers +mesh.onEvent { event, peer -> + Log.d("BLE", "Received kind ${event.kind} from ${peer.deviceUuid}") + myDatabase.save(event) +} + +// 3. Optional: track peer connectivity +mesh.onPeerConnected { peer -> updatePeerCount(mesh.connectedPeers().size) } +mesh.onPeerDisconnected { peer -> updatePeerCount(mesh.connectedPeers().size) } +mesh.onError { error -> Log.e("BLE", error) } + +// 4. Start (begins advertising + scanning) +mesh.start() + +// 5. Broadcast events to all connected peers +mesh.broadcast(mySignedEvent) + +// 6. Stop when done +mesh.stop() +``` + +### Required Android Permissions + +```xml + + + + +``` + +Request these at runtime before calling `mesh.start()`. + +## How It Works + +When two devices running NIP-BE discover each other: + +1. **Discovery** - Both devices advertise and scan for the Nostr BLE service UUID +2. **Role assignment** - The device with the higher UUID becomes the GATT Server (relay), + the other becomes the GATT Client +3. **Connection** - The client connects to the server's GATT service +4. **Messaging** - NIP-01 JSON messages are DEFLATE-compressed, split into chunks, + and exchanged via BLE characteristics +5. **Event spreading** - New events are automatically forwarded to all connected peers + +## Package Structure + +``` +nipBEBle/ + BleNostrMesh.kt # Easy-to-use facade (start here) + BleConfig.kt # NIP-BE constants (UUIDs, sizes) + BlePeer.kt # Peer data model + BleRole.kt # Role assignment (SERVER/CLIENT) + + protocol/ # Wire format + BleMessageChunker.kt # DEFLATE compression + chunk splitting/joining + BleChunkAssembler.kt # Reassembles incoming chunks into messages + + transport/ # Platform BLE abstraction + BleTransport.kt # Interface for BLE operations + AndroidBleTransport.kt # Android implementation (androidMain) + + relay/ # Nostr relay protocol over BLE + BleNostrClient.kt # Client side (implements IRelayClient) + BleNostrServer.kt # Server side (receives commands, sends messages) + BleMeshManager.kt # Orchestrates discovery, connections, broadcasting +``` + +## Advanced Usage + +### Use a BLE peer as a standard relay client + +`BleNostrClient` implements `IRelayClient`, so you can use BLE peers alongside +WebSocket relays in the existing subscription infrastructure: + +```kotlin +val relayClient = mesh.getRelayClient(peer.deviceUuid) +relayClient?.sendIfConnected(ReqCmd("sub1", listOf(filter))) +``` + +### Access raw protocol messages + +```kotlin +mesh.onMessage { relay, msg -> + // Nostr messages received when acting as client (EVENT, EOSE, OK, etc.) +} + +mesh.onCommand { server, cmd -> + // Nostr commands received when acting as server (REQ, EVENT, CLOSE, etc.) +} +``` + +### Implement a custom BLE transport + +For platforms without a built-in transport, implement `BleTransport`: + +```kotlin +class MyPlatformBleTransport : BleTransport { + override val deviceUuid = UUID.randomUUID().toString() + override fun startAdvertising() { /* ... */ } + override fun startScanning() { /* ... */ } + // ... other methods +} +``` + +### Use BleMeshManager directly + +For full control over the relay protocol layer: + +```kotlin +val transport = AndroidBleTransport(context) +val manager = BleMeshManager(transport, object : BleMeshListener { + override fun onEventReceived(event: Event, fromPeer: BlePeer) { /* ... */ } +}) +transport.setListener(manager) +manager.start() +``` + +## Wire Protocol + +Messages follow [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) JSON format, +compressed with DEFLATE and split into chunks: + +``` +[chunk index (1 byte)][payload (up to 500 bytes)][total chunks (1 byte)] +``` + +| GATT Characteristic | UUID | Direction | +|---------------------|------|-----------| +| Write | `87654321-0000-1000-8000-00805f9b34fb` | Client -> Server | +| Read (Notify) | `12345678-0000-1000-8000-00805f9b34fb` | Server -> Client | + +Service UUID: `0000180f-0000-1000-8000-00805f9b34fb` diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleChunkAssembler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleChunkAssembler.kt similarity index 97% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleChunkAssembler.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleChunkAssembler.kt index 941681edd..dfd154708 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleChunkAssembler.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleChunkAssembler.kt @@ -18,7 +18,7 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.protocol /** * Accumulates incoming BLE chunks for a single message and reassembles diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMessageChunker.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleMessageChunker.kt similarity index 97% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMessageChunker.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleMessageChunker.kt index 2a2c5b0f3..b26a227a5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMessageChunker.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleMessageChunker.kt @@ -18,8 +18,9 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.protocol +import com.vitorpamplona.quartz.nipBEBle.BleConfig import com.vitorpamplona.quartz.utils.Deflate /** diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMeshManager.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleMeshManager.kt similarity index 96% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMeshManager.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleMeshManager.kt index d7a68989b..f2f4a6574 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMeshManager.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleMeshManager.kt @@ -18,7 +18,7 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.relay import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener @@ -27,6 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd +import com.vitorpamplona.quartz.nipBEBle.BlePeer +import com.vitorpamplona.quartz.nipBEBle.BleRole +import com.vitorpamplona.quartz.nipBEBle.assignRole +import com.vitorpamplona.quartz.nipBEBle.protocol.BleMessageChunker +import com.vitorpamplona.quartz.nipBEBle.transport.BleTransport +import com.vitorpamplona.quartz.nipBEBle.transport.BleTransportListener import com.vitorpamplona.quartz.utils.Log /** diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrClient.kt similarity index 94% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrClient.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrClient.kt index d0deb8ce6..371b701d7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrClient.kt @@ -18,13 +18,18 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.relay import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nipBEBle.BleConfig +import com.vitorpamplona.quartz.nipBEBle.BlePeer +import com.vitorpamplona.quartz.nipBEBle.protocol.BleChunkAssembler +import com.vitorpamplona.quartz.nipBEBle.protocol.BleMessageChunker +import com.vitorpamplona.quartz.nipBEBle.transport.BleTransport import com.vitorpamplona.quartz.utils.Log /** diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrServer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrServer.kt similarity index 94% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrServer.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrServer.kt index 3e6554b85..2a0b493e6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleNostrServer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrServer.kt @@ -18,11 +18,16 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.relay import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command +import com.vitorpamplona.quartz.nipBEBle.BleConfig +import com.vitorpamplona.quartz.nipBEBle.BlePeer +import com.vitorpamplona.quartz.nipBEBle.protocol.BleChunkAssembler +import com.vitorpamplona.quartz.nipBEBle.protocol.BleMessageChunker +import com.vitorpamplona.quartz.nipBEBle.transport.BleTransport import com.vitorpamplona.quartz.utils.Log /** diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleTransport.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/BleTransport.kt similarity index 97% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleTransport.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/BleTransport.kt index 417fc5524..354d1d49f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/BleTransport.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/BleTransport.kt @@ -18,7 +18,9 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.transport + +import com.vitorpamplona.quartz.nipBEBle.BlePeer /** * Platform-agnostic interface for BLE transport operations. diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/BleChunkAssemblerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleChunkAssemblerTest.kt similarity index 98% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/BleChunkAssemblerTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleChunkAssemblerTest.kt index 691d3b538..523ff36ec 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/BleChunkAssemblerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleChunkAssemblerTest.kt @@ -18,7 +18,7 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.protocol import kotlin.test.Test import kotlin.test.assertEquals diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMessageChunkerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleMessageChunkerTest.kt similarity index 99% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMessageChunkerTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleMessageChunkerTest.kt index 17f7a6468..e8c6d997e 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/BleMessageChunkerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipBEBle/protocol/BleMessageChunkerTest.kt @@ -18,7 +18,7 @@ * 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 +package com.vitorpamplona.quartz.nipBEBle.protocol import kotlin.test.Test import kotlin.test.assertEquals