合宙Air601连接阿里云物联网平台-MQTT协议学习

康师傅 Lv3

合宙Air601连接阿里云物联网平台-MQTT协议学习

开发板9块9包邮)买来玩玩。

首先赞美一下其他模块有现成AT/MQTT固件,因为Air601刚出来不久,现在还没有AT/MQTT固件,所以要TCP连接后手搓MQTT协议,就当学习了~

鼓捣了一晚上,记录记录过程~

一些官方介绍:

Air601-12F 是合宙通信推出的 Wi-Fi - BLE二合一通信模块;
Air601-12F采用合宙Air601芯片平台,支持Wi-Fi 802.11b/g/n协议,支持BLE 4.2协议;
Air601-12F 兼容业内主流12F封装(SMD-22),板载PCB天线,极致成本,满足小型化低成本需求;
Air601-12F 支持AT指令开发,指令集兼容,可无缝替换。
Wi-Fi 安全支持 Wi-Fi WMM/WMM-PS/WPA/WPA2/WPS;
支持20/40MHz带宽,最高支持150Mbps速率;
支持 Station 、Station + SoftAP 、SoftAP 模式;
支持TLS加密通信,硬件加密模块加速,支持多路TLS连接;
支持fota空中升级;
支持低功耗休眠,休眠电流小于20 μA

准备工作

获取AT指令手册

上海合宙WIFI模组(Air601系列)AT命令手册V1.0.3.pdf

因为直接买的开发板,所以开箱即用。使用type-c数据线,通过串口进行操作。

具体指令信息可以参考手册,这里只记录必要过程。

连接wifi

1
AT+CWJAP="WiFi名","WiFi密码"

若返回WIFI CONNECTED,则连接成功。

阿里云物联网平台创建好设备

比较简单就不演示了。

计算MQTT协议报文内容

MQTT(消息队列遥测传输)是一个基于客户端-服务器的消息发布/订阅传输协议。

MQTT中文手册

MQTT控制报文

MQTT控制报文由三部分组成:

报文部分 描述
Fixed header 固定报头,所有控制报文都包含
Variable header 可变报头,部分控制报文包含
Payload 有效载荷,部分控制报文包含

CONNECT 报文结构

建立客户端(Client)到服务端(Server)的网络连接后,客户端发送给服务端的第一个报文必须是 CONNECT 报文(连接到服务端)。
并且在一次连接中,只能发送一次 CONNECT 报文,服务端会将客服端发送的第二个CONNECT报文当做协议违规处理并断开连接。
CONNECT报文结构为:固定报头+可变报头+有效载荷。

固定报头

固定报文格式
Bit
7
6
5
4
3
2
1
0
byte1
MQTT控制报文的类型(1)
Reserved 保留位
byte2...
剩余长度

其中剩余长度=可变报头长度+有效载荷长度 ,这个在最后计算。

剩余长度详解

位置:从第2个字节开始。

剩余长度(Remaining Length)表示当前报文剩余部分的字节数,包括可变报头和负载的数据。剩余长度不包括用于编码剩余长度字段本身的字节数。

剩余长度字段使用一个变长度编码方案,对小于128的值它使用单字节编码。若剩余长度大于等于128字节,则使用双字节低端编码的方式(低八位 - 高八位),最多使用 4 个字节来表示长度:

字节数 最小值 最大值
1 0 (0x00) 127 (0x7F)
2 128 (0x80, 0x01) 16 383 (0xFF, 0x7F)
3 16 384 (0x80, 0x80, 0x01) 2 097 151 (0xFF, 0xFF, 0x7F)
4 2 097 152 (0x80, 0x80, 0x80, 0x01) 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F)

其中,每个字节的低七位用于编码数据,第八位延续位用于指示是否有更多的字节。因此每个字节可以编码127个数值(0111 1111)和一个延续位。

例:

  1. 小于128字节时:

    剩余长度98个字节(0110 0010)转换成16进制为 0x62。

  2. 大于128字节小于16384字节时

    剩余长度300个字节时,按照低八位 - 高八位排列,低八位的最高位为1(延续位)。

    300 = 1 0010 1100

    低八位:1(固定)+300低7位(010 1100) -> 1010 1100

    高八位:300低7位前(10) -> 0000 0010

    所以剩余长度表示为 AC 02

得到固定报头:

16进制,后面均为16进制,不再说明:

1
10 ??

其中??为剩余长度,由最后计算获得。

可变报头

某些MQTT控制报文包含一个可变报头部分。它在固定报头和负载之间。可变报头的内容根据报文类型的不同而不同。

