STM32+ESP8266+MQTT协议连接腾讯物联网开发平台

2022-01-17 15:39:29 浏览数 (2)

一、环境介绍

单片机采用:STM32F103C8T6

上网方式:采用ESP8266,也可以使用其他设备代替,只要支持TCP协议即可。比如:GSM模块、有线网卡等。

开发软件:keil5

物联网平台: 腾讯IOT物联网物联网平台。腾讯的物联网平台比起其他厂家的物联网平台更加有优势,腾讯物联网平台可以将数据推到微信小程序上,用户可以直接使用小程序绑定设备,完成与设备之间交互,现在用户基本都会使用微信,所以使用起来非常方便。

本文章配套使用的STM32设备端完整源代码下载地址: https://download.csdn.net/download/xiaolong1126626497/18785807

STM32 ESP8266使用MQTT协议连接OneNET 中国移动物联网开发平台https://blog.csdn.net/xiaolong1126626497/article/details/107385118

STM32 ESP8266使用MQTT协议连接阿里云物联网开发平台:https://blog.csdn.net/xiaolong1126626497/article/details/107311897

二、功能介绍

本文章接下会介绍如何在腾讯物联网平台上创建设备、配置设备、推送到微信小程序、并编写STM32设备端代码,使用ESP8266联网登录腾讯物联网平台,完成数据交互。

功能: STM32采集环境温度、湿度、光照强度实时上传至物联网平台,在微信小程序页面上,用户可以实时查看这些数据,并且可以通过界面上的按钮控制设备端的电机、LED灯的开关,完成数据上传和远程控制。

说明: STM32设备端所有代码均有自己全部编写,没有使用任何厂家的SDK,MQTT协议也是参考MQTT官方文档编写;ESP8266也没有使用任何专用固件,所以代码的移植性非常高。 任何能够联网的设备都可以参考本篇文章代码连接腾讯物联网平台,达到相同的效果。

三、登录腾讯物联网平台创建设备

腾讯云官网: https://cloud.tencent.com/

下面是手机上的截图:操作过程

现在设备是离线状态,是无法查看的,接下来就使用MQTT客户端模拟设备,登录测试。

四、使用MQTT客户端模拟设备--测试

4.1 下载MQTT客户端

MQTT客户端可执行文件下载地址(.exe): https://download.csdn.net/download/xiaolong1126626497/18784012

这个MQTT客户端采用QT开发,如果需要了解它的源码,请看这里: https://blog.csdn.net/xiaolong1126626497/article/details/116779490

4.2 查看物联网平台端口号与域名(IP地址)

官方文档: https://cloud.tencent.com/document/product/634/32546

通过这里得到信息: 如果是广州域的设备(其实哪里都一样,只是服务器距离的远近),就填入 <产品ID>.iotcloud.tencentdevices.com ,端口号是 1883(这是密匙认证的端口号,如果是证书认证就是另一个)。

查看产品ID的方法:

得打产品ID之后,那么要连接我的设备,域名就填: 8O76VHCU7Y.iotcloud.tencentdevices.com 端口就填: 1883

由于我的测试用的MQTT客户端不支持域名输入,只支持IP地址输入,所有我这里需要先将域名转为IP地址在进行下面的测试,ESP8266内部支持域名解析的,所有可以直接输入域名即可,不需要做这一步。

在线解析域名的网址: https://site.ip138.com/8O76VHCU7Y.iotcloud.tencentdevices.com/

得到广州腾讯云的IP地址为: 106.55.124.154

4.3 生成MQTT登录参数

就像我们登录QQ、登录微信需要账号密码一样,设备登录物联网平台也需要类似的东西。

官方文档地址: https://cloud.tencent.com/document/product/634/32546

上面需要的参数,在设备调试页面,点击具体的设备进行查看:

Python源代码:

代码语言:javascript复制
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import base64
import hashlib
import hmac
import random
import string
import time
import sys
# 生成指定长度的随机字符串
def RandomConnid(length):
    return  ''.join(random.choice(string.ascii_uppercase   string.digits) for _ in range(length))
# 生成接入物联网通信平台需要的各参数
def IotHmac(productID, devicename, devicePsk):
     # 1. 生成 connid 为一个随机字符串,方便后台定位问题
     connid   = RandomConnid(5)
     # 2. 生成过期时间,表示签名的过期时间,从纪元1970年1月1日 00:00:00 UTC 时间至今秒数的 UTF8 字符串
     expiry   = int(time.time())   30*24*60 * 60
     # 3. 生成 MQTT 的 clientid 部分, 格式为 ${productid}${devicename}
     clientid = "{}{}".format(productID, devicename)
     # 4. 生成 MQTT 的 username 部分, 格式为 ${clientid};${sdkappid};${connid};${expiry}
     username = "{};12010126;{};{}".format(clientid, connid, expiry)
     # 5. 对 username 进行签名,生成token
     secret_key = devicePsk.encode('utf-8')  # convert to bytes
     data_to_sign = username.encode('utf-8')  # convert to bytes
     secret_key = base64.b64decode(secret_key)  # this is still bytes
     token = hmac.new(secret_key, data_to_sign, digestmod=hashlib.sha256).hexdigest()
     # 6. 根据物联网通信平台规则生成 password 字段
     password = "{};{}".format(token, "hmacsha256")
     return {
        "clientid" : clientid,
        "username" : username,
        "password" : password
     }
if __name__ == '__main__':
    # 参数分别填入: 产品ID,设备名称,设备密匙
    print(IotHmac("8O76VHCU7Y","SmartAgriculture","OHXqYLklNBU4xLqqoZbXMQ=="))

得到的登录信息如下:

代码语言:javascript复制
clientid: 8O76VHCU7YSmartAgriculture
username: 8O76VHCU7YSmartAgriculture;12010126;J4MCD;1623766532
password: a962b484079864239148b255281d54372aa66247aa8d6259d11aa6fef650fd5b;hmacsha256

4.4 了解主题上报和订阅的格式

登录之前需要先了解如何订阅设备的主题和上报的数据流格式。

如果设备端想要得到APP页面的按钮状态就需要订阅属性下发和属性上报的响应,主题格式就是这样的:

代码语言:javascript复制
格式:
$thing/down/property/8O76VHCU7Y/设备名称
示例:
$thing/down/property/8O76VHCU7Y/SmartAgriculture

如果设备端想要像APP页面上传数据,那么就需要使用属性上报--发布主题:

代码语言:javascript复制
格式:
$thing/up/property/8O76VHCU7Y/${deviceName}
示例:
$thing/up/property/8O76VHCU7Y/SmartAgriculture

设备端向APP页面上报属性时,需要上传具体的数据,数据流的格式如下:

官方文档: https://cloud.tencent.com/document/product/1081/34916

比如: 我的产品里有温度、湿度、电机三个设备,我可以选择一次上传3个设备的信息,数据格式就这样写:

代码语言:javascript复制
{"method":"report","clientToken":"123","params":{"temperature":20.23,"humidity":50,"Motor":1}}

其中: "temperature"、"humidity"、"Motor" 是设备的标识符,根据自己的情况修改,冒号后面就是给这个设备上传的具体数据。

4.5 使用MQTT客户端登录设备测试

万事俱备,下面就使用MQTT客户端进行登录测试。

MQTT客户端操作步骤:

1. 填写相关参数

2. 点击登录

3. 订阅主题

4. 发布主题

5. 去APP页面查看信息

4.6 微信小程序效果

已经收到MQTT客户端上传的数据,点击按钮,MQTT客户端也会收到按钮下发的数据。

五、STM32设备端代码

本文章配套使用的STM32设备端完整源代码下载地址: https://download.csdn.net/download/xiaolong1126626497/18785807

5.1 下载程序

5.2 连接状态

STM32设备上按下按键后,手机打开微信小程序可以看到实时上传的数据,速度非常快。

5.3 main.c文件

代码语言:javascript复制
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
#include "key.h"
#include "usart.h"
#include <string.h>
#include "timer.h"
#include "bluetooth.h"
#include "esp8266.h"
#include "mqtt.h"

//腾讯物联网服务器的设备信息
#define MQTT_ClientID "8O76VHCU7YSmartAgriculture"
#define MQTT_UserName "8O76VHCU7YSmartAgriculture;12010126;J4MCD;1623766532"
#define MQTT_PassWord "a962b484079864239148b255281d54372aa66247aa8d6259d11aa6fef650fd5b;hmacsha256"

//订阅与发布的主题
#define SET_TOPIC  "$thing/down/property/8O76VHCU7Y/SmartAgriculture"  //订阅
#define POST_TOPIC "$thing/up/property/8O76VHCU7Y/SmartAgriculture"  //发布

char mqtt_message[200];//上报数据缓存区

