nrf9160做主控连接阿里云(mqtt_simple例程)——附MQTT协议浅解

简介:基本每一个云都支持MQTT,这种轻量级协议在数据量不大的应用上是一个很好的选择。上一篇博客使用SLM例程去连接了阿里云,本次使用mqtt_simple去连接云进行测试,关于一些已近在前面文章中演示过环境配置就不在赘述了,mqtt_simple例子只能使用MQTT的方式去连接云,没有像SLM那样可以使用AT指令通过各种方式(http、https、mqtt)去连接云。

在开始之前依然把我们需要的软硬件列举一些:

前期准备:

1、nrf9160的官方开发板或者9160的模组一块;

2、物联网卡一张

3、官方的mqtt_simple例程和工具,这个在nordic官方都可以下载,如果你是一次接触,那么可以看一下我前面的博客,或者直接去看官方的中文博客,去下载安装好NCS还有开发环境,中文官方博客连接:开发你的第一个nRF Connect SDK(NCS)/Zephyr应用程序 - iini - 博客园 (cnblogs.com)

下面依然走一遍流程(在其余博客中有详细演示的就只简单带过,如果不知道可以在其余博客中去寻找答案)

注:本次主要使用了1.8版本的SDK(NCS),到本文编写时最新的NCS-V2.1版本的也进行过测试,没有问题。

一、给nrf9160下载固件

1、官网下载modem固件

去官网下载modem固件,注意使用和NCS版本对应的modem固件:

下载完成后你会得到一个如下图所示的压缩包(该压缩包对应NCS-V1.8版本)。

2、烧写modem

然后我们使用nordic的PC端工具programmer进行下载modem固件,把nrf9160的开发板或者模块连接到PC端,然后打开后如下(我使用的是DK板,所以显示为DK,如果你使用的是模块可能不一样),然后我们点击连接,等待识别完成。

可以点击擦除一下,然后拖拽刚刚下载好的modem固件压缩包到工具界面,然后进行下载。

 这里依然提醒一下:modem固件的存放位置路径不要有中文,如果出现有中文,会导致无法下载成功。

等待片刻后下载成功如下,关闭即可:

二、注册阿里云设备

本次实验依然采用的是阿里云的免费物联网云进行测试,接入方式依然为一机一密方式,在阿里云文档中的阿里云物联网平台有相关的文档介绍:

2.1、注册打开物联网平台

(这一小节阿里云设备建立即为nrf9160做modem——测试连接阿里云 - 星辰_stars - 博客园 (cnblogs.com)中的流程)

在浏览器中搜索阿里云(https://www.aliyun.com/?utm_content=se_1012440659),如果你没有注册过,请注册然后登陆,登陆后在搜索框中输入物联网平台,然后搜索。

 在跳转的界面点击进入控制台

 跳转到如下界面,点击公共实例

 在跳转的界面如下操作开始创建产品

 2.2、创建物联网产品

在点击创建产品后,在产品创建界面创建自己的设备

1)、设置名字为nrf9160_test

 2)、选择所属类别

你可以根据自己的需要选择标准品类,或者自定义,我本次选择标准品类,然后选择任意一个类型

3)、节点类型

这里必选选择直连设备

 4)、连网与数据

联网选择蜂窝,数据必须为ICA的JSON格式

 5)、认证方式

选择为设备秘钥方式

设置以上选项,本次测试在次创建一个产品设备,本次创建的设备信息如下(区别于上一篇博客的是,为了方便后续讲解在联网方式上选择了wifi,如给你是物联网设备(在板子上需要SIM卡的)请你依然选择蜂窝方式):

 在产品界面我可以看到我们创建好的产品:

2.3、在产品中创建物联网设备

点击管理设备:

 

 然后添加设备:

 这次随意添加一个设备T123:

然后我点击产品界面,test产品的查看,发布一下我们的产品(不理解这一步的可以看一下前面的文章)

三、根据三元组计算链接参数——MQTT的CONNECT(连接服务端)报文

3.1、获取三元组

重要:在创建好设备后就可以获取设备的三元组,然后根据三元组和阿里云的要求计算获得连接参数写到mqtt_simple程序中,即可进行程序连接了,有些云不一定需要,不同的云可能有不同的要求,可以咨询提供云服务的官方或者参考相关云的官方文档。

