跳到主要内容

Android Mesh SDK 对外接入文档

SDK Version: 1.0.0

适用模块:Mesh SDK 1.0.0 入口类:com.xtalk.mesh.sdk.MeshSdk

能力概览

Android Mesh SDK 封装 aiTalk 当前使用的 MeshWire-over-TK8620 mesh 能力,业务侧只需要提供 RF 发送和 AT 命令执行通道,然后通过 MeshSdk 收发事件。

能力对外接口事件
公共群文本sendPublicText(...)Event.TextReceived
公共群语音sendPublicVoiceAiCodecFrames(...)Event.VoiceReceived
可修复语音片段sendRepairablePublicVoiceAiCodecFrames(...)Event.VoiceReceived
语音修复 ACKsendPublicVoiceRepairAck(...)Event.VoiceRepairAckReceived
地图附近的人discoverNearbyMapUsers(...)返回 MapNearbyPeer
地图加好友sendMapFriendRequest(...) / sendMapFriendAck(...)MapFriendRequest / MapFriendAck
位置共享sendMapLocShareRequest(...)MapLocShareRequest / MapLocHeartbeat
群组邀请sendMapGroupAddMembersRequest(...)MapGroupAddRequest / MapGroupRosterSync
好友直聊文本sendFriendDirectText(...) / sendFriendDirectAck(...)FriendDirectTextReceived / FriendDirectTextAck
Mesh ACK/超时updateAckEnabled(...)Ack / Timeout
路由追踪sendTraceRoute(...)TraceRouteResult
路由学习快照learnedRouteSnapshot()无,主动读取
正式 FSK 频点表AiTalkMeshRadioProfile.channels / channel(...)无,不可变配置数据
配对 PSK 派生MeshPairingPsk.derivePsk16(...)无,纯函数

SDK 边界

Mesh SDK 1.0.0 只负责 Mesh 协议和 aiTalk Mesh 业务 payload:

  • 不负责 BLE 扫描、连接、认证、设备初始化、UART 打开或音频编解码。
  • 不依赖 app UI、Compose、Activity、ViewModel、uimockAppUiState
  • 业务侧只需要提供 MeshSdk.Transport,把 SDK 生成的完整 RF payload 发给设备,并把设备收到的完整 RF payload 喂回 handleInbound(...)
  • 现有 app 里的 RadioTransportMeshAdapterSharedPrefsLearnedRouteStoreMeshSdkCadCompat 是接入示例或 app 辅助代码,不属于对外 Mesh SDK 必需文件。

当前 Android runtime 依赖:

依赖用途
org.jetbrains.kotlin:kotlin-stdlibKotlin runtime
org.jetbrains.kotlinx:kotlinx-coroutines-core事件、延迟、并发发送
com.squareup.okhttp3:okhttpInternet Mesh Developer Mode 的 WebSocket
Android SDK android.util.Log / Base64日志和编码

同一 Android 工程里还有 com.xtalk.mesh.sdk.bleauthdeviceinittransportuartsttchannel 等包名相近的模块。它们是独立 SDK 模块,不是 Mesh SDK 1.0.0 散落出去的代码。客户如果需要“从连接设备到发 Mesh”的完整链路,需要同时接入对应 transport / BLE / UART / auth / init SDK;如果只接入 Mesh 协议层,只需要本模块和自己实现的 Transport

三种不同的 Channel

当前代码和业务里有三个数字范围可能重合、但语义完全独立的概念:

名称用途是否选择 RF
正式 FSK/RF CH1..16AiTalkMeshRadioProfile 中的固定频率和 BCNID 行是,由 App 选中当前行
MeshWire publicChannel决定 public group name、channel hash 和 MeshWire PSK
QR/业务 pairChannelkey4 一起派生 16 字节配对 PSK

