基于MQTT协议的阿里云IOT与单片机开发(一)

  基于MQTT通信协议 实现单片机与腾讯云IOT|阿里云IOT|中国移动ONENET的对接。
在实际调试时选择STM32F103C8T6与ESP826601S的开发板,对接过程是一个比较艰难的过程。编译、烧录、下载
至单片机后通过串口调试助手能够实现配置连接WIFI加入网络,和远端服务器建立TCP联系,但是云端设备始终处于
未激活状态,查找了很多做IOT通信方面的例程,很多例程都是基于云的SDK实现的,对于第一次调试玩IOT的小白而言
着实有点不大友好。后来查找设备未激活的缘由,主要认为是本机地址没能够和远程服务器实现匹配,然后就通过网络
调试助手,将MQTT协议报文发送过去。
  ONENET更新的幅度有点大,导致我竟然找不到它的加入产品与设备在哪个点击栏。我根据MQTT协议的配置方式和
 腾讯云主机地址、官方文档、加密解密方式发现挣扎了很久还是没能够得到对方主机20 02 00 00的CONNACK报文回复。
 那我最后辗转反侧又回到了阿里云的IOT,真的得感谢马云爸爸,总算是成功实现了设备激活,那主要就是分享一下和
 阿里云的故事了。

 1.首先要登录阿里云,可以使用支付宝登录,进行个人实名验证。然后找到物联网通信,阿里在物联网通信这方面的开发文档
 说实话其实蛮丰富的,其SDK环境也为各种语言的开发提供了可能,还提供了设备与云中间的开发平台便利操作。
 
 2.进入物联网通信控制平台,选择产品创建,在产品创建中完善相关信息,可以选择JSON数据格式,我本人选择的还是
 比较Low级别的自定义透传的二进制,选择设备节点类型。这个页面完成之后会出现产品密钥PRODUCTKEY,这个密钥可
 以复制到建立的记事本中,后面的一些配置会用到。还有一点,在这个创建产品的时候你需要注意到左上角你的远程
 主机地址是在哪,一般情况下默认设置为华东上海地区,地址不同可能导致你最后需要建立连接的域名也是不同的。
 以前阿里云和腾讯云的远程主机对接是不同的,现在好像两家都是一样的,配置时前面是:产品ID + “.”  + 服务器地址
 类似于这样:PRODUCTID.iot-as-mqtt.cn-shanghai.aliyuncs.com   PRODUCTID是你创建产品时会生成的,一般格式
 可能就是这样a16lzjHgAYy
 
 3.产品一栏中有产品名称与设备管理,点击设备管理,添加设备,添加设备页面需要填写对应归属的产品和设备名称,
 设备名称要用中文,一般情况下如果是建立三元关系组(APP IOT 单片机)的话,那可以命名为###kfb ###app,现在的
 要求长度是至少六个字符,我在设置的时候用的szykfb  szyapp分别加入两个设备,设备建立完成后会出现如下提醒信息
 那分别是产品密钥和设备名称、设备密钥
 {
  "ProductKey": "a16lzjHgAYy",
  "DeviceName": "szykfb",
 "DeviceSecret": "PKxVLqDI5bOnvbUXw299vG5nDOTjosbj"
 }
 那剩下APP设备的加入也是这样,那接下来主要就是配置相应的加密解密信息了。
 
 4.阿里云MQTT协议需要首先建立connect报文,建立三元关系组的connect报文。
 客户端ID :  *|securemode=3,signmethod=hmacsha1|             *设备名称   注意替换
 用户名   :  *&#                 *设备名称 #ProductKey  注意替换   
 密码    用设备密钥对clientId*deviceName*productKey#进行hmacsha1加密后的结果      *设备名称 #ProductKey  注意替换

 这里提供一个网站:http://encode.chahuo.com/ 用来实现获取密码  加密hmacsha1
 那么把上述三元关系组存储在记事本中,我们的记事本中现在有以下信息:
 
 开发板:
 PRODUCTKET :a16lzjHgAYy
 客户端ID : szykfb|securemode=3,signmethod=hmacsha1|
 用户名:szykfb&a16lzjHgAYy
 密码:c6109a56f9992082a159383cd59ff4894e618285
 主机地址:a16lzjHgAYy.iot-as-mqtt.cn-shanghai.aliyuncs.com
 5. 接下来就是要了解MQTT协议的 CONNECT报文了,CONNECT报文主要有固定报头和可变报头组成,没有有效负载。
 那我这里就以程序为例附上报文的讲解吧,具体的协议内容还是结合MQTT协议的手册去看吧。当然最好先用网络调试
 助手TCP CLIENT模式 ,远程主机选择PRODUCTID.iot-as-mqtt.cn-shanghai.aliyuncs.com,端口号1883,对上述三元关系组
 进行16进制转换建立CONNECT报文,具体程序如下,CONNECT报文固定报头为:00 ?? 4D 51 54 54 04 C2 00 78
 我这里C2是建立在不保留当前会话与不发布遗嘱消息的前提下的,最后两位KEEP ALIVE 78表示120s的对话时间,也就是
 说两条报文发送时间不能超过这个时间,否则的话设备就会自动离线状态了。那也可以根据自己想要的时间进行修改。??主要是计算??后面发送的字节长度,最后要转换为十六进制才行。
 后面就是分别将客户端ID 用户名 密码进行ASCII转16进制,并且计算长度,主要就是00+长度+内容组合起来,千万要细心
 不然你可能会抓狂到为什么还没激活呢?为什么还没激活呢?!!那我这个例子调出来的connect报文就是:
 10 72 00 04 4D 51 54 54 04 C2 00 78 00 28 73 7A 79 61 70 70 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 12 73 7A 79 61 70 70 26 61 31 36 6C 7A 6A 48 67 41 59 79 00 28 30 32 66 34 66 62 39 30 34 39 66 61 35 34 34 33 63 39 64 31 33 65 33 63 65 63 30 35 63 34 64 64 33 31 66 36 33 62 34 31
 然后点击发送,你会接收到20 02 00 00,其实你会发现 就算你某个字节出了问题发送给阿里云iot他也会回复你,
 不过回复的是20 02 00 04,可是腾讯云就不一样,他就是不搭理你,可能马化腾爸爸比较高冷吧。那这就很有意思了。
//首先需要发送CONNECT报文 第二次CONNECT报文被处理为违规
void MQTT_ConnectPack(void)
{
	Fixed_len = 2;
	Variable _len = 10;
	//可变报头包含协议名 协议级别 连接标志 保持连接
	/*		协议名称
			0x00
			0x04
			M 0x4D
			Q 0x51
			T 0x54
			T 0x54
			协议级别  不支持的协议级别服务器发送返回码0x01的CONNACK报文断开连接
			0x04
			连接标志:Username flag + Passward flag +Will Retain +Will Qos+Will flag +Clean Sessision +Reserved
			reserved = 0 
			clean session = 0     基于当前会话
			will flag = 1  发送遗嘱消息 will flag =0 不能发送遗嘱消息 
			will qos  = 0x01 | 0x02 | 0x00
			will retain = 0 遗嘱标志为1时服务端将遗嘱消息当做非保留消息发布 will retain = 1当做保留消息发布
			username flag = 0  有效载荷不能包含用户名字段
			passward flag = 0 有效载荷不能包含密码字段
			连接标志设为11000010 0xC2
			用户名与密码标志为1 有效载荷后面加CLIENTID +遗嘱主题+遗嘱消息+用户名+密码  由于willflag=0所以不需要考虑遗嘱
			
	*/
	Payload_len = 2 + ClientID_len + 2 + Username_len + 2 + Passward_len;
	//有效载荷长度 客户端标识符 遗嘱主题 遗嘱消息 用户名 密码
	
	temp_buffer[0] = 0x10; //固定报头第一位
	temp_buffer[1] = Variable_len + Payload_len;//固定报头第二位
	//可变报文
	temp_buffer[2] = 0x00; //协议名称长度MSB
	temp_buffer[3] = 0x04;//协议名称长度LSB
	temp_buffer[4] = 0x4D;//M
	temp_buffer[5] = 0x51;//Q
	temp_buffer[6] = 0x54;//T
	temp_buffer[7] = 0x54;//T
	temp_buffer[8] = 0x04;//协议级别
	temp_buffer[9] = 0xC2; //不保留会话状态与发布遗嘱消息
	temp_buffer[10] = 0x00;//保持连接MSB
	temp_buffer[11] = 0x78;//保持连接LSB,取120s为两个报文之间发送的时间室间隔
	//有效载荷
	temp_buffer[12] = ClientID / 256;//计算客户端ID的高字节
	temp_buffer[13] = ClientID % 256;//计算客户端ID的低字节
	memcpy(&temp_buffer[14],ClientID,ClientID_len);//存储客户端ID
	temp_buffer[14+ClientID_len] = Username / 256;//计算用户名的高字节
	temp_buffer[15+ClientID_len] = Username % 256;//计算用户名的低字节
	memcpy(&temp_buffer[16+ClientID_len],Username,Username_len);//存储用户名信息
	temp_buffer[16+ClientID_len+Username_len] = Passward / 256;//存储密码的高字节
	temp_buffer[17+ClientID_len+Username_len] = Passward % 256;//存储密码的低字节
	memcpy(&temp_buffer[18+ClientID_len],Passward,Passward_len);//存储密码信息
	
	TxDataBuf_Deal(temp_buff, Fixed_len + Variable_len + Payload_len); //加入发送数据缓冲区
	
}
 	6.那我们为什么会接受到20 02 00 00 与20 02 00 04两种结果呢?
 	来看看CONNACK报文,固定报头是20 02,可变报头长度为2表示2个字节,可变报头第一个字节在于当前会话位。
 	如果服务端收到清理会话(CleanSession)标志为 1 的连接,除了将 CONNACK 报文中的返回码设置为 之外,
 	还必须将 CONNACK 报文中的当前会话设置(Session Present)标志为 0 [MQTT-3.2.2-1]。
	如果服务端收到一个 CleanSession 为 0 的连接,当前会话标志的值取决于服务端是否已经保存了 ClientI对应客户端的会话状态。
	如果服务端已经保存了会话状态,它必须将 CONNACK 报文中的当前会话标志设
	置为 1 [MQTT-3.2.2-2]。如果服务端没有已保存的会话状态,它必须将 CONNACK报文中的当前会话设为 0。
	还需要将 CONNACK 报文中的返回码设置为 0 [MQTT-3.2.2-3]。当前会话标志使服务端和客户端在是否有已存储的会话状态上保持一致。
	一旦完成了会话的初始化设置,已经保存会话状态的客户端将期望服务端维持它存储的会话状态。如果客
	户端从服务端收到的当前的值与预期的不同,客户端可以选择继续这个会话或者断开连接。客户端可以丢
	弃客户端和服务端之间的会话状态,方法是,断开连接,将清理会话标志设置为 1,再次连接,然后再次
	断开连接。如果服务端发送了一个包含非零返回码的 CONNACK 报文,它必须将当前会话标志设置为 0 [MQTT-3.2.2-4]。
 	第二个字节中00表示已经接受,01表示不支持协议版本,02表示服务端不可用,04表示用户名或密码错误,那
 	这个时候就要重新检查哪个地方出现问题了,粗心真是要命的。05表示客户端未授权。
 	
 	7.接下来就是处理订阅和发布消息 PUBLISH和SUBSCRIBE的问题了。SUBSCIRBE报文也是由固定报头和可变报头组成对的,
 	固定报头0x82 + 剩余长度,剩余长度基本都是可变长度+有效载荷,可变长度为2,主要是报文标识符的MSB和LSB,
 	这个感觉没有特别关键的地方,手册上距离0x0A也就是设置为10。有效载荷主要是主题过滤器和Qos等级字段组合,如果没有
 	就会违反MQTT协议了。前两个字节也就是主题的长度,然后再是主题和服务质量要求 可以设置为Qo0,Qo1,Qo2,然后放到
 	发送缓冲区就行,那我们用网络调试助手调试的时候也是按照这种方式构建二进制串就行。具体操作方式同6中处理方式一样,
 	按照协议要求转换即可。
 	比如:/a16lzjHgAYy/szykfb/user/s_data 的订阅,转换为16进制写成SUBSCRIBE报文为:82 24 00 0A 00 1F 2F 61 31 36 6C 7A 6A 48 67 41 59 79 2F 73 7A 79 6B 66 62 2F 75 73 65 72 2F 73 5F 64 61 74 61 00 (Qos0)
	82 24 00 0A 00 1F 2F 61 31 36 6C 7A 6A 48 67 41 59 79 2F 73 7A 79 6B 66 62 2F 75 73 65 72 2F 73 5F 64 61 74 61 01 (Qos1)
	发送报文后可以受到服务器的SUBACK报文,固定报头为90+剩余长度(后面几个字节的长度)+可变报头(报文标识符,同SUBSCRIBE中一样)+有效载荷(0x00|0x01|0x02|0x80) 0X80表示失败,前面几个表示Qos等级
	我在实际网络助手调试的时候也遇到了接收到0x80这种情况,检查之后发现我在订阅消息的时候connect错了对象,牛头不对马嘴不就是这个意思嘞?千万不要把张三戴到李四头上。
	那么程序主要就是如下了,对着协议应该是比较容易理解的。
//订阅报文,制定Qos等级
void MQTT_Subscribe(char *topic_name, int QoS)
{
	Fixed_len = 2;
	Variable_len = 2;
	Payload_len = 2 + strlen(topic_name) + 1; //计算有效负荷长度 = 2字节(topic_name长度)+ topic_name字符串的长度 + 1字节服务等级
	temp_buffer[0] = 0x82;//固定报头首个字节
	temp_buffer[1] = Variable_len + Paylode_len;//存储剩余长度
	temp_buffer[2] = 00;//报文标识符MSB
	temp_buffer[3] = 0A;//报文标识符LSB  标识符为10时
	temp_buffer[4] = strlen(topic_name) / 256;//主题长度MSB
	temp_buffer[5] = strlen(topic_name) % 256;//主题长度LSB
	memcpy(&temp_buffer[6],topic_name,strlen(topic_name));//拷贝主题名称
	temp_buffer[6+strlen(topic_name)] = Qos;//订阅等级
	TxDataBuf_Deal(temp_buff, Fixed_len + Variable_len + Payload_len);  //加入发送数据缓冲区
	
}
 8.有了订阅当然还要有发布,那发布就是靠PUBLISH来实现了。发布消息的固定报头相比前面几个有点繁琐,
 可以确定的是第一个字节的高位0011也就是3,后面就要根据重发标志DUP、QOS等级和保留位来进行确定了。
 那这里可以分析一下QOS不同等级的关系,QOS0的话最多只能发一次,
 发了一次后服务端到底有没有收到也就不管了,QOS1的话至少发一次,发送之后呢服务端会有对应PUBACK报文返回,
 QOS2的话仅仅发一次,发了一次后也会有相应的报文数据反馈。那我们这里认为DUP取0,选择订阅QOS0消息,
 因为我要的实时数据,你发送一次就够了我肯定不要上一秒的数据,肯定是越新越好。那我第一个字节这样分析的话
 就确定为0x30了。第二个字节就是老朋友剩余长度,然后依次放入可变长度,具体地在代码中可能看得比较详细了。
