STM32+ESP8266+MQTT協定連線騰訊物聯網開發平臺

2021-05-17 23:00:02

一、環境介紹

微控制器採用: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原始碼:

#!/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=="))

得到的登入資訊如下:

clientid: 8O76VHCU7YSmartAgriculture
username: 8O76VHCU7YSmartAgriculture;12010126;J4MCD;1623766532
password: a962b484079864239148b255281d54372aa66247aa8d6259d11aa6fef650fd5b;hmacsha256

 

4.4 瞭解主題上報和訂閱的格式

登入之前需要先了解如何訂閱裝置的主題和上報的資料流格式。

如果裝置端想要得到APP頁面的按鈕狀態就需要訂閱屬性下發和屬性上報的響應,主題格式就是這樣的:

格式:
$thing/down/property/8O76VHCU7Y/裝置名稱
範例:
$thing/down/property/8O76VHCU7Y/SmartAgriculture

如果裝置端想要像APP頁面上傳資料,那麼就需要使用屬性上報--釋出主題:

格式:
$thing/up/property/8O76VHCU7Y/${deviceName}
範例:
$thing/up/property/8O76VHCU7Y/SmartAgriculture

裝置端向APP頁面上報屬性時,需要上傳具體的資料,資料流的格式如下:

官方檔案:  https://cloud.tencent.com/document/product/1081/34916

比如: 我的產品裡有溫度、溼度、電機三個裝置,我可以選擇一次上傳3個裝置的資訊,資料格式就這樣寫:

{"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檔案

#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:%d\n",ESP8266_STA_TCP_Client_Mode("OnePlus5T","1126626497","183.230.40.16",8883,1));
      
      //非加密埠
      USART1_Printf("WIFI:%d\n",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("傳送狀態1\r\n");
        }
        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("傳送狀態0\r\n");
        }  

        if(USART3_RX_FLAG)
        {
            USART3_RX_BUFFER[USART3_RX_CNT]='\0';
            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

#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);
}