三者不建立数字映射。普通产品路径的文字、语音和实时通话使用 App 当前 active RF,正常由所选正式 FSK 行得到;显式 developer manual override 是独立的任意 frequency + BCNID=0 非消息路径。QR 里的 CH 只解析为 pairChannel 并进入 KDF,不会变成 AT+FREQAT+BCNID 参数。

正式 AiTalk Mesh V1 Radio Profile

AiTalkMeshRadioProfile.channels 是当前唯一正式 FSK 表;channel(channelId) 用于按 CH 查找。PROFILE_ID = 11_498 是稳定标识。该值保留了旧 ChannelPresenceChannelPlan.channelPlanId(470.250MHz) 的 wire 值,但正式 profile 不再从可配置起始频点派生标识或频率。

RF CH频率BCNID
1470.250 MHz1
2475.250 MHz2
3480.250 MHz3
4485.250 MHz4
5490.250 MHz5
6495.250 MHz6
7500.250 MHz7
8505.250 MHz8
9440.200 MHz9
10440.600 MHz10
11441.000 MHz11
12441.400 MHz12
13441.800 MHz13
14442.200 MHz14
15442.600 MHz15
16443.000 MHz16

扫描按上表行/CH 顺序执行,不按频率排序;CH8 -> CH9 会从 505.250MHz 跳到 440.200MHz,因此频率并非单调。

Channel Presence 仍保留显式候选 ChannelPlan 输入,start + index * 2MHz 仅是 generic/custom/legacy compatibility helper,不是这张正式表的生成规则。

配对 PSK API

Android 公开纯函数:

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

规则是 first16(SHA256("aitalk-mesh-psk-v1|ch=<pairChannel>|key=<trimmed-key4>"))。跨端 golden vector:

pairChannel=25, key4=1234
PSK16=166bf40821e0aa173fdf424424e42b49

MeshPairingPsk 只 trim key4 并派生 PSK;它不验证业务输入,不查频率,不选择/切换 RF,不发 transport 命令。QR 或业务层继续负责输入校验。

Gradle 模块

include(":mesh-sdk")

App 模块需要依赖:

初始化

MeshSdk 初始化需要四个参数:

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

Transport

业务侧必须实现 MeshSdk.Transport

class AppMeshTransport : MeshSdk.Transport {
override suspend fun sendRfPayload(bytes: ByteArray) {
// 把完整 RF payload 发给设备,例如 AT+SENDB 的二进制发送通道。
}

override suspend fun runAtCommand(command: String, timeoutMs: Long): List<String> {
// 给 CAD/LBT 使用,例如 AT+FREQ? 和 AT+LISTEN=...
return emptyList()
}
}

现有 App 中的接入点在 XTalkMeshUiApp.ktsendRfPayloadAppTransport.sendBinaryFrameAndWaitSendFinish(...)runAtCommandAppTransport.runAtCommand(...)

LearnedRouteStore

LearnedRouteStore 用于保存 SDK 学到的下一跳路由:

class AppRouteStore : MeshSdk.LearnedRouteStore {
override fun load(): List<MeshSdk.LearnedRoute> {
return emptyList()
}

override fun save(routes: List<MeshSdk.LearnedRoute>) {
// 保存 targetNode、nextHop、updatedAtMs、learnedFrom。
}
}

如果业务侧提供 store,SDK 初始化时会恢复历史路由;收到 ACK 或 trace reply 后会自动保存新路由。

监听事件

MeshSdk.eventsSharedFlow<MeshSdk.Event>

scope.launch {
meshSdk.events.collect { event ->
when (event) {
is MeshSdk.Event.TextReceived -> {
renderText(event.groupId, event.fromNode, event.text)
}
is MeshSdk.Event.VoiceReceived -> {
playOrStoreVoice(event.voice)
}
is MeshSdk.Event.Ack -> {
updateSendState(event.packetId, event.ok)
}
is MeshSdk.Event.Timeout -> {
markTimeout(event.packetId)
}
else -> Unit
}
}
}

事件里常用诊断字段:

