打开主菜单

谷雨文档中心 β

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公司买下了使用的版权,所以是免费的许可证。

假如没有弹出的对话框中没有显示Activate Your Free License,可以直接访问https://license.segger.com/Nordic.cgi注册,后续过程一致。

我们按照要求填写好信息,主要是邮箱地址(用于接收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个开发板分别对应client、server以及provisioner三个类型。

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服务,这个服务和上方的两个服务不太一样,他不是我们的特殊必要服务,是一个通用的模型(用于具体的功能数据的处理,这里是用于开关功能)。借用这个服务模型,我们给大家介绍一下如何创建一个符合BLE MESH协议的模型。

我们看下模型的初始化方法,在generic_onoff_server_init()函数中,首先我们使用access_model_add_params_t定义一个模型分配参数的结构体init_params,在这个结构体当中,我们要给定:

1、model_id(模型的ID),这个是mesh协议规定好的,我们可以查看“归档资料\8-蓝牙协议手册和芯片手册\蓝牙mesh协议手册\《MshMDLv1.0.1.pdf》”的第7.3章节有归纳

2、element_index(元素索引),这个看我们准备将这个服务添加到哪一个元素下,这里我们因为只有一个主元素,所以我们的元素索引为0

3、p_opcode_handlers(操作码列表,指令码列表),这个也是mesh协议规定好的,我们可以查看“归档资料\8-蓝牙协议手册和芯片手册\蓝牙mesh协议手册\《MshMDLv1.0.1.pdf》”的第7.1章节归纳,具体的操作码的功能及要求,大家需要对应这个pdf手册,自己查看一下,对于一个模型ID,我们不一定要注册所有的属于它的opcode,只要注册我们需要使用的即可

4、opcode_count(操作码数量),准备注册的操作码的数量

5、p_args(通用参数指针),用于参数的传递

6、publish_timeout_cb(发布超时时间),发布的定时器到期,会进入此回调,我们可以做一些处理(例如重新发送数据等等)。如果我们的模型不支持定期发布,可以将此值设置为NULL。

当我们配置好模型的参数结构体之后,会调用access_model_add()函数去初始化模型并添加到元素,同时会返回给我们用于存储此模型的句柄指针p_server->model_handle。

因为我们的onoff模型,需要订阅消息,所以我们需要调用access_model_subscription_list_alloc()函数,携带我们的模型句柄去分配模型的订阅列表。

看完模型的初始化注册之后,我们来看下模型包含的opcode操作码,可以看到在m_opcode_handlers[]操作码列表中包含了三个opcode。这三个opcode我们可以看下他的定义,以及对照一下《MshMDLv1.0.1.pdf》第7.1章节的归纳,可以看到是一一对应的关系。具体这三个opcode的功能,大家可以查看手册中的说明,或者直接看下他们对应的功能处理函数。

接下来我们看下GENERIC_ONOFF_OPCODE_SET与GENERIC_ONOFF_OPCODE_SET_UNACKNOWLEDGED对应的handle_set,可以看到在这个函数的最后,我们会去调用status_send()函数去发送数据。

然后我们看下status_send()函数,在这个函数中,我们最终会调用access_model_publish来发布我们的数据,或者使用accsee_model_reply来回复数据。

这两个函数再往下就是我们之前在广播部分讲解的packet_alloc_and_tx函数去发送广播数据了,这个有不清楚的,大家可以到前面去查看一下。

9.3.1 model bind appkey

那么按照上方的方法注册好模型,然后我们的节点设备被成功的配置入网之后,我们如何让这个模型工作起来。

首先第一步是需要将我们的模型绑定到相应的appkey,只有这样我们的模型才能拿到用于通信的密钥,例如如下的截图当中,我们利用手机app软件nRF Mesh去给我们的Generic On Off Server模型绑定Appkey1。

当我们在使用app去绑定appkey的时候,在我们的代码中的体现是在config server下的opcode_handlers[]操作码列表中的CONFIG_OPCODE_MODEL_APP_BIND。

我们追踪一下CONFIG_OPCODE_MODEL_APP_BIND对应的handle_model_app_bind_unbind(),这个函数比较长,这边只给大家截取了部分,在为截取到部分,还有获取元素索引的get_element_index,获取模型ID和模型handle的access_handle_get,然后才是我们的dsm_appkey_index_to_appkey_handle函数用于将appkey索引给到appkey句柄。然后调用access_model_application_bind函数去绑定模型和appkey,最终如果是成功绑定,则调用access_flash_config_store函数去存储修改的绑定信息。

9.3.2 model publish address

当我们完成了模型的appkey绑定之后,我们就可以开始设置puhlish的地址以及subscription的地址。

这里我们先看下nRF Mesh配置publish Address,可以看到我们选择的定义的0xC000组地址。

我们可以看到,在我们配置publish地址的时候,RTTViewer中会打印黄色标注的操作码数据信息,可以看到他的OPCODE是0x03。

我们来看下这个操作码对应的功能,可以看到CONFIG_OPCODE_MODEL_PUBLICATION_SET操作码对应的功能函数是handle_config_model_publication_set,根据函数的名称我们就可以看出,是用于配置模型发布的。

在handle_config_model_publication_set函数中,可以看到我们先使用dsm_appkey_index_to_appkey_handle函数获取了appkey的句柄。然后调用dsm_address_publish_add函数去添加发布的地址。

9.3.3 model subscription address

然后我们再看下订阅地址的添加,可以看到我们使用nRF Mesh配置subscription订阅了组地址0xC001。

可以看到,当我们分配subscription地址的时候,可以看到RTTViewer中打印黄色标注的信息。可以看到他的OPCODE是0x801B。

我们来看下这个操作码对应的功能,可以看到CONFIG_OPCODE_MODEL_SUBSCRIPTION_ADD操作码对应的功能函数是handle_config_model_subscription_add,根据函数的名称我们就可以看出,是用于配置模型订阅的。

在handle_config_model_subscription_add函数中,我们会调用dsm_address_subscription_add函数去添加我们的订阅地址。

10 Mesh天猫精灵实验

10.1 天猫精灵介绍

天猫精灵(TmallGenie)是阿里巴巴人工智能实验室(Alibaba A.I.Labs)于2017年7月5日发布的AI智能产品品牌,当天同步发布了天猫精灵首款硬件产品——AI智能语音终端设备天猫精灵,未来还将推出更多AI智能产品。

天猫精灵内置AliGenie操作系统,AliGenie生活在云端,它能够听懂中文普通话语音指令,目前可实现智能家居控制、语音购物、手机充值、叫外卖、音频音乐播放等功能,带来人机交互新体验。依靠阿里云的机器学习技术和计算能力,AliGenie能够不断进化成长,了解使用者的喜好和习惯,成为人类智能助手。

10.2 AliGenie平台介绍

阿里精灵AliGenie平台地址:www.aligenie.com

IOT接入平台控制中心:iot.aligenie.com/home

IOT平台接入规范手册:[/www.aligenie.com/doc/357554/tgllbp https://www.aligenie.com/doc/357554/tgllbp]

天猫精灵IoT开放平台,是阿里巴巴人工智能实验室(Alibaba A.I.Labs)面向品牌商、方案商、模组商/芯片商以及个人开发者推出的,将IoT物联网技术(蓝牙Mesh协议、WiFi协议、天猫精灵IoT云服务)和AI(天猫精灵ASR语音识别、NLP自然语言处理、TTS语音合成)等对外输出的开放式平台。

开发者可按直连接入(WiFi模组、蓝牙Mesh模组)、云云接入(OAuth2.0)2类方式,接入天猫精灵软硬件生态(天猫精灵音箱、天猫精灵App、AliGenie Inside智能设备)及阿里巴巴集团生态服务,实现语音和触屏交互,为用户提供天猫精灵IoT控制、查询、播报和主动服务。目前已支持40+个IoT类目,1000+型号。天猫精灵IoT开放平台会持续创新,不断为开发者带来新技术,同时降低平台开发者进驻门槛,让AI普惠大众!

10.3 硬件连接

1、NRF52832DK连接Jlink-Lite仿真器,并连接到电脑USB

2、天猫精灵(方糖R)接电源供电,根据天猫精灵使用手册,将其连接上家里的WIFI

10.4 平台设备创建

1、  登录IOT接入平台控制中心:iot.aligenie.com/home,大家先点击右上角注册一下,因为是阿里巴巴旗下的产品,所以我们只需要用我们的淘宝账号就可以完成注册。

2、  注册完成之后,我们登入平台,并且创建一个蓝牙mesh设备,如下图所示:

3、  记录刚刚平台分配给我们的三元组,任意一个就行,不需要所有的,举例如下:

Product ID Device Secret Mac 地址
9244 0855f693ce1246f3e09653603283dc02 f8a7635b7683

10.5 软件配置

将例程“ghostyu-tmjl”解压到mesh SDK的如下路径中,并且确认编译无误:\nrf5SDKforMeshv310src\examples\ghostyu-tmjl

10.5.1 三元组配置

三元组具体说明可以查看https://www.aligenie.com/doc/357554/gtgprq

举例如平台设备创建第三步记录的三元组:

Product ID9244 ,转成16进制就是0x0000241C ,所以最终数据是{1c 24 00 00}(4字节、低位在前)

Mac地址f8a7635b7683,所以最终数据是{83 76 5b 63 a7 f8}(6字节,低位在前)

AUTH Data:利用三元组的数据以及SHA256计算器得到我们的加密数据,如下图所示,最终我们得到加密数据为:{ 0x73,0x76,0x09,0x93, 0x8d,0x6b,0x6a,0x52, 0x42,0x54,0xe5,0x41, 0x57,0xe4,0x75,0x15}

SHA256计算器https://www.jisuan.mobi/pmHbB1bmuum16Xxx.html

10.5.2 代码修改

根据刚刚三元组配置产生的数据,修改mian文件中对应的值。

10.5.3 软件烧写

修改好代码之后,我们依次进行如下步骤操作,注意不可跳跃任意步骤。

1、利用nrfgo工具擦除芯片

2、下载正确的softdevice

3、编译天猫精灵例程(注意:已经完成“代码修改”步骤)

4、将编译好的程序下载到开发板中

10.6 调试阶段

10.6.1 确认设备状态

在进入调试之前,我们确认一下设备状态,设备状态正确后,进入调试流程。

1、  按下开发板复位按键,4个LED灯闪烁2次后熄灭

2、  对天猫精灵说“天猫精灵,你联网了吗”,等待天猫精灵回复“我已联网”

10.6.2 调试流程

1、  对天猫精灵说“天猫精灵,找队友”,天猫精灵回复“正在查找智能设备”

2、  如果我们前面的软件配置是对的,过一会天猫精灵会回复“找到智能灯设备,是否连接”;如果配置有问题,天猫精灵找不到设备会回复“未找到智能设备”,这个时候我们需要检查软件配置流程中的product ID和Mac地址

3、  若天猫精灵回复“找到智能灯设备,是否连接”,我们回答“连接”,天猫精灵会回复“正在连接智能设备,请稍等”

4、  等待1段时间(1min以内),如果回复“智能设备连接成功”,则代表成功连接开发板;如果回复“连接失败”,则检查软件配置流程中的Data Auth密码

5、  如果连接成功,我们按下开发板按键S2(注意:复位开发板后,都需要重新按下S2

6、  我们对天猫精灵说“天猫精灵,打开灯”,这个时候开发板上D1会点亮;如果如果说“天猫精灵,关闭灯”,这个时候开发板上D1会熄灭

7、  到第6步结束,已经完成测试,接下来我们也可以到平台上调试一下。此时我们只能测试“打开灯”以及“关闭灯”功能。

11 Mesh串口透传实验