STM32+ESP8266+MQTT 協議連線 OneNet 物聯網平臺
一、環境介紹
微控制器採用: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 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);
}
複製程式碼
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 + %2B
2 空格%20
3 / %2F
4 ? %3F
5 % %25
6 # %23
7 & %26
8 = %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;
}
}
}
複製程式碼
七、裝置登入執行效果
登入成功之後,網頁會顯示線上狀態。
按下開發按鍵上傳煙霧資料到伺服器效果:


- 那些 Go 語言發展歷史上的重大決策
- 從趨勢到挑戰,一站式解讀作業系統運維和可觀測性
- 百萬級 Topic,騰訊雲的 Apache Pulsar 穩定性實踐
- Apache Doris 在思必馳的應用優化實踐:海量語音通話資料下,實時、離線一體的數倉架構設計實踐
- 愛數正式開源認知智慧開發框架 KWeaver
- 運維智慧化的三大關鍵技術
- “抄我的還‘反捅’我一刀”,Gary Marcus 發文駁斥圖靈獎得主 Yann LeCun
- 當出海成為必選項,企業如何構建全場景全生態技術底座?
- 數智底座必備能力三:快速構建創新應用
- Docker 多階段構建實戰 (multi-stage builds)
- 工作筆記之 SELECT 語句在 SAP ABAP 中的用法總結(上)
- 經久不衰的設計定律是不要讓我思考的設計
- 不要指望下一個像 GPT 這樣的大型語言模型會民主化
- Java 近期新聞:Helidon Níma、Spring Framework、MicroProfile、MicroStream、Kotlin 和 Piranha
- 一文入門 jQuery
- C 學習 ---__libc_open 函式的原理
- 監控系統工作原理
- 甲骨文新微服務框架 Helidon Níma:使用虛擬執行緒實現高效能
- 【雲原生 | 從零開始學 Kubernetes】二、使用 kubeadm 搭建 K8S 叢集
- Elasticsearch 聚合學習之四:結果排序