跳到主要内容

信道发现 SDK 对外接入文档

SDK Version: 1.0.0

适用范围:首次安装后第一次进入设备,以及用户后续在设置中点击“重新搜索信道”。 V1 通过“好友抽样 probe / 无好友 generic Mesh probe + mesh ACK”判断当前信道在本机当前射频范围内是否有人;不枚举全量设备,不统计真实人数,不处理网络分裂。

能力概览

信道发现 SDK 接收一组显式、有顺序的 ChannelPlan 并逐个切信道。当前产品 App 必须把 Mesh SDK 的 AiTalkMeshRadioProfile.channels 映射为计划,不能再用可配置起点或等差公式生成正式表。这里的 channel_id=1..16 是正式 RF/FSK 表的 CH 行号;它与 MeshWire 加密域的 publicChannel、二维码配对/KDF 输入的 pairChannel 是三个彼此独立的概念。

正式表如下,BCNID 与 CH 一一对应:

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。AiTalkMeshRadioProfile.PROFILE_ID/profileId = 11_498 是正式表的稳定标识。数值 11_498 只是保留旧 channelPlanId(470.250MHz) 的 wire 值,正式表已不再由该起点派生。ChannelPresenceChannelPlan.channels(startFrequencyHz)start + index * 2MHz 行为只保留给 caller-defined generic/custom/legacy 候选计划,不是当前产品规范。

每次进入某个消息信道,App host 必须先配置频点,再配置该信道对应的 BCNID:AT+FREQ=<frequency_hz> 成功后发送 AT+BCNID=<bcn_id>;两步都必须等待设备返回成功 AT_OK,不能只确认 BLE/UART 写入完成;两步都成功后才允许发 probe、等待 ACK 或常驻。配置普通 Mesh 消息信道表之外的频点时,必须配置 AT+BCNID=0

每到一个信道,如果本地有好友,SDK 从本地好友里抽 3~8 个候选好友发送静默 probe;如果本轮本地还没有好友候选,SDK 在每个信道发送 1 个 generic Mesh presence probe。只要本机听到 probe 被 mesh 转发、收到匹配 packet ID 且同信道的 ACK,或 App host 在当前 dwell 窗口内实际收到 Mesh RF 帧,就认为该信道在当前范围内有人;只有 ACK 命中目标好友时,才把该信道标记为 有好友

顺序开机场景必须成立:A 已经完成扫描并常驻在信道 Y,B 后来扫描到信道 Y 时,B 发出的 generic probe 会被 A 按 Mesh 规则转发;B 听到自己的 packet 被转发后记录 MeshImplicitAck,并自动切到 Y。为了覆盖这段转发时间,App 传给 SDK 的无好友 dwellMs 必须用 Mesh flood settle 估算:estimateFloodSettleMs(rfPayloadBytes=42, hopLimit=currentHopLimit) + 500ms,最小 1500ms40~260ms DID 错峰只用于增强多台设备同时扫描时的表现。

场景SDK 行为UI 行为
首次安装自动扫描当前信道表一次;无好友时每信道发 1 个 generic Mesh probe有 ACK 时自动切到推荐信道;全程无 ACK 时继续扫频并切到 RSSI 最优信道
设置页重新搜索记录扫描前信道,逐信道抽样探测;没有好友候选时也发送 generic probe有 ACK 时自动切到推荐信道;全程无 ACK 时继续扫频并切到 RSSI 最优信道
未收到任何 ACK返回空列表或该信道不入列表只代表本机当前范围、本轮扫描窗口未探到
扫到多个信道按好友命中优先;没有好友命中时选择本轮扫描顺序中后扫到的可达信道弹窗展示探测到的信道,并标记 有好友

V1 边界