字段含义
groupId公共群 ID,例如 10000001
publicChannelMeshWire 逻辑公共组加密域,范围 1..16;不是 RF CH
fromNode发送方完整 nodeId
srcNodeNumfromNode 的最后 1 字节
packetIdMeshWire 包 ID
rssi / snr业务侧传入 handleInbound(...) 的链路指标
routeSummarySDK 根据收到的 relay trace 生成的路由摘要

接收入口

设备收到 RF payload 后,业务侧调用:

meshSdk.handleInbound(raw = packet, snr = snr, rssi = rssi)

要求:

  • raw 是完整 Mesh RF payload,包含 16 字节 MeshWire header。
  • 不包含 AT+DI: 文本前缀。
  • snr / rssi 可以为空;传入后会原样进入事件,便于 UI 和日志展示。

公共群 ID

公共群使用 8 位 HEX 字符串:

公共信道groupId
110000001
210000002
......
1610000010

可使用工具函数:

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

频道 PSK

公共群默认使用 SDK 内部默认 PSK。业务侧可以为某个公共信道设置 16 或 32 字节 PSK:

meshSdk.setPublicGroupPsk(publicChannel = 2, psk = psk16)

清除自定义 PSK:

meshSdk.setPublicGroupPsk(publicChannel = 2, psk = null)

这个接口会影响:

  • MeshWire header 里的 channel hash。
  • Mesh payload 的 AES-CTR 加解密 key。

好友密钥

好友直聊、位置共享、群组邀请使用 app-level 16 字节好友密钥:

meshSdk.setFriendKey16(forNode = peerNodeId, key16 = friendKey)

清除:

meshSdk.setFriendKey16(forNode = peerNodeId, key16 = null)

这些接口调用前必须配置好友密钥:

  • sendFriendDirectText(...)
  • sendFriendDirectAck(...)
  • sendMapLocShareRequest(...)
  • sendMapLocShareAck(...)
  • sendMapLocHeartbeatBroadcast(...)
  • sendMapLocStopBroadcast(...)
  • sendMapGroupAddMembersRequest(...)
  • sendMapGroupAddMembersAck(...)

发送公共群文本

val packetId =
meshSdk.sendPublicText(
groupId = "10000001",
text = "hello mesh",
wantAck = false,
hopLimit = null,
)

说明:

  • 单包能放下时使用 MeshWire TEXT_MESSAGE_APP,portnum = 1
  • 超过当前 maxRfPayloadBytes 时,SDK 自动拆成 aiTalk 文本分片,portnum = 257
  • 返回值是最终 message id;分片文本使用最后一个 chunk 的 packetId 作为 message id。
  • wantAck = true 时,SDK 会注册可靠发送;公共群主要依赖 implicit ACK,即听到其他节点转发同一个包。

发送公共群语音

val packetIds =
meshSdk.sendPublicVoiceAiCodecFrames(
groupId = "10000001",
frames41 = aiCodecFrames,
codecFamilyByte = 0,
autoPlay = false,
hopLimit = null,
wantAckLast = false,
)

说明:

  • frames41 是业务侧已经编码好的 AiCodec 41 字节帧列表。
  • SDK 会按当前 maxRfPayloadBytes 规划语音包大小。
  • 语音走 aiTalk private voice app,portnum = 256
  • 每个包之间会按 estimateFloodSettleMs(...) 延迟,减少同一条语音在 mesh 泛洪中的冲突。

可修复语音

发送全部或部分语音片段:

val result =
meshSdk.sendRepairablePublicVoiceAiCodecFrames(
groupId = "10000002",
frames41 = frames41,
messageId = messageId,
codecFamilyByte = 0,
autoPlay = false,
selectedFragmentIndexes = intArrayOf(3, 4),
)

发送缺片 ACK:

val ackPacketId =
meshSdk.sendPublicVoiceRepairAck(
groupId = "10000002",
messageId = messageId,
totalFragments = totalFragments,
missingFragmentIndexes = intArrayOf(3, 4),
)