int main()
{
   u32 time_cnt=0;
   u32 i;
   u8 key;
   LED_Init();
   BEEP_Init();
   KEY_Init();
   USART1_Init(115200);
   TIMER1_Init(72,20000); //超时时间20ms
   USART2_Init(9600);//串口-蓝牙
   TIMER2_Init(72,20000); //超时时间20ms
   USART3_Init(115200);//串口-WIFI
   TIMER3_Init(72,20000); //超时时间20ms
   USART1_Printf("正在初始化WIFI请稍等.n");
   if(ESP8266_Init())
   {
      USART1_Printf("ESP8266硬件检测错误.n");  
   }
   else
   {
      //加密端口
      //USART1_Printf("WIFI:%dn",ESP8266_STA_TCP_Client_Mode("OnePlus5T","1126626497","183.230.40.16",8883,1));
      
      //非加密端口
      USART1_Printf("WIFI:%dn",ESP8266_STA_TCP_Client_Mode("CMCC-Cqvn","99pu58cb","106.55.124.154",1883,1));
  
   }
   
    //2. MQTT协议初始化	
    MQTT_Init(); 
    //3. 连接OneNet服务器        
    while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord))
    {
        USART1_Printf("服务器连接失败,正在重试...n");
        delay_ms(500);
    }
    USART1_Printf("服务器连接成功.n");
    
    //3. 订阅主题
    if(MQTT_SubscribeTopic(SET_TOPIC,0,1))
    {
        USART1_Printf("主题订阅失败.n");
    }
    else
    {
        USART1_Printf("主题订阅成功.n");
    }        
    
    while(1)
    {    
        key=KEY_Scan(0);
        if(key==2)
        {
            time_cnt=0;
            sprintf(mqtt_message,"{"method":"report","clientToken":"123","params":{"temperature":20.23,"humidity":50,"Motor":1}}");
            MQTT_PublishData(POST_TOPIC,mqtt_message,0);
            USART1_Printf("发送状态1rn");
        }
        else if(key==3)
        {
            time_cnt=0;
            sprintf(mqtt_message,"{"method":"report","clientToken":"123","params":{"temperature":10.23,"humidity":60,"Motor":0}}");
            MQTT_PublishData(POST_TOPIC,mqtt_message,0);
            USART1_Printf("发送状态0rn");
        }  

        if(USART3_RX_FLAG)
        {
            USART3_RX_BUFFER[USART3_RX_CNT]='';
            for(i=0;i<USART3_RX_CNT;i  )
            {
                USART1_Printf("%c",USART3_RX_BUFFER[i]);
            }
            USART3_RX_CNT=0;
            USART3_RX_FLAG=0;
        }

        //定时发送心跳包,保持连接
        delay_ms(10);
        time_cnt  ;
        if(time_cnt==500)
        {
            MQTT_SentHeart();//发送心跳包
            time_cnt=0;
        }
    }
}

5.4 mqtt.c

代码语言:javascript复制
#include "mqtt.h"

u8 *mqtt_rxbuf;
u8 *mqtt_txbuf;
u16 mqtt_rxlen;
u16 mqtt_txlen;
u8 _mqtt_txbuf[256];//发送数据缓存区
u8 _mqtt_rxbuf[256];//接收数据缓存区

typedef enum
{
	//名字 	    值 			报文流动方向 	描述
	M_RESERVED1	=0	,	//	禁止	保留
	M_CONNECT		,	//	客户端到服务端	客户端请求连接服务端
	M_CONNACK		,	//	服务端到客户端	连接报文确认
	M_PUBLISH		,	//	两个方向都允许	发布消息
	M_PUBACK		,	//	两个方向都允许	QoS 1消息发布收到确认
	M_PUBREC		,	//	两个方向都允许	发布收到(保证交付第一步)
	M_PUBREL		,	//	两个方向都允许	发布释放(保证交付第二步)
	M_PUBCOMP		,	//	两个方向都允许	QoS 2消息发布完成(保证交互第三步)
	M_SUBSCRIBE		,	//	客户端到服务端	客户端订阅请求
	M_SUBACK		,	//	服务端到客户端	订阅请求报文确认
	M_UNSUBSCRIBE	,	//	客户端到服务端	客户端取消订阅请求
	M_UNSUBACK		,	//	服务端到客户端	取消订阅报文确认
	M_PINGREQ		,	//	客户端到服务端	心跳请求
	M_PINGRESP		,	//	服务端到客户端	心跳响应
	M_DISCONNECT	,	//	客户端到服务端	客户端断开连接
	M_RESERVED2		,	//	禁止	保留
}_typdef_mqtt_message;

//连接成功服务器回应 20 02 00 00
//客户端主动断开连接 e0 00
const u8 parket_connetAck[] = {0x20,0x02,0x00,0x00};
const u8 parket_disconnet[] = {0xe0,0x00};
const u8 parket_heart[] = {0xc0,0x00};
const u8 parket_heart_reply[] = {0xc0,0x00};
const u8 parket_subAck[] = {0x90,0x03};

