<!--

前言

通过前面的理论铺垫,我相信大部分读者应该对SIG MESH有了一定的了解了吧;因为前面的几个章节都比较偏向于理论,所以该章节我们配合示例工程来巩固并加深对SIG MESH的一些概念以及流程的了解。

Light Switch Server

本章小编选择最简单的标准模型Generic On Off为例,将前面讲解到的理论知识在本示例工程中尽可能多地穿插讲解。在开始之前,我们先看看主函数由哪几部分组成:

initialize

首先,app_timer以及硬件相关初始化就没有什么好说的了。前者,就是定义一个低功耗定时器;后者,就是硬件相关的初始化;因此,我们这里着重看看GAP以及连接参数和MESH初始化是做了些什么事情。

GAP以及连接参数初始化

start

接下来我进入看起来很简单但却是最重要、最复杂的步骤,该函数完成了我们MESH启动时的一些关键参数、相关事件的回调处理函数地填充以及开始启动Mesh协议栈等等动作;废话不多说,先上代码。

该函数主要是填充启动配置时所使用的相关参数及事件回调处理函数,其中会携带16字节的静态认证数据用于在认证阶段时使用,但是就算这里填充了,在后继的认证阶段也可以不用,这个取决于Authentication method字段的内容。至于各个回调函数的作用,如下所示:

配置完上述的设置之后,就可以调用 mesh_stack_start() 函数开启并响应mesh的相关动作行为了。然而,我们需要注意的是 mesh_stack_start() 仅仅开启了响Mesh协议栈的相关行为,例如:响应Mesh的事件并触发相应的回调函数。而Mesh的Beacon则是在 mesh_provisionee_prov_start被调用之后,它才会被广播出去。

入网过程细述

基本上该示例工程的框架就是这样,先初始化,包括硬件、BLE以及BLE Mesh->填充MESH协议栈开放给应用层相关的回调处理函数以及启动MESH的相关事件行为;正常来说,我们只需要好好处理在start() 中的回调函数即可。然而,如果仅仅是如此的话小编认为这是完全不够的:

让我们再次回顾一下基于PB-GATT的整个入网过程是怎么样的。

Beacon

显然,在开始入网之前unprovisioned device应该发出unprovisioned beacon;但是,我们在上面的分析过程中并没有看到有接口让应用层设置Beacon的信息内容;万一我们非要强行修改呢?又或者说我们需要修改beacon之间的间隔呢?那么我们带着这些疑问在下面这个函数中找找答案:

我们从上述函数的关键字static就知道,一般情况下我们是无法直接调用的,也就是说其不对外开放给应用层使用。由prov_provisionee_listen()函数间接调用。但是,我们基本上可以知道是哪个函数开启了unprovisioned beacon。同时,我们也了解到beacon的间隔是2秒(这点我们可以从NRF_MESH_PROV_BEARER_ADV_UNPROV_BEACON_INTERVAL_MS可知),当然我们从捉包中也可以验证这么一点:

讲到这里,如果下次我们要修改unprovisioned beacon的间隔就可以通过修改宏NRF_MESH_PROV_BEARER_ADV_UNPROV_BEACON_INTERVAL_MS实现;至于,unprovisioned beacon的payload我们能修改的也只有URI以及OOB information两项内容了。基本上 send_unprov_beacon() 函数只是拼装并发出beacon信息,我们可以通过下图验证拼装的beacon数据包:

入网过程

继beacon信号出来之后,接下来provisioner捕获到unprovisioned beacon之后,就要开始发出邀请等一序列的操作。那么Nordic Mesh SDK又是在哪里处理并响应这些事件呢?同样的,让我们带着这样或者那样的疑问来看看下面的事件回调函数:

显而易见,整个入网过程中所触发的大部分事件均在prov_provisionee_pkt_inprov_provisionee_cb_ack_received两个回调函数中处理。至于,接下来所涉及的不同PDU的帧格式详情,请参考PB-GATT入网过程

邀请

当unprovisioned beacon被provisioner成功捕获到之后,就会向其发出邀请的PDU,那么发具体接收到的Invite PDU具体又是什么内容呢?让我们一起来看看:

其中,接收到的邀请PDU与捉包所捕获的完全一样:

我们从上述的两幅图中可知,其中pdu_type为0表示当前的类型是PROV_PDU_TYPE_INVITE,而attention_time则为provisionee用于产生任何可以引起注意的动作时长。

Capabilities响应

接收到provisioner的邀请之后,provisionee必须在NRF_MESH_PROV_LINK_TIMEOUT_MIN_US秒内(最小为60秒),将自身支持的Capabilities告诉provisioner,否则入网失败,provisioner会主动断开连接。那么,在SDK工程中的哪里回复或者填充这些内容呢?至于Capabilities PDU格式及含义,我们在PB-GATT入网过程已经细述,这里我们不再重复。

从上述内容可知,send_capabilities() 回复了provisioner的邀请PDU,同时我们也应该注意到该函数是由关键字static修饰的;因此,我们不能直接在该函数修改相对应的capabilities参数。那么,这些参数又是从哪里传进来的呢?我们可以在mesh_provisionee_prov_start() 函数中找到答案:

mesh_provisionee_prov_start()函数中定义了一个nrf_mesh_prov_oob_caps_t类型的变量prov_caps,所以在send_capabilities() 中的关于Capabilities均在这里传过去的,也就是说数据的源头在mesh_provisionee_prov_start() 函数。此时,如果我们想要修改provisionee的Capabilities就可以修改prov_caps变量值即可。

Provisioning Start

