STM32+ESP8266+MQTT 協議連線 OneNet 物聯網平臺

語言: CN / TW / HK

一、環境介紹

微控制器採用:STM32F103C8T6

上網方式:採用 ESP8266,也可以使用其他裝置代替,只要支援 TCP 協議即可。比如:GSM 模組、有線網絡卡等。

開發軟體:keil5

硬體連線功能:ESP8266 接在 STM32 的串列埠 3 上。通過 AT 指令與 ESP8266 進行通訊。

二、功能介紹

2.1 功能說明

通過 OneNet 物聯網伺服器實現裝置資料遠端上傳、下發,實現資料互動(不清楚 OneNet 物聯網伺服器功能的可以百度一下進入官網看簡介)。之前的 OneNet 伺服器不支援標準 MQTT 協議登入的,現在官網更新之後支援標準的 MQTT 協議,本篇文章介紹使用 STM32+ESP8266 使用標準 MQTT 協議登入 Onenet 伺服器,實現資料互動。實現步驟 OneNet 官方提供了很詳細的文件,可以參考一下。

文件地址: https://open.iot.10086.cn/devdoc/

2.2 硬體資源

在當前使用的開發板上有 4 盞 LED 燈、一個蜂鳴器、4 個按鍵,ESP8266 型號是 ESP-12F,STM32 型號是:STM32F103C8T6。

三、OneNet 支援的 MQTT 協議版本

目前 OneNet 伺服器支援 MQTT 3.1.1 版本,MQTT 協議官網: http://mqtt.org/?spm=a2c4g.11186623.2.11.19083f86gxhJ7h

報文支援情況: 支援 connect、subscribe、publish、ping、unsubscribe、disconnect 等報文,不支援 pubrec、pubrel、pubcomp 報文。

四、登入 OneNet 伺服器建立物聯網產品

沒有註冊賬號的,需要提前登入官網註冊賬號,再進入下面步驟:

這裡根據自己產品情況填寫。

產品建立成功之後,點選產品名稱,跳轉頁面,繼續新增裝置。

下面選擇儀表盤的資料來源,根據自己建立的資料點選擇。

建立一個文字控制元件,顯示資料點更新的時間,方便除錯。

OneNte 有手機版本的 APP,登入之後也可以看到該頁面。

下載地址: https://open.iot.10086.cn/doc/book/device-develop/multpro/sdk-doc-tool/APP.html

下面是手機上登入 APP 看到的介面效果:

五、OneNet 伺服器 MQTT 登入地址與訂閱主題相關格式介紹

官網介紹文件地址: https://open.iot.10086.cn/doc/mqtt/book/get-start/connect.html

5.1 MQTT 伺服器登入地址

目前 MQTT 協議支援兩個 IP 地址和埠號,一個需要加密、一個不需要加密。

注意:微控制器上移植加密演算法很麻煩,這裡採用不需要加密的埠。(IP 地址: 183.230.40.96 埠: 1883)

5.2 MQTT 登入的:裝置 ID、使用者名稱稱、密碼 格式引數

上面圖片裡說明了,OneNet 的裝置引數與標準 MQTT 協議的登入引數對應關係。 OneNet 的裝置引數,在裝置頁面可以去檢視。

登入密碼生成看下面步驟:

注意:該工具在 win10 系統執行可能會提示非信任程式,點選任要執行即可。

下面是生成 MQTT 登入密匙的工具使用示例。

注意:工具中填的引數說明請看文件介紹。

res 選項引數的格式: products/{產品 ID}/devices/{裝置名稱}

et 是設定 token 過期時間:算出 1970-1-1 到你想要設定的到期時間,單位是秒,填入即可。

比如: 超時時間設定為 2020-07-20 ,那麼,這裡填入的秒就是:1970-1-1 到 2020-07-20 之間的秒單位時間。

Linux 下程式碼:

#include <stdio.h>#include <time.h> #include <time.h>
int main(){ time_t time_sec; time_sec=time(NULL); //當前的秒單位時間--UTC時間 printf("當前時間(秒):%ld\n",time_sec); printf("加30天的時間(秒):%ld\n",time_sec+30*24*60*60); return 0;}

複製程式碼

key 的引數格式: 就是裝置建立之後,在裝置詳情頁的 key

工具生成的結果值,直接當做 MQTT 登入的密碼。

5.3 主題訂閱格式

文件地址: https://open.iot.10086.cn/doc/mqtt/book/device-develop/protocol.html

5.4 裝置保活時間

5.5 向伺服器傳資料點

六、核心程式碼

6.1 matt.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 00const 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);}

複製程式碼

6.2 mqtt.h 程式碼

#ifndef __FY_MQTT_H_#define __FY_MQTT_H_
#include "stm32f10x.h"#include "string.h"#include "stdio.h"#include "stdlib.h"#include "stdarg.h"#include "delay.h"#include "usart.h"
#define BYTE0(dwTemp) (*( char *)(&dwTemp))#define BYTE1(dwTemp) (*((char *)(&dwTemp) + 1))#define BYTE2(dwTemp) (*((char *)(&dwTemp) + 2))#define BYTE3(dwTemp) (*((char *)(&dwTemp) + 3)) //使用者名稱初始化void OneNet_LoginInit(char *ProductKey,char *DeviceName,char *DeviceSecret);//MQTT協議相關函式宣告u8 MQTT_PublishData(char *topic, char *message, u8 qos);u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether);void MQTT_Init(void);u8 MQTT_Connect(char *ClientID,char *Username,char *Password);void MQTT_SentHeart(void);void MQTT_Disconnect(void);void MQTT_SendBuf(u8 *buf,u16 len);#endif

複製程式碼

6.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 "esp8266.h"#include "mqtt.h"
/*序號 符號 編碼1 + %2B2 空格%203 / %2F4 ? %3F5 % %256 # %237 & %268 = %3D*/
//OneNet物聯網伺服器的裝置資訊#define MQTT_ClientID "mq2"#define MQTT_UserName "361594"
#define MQTT_PassWord "version=2018-10-31&res=products%2F361594%2Fdevices%2Fmq2&et=1597492895&method=sha1&sign=uqvA0KkjXw0FlN01aT6fWrGBLGw%3D"
//訂閱與釋出的主題//格式:$sys/{產品ID}/{裝置名稱}/##define SET_TOPIC "$sys/361594/mq2/#" //訂閱裝置所有資訊
//格式: $sys/{產品ID}/{裝置名稱}/dp/post/json#define POST_TOPIC "$sys/361594/mq2/dp/post/json" //釋出
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 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("OnePlus5T","1126626497","183.230.40.96",1883,1)); } //2. MQTT協議初始化 MQTT_Init(); //3. 連線OneNet伺服器 while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)) { USART1_Printf("OneNet伺服器連線失敗,正在重試...\n"); delay_ms(500); } USART1_Printf("OneNet伺服器連線成功.\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,"{\"id\":1,\"dp\":{\"mq2\":[{\"v\":50}]}}"); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("傳送狀態1\r\n"); } else if(key==3) { time_cnt=0; sprintf(mqtt_message,"{\"id\":1,\"dp\":{\"mq2\":[{\"v\":80}]}}"); 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; } }}

複製程式碼

七、裝置登入執行效果

登入成功之後,網頁會顯示線上狀態。

按下開發按鍵上傳煙霧資料到伺服器效果: