Friendship 工作流程与空口交互
概述
BLE Mesh 中Friend和LPN(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 Provisioning | Mesh 配网协议 | 「加入网络」 |
| Mesh Friendship | Mesh 网络层 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」 |
| ② Offer | established=0 | 协商响应:Friend「我可以服务你」 |
| ③第一个 Poll | established=1← 关键! | 建立触发:LPN「我确认选你」+ Friend「正式接受」 |
| ④第一个 Update | 回带 Flags + IV Index | 建立确认 + 网络状态同步 |
| 后续 Poll/Update | established保持 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 消息固定 8BTransport 层 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/队列