【STM32×ESP8266】MQTTサーバーに接続(メッセージ、一部ソースコード解析あり)

MQTT プロトコルは、モノのインターネットにとって非常に重要な伝送プロトコルです. 使用方法は非常に重要です. 理解できない場合は、ここをクリックして学習できます. ここでは簡単な紹介をします. 同時に、MQTT 3.1.1 プロトコルの中国語版の pdf へのリンクがあります. プロトコルの最下層に興味のある学生は、ダウンロードして学習できます. 同時に、次の実装機能このメッセージに基づいて実装されます
ここでプロジェクト全体をダウンロードします(ポイントを獲得)。メッセージの確認とデバッグは簡単ではありません。サポートをお願いしますこのプロジェクトは、Wildfire の 3-DHT11 温度と湿度をコンピュータ ネットワーク アシスタントにアップロードし、MQTT 部分を追加することに基づいており、主に内部でTCP 接続 + 透過的な伝送設定を使用しています。
関数の例: esp8266 を制御して、 stm32 を介してAlibaba Cloud MQTT サーバー/独自のサーバー
(EMQ など) 上に構築された MQTT サーバー/他のパブリック MQTT サーバー
に接続します。ユーザーは、 mqtt_config.hファイル内の MQTT サーバーの関連情報のみを変更する必要がありますAlibaba Cloud MQTT サーバーへの接続方法
内容は、この記事にあります.必要に応じて、使用方法を参照してください。

開発バージョン: Wildfire Guide + ESP8266
ESP8266 要件: 元のファームウェアを使用します。MQTT サーバーに特別に接続されたファームウェアを書き込む必要はありません。

1. MQTT プロトコル メッセージ識別

1. CONNECT 接続メッセージ

1.1 クライアント ID

ClientId は MQTT クライアントの識別子です。MQTT サーバーは、この識別子を使用してクライアントを識別します。したがって、ClientId は独立している必要があります2 つの MQTT クライアントが同じ ClientId を使用する場合、サーバーはそれらを同じクライアントとして扱います。

1.2 セッションのクリア

① このフラグが設定されていない場合( cleanSession = "false" && QoS > 0 )、サーバーはクライアントによって確認されていないメッセージを保存し、クライアントにメッセージを再度送信しようとし、クライアントを待ちます。再度送信する 情​​報を確認してください。
このフラグが設定されている場合 ( cleanSession = "true" )、サーバーはクライアントがメッセージの受信を確認する必要がなく、メッセージを保存しませんこの場合、サーバーが送信したメッセージをクライアントが見逃しても、サーバーがメッセージを再送信する方法はありません。

1.3 ハートビート間隔

クライアントがサーバーに情報を送信しない場合は、定期的にサーバーにメッセージを送信しますハートビート要求の機能は、現在のクライアントがまだオンラインであることをサーバーに通知することです。
たとえば、ハートビート間隔が 60 秒の場合。その後、サーバーがクライアントから発行されたメッセージまたはハートビート要求 (PINGREQ) 要求を 90 秒以内に受信しない場合、サーバーはクライアントがオフラインになったと見なします。

2. PUBLISH パブリッシュ メッセージ メッセージ

1.1 予約フラグ

デフォルトでは、クライアントがトピックをサブスクライブしても、トピックに関する情報をすぐには受け取りません。クライアントがトピックをサブスクライブし、サーバーがトピックに関する新しい情報を受信した後でのみ、サーバーは受信した最新のトピック情報をクライアントにプッシュします。
しかし場合によっては、クライアントが topic にサブスクライブした直後にトピックに関する情報を受け取る必要がありますこのとき、予約フラグの情報が必要です
各トピックは「予約済みメッセージ」を 1 つだけ持つことができます。クライアントが「予約済みメッセージ」を更新したい場合は、サーバーが新しい「予約済みメッセージ」を上書きするように、トピックに新しい「予約済みメッセージ」を送信する必要があります。保留メッセージ」の . トピックの「予約済みメッセージ」を削除し
たい場合は、**空の「予約済みメッセージ」**をトピックに公開できます。

1.2 QoS – QoS クラス

QoS = 0 –> 最大 1 回送信する
QoS = 1 –> 少なくとも 1 回送信する
QoS = 2 –> 1 回受信することを保証する

① QoS = 0 -> 最大 1 回送信

QoS = 0 の場合、MQTT プロトコルは、すべてのメッセージを送信できることを保証しません。つまり、MQTT サーバーとクライアントは、メッセージ送信が成功したかどうかを確認およびチェックしませんメッセージを正常に送信できるかどうかは、ネットワーク環境が安定しているかどうかに依存します。送信者は、送信したメッセージが正しく受信できるかどうかを確認しません

② QoS = 1 –> 少なくとも 1 回送信する

メッセージを受信者に送信した後、送信者は受信者からの確認を待ちます。受信側がメッセージを正常に受信した後、確認メッセージ PUBACK を送信側に送信します送信者は、この PUBACK 確認メッセージを受信すると、メッセージが正常に受信されたことを認識します。
一定期間後、送信者がPUBACK メッセージを受信しない場合、送信者はメッセージを再送信し、受信者からの PUBACK 確認メッセージを再び待ちます。したがって、QoS = 1 の場合、送信者は、受信者から PUBACK 確認メッセージを受信する前に、同じメッセージを繰り返し送信します
ここに画像の説明を挿入

③ QoS = 2 → 1回受信保証

QoS = 2 のトランシーバーは比較的複雑です。送信者は、受信者からの 2 つのメッセージ確認が必要ですしたがって、MQTT QoS レベル 2 は最も安全なサービス レベルであり、最も遅いサービス レベルでもあります。
QoS = 2 メッセージを受信した後、受信者は PUBREC メッセージを応答として返します。PUBREC メッセージを受信した後、送信者はメッセージを保存し、応答として PUBREL メッセージを返します。受信側が PUBREL メッセージを受信すると、PUBCOMP メッセージを送信側に返信します。ここまでで、QoS2 MQTT メッセージの送信は終了しました。
ここに画像の説明を挿入

3.意志

クライアントが予期せず切断すると、サーバーはクライアントの意志を公開できます。
ウィルには、ウィル サブジェクト、ウィル メッセージ、ウィル QoS、ウィル 予約が含まれます。

3.1 意志操作に関する提案

MQTT クライアントがあるとします。そのクライアント ID は client-1 です。その遺言の件名は "client-1-will" です

  1. client-1 がサーバーに接続するとき、CONNECT メッセージ内の will メッセージは「オフライン」です。そして、probate が true に設定されています

  2. client-1 がサーバーに正常に接続すると、すぐに「オンライン」メッセージを will トピック「client-1-will」にパブリッシュします。また、このメッセージを投稿すると、予約済みフラグが true に設定されます。このようにして、client-1 がオンラインである限り、「client-1-will」にサブスクライブするすべてのデバイスは、デバイスがオンラインであるというメッセージ「online」を受信できます

  3. client-1 が予期せずオフラインになった場合。次に、デバイスが「client-1-will」にサブスクライブするとすぐに、デバイスがオフラインであるというメッセージ「オフライン」を受信します

  4. client-1 がconnected に戻ると、will トピック「client-1-will」の予約済みメッセージが「online」に変更され、「client-1-will」にサブスクライブするすべてのデバイスが次のメッセージを受信します。デバイスはオンライン「オンライン」です。

2. プロジェクト分析

注: このプロジェクトの実践対象は、自分で構築した MQTT サーバー EMQ です. Alibaba Cloud MQTT サーバーへの接続方法の内容は、この記事にあります. 必要に応じて、使用方法を確認することができます.

1. ESP8266 が WIFI に接続 + MQTT サーバーに接続

bsp_esp8266_test.h

/********************************** 用户需要设置的参数**********************************/
#define      macUser_ESP8266_ApSsid             ""      //要连接的热点的名称
#define      macUser_ESP8266_ApPwd              ""      //要连接的热点的密钥

#define      macUser_ESP8266_TcpServer_IP       IP      //要连接的服务器的 IP(在 mqtt_config.h 中定义)
#define      macUser_ESP8266_TcpServer_Port     PORT    //要连接的服务器的端口(在 mqtt_config.h 中定义)

2、mqtt_config.h

2.1 他の MQTT サーバーに接続するための切り替え

#define	USE_Aliyun_MQTT					0			// 是否使用阿里云的免费MQTT服务器

2.2 デバッグスイッチ

#define DEBUG_1							1			// 为 0 屏蔽内部所有串口输出信息
#define DEBUG_2							1			// 查看接收/发送的报文

2.3 バッファサイズを変更する

#define MAX_BUF_SIZE					2048		// 接收/发送的数据缓冲区
#define MAX_THEME_NUM					10			// 最大存储订阅主题数目

2.4 移植に必要なもの

#define Rx_Finish_Flag					strEsp8266_Fram_Record.InfBit.FramFinishFlag	// 串口接收完成标志
#define	Rx_Buffer						strEsp8266_Fram_Record.Data_RX_BUF				// 串口接收缓冲区
#define	Rx_Buffer_Len					strEsp8266_Fram_Record.InfBit.FramLength		// 串口接收数据长度
#define	ESP8266_USART					macESP8266_USARTx								// ESP8266的串口
/*
	提供一个能够准确计算一长串16进制数据的长度的函数(strlen函数有些情况统计的长度要比实际长度小)
	函数格式:int UpdateStrlen_uint8_t(const void* source),函数返回值是实际长度值(该函数在my_string.c中)
*/
#define	Count_Hex_Num(source)			UpdateStrlen_uint8_t(source)
/*
	提供发送报文函数,该函数要一个一个字节的接收
*/
#define	Send_Message(usart, ch)			Usart_SendByte(usart, ch)

2.5 MQTT 接続パラメータのユーザー詳細設定

ハートビートサイクルの設定意志を使用するかどうか、および意志に関する関連情報を含む(Alibaba Cloud MQTT サーバーは、意志を使用すると接続されず、メッセージが返されないため、意志を使用できないようです) 、ClientID を設定し匿名でログインするかどうかを決定します(Aliyun MQTT サーバーはアカウントとパスワードでのみログインできます)

#define Set_KeepAlive 					60			// 设置心跳周期