必须做:

  • Android 和 iOS 都以 SDK API 形式提供同一套能力。
  • 使用 Mesh SDK 统一的 AiTalkMeshRadioProfile 构造当前正式 ChannelPlan;App 不复制表,Channel Presence SDK 也不从起点推导正式表。
  • ChannelPresenceChannel 必须同时包含 channelIdfrequencyHzbcnId;普通 Mesh 消息信道 channelId=1..16bcnId=channelId,非消息频点 bcnId=0
  • App host 切到消息信道时必须按 AT+FREQ -> AT+BCNID 的顺序配置,并等待设备返回 AT_OK 后才允许发送 probe;配置消息信道表之外的频点时必须写 AT+BCNID=0
  • 有好友候选时,每个信道默认最多抽 8 个好友,允许配置为 3..8
  • 没有好友候选时,每个信道发送 1 个 generic Mesh presence probe,收到 packet ID 匹配的 Mesh 转发/ACK,或当前 dwell 窗口内可匹配的 Mesh RF 帧时,只标记 当前范围有人,不标记 有好友
  • generic Mesh presence probe 发包前必须按 local_did + channel_id 计算 40~260ms 确定性错峰;Android/iOS 常量和 hash 公式必须一致。
  • 无好友的每信道停留窗口必须由 App 按 Mesh flood settle 计算,最小 1500ms,不能固定 800ms
  • ACK 必须携带或附带真实 channelId;SDK 保存结果前必须校验 ack.channelId == sentProbe.channelId,不一致时丢弃,避免异频接收污染结果。App host 不得用“当前正在扫描的信道”补写普通无真实信道来源的 Mesh ACK。
  • App host 可以实现当前信道 RX 窗口:成功切到某信道并且 probe 发送成功后,在 dwell 超时前收到的 Mesh RF 帧可转成 implicit=trueChannelPresenceMeshAckchannelId 使用当前 dwell 信道;切信道、超时或扫描结束必须清空窗口。
  • 当前信道 RX 窗口必须解析 Mesh 包头里的 fromNode/packetId/relayNodefromNode 是原始发送者,不是回复者;fromNode == localNodeId 时,只有 packetId 等于当前已发送 probe 且 relayNode 是远端短 ID,才说明有其他节点转发了本机 probe,可创建普通可达证据。
  • probe 是静默业务,不进入聊天 UI,不生成聊天消息,不触发通知。
  • 收到 mesh implicit ACK 时,标记该信道 当前范围有人
  • 收到目标好友 routing ACK 或好友加密 app ACK 时,标记该信道 有好友
  • 扫到活跃信道后,App 自动切到推荐信道:优先 有好友,否则使用本轮扫描顺序中后扫到的可达信道。例如正式表同时探到 CH4=485.250MHzCH16=443.000MHz 且都没有好友标记时,应选择后扫到的 CH16;不能改成按频率高低选择。
  • 首次安装和设置页重新搜索全程无 ACK 时,App 继续执行本地 RSSI 扫频,选择 RSSI 最优信道并常驻。
  • 设置页重新搜索前记录当前/上次信道;只有扫描失败或用户取消时才保持原信道。

明确不做:

  • 不解决网络分裂。
  • 不同步完整成员表。
  • 不统计信道内实际设备总数。
  • 不广播询问所有设备都回复。
  • 不协商 network_id、leader、epoch 或常驻信道。
  • 不创建网络;首次安装无可达设备时只做本机信道选择。

术语

名称含义
AiTalkMeshRadioProfileMesh SDK 拥有的正式 16 行 RF/FSK 频点与 BCNID 表。
meshInitialFrequencyHz仅供 generic/custom/legacy 连续候选计划工厂使用的起点;当前正式表不使用它。
ChannelPlan调用方显式传给扫描 SDK 的有序 RF 候选行;当前产品由 AiTalkMeshRadioProfile.channels 映射得到。
channel_plan_id正式表固定为 11_498;custom/legacy 计划才可使用起点派生辅助函数。
channel_id本文扫描结果中的正式 RF/FSK CH 行号 1..16,不是 publicChannelpairChannel
bcn_id固件 BCNID 参数。普通 Mesh 消息信道为 bcn_id=channel_id,即 1..160 保留给默认/非消息频点。
publicChannelMeshWire 逻辑/加密域字段,不决定 RF 频率。
pairChannel二维码/业务配对值,只参与 MeshPairingPsk 派生,不决定 RF 频率。
scan_id单次扫描随机 ID,用于匹配本次 probe 和 ACK。
Probe 好友本信道被 SDK 抽中发送静默 probe 的本地好友。
Generic Mesh presence probe没有好友候选时发送的广播 Mesh 探测包,用于快速判断当前范围内是否有 Mesh 节点。
Generic probe 错峰窗口generic probe 发包前的 40~260ms 确定性等待窗口,由 local_did + channel_id 派生,用于让同时扫描的新设备在同一信道内错开发包。
Generic probe dwell 窗口无好友每个信道等待 Mesh ACK 的窗口,由 Mesh flood settle 估算,最小 1500ms
当前范围有人本机在当前信道、当前射频范围、当前扫描窗口内收到 mesh implicit ACK、routing ACK、好友 app ACK,或当前 dwell 窗口内 Mesh RF 帧。
有好友本信道收到目标好友 routing ACK 或好友加密 app ACK。
未探测到当前信道在本机当前范围内没有收到任何 ACK;按产品逻辑认为当前范围无人可达。
RSSI 最优信道首次安装或设置页重新搜索全程无 ACK 后,App 通过本地 AT+LISTEN 扫频得到的信号质量最佳信道。
上次信道扫描前正在使用的信道,或用户上次选择进入的信道。

