【蓝牙精品系列文档】【03_blemesh架构】06_Friendship流程与空口交互

Friendship 工作流程与空口交互

概述

BLE Mesh 中FriendLPN(Low Power Node)是配对的低功耗方案,二者建立的关系称为Friendship。本文档基于 mynewt-nimble 源码(lpn.c/friend.c/transport.h)梳理完整流程与空口格式。


一、前置条件:必须配网完成后才能建立 Friendship(问题 1)

是的,Friend 和 LPN 都必须先完成配网,才能建立 Friendship。

原因

Friendship 的所有交互都走Mesh Network PDU(AD Type0x2A),属于Control 消息(CTL=1),需要:

必要条件来源
Unicast 地址配网时 Provisioner 在 Provisioning Data 中下发
NetKey配网时 Provisioner 下发,派生 Encryption Key / Privacy Key
IV Index配网时下发,与全网同步
DevKey配网时 ECDH 派生(Friendship 不直接用,但节点本身需要)

没有 NetKey = 不能发 Network PDU = 不能发 Friend Req。配网前节点只能发0x2BUnprovisioned Beacon,根本无法参与 Friendship。

关键区分:Friendship ≠ BLE 链路层 Pairing

概念层级是否需要配网
BLE Pairing链路层(L2CAP/SMP)与 Mesh 无关,Mesh 不用
Mesh ProvisioningMesh 配网协议「加入网络」
Mesh FriendshipMesh 网络层 Control 消息必须在配网之后

顺序:设备出厂 → 配网(获得 NetKey + Unicast)→建立 Friendship(LPN 找 Friend)→ 稳态运行。

同网约束