接收侧通过:

  • Event.VoiceReceived.voice.messageId
  • Event.VoiceReceived.voice.totalFragments
  • Event.VoiceReceived.voice.fragmentIndex
  • Event.VoiceRepairAckReceived.ack.missingFragmentIndexes

完成业务层补片。

地图附近的人

meshSdk.updateMapUserName("Alice")
meshSdk.updateMapLocation(
latE7 = 399123456,
lonE7 = 1161234567,
precisionBits = 24,
)

val peers =
meshSdk.discoverNearbyMapUsers(
groupId = "10000001",
wantName = true,
wantPos = true,
hopLimit = 0,
groupCount = 4,
slotCount = 16,
rounds = 2,
onPeerFound = { peer -> showPeer(peer) },
)

说明:

  • 默认 hopLimit = 0,用于搜索直接邻居。
  • SDK 会发送 v2 discovery request,等待多个 slot 窗口后返回去重后的 MapNearbyPeer
  • onPeerFound 会在扫描过程中实时回调;最终返回完整列表。
  • updateDiscoveryAddressByteOverride(byte) 可设置确定性 slot 使用的地址字节,推荐传已连接设备 BLE MAC 最后 1 字节。

地图加好友

发送请求:

val reqId =
meshSdk.sendMapFriendRequest(
groupId = "10000001",
toNode = peerNode,
pairChannel = 2,
pairKey4 = 1234,
selfName = "Alice",
)

发送同意或拒绝:

meshSdk.sendMapFriendAck(
groupId = "10000001",
toNode = peerNode,
reqId = reqId,
result = 1,
pairChannel = 2,
pairKey4 = 1234,
selfName = "Bob",
)

事件:

  • Event.MapFriendRequest
  • Event.MapFriendAck

pairChannel / pairKey4 对齐面对面加好友 QR 中的配对域标识和 4 位 key。这里的 pairChannel 不是 RF 频道。

好友直聊文本

先设置好友密钥:

meshSdk.setFriendKey16(forNode = peerNode, key16 = friendKey16)

发送文本:

val lastPacketId =
meshSdk.sendFriendDirectText(
toNode = peerNode,
messageId = localMessageId,
text = "hello",
groupId = "10000001",
hopLimit = null,
)

收到文本后发送业务 ACK:

meshSdk.sendFriendDirectAck(
toNode = peerNode,
messageId = messageId,
groupId = "10000001",
)

事件:

  • Event.FriendDirectTextReceived
  • Event.FriendDirectTextAck

说明:

  • 好友直聊走 portnum = 259
  • 文本 payload 使用 AES-GCM,密钥是 setFriendKey16(...) 设置的 16 字节好友密钥。
  • 长文本会按 FriendDirectChatProtocol.suggestedChunkBytes(maxRfPayloadBytes) 分片,接收侧自动重组。

位置共享

请求对方共享位置:

val reqId =
meshSdk.sendMapLocShareRequest(
groupId = "10000001",
toNode = peerNode,
selfName = "Alice",
intervalSec = 10,
ttlMin = 0,
)

回复请求:

meshSdk.sendMapLocShareAck(
groupId = "10000001",
toNode = peerNode,
reqId = reqId,
result = 1,
sessionId = sessionId,
selfName = "Bob",
intervalSec = 10,
ttlMin = 0,
)

发送心跳:

meshSdk.sendMapLocHeartbeatBroadcast(
groupId = "10000001",
peerNode = peerNode,
sessionId = sessionId,
seq = seq,
latE7 = latE7,
lonE7 = lonE7,
precisionBits = 24,
posAgeSec = 0,
)

停止共享:

meshSdk.sendMapLocStopBroadcast(
groupId = "10000001",
peerNode = peerNode,
sessionId = sessionId,
reason = 0,
)

事件:

  • Event.MapLocShareRequest
  • Event.MapLocShareAck
  • Event.MapLocHeartbeat
  • Event.MapLocStop

群组邀请