void MQTT_Init(void)
{
    //缓冲区赋值
	mqtt_rxbuf = _mqtt_rxbuf;
    mqtt_rxlen = sizeof(_mqtt_rxbuf);
	mqtt_txbuf = _mqtt_txbuf;
    mqtt_txlen = sizeof(_mqtt_txbuf);
	memset(mqtt_rxbuf,0,mqtt_rxlen);
	memset(mqtt_txbuf,0,mqtt_txlen);
	
	//无条件先主动断开
	MQTT_Disconnect();
    delay_ms(100);
	MQTT_Disconnect();
    delay_ms(100);
}

/*
函数功能: 登录服务器
函数返回值: 0表示成功 1表示失败
*/
u8 MQTT_Connect(char *ClientID,char *Username,char *Password)
{
    u8 i,j;
    int ClientIDLen = strlen(ClientID);
    int UsernameLen = strlen(Username);
    int PasswordLen = strlen(Password);
    int DataLen;
	mqtt_txlen=0;
	//可变报头 Payload  每个字段包含两个字节的长度标识
    DataLen = 10   (ClientIDLen 2)   (UsernameLen 2)   (PasswordLen 2);
	
	//固定报头
	//控制报文类型
    mqtt_txbuf[mqtt_txlen  ] = 0x10;		//MQTT Message Type CONNECT
	//剩余长度(不包括固定头部)
	do
	{
		u8 encodedByte = DataLen % 128;
		DataLen = DataLen / 128;
		// if there are more data to encode, set the top bit of this byte
		if ( DataLen > 0 )
			encodedByte = encodedByte | 128;
		mqtt_txbuf[mqtt_txlen  ] = encodedByte;
	}while ( DataLen > 0 );
    	
	//可变报头
	//协议名
    mqtt_txbuf[mqtt_txlen  ] = 0;        	// Protocol Name Length MSB    
    mqtt_txbuf[mqtt_txlen  ] = 4;           // Protocol Name Length LSB    
    mqtt_txbuf[mqtt_txlen  ] = 'M';        	// ASCII Code for M    
    mqtt_txbuf[mqtt_txlen  ] = 'Q';        	// ASCII Code for Q    
    mqtt_txbuf[mqtt_txlen  ] = 'T';        	// ASCII Code for T    
    mqtt_txbuf[mqtt_txlen  ] = 'T';        	// ASCII Code for T    
	//协议级别
    mqtt_txbuf[mqtt_txlen  ] = 4;        		// MQTT Protocol version = 4   对于 3.1.1 版协议,协议级别字段的值是 4(0x04)   
	//连接标志
    mqtt_txbuf[mqtt_txlen  ] = 0xc2;        	// conn flags 
    mqtt_txbuf[mqtt_txlen  ] = 0;        		// Keep-alive Time Length MSB    
    mqtt_txbuf[mqtt_txlen  ] = 100;        	// Keep-alive Time Length LSB  100S心跳包    保活时间
	
    mqtt_txbuf[mqtt_txlen  ] = BYTE1(ClientIDLen);// Client ID length MSB    
    mqtt_txbuf[mqtt_txlen  ] = BYTE0(ClientIDLen);// Client ID length LSB  	
	memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen);
    mqtt_txlen  = ClientIDLen;
    
    if(UsernameLen > 0)
    {   
        mqtt_txbuf[mqtt_txlen  ] = BYTE1(UsernameLen);		//username length MSB    
        mqtt_txbuf[mqtt_txlen  ] = BYTE0(UsernameLen);    	//username length LSB    
		memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen);
        mqtt_txlen  = UsernameLen;
    }
    
    if(PasswordLen > 0)
    {    
        mqtt_txbuf[mqtt_txlen  ] = BYTE1(PasswordLen);		//password length MSB    
        mqtt_txbuf[mqtt_txlen  ] = BYTE0(PasswordLen);    	//password length LSB  
		memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen);
        mqtt_txlen  = PasswordLen; 
    }    
	
  
    memset(mqtt_rxbuf,0,mqtt_rxlen);
    MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
    for(j=0;j<10;j  )
    {
        delay_ms(50);
        if(USART3_RX_FLAG)
        {
            memcpy((char *)mqtt_rxbuf,USART3_RX_BUFFER,USART3_RX_CNT);
            
            //memcpy
           
             for(i=0;i<USART3_RX_CNT;i  )USART1_Printf("%#x ",USART3_RX_BUFFER[i]);
            
            USART3_RX_FLAG=0;
            USART3_RX_CNT=0;
        }
        //CONNECT
        if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功			   
        {
            return 0;//连接成功
        }
    }
    
	return 1;
}

