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 |
| 语音修复 ACK | sendPublicVoiceRepairAck(...) | 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、
uimock或AppUiState。 - 业务侧只需要提供
MeshSdk.Transport,把 SDK 生成的完整 RF payload 发给设备,并把设备收到的完整 RF payload 喂回handleInbound(...)。 - 现有 app 里的
RadioTransportMeshAdapter、SharedPrefsLearnedRouteStore和MeshSdkCadCompat是接入示例或 app 辅助代码,不属于对外 Mesh SDK 必需文件。
当前 Android runtime 依赖:
| 依赖 | 用途 |
|---|---|
org.jetbrains.kotlin:kotlin-stdlib | Kotlin runtime |
org.jetbrains.kotlinx:kotlinx-coroutines-core | 事件、延迟、并发发送 |
com.squareup.okhttp3:okhttp | Internet Mesh Developer Mode 的 WebSocket |
Android SDK android.util.Log / Base64 | 日志和编码 |
同一 Android 工程里还有 com.xtalk.mesh.sdk.ble、auth、deviceinit、transport、uart、stt、channel 等包名相近的模块。它们是独立 SDK 模块,不是 Mesh SDK 1.0.0 散落出去的代码。客户如果需要“从连接设备到发 Mesh”的完整链路,需要同时接入对应 transport / BLE / UART / auth / init SDK;如果只接入 Mesh 协议层,只需要本模块和自己实现的 Transport。
三种不同的 Channel
当前代码和业务里有三个数字范围可能重合、但语义完全独立的概念:
| 名称 | 用途 | 是否选择 RF |
|---|---|---|
正式 FSK/RF CH1..16 | AiTalkMeshRadioProfile 中的固定频率和 BCNID 行 | 是,由 App 选中当前行 |
MeshWire publicChannel | 决定 public group name、channel hash 和 MeshWire PSK | 否 |
QR/业务 pairChannel | 与 key4 一起派生 16 字节配对 PSK | 否 |
三者不建立数字映射。普通产品路径的文字、语音和实时通话使用 App 当前 active RF,正常由所选正式 FSK 行得到;显式 developer manual override 是独立的任意 frequency + BCNID=0 非消息路径。QR 里的 CH 只解析为 pairChannel 并进入 KDF,不会变成 AT+FREQ 或 AT+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 |
|---|---|---|
| 1 | 470.250 MHz | 1 |
| 2 | 475.250 MHz | 2 |
| 3 | 480.250 MHz | 3 |
| 4 | 485.250 MHz | 4 |
| 5 | 490.250 MHz | 5 |
| 6 | 495.250 MHz | 6 |
| 7 | 500.250 MHz | 7 |
| 8 | 505.250 MHz | 8 |
| 9 | 440.200 MHz | 9 |
| 10 | 440.600 MHz | 10 |
| 11 | 441.000 MHz | 11 |
| 12 | 441.400 MHz | 12 |
| 13 | 441.800 MHz | 13 |
| 14 | 442.200 MHz | 14 |
| 15 | 442.600 MHz | 15 |
| 16 | 443.000 MHz | 16 |
扫描按上表行/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.kt:sendRfPayload 走 AppTransport.sendBinaryFrameAndWaitSendFinish(...),runAtCommand 走 AppTransport.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.events 是 SharedFlow<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 |
publicChannel | MeshWire 逻辑公共组加密域,范围 1..16;不是 RF CH |
fromNode | 发送方完整 nodeId |
srcNodeNum | fromNode 的最后 1 字节 |
packetId | MeshWire 包 ID |
rssi / snr | 业务侧传入 handleInbound(...) 的链路指标 |
routeSummary | SDK 根据收到的 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 |
|---|---|
| 1 | 10000001 |
| 2 | 10000002 |
| ... | ... |
| 16 | 10000010 |
可使用工具函数:
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.messageIdEvent.VoiceReceived.voice.totalFragmentsEvent.VoiceReceived.voice.fragmentIndexEvent.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.MapFriendRequestEvent.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.FriendDirectTextReceivedEvent.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.MapLocShareRequestEvent.MapLocShareAckEvent.MapLocHeartbeatEvent.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.MapGroupAddRequestEvent.MapGroupAddAckEvent.MapGroupAddJoinNoticeEvent.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 自动送入这些接口。