Android API

Android 独立模块为 Channel Presence SDK 1.0.0,App 通过 host adapter 调用,不直接修改 Mesh SDK 内部实现。

val officialChannels =
AiTalkMeshRadioProfile.channels.map { row ->
ChannelPresenceChannel(
channelId = row.channelId,
frequencyHz = row.frequencyHz,
bcnId = row.bcnId,
)
}

val result =
ChannelPresenceScanSdk(host)
.scan(
ChannelPresenceScanRequest(
localNodeId = localNodeId,
scanId = scanId,
channels = officialChannels,
candidates = friendCandidates,
trigger = ChannelPresenceScanTrigger.FirstInstall,
channelPlanId = AiTalkMeshRadioProfile.PROFILE_ID,
),
)
interface ChannelPresenceScanSdk.Host {
fun switchToChannel(channel: ChannelPresenceChannel)
fun sendProbe(probe: ChannelPresenceBuiltProbe): Boolean
fun waitForMeshAck(packetIds: Set<Int>, timeoutMs: Long): ChannelPresenceMeshAck?
}
data class ChannelPresenceScanRequest(
val localNodeId: Int,
val scanId: Int,
val channelPlanId: Int,
val channels: List<ChannelPresenceChannel>,
val candidates: List<ChannelPresenceFriendCandidate>,
val trigger: ChannelPresenceScanTrigger,
val requestedProbeCount: Int = 8,
val dwellMs: Long = 800L,
val hopLimit: Int = 3,
)

enum class ChannelPresenceScanTrigger {
FirstInstall,
SettingsRescan,
}
data class ChannelPresenceMeshAck(
val packetId: Int,
val ok: Boolean,
val implicit: Boolean,
val fromNode: Int?,
val relayNode: Int?,
val channelId: Int,
val rssiDbm: Int? = null,
)
data class ChannelPresenceChannel(
val channelId: Int,
val frequencyHz: Long,
val bcnId: Int,
)

ChannelPresenceChannelPlan.channels(startFrequencyHz) 仍可生成 16 个、2MHz 间隔且 bcnId=channelId 的 caller-defined generic/custom/legacy 候选计划;它不代表正式产品表。正式请求必须使用 AiTalkMeshRadioProfile.channels 的显式行和 AiTalkMeshRadioProfile.PROFILE_ID。如果 App host 临时配置调试频点或其他不属于消息信道表的频点,必须使用 ChannelPresenceChannelPlan.nonMessageChannel(...) 或等价逻辑把 bcnId 设为 0

普通 ACK 的 channelId 必须来自底层/射频元数据报告的真实接收或回复逻辑信道;不得用 host 当前正在扫描并等待 ACK 的逻辑信道补写。SDK reducer 会用 packetId 找到发出的 probe,再校验 ack.channelId == sentProbe.channelId;不一致的 ACK 视为异频接收或跨信道残留 ACK,直接丢弃。底层 ACK 事件没有真实 channelId 时,host 不得把它直接传入 SDK 作为具体信道命中。

当前信道 RX 窗口是单独规则:host 在 switchToChannel 成功后打开窗口,在 sendProbe 成功后 arm 当前 probe packet ID;如果 dwell 超时前收到 Mesh RF 帧,host 可以合成 implicit=trueChannelPresenceMeshAck(packetId=armedPacketId, channelId=currentDwellChannel)。该证据来自本机在当前信道物理收到的帧,只能标记普通 可达信道,不能标记 有好友。host 合成前必须校验包头:fromNode == localNodeId 时,packetId 必须等于当前 armed probe,且 relayNode 必须是远端短 ID。例如本机 DID 0xC1810528CH9 dwell 内收到 from=0xC1810528 relay=0xA6 packetId=当前probe,表示 0xA6 转发了本机 probe,可以记录 CH9 普通可达;但这不是对方显式返回 CH9,不能标记好友,也不能当成对方常驻信道声明。

