Skip to content

Wavesonics/MeshCoreKmp

Repository files navigation

MeshCoreKmp

Maven Central build-and-test

KMP badge-android badge-ios

A Kotlin Multiplatform MeshCore Library.

Provides high-level, ergonomic access to to send and receive data via Bluetooth MeshCore Companions.

Run Sample App

  • Android: open project in Android Studio and run the sample app
  • iOS: open 'sample/iosApp/iosApp.xcodeproj' in Xcode and run the sample app

API Usage

Setup

Add the dependency to your build.gradle file:

implementation("com.darkrockstudios:meshcore:0.12.0")

Create a BlueFalconBleAdapter with a platform-specific BlueFalcon instance, then pass it to DeviceScanner:

// Android
val blueFalcon = BlueFalcon(context = application)
val scanner = DeviceScanner(BlueFalconBleAdapter(blueFalcon))

// iOS
val blueFalcon = BlueFalcon(context = UIApplication.sharedApplication)
val scanner = DeviceScanner(BlueFalconBleAdapter(blueFalcon))

Scanning for Devices

// Start scanning for MeshCore companion devices                                                                                                                                                                                                                                                                            
scanner.startScan(scope = coroutineScope)                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                            
// Observe discovered devices via StateFlow                                                                                                                                                                                                                                                                                 
scanner.discoveredDevices.collect { devices ->                                                                                                                                                                                                                                                                              
    devices.forEach { device ->                                                                                                                                                                                                                                                                                             
        println("Found: ${device.name} (${device.identifier}) rssi=${device.rssi}")                                                                                                                                                                                                                                         
    }                                                                                                                                                                                                                                                                                                                       
}                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                            
// Optionally filter by name prefix                                                                                                                                                                                                                                                                                         
scanner.startScan(                                                                                                                                                                                                                                                                                                          
    filter = ScanFilter(namePrefix = "MeshCore"),                                                                                                                                                                                                                                                                           
    scope = coroutineScope,                                                                                                                                                                                                                                                                                                 
)                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                            
// Stop scanning when done                                                                                                                                                                                                                                                                                                  
scanner.stopScan()                                                                                                                                                                                                                                                                                                          

Connecting to a Device

val connection = scanner.connect(                                                                                                                                                                                                                                                                                           
    device = selectedDevice,                                                                                                                                                                                                                                                                                                
    scope = coroutineScope,                                                                                                                                                                                                                                                                                                 
    config = ConnectionConfig(                                                                                                                                                                                                                                                                                              
        appName = "MyApp",                                                                                                                                                                                                                                                                                                  
        autoSyncTime = true,                                                                                                                                                                                                                                                                                                
        autoFetchChannels = true,                                                                                                                                                                                                                                                                                           
        autoPollMessages = true,                                                                                                                                                                                                                                                                                            
    ),                                                                                                                                                                                                                                                                                                                      
)                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                            
// Observe connection state                                                                                                                                                                                                                                                                                                 
connection.connectionState.collect { state ->                                                                                                                                                                                                                                                                               
    when (state) {                                                                                                                                                                                                                                                                                                          
        is ConnectionState.Connected -> println("Connected!")                                                                                                                                                                                                                                                               
        is ConnectionState.Connecting -> println("Connecting...")                                                                                                                                                                                                                                                           
        is ConnectionState.Disconnected -> println("Disconnected")                                                                                                                                                                                                                                                          
        is ConnectionState.Error -> println("Error: ${state.message}")                                                                                                                                                                                                                                                      
    }                                                                                                                                                                                                                                                                                                                       
}                                                                                                                                                                                                                                                                                                                           

Sending Messages

// Send a message on a channel
val confirmation = connection.sendChannelMessage(
	channelIndex = 0,
	text = "Hello mesh network!",
)
println("Sent! Ack expected: ${confirmation.expectedAck}")

// Wait for the acknowledgment
connection.acks.first { it == confirmation.expectedAck }
println("Message acknowledged!")

Direct Messages

// Send a direct message to a contact by their 6-byte public key prefix
val confirmation = connection.sendDirectMessage(
	publicKeyPrefix = contactKeyPrefix,
	text = "Hello!",
)
println("Sent! Ack expected: ${confirmation.expectedAck}")

Send and Await Acknowledgment

Use sendAndAwaitAck to send a message and suspend until the ack is received (or timeout). Returns a Result<MessageSentConfirmation> so you can handle timeouts without exceptions:

