NRF52832DK-Mesh组网实验
蓝牙Mesh和BLE是并行发展的一个独立分支,虽然底层共用Physical Layer和Link Layer,但是上层协议并不相同。因此,nRF52832的Mesh例程,并不在BLE协议栈sdk中,而是安装一个扩展包。
本文介绍蓝牙Mesh相关的环境搭建和实验介绍。
目录
1 Mesh开发环境
1.1 蓝牙Mesh SDK
SDK位置:归档资料/2-协议栈SDK/nRF5_SDK_Mesh_v310.zip
,注意文件名中的Mesh字样。
和BLE协议栈一样,将该压缩包直接解压到BLE协议栈同级目录即可使用,解压后的SDK路径如下:E:\project-nordic\nRF5_SDK_Mesh_v310
1.2 集成开发软件SES
SES时是Segger Embedded Studio编译器的缩写,用来编译蓝牙Mesh代码,SES功能与IAR或Keil类似。
备注:Nordic为何使用SES而不是IAR,原因不得而知。
1.2.1 SES安装
SES软件位置:归档资料/7-编译器SES/Setup_EmbeddedStudio_ARM_v410_win_x64.exe
。
我们双击打开安装界面,按照如下动图所示方式进行安装(可以全部默认安装)。
1.2.2 SES许可证激活
我们双击打开安装好的SES,选择编译自带测试例程,此时弹出需要激活的界面。
由于我们准备使用SES编译nordic的NRF52832的SDK,所以这里我们选择Activate Your Free License。这里因为nordic公司已经帮客户想SEGGER公司买下了使用的版权,所以是免费的许可证。
我们按照要求填写好信息,主要是邮箱地址(用于接收SEGGER公司发给我们的许可证),填写完成之后点击Request License。下面是我们接收的邮件信息,我们将红色框内的内容复制下来。
打开SES,选择Tools\License Manager...,我们选择Activate Embedded Studio,打开如下界面,并将我们的许可证粘贴进去,然后点击Install License。
安装好许可证之后,跳转到如下的界面。
此时我们的许可证激活完毕(有时候需要等待一段时间,才会显示激活成功)。
2 mesh实验学习流程概论
1、首先大家一定要先查看一下我们资料中的手册“BLE-Mesh技术揭秘”,这个是很重要的一个mesh基本概念的介绍,只有看完这个文档,我们才能明白mesh组网是什么,是怎么完成的,后续的我们实验内容,也会在这个文档中查到相应的对照。
2、按照Light_switch测试实验的实验说明,跑通测试流程(可以固件清零重新烧写多测试几遍,有助于增强我们的流程熟练度,利于后面的开发)。light_switch实验是现在nordic提供给我们的唯一的真正意义上的mesh组网通信例程。
3、根据我们下面拆分的组网流程,对照依次的实验说明,进行mesh组网流程的分步学习。前期实验部分我们主要讲解节点设备,配置者设备功能我们使用手机app完成,目的是减少大家的学习难度,配置者设备我们会放在最后一个实验给大家做介绍。
①广播(通信TX)——了解节点设备的广播功能,分为三个重要部分:PB-ADV和Unprov Beacon,PB-GATT的广播,以及模型的Publish用于发送数据。
②扫描(通信RX)——了解节点设备的扫描功能,分为两个重要部分:普通的BLE SCAN功能,以及模块的Subscription用于接收数据。
③入网验证——了解节点设备接入网络的验证过程,实验中使用的是PB-GATT去配置的,包含了:交换公共密钥,完成验证(是否有带外OOB)。
④设备信息分配——启动配置数据分配,给节点分配如下数据:网络key(Netkey)、应用key(Appkey)以及节点设备地址(nodeAddr)。
⑤元素——了解节点设备的元素与模型定义。
⑥模型——了解模型的功能和创建方法,模型的订阅subscription和发布publish功能。
3 Light_switch测试实验
3.1 实验简介
此示例演示了包含充当两个角色的设备的网状生态系统:配置角色(Provisioner role)和节点角色(Node role、provisionee role)。 它还通过在应用程序中使用[Generic OnOff model]来演示了如何使用Mesh模型。
该示例由三个较小的示例组成:
Light switch server:一个实现了[Generic OnOff server model]的简约服务器,用于接收状态数据并控制板上LED 1的状态。
Light switch client:一个实现了[Generic OnOff client model]的简约客户端。当用户按下任意按钮时,OnOff Set消息将发送到配置的目标地址。
Mesh Provisioner:一个简单的静态配置设备应用,用于建立演示网络,该配置设备在一个网状网络中配置所有节点。 此外,配置设备还可以在这些节点上配置网格模型[Generic OnOff model]实例的绑定以及发布和订阅设置,以使它们能够相互通信。
[Generic OnOff Client/Server]模型用于操纵打开/关闭状态。请注意,当服务器设置了发布地址(如本例所示)时,服务器会将其状态更改的任何操作披露到其发布地址。
下图给出了将由静态供应商设置的网状网络的整体视图, 括号中的数字表示预配置程序分配给这些节点的地址。
灯开关服务器和灯开关客户端示例均具有预配方角色。它们支持通过广告承载(PB-ADV)和GATT承载(PB-GATT)进行配置,并且还支持网状代理服务器(Proxy Service),但是不支持代理客户端(Proxy Client)。
3.2 硬件说明
完成这个实验,我们最少需要两个开发板硬件用做我们的节点设备(Node):
- 一个开发板用做client
- 一个或者多个开发板用做server
此外,我们还需要以下之一作为我们的配置设备(Provisioner):
- 如果您决定使用静态预配器示例,则多准备一个开发板
- 如果您决定使用app程序进行设置,则需要安装手机app@link_nrf_mesh_app(@link_nrf_mesh_app_ios或@link_nrf_mesh_app_android)。
3.3 软件说明
1、我们使用NrfGo上位机,依次对三个开发板进行擦除及softdevice的烧写
2、使用SEGGER Embedded Studio for ARM编译器分别打开我们`<InstallFolder>/examples/light_switch`路径下的3个实验,并完成编译
●\client\light_switch_client_nrf52832_xxAA_s132_6_1_0.emProject
●\server\light_switch_server_nrf52832_xxAA_s132_6_1_0.emProject
●\provisioner\light_switch_provisioner_nrf52832_xxAA_s132_6_1_0.emProject
3、此时我们对3个开发板分别进行固件的烧写,分别烧写client、server、provisioner(建议大家给3个开发板贴上标签,方便后续区分)
3.4 实验现象
注意:保证我们的3个设备都是刚按照软件说明部分的步骤配置完成,并且没有进行过任何硬件控制(按键操作)。目的是保证所有设备均未被我们人为配置过,没有进行过网络配置,否则可能有任意异常(不同网络配置,导致的不同现象) |
1、首先我们分别给client和provisioner供电,然后我们按下provisioner的按键S1:
●provisoner:LED灯点亮,代表正在配置查找设备,配置组网
●client:首先LED3和LED4闪烁,代表正在组网;LED1-LED4四个灯一起闪烁,代表组网完成
我们如果打开RTT检测log打印,可以看到如下的信息,这个时候我们已经给client设备分配了Node Address为0x0100。provisioner信息(上),client信息(下)。
2、使用provisioner配置好client之后,我们给server开发板上电,这个时候provisioner将会配置server入网:
●provisoner:LED1点亮代表正在配置网络,LED2点亮代表网络配置完成
●server:首先LED3和LED4闪烁,代表正在组网;LED1-LED4四个灯一起闪烁,代表组网完成
我们打开RTT检测log打印,可以看到server已经配置入网,并且被分配了地址Node Address为0x0104。provisioner信息(上),server信息(下)。
3、此时整个网络的配置已经完成,这个时候我们可以按下client上的按键来控制server上的LED点亮或者熄灭。
由于我们的server被provisioner分配的Node Address是0x0104(偶数),所以我们通过client的S3控制server的LED1点亮,通过client的S4控制server的LED1熄灭。
client设备对于server设备的控制,通过Node Address的奇偶位不同,分成了两个组。
client的S1和S2分别控制Node Address为奇数的server设备的LED1点亮和熄灭。 client的S3和S4分别控制Node Address为偶数的server设备的LED1点亮和熄灭。 |
我们打开RTT检测log打印,可以看到client分别按下button2(S3)以及button3(S4),分别会设置server的GPIO输出高低电平。client信息(上),server信息(下)。
4、至此我们的mesh组网的lightswitch实验测试完成。
4 ①广播(通信TX)
有关广播部分,我们需要给大家讲解三个方面,分别如下:
1、PB-ADV,这个是mesh中的广播承载层,用于我们启动配置过程(暂时的例程和手机app都是用的PB-GATT来配置的,但是源码中PB-ADV的流程是包含的)。
2、Unprov Beacon,这个是广播一个未被配置的beacon,就是一个特殊形式的广播数据包,用于声明自己是未经配置的设备。
3、PB-GATT,这个是mesh中的GATT承载层,用于我们启动配置过程(当前手机app就是使用PB-GATT,也就是连接之后,通过GATT服务去发送配置信息)。
4、Publish,这个是我们用于模型发布数据的部分,我们将需要发送的数据通过advData广播出去。
4.1 PB-ADV
PB-ADV说明请查看“BLE-Mesh技术揭秘”第8.1章节。
我们来看下PB-ADV整个的流程,首先在mian文件中,我们的start()函数下,我们调用了mesh_provisionee_prov_start()函数去启动节点设备的等待配置。
我们继续追踪mesh_provisionee_prov_start(),可以看到在使能宏定义MESH_FEATURE_PB_ADV_ENABLED的情况下,可以追踪到nrf_mesh_prov_bearer_adv_interface_get()函数。
在nrf_mesh_prov_bearer_adv_interface_get()函数中,我们可以看到prov_bearer_interface_t结构体,这个结构体下面提供了我们配置承载层需要的API函数。
4.2 Unprov Beacon
Unprov Beacon说明请查看“BLE-Mesh技术揭秘”第5.5及5.5.1章节。
上面我们有说到PB-ADV是承载层用于配置节点的功能,而Unprov Beacon则是我们的节点设备用于声明自己未经配置,所以这两个其实是一脉相承的。
紧接着上面的nrf_mesh_prov_bearer_adv_interface_get()函数,其中的prov_bearer_interface_t结构体包含的是处理承载层的API函数,在这个结构体当中有一个.listen_start = prov_bearer_adv_listen,是用于开始监听传入的配置链接的功能,在这个函数中我们可以看到send_unprov_beacon()函数。
send_unprov_beacon()函数是用于发送Unprov Beacon数据的函数,在这个函数中我们调用prov_beacon_unprov_build函数去创建用于广播的Unprov Beacon数据包,然后调用advertiser_interval_set函数去设置广播的间隔,最后调用advertiser_packet_send函数去将我们的数据包发送出去。
默认的Unprov Beacon广播间隔是NRF_MESH_PROV_BEARER_ADV_UNPROV_BEACON_INTERVAL_MS 2000。
最终调用的是advertiser_packet_send(0函数去将广播数据发送出去的。
这里我们再最后看一下prov_beacon_unprov_build()函数,这个函数是生成我们的Unprov Beacon的数据包。
其中标识位是0x2B,这个flag是beacon的意思。data的第一个字节0x00代表是我们的BEACON_TYPE_UNPROV(未配置的节点设备),接下来的16个字节是device uuid(这个是寄存器FICR中的device id生成的),接下来2字节是oob info(这里是00 00,代表没有使能OOB),最后的4个字节是URI通过AES-CMAC算法计算出来的值。
4.3 PB-GATT
PB-GATT说明请查看“BLE-Mesh技术揭秘”第8.1章节。
和PB-ADV一样的,我们从main文件中的start()函数,追踪到mesh_provisionee_prov_start(),可以看到在使能宏定义MESH_FEATURE_PB_GATT_ENABLED的情况下,可以追踪到nrf_mesh_prov_bearer_gatt_init()以及nrf_mesh_prov_bearer_gatt_interface_get()函数。
首先我们看一下nrf_mesh_prov_bearer_gatt_init()函数,这个是初始化并设置PB-GATT服务(通过Mesh GATT接口)的函数。对比我们使用nrf master control panel手机APP连接之后获取到的服务,可以看到其中的PB-GATT的服务以及特征值。
再往下的mesh_gatt_init()函数,这个就不给大家分析了,是和我们普通的BLE一样的方式去注册一个服务。
在nrf_mesh_prov_bearer_gatt_interface_get()函数中,我们可以看到prov_bearer_interface_t结构体,这个结构体下面提供了我们配置承载层需要的API函数(这个是和上方的PB-ADV是一样的)。
和Unprov Beacon类似的,当我们开始监听配置链接的时候,会触发.listen_start = listen_start_cb的回调,在这个回调中我们触发FSM(有限状态机)的事件E_LISTEN_START,然后调用link_evt_send()去post顺序承载事件。
然后我们来到状态机列表m_pb_gatt_fsm_transition_table[],可以看到E_LISTEN_START事件ID代表的动作ID是A_LISTEN_START。而最终我们的A_LISTEN_START动作,执行的是a_listen_start函数功能(这其中涉及的代码逻辑,暂时先不说明,大家可以自己查看源码的引用)。
然后我们继续追踪a_listen_start(),可以看到其中调用了mesh_adv_params_set函数设置了我们广播的超时时间MESH_ADV_TIMEOUT_INFINITE以及广播间隔NRF_MESH_PROV_BEARER_GATT_UNPROV_BEACON_INTERVAL_MS,调用mesh_adv_data_set函数设置了我们的广播数据内容,最后调用mesh_adv_start函数启动广播。
默认的PB-GATT的广播数据的广播周期是NRF_MESH_PROV_BEARER_GATT_UNPROV_BEACON_INTERVAL_MS 200。
我们同样的看一下mesh_adv_data_set()函数最终配置生成的广播数据,可以看到广播数据的flag是0x06,服务的UUID就是我们上面提到的PB-GATT服务的UUID,接下来16字节UUID是和PB-ADV一样的FICR寄存器的device id值,最后的扫描回调数据携带的是设备的全名“nRF5x Mesh Light”。
最后是我们的mesh_adv_start()函数去启动广播,可以看到调用是我们ble stack sotfdevice当中的API函数sd_ble_gap_adv_start()去启动的。
4.4 Publish(模型发布)
这一章节我们只给大家讲解模块publish的数据发布流程,暂时不说明模型的创建方法,以及如何使用,这个放在下面单独的模型章节进行说明。
以generic_onoff_server模型为例,我们找到access_model_publish以及access_model_reply函数,这两个函数的功能类似的,都是用于我们用户的模型发送数据的。
其中access_model_publish是用于模型主动发送,而access_model_reply是用于模型给对应的Opcode回应数据。
这里我们以access_model_publish函数为主,来看下发布数据的代码流程。
查看一下access_model_publish()函数,我们可以看到packet_alloc_and_tx()函数,这个是我们继续发送数据的函数。下面还有一个蓝牙方框中的内容,是我们定义的重传机制(mesh数据为了保证传输的成功率,默认都是会重传数据包的,默认重传次数是4次)。
然后我们继续看一下packet_alloc_and_tx()函数,在这个函数中,我们最终会调用packet_tx()函数将数据发送出去。
继续追踪packet_tx()函数,这个函数的内容比较多,我们只截取了其中数据发送的部分,也就是我们的nrf_mesh_packet_send()函数,我们在他的tx_params中赋值好我们需要发送出去的数据内容。
nrf_mesh_packet_send()函数最终调用的是transport_tx()函数发送数据。而transport_tx()函数,最终是调用upper_transport_tx()函数进行的数据发送。
我们再继续追踪upper_transport_tx()函数,可以看到我们会根据p_metadata->segmented来判断数据是否要分包发送。
接下里我们以不分包发送函数unsegmented_packet_tx()为例,来继续说明数据传输流程。
我们可以看到我们的unsegmented_packet_tx()函数中,最终是调用的network_packet_send()函数。(分包的segmented_packet_tx()函数最终也是调用的network_packet_send(),大家可以自己追踪代码看一下)
继续追踪我们的network_packet_send()函数,可以看到我们的net_packet_encrypt()加密函数,这个是将我们的明文数据,做好加密工作。 然后调用core_tx_packet_send()函数发送出去。
最后我们追踪到core_tx_packet_send()函数,可以看到调用的是p_bearer->p_interface->packet_send()函数去发送的。所以接下来我们要找到我们的packet_send()功能是在哪边实现的。
可以看到我们是在core_tx_bearer_add函数中去获得的p_interface这个功能。
我们追踪core_tx_bearer_add()函数的引用路径,可以看到是在core_tx_adv_init()当中。(除了此处,还有另一处地方也有引用,功能是类似的,大家可以自行了解)。 我们看下参数m_interface,可以看到m_interface->packet_send功能,就是对应我们在core_tx_packet_send()函数调用的p_bearer->p_interface->packet_send()。
所以我们接下来来看下m_interface结构体当中的packet_send()函数实现的是什么功能,可以看到最终调用的是advertiser_packet_send()函数将数据发送出去。看到这边我们就很熟悉了,和上面的几个广播功能,最后调用的都是相同的广播包发送函数。
5 ②扫描(通信RX)
5.1 标准BLE SCAN
看过了我们的广播功能之后,我们来看下扫描的部分。在ble mesh中,我们的扫描功能scan,就是用于获取数据的,类似于数据通信的RX。
我们从main文件中的initialize()函数开始,首先是我们mesh_init()函数,最终我们会调用mesh_stack_init()函数。
在mesh_satck_init函数中,我们可以看到调用了nrf_mesh_init函数去初始化core的参数。
这一段是nrf_mesh_init函数部分,由于这个初始化函数太长,所以我们只截取了scan相关的部分。
首先是第一点scanner_init(scanner_packet_process_cb)函数,在这个函数中,我们回去初始化扫描的参数配置,并且会开启扫描功能。最终我们可以从scanner_packet_process_cb这个回调中得到我们扫描到的数据(获得所有的BLE广播数据)。
然后是第二点ad_listener_subscribe(&m_nrf_mesh_listener),这个是用来侦听订阅的消息的(从获得的所有BLE广播数据中,筛选出的订阅的消息)。
然后我们继续看一下scanner_init的扫描初始化函数,在这个函数中我们首先调用scanner_config_reset()函数去配置扫描的默认参数。其中包含了:
1、扫描的通道SCANNER_CHANNELS_DEFAULT,也就是BLE的{37,38,39}三个channels都去扫描。
2、规范定义的非连接状态访问地址BEARER_ACCESS_ADDR_DEFAULT。
3、配置物理层协议是1Mbit模式。
4、扫描的间隔时间BEARER_SCAN_INT_DEFAULT_MS,扫描的窗口时间BEARER_SCAN_WINDOW_DEFAULT_MS
5、射频的发射功率RADIO_POWER_NRF_0DBM。
6、BLE广播包最大长度RADIO_CONFIG_ADV_MAX_PAYLOAD_SIZE。
最后我们会在nrf_mesh_enable()函数中调用scanner_enable()函数去启动扫描,这个大家可以自己查看一下代码,比较容易理解。
最后我们来看一下scanner_packet_process_cb()回调函数,其中我们可以直接拿到的扫描的数据包是rx_data,包含了数据的类型、数据内容以及数据长度等等信息。到这边我们的扫描功能是已经完成的,但是我们还需要再看下下面一部分的ad_listener的流程。
5.2 Subscription(模型订阅)
我们需要注意下ad_listener_process()函数,这个函数是将我们扫描到的订阅数据(不符合标准的BLE广播数据,则认为是侦听到的订阅数据)传递到订阅者进行处理(订阅者指的是带订阅功能的模型)。这个是和我们上方说的关注点二ad_listener_subscribe()函数是一脉相承的。
然后我们来对照着看一下ad_listener_subscribe()函数以及ad_listener_process()函数。这两个函数一个是定义了侦听订阅,一个是将扫描获取到的数据传递给订阅者。这两者间的运作关系,大家有兴趣的可以继续查阅他的底层代码逻辑处理,一个是向list中添加订阅者信息,一个是将数据通过p_listener->handler()将数据传递给订阅者处理。
最终当我们扫描到订阅消息,并调用ad_listener_process()函数传递时,会触发我们注册的回调nrf_mesh_listen(调用ad_listener_subscribe(&m_nrf_mesh_listener)时,在m_nrf_mesh_listener中)。我们来看下nrf_mesh_listen函数中的处理,其中我们先从数据包中解析了数据的类型type,然后根据不同类型的数据进行相应的处理。
AD_TYPE_MESH | 蓝牙网格物体的AD类型 |
AD_TYPE_BEACON | 蓝牙网状信标的AD类型 |
AD_TYPE_PB_ADV | PB-ADV消息的AD类型 |
AD_TYPE_DFU | nRF OpenMesh消息的AD类型 |
6 ③入网验证
经过前两章的学习,我们对于BLE MESH的广播及扫描的代码流程已经有了一定的了解,也就是已经大概的明白了数据的发送和接收的流程。
那么从这一章节开始,我们将会给大家展示,如何配置一个节点设备入网。配置者设备,我们使用的是手机APP:nRF Mesh。
我们从main文件中的start()函数开始,来看一下入网的代码处理及其流程。在start()函数中我们最后是调用的mesh_provisionee_prov_start()函数来启动的节点配置功能,我们来查看一下它所携带的参数prov_start_params。这部分的参数都是比较重要的,有携带的是设备的信息的、有携带认证密码的,有回调返回的,所以我们会挨个介绍一下。
首先携带的是static_auth_data,这个是我们带外OOB认证的密码。如果我们在使用nRF Mesh去进行身份验证的时候,选择使用Static OOB,那么当我们在进行PB-GATT的连接过程中,会弹出一个新的窗口,要求我们去输入这个静态的128bit的OOB密钥。
接下来携带的是provisioning_complete_cb()回调函数,这个函数处理的是节点设备成功的被配置入网,在这里我们可以获取到我们的节点设备被分配的node_Address。
然后携带是device_identification_start_cb()回调函数,这个回调函数是用于通知开发者,我们的节点设备当前已经开始配置。
再然后携带的是URI【统一资源标识符(Uniform Resource Identifier,URI)】,这个是用于标识我们的设备类型的,这个数据会通过AES-CMAC算法加密后添加到我们的Unprov Beacon数据的末尾。
这里我们是light switch的server设备,所以我们定义URI是EX_URI_LS_SERVER。
分析完携带的参数之后,我们继续来查看mesh_provisionee_prov_start()函数。在这个函数中,我们首先是定义了一下prov_caps,也就是配置支持的OOB功能。然后我们调用nrf_mesh_generate_keys()函数去生成有效的密钥对等待使用。
再然后是调用nrf_mesh_prov_init()函数去初始化prov配置信息的结构体参数,里面有结构体本身m_pro_ctx,需要配置的参数公钥m_public、私钥m_private,以及我们刚刚定义的oob功能prov_caps,最后还携带了一个事件回调函数prov_ect_handler。
接下来是PB-ADV以及PB-GATT的处理过程,这个前面已经讲解过,这里不再赘述。
最后我们调用provisionee_start()函数去开启配置。
我们继续来追踪一下provisionee_start()函数,在这个函数中我们调用nrf_mesh_prov_listen()函数去开启侦听承载层的消息。这里我们需要侦听PB-ADV以及PB-GATT两个。
然后在nrf_mesh_prov_listen()函数中,我们调用prov_provisionee_listen()函数携带我们刚刚传入的nrf_mesh_prov_ctx_t结构体,以及p_bearer、URI、oob_info_source等参数信息,继续向下传递。
最终我们来查看下prov_provisionee_listen()函数,在这个函数中我们传递了p_bearer->p_callbacks给m_prov_callbacks回调,然后调用p_bearer->p_interface->listen_start()函数去启动PB-ADV以及PB-GATT的侦听。
当我们在侦听之后,接收到任何配置相关的消息,都将通过m_prov_callbacks()函数回调传递给我们处理,大家可以自行看下其中4个不同的功能函数,具体处理的都是什么消息。
在prov_provisionee_listen()函数中,我们可以看到回调函数指针m_prov_callbacks指向的是p_bearer->p_callbacks,我们一路向上查找,最终可以找到这个p_bearer->p_callbacks函数就是指的prov_evt_handler()函数。所以其实我们用户可以在这一层去查看比较重要的配置事件返回,而不需要到上面的m_prov_callbacks中这样偏底层的地方去查看。
在这个回调函数中,我们可以看到mian文件中注册的prov_device_identification_start_cb、prov_device_identification_stop_cb、provisioning_aborted_cb的返回。
那么除了上方已经存在的几个回调,我们可以看到mian文件中的start()函数下的prov_start_params参数,还缺少一个配置完成的回调prov_complete_cb()。
这个回调的返回方式比较特殊,和上方其他的都不同,是由SD(softdevice)直接从底层返回给我们的。注册这个回调的函数是NRF_SDH_STATE_OBSERVER,返回的回调函数是sd_state_evt_handler。其中触发的事件NRF_SDH_EVT_STATE_ENABLED,这个是在mian文件中的initialize()函数下的ble_stack_init()函数中就被触发的,这个大家可以自行查看一下,不太重要,因为都是nordic底层处理好的消息。
以上整个配置流程的处理,不太好分步展示相应的现象,这个大家可以自己使用SES在线仿真的方式去调试一下流程,这里我们直接给大家展示整个流程打印出的相应的信息,以及nRF Mesh中暂时出来的现象。
7 ④设备信息分配
当我们成功的配置节点设备入网之后,我们需要给节点设备主要分配如下几个信息,有关这几个要点的介绍,可以查看“BLE-Mesh技术揭秘”第3章节:
1、节点在网络中的地址Node Address
2、网络密钥Netkey
3、应用密钥Appkey
4、元素的地址Element Address
5、绑定model的Appkey,分配Publish Address,Subscription Address
7.1 Node Address
首先我们来看一下我们使用nRF Mesh成功配置我们的节点之后,打印出来的消息,开始的紫色字体显示了我们给当前节点设备分配的Node Address。
后面紧接着还有四段黄色字体的内容(这个部分大家的工程是不会打印的,是我们修改例程打印出来给大家展示一下),这四个部分就是我们配置节点的时候,配置者设备(nRF Mesh)给节点设备的四个主要指令,这些指令的具体功能,大家可以查看mesh的协议手册。
在代码中,我们会在mian文件的provisioning_complete_cb()回调函数中获取并打印我们的Node Address,这个回调触发的含义就是我们的节点配置已经完成了。 具体这个回调在代码中的回调及触发流程,大家看可以查看“③入网验证”的说明。
我们使用nRF Mesh配置好节点设备之后,app界面显示内容如下,包含了节点的信息(地址、名称、配置时间等等),元素和模型,以及Netkey和Appkey等几个重要的设备信息。
7.2 Netkey
我们点击查看一下Network key,可以看到里面有一个Network Key 1,这是一个16字节的网络密钥。我们可以在nRF Mesh的Settings界面去增加或者删除一个network key,用户可以自定义此密钥。
在我们的代码中,我们是在pro_evt_handler回调的NRF_MESH_PROV_EVT_COMPLETE事件中获取这个netkey,这个回调函数的触发流程同样是在“③入网验证”已经有说明。
我们直接看下NRF_MESH_PROV_EVT_COMPLETE事件下的mesh_stack_provisioning_data_store()函数,在这个函数中我们主要的任务,就是将配置者分配给我们的node address、netkey、devkey等等信息,分别做一些存储以及功能绑定。
7.3 Appkey
同样的我们打开Application Key,可以看到其中包含的3个密钥,这里我们只选用了Application Key 1作为我们的密钥。大家同样可以在nRF Mesh的Settings界面去增加或者删除一个App key,用户可以自定义此密钥。
appkey这里和netkey不同,netkey是我们网络中的节点设备以及我们配置者设备共有的一个密钥,只有拥有这个密钥,我们才能在当前网络中通信,所以当我们的节点设备配置入网后,会立刻得到netkey(必须要分配给我的节点设备才行)。 而appkey只是我们的某个模型用于通信的密钥,所以我们称它为应用密钥,这个appkey的分配,是在我们的config server下。这个config_server_init的初始化流程,是在我们的mesh_stack_init()函数下,就去初始化配置好的。最终当我们有信息配置的时候,会触发opcode_handlers,我们可以从这个操作码列表中查找相应的信息处理。
我们来看下config server的opcode_handlers[]操作码处理程序列表,使用黄色标记给大家圈了几个操作码指令,这几个是我们配置一个最简单的server节点设备,都需要使用到的,包含了:
1、appkey的添加,CONFIG_OPCODE_APPKEY_ADD
2、publish发布地址,CONFIG_OPCODE_MODEL_PUBLICATION_VIRTUAL_ADDRESS_SET
3、subscription订阅地址,CONFIG_OPCODE_MODEL_SUBSCRIPTION_ADD
4、模型appkey的绑定,CONFIG_OPCODE_MODEL_APP_BIND
5、netkey的更新,CONFIG_OPCODE_NETKEY_ADD
我们可以看到当接收到CONFIG_OPCODE_APPKEY_ADD操作码的数据,会调用handle_appkey_add()函数去处理,在这个函数中我们最终会调用dsm_appkey_add()函数去存储我们的appkey。
8 ⑤Element元素
元素简单的说就是定义了节点的功能,一些具有复杂功能的节点设备,会包含多个元素。一个节点设备最少需要包含一个元素,我们称之为主元素。
从主元素开始(如果有多个元素,则第一个元素就是主元素),我们会给它分配相应的元素地址。主元素的地址就是节点的地址,第二个元素的地址就是主元素的地址基础上加上1,依次类推。
可以看到在我们的主元素Element:0x0002中包含了3个不同的服务,这3个服务就是我们的模型Model,其中Configuration Server与Health Server这两个服务,是每一个节点都必须携带的模型。Configuration Server(配置服务)是用于配置节点设备信息,Health Server(健康服务)是用于节点进行网络内的心跳联系。剩下的Generic On Off Server服务,这个就是我们用于控制一个开关量的模型(在例程中我们通过控制LED的IO口高低电平来展示)。
在我们的代码中,我们首先需要在nrf_mesh_config_app.h中定义好我们节点设备的最大元素数量,可以看到我们定义#define ACCESS_ELEMENT_COUNT (1),也就是说我们当前的例程,只含有一个主元素。
而这个ACCESS_ELEMENT_COUNT ,是在我们开启节点配置的地方被调用的,这个mesh_provsionionee_prov_start()函数在前面的内容中有多次讲到,这里就不再说明了。
9 ⑥model模型
如果说元素是一个功能集合,那么模型就是元素下的具体的某一个功能。
我们以灯的控制为例,灯的常用控制有开关、亮度、色温几种,其中开关、亮度和色温就是具体的模块,而处理灯控的集合就是元素。
9.1 Configure Server
配置服务模型,这个的大概代码处理流程在我们的“④设备信息分配->Appkey”已经有了说明,它是我们节点设备必须包含的一个特殊功能的模型。
它的实际功能就像它的名字一样,就是用于配置(configuration)我们的设备信息,比如appkey的添加删除,密钥的绑定删除,其他的模型的发布和订阅控制。具体包含的操作码的使用方法,我们会在generic on off server模型当中使用的时候来说明。
9.2 Health Server
健康服务,顾名思义就是判断我们的设备是否在网络中保持健康(正常)工作,它同样是一个必须包含的特殊功能的模型。
它的初始化和config server是一样的,都是放在了mesh_stack_init()函数下。
我们来看下health_server_init()函数,最终我们会调用access_model_add()函数去初始化模型,并将模型添加到对应的元素中。
其中的access_model_add_params_t参数,包含的分别是元素的索引(将模型添加到第几个元素当中),模型的ID(由mesh协议手册确定好了),以及最最重要的操作码列表,这个列表中会包含我们的指令码,以及对应的函数功能处理。
最后我们看下具体包含了哪些健康模型操作码:
1、“运行状况当前状态”消息的操作码,HEALTH_OPCODE_CURRENT_STATUS
2、“ 健康故障状态”消息的操作码,HEALTH_OPCODE_FAULT_STATUS
3、“ 获取健康注意事项”消息的操作码,HEALTH_OPCODE_ATTENTION_GET
4、“ 健康注意集”消息的操作码,HEALTH_OPCODE_ATTENTION_SET
5、“未确认健康注意事项”消息的操作码,HEALTH_OPCODE_ATTENTION_SET_UNACKED
6、“健康注意状态”消息的操作码,HEALTH_OPCODE_ATTENTION_STATUS
7、“健康故障清除”消息的操作码,HEALTH_OPCODE_FAULT_CLEAR
8、“健康故障清除未确认”消息的操作码,HEALTH_OPCODE_FAULT_CLEAR_UNACKED
9、“健康故障获取”消息的操作码,HEALTH_OPCODE_FAULT_GET
10、“健康故障测试”消息的操作码,HEALTH_OPCODE_FAULT_TEST
11、“未确认健康故障测试”消息的操作码,HEALTH_OPCODE_FAULT_TEST_UNACKED
12、“健康期获取”消息的操作码,HEALTH_OPCODE_PERIOD_GET
13、“健康期设置”消息的操作码,HEALTH_OPCODE_PERIOD_SET
14、“未确认健康期设置”消息的操作码,HEALTH_OPCODE_PERIOD_SET_UNACKED
15、“健康期状态”消息的操作码,HEALTH_OPCODE_PERIOD_STATUS
在大家的前期使用中,一般也不会真的需要大家去处理health server下的指令,毕竟我们刚开始使用,也很难让设备在网络中出现各种运行问题(节点少功能少)。但是当大家自己开始开发实际功能时,就一定要注意分析health server下的指令码返回,用来快速解决我们网络中的问题。
9.3 Generic On Off Server
我们来看下Generic On Off Server的