iOS Mesh SDK 对外接入文档
SDK Version: 1.0.0
SwiftPM product:
XTalkMeshSdk入口类:MeshSdk当前形态:已可作为 SwiftPM 源码包发布;XTalkMesh app target 通过本地XTalkMeshSdkproduct 依赖 SDK,不再直接编译 SDK 实现源文件。
能力概览
iOS Mesh SDK 与 Android Mesh SDK 1.0.0 对齐,封装 aiTalk 当前使用的 MeshWire-over-TK8620 mesh 能力。宿主侧只需要提供设备发送/AT 命令通道,并把收到的 RF payload 喂给 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(id:) | 无,不可变配置数据 |
| 配对 PSK 派生 | MeshPairingPsk.derivePsk16(...) | 无,纯函数 |
SDK 边界
iOS Mesh SDK 只负责 Mesh 协议和 aiTalk Mesh 业务 payload:
- 不负责 BLE 扫描、连接、认证、设备初始化、音频采集或音频编解码。
- 不依赖 SwiftUI、UIKit、
AppModel、XTalkBleClient或 app 数据库。 - 宿主侧必须实现
MeshSdk.Transport。 - 宿主侧必须在设备收到完整 RF payload 后调用
handleInbound(raw:snr:rssi:)。
当前系统依赖/import:
| 依赖 | 用途 |
|---|---|
| Foundation | Data、Task、URL、JSON、基础类型 |
| Combine | PassthroughSubject 事件流 |
| CommonCrypto | HKDF/HMAC 等兼容实现 |
| CryptoKit | 部分加密和摘要能力 |
当前 SwiftPM target 已纳入这些文件:
| 文件 | 原因 |
|---|---|
XTalkMesh/Mesh/MeshSdk.swift | 主入口、事件、发送/接收、协议编排 |
XTalkMesh/Mesh/AiTalkMeshRadioProfile.swift | 正式 AiTalk Mesh V1 FSK/BCNID profile |
XTalkMesh/Mesh/MeshPairingPsk.swift | pairChannel + key4 配对 PSK16 KDF |
XTalkMesh/Mesh/MeshWireCadScheduler.swift | 发送前 CAD/LBT 调度 |
XTalkMesh/Mesh/ListenRssiCalibrator.swift | CAD/RSSI 校准辅助 |
XTalkMesh/XTalkSdk/Realtime/InternetMeshDeveloperSdk.swift | Internet Mesh Developer Mode WebSocket relay |
iOS app 的真实构建图为:
XTalkMesh app -> local XTalkMeshSdk product -> SDK sources
MeshSdk.swift、MeshWireCadScheduler.swift、ListenRssiCalibrator.swift 和 InternetMeshDeveloperSdk.swift 可以保留在 Xcode navigator 中,但不在 app Sources build phase 内;App 代码通过 import XTalkMeshSdk 访问公开 API。
三种不同的 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,不会生成 RF 切换命令。
正式 AiTalk Mesh V1 Radio Profile
AiTalkMeshRadioProfile.channels 暴露固定表,channel(id:) 按 RF CH 查找,profileId = 11_498 是稳定标识。11_498 保留了旧 ChannelPresenceChannelPlan.channelPlanId(startFrequencyHz: 470.250MHz) 的 wire 值,正式 profile 已不再从可配置 start frequency 派生。
| 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。Channel Presence 的 start + index * 2MHz 工厂只是 generic/custom/legacy compatibility helper,不是正式产品表。
配对 PSK API
let psk16 = MeshPairingPsk.derivePsk16(pairChannel: 25, key4: "1234")
KDF 是 first16(SHA256("aitalk-mesh-psk-v1|ch=<pairChannel>|key=<trimmed-key4>")),跨端 golden vector 为 25 / 1234 -> 166bf40821e0aa173fdf424424e42b49。该 API 只 trim key4 并派生 PSK;不验证业务输入,不查频率,不选择/切换 RF,不发 transport 命令。
Internet Mesh Developer Mode 默认关闭;如果客户生产包要启用这项能力,必须显式传入自己的 serverURLString,不要把默认测试地址当成生产服务。
初始化
当前 MeshSdk 初始化签名:
init(
transport: MeshSdk.Transport,
initialConfig: MeshSdk.Config,
learnedRouteStore: MeshSdk.LearnedRouteStore? = nil
)
示例:
let meshSdk = MeshSdk(
transport: AppMeshTransport(radio: radio),
initialConfig: MeshSdk.Config(
nodeId: localNodeId,
defaultHopLimit: 3,
rebroadcastEnabled: true,
rebroadcastDelayMinMs: 200,
rebroadcastDelayMaxMs: 400,
rateMode: 7,
maxRfPayloadBytes: 105
),
learnedRouteStore: routeStore
)
Transport
宿主侧必须实现:
final class AppMeshTransport: MeshSdk.Transport {
private let radio: AppRadioClient
init(radio: AppRadioClient) {
self.radio = radio
}
func sendRfPayload(_ bytes: Data) async throws {
try await radio.sendRfPayload(bytes)
}
func runAtCommand(_ plain: String, timeoutMs: Int, log: Bool) async throws -> [String] {
try await radio.runAtCommand(plain, timeoutMs: timeoutMs, log: log)
}
}
sendRfPayload(_:) 输入的是完整 Mesh RF payload,已经包含 16 字节 MeshWire header。宿主侧不要重新组包或改写 payload。
runAtCommand(_:timeoutMs:log:) 主要给 CAD/LBT 使用,例如 AT+FREQ? 和 AT+LISTEN=...。
LearnedRouteStore
LearnedRouteStore 用于保存 SDK 学到的下一跳路由:
final class AppRouteStore: MeshSdk.LearnedRouteStore {
func load() -> [MeshSdk.LearnedRoute] {
[]
}
func save(routes: [MeshSdk.LearnedRoute]) {
// 保存 targetNode、nextHop、updatedAtMs、learnedFrom。
}
}
传 nil 可以不持久化;SDK 仍会在内存里学习路由,但重启后丢失。
监听事件
MeshSdk.events 是 Combine PassthroughSubject<MeshSdk.Event, Never>:
var cancellables = Set<AnyCancellable>()
meshSdk.events
.sink { event in
switch event {
case let .textReceived(text):
renderText(text.groupId, text.fromNode, text.text)
case let .voiceReceived(voice):
playOrStoreVoice(voice.voice)
case let .ack(ack):
updateSendState(ack.packetId, ack.ok)
case let .timeout(timeout):
markTimeout(timeout.packetId)
default:
break
}
}
.store(in: &cancellables)
常用事件字段:
| 字段 | 含义 |
|---|---|
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 和日志展示。
文字和语音只支持 MeshWire 承载。非 MeshWire raw TK8620 text/voice 不属于 SDK 支持协议;当 file、low-latency、realtime、MeshWire 等更高优先级入口均未消费时,宿主应直接忽略,不再做 raw codec fallback。正常 MeshWire text、repairable voice、ACK / NACK,以及 MeshWire inner legacy voice 接收仍保留。
可用快速判断:
if MeshSdk.looksLikePacket(raw) {
meshSdk.handleInbound(raw: raw, snr: snr, rssi: rssi)
}
公共群和密钥
公共群使用 8 位 HEX 字符串:
| 公共信道 | groupId |
|---|---|
| 1 | 10000001 |
| 2 | 10000002 |
| ... | ... |
| 16 | 10000010 |
公共群默认使用 SDK 内部默认 PSK。业务侧可以为某个公共信道设置 16 或 32 字节 PSK:
meshSdk.setPublicGroupPsk(publicChannel: 2, psk: psk16)
meshSdk.setPublicGroupPsk(publicChannel: 2, psk: nil)
好友直聊、位置共享、群组邀请使用 app-level 16 字节好友密钥:
meshSdk.setFriendKey16(forNode: peerNodeId, key16: friendKey16)
meshSdk.setFriendKey16(forNode: peerNodeId, key16: nil)
这些接口调用前必须配置好友密钥:
sendFriendDirectText(...)sendFriendDirectAck(...)sendMapLocShareRequest(...)sendMapLocShareAck(...)sendMapLocHeartbeatBroadcast(...)sendMapLocStopBroadcast(...)sendMapGroupAddMembersRequest(...)sendMapGroupAddMembersAck(...)
发送示例
公共群文本:
let packetId = try await meshSdk.sendPublicText(
groupId: "10000001",
text: "hello mesh",
wantAck: false,
hopLimit: nil
)
公共群语音:
let packetIds = try await meshSdk.sendPublicVoiceAiCodecFrames(
groupId: "10000001",
frames41: aiCodecFrames,
codecFamilyByte: 0,
autoPlay: false,
hopLimit: nil,
wantAckLast: false
)
地图附近的人:
let peers = try await meshSdk.discoverNearbyMapUsers(
groupId: "10000001",
wantName: true,
wantPos: true,
hopLimit: 0
) { peer in
renderPeer(peer)
}
好友直聊文本:
let messageId = UInt32.random(in: 1 ... UInt32.max)
try await meshSdk.sendFriendDirectText(
toNode: peerNode,
messageId: messageId,
text: "hello",
groupId: "10000001",
hopLimit: nil
)
动态配置
| 接口 | 用途 |
|---|---|
updateNodeId(_:) | 更新本机 MeshWire nodeId |
updateRateMode(_:) | 更新当前 RF rate mode,影响 airtime 和 CAD backoff |
updateMaxRfPayloadBytes(_:) | 更新当前设备可发送 RF payload 上限 |
updateDefaultHopLimit(_:) | 更新默认 hopLimit,范围 0..7 |
updateAckEnabled(_:) | 开关可靠发送和 ACK 状态 |
setCadEnabled(_:) | app 在实时通话等场景协调 CAD |
updateInternetMeshDeveloperConfig(_:) | 显式配置 Internet Mesh Developer Mode |
推荐接入流程
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"]
当前仍保留显式候选频点计划、target-frequency、通话前 prepare/switch 与 cleanup restore 接口位置,供未来跨频设计。当前未实现联系人频点自动发现/漫游、联系人跨频自动切换/恢复、跨频聊天或跨频通话恢复;现有通话结束恢复 message/default 状态继续保留。pairChannel 不会自动进入这些接口。