#if(USE_Aliyun_MQTT == 1)		/* 使用阿里云MQTT服务器 */
#define Enable_Will_Topic				0			// 阿里云MQTT服务器应该是不能使用遗嘱的,置 0
#define ClientID						"zyt"		// 自定义
#define	Enable_Username_And_Password	1			// 必须置 1,不能置零!!!

#else	/* USE_Aliyun_MQTT == 0	使用其他的MQTT服务器 */
#define Enable_Will_Topic				1			// 是否使用遗嘱主题
#define MQTTClientID					"zyt"		// 自定义
#define	Enable_Username_And_Password	1			// 是否使用用户名密码,有些MQTT服务器支持匿名登录
#endif	/* USE_Aliyun_MQTT */

#if (Enable_Will_Topic == 1)	/* 使用遗嘱主题 */
#define	Will_Topic_Qos					Qos1		// 遗嘱主题 Qos 等级
#define	Will_Topic_Name					"/user/will"// 遗嘱主题名字
#define	Will_Topic_Message				"off_line"	// 遗嘱主题的消息内容
#endif	/* Enable_Will_Topic */

3、mqtt.h

この定義はvoid MQTT_ReceiveMsg(u8 mqtt_msg_type, u8 *mqtt_rxbuf)関数で使用され、主にさまざまな送信メッセージの戻りメッセージの長さを区別するために使用されます。詳細については、以下を参照してください。
この定義を変更しないでください。!

/* ========================== MQTT报文类型 ========================== */
#define MQTT_TypeCONNECT 				1			//请求连接
#define MQTT_TypeCONNACK 				2			//请求应答
#define MQTT_TypePUBLISH 				3			//发布消息
#define MQTT_TypePUBACK 				4			//发布应答
#define MQTT_TypePUBREC 				5			//发布已接收,保证传递1
#define MQTT_TypePUBREL				 	6			//发布释放,保证传递2
#define MQTT_TypePUBCOMP 				7			//发布完成,保证传递3
#define MQTT_TypeSUBSCRIBE 				8			//订阅请求
#define MQTT_TypeSUBACK 				9			//订阅应答
#define MQTT_TypeUNSUBSCRIBE 			10			//取消订阅
#define MQTT_TypeUNSUBACK 				11			//取消订阅应答
#define MQTT_TypePINGREQ 				12			//ping请求
#define MQTT_TypePINGRESP 				13			//ping响应
#define MQTT_TypeDISCONNECT 			14			//断开连接
#define MQTT_WriteMsg					15			//等待接收订阅的消息(自定义的)

これらのマクロ定義は、CONNECT 接続メッセージのフラグ ビットと、遺言の件名の設定を設定するためのものです。遺言書を使用する必要がない場合は、MQTT_StaWillFlag を 0 に設定するだけで、遺言書に関する内容は無効になります。MQTT_StaUserNameFlag と MQTT_StaPasswordFlag を 0 に設定するだけで、ユーザー名とパスワードを使用しない同じ操作です。
ここに画像の説明を挿入
CONNECT 接続メッセージの内容は、実際の要件に応じて変更できます。CONNACK メッセージの戻りコードは変更できません。

/* ========================== CONNECT报文设置 ========================== */
#define MQTT_StaCleanSession 			1 				//清理会话
#define MQTT_StaWillFlag 				1				//遗嘱标志
#define MQTT_StaWillQoS 				0 				//遗嘱QoS连接标志的第4和第3位。
#define MQTT_StaWillRetain 				0 				//遗嘱保留
#define MQTT_StaUserNameFlag 			1 				//用户名标志 User Name Flag
#define MQTT_StaPasswordFlag 			1 				//密码标志 Password Flag
#define MQTT_KeepAlive 					120				//心跳周期
#define MQTT_ClientIdentifier 			"111" 			//客户端标识符 Client Identifier
#define MQTT_WillTopic 					"yizhu" 		//遗嘱主题 Will Topic
#define MQTT_WillMessage 				"zheshiyizhu" 	//遗嘱消息 Will Message
#define MQTT_UserName 					"zyt" 			//用户名 User Name
#define MQTT_Password 					"010823"		//密码 Password
 
/* ========================== CONNACK报文返回码 ========================== */
#define Connect_Accept						0x00		// 连接已接受
#define	Connect_Refuse_Version				0x01		// 连接已拒绝,不支持的协议版本
#define	Connect_Refuse_ClientId				0x02		// 连接已拒绝,不合格的客户端标识符
#define	Connect_Refuse_Sever_Unavailable 	0x03		// 连接已拒绝,服务端不可用
#define	Connect_Refuse_Acc_Or_Pass			0x04		// 连接已拒绝,无效的用户名或密码

4・mqtt.c

4.1 MQTT メッセージの固定ヘッダー関数を生成します: GetDataFixedHead() (ユーザーが呼び出す必要はありません)

この関数はメッセージを生成するときに使用され、ユーザー層はこの関数を呼び出す必要はありません。
ここに画像の説明を挿入
残りの長さの値は各メッセージの長さに関連するため、この部分は通常最後に生成され、関数がstatic int AddRemainingLength(void* mqtt_txbuf, uint8_t cps_len)責任を負います。