/*
函数功能: MQTT订阅/取消订阅数据打包函数
函数参数:
    topic       主题   
    qos         消息等级 0:最多分发一次  1: 至少分发一次  2: 仅分发一次
    whether     订阅/取消订阅请求包 (1表示订阅,0表示取消订阅)
返回值: 0表示成功 1表示失败
*/
u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether)
{    
    u8 i,j;
	mqtt_txlen=0;
    int topiclen = strlen(topic);
	
	int DataLen = 2   (topiclen 2)   (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度
	//固定报头
	//控制报文类型
    if(whether)mqtt_txbuf[mqtt_txlen  ] = 0x82; //消息类型和标志订阅
    else	mqtt_txbuf[mqtt_txlen  ] = 0xA2;    //取消订阅

	//剩余长度
	do
	{
		u8 encodedByte = DataLen % 128;
		DataLen = DataLen / 128;
		// if there are more data to encode, set the top bit of this byte
		if ( DataLen > 0 )
			encodedByte = encodedByte | 128;
		mqtt_txbuf[mqtt_txlen  ] = encodedByte;
	}while ( DataLen > 0 );	
	
	//可变报头
    mqtt_txbuf[mqtt_txlen  ] = 0;			//消息标识符 MSB
    mqtt_txbuf[mqtt_txlen  ] = 0x0A;        //消息标识符 LSB
	//有效载荷
    mqtt_txbuf[mqtt_txlen  ] = BYTE1(topiclen);//主题长度 MSB
    mqtt_txbuf[mqtt_txlen  ] = BYTE0(topiclen);//主题长度 LSB   
	memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen);
    mqtt_txlen  = topiclen;
    
    if(whether)
    {
       mqtt_txbuf[mqtt_txlen  ] = qos;//QoS级别
    }
    
    for(i=0;i<10;i  )
    {
        memset(mqtt_rxbuf,0,mqtt_rxlen);
		MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
        for(j=0;j<10;j  )
        {
            delay_ms(50);
            if(USART3_RX_FLAG)
			{
                memcpy((char *)mqtt_rxbuf,(char*)USART3_RX_BUFFER,USART3_RX_CNT);
				USART3_RX_FLAG=0;
				USART3_RX_CNT=0;
			}
			
			if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功			   
			{
				return 0;//订阅成功
			}
        }
    }
	return 1; //失败
}

//MQTT发布数据打包函数
//topic   主题 
//message 消息
//qos     消息等级 
u8 MQTT_PublishData(char *topic, char *message, u8 qos)
{  
    int topicLength = strlen(topic);    
    int messageLength = strlen(message);     
    static u16 id=0;
	int DataLen;
	mqtt_txlen=0;
	//有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度
	//QOS为0时没有标识符
	//数据长度             主题名   报文标识符   有效载荷
    if(qos)	DataLen = (2 topicLength)   2   messageLength;       
    else	DataLen = (2 topicLength)   messageLength;   

    //固定报头
	//控制报文类型
    mqtt_txbuf[mqtt_txlen  ] = 0x30;    // MQTT Message Type PUBLISH  

	//剩余长度
	do
	{
		u8 encodedByte = DataLen % 128;
		DataLen = DataLen / 128;
		// if there are more data to encode, set the top bit of this byte
		if ( DataLen > 0 )
			encodedByte = encodedByte | 128;
		mqtt_txbuf[mqtt_txlen  ] = encodedByte;
	}while ( DataLen > 0 );	
	
    mqtt_txbuf[mqtt_txlen  ] = BYTE1(topicLength);//主题长度MSB
    mqtt_txbuf[mqtt_txlen  ] = BYTE0(topicLength);//主题长度LSB 
	memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题
    mqtt_txlen  = topicLength;
        
	//报文标识符
    if(qos)
    {
        mqtt_txbuf[mqtt_txlen  ] = BYTE1(id);
        mqtt_txbuf[mqtt_txlen  ] = BYTE0(id);
        id  ;
    }
	memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength);
    mqtt_txlen  = messageLength;
        
	MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
    return mqtt_txlen;
}

void MQTT_SentHeart(void)
{
	MQTT_SendBuf((u8 *)parket_heart,sizeof(parket_heart));
}

void MQTT_Disconnect(void)
{
	MQTT_SendBuf((u8 *)parket_disconnet,sizeof(parket_disconnet));
}

void MQTT_SendBuf(u8 *buf,u16 len)
{
	USARTx_DataSend(USART3,buf,len);
}	

0 人点赞