rssiDbm 来自收到该 Mesh RF 帧时的 transport/DI metadata,payload 内不强制携带。host 必须使用与 RSSI 扫频一致的机型/固件 offset 校准后再传入 SDK:display_rssi = raw_rx_rssi - rssi_offset_db。例如设备上报 raw_rx_rssi=-60、当前 TurMass offset 为 19dB,SDK 和 UI 记录/显示 -79 dBm。如果 transport 没有真实 RSSI metadata,不传 rssiDbm;不要把 0 这类缺省值显示成信号强度。SDK 对同一信道多次命中时保留最强的校准值,也就是数值更大的 dBm(如 -79 强于 -88)。该 RSSI 只用于结果展示和现场判断异频接收点,不单独决定可达性,也不替代 ack.channelId == sentProbe.channelId 校验。

host 层实现必须把“隐式 Mesh 转发”和“显式业务 ACK”分开:前者只说明当前 dwell 信道内有人参与转发;后者由 CHANNEL_PROBE_ACK 携带 src_did + channel_id,表示接收方明确告诉扫描方“我的当前逻辑信道”。如果产品希望无好友 generic probe 也拿到对方 DID 和信道,需要新增 generic app ACK;V1 generic path 不从 fromNode=本机DID 推断这些信息。

dwellMs 的默认值只适用于常规好友抽样扫描。candidates 为空时,App 必须显式传入按当前 Mesh 参数计算的 dwell:max(1500, estimateFloodSettleMs(42, hopLimit) + 500)

data class ChannelPresenceFriendCandidate(
val nodeId: Int,
val displayName: String?,
val key16: ByteArray,
val lastKnownChannelId: Int?
)

候选好友由 App 注入,SDK 不直接读取 App 数据库。Android 可以用 FriendKeyStore、聊天数据库、最近会话和好友资料实现。如果 candidates 为空,SDK 会进入 generic Mesh presence probe 路径;设置页重新搜索也不能提前空返回。

data class ChannelPresenceScanResult(
val activeChannels: List<ChannelPresenceChannelResult>,
val bestScannedChannel: ChannelPresenceScannedChannel? = null,
)

data class ChannelPresenceChannelResult(
val channelId: Int,
val friendMarkers: List<ChannelPresenceFriendMarker>,
val evidenceKinds: List<ChannelPresenceAckKind>,
val rssiDbm: Int? = null,
)

data class ChannelPresenceScannedChannel(
val channelId: Int,
val frequencyHz: Long,
val rssiDbm: Int,
)

enum class ChannelPresenceAckKind {
MeshImplicitAck,
TargetRoutingAck,
FriendAppAck,
}

探测判定

每个信道的判定口径:

  • 收到 MeshImplicitAck:本机当前范围内有人转发了 probe,该信道可用。
  • 收到 TargetRoutingAck:目标好友 DID 收到了 probe,该信道可用并标记 有好友
  • 收到 FriendAppAck:目标好友 SDK 解密并处理了 probe,该信道可用并标记 有好友
  • 无好友 generic probe 收到 ACK、收到其他节点转发本机 probe,或当前 dwell 窗口内听到其他节点 Mesh RF RX:只标记该信道 当前范围有人friendMarkers 为空。
  • app ACK 的 channelId 必须等于发出 probe 的 channelId 才能保存;否则丢弃。
  • 未收到任何 ACK:当前信道在本机当前范围内未探测到可达设备。

未收到任何 ACK 是产品扫描结果,不表达该信道在其他位置、其他距离、其他天线状态下没有设备。

首次安装流程

if (!prefs.channelPresenceFirstInstallDone) {
val officialChannels = AiTalkMeshRadioProfile.channels.map(::toChannelPresenceChannel)
val result =
ChannelPresenceScanSdk(host).scan(
ChannelPresenceScanRequest(
localNodeId = localNodeId,
scanId = scanId,
channels = officialChannels,
candidates = friendProbeCandidateProvider.readAll(), // 可以为空
trigger = ChannelPresenceScanTrigger.FirstInstall,
channelPlanId = AiTalkMeshRadioProfile.PROFILE_ID,
),
)

val selected =
selectActiveChannel(result.activeChannels)
?: selectBestScannedChannelForChannelPresence()

selected?.let { radio.setResidentMessageChannel(it.channelId, it.frequencyHz, it.bcnId) }
prefs.channelPresenceFirstInstallDone = true
prefs.lastChannelPresenceScan = result.copy(bestScannedChannel = selected?.scannedChannel)
showChannelPresenceResult(result)
}