点击设备,找到刚刚创建的设备,然后点击Devicesecret可以获取到我的三元组信息

 然后一键复制粘贴到剪切板

3.2、MQTT协议CONNECT报文命令解析——并计算连接信息

讲到这想要记录一下我学习的MQTT协议连接命令——CONNECT连接服务端命令,可以便于我们在出现连接错误有不知道为什么时,可以抓包进行分析,这一步我觉得是很有必要的。下面就让我们来一起学习一下。

MQTT报文一共有14条,下面附图,在本节主要讲解CONNECT报文:

表3.1:

 本节参考了MQTT 协议 3.1.1 中文版,在现有的NCS中使用的也是mqtt-V3.1.1版本。

由上图可知CONNECT报文一共由三个部分组成分别是固定报头、可变报头、负载

在这之前我们先来了解一下MQTT的消息质量是三个等级(句号后的黄色部分是作者自己理解的,如有错误欢迎指正):

  • QoS 0:消息最多传递一次。如果当时客户端不可用,则会丢失该消息。只要发送了一条消息就不再关心它有没有发送到对方,也不设置任何重发机制
  •  
  • QoS 1:消息传递至少 1 次。包含了从传机制,如果服务器来不及应答,就会导致客户端端超时,再次发送一次消息,服务端每次对没一个消息都要回复,在有多条消息时,不会像QoS2一样去和客户端确认说,你连续发了这么多,是都要执行嘛,还是只执行一次,多发的是误发


  • QoS 2:消息恰好传送送一次。相同的命令值希望执行一次,不会由于如QoS1一样导致说,本来只想执行一次的信息,多执行了几次,由于有了这个保证,要完成这个机制(多一个应答机制),导致系统开销会大,但是保证了消息的精准性,QoS2质量的消息只有在比较重要的领域应用,如医疗、电力,金融等

QoS2因此是最高质量的消息,就如我们本次使用的阿里云占时还在不支持这个等级的消息。 

3.2.1、固定报头

1)第一字节:

 

 

 如截图,第一字节,一共8bit,分为两部分,4~7bit定义了每一包报文是一个什么类型,所有报文如表3.1中所示,下图是MQTT协议中对CONNECT报文的定义:

 

由此我们可以确定整个报文的第一字节为0x10(为了书写方便后续将不在写0x这个十六进制的标号,将直接使用10标识)。

2)第二字节(也有可能是第2到第3、或者2到4,或者2到5)

为什么会有不同的选择,这是由于在第一字节确定了本包报文是什么类型后,会在后续告诉对方后面的可变报头和负载一共有多少字节,当后面只有120个字节时可以用一个字节就表示好,但是当有500,或者1000时就不是一个字节可以表示的长度了(二进制表示方式,不理解的这里可以自己百度)。为什么会有500,或者1000的那么大的差别呢,这由于有些可选配置,如有需要可配置进去(然后再后面的可变报头给对应的bit位写1,表示我要使用改配置,那么服务端检测到该标志就可以知道说原来你本次有这个可选配置,我会在检测负载数据时进行检测读取的),然后就是如果你设置设备名字或者密码等时给了一个很长的名字,那数据不就是增加了,所以才在这把长度搞成这样的可选,然后还经过特殊的设置让接收设备可以很好的知道本次报文这部分到低是用几个字节表示后面数据的长度,接下来我们会详细讲解一下,先把截图放在下面,这就是为什么剩余长度bute2...有三个点的原因:

 分别表示(每个字节的低 7 位用于编码数据, 最高位是标志位) :
1 个字节时(整个报文包的第2字节), 从 0(0x00)127(0x7f)
2 个字节时(整个报文包的第2字节和第3字节), 从 128(0x80,0x01)16383(0Xff,0x7f)
3 个字节时(整个报文包的第2、3、4字节), 从 16384(0x80,0x80,0x01)2097151(0xFF,0xFF,0x7F)
4 个字节时(整个报文包的第2、3、4、5字节), 从 2097152(0x80,0x80,0x80,0x01)268435455(0xFF,0xFF,0xFF,0x7F))

