Mesh 网络层
核心概念
网络层(Network Layer)负责 Mesh 消息的寻址、中继和加密。它是在承载层之上、传输层之下的核心协议层。
地址类型
Unicast Address(单播地址)
| 范围 | 说明 |
|---|---|
| 0x0001 ~ 0x7FFF | 分配给配网后的节点,每个节点至少一个 |
| 特点 | 全网唯一,配网时由 Provisioner 分配 |
| 用途 | 点对点通信 |
Group Address(组播地址)
| 范围 | 说明 |
|---|---|
| 0xC000 ~ 0xFEFF | 固定或动态分配的组地址 |
| 特点 | 多个节点可订阅同一组地址 |
| 用途 | 分组控制(如:所有客厅灯) |
SIG 预定义组地址:
| 地址 | 名称 | 用途 |
|---|---|---|
| 0xC000 | all-proxies | 所有 Proxy 节点 |
| 0xC001 | all-friends | 所有 Friend 节点 |
| 0xC002 | all-relays | 所有 Relay 节点 |
| 0xC003 | all-nodes | 所有节点 |
是的,Relay = 中继。
在 BLE Mesh 里,Relay是一种节点特性(Feature):节点收到不是发给自己的Mesh 消息后,会重新广播转发,让消息多跳到达更远节点。
你文档里all-relays(0xC002)就是所有开启了 Relay 功能的节点组成的组地址。
和「中继」的对应关系
| 英文 | 中文 | 含义 |
|---|---|---|
| Relay | 中继 / 转发 | 转发 Mesh 消息,实现多跳 |
| Relay Node | 中继节点 | 具备 Relay 能力的节点 |
| all-relays | 所有中继节点 | 组播发给全网所有 Relay 节点 |
中继怎么工作
Node A (TTL=5) ──PDU──> Relay Node (TTL=4) ──PDU──> Node B (TTL=3) │ TTL 减 1 后重发- 每中继一次,TTL 减 1
- TTL 到 0 或 1 时不再转发
- 随机延迟 0~50ms,减少冲突
和其他 SIG 组地址对比
| 组地址 | 节点类型 | 作用 |
|---|---|---|
| all-proxies | Proxy | 帮手机走 GATT 接入 Mesh |
| all-friends | Friend | 帮 Low Power 节点缓存消息 |
| all-relays | Relay | 负责多跳转发,扩大覆盖 |
| all-nodes | 全部 | 广播给网络内所有节点 |
实际场景
照明网络里,常供电的灯通常会开 Relay,既当业务节点,也当中继,把控制命令传到远处开关或传感器。
注意:Relay 和 WiFi 里的「中继器」概念类似,都是转发别人流量;Mesh 里是否中继由Relay State配置决定,可以开关。
Virtual Address(虚拟地址)
| 范围 | 说明 |
|---|---|
| 0x8000 ~ 0xBFFF | 基于 Label UUID 的地址 |
| 特点 | 128-bit UUID 哈希生成,碰撞概率极低 |
| 用途 | 跨网络共享资源 |
其他
| 地址 | 名称 | 说明 |
|---|---|---|
| 0x0000 | Unassigned | 未分配地址(未配网设备) |
| 0xC004~0xCFFF | RFU | 保留 |
| 0xD000~0xFEFF | Dynamic Group | 动态组地址 |
| 0xFF00~0xFFFF | RFU | 保留 |
网络 PDU 格式
Network PDU: ┌────────┬────────┬─────────┬──────────┬───────────┬──────────┬─────┐ │ IVI:1b │ NID:7b │ CTL:1b │ TTL:7b │ SEQ:24b │ SRC:16b │ ... │ │ │ │ │ │ │ │ │ └────────┴────────┴─────────┴──────────┴───────────┴──────────┴─────┘ 完整结构: ┌──────────┬─────┬──────┬──────┬────────┬──────┬─────────┬──────┐ │ IVI+NID │ CTL │ TTL │ SEQ │ SRC │ DST │ Transport│ MIC │ │ 1 B │ 1b │ 7b │ 24b │ 16b │ 16b │ PDU │ 4/8B │ │ │ │ │ │ │ │ (加密) │ │ └──────────┴─────┴──────┴──────┴────────┴──────┴─────────┴──────┘字段详解
| 字段 | 大小 | 说明 |
|---|---|---|
| IVI | 1 bit | IV Index 最低位,用于识别 Network Key |
| NID | 7 bits | Network ID,派生自 Network Key,快速识别所属网络 |
| CTL | 1 bit | 0=Access 消息,1=Control 消息 |
| TTL | 7 bits | 生存时间,每中继一次减 1,到 0 不再转发 |
| SEQ | 24 bits | 序列号,防重放攻击 |
| SRC | 16 bits | 源地址(Unicast) |
| DST | 16 bits | 目标地址(Unicast/Group/Virtual) |
| Transport PDU | 变长 | 加密的传输层数据 |
| MIC | 4/8 B | 消息完整性校验码(CTL=0 用 4B,CTL=1 用 8B) |
IV Index
- 32-bit 计数器,用于加密和防重放
- 所有节点共享同一 IV Index
- IV Index 只能递增
- IV Update 过程:
- 节点检测到 IV Index 需要更新
- 发送 Secure Network Beacon,IV Update Flag = 1
- 其他节点进入 IV Update 状态
- 完成后 IV Update Flag = 0,新 IV Index 生效
中继机制
Relay 节点
- 收到非自己的 Mesh 消息后重新广播
- 每次中继 TTL 减 1
- TTL = 0 或 TTL = 1 时不中继
- 中继延迟:随机 0~50ms(避免冲突)
中继流程
Node A (TTL=5) ──PDU──> Relay Node (TTL=4) ──PDU──> Node B (TTL=3) │ TTL 减 1 后重发Relay 特性配置
| 配置 | 说明 |
|---|---|
| Relay State | 开启/关闭中继功能 |
| Relay Retransmit Count | 中继重发次数 (0~7) |
| Relay Retransmit Interval | 中继重发间隔 (0~31, 单位 10ms) |
中继节点是怎么"宣告"的(问题 1)
关键:Relay 不靠 Beacon 周期性宣告身份。Mesh 协议里 Relay 的"身份"通过两个机制暴露:
1. Composition Data(静态能力上报)
节点配网后,Provisioner 通过Config Composition Data Get读取节点的 Composition Data,里面包含节点支持哪些 Feature:
Composition Data Page 0 ├── CID / PID / VID ├── Features 字段(16-bit 位图) │ ├── bit0: Relay ← 是否支持中继能力 │ ├── bit1: Proxy │ ├── bit2: Friend │ └── bit3: Low Power └── Elements / Models这是能力声明:「我硬件上能做 Relay」,不等于"现在开着"。
2. Relay State(运行时开关)
是否真正启用中继,由Relay State决定,Provisioner 通过Config Server Model远程配置(详见下文「Relay 配置消息空口交互」章节)。
全网没有任何节点需要预先知道「谁是 Relay」——这是 Mesh 泛洪的本质:每个收到包的节点自己判断要不要转发。
转发时 TTL-1 是规范硬性要求(问题 1)
NimBLEbt_mesh_net_relay()实现清晰:
步骤 1:TTL 检查,TTL ≤ 1 不转发
if(rx->ctx.recv_ttl<=1U){return;}- TTL=0:禁止转发(规范定义)
- TTL=1:到达最后一跳,转发出去会变 TTL=0,没意义,不转发
步骤 2:检查 Relay 是否开启
if(rx->net_if==BT_MESH_NET_IF_ADV&&!rx->friend_cred&&bt_mesh_relay_get()!=BT_MESH_RELAY_ENABLED&&bt_mesh_gatt_proxy_get()!=BT_MESH_GATT_PROXY_ENABLED){return;}- 从 ADV 收到的包:必须 Relay 开启
- 从 GATT 收到的包:必须 GATT Proxy 开启
步骤 3:TTL 减 1 写回包头(保留 CTL 位)
/* Leave CTL bit intact */sbuf->om_data[1]&=0x80;sbuf->om_data[1