/**
	* @brief  生成固定报头
	*
	* @param  MesType: mqtt报文类型(详见mqtt.h)
	* @param	DupFlag: 重发标志
	*		@arg	0: 客户端或服务端第一次请求发送这个报文
	*		@arg	1: 可能是一个早前报文请求的重发
	* @param	QosLevel: Qos等级
	* @param	Retain: 保留标志(设置后在订阅了该主题后马上接收到一条该主题的信息)
	*		@arg	0: 不发布保留消息
	*		@arg	1: 发布保留消息
	*
	* @retval 返回固定报头(8bit/1位)
*/
uint8_t GetDataFixedHead(unsigned char MesType, unsigned char DupFlag, unsigned char QosLevel, unsigned char Retain)
{
    
    
	unsigned char dat = 0;
	
	dat = (MesType & 0x0f) << 4;
	dat |= (DupFlag & 0x01) << 3;
	dat |= (QosLevel & 0x03) << 1;
	dat |= (Retain & 0x01);
	
	return dat;
}

4.2 MQTT メッセージの固定ヘッダーに残りの長さ部分の関数を生成します: AddRemainingLength() (ユーザーが呼び出す必要はありません)

ここに画像の説明を挿入

/**
	* @brief  往发送的报文中插入剩余长度
	*
	* @param  mqtt_txbuf: 要插入剩余长度的报文缓存区
	* @param  cps_len: 补偿长度(使用这个位通常是报文最后面是0,要额外加上去)
	*
	* @retval 返回报文总长度
*/
static int AddRemainingLength(void* mqtt_txbuf, uint8_t cps_len)
{
    
    
    uint8_t* txbuf = mqtt_txbuf;
    uint8_t last = 1, cur = 0, next_1 = 0, next_2 = 0;
    int src_len = -1;
    int i;
	int remain_len, remain_num;

	// 获取该数组的长度,当检测到连续三个元素都为0的时候退出循环
    for (i = 0; ; i++)
    {
    
    
        if (i)
            last = cur;

        cur = *(txbuf + i);
        next_1 = *(txbuf + i + 1);
        next_2 = *(txbuf + i + 2);
        if (last == 0 && cur == 0 && next_1 == 0 && next_2 == 0)
            break;
        else
            src_len++;
    }
		
	// 得到剩余长度值
    remain_len = src_len + cps_len - 1;
		
	// 将剩余长度值插入进发送数组中
	if(remain_len <= 127)
		remain_num = 1;
	else if(remain_len <= 128 && remain_len >= 16383)
		remain_num = 2;
	
	for(i = src_len; i > 0; i--)
			txbuf[i + remain_num] = txbuf[i];
	switch(remain_num)
	{
    
    
		case 1:
			txbuf[1] = remain_len;
			break;
		case 2:
			txbuf[1] = 0x80 | (remain_len / 128);
			txbuf[2] = remain_len % 128;
			break;
	}
	return (remain_len + remain_num + 1);
}

4.3 MQTT サーバー関数の接続: MQTT_Connect()

この関数は、CONNECT 接続メッセージを送信する際に、サーバーから返された CONNACK 確認メッセージも受信します。

ここに画像の説明を挿入
CONNACK 確認接続メッセージの最も重要な部分は、4 番目のバイトの接続戻りコードです。
ここに画像の説明を挿入
ここに画像の説明を挿入

#define MQTT_Connect()										SendCONNECT()
/**
	* @brief  获取连接的数据包
	*
	* @param  None
	*
	* @retval 返回连接返回码
	*		@arg	Connect_Accept: 连接已接受
	*		@arg	Connect_Refuse_Version:	连接已拒绝,不支持的协议版本
	*		@arg	Connect_Refuse_ClientId:	连接已拒绝,不合格的客户端标识符
	*		@arg	Connect_Refuse_Sever_Unavailable:	连接已拒绝,服务端不可用
	*		@arg	Connect_Refuse_Acc_Or_Pass:	连接已拒绝,无效的用户名或密码
*/
uint8_t SendCONNECT(void)
{
    
    
	unsigned int i,len,lennum = 0;
	unsigned char *msg;
	
	memset(strEsp8266_Fram_Record.Data_RX_BUF, 0, RX_BUF_MAX_LEN);
	
/* 1、固定报头 */
	mqtt_txbuf[0] = SendFixedHead(MQTT_TypeCONNECT, 0, 0, 0);
	// 最后再添加剩余长度的值
	
/* 2、可变报头 */
	// 2.1 协议名
	mqtt_txbuf[1] = 0x00;
	mqtt_txbuf[2] = 0x04;
	mqtt_txbuf[3] = 'M';
	mqtt_txbuf[4] = 'Q';
	mqtt_txbuf[5] = 'T';
	mqtt_txbuf[6] = 'T';
	// 2.2 协议级别	0x04:MQTT 3.1.1
	mqtt_txbuf[7] = 0x04;
	// 2.3 连接标志
	mqtt_txbuf[8] = 0 | (MQTT_StaCleanSession << 1) | (MQTT_StaWillFlag << 2) 
							| (MQTT_StaWillQoS << 3) | (MQTT_StaWillRetain << 5) 
							| (MQTT_StaPasswordFlag << 6) |(MQTT_StaUserNameFlag << 7);
	// 2.4 保持连接时间(心跳周期)
	mqtt_txbuf[9] = MQTT_KeepAlive >> 8;
	mqtt_txbuf[10] = MQTT_KeepAlive;
	
/* 3、有效载荷 */
	// 3.1 客户端标识符(ClientId)
	// 客户端标识符 = 0,必须同时将清理会话标志 MQTT_StaCleanSession 设置为 1
	len = strlen(MQTT_ClientIdentifier);
	mqtt_txbuf[11] = len >> 8;
	mqtt_txbuf[12] = len;
	msg = (u8 *)MQTT_ClientIdentifier;
	for(i = 0; i < len; i++)
	{
    
    
		mqtt_txbuf[13 + i] = msg[i];
	}
	lennum += len;
#if (MQTT_StaWillFlag == 1)	// 3.2 遗嘱主题
	len = strlen(MQTT_WillTopic);
	mqtt_txbuf[12 + lennum + 1] = len >> 8;
	mqtt_txbuf[12 + lennum + 2] = len;
	lennum += 2;
	msg = (u8 *)MQTT_WillTopic;
	for(i = 0;i<len;i++)
	{
    
    
		mqtt_txbuf[13 + lennum + i] = msg[i];
	}
	lennum += len;
	// 3.3 遗嘱消息
	len = strlen(MQTT_WillMessage);
	mqtt_txbuf[12 + lennum + 1] = len >> 8;
	mqtt_txbuf[12 + lennum + 2] = len;
	lennum += 2;
	msg = (u8 *)MQTT_WillMessage;
	for(i = 0; i < len; i++)
	{
    
    
		mqtt_txbuf[13 + lennum + i] = msg[i];
	}
	lennum += len;
#endif /* (MQTT_StaWillFlag == 1) */
#if (MQTT_StaUserNameFlag == 1)	// 3.4 用户名
	len = strlen(MQTT_UserName);
	mqtt_txbuf[12 + lennum + 1] = len >> 8;
	mqtt_txbuf[12 + lennum + 2] = len;
	lennum += 2;
	msg = (u8 *)MQTT_UserName;
	for(i = 0; i< len; i++)
	{
    
    
		mqtt_txbuf[13 + lennum + i] = msg[i];
	}
	lennum += len;
#endif	/* (MQTT_StaUserNameFlag == 1) */
#if (MQTT_StaPasswordFlag == 1)	// 3.5 密码
	len = strlen(MQTT_Password);
	mqtt_txbuf[12 + lennum + 1] = len >> 8;
	mqtt_txbuf[12 + lennum + 2] = len;
	lennum += 2;
	msg = (u8 *)MQTT_Password;
	for(i = 0; i < len; i++)
	{
    
    
		mqtt_txbuf[13 + lennum + i] = msg[i];
	}
	lennum += len;
#endif	/* (MQTT_StaPasswordFlag == 1) */
	
/* 1、固定报头(补剩余长度值) */
	len = AddRemainingLength(mqtt_txbuf, 0);
	
/* 将 CONNECT 报文发送 */
	MQTT_SendMsg(mqtt_txbuf, len);
	
/* 接收发送回来的报文,判断是否连接成功 */
	MQTT_ReceiveMsg(MQTT_TypeCONNECT, mqtt_rxbuf);	
	// 处理连接返回码
	return mqtt_rxbuf[3];
}

4.4 サブスクライブ トピック関数: MQTT_Subscribe_Topic()

トピックへのサブスクライブに成功した後、サーバーは SUBACK サブスクリプション確認メッセージを返し、返されたメッセージ タイプを確認して、サブスクリプションが成功したかどうかを判断します。成功後、テーマ名は themes[] 構造体配列に保持されますサーバーが送信する PUBLISH メッセージを後で受信する際に、トピックが送信したメッセージを途中で抽出する必要があり、メッセージ内のトピック名とメッセージの内容が近接しているため、この部分は非常に重要です。購読しているトピック名を保存すると、トピック名とメッセージ コンテンツの境界がどこにあるかを判断することはできません