长度计算方式:

  每个字节只取前面7位表示数据,第8位表示有没有进位,如果第8位为1就表示有进位,长度还应该检查第3字节的前前7位来乘128,因为2的7次方为128(这里不明白为什么是2的7次方可以百度),同理第3字节的第八为如果也是1,那么就应该检查第4字节来加入计算,注意这里是乘于128*128,一直到第5字节:

  eg1:假设我们现在有的可变报头和负载一共有100(十进制)字节数据

    100(十进制)的十六进制为0x64——所以我们该部分只有0x64即可

  eg2:假设可变报头和负载一共有500个数据(十进制)字节数据

    500/128=3剩余116,那么116转化为0x74,但是由于有进位所以第8位应该为1,所以原本的0x74(01110100)第8位变1(11110100)0xF4,所以第二字节为0xF4,那么由于有进位就有第三字节,所以第三字节为0x03。

3)阿里云链接报文CONNECT的固定报头确定

有上面的讲解,我们可以确定本次链接报文的固定报头为(十六进制):

10 ?(问号的意思是现在还不知道我们本次可变报头个负载数据长度,我们最后添加)

3.2.2、可变报头

在MQTT协议栈中规定可变报头包含4个字段,分别为协议名(Protocol name)、协议级别(Protocol  Level),连接标志(Protocol  flags)、保持连接(Keep alive),下面我, 来分别看一下。

1)、协议名

这一共6个字节,是协议直接规定的,我们直接带入就行,每一字节数据如下:

  说明 7 6 5 4 3 2 1 0
byte1 长度MSB(0) 0 0 0 0 0 0 0 0
byte2 长度LSB(4) 0 0 0 0 0 1 0 0
byte3 “M” 0 1 0 0 1 1 0 1
byte4 “Q” 0 1 0 1 0 0 0 1
byte5 “T” 0 1 0 1 0 1 0 0
byte6 “T” 0 1 0 1 0 1 0 0

那根据协议规定我们可以得到如下的数据:

10 ?00 04 40 51 54 54

2)协议级别

用一个字节表示协议级别,前面有说过我们使用和参考的协议为MQTT-V3.1.1,那么他的标识就是0x04,

3)连接标志

用一个字节表示链接标志,其中每一位都有不同的意思,连接标志如图标所示

  • clean session:0——不清除设备的连接信息,全新的设备A第一次连接记录了一下信息Aq,那么第二次连接还是要有这些信息或者基于进行连接或者保留,1——每一次断开连接都清除连接记录
  • Will Flag:0——没有遗嘱消息,1——有遗嘱消息(如当设备1,2,3都订阅了遗嘱消息,当1设备突然断电,导致还没有发DISCONNECT报文就断开了(还有其余情况下的错误),那么2和3这两个链接到服务器的设备就会收到一条1的遗嘱消息)
  • Will QoS:用于指定发布遗嘱消息时使用的服务质量等级,0——(will flag=0时必须为零),当will flag=1时可以设置为0,1,2表示消息质量(如前面消息质量)
  • Will retain:遗嘱保留,0——不保留信息,掉线后从连无法获取信息,1——保留信息,掉线后重连可以获取信息(这一点我理解的是这样,不知道对不对,如有更好的解释可以在评论区讨论)
  • password flag:密码,0——在负载中不包含密码,1——在负载中包含密码
  • user name flag:用户名,0——在负载中不包含用户名,1——在负载中包含用户名

由于我们连接的是阿里云,阿里云要求必须是有用户名和密码的,不使用遗嘱消息,且有不保留信息,也就是要清除所以这一字节为(11000010)0xC2

4)保持连接

这一部分为两个字节,在实际连接中,要不间断的在规定时间内给服务器发送PING保活包,那这个规定时间内时间是多长时间,就在这个设定好,当服务器和你连接完成后,你没有其他任何控制报文时,需要在一定时间内发送PING包,如果你在这个时间内没有发送到PING包,那么服务器就认为你断开连接了。单位是秒。这里每个服务器在MQTT协议规定的最大时间内还可以规定自己的最大时间,本次测试就设定为100s(64s)内必须有PING包出现,不然就认为是断开连接,对于嵌入式设备来说这个时间越长越低功耗。