邀请好友加入私有群:

val reqId =
meshSdk.sendMapGroupAddMembersRequest(
groupId = "10000001",
toNode = peerNode,
inviteGroupId = "10000002",
inviteGroupChannel = 2,
inviteGroupKey4 = 1234,
inviteGroupTitle = "Team",
selfName = "Alice",
)

回复邀请:

meshSdk.sendMapGroupAddMembersAck(
groupId = "10000001",
toNode = peerNode,
reqId = reqId,
result = 1,
inviteGroupId = "10000002",
selfName = "Bob",
)

广播入群通知和成员同步:

meshSdk.sendMapGroupJoinNoticeBroadcast(
groupId = "10000002",
inviteeNode = peerNode,
inviterName = "Alice",
inviteeName = "Bob",
)

meshSdk.sendMapGroupRosterSyncBroadcast(
groupId = "10000002",
members =
listOf(
MeshSdk.Event.MapGroupRosterMember(1, localNode, "Alice"),
MeshSdk.Event.MapGroupRosterMember(2, peerNode, "Bob"),
),
)

事件:

  • Event.MapGroupAddRequest
  • Event.MapGroupAddAck
  • Event.MapGroupAddJoinNotice
  • Event.MapGroupRosterSync

ACK 和超时

打开可靠发送:

meshSdk.updateAckEnabled(true)

关闭时会清空当前 pending reliable sender:

meshSdk.updateAckEnabled(false)

业务侧通过事件更新发送状态:

when (event) {
is MeshSdk.Event.Ack -> {
if (event.ok) markDelivered(event.packetId) else markFailed(event.packetId)
}
is MeshSdk.Event.Timeout -> {
markTimeout(event.packetId)
}
else -> Unit
}

说明:

  • implicit = true 表示本机听到了其他节点转发自己的包。
  • implicit = false 表示收到了 routing ACK/NACK。
  • errorReason 对应 MeshWireRoutingError

Trace Route 和路由学习

发送 trace route:

val packetId =
meshSdk.sendTraceRoute(
groupId = "10000001",
toNode = peerNode,
)

读取当前学到的路由:

val routes = meshSdk.learnedRouteSnapshot()
val predicted = meshSdk.predictedRouteSummary(peerNode)

清理:

meshSdk.clearLearnedRoute(peerNode)
meshSdk.clearAllLearnedRoutes()

SDK 会从两类数据学习下一跳:

  • 对方返回的 routing ACK。
  • trace route reply 里的 route / routeBack

动态配置

接口用途
updateNodeId(nodeId)更新本机 MeshWire nodeId
updateRateMode(rateMode)更新当前 RF rate mode,影响 airtime 和 CAD backoff
updateMaxRfPayloadBytes(maxRfPayloadBytes)更新当前设备可发送 RF payload 上限
updateDefaultHopLimit(defaultHopLimit)更新默认 hopLimit,范围 0..7
updateAckEnabled(enabled)开关可靠发送和 ACK 状态
estimateFloodSettleMs(...)估算 mesh 泛洪稳定时间
defaultMarginMsForRateMode(rateMode)获取 rate mode 对应的默认 margin

推荐接入流程

flowchart TD
A["App 获取本机 nodeId"] --> B["创建 MeshSdk.Config"]
B --> C["实现 MeshSdk.Transport"]
C --> D["创建 MeshSdk"]
D --> E["监听 meshSdk.events"]
D --> F["设备 RX 时调用 handleInbound"]
D --> G["按业务调用发送 API"]
G --> H["根据 Ack/Timeout/业务事件更新 UI"]

当前仍保留显式候选频点计划、preferred frequency 以及通话前切换/结束后恢复的接口位置,供未来跨频能力使用。当前尚未实现联系人频点自动发现/漫游、联系人跨频自动切换/恢复、跨频聊天或跨频通话恢复;现有通话清理恢复 message/default 状态继续保留。宿主不得把 pairChannel 自动送入这些接口。