本文翻译摘取自Nordic官网关于Mesh SDK的章节,鉴于作者本人水平有限,翻译如有纰漏,烦请提出指正,感谢阅读!
点击这里,下载本文pdf文档
Bluetooth Mesh是Bluetooth SIG开发和发布的配置文件规范。 本文档介绍了蓝牙网状网络的基本概念,并概述了配置文件的操作和功能,并解释了网状网络设备的生命周期。 有关Nordic半导体实施蓝牙网格的更多具体信息,请参阅网格体系结构文档。
1.1 与低功耗蓝牙的关系
蓝牙网格基于蓝牙4.0规范中的蓝牙低功耗部分,并与该协议共享最低层。 由于网状信息包含在蓝牙低能量广播信息包的有效载荷内,所以蓝牙网状物理表示与现有的蓝牙低功耗设备兼容。 然而,蓝牙网格指定了一个全新的主机层,尽管一些概念是共享的,但是蓝牙网格与蓝牙低功耗主机层不兼容。
1.2 应用领域
蓝牙Mesh主要针对简单的控制和监控应用,如光控或传感器数据采集。 数据包格式针对小型控制数据包进行了优化,发出单个命令或报告,不适用于数据流或其他高带宽应用。与传统的蓝牙低功耗应用相比,使用蓝牙网卡会导致更高的功耗。 这主要是由于需要保持无线电持续运行。 因此,与蓝牙低功耗广告主不同的是,主动网状设备不能长时间使用纽扣电池。蓝牙网状网支持多达32767个网络设备,最大网络直径为126跳。
1.3 网络拓扑和中继
蓝牙Mesh是基于广播的网络协议,网络中的每个设备都发送和接收来自无线电通信范围内的所有设备的所有消息。 在网状网络中没有连接的概念。 网络中的任何设备都可以中继来自任何其他设备的消息,这使得网状设备能够通过使一个或多个其他设备将消息中继到目的地而将消息发送到无线电范围之外的设备。 此属性还允许设备随时移动并进出网络。
1.4 网格运输
蓝牙Mesh利用蓝牙低功耗广告商和扫描器角色,通过蓝牙低功耗广告数据包进行通信。 广告数据包由附近的网状设备拾取并像其他蓝牙低功耗广告数据包一样处理。 网格数据包以唯一的AD类型表示,并添加到广告数据包有效负载中。蓝牙低功耗设备每隔一段时间发送一次广告报文,网格包也不例外。 但是,与传统的广告主不同,网状设备将在每个传输中改变其广告载荷,在堆叠中排队时传播新的网状分组。 每个蓝牙网状网络的广告只会针对每个设备传输一次,如果网状网络中没有流量,设备将保持沉默状态。
1.5 中继
蓝牙网格通过中继消息来扩展网络的范围。 任何网格设备都可以配置为中继,不需要专门的中继设备来构建网络。 作为中继的每个设备都会减少接收到的消息中的生存时间(TTL)值,并在TTL为2或更高时将其转发。 这种无向中继被称为消息泛滥,并确保消息传递的高可能性,而不需要关于网络拓扑的任何信息。 Mesh Profile Specification没有提供任何路由机制,所有的消息都被所有的中继转发,直到TTL值达到零。 为了避免消息被同一个中继反复转发,所有的mesh设备都维护一个消息缓存。 该缓存用于过滤设备已处理的数据包。基于洪泛的消息中继方法会在空中造成大量的冗余流量,这可能会影响网络的吞吐量和可靠性。 因此,强烈建议限制网络中继电器的数量来限制这种影响。 网络中启用中继的设备的速率是消息路由冗余和可靠性之间的折衷。 应根据网络密度,流量,网络布局以及对可靠性和响应性的要求进行调整。
1.6 能量消耗
为了实现基于广播的通信,设备必须持续保持其无线电收听模式,从而导致比典型的蓝牙低功耗设备更高的功耗。 为了使低功耗设备能够参与到网状网络中,蓝牙网状网包含了低功耗的友好功能。 该协议允许低功率设备与常规网状设备建立关系,然后该设备将定期缓存并将消息转发给低功率设备。 这使低功率设备不必继续收听收到的消息。
1.7 GATT代理
为了让不支持接收网格分组的传统蓝牙低功耗设备加入网格网络,蓝牙网格定义了一个单独的协议,用于通过蓝牙低功耗GATT协议来隧穿网格消息。 为此,Mesh Profile Specification定义了一个GATT承载和相应的GATT代理协议。 该协议允许传统的蓝牙低功耗设备通过与具有代理功能的网状网络设备建立GATT连接来加入网状网络。传统设备被分配一个地址和必要的密钥,成为网络的正式成员。 设备通过常规配置程序或通过某些带外机制接收安全证书。
1.8 地址关系
蓝牙网格寻址方案不同于蓝牙低能量寻址方案。 它具有三种类型的地址:
单播地址:每个设备都是唯一的
组地址:允许形成一组设备并一次处理所有设备
虚拟地址:具有大地址空间的未跟踪的基于UUID的地址
当一个设备被添加到一个网络时,它被分配了一系列代表它的单播地址。 设备的单播地址不能改变,始终是连续的。 单播地址空间支持在单个网状网络中具有32767个单播地址。 单播地址可以被任何应用使用来直接发送消息给设备。
组地址作为网络配置过程的一部分进行分配和分配。 组地址可以表示任何数量的设备,并且设备可以是任何数量的组的一部分。 网状网络中最多可以有16127个通用组地址。
虚拟地址可以被认为是组地址的一种特殊形式,可以用来表示任何数量的设备。 每个虚拟地址是从文本标签生成的128位UUID。 虚拟地址不必由网络配置设备跟踪,这样,用户可以在部署之前生成虚拟地址,或者可以在网络中的设备之间临时生成地址。
1.9 供给者(provisioning)
在设备可以参与正常的网状操作之前,必须预先配置。 在配置期间,设备将被添加到网络,并分配有单播地址,网络密钥和设备密钥。 配置由Provisioner完成,Provisioner是一个受信任的设备,可以访问网络中的设备的完整列表及其地址。 新设备配置完成后,配置人员需要使用新设备的设备密钥来建立一个安全通道来配置它。
1.10 模型和元素
为了标准化来自不同厂商的设备之间的通信,Mesh Profile Specification定义了一个接入层,在设备的不同模型之间路由网状消息。 模型表示特定的行为或服务,并定义一组状态和消息,这些状态和消息作用于这些状态。 Mesh Profile Specification和Mesh Model Specification定义了一组模型,以涵盖典型的使用场景,如设备配置,传感器读数和灯光控制。 除此之外,供应商可以自由地定义与自己的消息和状态相关的模型。设备中的模型属于元素。 每个设备都有一个或多个元素,每个元素在网格中充当虚拟实体,具有自己唯一的单播地址。 每个传入消息由元素中的模型实例处理。 为了能够唯一地解析消息的处理方式,每个元素只有一个模型实例可以实现特定消息操作码的处理程序。 如果设备具有相同型号的多个实例,则必须将每个实例分配给一个单独的元素。 同样,如果两个模型为相同的消息实现处理程序,则这些模型必须位于不同的元素中。为了用最小的消息和状态重复表示复杂的行为,模型可以由其他模型组成,可能跨越多个元素。 这些模型被称为扩展模型。 完全独立的模型被称为根模型。模型通过发布和订阅系统彼此交谈。 每个模型可以订阅一组组和虚拟地址,模型将只处理发布到其订阅地址或包含元素的单播地址之一的消息。 任何模型都可以维护发布消息的发布地址。 这个发布地址可以是任何类型的。
1.11 配置网络
蓝牙网格将网络配置留给中央网络配置器。 设备预计不会自行进行任何类型的服务发现。 为了控制其他设备,像照明开关这样的设备必须由provisioner配置,或者通过用户交互或者从数据库加载预定的配置。 每个设备必须在其第一个元素中实现一个强制的Configuration Server模型,该模型用于配置模型的其余部分。配置完成后,配给器使用配置客户端模型的实例为新设备提供一组应用程序密钥和地址。 设备将在网络上使用这些密钥和地址,除非它被重新配置。
1.12 示例场景:灯控开关
配置新的灯开关后,配置器中的Configuration Client模型会读出新设备的模型和元素列表,并将其呈现给用户。 用户在设备的型号列表中找到灯开关型号,并给它“灯光控制”应用程序键。 接下来,用户将模型的发布地址设置为厨房中的所有灯泡订阅的“厨房区域”组地址。 下一次按下新的电灯开关时,厨房内的所有灯泡都会亮起。
1.13 安全
蓝牙网状网采用多种安全措施来防止第三方干扰和监测。
1.14 认证
设备身份验证是供应过程的一部分,并允许用户确认添加到网络的设备确实是他们认为是的设备。 Mesh配置文件规范定义了一系列带外验证方法,例如灯的闪烁,输出和输入密码,以及针对预共享密钥的静态验证。 为了确保供应程序,使用椭圆曲线Diffie-Helman(ECDH)公钥密码术。 设备配置完成后,它将成为网络的一部分,并将其所有消息视为已认证。
1.15 消息加密
蓝牙网状网具有两级AES-CCM加密功能,128位密钥用于网络上的所有消息。
最底层的网络加密保护网状网络中的所有消息不被网络中的设备读取。 加密是通过网络加密密钥完成的,任何网络最多可以由4096个不同的子网组成,每个子网都有自己的网络密钥。 所有共享网络密钥的设备都被视为网络的一部分,并可能通过它发送和中继消息。 通过使用多个网络密钥,网络管理员可以将其网络有效地划分为多个子网,因为网状网中继仅转发使用已知网络密钥加密的消息。
第二个加密层是传输加密。 此加密层通过使用应用程序或设备密钥加密应用程序负载来限制哪些设备可以在网络中执行什么操作。 作为一个例子,考虑在酒店中部署的网状网络,其中希望限制由员工控制的某些功能(例如配置钥匙卡或访问存储区域)以及一些可供客人使用的功能(例如控制 室内照明或空调)。 为此,我们可以为客人提供一个应用程序密钥,为员工提供一个应用程序密钥,从而允许通过同一网络中继消息,同时防止客人和员工读取彼此的消息。
应用程序密钥用于分隔网络中不同应用程序的访问权限,而设备密钥则用于管理网络中的设备。 每个设备都有唯一的设备密钥,只有配置器和设备本身才知道。 使用新加密密钥(网络或应用程序密钥)或地址配置设备时,除了设置其他设备特定参数外,还使用设备密钥。 通过将新密钥传输到网络中的所有其他设备(传输密钥时使用它们各自的设备密钥),还可以用来从网络中驱逐恶意设备。 这个过程被称为密钥刷新过程。
每个加密层都包含一个消息完整性检查值,用于验证消息内容是否使用了指定的加密密钥进行加密。
1.16 隐私
所有的Mesh消息有效载荷都被完全加密。 源地址和消息序列号等消息元数据使用从网络密钥导出的隐私密钥进行模糊处理,即使对于公用头字段也提供有限的隐私。
1.17 重播保护
为防止重放以前消息的恶意设备,每台设备都保留一个正在运行的序列号,用于出站消息。 每个Mesh消息都用一对唯一的序列号和源地址发送。 接收设备接收到一条消息时,会存储该序列号,并确保它比来自同一个源地址的最后一个序列号更新。
网格堆栈由许多通过一组API模块接口的子系统组成。 API模块隐藏了子系统的复杂性。 API中提供的功能足以构成一个功能正常的网状设备,因此不需要绕过API。网格堆栈的结构基于蓝牙网格规范的结构,并遵循相同的命名约定。 有关蓝牙网格的介绍,请参阅基本的蓝牙mesh概念。
概览:
模型:蓝牙网格模型呈现并实现设备行为。
访问:蓝牙网状接入层组织模型和通信。
DSM:设备状态管理器存储在模型中使用的地址和加密密钥。
核心:核心蓝牙网格层负责加密和消息中继。
配置:蓝牙网状配置协议用于将设备添加到网络。
承载:承载层负责低层无线电操作。
DFU:设备固件升级模块与引导加载程序配合,通过网格启用固件升级。
节点配置(未显示):节点配置是管理设备配置和设置的实用程序模块。
串行(未显示):Mesh API的应用程序级序列化允许网格由单独的主机设备控制
2.1 模型(Models)
这些模型定义了通过网格传输的所有数据的行为和通信格式。 相当于蓝牙低功耗的GATT服务,网格模型是独立的,不可变的特定行为或服务的实现。 所有的网格通信都是通过模型来实现的,任何通过网格公开其行为的应用程序都必须通过一个或多个模型来引导通信。蓝牙网格规范为典型的使用场景定义了一组不变的模型,但是厂商也可以自由地实现自己的模型。您可以阅读更多关于如何在创建自定义模型中实现自己的模型。
2.2 访问(Access)
API: access.h
访问层控制设备的模型组成。 它包含对设备上存在的模型的引用,这些模型接受的消息以及这些模型的配置。 当设备接收到网状消息时,接入层会查找消息所用的模型并将其转发给模型实现。
2.3 设备状态管理器(Device State Manager DSM)
API: device_state_manager.h
设备状态管理器存储网状堆栈使用的加密密钥和地址。 当通过配置为模型分配应用程序密钥并发布地址时,设备状态管理器将存储原始值并提供模型在引用这些值时使用的句柄。设备状态管理器将其数据存储在永久性存储器中,可以在启动时恢复。
2.4 网状网络核心(Mesh Core)
API: nrf_mesh.h
Mesh Core模块由网络和传输层组成,为消息提供网格特定的传输。传输层通过使用应用密钥对网格数据包进行加密并将它们分成可以传播的更小段来提供网络内的安全性。 传输层重新汇编传入的数据包段,并将全网格消息呈现给接入层。网络层使用网络密钥对每个传输层数据包段进行加密,并填充源地址字段和目标地址字段。 当收到一个Mesh包时,网络层对这个消息进行解密,检查源地址和目的地址,并决定这个包是否打算发给这个设备,以及网络层是否应该中继它。Mesh Core通过双层加密,重放保护和数据包头混淆提供针对恶意行为和网状网络攻击的保护。
2.5 供给(Provisioning)
API: nrf_mesh_prov.h
配置是将设备添加到网状网络的行为。 供应模块通过实现供应者角色(网络所有者provisioner)和被供应者角色(要添加的设备provisionee)来处理此过程的两个方面。要参与网状通信,必须调配每个设备。通过配置过程,新设备接收一系列地址,一个网络密钥和一个设备密钥。Mesh堆栈提供了两种配置设备的方法:直接通过PB-ADV配置承载或通过远程配置。 PB-ADV供应只能在供应者和设备之间互相无线电范围内进行,而远程供应实施两个网格模型,通过网格一起创建隧道,在PB-ADV代理设备的帮助下,允许供应商从远处添加设备。
重要提示:远程配置是北欧专有功能,不能与其他供应商的设备一起使用。远程供应客户端和服务器示例演示了远程供应。 光开关示例显示PB-ADV的提供者和被供者端作为建立网络的第一步。有关如何使用资源调配的详细指南,请参阅网格资源调配。
2.6 承载(Bearer)
承载是低层无线电控制器,为上层的无线数据包发送和接收提供异步接口。 它通过SoftDevice Timeslot API在数据包格式和定时上强制执行蓝牙低能量标准,并直接在无线电硬件上运行。承载是通常不需要由应用程序访问的内部模块。
2.7 设备固件升级(Device Firmware Upgrade DFU)
API: nrf_mesh_dfu.h
设备固件升级模块通过与引导加载程序合作提供网格上的固件更新功能。 它能够并发,认证的固件传输到网络中的所有设备,而不会中断应用程序。DFU程序和协议与nRF OpenMesh共享。 它与nRF5 SDK中使用的蓝牙低功耗安全DFU过程不兼容。
重要说明:网状DFU是Nordic专有的功能,不能与其他供应商的设备一起使用。有关DFU过程的更多信息,请访问nRF OpenMesh存储库或查看快速入门指南。
2.8 节点配置(Node Config)
API: nrf_mesh_node_config.h
节点配置模块是围绕顶层网格模块的薄包装,使得网格易于使用。 它负责网格初始化并启动供应者角色,请求添加到网络中。 节点配置模块是启动并运行新网格设备的最简单的方法,但是它隐藏了网格设备启动的一些更为复杂的功能和用例。
2.9 串行(Serial)
API: nrf_mesh_node_serial.h
串口模块提供了完整的网格API序列,允许其他设备通过UART接口来控制nRF5网格设备。 为了适应网络网关和类似的复杂应用,串行接口提供了一种通过北欧设备访问网格的方法,而不必将其作为单元的主控制器。网状串行接口基于nRF8001 ACI串行接口,并可选择支持SLIP编码操作。 串行协议可以作为独立的应用程序运行(参见串行示例),也可以作为普通的网格应用程序运行。串行数据包格式,命令和事件的概述可以在串行文档中找到。
3.1 介绍
provisoning用于为新设备提供加入网络所需的信息。 在供应过程中,为新设备提供网络密钥,地址和设备密钥,该密钥是仅用于供应者和被供应者之间的私人通信的特殊密钥(例如,当设备在provison后被configure)。设备可以是提供者provisioner或被提供者provisionee。
3.2 使用供给者的API
供应API提供用于设置供应者的功能和用于设置被供应者的功能。 如果您的设备不使用它,您可以排除其中一个角色的代码。 为此,请仅链接您希望设备支持的角色的代码。如果调用的函数不受支持,因为该角色的功能尚未编译到库中,则会返回NRF_ERROR_NOT_SUPPORTED错误。
3.3 供应过程
下图说明了配置过程,添加了所有函数调用,消息和事件。
3.4 初始化
初始化步骤对于提供者和被提供者角色都是通用的。 在这两种情况下,都必须设置配置上下文。 上下文维护供应过程的状态。
配置状态使用nrf_mesh_prov_init()函数进行初始化。 除了配置上下文之外,该功能还需要3个参数。 它用于设置供应上下文的初始状态:
公钥和私钥:用于加密的密钥对。 这些键可以预编程或使用nrf_mesh_prov_generate_keys()函数生成。 请注意,这些数字必须是特定椭圆曲线上的点,这意味着规则随机数不能用作密钥对。
OOB身份验证功能:用于确定可以与节点一起使用哪种身份验证。 如果不使用认证,则只需要设置算法字段。
请注意,在使用配置堆栈之前,必须初始化并启用SoftDevice和Mesh堆栈。 您还必须初始化接入层模块,以便在配置过程完成后配置设备。 这可以通过调用设备状态管理器(DSM)和访问层的初始化函数来完成:
dsm_init();
access_init();
3.5 认证
供应程序为带外(OOB)认证提供了几种备选方案,用于验证所供应的设备确实是预期的设备。
静态身份验证是OOB身份验证的最简单形式。 提供者和提供者都具有在提供过程中被验证的静态预编程密钥。 如果两个设备中的静态密钥匹配,则认证成功。输入/输出认证可用于具有输入/输出功能的设备。 在配置过程中,其中一个设备是输入设备,另一个是输出设备。 输出设备显示代码(例如,在数字显示屏上或通过闪烁灯光),并且用户必须将所指示的值输入到输入设备中(例如通过按下所需次数的按钮或通过输入值 键盘)。
供应商选择使用哪种形式的认证以响应NRF_MESH_PROV_CAPS_RECEIVED事件。
如果选择静态身份验证,那么应用程序将在供应过程中的某个时间点接收NRF_MESH_PROV_STATIC_REQUEST事件。 应用程序应通过使用静态密钥数据调用nrf_mesh_prov_auth_data_provide()来响应此事件。
如果选择输入/输出身份验证,输入设备将收到一个NRF_MESH_PROV_INPUT_REQUEST事件。 然后应用程序应该请求来自用户的输入,并通过调用nrf_mesh_prov_auth_data_provide()将此输入提供给供应堆栈。 输出设备将收到一个NRF_MESH_PROV_OUTPUT_REQUEST事件,然后应该向用户显示提供的数据。
3.6 被提供者
Provisionee行为通常由nrf_mesh_node_config()API函数处理。 但是,如有必要,也可以使用配置API手动处理配置。
以下示例假定使用静态身份验证。 供应商申请的步骤如下:
(1)如“初始化”一节中所述初始化配置上下文。
(2)使用nrf_mesh_prov_listen()函数开始监听传入的供应链接请求。 该功能使用提供的承载类型作为基础传输来启动未提供的节点广播信标。 广告可以可选地包括指向附加数据的位置的URI和指定可以找到OOB认证数据的位置的字段。 输入链接请求将被设备自动接受。
(3)链接建立后,NRF_MESH_EVT_PROV_LINK_ESTABLISHED事件被传递给应用程序。
(4)如果启用了静态身份验证,则会将NRF_MESH_EVT_PROV_STATIC_REQUEST事件传递给应用程序。 应用程序应通过将nrf_mesh_prov_auth_data_provide()函数传递给堆栈来响应静态身份验证数据。 请注意,静态验证数据总是16个字节长。
(5)在收到NRF_MESH_EVT_PROV_COMPLETE事件时,可以认为配置成功。 此事件为设备提供配置数据和设备密钥。 接收到的供应数据必须输入到设备状态管理器以供接入层模块使用。 此时,设备可以通过配置模型从配置器接收进一步的配置。 只要配置模型服务器已初始化,配置就会在后台自动进行(请参阅Light开关示例以了解如何执行此操作的示例)。
3.7 提供者
提供者是网状节点,负责网络中其他节点的配置。 通常,自动配置器包含配置客户端和客户端节点,用于控制其他节点(如灯光或空调)中的特定功能。 提供者通常是网关设备的一部分,网关设备是在网状网络和其他网络技术(例如因特网)之间提供桥接的设备。
有两种主要的设置方法:将其作为独立应用程序运行,或通过串行接口由主机应用程序控制。
独立预配置提供配置功能,而不依赖于外部主机。 因此,独立供应商必须能够存储关于供应商能够配置节点所必需的关于供应节点在网络中的信息,包括其地址和设备密钥。 由于嵌入式处理器的可用内存量有限,因此使用独立配置程序会限制可配置节点的数量,从而限制网状网络的最大大小。
串行预配置器使用串行接口进行配置,使主机控制器可以使用外部微控制器作为网状“调制解调器”与网状网络进行交互。 在这种情况下,主机控制器存储有关网络上节点的信息,释放外部微控制器中的RAM以用于其他应用特定用途。 在这种情况下,网状网络的大小仅受主机中可用资源的限制。
3.8 独立提供者
独立供应商应用程序必须采取的配置设备步骤如下:
(1)如“初始化”一节中所述初始化配置上下文。
(2)等待未提供的节点广播信标。 当收到信标时,应用程序会收到一个NRF_MESH_EVT_UNPROVISIONED_RECEIVED事件,其中包含收到信标的设备的UUID。
(3)使用nrf_mesh_prov_provision()函数建立到供应的设备的链接。
(4)当建立到未设置设备的链接时,会将NRF_MESH_EVT_PROV_LINK_ESTABLISHED事件传递给应用程序。
(5)当收到供应商的带外认证功能时,会收到一个NRF_MESH_EVT_PROV_CAPS_RECEIVED事件。 应用程序应根据自己的功能检查供应商的功能,并决定采用何种身份验证。 使用nrf_mesh_prov_oob_use()函数选择要使用的身份验证方法。
(6)如果启用了静态身份验证,则会将NRF_MESH_EVT_PROV_STATIC_REQUEST事件传递给应用程序。 应用程序应该通过将nrf_mesh_prov_auth_data_provide()函数传递给堆栈来响应静态认证数据。
(7)当设备供应完成时,NRF_MESH_EVT_PROV_COMPLETE事件被传递给应用程序。 此时,配置器可以使用配置模型继续配置设备。 有关如何执行配置的更多信息,请参阅Light Switch客户端示例应用程序。
提供者流程图:
3.9 串行提供者
创建串行供应商应用程序的步骤如下所示:
(1)如“初始化”一节中所述初始化配置上下文。
(2)使用nrf_mesh_serial_init()和nrf_mesh_serial_enable()API函数初始化并启用串行接口。
然后可以使用“独立供应商”下描述的过程来供应设备,用串行命令和串行事件的事件代替API调用。
3.10 ECDH卸载
ECDH(Elliptic Curve Diffie-Hellman)是一种用于在两台设备之间安全地创建共享机密的密码算法。 它用于创建一个加密密钥,然后将其用于在供应数据传输到被提供者时保护供应数据。
如果并行运行多个置备程序,则应启用ECDH卸载。 ECDH是一个处理器密集型算法,很容易成为瓶颈。 ECDH卸载功能可让主机处理器计算ECDH共享密钥,释放目标处理器中的CPU资源。
在初始化设备时,可以通过运行以下代码来启用ECDH卸载:
nrf_mesh_opt_t value = {.len = 4,.opt.val = 1};
nrf_mesh_opt_set(NRF_MESH_OPT_PROV_ECDH_OFFLOADING,&value);
3.11 远程供应
远程供应(PB-Remote)允许供应商在其无线电范围之外供应设备。 这是通过使用网格节点将供应消息中继到要供应的设备的范围内的节点来完成的。
PB-Remote功能由PB-Remote客户端和服务器型号提供。 有关远程配置和远程配置模型的更多信息,请参阅PB-Remote手册。
3.12 错误
如果在配置过程中发生错误,则关闭该链接。 NRF_MESH_EVT_PROV_LINK_CLOSED事件被传递给应用程序。 如果在NRF_MESH_EVT_PROV_COMPLETE事件之前接收到NRF_MESH_EVT_PROV_LINK_CLOSED事件,则必须将供应过程视为失败。 NRF_MESH_EVT_PROV_LINK_CLOSED事件还包含一个close_reason参数,可用于确定导致设置失败的原因。
以下指南介绍了如何使用电灯开关示例作为参考来设置简单的蓝牙网状网络。 它将简要概述provision和配置,以及在示例中如何使用API。请参阅基本的蓝牙网格概念,以获得有关蓝牙网格概念的更详细的概述。
4.1 供应和配置
在设备加入Mesh网络之前,必须预先provisioned。 这个过程包括验证新设备并为其提供基本的网络信息,网络密钥,唯一的设备密钥和为其元素保留的单播地址空间。
provision完成后,必须配置设备。 配置服务器模型对于所有的蓝牙网格节点是强制性的,并且处理与配置客户机模型的通信和指令 - 实际上由配置器控制。 它始终分配在节点的根元素上,并使用设备的设备密钥加密与它的通信。 典型的配置步骤是:
(1)读取设备的组成数据。 这为设备提供了有关设备元数据的信息,以及哪些模型绑定了设备中的元素。
(2)添加应用程序和/或网络密钥。
(3)将密钥绑定到不同的模型。
(4)设置模型的发布状态(哪个地址发布状态事件,使用什么键,使用什么TTL值等)。
(5)管理订阅。
4.2 示例:简单的家庭照明
想象一个用一个灯开关和几个灯泡的用例。 被供应商(灯泡)将开始信标其未提供的信标,向供应商发信号表示正在寻找加入的网络。 供应商验证灯泡的灯标,并邀请它加入网络。 如果认证成功,设备被赋予必要的密钥和地址成为网络的一部分,并准备好进行配置。 接下来,灯泡被给予“家庭自动化”应用密钥,绑定到控制灯的OnOff服务器,OnOff服务器的发布状态被设置,并且最后添加对“灯组”的订阅。 该消息交换如下图所示(请注意,图中不包括确认信息):
每个加入网络的灯泡都会重复这个过程。
4.3 实际的例子
灯开关示例旨在展示供应商和被供应商角色的API以及如何配置简单的蓝牙网状网络。 该网络由一个组合的提供者和简单的OnOff客户端(灯开关)和三个被提供者与简单的OnOff服务器(灯泡)组成。
Provisionee:灯开关服务器
灯开关服务器主要有两个API接口:
(1)Configuration module
(2)Simple OnOff server model
配置模块实现简单的供应设备的行为。 它处理与配置堆栈的接口,设置配置服务器以及从闪存恢复设备状态。
如examples / light_switch / server / src / main.c所示,应用程序所需的代码量是最小的。 它实现了以下功能:
(1)设置基本配置参数,支持Out-Of-Bound(OOB)方法,时钟配置,回调等
(2)添加模型及其事件回调。
当调用configuration_complete()回调函数时,设备将被调配并准备好由调配程序进行配置。 下图说明了相关API调用的设置:
Provisioner:灯开关客户端
灯开关客户端与以下API接口:
(1)Core mesh stack
(2)Provisioning
(3)Configuration client
(4)Simple OnOff client
一般而言,在资源需求和应用程序复杂性方面,提供者角色比提供者角色更复杂一个数量级。 因此,没有简单的“新闻播放和工作” - 供应商的API。 但是,对于特定的使用情况,可以将其简化为一组简单的步骤,如轻型开关客户端示例中所示:
(1)初始化
a. Core mesh stack
b. Device state manager
c. .Access layer
d. (Optional) Load flash configuration.
(2)监听unprovisioned 的信标
(3)Provision 设备
(4)配置设备
(5)如果有更多的设备加入网络,请返回步骤2。
在这个例子中,行为被分割为examples / light_switch / client / src / main.c和examples / light_switch / client / src / provisioner.c,其中前者处理初始化和设置,用户界面等等。 提供和配置状态。 下图显示了如何使用提供的API实现配置和配置的详细信息。 请注意,该图可能会简化一些API调用以提供更清晰的理解。 有关详细信息,请参阅相关源文件。
在蓝牙网格解决方案中,模型用于定义节点的功能。 每个模型代表一组状态和行为,并定义用于与模型状态交互的消息。一个模型的例子是配置模型,它是所有网格设备中的强制模型。 此模型表示节点的配置(以各种状态的形式)并提供消息来设置或检索配置参数(行为)。本指南介绍了如何创建新模型的基础知识。 您可以实现您自己的特定于供应商的模型,使您的设备能够提供未定义的标准模型所涵盖的自定义状态和行为。
5.1 用网络堆栈实现模型
要实现一个模型,你必须采取以下基本步骤:
定义操作码处理程序:通过创建一个access_opcode_handler_t数组来定义传入消息的处理程序表。 此数组中的每个元素都充当查找表项,用于处理目标为此模型的传入消息的操作码。
分配模型并将其绑定到一个元素:使用access_model_add()API分配,初始化并将模型绑定到给定元素索引处的元素。 此模型实例由分配给输出参数p_model_handle的句柄值标识。 调用访问层API函数时使用此句柄。 所有模型必须绑定到一个元素。 元素表示设备中的可寻址单元,例如灯具中的灯泡。 因此,每个元素由供应者分配一个单独的单播地址。
请注意,模型可以扩展一个或多个其他模型。 这些父模型实例可以绑定到不同的元素,使整个模型跨越多个元素。 这些模型被称为扩展模型。 有关更多信息和示例,请参阅“蓝牙网状网络规范”的“网状模型规范”。
5.2 发布
从模型发送消息是通过发布完成的。 每个模型都有一个发布地址。 消息的发布可以是周期性的或一次性的,发布的消息可以发送到单播,群组或虚拟地址。 发布相关状态的配置通常由配置模型的配置者控制。 例如,发布对于允许传感器节点定期报告数据读数是有用的。 可以使用access_model_publish()API函数发布消息,该函数将根据模型的发布设置(间隔,目的地)发布消息。客户机模型也使用发布将消息发送到服务器模型。 然而,在许多情况下,应用程序想要控制从客户端模型发布的消息的目的地,而不是依赖于外部提供者(在许多情况下,包含客户端的应用程序是提供者)。 为此,提供了API函数access_model_publish_address_set()。
5.3 订阅
订阅允许模型侦听来自特定地址的传入消息。 这可以用来收听例如从传感器节点发布的周期性消息。 要允许模型订阅地址,首先需要使用access_model_subscription_list_alloc()API函数分配订阅列表。请注意,使用客户端模型时,不需要订阅您要发送消息的地址,以便接收对这些消息的回复。 订阅仅用于接收来自节点的未经请求的消息。
5.4 示例:供应商特定的Simple OnOff模型
本指南的以下部分将介绍如何实现供应商特定的Simple OnOff模型,该模型可用于开启或关闭某些东西(例如灯泡,加热器或洗衣机)。
重要提示:Mesh Model Specification指定了一个名为“Generic OnOff Model”的模型,该模型应该在使用网格的实际应用程序中使用。 但是,本指南中介绍的示例模型更简单,并且可以作为创建自定义网格模型的很好的介绍性示例。
使用客户端 - 服务器体系结构指定网格应用程序,其中客户机和服务器模型使用发布/订阅机制来相互通信。 因此,该模型的预期功能将通过两部分来实现:服务器模型,维护OnOff状态和客户端模型,用于操纵服务器上的OnOff状态。当此服务器模型从客户端模型接收到GET或(可靠)SET消息时,它将发送OnOff状态的当前值作为响应。 这使客户端保持服务器状态的最新状态。
下表显示了此模型支持的操作码:
发送的操作码是供应商特定型号的三个字节。 完整的操作码是特定于供应商的操作码和公司标识符的组合。 有关更多信息,请参阅access_opcode_t文档。
我们将为此模型使用以下标识符:
本表中使用的公司标识符是Nordic Semiconductor分配的蓝牙公司标识。 在一个真实的应用程序中,您应该使用您自己的公司分配的ID。
在遵循本指南的同时,请记住,为了简洁,跳过了一些重要的功能,例如错误处理。 但是,在编写应用程序时,应注意检查从所有API函数返回的错误代码,以防止轻松避免的错误进入应用程序。
如果要使用本指南中所述的相同基本布局来探索完整的模型实现,请参阅examples / models / simple_on_off中的Simple OnOff模型实现。 另外,如果您想将其看成一个完整的应用程序,请参阅examples / light_switch目录中的Light开关演示。
5.5 服务器模型
OnOff服务器收到SET和GET消息后,调用应用程序提供的回调函数,通过回调函数参数共享/请求数据。 为此,我们需要定义一个包含指向回调函数指针的模型上下文结构。 这个上下文结构被传递给所有的消息处理器。 以下代码片段显示了服务器模型(simple_on_off_server_t)所需的上下文结构:
typedef struct __simple_on_off_server simple_on_off_server_t;
typedef bool(* simple_on_off_get_cb_t)(const simple_on_off_server_t * p_self);
typedef bool(* simple_on_off_set_cb_t)(const simple_on_off_server_t * p_self,bool on_off);
struct __simple_on_off_server
{
access_model_handle_t model_handle;
simple_on_off_get_cb_t get_cb;
simple_on_off_set_cb_t set_cb;
};
接下来,我们需要定义操作码并创建必要的操作码处理函数来处理服务器模型的传入消息。 所有模型的所有操作码处理程序应使用与以下定义相同的函数原型:
typedef void(* access_opcode_handler_cb_t)(access_model_handle_t handle,const access_message_rx_t * p_message,void * p_args);
我们在服务器中需要三个操作码处理程序来处理SIMPLE_ON_OFF_OPCODE_GET,SIMPLE_ON_OFF_OPCODE_SET和SIMPLE_ON_OFF_OPCODE_SET_UNRELIABLE消息。 这些操作码处理程序中的每一个将从上下文结构中调用相应的用户回调函数。 该上下文结构通过p_args参数传递给操作码处理程序。
另外,如Mesh Profile Specification v1.0的3.7.5.2节中所定义的,每个接收元件通过响应该消息来发送并确认已确认的消息。 响应通常是状态消息。 状态消息通常包含SET消息设置的状态的当前值。 因此,模型使用set_cb()回调从用户应用程序获取当前OnOff状态值,并使用reply_status()函数发送该值。 如果发布地址由供应商设置,服务器模型还会使用publish_state()函数发布其状态以响应任何收到的消息。
以下片段显示了为Simple OnOff服务器模型定义的操作码处理程序:
static void handle_set_cb(access_model_handle_t handle, const access_message_rx_t * p_message, void * p_args)
{
simple_on_off_server_t * p_server = p_args;
NRF_MESH_ASSERT(p_server->set_cb != NULL);
bool value = (((simple_on_off_msg_set_t*) p_message->p_data)->on_off) > 0;
value = p_server->set_cb(p_server, value);
reply_status(p_server, p_message, value);
publish_state(p_server, value);
}
static void handle_get_cb(access_model_handle_t handle, const access_message_rx_t * p_message, void * p_args)
{
simple_on_off_server_t * p_server = p_args;
NRF_MESH_ASSERT(p_server->get_cb != NULL);
reply_status(p_server, p_message, p_server->get_cb(p_server));
}
static void handle_set_unreliable_cb(access_model_handle_t handle, const access_message_rx_t * p_message, void * p_args)
{
simple_on_off_server_t * p_server = p_args;
NRF_MESH_ASSERT(p_server->set_cb != NULL);
bool value = (((simple_on_off_msg_set_unreliable_t*) p_message->p_data)->on_off) > 0;
value = p_server->set_cb(p_server, value);
publish_state(p_server, value);
}
reply_status()函数使用access_model_reply()API将SIMPLE_ON_OFF_OPCODE_STATUS消息中的当前状态值作为回复发送给客户端。 此API需要某些参数才能正确发送消息,这就是为什么它已被包装在send_reply()中。 像这样实现send_reply():
static void reply_status(const simple_on_off_server_t * p_server,const access_message_rx_t * p_message,bool present_on_off)
{
simple_on_off_msg_status_t status;
status.present_on_off = present_on_off ? 1 : 0;
access_message_tx_t reply;
reply.opcode.opcode = SIMPLE_ON_OFF_OPCODE_STATUS;
reply.opcode.company_id = ACCESS_COMPANY_ID_NORDIC;
reply.p_buffer = (const uint8_t *) &status;
reply.length = sizeof(status);
(void) access_model_reply(p_server->model_handle, p_message, &reply);
}
publish_state()函数与reply_status()函数非常相似,只不过它使用access_model_publish()API来发布响应消息。 如果客户端模型的发布地址未由预配置器配置,则access_model_publish()将不发布给定的消息。为了将给定的操作码和公司ID链接到其对应的处理函数,指定了操作码处理程序查找表。 在向接入层注册模型时,将查找表作为输入参数。 表中的每个条目都是access_opcode_handler_t类型,由操作码,供应商ID和操作码处理函数指针组成。 对于服务器模型,它被定义为:
static const access_opcode_handler_t m_opcode_handlers[] =
{
{ACCESS_OPCODE_VENDOR(SIMPLE_ON_OFF_OPCODE_SET,ACCESS_COMPANY_ID_NORDIC),handle_set_cb},
{ACCESS_OPCODE_VENDOR(SIMPLE_ON_OFF_OPCODE_GET,ACCESS_COMPANY_ID_NORDIC),handle_get_cb},
{ACCESS_OPCODE_VENDOR(SIMPLE_ON_OFF_OPCODE_SET_UNRELIABLE, ACCESS_COMPANY_ID_NORDIC),
handle_set_unreliable_cb}
};
我们现在拥有将模型放在一起的所有东西。 初始化函数必须分配和添加模型到接入层:
uint32_t simple_on_off_server_init(simple_on_off_server_t * p_server, uint16_t element_index)
{
if (p_server == NULL ||p_server->get_cb == NULL ||p_server->set_cb == NULL)
{
return NRF_ERROR_NULL;
}
access_model_add_params_t init_params;
init_params.element_index = element_index;
init_params.model_id.model_id = SIMPLE_ON_OFF_SERVER_MODEL_ID;
init_params.model_id.company_id = ACCESS_COMPANY_ID_NORDIC;
init_params.p_opcode_handlers = &m_opcode_handlers[0];
init_params.opcode_count = sizeof(m_opcode_handlers) / sizeof(m_opcode_handlers[0]);
init_params.p_args = p_server;
init_params.publish_timeout_cb = NULL;
return access_model_add(&init_params, &p_server->model_handle);
}
现在,您已经拥有了一个简单的OnOff服务器模型的基本框架,可以对其进行扩展或调整,以生成更复杂的服务器模型。 请参阅示例/ models / simple_on_off /以获取此模型的完整代码。
5.6 客户端模型
客户端模型用于与相应的服务器模型进行交互。 它发送SET和GET消息并处理传入的状态回复。 客户端模型使用发布机制发送消息。 它使用分配的发布地址作为传出消息的目的地。就像在服务器实现中一样,客户端需要一个上下文结构来保存关于回调和其模型句柄的信息。 此外,我们使用布尔变量来跟踪事务是否当前处于活动状态,并防止运行多个同时事务。在网状网络中,消息可能不按顺序发送,或根本不能发送。 因此,一个客户端应该只使用相应的服务器一次执行一个事务。客户端模型使用回调函数向用户应用程序提供有关服务器状态的信息。 如果服务器在给定的时间范围内没有回复,则会通知用户应用程序错误代码为SIMPLE_ON_OFF_STATUS_ERROR_NO_REPLY。
以下代码片段显示了此模型所需的状态码(simple_on_off_status_t)和上下文结构(simple_on_off_client_t):
typedef enum
{
SIMPLE_ON_OFF_STATUS_ON,
SIMPLE_ON_OFF_STATUS_OFF,
SIMPLE_ON_OFF_STATUS_ERROR_NO_REPLY
} simple_on_off_status_t;
typedef struct __simple_on_off_client simple_on_off_client_t;
typedef void (*simple_on_off_status_cb_t)(const simple_on_off_client_t * p_self, simple_on_off_status_t status, uint16_t src);
struct __simple_on_off_client
{
access_model_handle_t model_handle;
simple_on_off_status_cb_t status_cb;
struct
{
bool reliable_transfer_active;
simple_on_off_msg_set_t data;
} state;
};
客户端模型可以发送两种消息:可靠(已确认)消息和不可靠(未确认)消息。 客户端模型需要使用access_model_reliable_publish()API发送可靠的消息,而access_model_publish()API用于发送不可靠的消息。
access_model_reliable_publish()API通过重传消息来保证消息的传递,直到从目标节点收到答复或事务超时。 当事务完成(或超时)时,会调用回调函数来通知客户机模型。 如果没有收到已确认的SET消息的服务器模型的响应,则通过调用用户的状态回调函数将相应的错误通知给用户应用程序。
以下片段显示客户端模型的可靠性_status_cb()回调函数和send_reliable_message()函数:
static void reliable_status_cb(access_model_handle_t model_handle,
void * p_args,
access_reliable_status_t status)
{
simple_on_off_client_t * p_client = p_args;
NRF_MESH_ASSERT(p_client->status_cb != NULL);
p_client->state.reliable_transfer_active = false;
switch (status)
{
case ACCESS_RELIABLE_TRANSFER_SUCCESS:
/* Ignore */
break;
case ACCESS_RELIABLE_TRANSFER_TIMEOUT:
p_client->status_cb(p_client, SIMPLE_ON_OFF_STATUS_ERROR_NO_REPLY, NRF_MESH_ADDR_UNASSIGNED);
break;
default:
/* Should not be possible. */
NRF_MESH_ASSERT(false);
break;
}
}
static uint32_t send_reliable_message(const simple_on_off_client_t * p_client,
simple_on_off_opcode_t opcode,
const uint8_t * p_data,
uint16_t length)
{
access_reliable_t reliable;
reliable.model_handle = p_client->model_handle;
reliable.message.p_buffer = p_data;
reliable.message.length = length;
reliable.message.opcode.opcode = opcode;
reliable.message.opcode.company_id = ACCESS_COMPANY_ID_NORDIC;
reliable.reply_opcode.opcode = SIMPLE_ON_OFF_OPCODE_STATUS;
reliable.reply_opcode.company_id = ACCESS_COMPANY_ID_NORDIC;
reliable.timeout = ACCESS_RELIABLE_TIMEOUT_MIN;
reliable.status_cb = reliable_status_cb;
return access_model_reliable_publish(&reliable);
}
现在我们需要为用户应用程序创建API函数来发送GET和SET消息。 以下片段定义了这些函数:
uint32_t simple_on_off_client_set(simple_on_off_client_t * p_client, bool on_off)
{
if (p_client == NULL || p_client->status_cb == NULL)
{
return NRF_ERROR_NULL;
}
else if (p_client->state.reliable_transfer_active)
{
return NRF_ERROR_INVALID_STATE;
}
p_client->state.data.on_off = on_off ? 1 : 0;
p_client->state.data.tid = m_tid++;
uint32_t status = send_reliable_message(p_client,
SIMPLE_ON_OFF_OPCODE_SET,
(const uint8_t *)&p_client->state.data,
sizeof(simple_on_off_msg_set_t));
if (status == NRF_SUCCESS)
{
p_client->state.reliable_transfer_active = true;
}
return status;
}
uint32_t simple_on_off_client_set_unreliable(simple_on_off_client_t * p_client, bool on_off, uint8_t repeats)
{
simple_on_off_msg_set_unreliable_t set_unreliable;
set_unreliable.on_off = on_off ? 1 : 0;
set_unreliable.tid = m_tid++;
access_message_tx_t message;
message.opcode.opcode = SIMPLE_ON_OFF_OPCODE_SET_UNRELIABLE;
message.opcode.company_id = ACCESS_COMPANY_ID_NORDIC;
message.p_buffer = (const uint8_t*) &set_unreliable;
message.length = sizeof(set_unreliable);
uint32_t status = NRF_SUCCESS;
for (uint8_t i = 0; i < repeats; ++i)
{
status = access_model_publish(p_client->model_handle, &message);
if (status != NRF_SUCCESS)
{
break;
}
}
return status;
}
uint32_t simple_on_off_client_get(simple_on_off_client_t * p_client)
{
if (p_client == NULL || p_client->status_cb == NULL)
{
return NRF_ERROR_NULL;
}
else if (p_client->state.reliable_transfer_active)
{
return NRF_ERROR_INVALID_STATE;
}
uint32_t status = send_reliable_message(p_client,
SIMPLE_ON_OFF_OPCODE_GET,
NULL,
0);
if (status == NRF_SUCCESS)
{
p_client->state.reliable_transfer_active = true;
}
return status;
}
为了处理回复消息,我们需要为SIMPLE_ON_OFF_OPCODE_STATUS操作码添加一个操作码处理程序。 所有传入的消息,即使是对从节点发送的消息的回复,都需要处理操作码处理程序。 这段代码显示了操作码处理程序的实现,并为客户端模型定义了操作码处理程序查找表:
static void handle_status_cb(access_model_handle_t handle, const access_message_rx_t * p_message, void * p_args)
{
simple_on_off_client_t * p_client = p_args;
NRF_MESH_ASSERT(p_client->status_cb != NULL);
if (!is_valid_source(p_client, p_message))
{
return;
}
simple_on_off_msg_status_t * p_status =
(simple_on_off_msg_status_t *) p_message->p_data;
simple_on_off_status_t on_off_status = (p_status->present_on_off ?
SIMPLE_ON_OFF_STATUS_ON : SIMPLE_ON_OFF_STATUS_OFF);
p_client->status_cb(p_client, on_off_status, p_message->meta_data.src.value);
}
static const access_opcode_handler_t m_opcode_handlers[] =
{
{{SIMPLE_ON_OFF_OPCODE_STATUS, ACCESS_COMPANY_ID_NORDIC}, rx_status_cb}
};
客户端模型初始化的完成方式与服务器模型完全相同:
uint32_t simple_on_off_client_init(simple_on_off_client_t * p_client, uint16_t element_index)
{
if (p_client == NULL || p_client->status_cb == NULL)
{
return NRF_ERROR_NULL;
}
access_model_add_params_t init_params;
init_params.model_id.model_id = SIMPLE_ON_OFF_CLIENT_MODEL_ID;
init_params.model_id.company_id = ACCESS_COMPANY_ID_NORDIC;
init_params.element_index = element_index;
init_params.p_opcode_handlers = &m_opcode_handlers[0];
init_params.opcode_count = sizeof(m_opcode_handlers) / sizeof(m_opcode_handlers[0]);
init_params.p_args = p_client;
init_params.publish_timeout_cb = NULL;
return access_model_add(&init_params, &p_client->model_handle);
}
客户端现在已经完成了,您应该可以通过与服务器节点通信来打开或关闭某些东西! 完整的实现请参见Simple OnOff模型。