Skip to main content

Android Mesh SDK

SDK Version: 1.0.0

The Mesh SDK implements xTalk MeshWire messaging over a customer-supplied radio transport. It owns packet framing, encryption, flooding, acknowledgements, route learning, group text and voice messages, friend messaging, nearby discovery, location sharing, and group invitations. It does not open BLE/UART, authenticate a device, record audio, or render UI.

Dependency and entry points

Add the supplied Mesh SDK 1.0.0 AAR or Maven artifact and import:

import com.xtalk.mesh.sdk.AiTalkMeshRadioProfile
import com.xtalk.mesh.sdk.MeshPairingPsk
import com.xtalk.mesh.sdk.MeshSdk

The runtime requires Kotlin, coroutines, and OkHttp. Use the dependency metadata supplied with the release instead of copying a bare AAR without its dependencies.

Create the SDK

class CustomerMeshTransport(
private val radio: CustomerRadio,
) : MeshSdk.Transport {
override suspend fun sendRfPayload(bytes: ByteArray) {
radio.sendAndWaitForCompletion(bytes)
}

override suspend fun runAtCommand(command: String, timeoutMs: Long): List<String> =
radio.runAt(command, timeoutMs)
}

val mesh = MeshSdk(
scope = appScope,
transport = CustomerMeshTransport(radio),
initialConfig = MeshSdk.Config(
nodeId = localDeviceId,
defaultHopLimit = 3,
rebroadcastEnabled = true,
rebroadcastDelayMinMs = 200,
rebroadcastDelayMaxMs = 400,
rateMode = 7,
maxRfPayloadBytes = 105,
),
learnedRouteStore = routeStore,
)

MeshSdk.Transport.withRfSession(...) may be overridden when the host needs an exclusive RF lease. RfUse identifies the requested operation. LearnedRouteStore.load() and save(routes) are optional persistence hooks.

Receive and observe

Route every complete Mesh RF payload to the SDK exactly once. Remove any transport text prefix first.

mesh.handleInbound(raw = payload, snr = snr, rssi = rssi)

appScope.launch {
mesh.events.collect { event ->
when (event) {
is MeshSdk.Event.TextReceived -> showText(event.fromNode, event.text)
is MeshSdk.Event.VoiceReceived -> saveVoice(event.voice)
is MeshSdk.Event.Ack -> updateDelivery(event.packetId, event.ok)
is MeshSdk.Event.Timeout -> showTimeout(event.packetId)
else -> handleBusinessEvent(event)
}
}
}

Event includes TextReceived, VoiceReceived, VoiceRepairAckReceived, Ack, Timeout, TraceRouteResult, FriendDirectTextReceived, FriendDirectTextAck, MapNearbyPeer, MapFriendRequest, MapFriendAck, MapLocShareRequest, MapLocShareAck, MapLocHeartbeat, MapLocStop, MapGroupAddRequest, MapGroupAddAck, MapGroupAddJoinNotice, and MapGroupRosterSync. Receive models expose node IDs, packet/message IDs, group/channel information, RSSI/SNR when supplied, and a route summary.

Radio profile and pairing

AiTalkMeshRadioProfile.channels contains the authoritative 16 RF records. channel(channelId) returns one record or null. RF channel, Mesh publicChannel, and pairing pairChannel are separate domains.

val rf = requireNotNull(AiTalkMeshRadioProfile.channel(1))
// rf.frequencyHz == 470250000L; rf.bcnId == 1

val psk16 = MeshPairingPsk.derivePsk16(pairChannel = 25, key4 = "1234")

Pairing vector: channel 25, key 1234 produces 166bf40821e0aa173fdf424424e42b49. The pairing function derives a key only; it never changes RF.

Groups and keys

val groupId = GroupUtil.publicGroupId(channel = 1, withPrefix = false)
val channel = GroupUtil.channelFromGroupId(groupId)
val isPublic = GroupUtil.isPublicGroup(groupId)

mesh.setPublicGroupPsk(publicChannel = 1, psk = customerPsk16)
mesh.setFriendKey16(forNode = peerNodeId, key16 = friendKey16)

Pass null to either setter to remove the override. Public-group PSKs must be 16 or 32 bytes. Friend keys must be 16 bytes and are required by friend text, location sharing, and private group invitation APIs.

Text and voice

val textPacket = mesh.sendPublicText(
groupId = "10000001",
text = "hello mesh",
wantAck = true,
hopLimit = null,
)