#define	MQTT_Subscribe_Topic(topic_name, Qos)				SendSUBSCRIBE(topic_name, Qos)
/**
	* @brief  发送订阅消息的 SUBSCRIBE 报文
	*
	* @param  topic: 想要订阅的主题
	* @param  RequestedQoS: 服务质量等级 Qos
	*
	* @retval >=0: 订阅成功,具体返回订阅的 Qos值 	-1:订阅失败
*/
int SendSUBSCRIBE(const char *topic,unsigned char RequestedQoS)
{
    
    
	unsigned int i,len = 0,lennum = 0;
	uint8_t cps_len = 0;
	
/* 1、固定报头 */
	// 第3,2,1,0位是保留位,必须分别设置为0,0,1,0
	mqtt_txbuf[0] = 0x82;
	
/* 2、可变报头 */
	// 2.1 报文标识符(如果一个客户端要重发这个特殊的控制报文,在随后重发那个报文时,它必须使用相同的标识符)
	// 校验输入的报文标识符是否符合要求
	if(RequestedQoS == 0)
		cps_len = 1;
	else if(RequestedQoS > 0)
		new_pid++;
	mqtt_txbuf[1] = new_pid >> 8;
	mqtt_txbuf[2] = new_pid;
	
/* 3、有效载荷 */
	// 3.1 想要订阅的主题(符合主题过滤器的要求,即主题中含有'/'的分级符)
	len = strlen(topic);
	mqtt_txbuf[3] = len >> 8;
	mqtt_txbuf[4] = len;
	for(i = 0; i < len; i++)
		mqtt_txbuf[5 + i] = topic[i];
	lennum = len;
	// 3.2 服务质量要求(Requested QoS)
	mqtt_txbuf[5 + lennum] = RequestedQoS;

/* 1、固定报头(补剩余长度值) */
	len = AddRemainingLength(mqtt_txbuf, cps_len);
	
	/* 发送 SUBSCRIBE 报文 */
	MQTT_SendMsg(mqtt_txbuf, len);
	
/* 接收发送回来的报文,判断是否订阅成功 */
	MQTT_ReceiveMsg(MQTT_TypeSUBSCRIBE, mqtt_rxbuf);
	if(Get_Fixed_Header_Type(mqtt_rxbuf) == MQTT_TypeSUBACK && mqtt_rxbuf[4] == RequestedQoS)
	{
    
    
		themes[ThemeNum].ThemeName = (char *)topic;
		themes[ThemeNum].ThemeMsg = NULL;
		ThemeNum++;
#if (DEBUG_1 == 1)
		printf("\"%s\" 主题订阅成功!\n", themes[ThemeNum - 1].ThemeName);
#endif
		return RequestedQoS;
	}
	else
		return -1;
}

4.5 パブリッシュメッセージ関数: MQTT_Publish_Topic()

メッセージを公開するときは、さまざまなレベルの Qos の処理が必要です。Qos0レベルは「送信した後はどうにもならない」ため、PUBLISH メッセージをサーバーに送信するだけで、サーバーは確認メッセージを返しません。 . ただし、Qos1 レベルは「send once, send back」であり、PUBLISH メッセージを送信した後、サーバーは PUBACK 確認メッセージを返します。Qos2 レベルは「4 ウェイ ハンドシェイク」で、PUBLISH メッセージを送信する場合、PUBREC メッセージを受信した後、PUBREL メッセージを送信し、PUBCOMP メッセージが完了するのを待ちます。

ここに画像の説明を挿入
PUBRELメッセージを送信するときは、最初のバイトの予約済みビットが 0、0、1、0 に設定されることに特に注意してください。

ここに画像の説明を挿入

#define Qos0								0
#define	Qos1								1
#define Qos2								2
#define Save_Msg							1
#define No_Save_Msg							0

#define MQTT_Publish_Topic(topic_name, msg, Qos, retain)	SendPUBLISH(0, Qos, retain, topic_name, msg)
/**
	* @brief  获取发布消息的数据包
	*
	* @param	dup: 重发标志
	*		@arg	0: 客户端或服务端第一次请求发送这个报文
	*		@arg	1: 可能是一个早前报文请求的重发
	* @param	qos: Qos等级
	* @param	retain: 保留标志(设置后在订阅了该主题后马上接收到一条该主题的信息)
	*		@arg	0: 不发布保留消息
	*		@arg	1: 发布保留消息
	* @param  topic: 主题名
	* @param  msg: 待发布的消息
	*
	* @retval 0: 订阅成功	-1:订阅失败
*/
int SendPUBLISH(unsigned char dup, unsigned char qos, unsigned char retain, const char *topic, const char *msg)
{
    
    
	unsigned int i,len = 0,lennum = 0;
	uint8_t cps_len = 0;
	
/* 1、固定报头 */
	mqtt_txbuf[0] = SendFixedHead(MQTT_TypePUBLISH, dup, qos, retain);

/* 2、可变报头 */
	// 2.1 主题名
	len = strlen(topic);
	mqtt_txbuf[1] = len >> 8;
	mqtt_txbuf[2] = len;
	for(i = 0; i < len; i++)
		mqtt_txbuf[3 + i] = topic[i];
	lennum = len;
	
	// 2.2 报文标识符
	// 校验输入的报文标识符是否符合要求
	if(qos == 0)
		cps_len = 1;
	else if(qos > 0)
		new_pid++;
	mqtt_txbuf[2 + lennum + 1] = new_pid >> 8;
	mqtt_txbuf[2 + lennum + 2] = new_pid;
	lennum += 2;
	
/* 3、有效载荷 */
	// 3.1 消息
	len = strlen(msg);
	for(i = 0; i < len; i++)
		mqtt_txbuf[3 + i + lennum] = msg[i];
	lennum += len;
/* 1、固定报头(补剩余长度值) */
	len = AddRemainingLength(mqtt_txbuf, cps_len);
	
/* 发送 PUBLISH 报文 */
	MQTT_SendMsg(mqtt_txbuf, len);
	
	if(qos == 0)
		return 0;
	else
	{
    
    
		/* 接收发送回来的 PUBACK/PUBREC 报文,判断是否发布成功(Qos > 0 才会有返回报文) */
		MQTT_ReceiveMsg(MQTT_TypePUBLISH, mqtt_rxbuf);
		if(mqtt_rxbuf[2] == 0 && mqtt_rxbuf[3] == new_pid)	// 处理报文标识符
		{
    
    
			// 接收到的是 PUBREC 报文
			if(Get_Fixed_Header_Type(mqtt_rxbuf) == MQTT_TypePUBREC)
			{
    
    
				if(SendPUBREL(new_pid) < 0)
					return -1;
				else 
					return 0;
			}
			// 接收到的是 PUBACK 报文
			else if(Get_Fixed_Header_Type(mqtt_rxbuf) == MQTT_TypePUBACK)
				return 0;
			else
				return -1;
		}
		else 
			return -1;
	}
}
/**
	* @brief  接收到 Qos = 2 的 PUBREC 报文,需要回复 PUBREL 报文,
	*		  并且要接收对方发送的 PUBCOMP 报文
	*
	* @param  pid:报文标识符,要与 Qos = 2 的 PUBLISH 报文标识符一致
	*
	* @retval 0:对方成功接收	-1:对方接收失败
*/
static int SendPUBREL(uint16_t pid)
{
    
    	
/* 1、固定报头 */
	mqtt_txbuf[0] = SendFixedHead(MQTT_TypePUBREL, 0, 1, 0);
	mqtt_txbuf[1] = 2;
	
/* 2、可变报头 */
	// 2.1 报文标识符
	mqtt_txbuf[2] = pid >> 8;
	mqtt_txbuf[3] = pid;
	
/* 发送 PUBREL 报文 */
	MQTT_SendMsg(mqtt_txbuf, 2 + mqtt_txbuf[1]);
/* 接收发送回来的报文,判断是否发布成功 */
	MQTT_ReceiveMsg(MQTT_TypePUBREL, mqtt_rxbuf);
	if((mqtt_rxbuf[0] >> 4) == MQTT_TypePUBCOMP)	// 处理报文标识符
		return 0;
	else 
		return -1;
}

