跳到主要内容

iOS Mesh SDK 对外接入文档

SDK Version: 1.0.0

SwiftPM product:XTalkMeshSdk 入口类:MeshSdk 当前形态:已可作为 SwiftPM 源码包发布;XTalkMesh app target 通过本地 XTalkMeshSdk product 依赖 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
语音修复 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(id:)无,不可变配置数据
配对 PSK 派生MeshPairingPsk.derivePsk16(...)无,纯函数

SDK 边界

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

  • 不负责 BLE 扫描、连接、认证、设备初始化、音频采集或音频编解码。
  • 不依赖 SwiftUI、UIKit、AppModelXTalkBleClient 或 app 数据库。
  • 宿主侧必须实现 MeshSdk.Transport
  • 宿主侧必须在设备收到完整 RF payload 后调用 handleInbound(raw:snr:rssi:)

当前系统依赖/import:

依赖用途
FoundationData、Task、URL、JSON、基础类型
CombinePassthroughSubject 事件流
CommonCryptoHKDF/HMAC 等兼容实现
CryptoKit部分加密和摘要能力

当前 SwiftPM target 已纳入这些文件:

文件原因
XTalkMesh/Mesh/MeshSdk.swift主入口、事件、发送/接收、协议编排
XTalkMesh/Mesh/AiTalkMeshRadioProfile.swift正式 AiTalk Mesh V1 FSK/BCNID profile
XTalkMesh/Mesh/MeshPairingPsk.swiftpairChannel + key4 配对 PSK16 KDF
XTalkMesh/Mesh/MeshWireCadScheduler.swift发送前 CAD/LBT 调度
XTalkMesh/Mesh/ListenRssiCalibrator.swiftCAD/RSSI 校准辅助
XTalkMesh/XTalkSdk/Realtime/InternetMeshDeveloperSdk.swiftInternet Mesh Developer Mode WebSocket relay

iOS app 的真实构建图为:

XTalkMesh app -> local XTalkMeshSdk product -> SDK sources

MeshSdk.swiftMeshWireCadScheduler.swiftListenRssiCalibrator.swiftInternetMeshDeveloperSdk.swift 可以保留在 Xcode navigator 中,但不在 app Sources build phase 内;App 代码通过 import XTalkMeshSdk 访问公开 API。

三种不同的 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,不会生成 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
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。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
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 和日志展示。

文字和语音只支持 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
110000001
210000002
......
1610000010

公共群默认使用 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 不会自动进入这些接口。