0. 前言
网上这种东西不太多,我也是看了不少资料弄出来了,觉得应该写点东西出来。
我用的板子不是arduino,用的是stm32,开发工具是Arduino IDE,因为Arduino IDE集成了较多的函数库,我们不用管底层的一些东西,都封装好了,写着方便一些。
当然,不管你板子是什么,这篇文章主要讲的不是板子的问题,而是如何通过串口的AT指令控制GA6-B这种支持GPRS的短信模块来实现MQTT协议,以及微信小程序显示单片机发布的数据。
1. 快速弄懂MQTT协议
我觉得学习一个新东西,最有效的办法就是先快速的实现他,先看到预期的结果,再慢慢深入进去,这样会比较快。
弄这个小项目之前,我没有了解过MQTT协议,看了看也比较蒙,因为有人用的是json格式发送数据,有人还用TCP报文,所以我就不太明白了,最后还是找一些例子直接上手操作,如果你之前没有接触过MQTT协议的话,不太明白他是怎么回事,你可以先按照我下边的步骤,弄清楚他的流程,我这里用的都是弄的TCP报文,至于其他的我也不太明白。
需要工具:
- TCP测试工具
- MQTT.fx
- 测试平台阿里云IOT
1.1 在阿里云平台创建测试设备
首先到https://iot.console.aliyun.com/,没有注册的自己注册。
登录上去以后,点击左侧“设备管理”-“产品”。然后点击“创建产品”
我的配置如下:
注意,这里的数据格式一定要选“透传/自定义”,连网方式随意,但是为了和后边讲短信模块的操作一致,就先选择蜂窝吧。都选择好了以后,点击下边的“保存”。
注:产品,是一个种类,他包括很多个设备,比如说某款手机,比如说小米6手机,那这就是一款产品,你不能说它是一个设备,但是如果具体到某个实物了,也就是你手里握着的那个,那个就是一个设备,总之,一个产品包括好多个设备。
产品新建好了以后,需要添加设备。
点击“确认”,一个设备就新建好了。新建好的设备,你看他的状态,会显示未激活,那是因为这个设备还没有登录过,上线一次就会显示“在线/离线”了。一个设备,我们需要获得它的三个关键信息,也就是大多数人说的“三元组”,点击“查看”。
再点击“查看”
出来的三个数据就是该设备的三元组了,一会要用到。
至此,一个设备就创建成功了。
1.2 Topic说明
在MQTT协议中,不允许两个用户之间直接通信,任何两个用户之间通信,需要MQTT服务器作中间人,来回“传话”,意思是这样,但不是真正的传话。MQTT协议采用“发布/订阅”模式,顾名思义,发布,就是把消息散发出去,你理解为客户端把数据发送给MQTT服务器就可以了。订阅的意思就是说,我想要收到某个用户的数据,我在你服务器这里订阅了这个消息,如果我订阅的那个用户有消息发出来了,那你一定要发送给我看。
在刚刚新建的设备那里,点击Topic列表
阿里云IOT平台给出了三种Topic,我们点击自定义Topic
其实,所有的Topic都可以用来通信,只不过用的地方不同,具体你想用到哪里,你可以自己定。打个比方,比如图里的这个第一个,后缀是update,你可以用它来上传传感器数据,当然也可以用来上报错误信息,再看第二个Topic,后缀是error,好像是用来上报错误信息的,但是你非要拿他来上报传感器数据,也是可以的。后边我们就用我图上圈起来的两个进行操作。
1.3 使用MQTT.fx工具进行通信
上边说完了基础的一些东西,设备也建好了,下边该进行最关心的通信测试了,我们只有先打通整个流程,能够直观看到数据通信成功了,才知道MQTT是怎么回事,要不然总是云里雾里。好了,继续。
1.3.1 登录信息填写
打开MQTT测试工具MQTT.fx,先点击下图的按钮配置连接信息
这里要注意,这里信息不能错,错了就连接不上MQTT服务器了,这个界面里最主要的就是图中边框发红的那三个输入框,其他的默认就可以。
Broker Address填入服务器的地址,填写规则如下:
格式:*.iot-as-mqtt.cn-shanghai.aliyuncs.com
*表示自己账号的ProductKey注意替换
我的ProductKey 是a1aExJDxQRT,那么我的这个地址就是a1aExJDxQRT.iot-as-mqtt.cn-shanghai.aliyuncs.com
Broker Port是服务器的端口号,填入1883
Client ID是客户端ID,填写规则如下:
格式:*|securemode=3,signmethod=hmacsha1|
*代表设备名称 注意替换
我的设备名称是tcp_client,所以客户端ID就是tcp_client|securemode=3,signmethod=hmacsha1|
填写好以后先别点击确定,先点击下图选项卡
填写登录的用户名和密码。
用户名规则如下
格式:*&#
*代表设备名称 #代表ProductKey
所以我的就是tcp_client&a1aExJDxQRT
密码的规则如下:
用三元组中的DeviceSecret做为秘钥对clientId*deviceName*productKey#进行hmacsha1加密后的结果就是登录密码
*代表设备名称 #代表ProductKey 注意替换
有很多可以在线加密的网址,随便找一个加密一下就可以了。
我的加密结果是14f0e4037fd1ec3d7cf61902d4352c8bd82d2603
1.3.2 客户端发布数据
都填写好了以后,点击下边的应用,然后关闭该窗口,回到主界面后点击连接,如果信息没有填错的话,应该是连接成功了,如下图。
登录成功以后,找到上边提到的两个Topic中的那个发布的Topic,把地址复制下来,填写到下图的位置。
先别点击发布,我们在阿里云IOT平台上,点击日志服务,再点击前往查看。
在这里可以看到一些关于该设备的信息,因为刚才只登录了,所以这里只有online或者offline的信息。
回到MQTT.fx界面,点击“Publish”发布一条消息,内容为test
再回到刚刚阿里云IOT的日志界面,点击搜索,可以看到有数据传上来了。
点击蓝色的那个数字,查看消息内容。
这个过程就是设备上传的过程,只不过在嵌入式开发的时候,我们就不是用MQTT.fx这种工具来操作了,需要手动实现MQTT.fx刚才的动作。下边来看客户端怎么获得服务器下发的消息。
1.3.3 客户端订阅数据
在阿里云IOT网页里,我们回到“设备详情”界面下,点击“Topic” - “自定义Topic”,我们尝试下发一个数据给已经登录的MQTT客户端。
将“订阅”的这个Topic复制下来,粘贴到MQTT.fx的订阅选项卡界面里边,如下图:
点击“Subscribe”订阅该Topic,此时,我们回到阿里云IOT的网页,点击那个Topic后边的“下发消息”,填入一条数据,点击“确认”
这个时候MQTT.fx中就可以收到这个消息了
至此,就完成了客户端订阅消息的流程,这只是帮助你了解MQTT通信的过程,在下边的项目中,订阅消息是由小程序做的。
1.4 MQTT双向通信
上边我们说了一个设备的“发布和订阅”,那么在一个项目中,设备端(数据采集的这端)和小程序端如果都用同一个阿里云的设备,会挤掉线的,也就是说阿里云IOT的一个设备不能同时在两个地方登陆,那我们要想实现采集器的数据发给小程序,该怎么做呢?答案就是我们需要建两个阿里云IOT的设备,一个设备是采集器,一个设备是小程序,采集器只发布数据,小程序只订阅数据,那么这样一来,好像可以了,但实际不可以,因为每一个阿里云IOT的设备只能订阅和发布自己的Topic,不能订阅别的设备的。所以我们还需要做数据的转发,**将采集器用于发布的Topic获得的数据 转发 给小程序订阅的Topic。**而中间的这个转发过程,阿里云IOT平台可以自动完成,这样来看,整个过程可以实现了,下边来操作。
新建两个设备,这里不赘述了,都是一模一样的,两个设备建立在一个产品里就可以,不用两个产品。建立好了以后再往下看。
点击阿里云平台左侧的云产品流动
创建一个规则,规则名称随意,数据格式选择二进制,然后进入到规则编辑界面,点击“编写SQL”
字段填入一个*号,*号是通配符,表示转发所有的数据。
因为是从采集器转发到小程序,所以这里要选择采集器这个设备的“发布的Topic”,也就是user/update,点击确定。
在点击下图按钮,添加一个数据转发的目的地
注意这里的Topic别选错,因为微信端是要订阅,点击确定。
至此,可以实现两个设备间单向通信了,你可以开两个MQTT.fx工具进行模拟操作,一个模拟采集器发布数据,一个模拟小程序订阅数据,测试下是否可以正常收到订阅信息,在这我就不写了。
需要说明的是,到这里我们只是对MQTT协议的通信模式有了一些直观的感受,但是具体底层是怎么做的,承载MQTT协议的TCP到底是怎么构造的还没有说,这些将在第3节展开说。
2. GA6-B短信模块使用的AT指令
其实用到的AT指令只有两个,一个是连接TCP的,还有一个是发送TCP数据的。至于具体指令是什么,你直接查看你模块的开发文档就好了。
3. 单片机构造MQTT报文
MQTT是应用层的协议,运输层采用TCP进行承载,但是GA6-B模块呢,只支持TCP和UDP两种运输层的协议,好在MQTT官方有说明怎么构造TCP报文,具体的文件内容看这里。https://mcxiaoke.gitbooks.io/mqtt-cn/content/mqtt/03-ControlPackets.html
协议的格式,官方已经说的很清楚了,如果你想测试的话,想验证一下这个协议的构成呢,可以按照下边的方法验证,或者说实践。
在1.3中,我们使用MQTT.fx软件进行登录和其他的操作,但是点击登录按钮以后,程序发出的TCP报文是什么样子的呢?我们怎么才能看到?这也好办,我们只需要搭建一个TCP服务器就可以了,在MQTT.fx中配置登录信息的时候,把服务器ip地址写成127.0.0.1,然后其他的信息不变。此时,我们用下载好的TCP测试工具,创建一个服务器,端口号为1883
启动服务器后,在MQTT.fx中链接服务器,在TCP测试工具中就可以看到MQTT.fx发出的登录数据包了,当然要以16进制查看才可以。
看到这些16进制数据,你应该就会明白了,上边给出的开发文档中说的那些什么字节啊什么的都是什么意思了,都摆在你眼前了,也就可以自己构造TCP报文了。当然,在这,有的人可能会问,这只是看到了登录的报文,那如果想查看发布或者订阅的怎么办呢?我这还没登录成功呢,就不能发布或者订阅啊?
解决办法:MQTT.fx连接服务器,只是发送了请求,那我们模拟出来的这个TCP服务器还没做出回应啊,我们回应一个它登录成功的报文不就好了么,根据官方文档,成功登录的报文16进制是 20 02 00 00这四个字节,我们在模拟的TCP服务器中按照16进制回复一下,就可以在MQTT.fx中看到登录成功了。
说大这,其实你应该已经知道了怎么构造报文了,只需要按照官方给出的那个格式弄就可以了,用刚才查看模拟TCP的办法,检验一下自己构造的正确与否就可以了。网上有人整理好了,我这里借用一下代码:
#define u8 unsigned char
#define u16 unsigned short
#define u32 unsigned int
/*************构造MQTT连接包*******************/
u8 mqtt_connect_message(u8 *mqtt_message, char *client_id, char *username, char *password)
{
u8 client_id_length = strlen(client_id);
u8 username_length = strlen(username);
u8 password_length = strlen(password);
u8 packetLen;
u8 i, baseIndex;
packetLen = 12 + 2 + client_id_length;
if (username_length > 0)
packetLen = packetLen + 2 + username_length;
if (password_length > 0)
packetLen = packetLen + 2 + password_length;
mqtt_message[0] = 16; //0x10 // MQTT Message Type CONNECT
mqtt_message[1] = packetLen - 2; //
baseIndex = 2;
if (packetLen > 127)
{
mqtt_message[2] = 1; //packetLen/127;
baseIndex = 3;
}
mqtt_message[baseIndex++] = 0; // Protocol Name Length MSB
mqtt_message[baseIndex++] = 4; // Protocol Name Length LSB
mqtt_message[baseIndex++] = 77; // ASCII Code for M
mqtt_message[baseIndex++] = 81; // ASCII Code for Q
mqtt_message[baseIndex++] = 84; // ASCII Code for T
mqtt_message[baseIndex++] = 84; // ASCII Code for T
mqtt_message[baseIndex++] = 4; // MQTT Protocol version = 4
mqtt_message[baseIndex++] = 194; // conn flags
mqtt_message[baseIndex++] = 0; // Keep-alive Time Length MSB
mqtt_message[baseIndex++] = 300; // Keep-alive Time Length LSB
mqtt_message[baseIndex++] = (0xff00 & client_id_length) >> 8; // Client ID length MSB
mqtt_message[baseIndex++] = 0xff & client_id_length; // Client ID length LSB
// Client ID
for (i = 0; i < client_id_length; i++)
{
mqtt_message[baseIndex + i] = *(client_id + i);
}
baseIndex = baseIndex + client_id_length;
if (username_length > 0)
{
//username
mqtt_message[baseIndex++] = (0xff00 & username_length) >> 8; //username length MSB
mqtt_message[baseIndex++] = 0xff & username_length; //username length LSB
for (i = 0; i < username_length ; i++)
{
mqtt_message[baseIndex + i] = *(username + i);
}
baseIndex = baseIndex + username_length;
}
if (password_length > 0)
{
//password
mqtt_message[baseIndex++] = (0xff00 & password_length) >> 8; //password length MSB
mqtt_message[baseIndex++] = 0xff & password_length; //password length LSB
for (i = 0; i < password_length ; i++)
{
mqtt_message[baseIndex + i] = *(password + i);
}
baseIndex += password_length;
}
return baseIndex;
}
/*********MQTT发布消息包******************************/
u8 mqtt_publish_message(u8 *mqtt_message, char * topic, char * message, u8 qos)
{
u16 topic_length = strlen(topic);
u16 message_length = strlen(message);
u16 i, index = 0;
static u16 id = 0;
mqtt_message[index++] = 48; //0x30 // MQTT Message Type PUBLISH
if (qos)
mqtt_message[index++] = 2 + topic_length + 2 + message_length;
else
mqtt_message[index++] = 2 + topic_length + message_length; // Remaining length
mqtt_message[index++] = (0xff00 & topic_length) >> 8;
mqtt_message[index++] = 0xff & topic_length;
// Topic
for (i = 0; i < topic_length; i++)
{
mqtt_message[index + i] = *(topic + i);
}
index += topic_length;
if (qos)
{
mqtt_message[index++] = (0xff00 & id) >> 8;
mqtt_message[index++] = 0xff & id;
id++;
}
// Message
for (i = 0; i < message_length; i++)
{
mqtt_message[index + i] = *(message + i);
}
index += message_length;
return index;
}
/*****************发布确认包****************************/
u8 mqtt_puback_message(u8 *mqtt_message)
{
static u16 id = 0;
mqtt_message[0] = 64; //0x40 PUBACK
mqtt_message[1] = 2;
mqtt_message[2] = (0xff00 & id) >> 8;
mqtt_message[3] = 0xff & id;
id++;
return 4;
}
/*********构建订阅请求*********/
u16 mqtt_subscribe_message(u8 *mqtt_message, char *topic, u8 qos, u8 whether)
{
u16 topic_len = strlen(topic);
u16 i, index = 0;
static u16 id = 0;
id++;
if (whether)
mqtt_message[index++] = 130;
else
mqtt_message[index++] = 162;
mqtt_message[index++] = topic_len + 5;
mqtt_message[index++] = (0xff00 & id) >> 8;
mqtt_message[index++] = 0xff & id;
mqtt_message[index++] = (0xff00 & topic_len) >> 8;
mqtt_message[index++] = 0xff & topic_len;
for (i = 0; i < topic_len; i++)
{
mqtt_message[index + i] = *(topic + i);
}
index += topic_len;
if (whether)
{
mqtt_message[index] = qos;//QoS
index++;
}
return index;
}
/***********构建MQTT请求PING包********************/
u8 mqtt_ping_message(u8 *mqtt_message)
{
mqtt_message[0] = 192; //0xC0 PING
mqtt_message[1] = 0; //
return 2;
}
/************构建断开包******************/
u8 mqtt_disconnect_message(u8 *mqtt_message)
{
mqtt_message[0] = 224; //0xE0 DISCONNECT
mqtt_message[1] = 0; //
return 2;
}
至于怎么调用,就很简单了,你自己琢磨琢磨就可以了。单片机内构造好报文,然后通过串口让GA6-B发送出去就可以了。
4. 微信小程序使用MQTT协议
这个网上有介绍的比较详细的文章了,自行查找吧,可以解决的。
综上,可以完成采集器到小程序整个MQTT通信了,完结,撒花。