CONNECT报文的可变报头按下列次序包含四个字段:协议名(Protocol Name),协议级别(Protocol Level),连接标志(Connect Flags)和保持连接(Keep Alive)。

协议名 Protocol Name
说明 7 6 5 4 3 2 1 0
byte 1 长度 MSB (0) 0 0 0 0 0 0 0 0
byte 2 长度 LSB (4) 0 0 0 0 0 1 0 0
byte 3 ‘M’ 0 1 0 0 1 1 0 1
byte 4 ‘Q’ 0 1 0 1 0 0 0 1
byte 5 ‘T’ 0 1 0 1 0 1 0 0
byte 6 ‘T’ 0 1 0 1 0 1 0 0

协议名是表示协议名 MQTT 的UTF-8编码的字符串。MQTT规范的后续版本不会改变这个字符串的偏移和长度。

如果协议名不正确服务端可以断开客户端的连接,也可以按照某些其它规范继续处理CONNECT报文。对于后一种情况,按照本规范,服务端不能继续处理CONNECT报文。

数据包检测工具,例如防火墙,可以使用协议名来识别MQTT流量。

协议级别 Protocol Level
说明 7 6 5 4 3 2 1 0
byte 7 Level(4) 0 0 0 0 0 1 0 0

客户端用8位的无符号值表示协议的修订版本。对于3.1.1版协议,协议级别字段的值是4(0x04)。如果发现不支持的协议级别,服务端必须给发送一个返回码为0x01(不支持的协议级别)的CONNACK报文响应CONNECT报文,然后断开客户端的连接。

连接标志 Connect Flags

连接标志字节包含一些用于指定MQTT连接行为的参数。它还指出有效载荷中的字段是否存在。

连接标志
Bit
7
6
5
4
3
2
1
0
User Name Flag
Password Flag
Will Retain
Will QoS
Will Flag
Clean Session
Reserved
byte 8
X
X
X
X
X
X
X
0

一共有6个:清理会话 Clean Session,遗嘱标志 Will Flag,遗嘱QoS Will QoS,遗嘱保留 Will Retain,密码标志 Password Flag,用户名标志 User Name Flag。

服务端必须验证CONNECT控制报文的保留标志位(第0位)是否为0,如果不为0必须断开客户端连接。

各个有效位的具体解释见参考文档

一般使用 1100 0010 (用户名、密码、清理会话),转换成16进制为:C2

保持连接 Keep Alive
Bit 7 6 5 4 3 2 1 0
byte 9 保持连接 Keep Alive MSB
byte 10 保持连接 Keep Alive LSB

保持连接(Keep Alive)是一个以秒为单位的时间间隔,表示为一个16位的字,它是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。客户端负责保证控制报文发送的时间间隔不超过保持连接的值。如果没有任何其它的控制报文可以发送,客户端必须发送一个PINGREQ报文。

如:设置100s表示成16进制为 00 64

可变报头非规范示例
Description
7
6
5
4
3
2
1
0
Protocol Name
byte 1
Length MSB(0)
0
0
0
0
0
0
0
0
byte 2
Length LSB(4)
0
0
0
0
0
1
0
0
byte 3
'M'
0
1
0
0
1
1
0
1
byte 4
'Q'
0
1
0
1
0
0
0
1
byte 5
'T'
0
1
0
1
0
1
0
0
byte 6
'T'
0
1
0
1
0
1
0
0
Protocol Level
byte 7
Level(4)
0
0
0
0
0
1
0
0
Connect Flags
byte 8
User Name Flag(1)
1
1
0
0
1
1
1
0
Password Flag(1)
Will Retain(0)
Will QoS(01)
Will Flag(1)
Clean Session(1)
Reserved(0)
Keep Alive
byte 9
Keep Alive MSB(0)
0
0
0
0
0
0
0
0
byte 10
Keep Alive LSB(10)
0
0
0
0
1
0
1
0
得到可变报头

用户名、密码、清理回话,100s

1
00 04 4D 51 54 54 04 C2 00 64

有效载荷

需要用到的工具:

在线HMAC计算-ME2在线工具 (metools.info)

Hex to ASCII Text String Converter (rapidtables.com)

ASCII to Hex | Text to Hex Code Converter (rapidtables.com)

对于各参数的具体信息可以参考阿里云文档:MQTT-TCP连接通信 (aliyun.com)