4.6 ハートビート送信関数: MQTT_alive()

ハートビート サイクル内でハートビート関数を送信できることは非常に重要です。これにより、クライアントとサーバーは相手が何らかの理由で異常に切断されたかどうかを知ることができるため (切断するために DISCONNECT メッセージを送信しない)、 MQTT サーバーに接続するときに設定すると、サーバーは、テーマのクライアント側にサブスクライブしたサブスクライバーに意志を送信します。
同時に、クライアントはサーバーから切断されたことを知ることができます. このとき、再接続することで接続を復元できます.

#define MQTT_alive()										SendPINGREQ()
/**
	* @brief  发送心跳请求
	*
	* @param  None
	*
	* @retval 0:与MQTT服务器通讯正常	 	-1:通讯可能断开,可以多发几次确认一下
*/
int SendPINGREQ(void)
{
    
    
	mqtt_txbuf[0] = 0xc0;
	mqtt_txbuf[1] = 0x00;
	
	/* 发送 PINGREQ 报文 */
	MQTT_SendMsg(mqtt_txbuf, 2);
	/* 接收心跳响应报文 PINGRESP */
	MQTT_ReceiveMsg(MQTT_TypePINGREQ, mqtt_rxbuf);
	if(Get_Fixed_Header_Type(mqtt_rxbuf) == MQTT_TypePINGRESP)
		return 0;
	else
		return -1;
}

4.7 セミカバードウィル関数: MQTT_Modify_Will()

この関数の原理は、新しいメッセージを will トピックに再度送信することです。
なぜ「ハーフカバレッジ」と呼ばれるのですか?具体的な理由については、ここでは詳しく説明しない3.1 意志操作の提案を参照してください。

#if (MQTT_StaWillFlag == 1)
	#define	MQTT_Modify_Will(msg)							MQTT_Publish_Topic(MQTT_WillTopic, msg, MQTT_StaWillQoS, Save_Msg)
#endif	/*MQTT_alive()*/

4.8 リスニング サブスクリプション トピック送信メッセージ関数: MQTT_Listen_Topic()

この関数の機能は、サーバーから送信された PUBLISH メッセージを受信し、抽出された Qos レベルに応じて応答メッセージを送信する必要があるかどうかを判断することです. PUBREC、PUBCOMP、および PUBACK メッセージを送信する機能は投稿されていません。前作のPUBRELに似ています。
受信が完了すると、デフォルトでシリアル ポートを使用して、受信したメッセージがシリアル ポート デバッグ アシスタントに表示されます (中国語の表示をサポートします)。