由上述的入网过程示意图可知,该PDU由Provisioner向provisionee发送,其主要作用就是根据provisionee回复的capabilities内容,决定接下来的公钥交换和认证采用哪种方式,具体携带参数如下所示:

我们可以看出这里有No OOBOOB两个选项,这里也就验证了我们在start中描述的static_auth_data;前者则直接进入公钥交换过程紧接着进入下一个阶段,而后者则进入公钥交换过程后,还需要再进行静态OOB认证才会进入一个阶段。至于是Static、Input、Output的OOB认证方式,则完全取决于Authentication method,而Authentication method又取决于provisionee回复provisioner的Capabilities的内容,它们之间是相到关联的。

还有一点,小编觉得需要说明的是上述的内容跟OOB Public Key是两个不同的概念,前者用于交互公钥之后的认证而后者则是决定是否采用OOB的方式来交换公钥

Provisioning public key

此时,provisioner率先发送其生成的64Bytes的公钥给provisionee,而provisionee则回复其生成的公钥给provisioner,而provisionee的公钥和私钥则是在provisionee_start() 函数中生成。

这里我们应该注意的是生成的公钥和私钥都是随机的,也就是说如果该次入网失败,那么再次入网时生成的公钥和私钥均跟上次不同。

Provisioning Input Complete

这个也是认证的方式之一,当provisionee输入相关用于认证的数据之后,就会向provisioner发送该pdu,而是否激活该认证则取决于Authentication method

ValueDescription
0x00No OOB authentication is used
0x01Static OOB authentication is used
0x02Output OOB authentication is used
0x03Input OOB authentication is used
0x04-0xFFProhibited

Provisioning Start可知,当静态OOB认证方法被选中时,那么provisioning start PDU就会将Authentication method的值置为0x01。当然,这是要根据provisionee回复的Capabilities,从而做出相对应的选择。

!!! 说到oob_input_actions,那么小编这里就不得不提一下oob_output_actions,根据小编的调试发现当authentication method为0x02,且相对应的Output OOB Action为Output Numeric时,当前的Mesh SDK转换出来的Authentication value有误,导致入网时输入认证值一直提示认证值错误,后来小编凭借精湛的技术偷偷把这个Bug修复了😄

Provisioning confirmation

当程序运行到Provisioning confirmation交互时,入网过程已经即将接近尾声了。那么,互发Provisioning confirmation的作用是什么呢?看过小编上一章节写的PB-GATT入网过程就可以知道,该PDU主要将目前为止provisionee和provisioner交互过的所有PDU和OOB认证生成的authentication value以及即将产生的random加密后生成confirmation value希哈值,对方收到之后就会将该值暂存,用于下一个阶段收到Provisioning Random值之后,再使用同样的方法将目前为止收到的所有PDU和OOB认证生成的authentication value再加上收到的Random值,计算得出confirmation value的哈希值与接收的进行对比。而SDK是在哪个函数实现这个功能的呢?

正所谓一图胜千言,上图所述的内容基本上将 prov_utils_authentication_values_derive() 函数中所表达的意图已经阐述了,同时也将整个Provisioning Confirmation的内容也涵括了。

Provisioning random

现在我们再看这个PDU时,我们就可以很好的理解了甚至有种豁然开朗的感觉。此时,当收到provisioner的random值后,就会利用该值计算出confirmation value并且与接收到对比,如果对比成功,则开始分发网络秘钥等相关的入网通行证内容;否则,告诉应用层入网失败的原因。

上述的文字说明就是阐述了上述函数的整个过程,再结合Provisioning confirmation,我们可以很清楚地了解到在provisioning random匹配正确之后,才会将自己的random发送给provisioner;同样的,provisioner使用同样的方法匹配正确之后,才会开始发送Provisioning data的内容。还有一个重要的点就是,当随机数发送完毕紧接着provisionee会生成Session Key、data_nonce、Device Key,其中前面两者用于解密Provisioner发送的Provisioning Data的内容,而Device Key仅节点和Configuration Client知道,用于加解密节点和Configuration Client的通信。例如,发送NODE RESET命令。

!!!注意: 上述中有提到这个Nonce,可能很多人不太理解这个是什么意思?起初,小编也不理解这个是什么鬼玩意。但是,后继搜索得到:

Nonce,Number used once或Number once的缩写,在密码学中Nonce是一个只被使用一次的任意或非重复的随机数值,在加密技术中的初始向量和加密散列函数都发挥着重要作用,在各类验证协议的通信应用中确保验证信息不被重复使用以对抗重放攻击(Replay Attack)。---出自《百度百科》

Provisioning Data

前面Provisioning random我们也提到,当provisionee接收到provisioner的provisioning data时,它会使用session key和data_nonce来解密,从而获取Network Key相关的内容,如下所示:

FieldSize(octets)Notes
Network Key16NetKey
Key Index2Index of the NetKey
Flags1Flags bitmask
IV Index4Current value of the IV Index
Unicast Address2Unicast address of the primary element

而所有这些解密的工作均在函数handle_data() 中处理。

最后,解密且认证provisioning data通过之后,provisionee给provisioner发送provisioning complete数据包,否则发送入网失败的原因。至此,整个入网过程完成结束,接下来就可以愉快地进行模型之间的通信了。

Provisioning Complete or Failed

参考Provisioning Data

应用层处理函数

入网过程结束之后,我们就可以通过模型Generic On Off来控制设备了,而本篇章节所述的Light Switch Server示例工程中,相对应的控制回调函数如下所示:

最后

通过上述大篇幅的描述,我想读者此时应该对入网过程已经了如指掌了,同时也对前面所述的几个篇章内容有了更加深刻地认识。在小编看来,起码以后对入网这个过程再也无须担心了,一切都在掌控之中。