总结:

  • 客户端和服务器没有任何报文交流时,规定时间内要有保活包(PING报文)
  • 无控制报文交流的期间,客户端在任何时候都可以发送PING报文
  • 客户端发送PINGREQ(PING的一种)后,在合理时间(博主占时没找到说明)没有收到服务端来的PINGRESP(PING的一种),客户端要断开连接。
  • 保持连接值非零,设为T,那么在1.5T倍时间类服务端没有收到任何报文,服务端需要断开连接
  • 保持连接为零,服务端不需要因为客户端不活跃而断开连接

因此我们的数据包就变成了如下这样:

10 ?00 04 40 51 54 54 04 c2 00 64

3.2.3、负载(也就数据)

负载数据就是前面标志位中设定要包含的数据,全部有客户端标识符、遗嘱主题、遗嘱消息、用户名、密码,五部分组成,在这个值讲解三部分。这里先看一下阿里云对连接密码用户名和客户端标识符的要求

三元组:

1)、客户端标识符

阿里云参考规范:mqttClientId : clientId+"|securemode=3,signmethod=hmacsha1,timestamp=132323232|"

根据对参数的解释,clientId就为三元组中的T123,securemode由于选择一机一密所以已经固定,不要改默认就行,signmethod默认加密为hmacmd5没有改,后续的timestamp我们不需要设置省略掉

最终mqttClientId就为“T123|securemode=3,signmethod=hmacmd5|”

我们把这一串转换为十六进制为54 31 32 33 7C 73 65 63 75 72 65 6D 6F 64 65 3D 33 2C 73 69 67 6E 6D 65 74 68 6F 64 3D 68 6D 61 63 6D 64 35 7C 在再前面加上客户端ID的长度(两字节)最后变为:

00 25 54 31 32 33 7C 73 65 63 75 72 65 6D 6F 64 65 3D 33 2C 73 69 67 6E 6D 65 74 68 6F 64 3D 68 6D 61 63 6D 64 35 7C

2)、用户名

阿里云参考规范:mqttUsername:deviceName+"&"+productKey

用三元组件替换mqttUsername:T123&a1tETt7fUG1

变成十六进制:54 31 32 33 26 61 31 74 45 54 74 37 66 55 47 31 

加上长后为:00 10 54 31 32 33 26 61 31 74 45 54 74 37 66 55 47 31 

3)、密码获取

阿里云参考规范:mqttPassword:sign_hmac(deviceSecret,content)

这里需要用到加密算法hmacmd5使用三元组中的DeviceSecret做为秘钥对clientId*deviceName*productKey#加密后作为密码——其中*号为设备名称,#为ProductKey,注意替换

即clientIdT123deviceNameT123productKeya1tETt7fUG1,然后再网页上打开一个网页版加密工具:在线加密解密 - chahuo.com

 由此获得我们的密码:86a087f11cad5c325127ae5f79305109,经过转化后并加上两字节长度信息后:

00 20 38 36 61 30 38 37 66 31 31 63 61 64 35 63 33 32 35 31 32 37 61 65 35 66 37 39 33 30 35 31 30 39

由此我们来组合一下我们的CONNECT报文包

10 ? 00 04 4D 51 54 54 04 C2 00 64 00 25 54 31 32 33 7C 73 65 63 75 72 65 6D 6F 64 65 3D 33 2C 73 69 67 6E 
6D 65 74 68 6F 64 3D 68 6D 61 63 6D 64 35 7C 00 10 54 31 32 33 26 61 31 74 45 54 74 37 66 55 47 31 00 20 38
36 61 30 38 37 66 31 31 63 61 64 35 63 33 32 35 31 32 37 61 65 35 66 37 39 33 30 35 31 30 39

那么由此我就可以知道问号后面一共多少个字节了就是后面的绿色和黑色部分字节长度一共101(65)

因此整个报文信息就为:

10 65 00 04 4D 51 54 54 04 C2 00 64 00 25 54 31 32 33 7C 73 65 63 75 72 65 6D 6F 64 65 3D 33 2C 73 69 67 6E 
6D 65 74 68 6F 64 3D 68 6D 61 63 6D 64 35 7C 00 10 54 31 32 33 26 61 31 74 45 54 74 37 66 55 47 31 00 20 38 
36 61 30 38 37 66 31 31 63 61 64 35 63 33 32 35 31 32 37 61 65 35 66 37 39 33 30 35 31 30 39 

3.3、PC端工具连接测试

我们用PC端网络工具进行一下测试:

在测试前还需要知道云的地址,在阿里云这进行查看:

Url:a1tETt7fUG1.iot-as-mqtt.cn-shanghai.aliyuncs.com

port:1883

 可见服务器回复20 02 00 00,接受我们连接了(其中20,表示报文类型,20是回复包,02是剩余长度——后面还有两个字节,00相当于站位字节,最后以一个00,表示已经接受),这部分具体请看后续的CONNACK报文的讲解。

阿里云上也显示我们的设备在线:

如果连接包存在错误会是怎么样的结果,我们来看一下: 

 04提示我们无效的密码或者用户名,因为我们把原来的39改为了00,密码错误。

以上就是对CONNECT报文的讲解,那么我在上一篇博客使用了一个阿里云的计算器,其实就是完成上面我们复杂的计算,只要复制我们的设备信息,就可以一键生成我们的密码名称等。工具连接如下:阿里云参数小工具 (lovemcu.cn)这里值得注意的是每一个云的情况不一样,要去根据云的文档确定,但是报文格式是一样的。

上面是为了使用PC端工具进行连接,以便于我们更好的理解CONNNECT报文包,那下我们来使用nrf9160连接阿里云。

3.3.1、连接信息(有三元组计算得到)

本节获取的云连接信息如下:

clientid:T123|securemode=3,signmethod=hmacmd5|
username:T123&a1tETt7fUG1
password: 86a087f11cad5c325127ae5f79305109

4、MQTT协议CONNACK报文命令解析——服务器应答包

在第三节的测试过程中给服务端发送连接信息后,服务端会向我们客户端返回一个连接确认包,不管是否连接上还是被服务器拒绝连接时。该报文由两个部分组成、分别为固定报头,可变报头(该报文包没有负载数据)

4.1、固定报头

固定报头分两个字节、第一字节为报文类型、第二字节为本次报文的后续剩余字节长度

4.1.1、报文包类型字节

第一字节同样是说明本包报文是说明的,由前面表3.1我们可以知道在MQTT协议中CONNACK的值为2,所以第一个字节应该为0x20,因为只有4~7位是用来表示报文类型的,0~3是保留位,从下面MQTT协议中的截图就可知,属于协议规定,要用什么类型的报文第一字节就用对应的值就行

4.1.2、剩余长度字节

一般该部分固定为2,即该字节为0x02,为什么呢,因为在其后面只有可变报头了,协议规定CONNACK的可变报头只有两个字节,所以这里也把剩余长度固定了。

4.2、可变报头

可变报头由两字节组成,第一字节是连接确认标志,第二字节为连接返回码。

 4.2.1、连接确认标志

这一字节的1~7bit都是保留的,设置为0就行,为什么要保留,当然是等以后出现MQTT协议的版本更新,如果相对于现在是用的加入了一些功能,那么就可以启用这些保留位,给他们附上具体的意义了。

前面有说过,我们在连接报文CONNECT中可以选择这次连接是否需要服务端在我们断开后为我们保留好本次连接的一些信息,那如果选择了需要保存,就是CleanSession标志为零时不进行清除,那么第二次连接时,就可以通过这一个字节的告诉客户端的连接情况。如果上一次没有选择清除,即CleanSession=0,那么在上一次连接中有一些订阅以及推送,服务端把这些信息做了保存,那么SP就是1,如果没有保存那么SP就是0.

 4.2.2、连接返回码

由这一字节我们可以确定说我们CONNECT连接报文有没有连接到云,如果没有是什么原因,根据这我们就可以知道说当连接失败时,原因是什么。

现在对这最后一字节有一下这些参数选择:

返回码响应 描述
0 0x00 连接已接受 连接已被服务端接受
1 0x01 连接已拒绝,不支持的协议版本 服务端不支持客户端请求的 MQTT 协议级别
2 0x02 连接已拒绝, 不合格的客户端标识符

客户端标识符是正确的 UTF-8 编码, 但服务
端不允许使用

3 0x03 连接已拒绝, 服务端不可用 网络连接已建立, 但 MQTT 服务不可用
4 0x04连接已拒绝,无效的用户名或密码 用户名或密码的数据格式无效MQTT-3.1.1-CN 30
5 0x05连接已拒绝  未授权 客户端未被授权连接到此服务器