// Sends the message and waits for the ack in one call
connection.sendAndAwaitAck {
	sendDirectMessage(
		publicKeyPrefix = contactKeyPrefix,
		text = "Hello!",
	)
}.onSuccess { confirmation ->
	println("Message acknowledged! Ack: ${confirmation.expectedAck}")
}.onFailure { error ->
	println("Ack timed out: ${error.message}")
}

This works with any message-sending method that returns a MessageSentConfirmation:

val result = connection.sendAndAwaitAck {
	sendDirectMessage(publicKey = contactKey, text = "Hello!")
}
val confirmation = result.getOrNull() ?: return // handle timeout
// Note: Channel messages are broadcast and don't receive acks.

Receiving Messages

// Incoming messages are automatically polled and emitted                                                                                                                                                                                                                                                                   
connection.incomingMessages.collect { message ->                                                                                                                                                                                                                                                                            
    when (message) {                                                                                                                                                                                                                                                                                                        
        is ReceivedMessage.ChannelMessage -> {                                                                                                                                                                                                                                                                              
            println("[Channel ${message.channelIndex}] ${message.text}")                                                                                                                                                                                                                                                    
        }                                                                                                                                                                                                                                                                                                                   
        is ReceivedMessage.ContactMessage -> {                                                                                                                                                                                                                                                                              
            println("[DM from ${message.publicKeyPrefix}] ${message.text}")                                                                                                                                                                                                                                                 
        }                                                                                                                                                                                                                                                                                                                   
    }                                                                                                                                                                                                                                                                                                                       
}                                                                                                                                                                                                                                                                                                                           

Raw Binary Data

Send and receive arbitrary binary payloads over the mesh, useful for custom application protocols.

// Send raw data (broadcast/flood) with an optional path
connection.sendRawData(payload = byteArrayOf(0x01, 0x02, 0x03))

// Listen for incoming raw data from the mesh
connection.incomingRawData.collect { rawData ->
    println("Raw data received (SNR: ${rawData.snr}, RSSI: ${rawData.rssi})")
    println("Payload: ${rawData.payload.size} bytes")
}

Binary Requests

Send binary data to a specific contact by public key, with correlated responses.

// Send a binary request to a specific contact
val confirmation = connection.sendBinaryRequest(
    publicKey = contactPublicKey, // 32-byte public key
    requestData = myRequestPayload,
)
println("Binary request sent, tag: ${confirmation.expectedAck}")

// Listen for binary responses (correlated by tag)
connection.incomingBinaryResponses.collect { response ->
    println("Binary response for tag=${response.tag}: ${response.responseData.size} bytes")
}

Device Info and Stats

// Device info is fetched automatically on connect                                                                                                                                                                                                                                                                          
val info = connection.deviceInfo.value                                                                                                                                                                                                                                                                                      
println("Firmware: ${info?.firmwareBuild} | Model: ${info?.model}")                                                                                                                                                                                                                                                         
                                                                                                                                                                                                                                                                                                                            
// Battery                                                                                                                                                                                                                                                                                                                  
val battery = connection.getBattery()                                                                                                                                                                                                                                                                                       
println("Battery: ${battery.levelPercent}%")                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                            
// Radio stats                                                                                                                                                                                                                                                                                                              
val radio = connection.getRadioStats()                                                                                                                                                                                                                                                                                      
println("RSSI: ${radio.lastRssiDbm} dBm, SNR: ${radio.lastSnrDb} dB")                                                                                                                                                                                                                                                       

Channel Management

// Channels are auto-fetched on connect, observe via StateFlow                                                                                                                                                                                                                                                              
val channels = connection.channels.value                                                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                                                                                            
// Or fetch manually                                                                                                                                                                                                                                                                                                        
val allChannels = connection.getAllChannels()                                                                                                                                                                                                                                                                               
allChannels.forEach { ch ->                                                                                                                                                                                                                                                                                                 
    println("Channel ${ch.index}: ${ch.name} (empty=${ch.isEmpty})")                                                                                                                                                                                                                                                        
}                                                                                                                                                                                                                                                                                                                           

Disconnecting

connection.disconnect()                                                                                                                                                                                                                                                                                                     

About

A Kotlin Multiplatform library for connecting to and communicating with MeshCore companion nodes.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages