概述

Bluetooth Mesh 是基于低功耗蓝牙技术(Bluetooth Low Energy)的网状网络解决方案。 网状网络主要分为两种:泛洪网状网络(flooding-based mesh network)和路由网状网络(routing-based mesh network)。 这两种网络类型各有利弊,Bluetooth Mesh 对应于这两种网络的转发消息的方式分别称为 Managed Flooding 和 Directed Forwarding

LE 的通信信道有两类:advertising channel 和 data channel。 Mesh 主要工作在 advertising channel 上,通过 passive scanning 和 advertising,分别进行接收和发送。 而 data channel 主要是为了兼容现有的不支持 advertising 的设备,可以通过 LE link 的方式进行通信。

术语与概念

主要分为两部分介绍: Mesh 概念Mesh 框架

Mesh 概念

这部分介绍 Mesh 的主要概念,Mesh Protocol 的详细内容请参考 Mesh Protocol Specification, 初步了解可以查阅 Mesh Protocol Specification 的第二章 Mesh system architecture

Device UUID

每个设备出厂时被分配一个唯一的 16 字节 UUID,称作 Device UUID,用于唯一标识一个 mesh 设备,不用依赖蓝牙地址来标识设备。 在建立 PB-ADV bearer link 时,需要 Device UUID 字段来标识 device。然而,当 mesh device 获取 mesh address 后,即可用 mesh address 来唯一标识 device。

Mesh 地址

除了建立 LE link,Mesh 通信并不依赖蓝牙地址,即节点的蓝牙地址可以一样,或者随机变化。 Mesh 定义了长度为 2 字节的 Mesh 地址,分为 Unassigned Address、Unicast Address、Virtual Address 和 Group Address,地址范围如下表所示。

Mesh 地址

Range

Address

说明

0x0000

Unassigned Address

未分配地址,device 默认地址,不能用于发送或接收 mesh 消息

0x0001-0x7FFF

Unicast Address

单播地址,每个 element 会被分配一个单播地址,message 的源地址一定要是 unicast address

0x8000-0xBFFF

Virtual Address

虚拟地址,由 label uuid 生成,数量多,可以不用集中管理

0xC000-0xFFFF

Group Address

组播地址,分为两类

0xC000-0xFEFF: 自由分配组播地址

0xFF00~0xFFFF: 固定组播地址

其中固定组播地址定义包含如下。如有更新,请参考 Mesh Protocol SpecificationGroup address 小节。

Mesh 固定组播地址

Values

Fixed Group Address Name

0xFFFB

all-directed-forwarding-nodes

0xFFFC

all-proxies

0xFFFD

all-friends

0xFFFE

all-relays

0xFFFF

all-nodes

Mesh 地址并不是出厂时设置的,而是由用户自己统一管理和分配的。 用户在配置设备入网时,通过 Provisioning 流程给设备分配单播地址。Provisioner 需要确保给每个设备分配的单播地址是不重复的。 Mesh 设备可能不止一个 单播地址,设备内每个 Element 会被分配一个单播地址,且该地址是连续的。 多地址被设计用于区分 Mesh 设备上重复的功能模块 Model。

应用模型

LE 是 master 连接 slave 的一对一的通信,而 mesh 网络是多对多的通信。 因而,mesh 网络存在一个天然特性,就是节点之间并不知道其他节点的存在。 此时需要 Provisioner 作为第三方,将节点之间联系起来。 例如一个通用的开关,出厂后是未确定要控制哪盏或者哪些灯, 需要 Provisioner 通过 Configuration Client 配置开关发布 publish 消息(设置目的地址为单播地址、组播地址)。 如果目的地址是组播地址,则需同时配置相应的灯泡订阅 subscribe 消息(设置分组,即增加组播地址到订阅列表)。 然后,开关就可以控制这一盏灯、一组灯或者所有灯。

Mesh 将典型应用场景的操作进行了标准化,每个 mesh 设备上的应用是以 Model 为单位进行组织的。 Model 定义了一个 Model ID、一套 Opcodes 和一组状态,规定发送和接收哪些消息,分别操作哪些状态。 Model 和 LE 的 GATT service 是类似的,都用于定义一个特定的应用场景。

为了支持多个相同的 model,定义了 element 的概念,每个 element 会单独分配一个 element address,且地址是连续的。 第一个 element(primary element)的 element address 是在 Provisioning 过程中分配的 node address,其他 element 的地址顺序往后排。 例如一个 mesh 设备上有两盏完全一样的且可以独立控制的灯,开关设备去控制这个灯设备,需要区分控制哪盏灯。 让这两盏灯对应的两个 Model 分开放在两个 element 中,这样每盏灯分别有一个 mesh address, 就可以通过 mesh address 将两盏灯区分开来了,进行独立控制。 当然,两盏灯的 model 也可以订阅同一个组地址,实现同时控制。这样既能独立控制,也能同时控制。

设备上 element、model 组成情况通过 composition data page 0 表达, Provisioner 可以通过获取设备的 composition data page 0 来辨识设备支持的应用。

安全性

Mesh 中有很多保护网络安全和隐私的设计,能够抵挡被动监听、中间人攻击、重放攻击、垃圾桶攻击和暴力破解等常见的攻击。

Mesh 网络中所有 mesh 消息都会被加密和校验,防止被窃听或篡改。 mesh 网络中密钥分两层:NetKey 和 AppKey,每层最多可以有 4096 个密钥,通过 12 bit 的 index 标识。AppKey 必须绑定一个 NetKey,且只能绑定该 NetKey。 应用层发送消息会依次经过 AppKey 和 NetKey 两层加密和校验,接收消息会依次经过 NetKey 和 AppKey 两层解密和校验。 采用两层密钥,是为了防止 relay 节点窃听或者篡改消息。 例如节点 A 通过节点 B 转发给节点 C 发数据,A/B/C 有相同的 NetKey,A/C 有相同的 AppKey,而 B 没有该 AppKey。 那么 A 和 C 间的应用层通信对 B 来说是保密的,B 只是使用 NetKey 在网络层帮忙转发,因为没有 AppKey 而不能进行窃听或者篡改应用层消息。

NetKey 支持多个密钥,多密钥可以用来划分网络范围,实现设备间的隔离。 Key index 为 0 的是主网络密钥,其余的都是普通的其他子网络密钥。 只有主网络中的节点才能参与 IV Update Procedure,并将 IV 更新信息传递到其他子网中。 也就是说,只有主网络节点才能更新 IV index 网络参数,其他子网的节点只能被动的接收 IV index 更新。 这样不平等的网络密钥设计的目的是约束子网络节点的数据发送频次,防止子网络节点滥用 IV index 更新而耗尽 IV index,从而导致网络安全问题。 通常大部分节点在主网络中,部分节点同时处于主网络和某个子网络,少量节点只处于某个子网络, 此时这些少量节点只能在子网络内进行局部通信,从而限定这些少量节点的通信范围,例如酒店顾客只能控制自己房间内的灯。

AppKey 也支持多密钥,密钥之间并无区别。不同的应用可以使用不同的密钥,实现应用间的隔离。 例如灯控和门控使用不同的 AppKey。一个应用也可以使用多个 AppKey, 例如一盏灯上支持 2 个 AppKey,AppKey0 给用户 0 用,AppKey1 给用户 1 用,将 AppKey0 删除,那么用户 0 就不能再控制灯,而用户 1 则不受影响。

Device 上的 NetKey 和 AppKey 是 Provisioner 通过 Provisioning 和 Configuration Client 分发和管理的。 Provisioner 是网络管理员,管理着所有的 key,即管理网络中各个 device 各自可以使用哪些 key, 而 device 间只有共享相同的密钥才能互相通信,例如灯和灯的开关使用相同的秘钥。 Provisioning 过程会分发 mesh address 和有且只有一个 NetKey, 后续通过 Configuration Models 来管理,例如通过 Configuration Client 增加 NetKey 和 AppKey。

Provisioning 过程还会生成一种特殊的 AppKey,称作 DevKey。 DevKey 只有 Provisioner 和 Device 两者知道,不和任何其他 Device 共享, 保证了 Provisioner 可以单独和某一个 Device 进行秘密的一对一通信。 Configuration 配置被限制只能使用 DevKey,只有 Provisioner 才知道 Device 的 DevKey, 所以只有 Provisioner 才可以配置 Device。 举例而言,开关只能控制灯泡亮灭,而不能去配置灯泡的分组。

Provisioning

Device 出厂默认是没有地址和密钥的,需要通过 Provisioning 过程从 Provisioner 获得。 Device 被 Provisioning 后被称作 Node。本文和 Sample 中并没有严格区分 Device 和 Node 这两个概念。 Provisioning 过程采用 ECDH 算法进行密钥协商和分发,通过 authentication data 进行身份鉴权,能够防止窃听、暴力破解和中间人攻击。

根据 Provisioning 配置不同,会有不同的模式,public key 的交互分为 OOB 和 no OOB 两种, authentication data 的交互分为 input OOB、output OOB、static OOB 和 no OOB 四种,所以模式最多有8种。 Device 的应用层可能需要给 stack 提供 public/private key 和 authentication data。 Provisioner 的应用层可能需要给 stack 提供模式选择、Device 的 public key 和 authentication data。

Provisioning 流程可以工作在 advertising channel 和 data channel 两种信道上, 分别对应 PB-ADV 和 PB-GATT 两种传输层。 PB-ADV 使用 LE advertising 传输 Provisioning PDUs 进行配网; PB-GATT 则是需要建立 LE link,通过 Mesh Provisioning Service 来传输 Provisioning PDUs 进行配网。 Device 是被强制要求支持 PB-ADV 的,如果同时支持 PB-GATT,在 Provisioning 阶段可以由 Provisioner 任选一个。

Configuration

网络参数的管理是在 model 层实现的,称作 Configuration Models。 可以配置的网络参数有很多,例如 NetKey 和 AppKey 增加、删除、修改等,model的密钥绑定、消息发布、消息订阅等, 节点应用结构 Composition data page 0 的获取,节点的默认 TTL、支持的 feature、网络重传次数等。

网络参数的配置被限定为只能使用 DevKey,也就是说只有 Provisioner 才能配置节点的网络参数。

Proxy

Mesh主要工作在 advertising channel 上,为了兼容一些不能灵活自由的 advertising 的设备,mesh 定义了 proxy 特性。 基于 LE GATT profile,定义了 proxy service,让这些设备利用 LE link 的方式连接到支持 proxy 的节点上,从而接入到 mesh 网络。

Mesh 框架

Mesh框架如下图所示,分为 bearer layer、network layer、lower transport layer、 upper transport layer、access layer、foundation model layer 以及 model layer。

../../../../../_images/mesh_system_architecture.png

Mesh 框架

Model Layer (Foundation)

Model 定义了特定应用的一组状态和操作,通过 Model ID 来标识, 分为 16 bits Model ID 的 SIG model 和 32 bits Model ID 的 vendor model 两种。 实现上,会对 SIG Model ID 进行编码,16 bits SIG Model ID 会被编码成 32 bits Model ID。 例如 configuration server 和 client 的 Model ID 是 0x0000 和 0x0001, 但实现上将 configuration model 编码成了 0x0000FFFF 和 0x0001FFFF,具体代码如下。

#define MESH_MODEL_CFG_SERVER    0x0000FFFF
#define MESH_MODEL_CFG_CLIENT    0x0001FFFF

Element 用于隔离相同的 model,即使用了相同 access layer opcode 的 model。 重复的 model 会发送或接收相同的 opcode,此时无法区分是哪一个 model 在发送或者要接收这个 opcode 对应的 access message。 而 element 会被分配不同的 element address,这样可以通过 message 的 source address 或者 destination address 来有效的区分 model。 Mesh stack 采用先创建 element,再在指定 element 下注册 model 的方式。 如果注册重复的 model,需要为该 model 指定不同的 element,stack 会去检查一个 element 下的 model 是否冲突。

Mesh Protocol Specification 中强制要求的 Configuration Server Model 不需要 APP 注册,由 stack 负责注册和处理。

Health server model 只有在 primary element 是强制要求注册的,其他 element 用户可根据需求自行创建并注册。

Element 和 Model 创建注册完毕后,就可以生成 composition data page 0 的内容, 需要APP提供具体的 header 信息,包含 Company ID、Product ID、Version ID、RPL 大小以及所支持的 features。

Element 创建、Model 注册以及 Composition data page 0 生成的具体步骤示例代码如下。

void mesh_stack_init(void)
{
   ......
   /** create elements and register models */
   mesh_element_create(GATT_NS_DESC_UNKNOWN);
   health_server_reg(0, &health_server_model);
   ping_control_reg(ping_app_ping_cb, pong_receive);
   compo_data_page0_header_t compo_data_page0_header = {COMPANY_ID, PRODUCT_ID, VERSION_BUILD};
   compo_data_page0_gen(&compo_data_page0_header);
   ......
}

Access Layer

Access Layer 定义了上层应用的消息格式,其中包含 operation codes, 即 access opcode,所有应用消息必须包含统一的 opcode。 Mesh Protocol Specification 定义了一部分 opcode,也预留了空间让厂商自定义 opcode。 自定义 opcode 是 3 字节的,第一个字节的高两位为 1,低 6 位为厂商自定义的 opcode, 后两个字节是 Company ID,即每个 Company 只能自定义 64 个 opcode。

如下代码所示,0xC0 表示是 0 号 manufacturer-specific opcode,0x005D 对应 Realtek 的 Company ID,需要按字节序填入。

#define MESH_MSG_PING    0xC05D00

Access 层消息首先会被 transport layer 使用 AppKey 加密, Transport layer MIC¹ 校验长度支持 4 bytes 和 8 bytes。 如果使用了 4 bytes transport layer MIC,access 层消息最大长度为 380 bytes; 如果使用了 8 bytes transport layer MIC,则最大长度为 376 bytes。

Transport Layer (Upper & Lower)

Transport layer 负责应用层加解密,friendship 维护,heartbeat,directed forwarding 维护和 segmentation & reassembly。

Access layer 消息采用 4 bytes transport layer MIC,消息长度小于等于 11 bytes(包含 access opcode), 且不主动要求使用分片包格式传输,则这笔消息不会被分片 segmented,即以一笔 advertising packet 方式发送出去。 反之,access 消息会被 segmented,根据消息长度不同被一笔或者多笔 advertising packet 发送出去, 且会要求接收端回复 transport layer 的 acknowledge。

Network Layer

Network Layer 负责网络层加解密、加扰和 relay。

Bearer Layer

Bearer Layer 分为 loopback bearer、advertising bearer、GATT bearer 和 other bearer。

Loopback bearer 是自发自收通道,用于 model 间通信。

Advertising bearer 是使用 LE advertising 和 LE scan 进行发送和接收数据的通道,用于节点间相互通信。

GATT bearer 是基于 GATT 连接使用 proxy protocol 传输和接收数据的通道,一般用于不支持 advertising bearer 的节点通信。

Other bearer 是其他接口,用于在 bearer 层扩展 mesh 网络。例如 Gateway 可以通过 other bearer 连接 Mesh 网络和以太网, 使得 Mesh 网络可以接入 Internet。Other bearer 的注册发送消息函数接口是 bearer_other_reg() ,接收消息接口是 bearer_other_receive()

特性

  • Relay

    接收并通过 advertising bearer 重新传输 mesh message,以便扩展网络。

  • Proxy

    接收并在 GATT bearer 和 advertising bearer 之间重新传输 mesh message。

  • Low Power

    在 mesh 网络中可以大幅降低接收占空比,降低功耗运行,需要支持 Friend feature 的节点配合。

  • Friend

    通过存储支持 Low Power feature 节点的消息,帮助其运行。

  • Enhanced Provisioning Authentication

    支持 Provisioning Protocol 中更多的算法。

  • Remote Provisioning

    当 Provisioner 超出未 unprovisioned devices 的直接无线电范围时,仍可将其添加到 mesh 网络。 另外,Provisioner 可以使用通过 provisioning 数据包更改 mesh 节点的设备密钥。

  • Private Beacons

    提供 Secure Mesh Beacons 的隐私保护。

  • Directed Forwarding

    通过选择一部分节点将消息从源点中继到目的地,以帮助提高多跳网络的性能。

  • Subnet Bridge

    支持 mesh 网络的子网桥接。

  • Binary Large Object Transfer

    使 mesh 设备之间能够传输大量数据。

  • Device Firmware Update

    使 mesh 设备可以通过 mesh 网络完成固件升级。

接口提供

Mesh Stack 提供的 API 如下表所示。

Mesh Stack Provided APIs

Header File

Description

mesh_provision.h

mesh provisioning protocol

mesh_service.h

mesh service advertising

mesh_access.h

mesh access layer

mesh_transport.h

mesh transport layer

mesh_network.h

mesh network layer

mesh_bearer.h

mesh bearer layer

mesh_common.h

mesh common part

mesh_flash.h

mesh flash storage

mesh_node.h

mesh node management

mesh_api.h

mesh application interface

mesh_beacon.h

mesh beacon

mesh_config.h

mesh configuration

model_config.h

model configuration

provision_adv.h

provisioning ADV bearer

provision_device.h

provisioning device APIs

provision_generic.h

provisioning generic layer

provision_provisioner.h

provisioning provisioner APIs

proxy_protocol.h

proxy protocol

subnet_bridge.h

subnet bridge

directed_forwarding.h

directed forwarding

friendship_fn.h

friend node

friendship_lpn.h

low power node

heartbeat.h

heartbeat

provision_client.h

provision service client

provision_service.h

provision service

proxy_client.h

proxy service client

proxy_service.h

proxy service

remote_provisioning.h

remote provisioning

功能

本节主要介绍 Mesh Stack 不同功能或者模块如何使用和配置。

Mesh GAP

Mesh 工作在 advertising channel 上,通过 advertising 发数据以及通过 scanning 接收数据。 LE 的 advertising 和 scanning 是周期性行为,而 mesh 对 advertising channel 的使用比较复杂。 为了便于使用,Mesh stack 对 LE 的 GAP 接口进行封装,称作 Mesh GAP,此时 APP 不能再直接去调用 LE 的 GAP 接口。

Advertising

Mesh 封装后的 advertising 是一次性行为,而非周期性行为。不再有 advertising interval 参数,也不需要 enable 和 disable 动作来开关 advertising。

Advertising 示例如下,需要先通过 gap_sched_task_get() 获取到 buffer 之后,再使用 gap_sched_try() 进行发送。

uint8_t vendor_data[] =
{
   /* Flags */
   0x02,
   GAP_ADTYPE_FLAGS,
   GAP_ADTYPE_FLAGS_LIMITED | GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED,
   /* Local name */
   0x07,
   GAP_ADTYPE_LOCAL_NAME_COMPLETE,
   'V', 'E', 'N', 'D', 'O', 'R',
};

bool advertising_transmit(void)
{
   uint8_t *pbuffer = gap_sched_task_get();
   if (pbuffer == NULL)
   {
      return false;
   }
   gap_sched_task_p ptask = CONTAINER_OF(pbuffer, gap_sched_task_t, adv_data);
   memcpy(ptask->adv_data, vendor_data, sizeof(vendor_data));  // set adv data
   ptask->adv_len = sizeof(vendor_data);                       // set adv len
   ptask->adv_type = GAP_SCHED_ADV_TYPE_IND;                   // set adv type
   ptask->retrans_count = 3;                                   // set adv retrans count
   ptask->retrans_interval = 10;                               // set adv interval, ms
   gap_sched_try(ptask);                                       // send adv
   return true;
}

备注

retrans_countretrans_interval 指的是 advertising 时一次性行为的传输次数和间隔,传输结束会停止。 如果 APP 想发送周期性 advertising,需要自行创建定时器定时发送,达到周期性广播的效果。 例如每隔 1s 调用一次上述函数,实际效果是每隔 1s,设备会发 4 次广播,每次间隔 10ms。

Scanning

对于 mesh 应用而言,scan 通常要一直 enable。mesh stack 启动时, 根据 mesh_stack_init() 里面配置的 features 中的 bg_scan 置位与否,决定是否立即开始 scan。

void mesh_stack_init(void)
{
   ......
   /** config node parameters */
   mesh_node_features_t features =
   {
   .role = MESH_ROLE_DEVICE,
   .relay = 1,
   .proxy = 1,
   .fn = 0,
   .lpn = 0,
   .prov = 1,
   .udb = 1,
   .snb = 1,
   .bg_scan = 1, // enable background scan
   .flash = 1,
   .flash_rpl = 0
   };
   ......
}

Scan 的开关通过 gap_sched_scan() 控制;Scan 的参数是由 gap_sched_params_set() 设置,且在 mesh stack 启动前后调用都可以。

Initiating

与 LE 一致,未做额外封装,可以作为 master 建立连线。

Mesh Stack 的 GAP 行为

Mesh Stack 会有默认的 GAP 行为,无须用户实现。

  • Beacon Advertising
    设备处于 Unprovisioned 时,需要发送 Unprovisioned Device beacon。
    设备处于 Provisioned 时,需要发送 Secure Network beacon。
  • Service Advertising
    设备如果支持 PB-GATT,处于 unprovisioned 时,需要发送 provision advertising。
    设备如果支持 Proxy,处于 provisioned 时,需要发送 proxy advertising。
  • Receive Scanning

    Mesh 通信是实时的,任何时刻都有可能需要接收消息。但是 scan 时功耗比较高。可以通过设置 scan 参数降低 scan 占空比来降低功耗,但也因此会牺牲可靠性,有可能会漏掉一些消息。

  • Relay Scanning

    如果设备要支持 relay,是要一直开启 scan 才能更好地帮助其他设备转发消息。因此对于 relay 节点 background scan 通常是打开的。

设备信息配置

应用层可以调用 gap_sched_params_set() 接口配置 device name 和 appearance 等信息,具体代码如下。

void mesh_stack_init(void)
{
   ......
   /** set device name and appearance */
   char *dev_name = "Mesh Device";
   uint16_t appearance = GAP_GATT_APPEARANCE_UNKNOWN;
   gap_sched_params_set(GAP_SCHED_PARAMS_DEVICE_NAME, dev_name, GAP_DEVICE_NAME_LEN);
   gap_sched_params_set(GAP_SCHED_PARAMS_APPEARANCE, &appearance, sizeof(appearance));
   ......
}

Device UUID 设置

Mesh 节点可能拥有相同的蓝牙地址或者随机的蓝牙地址,需要通过 Device UUID 来唯一辨识 mesh 节点,设置 Device UUID 代码如下。

void mesh_stack_init(void)
{
   ......
   /** set device uuid */
   uint8_t dev_uuid[16] = MESH_DEVICE_UUID;
   device_uuid_set(dev_uuid);
   ......
}

网络特性和参数配置

Node 支持的 features、密钥个数、virtual address 个数、subscription list 大小、flash 存储、 advertising 周期等网络参数,需要 APP 根据实际情况配置,具体代码如下。

void mesh_stack_init(void)
{
   ......
   /** config node parameters */
   mesh_node_features_t features =
   {
      .role = MESH_ROLE_DEVICE,
      .relay = 1,
      .proxy = 1,
      .fn = 0,
      .lpn = 0,
      .prov = 1,
      .udb = 1,
      .snb = 1,
      .bg_scan = 1,
      .flash = 1,
      .flash_rpl = 1,
   };
   mesh_node_cfg_t node_cfg =
   {
      .dev_key_num = 2,
      .net_key_num = 6,
      .master_key_num = 3, // shall <= net_key_num
      .app_key_num = 3,
      .vir_addr_num = 3,
      .rpl_num = 20,
      .sub_addr_num = 10,
      .proxy_num = 1,
   };
   mesh_node_cfg(features, &node_cfg);
   ......
}

注意

参数配置中与 mesh flash layout 相关的参数如有改动,或者节点注册的 elements 或者 models 数量变化引起 layout 变化, 节点在初始化时会清除之前已经存储的信息并回到 Unprovisioned 状态。 会影响 mesh flash layout 的参数包括 dev_key_numnet_key_nummaster_key_numapp_key_numvir_addr_numrpl_numsub_addr_numflash_rpl。 如有新的 feature 支持,上述参数可能会增加。

Mesh Log 配置

Mesh stack 内部有多个模块,每个模块的各个级别 log 可以单独被开关,默认是都打开的。 Log 级别共有四级,由高到低等级和输出函数分别是 LEVEL_ERRORprinte()LEVEL_WARNprintw()LEVEL_INFOprinti() 以及 LEVEL_TRACEprintt(),越低级别的 log 越不重要。 由于 log 速率有限,当打印打太多 log 时,log 会丢失。为了保证关键 log 正常打印,可以关掉一些不重要的 log。 示例如下,关闭所有 mesh 模块 LEVEL_TRACE 级别的 log。

void mesh_stack_init(void)
{
   ......
   /** set mesh stack log level, default all on, disable the log of level LEVEL_TRACE */
   uint32_t module_bitmap[MESH_LOG_LEVEL_SIZE] = {0};
   diag_level_set(LEVEL_TRACE, module_bitmap);
   ......
}

Provisioning

Device 出厂设置中是不含任何网络信息的,需要通过 Provisioning 过程从 Provisioner 获取 Address 和 NetKey 等网络信息。

Device 需要配置 Provisioning capabilities,具体代码如下。

void mesh_stack_init(void)
{
   ......
   /** configure provisioning parameters */
   prov_capabilities_t prov_capabilities =
   {
      .algorithm = PROV_CAP_ALGO_FIPS_P256_ELLIPTIC_CURVE,
      .public_key = 0,
      .static_oob = 0,
      .output_oob_size = 0,
      .output_oob_action = 0,
      .input_oob_size = 0,
      .input_oob_action = 0
   };
   prov_params_set(PROV_PARAMS_CAPABILITIES, &prov_capabilities, sizeof(prov_capabilities_t));
   ......
}

Provisioner 和 Device 都需要注册 Provisioning 的 callback 处理函数,如下所示。 根据 Provisioning 具体流程的不同,需要在 Provisioning callback 里完成不同的处理。

void mesh_stack_init(void)
{
   ......
   prov_params_set(PROV_PARAMS_CALLBACK_FUN, prov_cb, sizeof(prov_cb_pf));
   ......
}

Provisioner 和 Device 间可以通过 PB-ADV 或者 PB-GATT 两种通道完成 Provisioning 流程。 通道建立成功后,才可以开始 Provisioning 流程。

PB-ADV

根据 Device 的 Device UUID,Provisioner 调用 pb_adv_link_open() 发起 PB-ADV 链路建立流程。

prov_cb() 回调函数里,会处理 PB-ADV 链路建立情况,具体代码如下。

bool prov_cb(prov_cb_type_t cb_type, prov_cb_data_t cb_data)
{
   ......
   switch (cb_type)
   {
   case PROV_CB_TYPE_PB_ADV_LINK_STATE:
      switch (cb_data.pb_generic_cb_type)
      {
      case PB_GENERIC_CB_LINK_OPENED:
         data_uart_debug("PB-ADV Link Opened!\r\n>");
         break;
      case PB_GENERIC_CB_LINK_OPEN_FAILED:
         data_uart_debug("PB-ADV Link Open Failed!\r\n>");
         break;
      case PB_GENERIC_CB_LINK_CLOSED:
         data_uart_debug("PB-ADV Link Closed!\r\n>");
         break;
      default:
         break;
      }
      break;
   ......
}

PB-GATT

Provisioner 需要主动和 Device 建立 LE link,查询 Provisioning service,并使能相应的 CCCD

Model 框架

蓝牙 SIG 组织定义了很多标准的 Model,Realtek 的 SDK 中已经包含了大部分常用 Model。 为了降低 APP 发难度,SDK 对 Model 层和 Access 层之间的消息处理进行了封装, 对 APP 提供一系列预定义的业务消息,使得 APP 只需关注于业务逻辑的开发。 关于 Mesh Model 的详细内容请参考 Mesh Model Specification

消息回调

在使用一个具体的 Model 之前都会定义一个如下所示的 Model 信息的结构体,其中需要用户关注的有如下三个回调函数。

  • model_receive() 函数是 Access 层到 Model 层消息的回调函数, Realtek 的 SDK 中已经实现了默认的 model_receive() 函数,但用户也可以替换为自定义的函数;

  • model_data_cb() 函数是 Model 层到 APP 层业务逻辑处理的回调函数,APP 需要根据应用场景执行相应的动作。 只有使用默认的 model_receive() 函数才会封装该回调给 APP,如果用户自定义了 model_receive() 函数,则该函数可以忽略;

  • model_send_cb() 函数是 Model 层消息发送结果的回调函数,用户可以根据需求来决定是否需要实现;

typedef struct _mesh_model_info_t
{
   /** provided by application */
   uint32_t model_id; //!< being equal or greater than 0xffff0000 means that the model is a SIG model.
   /** callback to receive related access msg
      If the model don't recognize the access opcode, it should return false! */
   model_receive_pf model_receive;
   model_send_cb_pf model_send_cb; //!< indicates the msg transmitted state
   model_pub_cb_pf model_pub_cb; //!< indicates it is time to publishing
   model_data_cb_pf model_data_cb;
#if MESH_MODEL_ENABLE_DEINIT
   model_deinit_cb_pf model_deinit;
#endif
   /** point to the bound model, sharing the subscription list with the binding model */
   struct _mesh_model_info_t *pmodel_bound;
   struct
   {
      uint8_t corresponding_present : 1;
      uint8_t format : 1;
      uint8_t extended_item_count : 6;
      uint8_t corresponding_group_id;
      struct _mesh_model_info_t *pmodel_extended_info;
   };
   /** configured by stack */
   uint8_t element_index;
   uint8_t model_index;
   void *pelement;
   void *pmodel;
   void *pargs;
} mesh_model_info_t;

消息定义

在每个具体 Model 的头文件中都会有如下所示的预定义业务消息,每个消息对应一个具体的消息结构,以 Generic_OnOff model 为例。

#define GENERIC_ON_OFF_SERVER_GET                                        0 //!< @ref generic_on_off_server_get_t
#define GENERIC_ON_OFF_SERVER_GET_DEFAULT_TRANSITION_TIME                1 //!< @ref generic_on_off_server_get_default_transition_time_t
#define GENERIC_ON_OFF_SERVER_SET                                        2 //!< @ref generic_on_off_server_set_t

typedef struct
{
   generic_on_off_t on_off;
} generic_on_off_server_get_t;

typedef struct
{
   generic_transition_time_t trans_time;
} generic_on_off_server_get_default_transition_time_t;

typedef struct
{
   generic_on_off_t on_off;
   generic_transition_time_t total_time;
   generic_transition_time_t remaining_time;
} generic_on_off_server_set_t;

消息发送处理

Mesh 消息发送需要明确的参数有地址、应用密钥和 TTL。地址包含源地址和目的地址,分别标记消息的发送者和接收者。 源地址只能是发送者的单播地址,是 Provisioner 分配的固定值,由 stack 自动配置。 而目的地址则可以是除 Unassigned address 外任意地址类型,可以是单播地址,也可以是组播地址。

Mesh 消息参数的设置,需要根据消息类型来区分对待。

  • 当消息是针对请求消息的应答时,目的地址即原请求消息的源地址,使用相同的 AppKey。此种被动消息的目的地址等参数设置也是被动的。

  • 当消息是 mesh 设备主动发出的,此类动作被称作 publish。 这些参数通常是 Provisioner 利用 Configuration Model 中 Publication Set 消息灵活配置的,stack 也会自动处理的,不需要 APP 干预。 例如:一个开关的开关灯消息发送给哪盏灯,是可动态配置的。

如果 Provisioner 没有配置 model 的 publish 参数,model 发送消息所需参数如何设置也分情况。

  1. Provisioner 的 Publish 行为和参数应由用户指定。例如 Provisioner 对不同的设备进行配置。

  2. Model 发送消息比较灵活,不希望受限于 Publish 参数,此时可以由用户自由指定。例如 ping control model。

  3. Model 自主设定 Publish 参数。例如预先固化协商好的目的地址,地址可以是单播地址、组播地址、虚拟地址,甚至 0xFFFF。

Publish 参数配置

用户可以通过 mesh_model_pub_check() 来判断该 model 是否被 Provisioner 设置了相关的 publish 参数。

Node 收到 Provisioner 对 Publish 参数的设置之后都会由 stack 处理,并保存到 Flash, 用户可以通过 access_cfg() 提取 default configuration 和 publish 等参数,为发送消息预设参数。

如果用户不想使用预设的 publish 参数,可以在调用 access_cfg() 之后,覆盖掉相关参数。

static mesh_msg_send_cause_t ping_control_send(uint16_t dst, uint8_t ttl, uint8_t *pmsg,
                                               uint16_t msg_len, uint8_t app_key_index)
{
   mesh_msg_t mesh_msg;
   mesh_msg.pmodel_info = &ping_control;
   access_cfg(&mesh_msg);
   mesh_msg.pbuffer = pmsg;
   mesh_msg.msg_len = msg_len;
   mesh_msg.dst = dst;  // rewrite dst
   mesh_msg.ttl = ttl;  // rewrite ttl
   mesh_msg.app_key_index = app_key_index;   // rewrite app key index
   return access_send(&mesh_msg);
}
AppKey 选择

设备可以同时拥有多个 AppKey,实际数量取决于 Provisioner 对 Device 的配置。

AppKey会在调用 access_cfg() 时,被配置成 publish AppKey。如果 publish AppKey 没被配置,需要 APP 自主选择一个已经绑定的 AppKey。

警告

Model 不允许使用未绑定的 AppKey 发送消息。

每个 AppKey 都有一个唯一的 global index,但实现上被映射成了 local index。APP 发送消息时,使用的是 AppKey 本地索引 local index。

  • 如果 model 绑定了多个 AppKey,使用其中任意一个 AppKey 发送消息时, 可以调用 mesh_model_get_available_key() 获取一个可用的 local index。

  • 如果 model 需要使用特定的 AppKey,例如应用指定了 AppKey 的 global index, 此时可以通过 app_key_index_from_global() 根据 AppKey 的 global index,获取 local index。

TTL 选择

TTL 会在调用 access_cfg() 时,被配置成 publish TTL。 如果 publish TTL 没被配置,会被配成设备 default TTL (默认设置为 7)。 APP 可以在调用 access_cfg() 后修改 TTL 参数。

消息接收处理

本节主要介绍节点接收到消息之后,APP 如何获取和处理消息。

业务逻辑处理

大部分 Model 都已经实现功能封装,APP 只需要向 Model 注册业务逻辑处理的回调函数。 在 Model 层,消息接收时的消息解析和消息应答都已经被处理好,当需要 APP 层参与业务逻辑处理时,Model 层会通过注册的回调通知 APP。

如下所示是一个简单的开关灯的业务逻辑的处理,先定义好 Model 的结构并给 model_data_cb() 函数指定具体的处理函数, 当远端发送开灯或者关灯的消息时,SDK 会调用到 generic_on_off_server_data() 函数, 并根据消息类型填上对应的信息,APP 需要在具体的消息下面取到开关灯的值之后对灯执行相应的动作, 不用关心 Model 层和 Access 层之间的消息处理。

/** generic on/off server model */
static mesh_model_info_t generic_on_off_server;

/** light on/off state */
generic_on_off_t current_on_off = GENERIC_OFF;

/** light on/off process callback */
static int32_t generic_on_off_server_data(const mesh_model_info_p pmodel_info,
                                          uint32_t type, void *pargs)
{
   int32_t ret = MODEL_SUCCESS;
   switch (type)
   {
   case GENERIC_ON_OFF_SERVER_GET:
      {
         generic_on_off_server_get_t *pdata = pargs;
         pdata->on_off = current_on_off;
      }
      break;
   case GENERIC_ON_OFF_SERVER_SET:
      {
         generic_on_off_server_set_t *pdata = pargs;
         current_on_off = pdata->on_off;
         ret = MODEL_STOP_TRANSITION;
      }
      break;
   default:
      break;
   }
   return ret;
}

/** light on/off model initialize */
void light_on_off_server_models_init(void)
{
   /* register light on/off models */
   generic_on_off_server.model_data_cb = generic_on_off_server_data;
   generic_on_off_server_reg(0, &generic_on_off_server);
}
Configuration Client 源地址获取

设备在配网的时候,通常 Provisioner 会分发密钥、绑定 model、设置订阅地址等等。 此时 provisioner 是用 configuration client model 发消息给 configuration server model,这些消息的源地址即 Provisioner 的地址。

由于 configuration 过程涉及到许多网络参数操作,configuration server 对用户不可见。 用户想知道 configuration client 在配置 configuration server 时做了哪些操作以及获取 configuration client 地址,可以通过下述方法。

在第一个 element 创建后,向 configuration server 注册新的 callback cfg_server_receive_peek(),参考代码如下。

bool cfg_server_receive_peek(mesh_msg_p pmesh_msg)
{
   bool ret = cfg_server_receive(pmesh_msg);
   if(ret)
   {
      /* The configuration client message can be peeked now! */
      printi("cfg_server_receive_peek: cfg client addr = 0x%04x", pmesh_msg->src);
   }
   return ret;
}

......

void mesh_stack_init(void)
{
   ......
   /** create elements and register models */
   mesh_element_create(GATT_NS_DESC_UNKNOWN);
   cfg_server.model_receive = cfg_server_receive_peek;
   ......
}

如果设备端需要将 configuration client 地址记录下来,可以将其存到 Flash 中。如果地址未发生变化,则不需要重复存储。

Friendship

为了支持低功耗设备,需要通过 friendship 方式,让 friend node 帮助 low power node 缓存消息。 low power node 通常是通过调整 scan 占空比来降低功耗。

friendship 会额外消耗内部 NetKey 空间。friend node 可以设置和建立的最大 friendship 个数小于 NetKey 个数。 low power node 在一个 NetKey 上只能建立一个 friendship,所以 friendship 个数小于等于总 NetKey 个数的一半。

Friend Node

如果要使用 friend node feature,需要先调用 fn_init() 初始化。

Low Power Node

如果要使用 low power node feature,需要先调用 lpn_init() 初始化。发起 friendship 建立流程可以调用 lpn_req()

Transport Ping

为了方便测试网络层通信,在 transport layer 自定义了 ping / pong 机制 trans_ping(),消息里面携带 TTL 信息用于计算 hop 次数。 该消息直接利用 network layer 的服务,只会被 NetKey 加密。 Device 被 Provisioning 后,就可以使用 transport layer 的 ping 功能测试 network layer 是否正常运行,此时 Device 还没有获得 AppKey。

Remote Provisioning

当 Provisioner 和 Unprovisioned device 的距离较远,超过直接传输数据的无线电范围时, 可以使用 Remote Provisioning 功能,利用现有的 mesh 网络传输 Provisioning PDUs,将 Unprovisioned device 添加到网络。 另外,Provisioner 可以通过 Remote Provisioning 功能来改变 Provisioned device 的 Device key、Node address 或者 Composition data。

Directed Forwarding

Directed forwarding 通过选择节点子集将消息从源节点中继到目标节点,来帮助提高 multi-hop 网络的性能。 该子集(包括源节点和目标节点)构成了定向转发所定义的路径。 路径由源地址和目标地址标识,目标地址可以是单播地址、组播地址或虚拟地址。

Directed forwarding 会额外消耗内部 NetKey 空间,且一个 Master key 会衍生出一个 directed forwarding key, 一个 directed forwarding key 上可以建立多个 directed forwarding path。

Directed forwarding path 分为 non-fixed path 和 fixed path。

  • Non-fixed path 由 directed forwarding node 动态建立,生命周期由 directed forwarding client 控制,且该路径不会存储到 Flash。

  • Fixed path 由 directed forwarding client 通过 directed forwarding model 添加,该路径一直存在,直到被 directed forwarding client 删除,且会存到 Flash。

Directed forwarding path 建立完成之后,源地址可以使用 directed forwarding key 加密发送消息,只有该路径上的节点才会转发这条消息。

  • 用户可以调用 df_cb_reg() 注册 directed forwarding callback 来获取 directed forwarding path discovery、establish、release 等相应的动作。

  • 用户可以调用 df_path_discovery() 发起 path discovery 流程,这里指的是 non-fixed path。 在很多场景中,path discovery 流程不需要用户主动发起。 例如,将某个 model 的 publish policy 设置为 directed forwarding, 发送消息时(TTL 大于等于 2)会自动触发 path discovery 流程。

  • 用户可以调用 df_path_dependents_update() 发起 path dependents update 流程。 在很多场景中,path dependents update 流程不需要用户主动发起。 例如,dependent node 状态变化时,supporting node 可能会自动触发 path dependents update 流程。

  • 用户可以调用 df_path_solicitation() 发起 path solicitation 流程。 在很多场景中,path solicitation 流程不需要用户主动发起。 例如,low power node 的订阅地址列表变化时,friend node 可能会自动触发 path solicitation 流程。

Subnet Bridge

属于多个子网的节点可以支持 subnet bridge 功能。 支持 subnet bridge 功能的节点经过配置之后,可以将消息从某个子网重传到其他子网。

Binary Large Object Transfer

BLOB Transfer Model 能传输大于最大 Access Layer PDUs 大小的数据对象,并且能同时多播到多个节点。

BLOB 接收方(BLOB transfer server),在传输之前需要调用 blob_transfer_server_init() 进行初始化。

BLOB 发送方(BLOB transfer client),可以使用model层的接口; 也可以使用对 client 行为已经封装好的 API blob_client_app.h, 其中包含 capabilities retrieve 流程 blob_client_caps_retrieve()、 blob transfer 流程 blob_client_blob_transfer()、 transfer cancel 流程 blob_client_transfer_cancel()

Device Firmware Update

请参考 Bluetooth Mesh OTA。

常见问题

列举了一些在 Mesh Stack 使用中常见的问题以及注意事项。

节点无法接收消息

可以通过注册 rpl_cb_reg() 确认消息是否被 RPL 机制过滤, RPL 的数量会在 stack 初始化阶段确定,该值的大小应该考虑整个网络中会与其通信的节点数量,并留有一定余量便于以后网络的扩展。

void mesh_stack_init(void)
{
   ......
   mesh_node_cfg_t node_cfg =
   {
      .dev_key_num = 2,
      .net_key_num = 6,
      .master_key_num = 3,
      .app_key_num = 3,
      .vir_addr_num = 3,
      .rpl_num = 20,
      .sub_addr_num = 10,
      .proxy_num = 1,
   };
   mesh_node_cfg(features, &node_cfg);
   ......
}

重复利用已删除节点的地址

Mesh 网络有 RPL 的机制,会将收到的消息的源地址和 Sequence 存储起来,防止重放攻击,且每次收到消息都会更新一次。 如果新的节点重复使用相同的地址,且网络中的节点 RPL list 已经存储过该节点的 Sequence 信息,此时新节点和旧节点通信会有异常。 实际上,已经被删除的地址不可以直接被重复利用,如果想要使用可以参考 Mesh Protocol Specification 的 3.11.7 小节 Node Removal procedure

示例工程

参考资料

[1] Mesh Protocol Specification

[2] Mesh Model Specification