val voicePackets = mesh.sendPublicVoiceAiCodecFrames(
groupId = "10000001",
frames41 = encodedFrames,
codecFamilyByte = 0,
autoPlay = false,
hopLimit = null,
wantAckLast = false,
)

Long text is fragmented automatically. Voice input is a list of already encoded 41-byte AiCodec frames; capture and codec ownership stay with the host.

Repairable voice uses:

val result = mesh.sendRepairablePublicVoiceAiCodecFrames(
groupId = "10000001",
frames41 = encodedFrames,
messageId = messageId,
selectedFragmentIndexes = intArrayOf(2, 3),
)

mesh.sendPublicVoiceRepairAck(
groupId = "10000001",
messageId = messageId,
totalFragments = result.totalFragments,
missingFragmentIndexes = intArrayOf(2, 3),
)

Nearby users and friends

mesh.updateMapUserName("Alice")
mesh.updateMapLocation(399123456, 1161234567, precisionBits = 24)

val peers = mesh.discoverNearbyMapUsers(
groupId = "10000001",
wantName = true,
wantPos = true,
hopLimit = 0,
onPeerFound = ::showPeer,
)

val requestId = mesh.sendMapFriendRequest(
groupId = "10000001",
toNode = peerNodeId,
pairChannel = 25,
pairKey4 = 1234,
selfName = "Alice",
)

mesh.sendMapFriendAck(
groupId = "10000001",
toNode = peerNodeId,
reqId = requestId,
result = 1,
pairChannel = 25,
pairKey4 = 1234,
selfName = "Bob",
)

updateDiscoveryAddressByteOverride(...) optionally supplies a stable device-address byte for discovery slot selection.

Direct text, location sharing, and invitations

mesh.sendFriendDirectText(peerNodeId, messageId, "hello", "10000001")
mesh.sendFriendDirectAck(peerNodeId, messageId, "10000001")

val shareRequest = mesh.sendMapLocShareRequest(
groupId = "10000001", toNode = peerNodeId,
selfName = "Alice", intervalSec = 10, ttlMin = 30,
)
mesh.sendMapLocShareAck(
groupId = "10000001", toNode = peerNodeId, reqId = shareRequest,
result = 1, sessionId = sessionId, selfName = "Bob",
intervalSec = 10, ttlMin = 30,
)
mesh.sendMapLocHeartbeatBroadcast(
groupId = "10000001", peerNode = peerNodeId, sessionId = sessionId,
seq = sequence, latE7 = latE7, lonE7 = lonE7,
precisionBits = 24, posAgeSec = 0,
)
mesh.sendMapLocStopBroadcast("10000001", peerNodeId, sessionId, reason = 0)

Group invitation methods are sendMapGroupAddMembersRequest, sendMapGroupAddMembersAck, sendMapGroupJoinNoticeBroadcast, and sendMapGroupRosterSyncBroadcast. The roster accepts Event.MapGroupRosterMember records.

Reliability and routing

mesh.updateAckEnabled(true)
val tracePacket = mesh.sendTraceRoute("10000001", peerNodeId)
val routes = mesh.learnedRouteSnapshot()
val predicted = mesh.predictedRouteSummary(peerNodeId)
mesh.clearLearnedRoute(peerNodeId)
mesh.clearAllLearnedRoutes()

Ack.implicit is true when another node rebroadcasts the local packet. Timeout reports reliable-send expiry. routeSummary(fromNode, packetId) describes an observed route. estimateFloodSettleMs(...) and defaultMarginMsForRateMode(...) support host scheduling.

Runtime configuration

The mutable configuration APIs are updateNodeId, updateRateMode, updateMaxRfPayloadBytes, updateDefaultHopLimit, updateAckEnabled, setCadEnabled, updateMapUserName, updateMapLocation, and updateDiscoveryAddressByteOverride. Apply RF changes in the device transport separately.

The optional Internet Mesh relay surface consists of InternetMeshDeveloperConfig, InternetMeshRouteInfo, InternetMeshReachablePeer, and InternetMeshRelayClient. Configure an explicitly provisioned wss:// endpoint, call reportReachablePeer(...), isRecentlyReachable(...), and sendMeshPacket(...), and route received raw packets back through handleInbound. Do not enable this path without a customer-owned or xTalk-provisioned relay service.

Failure handling

Input validation failures use IllegalArgumentException; protocol or send failures may throw MeshSdkError. Treat transport exceptions as connection failures. On transport replacement, stop sends, detach the old inbound route, create or update the adapter, and ensure each received packet still enters the SDK only once.