到这里我们是否就理解前面连接测试时,第一次连接成功后,服务器返回字符串的意思了呢(20 02 00 00,20是CONNACK,02表示后面还有两字节数据,00是连接标志,00是说服务器已经接受我们的连接),第二次故意使用错误的密码后,返回(20 02 00 04,其中04表示连接已经拒绝,原因是密码或用户名不对,因为问你更改了密码,肯定连接不上)。到这就讲解完成了CONNACK了,该报文没有负载数据包。

四、mqtt_simple程序修改(NCSV1.8)

使用vs code创建一个mqtt_simple工程,不会的请参看我前面的文章或者在本篇文章的顶部点击链接跳转到官方中文博客中进行学习,这就不细讲了,然后我们对程序进行修改。

1、prj.conf文件修改

1)、打开工程中的prj.conf配置文件,修改联网方式,中国只有NB网络,从LTE修改为NB

CONFIG_LTE_NETWORK_MODE_NBIOT_GPS=y

2)、加入PDN定义,为了把PDN的函数编译进来

CONFIG_PDN=y
CONFIG_PDN_LEGACY_PCO=y
CONFIG_PDN_SYS_INIT=y

编译后,对于1.8的NCS需要确定pdn.c文件中的AT%%XEPCO=0处为2个%分号,如果不是,请修改为

修改后如下:

 

然后再编译。

3)、加入连接参数和推送订阅的主题

CONFIG_MQTT_PUB_TOPIC="/a1tETt7fUG1/T123/user/get"
CONFIG_MQTT_SUB_TOPIC="/a1tETt7fUG1/T123/user/update"
CONFIG_MQTT_CLIENT_ID="T123|securemode=3,signmethod=hmacmd5|"
CONFIG_MQTT_BROKER_HOSTNAME="a1tETt7fUG1.iot-as-mqtt.cn-shanghai.aliyuncs.com"
CONFIG_MQTT_BROKER_PORT=1883

#对于端口1883和前面的等号不要有空格,这一点要注意

发布和订阅的主题需要在阿里云中,即我们前面建立的设备端下topic中去找一个有订阅和发布权限的类型即可(不知道在哪的可以去看上一篇文章)。

 如图中间的${deviceName}换为我们的设备名T123。

4)、用户名和密码缓冲区配置加入

如果不加入这,当你用户名和密码过长时会报-12的错误,因此我们给他大一点的空间

CONFIG_MQTT_MESSAGE_BUFFER_SIZE=512
CONFIG_MQTT_PAYLOAD_BUFFER_SIZE=512

2、程序修改

打开main.c找到client_init()函数

添加如下代码:

#define CONFIG_MQTT_BROKER_USERNAME_test  "T123&a1tETt7fUG1"
#define CONFIG_MQTT_BROKER_PASSWORD_test  "86a087f11cad5c325127ae5f79305109"
//这加成宏定义
//以下放置在client_init中
    struct mqtt_utf8 password_test={
        .utf8=CONFIG_MQTT_BROKER_PASSWORD_test,
        .size=strlen(CONFIG_MQTT_BROKER_PASSWORD_test)
    };
    struct mqtt_utf8 user_name_test={
        .utf8=CONFIG_MQTT_BROKER_USERNAME_test,
        .size= strlen(CONFIG_MQTT_BROKER_USERNAME_test)
    };

//以下直接修改
    client->password =&password_test;
    client->user_name =&user_name_test;

修改后:

如果你发现你这样定义后,程序依然报-12的错误,那么请改变一下你用户名和密码的定义方式,不是使用宏定义,而是使用数组的方式:

uint8_t password_test_21[50]="86a087f11cad5c325127ae5f79305109";
uint8_t user_test_21[50]="T123&a1tETt7fUG1";

    struct mqtt_utf8 password_test={
        .utf8=password_test_21,
        .size=strlen(password_test_21)
    };
    struct mqtt_utf8 user_name_test={
        .utf8=user_test_21,
        .size= strlen(user_test_21)
    };

    client->password =&password_test;
    client->user_name =&user_name_test;

3、mqtt_simple链接阿里云

 然后我们就编译下载:

 连接成功后,在阿里云端可以看到设备在线:

 

——未完待续,后续会继续完善这篇博客,欢迎大家指正

posted @ 2022-10-29 17:37  星辰_stars  阅读(931)  评论(0编辑  收藏  举报