有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段。如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码,且每个字段必须使用UTF-8编码字符串(需要在最前面添加表示长度的16进制字符

每个字段示例
Bit
7 - 0
byte 1
数据长度 MSB
byte 2
数据长度 LSB
byte 3...
数据长度大于0,这里就是数据部分

我们此时需要去阿里云获取三元组(我随便编一编):

1
2
3
4
5
{
"ProductKey": "ksf114514",
"DeviceName": "Air601",
"DeviceSecret": "ksf1145141919810dj15ltc137666"
}

我们约定:

  • #:ProductKey
  • *:DeviceName
  • &:DeviceSecret
客户端标识符

通用格式:

1
*|securemode=3,signmethod=hmacsha1|

进行填充:

1
Air601|securemode=3,signmethod=hmacsha1|

转换为16进制:

1
41 69 72 36 30 31 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 73 68 61 31 7C

添加长度(40 -> 00 28):

1
00 28 41 69 72 36 30 31 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 73 68 61 31 7C
用户名

通用格式:

1
*&#

进行填充:

1
Air601&ksf114514

转换为16进制

1
41 69 72 36 30 31 26 6B 73 66 31 31 34 35 31 34

添加长度(16 -> 00 10)

1
00 10 41 69 72 36 30 31 26 6B 73 66 31 31 34 35 31 34
密码

通用格式:

1
clientId*deviceName*productKey#

进行填充:

1
clientIdAir601deviceNameAir601productKeyksf114514

加密:

将填充后的内容使用DeviceSecret做密钥,进行HmacSHA1加密

1
e4b931f38037ef7241455709458b4b630b22eac2

转为16进制:

1
65 34 62 39 33 31 66 33 38 30 33 37 65 66 37 32 34 31 34 35 35 37 30 39 34 35 38 62 34 62 36 33 30 62 32 32 65 61 63 32

添加长度(40 -> 00 28)

1
00 28 65 34 62 39 33 31 66 33 38 30 33 37 65 66 37 32 34 31 34 35 35 37 30 39 34 35 38 62 34 62 36 33 30 62 32 32 65 61 63 32
合并,得到最终有效载荷

根据我们在可变报头中的选择:最终有效载荷=客户端标识符+用户名+密码

1
00 28 41 69 72 36 30 31 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 73 68 61 31 7C 00 10 41 69 72 36 30 31 26 6B 73 66 31 31 34 35 31 34 00 28 65 34 62 39 33 31 66 33 38 30 33 37 65 66 37 32 34 31 34 35 35 37 30 39 34 35 38 62 34 62 36 33 30 62 32 32 65 61 63 32

得到最终报文

首先计算固定报头中的剩余长度:剩余长度=可变报头长度+有效载荷长度

可得剩余长度为:10+102=112 -> 70。(小于128,所以正常编码)

最终报文:固定报头+可变报头+有效载荷

1
10 70 00 04 4D 51 54 54 04 C2 00 64 00 28 41 69 72 36 30 31 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 73 68 61 31 7C 00 10 41 69 72 36 30 31 26 6B 73 66 31 31 34 35 31 34 00 28 65 34 62 39 33 31 66 33 38 30 33 37 65 66 37 32 34 31 34 35 35 37 30 39 34 35 38 62 34 62 36 33 30 62 32 32 65 61 63 32

现在可以愉快的与阿里云物联网平台建立连接了~

与阿里云物联网平台建立连接

获取连接地址

直接在阿里云物联网平台的控制台就可以看见,比如我的(瞎编的):iot-ksf114514.mqtt.iothub.aliyuncs.com

建立TCP连接

使用串口助手发送:

1
AT+CIPSTART="TCP","iot-ksf114514.mqtt.iothub.aliyuncs.com",1883

如果建立连接10s不上报消息,阿里云物联网平台会发送CLOSED,并断开连接,这时候需要我们关闭TCP连接然后重新建立连接。

关闭连接:

1
AT+CIPCLOSE

发送报文

这里都是在单连接的情况(AT+CIPMUX=0)。

根据文档,有两种发送方式:

  1. AT+CIPSEND=<length>

    需要指定发送长度,达到指定长度后自动发送。

  2. AT+CIPSENDEX=<length>

    指定发送长度,但可以使用字符串 \0 (0x5c, 0x30 ASCII) 触发数据发送。

选择哪种?从实践来看,因为使用16进制发送CONNECT报文,报文中出现00同样会触发数据发送(emmmm),所以使用第一种指定长度的发送方式,毕竟报文已经提前算好了。

设置发送

最终报文长度:114

1
AT+CIPSEND=114

发送报文

注意,这里要使用16进制(就是切换成HEX)进行发送:

1
10 70 00 04 4D 51 54 54 04 C2 00 64 00 28 41 69 72 36 30 31 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 73 68 61 31 7C 00 10 41 69 72 36 30 31 26 6B 73 66 31 31 34 35 31 34 00 28 65 34 62 39 33 31 66 33 38 30 33 37 65 66 37 32 34 31 34 35 35 37 30 39 34 35 38 62 34 62 36 33 30 62 32 32 65 61 63 32

返回参数

串口返回数据:

1
[Rx][02:12:09.437] 0D 0A 2B 49 50 44 2C 34 3A 20 02 00 00 0D 0A

末尾的0D 是回车符,0A是换行符,因为使用串口助手,这俩不可避免。

其中前面的 0D 0A 2B 49 50 44 2C 34 3A,是+IPD,4,我们只需要关注后面的字节:**20 02 00 00** 。

要知道这玩意的意思,首先要了解CONNACK – 确认连接请求。

CONNACK – 确认连接请求

服务端发送CONNACK报文响应从客户端收到的CONNECT报文。服务端发送给客户端的第一个报文必须是CONNACK。

固定报头

CONNACK报文固定报头
Bit
7
6
5
4
3
2
1
0
byte 1
MQTT报文类型(2)
Reserved 保留位
0
0
1
0
0
0
0
0
byte 2...
剩余长度(2)
0
0
0
0
0
0
1
0

剩余长度字段:表示可变报头的长度。对于CONNACK报文这个值等于2。

固定报头编码

1
20 02

可变报头

CONNACK报文可变报头
描述
7
6
5
4
3
2
1
0
连接确认标志
Reserved 保留位
SP1
byte1
0
0
0
0
0
0
0
X
连接返回码
byte2
X
X
X
X
X
X
X
X

连接确认标志 Connect Acknowledge Flags

第1个字节是 连接确认标志,位7-1是保留位且必须设置为0。 第0 (SP)位 是当前会话(Session Present)标志。

当前会话 Session Present

位置:连接确认标志的第0位。

如果服务端收到清理会话(CleanSession)标志为1的连接,除了将CONNACK报文中的返回码设置为0之外,还必须将CONNACK报文中的当前会话设置(Session Present)标志为0。

)我们在发送报文中清理会话标志位为1,所以返回码为0。(其他情况参阅资料)

连接返回码 Connect Return code

位置:可变报头的第2个字节。

连接返回码字段使用一个字节的无符号值,在下表中列出。

返回码响应 描述
0 0x00连接已接受 连接已被服务端接受
1 0x01连接已拒绝,不支持的协议版本 服务端不支持客户端请求的MQTT协议级别
2 0x02连接已拒绝,不合格的客户端标识符 客户端标识符是正确的UTF-8编码,但服务端不允许使用
3 0x03连接已拒绝,服务端不可用 网络连接已建立,但MQTT服务不可用
4 0x04连接已拒绝,无效的用户名或密码 用户名或密码的数据格式无效
5 0x05连接已拒绝,未授权 客户端未被授权连接到此服务器
6-255 保留

如果服务端收到一个合法的CONNECT报文,但出于某些原因无法处理它,服务端应该尝试发送一个包含非零返回码(表格中的某一个)的CONNACK报文。如果服务端发送了一个包含非零返回码的CONNACK报文,那么它必须关闭网络连接。

正确连接的可变报头

1
00 00

有效载荷

CONNACK报文没有有效载荷。

最终结果

串口返回数据:

1
[Rx][02:12:09.437] 0D 0A 2B 49 50 44 2C 34 3A 20 02 00 00 0D 0A

根据我们在串口看到的返回结果20 02 00 00,可知连接已经建立成功。

这时候在阿里云物联网平台的设备页面,我们看见我们的设备上线了!

1
最后上线时间  2023/07/23 02:12:13.847

参考链接

Luat社区 (openluat.com)

MQTT-TCP连接通信 (aliyun.com)

MQTT中文手册

MQTT协议学习一、MQTT控制报文的结构与CONNECT报文构建 | 码农家园 (codenong.com)

第一章 - MQTT介绍 · MQTT协议中文版 (gitbooks.io)

MQTT - CONNECT报文_mqtt_connect_Hebo42的博客-CSDN博客

ESP8266-12F AT指令连接阿里云_GEEK.攻城狮的博客-CSDN博客

ESP8266-12F AT指令连接阿里云 - 黄豆爸 - 博客园 (cnblogs.com)

MQTT协议报文格式解析 (baidu.com)

  • 标题: 合宙Air601连接阿里云物联网平台-MQTT协议学习
  • 作者: 康师傅
  • 创建于 : 2023-07-22 20:31:10
  • 更新于 : 2023-07-23 03:08:42
  • 链接: https://blog.ksfu.top/posts/47a8/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论