LPN 和 Friend必须在同一 Mesh 网络(共享 NetKey),因为:

  • Friend Req 用 LPN 持有的 NetKey 加密
  • Friend 必须能用同一 NetKey 解密 Friend Req
  • 多子网时 LPN 会逐个尝试自己加入的子网(源码bt_mesh_subnet_next(NULL)

二、Friendship 建立流程(4 步握手)

重要:4 步缺一不可

Friendship 建立必须经过 4 步:Req → Offer → Poll → Update。只看 Req/Offer 不够——第一次 Poll/Update 和稳态 Poll/Update空口格式相同但语义完全不同

步骤Friend 侧状态语义
① Req创建 LPN 上下文 + 派生凭证,established=0协商请求:LPN「我想找 Friend」
② Offerestablished=0协商响应:Friend「我可以服务你」
第一个 Pollestablished=1← 关键!建立触发:LPN「我确认选你」+ Friend「正式接受」
第一个 Update回带 Flags + IV Index建立确认 + 网络状态同步
后续 Poll/Updateestablished保持 1稳态查询:问「有消息吗」答「有/没有」

源码证据:Friend 何时标记"Friendship 已建立"

/* friend.c - bt_mesh_friend_poll() 内 */if(!frnd->established){BT_DBG("Friendship established with 0x%04x",frnd->lpn);frnd->established=1;/* ← 第一个 Poll 才置 1 */}

Friend 收到第一个 Poll 时才把established标记为 1!在此之前虽然创建了上下文、派生了凭证,但不会为 LPN 缓存消息,很多操作不执行(多处if (!frnd->established)检查)。

为什么 Req/Offer 不够?3 个关键原因

1. 双向链路确认(最关键)
① Req: LPN → 0xC001(广播) ← LPN 能发,但 Friend 不知道 LPN 是否存在 ② Offer: Friend → LPN(单播) ← Friend 能发给 LPN,但不知道 LPN 收到没 ③ Poll: LPN → Friend(单播) ← LPN 收到 Offer 后回 Poll,Friend 才确认双向可达

如果只有 Req/Offer:Friend 发完 Offer 不知道 LPN 收到没有。Poll 是 LPN 对 Offer 的「确认收到」。

2. Friend 需要第一个 Poll 才正式生效

源码多处检查established,未建立时:

  • 不会为 LPN 缓存消息
  • 收到发给 LPN 的消息会丢弃
  • 不执行 Friend Subscription 更新

只有第一个 Poll 触发established=1后,Friend 才真正开始为 LPN 服务。

3. 第一个 Update 同步网络状态

LPN 长时间睡眠,可能错过全网的 IV Update / Key Refresh。第一个 Update带回当前网络状态

structbt_mesh_ctl_friend_update{uint8_tflags;// KR + IVU 状态uint32_tiv_index;// 当前 IV Indexuint8_tmd;// More Data};

相当于「你睡觉期间网络变了这些,现在同步一下」。后续稳态 Update 也带这些字段,但第一个 Update 的同步意义最大

完整交互时序(区分建立 vs 稳态)

═══════════════════════════════════════════════════════════ 阶段 A:Friendship 建立(4 步握手,一次性) ═══════════════════════════════════════════════════════════ LPN Friend │ │ │ ① Friend Req (DST=0xC001) │ established=0 │ [LPNCounter, RSSI 要求, 超时...] │ 创建 LPN 上下文 + 派生凭证 │ ──────────────────────────────────> │ │ │ │ ② Friend Offer │ established=0 │ [FriendCounter, QueueSize, RSSI] │ 回报能力 │ <────────────────────────────────── │ │ │ │ LPN 本地派生凭证 │ │ │ │ ③ 第一个 Poll (FSN=0) │ ★ established=1 ★ │ 「我确认选你」 │ 「我正式接受你」 │ ──────────────────────────────────> │ │ │ │ ④ 第一个 Update │ 确认建立 + 同步网络状态 │ [Flags, IVIndex, MD=0] │ (LPN 可能错过 IV Update) │ <────────────────────────────────── │ │ │ │ Friendship 正式建立 ✅ │ ═══════════════════════════════════════════════════════════ 阶段 B:稳态运行(Poll/Update 循环,周期性) ═══════════════════════════════════════════════════════════ │ Poll (FSN=1) │ established=1 │ 「有消息吗」 │ 查缓存队列 │ ──────────────────────────────────> │ │ │ │ Update (MD=0) │ 「没有,继续睡」 │ <────────────────────────────────── │ │ │ │ 睡眠 N 秒 │ 持续扫描 + 缓存

类比理解

Mesh Friendship现实类比
① Req投简历(广播求职)
② Offer公司发 offer(意向)
③ 第一个 Poll你回 offer 签字(确认接受)
④ 第一个 Update公司发入职通知 + 同步公司现状
后续 Poll/Update每天打卡 + 问「有我邮件吗」

只投简历 + 收 offer 不算入职——必须你签字(第一个 Poll)+ 公司确认(第一个 Update)才算正式建立雇佣关系。之后才是日常打卡。

状态机(LPN 侧,源码lpn.c

BT_MESH_LPN_DISABLED │ bt_mesh_lpn_set(true) ▼ BT_MESH_LPN_ENABLED │ send_friend_req() ▼ BT_MESH_LPN_WAIT_OFFER ── 收到 Friend Offer ──> 派生凭证 + send_friend_poll() │ │ │ 超时无 Offer ▼ ▼ BT_MESH_LPN_WAIT_UPDATE BT_MESH_LPN_REQ_RETRY ── 重发 Friend Req │ │ 收到 Friend Update ▼ BT_MESH_LPN_ESTABLISHED (稳态)

三、各 Control 消息空口格式

Friendship 消息都是CTL=1(Control 消息),走 Network PDU,NetMIC=8B(注意比 Access 消息的 4B 长)。

通用封装

AD Structure: [Length][0x2A] └─ Network PDU (CTL=1) ├─ IVI/NID 1B ├─ CTL=1/TTL 1B ← CTL 固定 1 ├─ SEQ 3B ├─ SRC 2B ← 发送方 Unicast ├─ DST 2B ← Friend Req 用 0xC001;后续用对方 Unicast ├─ Transport PDU 变长 ← Control Opcode + 载荷 └─ NetMIC 8B ← Control 消息固定 8B

Transport 层 Control 头部

┌─────────────────────────────────┐ │ Seg(1b) | Opcode(7b) │ 1 字节 └─────────────────────────────────┘ - Seg = 1 表示分段,Friendship 消息一般 Seg=0(短消息) - Opcode 见下方各消息

① Friend Req(Opcode0x03

LPN 广播寻找 Friend,DST =0xC001(all-friends 固定组地址)

源码结构体(transport.h):

structbt_mesh_ctl_friend_req{uint8_tcriteria;// 选择 Friend 的 criteria(RSSI/队列