UI 规则:

  • 扫描中展示进度:信道搜索 / 正在搜索信道 xx%。iOS 和 Android 设置页入口必须显示相同百分比口径。
  • 首次安装且无好友时,每个信道切换后先进入 generic probe 错峰窗口,再发送静默 generic Mesh probe,并使用 Mesh flood ACK dwell 窗口等待转发回包。
  • 有活跃信道:自动切到推荐信道,推荐顺序是 有好友 > 本轮扫描顺序中后扫到的可达信道。
  • 活跃信道如果带 rssiDbm,必须在信道标记后显示校准值,例如 可达信道 · RSSI -79 dBm有好友的信道 · RSSI -79 dBm;iOS 和 Android 文案/数值口径保持一致。
  • 无活跃信道:执行本地 RSSI 扫频,切到 RSSI 最优信道;最终结果不显示“当前未可达设备”作为结束状态。
  • 首次安装或设置页重新搜索无 ACK 后切到 RSSI 最优信道时,结果中填充 bestScannedChannel,弹窗显示已切到的信道号和 RSSI。

设置页重新搜索流程

设置页增加一个操作项:

重新搜索信道

它是按钮/操作项,不是布尔开关。点击后进入扫描流程。

val result =
ChannelPresenceScanSdk(host)
.scan(
ChannelPresenceScanRequest(
localNodeId = localNodeId,
scanId = scanId,
channels = AiTalkMeshRadioProfile.channels.map(::toChannelPresenceChannel),
candidates = friendProbeCandidateProvider.readAll(),
trigger = ChannelPresenceScanTrigger.SettingsRescan,
channelPlanId = AiTalkMeshRadioProfile.PROFILE_ID,
),
)

showChannelSearchResultDialog(
activeChannels = result.activeChannels,
bestScannedChannel = result.bestScannedChannel,
)

弹窗规则:

  • 标题:信道搜索
  • 列表项展示:信道号、是否有好友、可点击切换动作。
  • 收到好友 ACK 的信道显示 有好友,可附好友昵称。
  • 普通可达信道显示 可达信道
  • 如果该可达信道带 rssiDbm,在 可达信道有好友 后追加 RSSI xx dBm
  • 设置页手动重搜全程无 ACK 时继续执行本地 RSSI 扫频,并切到 bestScannedChannel
  • 如果已选出 bestScannedChannel,弹窗显示 已根据信号质量切到信道 X(RSSI xx dBm),不显示“当前未可达设备”。
  • 设置页手动重搜没有好友候选时,也要逐信道发送 generic Mesh probe;不能直接返回空结果。
  • 首次安装无 ACK 后如果已扫频选出信道,显示同一套 已根据信号质量切到信道 X 文案。
  • 操作按钮:确定

候选好友抽样

每个信道最多抽 3..8 个好友,默认 8 个。推荐排序:

  1. 最近已知在该信道或相邻配置中出现过的好友。
  2. 最近聊天、最近连接或最近互动的好友。
  3. 其他好友按 hash(scan_id, channel_id, friend_did) 的 u32 无符号值升序做随机排序。

一个扫描周期内同一信道不要重复 probe 同一好友。重复点击重新搜索时允许重新抽样。

数据持久化

App 侧保存:

channel_presence_first_install_done: bool
last_channel_presence_scan_at: timestamp
last_channel_presence_scan_result: ActiveChannel[]
last_best_scanned_channel_id: int?
last_best_scanned_frequency_hz: long?
last_best_scanned_rssi_dbm: int?
last_selected_channel_plan_id: int?
last_selected_channel_id: int?
last_selected_frequency_hz: long?

正式 profile 变化后,如果 last_selected_channel_plan_id 与当前不同,不能直接按旧 channel_id 切信道,必须重新通过当前 ChannelPlan 确认映射。

SDK 仍保留显式候选计划、推荐结果、App 发起切换和缓存恢复接口,方便未来实现“切到其他频点仍能找到并继续聊天”。当前版本没有联系人跨频自动发现/漫游、联系人跨频自动切换/恢复、跨频聊天或跨频通话恢复;现有失败/取消缓存恢复和通话结束恢复 message/default 状态不在这项未实现范围内。不能把这些接口写成已完成能力。