#define MQTT_Listen_Topic()									\
{
      
      															\
	MQTT_ReceiveMsg(MQTT_WriteMsg, mqtt_rxbuf); 			\
	PrintRecvMsg();											\
}
/**
	* @brief  从esp8266获取到mqtt服务器返回的报文
	*
	* @param  mqtt_msg_type: 在MQTT_SendMsg()函数中发送报文的类型(类型详见mqtt.h)
	* @param  mqtt_rxbuf: 存储从与esp8266连接的串口中暂存的报文
	*
	* @retval None
*/
void MQTT_ReceiveMsg(u8 mqtt_msg_type, u8 *mqtt_rxbuf)
{
    
    
	u8 len = 0;
	uint8_t qos;
	uint16_t PBULISH_pid;
	
	delay_nms(500);
	
	/* 如果接收到了ESP8266的数据 */
	if(strEsp8266_Fram_Record.InfBit.FramFinishFlag)
	{
    
    
		// 根据发送的报文类型得到接收到的报文长度
		switch(mqtt_msg_type)
		{
    
    
			case MQTT_TypeCONNECT:		// 返回报文类型是 CONNACK
			case MQTT_TypePUBLISH:		// 返回报文类型是 PUBACK/PUBREC
			case MQTT_TypePUBREC:		// 返回报文类型是 PUBREL
			case MQTT_TypePUBREL:		// 返回报文类型是 PUBCOMP
			case MQTT_TypeUNSUBSCRIBE:	// 返回报文类型是 UNSUBACK
				len = 4;break;
			case MQTT_TypeSUBSCRIBE:	// 返回报文类型是 SUBACK
				len = 5;break;
			case MQTT_TypePINGREQ:		// 返回报文类型是 PINGRESP
				len = 2;break;
			case MQTT_WriteMsg:			// 等待接收订阅的消息
				len = UpdateStrlen_uint8_t(strEsp8266_Fram_Record.Data_RX_BUF);break;
			default:return;
		}
	
		memset(mqtt_rxbuf, 0, MAX_BUF_SIZE);
		memset(buf, 0, MAX_BUF_SIZE);
		
#if ((DEBUG_1 == 1) && (DEBUG_2 == 1))
		// 将接收到的报文显示在串口调试助手上
		HexToAscii(strEsp8266_Fram_Record.Data_RX_BUF, buf, len, ADD_SPACE_AND_0X);
		printf("接收报文: %s\n", buf);
#endif
		
		// 将指定长度的报文保存至mqtt_rxbuf中
		memcpy(mqtt_rxbuf, strEsp8266_Fram_Record.Data_RX_BUF, len);
			
		// 函数接收到了订阅的主题发送过来的消息报文
		if(mqtt_msg_type == MQTT_WriteMsg)
		{
    
    
			// 判断接收到的报文是什么类型的
			switch(Get_Fixed_Header_Type(mqtt_rxbuf))
			{
    
    
				// 接收到 PUBLISH 报文
				case MQTT_TypePUBLISH:
					// 先保存发送过来的消息内容
					PBULISH_pid = Get_Packet_Identifier(mqtt_rxbuf, SaveReceiveBuf(len)); 
					// 提取固定报头中的 qos 等级
					qos = Get_Fixed_Header_Qos(mqtt_rxbuf);
					// qos = 1:PUBLISH -> PUBACK
					if(qos == 1)
						SendPUBACK(PBULISH_pid);
					// qos = 2:PUBLISH -> PUBREC PUBREL -> PUBCOMP
					else if(qos == 2)
					{
    
    
						SendPUBREC(PBULISH_pid);
						delay_nms(100);
						SendPUBREC(PBULISH_pid);
						delay_nms(100);
						SendPUBCOMP(PBULISH_pid);
					}
					break;
				default:break;
			}
		}

		strEsp8266_Fram_Record.InfBit.FramLength = 0;       //接收数据长度置零
		strEsp8266_Fram_Record.InfBit.FramFinishFlag = 0;   //接收标志置零
		memset(strEsp8266_Fram_Record.Data_RX_BUF, 0, RX_BUF_MAX_LEN);	// 清空数据缓冲区
	}
}

#if (DEBUG_1 == 1)
void PrintMsg(void)
{
    
    
	// 打印接收到的消息
	for(int i = 0; i < ThemeNum; i++)
	{
    
    
		if(themes[i].ThemeMsg != NULL)
		{
    
    
			printf("\"%s\" 主题发送:", themes[i].ThemeName);
			for(int j = 0; j < UpdateStrlen_uint8_t(themes[i].ThemeMsg); )
			{
    
    
				if( themes[i].ThemeMsg[j] >= 0xA1 && themes[i].ThemeMsg[j + 1] >= 0xA1 )
				{
    
    
					printf("%c%c", themes[i].ThemeMsg[j], themes[i].ThemeMsg[j + 1]);
					j += 2;
				}
				else
				{
    
    
					printf("%c", themes[i].ThemeMsg[j]);
					j++;
				}
			}
			printf("\n");
			free(themes[i].ThemeMsg);
			themes[i].ThemeMsg = NULL;
		}
	}
}
#endif

4.9 アクティブ切断関数: MQTT_Disconnect()

#define	MQTT_Disconnect()									SendDISCONNECT()
/**
	* @brief  发送断开连接的数据包
	*
	* @param  mqtt_txbuf: 存储待发送报文的数组
	*
	* @retval None
*/
void SendDISCONNECT(void)
{
    
    
	mqtt_txbuf[0] = 0xe0;
	mqtt_txbuf[1] = 0x00;
	
	/* 发送 DISCONNECT 报文 */
	MQTT_SendMsg(mqtt_txbuf, 2);
}

3. デモンストレーション例

1.コード構成

ここに画像の説明を挿入
ここに画像の説明を挿入

ここには 2 つのトピックが公開されています。1 つは will トピックで、もう 1 つは "/user/123" トピックです。「/user/abc」と「/user/test」の 2 つのトピックがサブスクライブされています。

ここに画像の説明を挿入

2. 運用効果

ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/weixin_48896613/article/details/127619566