/*
QoS0:最多分发一次的分发协议 Qos0 送到或没送到   发送者必须发送Qos=0,DUP=0的PUBLISH报文
QoS1:至少分发一次需要PUBACK确认 必须包含报文标识符 QoS=1 DUP = 0
QoS2:仅发送一次 QoS = 2 DUP = 0 从接受者处接收到对应的PUBREC报文,接到PUBREC后必须发送一个PUBREL且包含与原始PUBLISH报文相同的报文标识符
*/
//发布消息 
void MQTT_PublishQs0(char *topic, char *data, int data_len)
{
	Fixed_len = 2;                             
	Variable_len = 2 + strlen(topic);          //可变报头长度:2字节(topic长度)+ topic字符串的长度
	Payload_len = data_len;                    //有效负荷长度:就是data_len
	//固定报头主要有两个字节,第一个字节 MQTT控制报文类型0011 +重发标志DUP+Qos+保留标志Retain
	//根据分析QOS分析 DUP = 0 QOS = 00 RETAIN = 0  第一个字节为0030
	temp_buffer[0] = 0x30;//固定报头首个字节
	temp_buffer[1] = Variable_len + Paylode_len;
	temp_buffer[2] = strlen(topic) / 256; //放主题高字节
	temp_buffer[3] = strlen(topic) %256;  //主题低字节
	memcpy(&temp_buffer[4],topic,strlen(topic));//拷贝主题
	memcpy(&temp_buffer[4+strlen(topic)],data,data_len);//拷贝数据
	
	TxDataBuf_Deal(temp_buff, Fixed_len + Variable_len + Payload_len);  //加入发送数据缓冲区
}

那么还是来举个栗子,/a16lzjHgAYy/szykfb/user/p_data 转16进制,数据为123 转十六进制后为31 32 33,
最后可以得到对应十六进制数据串为 30 24 00 1F 2F 61 31 36 6C 7A 6A 48 67 41 59 79 2F 73 7A 79 6B 66 62 2F 75 73 65 72 2F 70 5F 64 61 74 61 31 32 33。打开两个网络调试助手,都接阿里云上海地址,分别发送APP KFB对应的CONNECT报文和SUBSCIRBE报文,最后在KFB端发送上述数据可以在APP处接收到同样的数据,这一步也需要在云端规则引擎处加入SQL语言配置。

基于MQTT协议的阿里云IOT与单片机开发(一)_第1张图片
同样的也可以按照上述完成APP转发到开发板消息的配置,这样就实现了相互接收。
到这里为止基本山 单片机 iot APP三者的对接就完成了。
后续故事继续等待探索,嗝。排版有点烂,也比较懒emmmm。

你可能感兴趣的:(单片机与嵌入式)