Kotlin Multiplatform SDK for Meshtastic mesh-network radios. One library. Connects to Meshtastic devices over BLE, TCP, or USB-serial from Android, JVM, and iOS. Wasm/browser is on the roadmap — see
docs/future/wasm-rpc-roadmap.md.
📚 API Reference (Dokka) — published from main.
Status: pre-1.0. APIs may change between minor versions; see
docs/versioning.md. Trackdocs/for spec evolution.
A Kotlin library that talks to Meshtastic radios using the device's PhoneAPI (the same protocol the official Meshtastic-Android and Meshtastic-Apple apps use). It owns the wire-protocol details — handshake, NodeDB, ACK correlation, channel decryption, deferred-decrypt, retries — and exposes them as ergonomic Kotlin coroutines + flows + sealed types.
Use it to build:
- Companion apps (Android, iOS, desktop) that don't want to reinvent the protocol.
- Headless gateways and mesh telemetry collectors on JVM/server.
- (post-1.0) Web tools (wasm) via a sidecar RPC server — design parked in
docs/future/wasm-rpc-roadmap.md.
This SDK does not include UI components, navigation, or storage policy — see docs/decisions/000-charter.md for the explicit non-goals.
// build.gradle.kts
dependencies {
implementation("org.meshtastic:sdk-core:0.1.0")
implementation("org.meshtastic:sdk-transport-tcp:0.1.0") // pick a transport
implementation("org.meshtastic:sdk-storage-sqldelight:0.1.0") // pick a storage
}Heads up:
0.1.0is not yet on Maven Central — the first release tag has not been cut. Until then, use the snapshot repository below.
Available transport modules: transport-ble, transport-tcp, transport-serial (single multiplatform module covering JVM and Android).
Available storage modules: storage-sqldelight. Or roll your own StorageProvider.
Optionally, depend on sdk-bom to align all module versions; see bom/README.md for usage and docs/versioning.md for the versioning policy.
Every push to main publishes 0.1.0-SNAPSHOT (and successor versions) to the Sonatype Central snapshot repository:
// settings.gradle.kts (or root build.gradle.kts repositories block)
dependencyResolutionManagement {
repositories {
mavenCentral()
maven("https://central.sonatype.com/repository/maven-snapshots/") {
mavenContent { snapshotsOnly() }
}
}
}
// build.gradle.kts
dependencies {
implementation("org.meshtastic:sdk-core:0.1.0-SNAPSHOT")
}Snapshots are rebuilt on every commit; pin to a specific commit by checking out a Git submodule for reproducibility.
Roadmap (post-1.0, non-breaking adds): transport-mqtt-proxy, transport-rpc, host-rpc-server, wasmJs browser support — see docs/future/wasm-rpc-roadmap.md.
Full module matrix: docs/architecture/module-graph.md.
import org.meshtastic.sdk.storage.sqldelight.SqlDelightStorageProvider
import org.meshtastic.sdk.RadioClient
import org.meshtastic.sdk.SendOutcome
import org.meshtastic.sdk.transport.tcp.TcpTransport
suspend fun run() {
val client = RadioClient.Builder()
.transport(TcpTransport(host = "meshtastic.local", port = 4403))
.storage(SqlDelightStorageProvider(baseDir = "/tmp")) // empty string = in-memory
.build()
client.connect() // throws MeshtasticException on failure
val handle = client.sendText("hello mesh")
when (val outcome = handle.await()) { // suspends until terminal
SendOutcome.Success -> println("acked or rebroadcast heard")
is SendOutcome.Failure -> println("failed: ${outcome.reason}")
}
// Or observe progress:
// handle.state.collect { println(it) } // Queued → Sent → Acked/Delivered/Failed
}To observe NodeDB changes alongside sending, run the collector in its own coroutine so it doesn't block the rest of your flow. client.nodes never completes — collect it from a launch { … } (or use take(N) for a bounded sample):
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.meshtastic.sdk.NodeChange
import org.meshtastic.sdk.RadioClient
suspend fun observeAndSend(client: RadioClient) = coroutineScope {
val nodesJob = launch {
client.nodes.collect { change ->
when (change) {
is NodeChange.Snapshot -> println("seeded with ${change.nodes.size} nodes")
is NodeChange.Added -> println("+ ${change.node.user?.long_name}")
is NodeChange.Updated -> println("~ ${change.node.user?.long_name} (${change.changed})")
is NodeChange.Removed -> println("- ${change.nodeId}")
}
}
}
client.sendText("hello mesh").await() // not blocked by the collector
nodesJob.cancel() // stop observing when you're done
}Notes:
- Wire-generated proto fields are snake_case (e.g.
user.long_name, notuser.longName). - On Android, also set
AndroidContextHolder.context = applicationContextonce in yourApplication.onCreate()before constructing aSqlDelightStorageProvider— see the integration guide. - For Android BLE, USB-serial setup, and
SqlDelightStorageProviderAndroidContextwiring, see the integration guide.
The example above wires TcpTransport. The other two transports plug
in identically — only the Builder.transport(...) line changes. Each
ships in its own artifact; pull in the one(s) you need.
// BLE — multiplatform (Android / iOS / JVM-desktop via Kable).
// Android: see AndroidManifest checklist + permission notes in the integration guide.
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import com.juul.kable.Peripheral
import com.juul.kable.Scanner
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.meshtastic.sdk.transport.ble.BleTransport
import org.meshtastic.sdk.storage.sqldelight.SqlDelightStorageProvider
import org.meshtastic.sdk.RadioClient
class BleQuickStartActivity : ComponentActivity() {
private val permissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
) { granted -> if (granted.values.all { it }) lifecycleScope.launch { connect() } }
override fun onStart() {
super.onStart()
permissions.launch(arrayOf(
android.Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_CONNECT,
))
}
private suspend fun connect() {
// Filter by the Meshtastic GATT service UUID in real code.
val ad = Scanner().advertisements.first()
val client = RadioClient.Builder()
.transport(BleTransport(Peripheral(ad), address = ad.identifier.toString()))
.storage(SqlDelightStorageProvider(baseDir = filesDir.absolutePath))
.build()
client.connect()
client.events.collect { println(it) }
}
}Full BLE setup (manifest entries, foreground service for API 34+, iOS
Info.plist keys, JVM/desktop notes) is in the
Android setup checklist
and the per-transport BLE section of
the integration guide.
| Symptom | Likely cause | Fix |
|---|---|---|
connect() throws MeshtasticException.TransportFailure on TCP |
Host unreachable or firmware TCP API disabled | Verify the radio's IP/hostname and that WiFi/Ethernet is enabled. See TCP setup. |
connect() hangs or fails repeatedly on BLE |
Device not bonded with the OS | Pair the radio in your OS Bluetooth settings before connect(). See BLE platform requirements. |
JvmSerialPorts.open(...) throws permission denied |
Serial device permission not granted | On Linux, add the user to dialout. On Android, request USB permission first. See Serial (USB). |
sendText returns SendFailure immediately on long messages |
Payload exceeds the 228-byte text limit | Split the text or send a smaller payload. See Sending messages. |
connect() throws MeshtasticException.HandshakeTimeout |
want_config_id reply never arrived from the device |
Power-cycle the radio, verify firmware ≥ 2.3, then retry. See Build a RadioClient. |
// USB-serial — JVM (jSerialComm) and Android (usb-serial-for-android)
import org.meshtastic.sdk.transport.serial.JvmSerialPorts // androidMain: AndroidSerialPorts
val portName = JvmSerialPorts.list().first() // e.g. "/dev/tty.usbserial-1410"
val transport = JvmSerialPorts.open(portName, baudRate = 115200)// TCP / WiFi — all targets
import org.meshtastic.sdk.transport.tcp.TcpTransport
val transport = TcpTransport(host = "meshtastic.local", port = 4403)Full per-platform setup (Android runtime permissions, foreground-service
requirements on API 34+, USB intent filters, iOS Info.plist keys) is
in the integration guide. For lifecycle,
DI, and R8 patterns see the consumer guides index.
Full API reference: docs/api-reference.md.
All published modules target the same Kotlin Multiplatform target set via the shared
meshtastic.kmp.library convention plugin: jvm, androidTarget (minSdk = 26),
iosArm64, iosX64, and iosSimulatorArm64. Per-module behaviour is summarised below;
see docs/architecture/module-graph.md for the
authoritative dependency graph.
| Module | JVM | Android (minSdk 26) |
iOS Arm64 | iOS Sim Arm64 | iOS X64 | Notes |
|---|---|---|---|---|---|---|
core |
✓ | ✓ | ✓ | ✓ | ✓ | Pure-Kotlin engine; no platform deps beyond :proto. |
proto |
✓ | ✓ | ✓ | ✓ | ✓ | Generated kotlinx.serialization proto types. |
transport-ble |
✓¹ | ✓ | ✓ | ✓ | ✓ | ¹ JVM uses BlueZ/BleZ-style adapter where available; see module README. |
transport-tcp |
✓ | ✓ | ✓ | ✓ | ✓ | Built on kotlinx-io sockets. |
transport-serial |
✓ | ✓ | — | — | — | iOS targets compile (empty actuals) but no USB-serial API on iOS. |
storage-sqldelight |
✓ | ✓ | ✓ | ✓ | ✓ | SQLDelight native driver on iOS, JDBC on JVM/Android. |
testing |
✓ | ✓ | ✓ | ✓ | ✓ | In-memory fakes + TestClock; safe to use in commonTest. |
✓ = supported and exercised in CI; — = not applicable (no platform API for this transport).
wasmJs (browser) remains on the post-1.0 roadmap — see
docs/future/wasm-rpc-roadmap.md.
docs/SPEC.md— master implementation plan (mission, scope, phases, locked defaults).docs/protocol.md— wire-level protocol bible (PhoneAPI, MeshPacket, channel encryption, MQTT proxy, …).docs/api-reference.md— full public Kotlin signatures with KDoc.docs/error-taxonomy.md— what throws, what returnsAdminResult, what surfaces asMeshEvent.docs/glossary.md— vocabulary (NodeNum vs NodeId, request_id vs packet_id, channel hash, …).docs/architecture/— handshake FSM, engine actor dataflow, storage, module dependency graph (Mermaid diagrams).docs/consumer-guides/— host-app integration recipes (reactive lifecycle, Hilt, MVVM, R8/Proguard).docs/decisions/— ADRs (charter, API shape, tooling, licensing, multi-module rationale).docs/roadmap.md— Phase 2 / Phase 3 deferred items (stubs, no-ops, missing observables).docs/versioning.md— SemVer + ABI policy.docs/security.md— threat model + scope.docs/manual-tests.md— device-conformance suite (CI cannot run these).docs/ci-cd.md— GitHub Actions workflows.docs/samples.md— what eachsamples/*demonstrates.
git clone --recurse-submodules git@github.com:meshtastic/meshtastic-sdk.git
cd meshtastic-sdk
./gradlew check # build + test + lint + checkKotlinAbi + detekt + :core:verifyModuleBoundaryRequirements:
- JDK 21 (
sdk install java 21-tem). - Android SDK with API 35 platform if building Android targets (set
ANDROID_HOME). - Xcode + iOS 14+ SDK if building iOS targets (mac only).
- Android API 26+ (Android 8.0 Oreo). All
sdk-*Android artifacts pinminSdk = 26. Apps withminSdk < 26will fail to resolve the dependency at build time, and reflection-based loaders on older OS versions will fail at runtime — there is no graceful fallback. See the Android setup checklist. - iOS 14+ for the BLE/TCP transports.
- JDK 17+ runtime for JVM consumers (bytecode is JDK 17, toolchain is JDK 21).
Local commands: see docs/ci-cd.md.
We use the Developer Certificate of Origin (DCO). Sign every commit:
git commit -s -m "Your message"See CONTRIBUTING.md for the full flow.
GPL-3.0-only. See LICENSE and docs/decisions/004-licensing.md.
meshtastic/firmware— device-side reference (read-only behavior anchor here).meshtastic/protobufs— wire schema (vendored here as a submodule).meshtastic/Meshtastic-Android,meshtastic/Meshtastic-Apple— flagship apps; cross-validation references.meshtastic/mqtt-client— sibling KMP library for direct MQTT broker use.