第五十五章 基於MQTT協議連接阿里雲服務器
本章主要學習lwIP提供的MQTT協議文件使用,通過 MQTT 協議將設備連接到阿里雲服務器,實現遠程互通。由於MQTT 協議是基於 TCP 的協議實現的,所以我們只需要在單片機端實現 TCP 客户端程序並使用 lwIP提供的MQTT文件來連接阿里雲服務器。
本章分為如下幾個部分:
55.1 MQTT協議簡介
55.2 硬件設計
55.3 軟件設計
55.4 下載驗證
55.1 MQTT協議簡介
(1) MQTT是什麼?
MQTT(Message Queuing Telemetry Transport,消息隊列遙測傳輸協議),是一種基於發佈/訂閲(Publish/Subscribe)模式的輕量級通訊協議,該協議構建於TCP/IP協議上,由IBM在1999年發佈,目前最新版本為v3.1.1。MQTT最大的優點在於可以以極少的代碼和有限的帶寬,為遠程設備提供實時可靠的消息服務。做為一種低開銷、低帶寬佔用的即時通訊協議,MQTT在物聯網、小型設備、移動應用等方面有廣泛的應用,MQTT協議屬於應用層。
(2) MQTT協議特點
MQTT 是一個基於客户端與服務器的消息發佈/訂閲傳輸協議。MQTT 協議是輕量、簡單開放和易於實現的,這些特點使它適用範圍非常廣泛。在很多情況下,包括受限境中,如:機器與機器(M2M)通信和物聯網(IoT)。其在,通過衞星鏈路通信傳感器、醫療設備、智能家居、及一些小型化設備中已廣泛使用。
(3) MQTT協議原理及實現方式
實現 MQTT 協議需要:客户端和服務器端MQTT 協議中有三種身份:發佈者(Publish)、代理(Broker)(服務器)、訂閲者(Subscribe)。其中,消息的發佈者和訂閲者都是客户端,消息代理是服務器,消息發佈者可以同時是訂閲者,如下圖所示。
圖55.1.1 MQTT訂閲和發佈過程
MQTT 傳輸的消息分為:主題(Topic)和消息的內容(payload)兩部分。
Topic:可以理解為消息的類型,訂閲者訂閲(Subscribe)後,就會收到該主題的消息內容(payload)。
Payload:可以理解為消息的內容,是指訂閲者具體要使用的內容。
55.1.1 MQTT協議實現原理
1,要在客户端與代理服務端建立一個TCP連接,建立連接的過程是由客户端主動發起的,代理服務一直是處於指定端口的監聽狀態,當監聽到有客户端要接入的時候,就會立刻去處理。客户端在發起連接請求時,攜帶客户端ID、賬號、密碼(無賬號密碼使用除外,正式項目不會允許這樣)、心跳間隔時間等數據。代理服務收到後檢查自己的連接權限配置中是否允許該賬號密碼連接,如果允許則建立會話標識並保存,綁定客户端ID與會話,並記錄心跳間隔時間(判斷是否掉線和啓動遺囑時用)和遺囑消息等,然後回發連接成功確認消息給客户端,客户端收到連接成功的確認消息後,進入下一步(通常是開始訂閲主題,如果不需要訂閲則跳過)。如下圖所示:
圖55.1.1.1 客户端與代理服務器建立連接示意圖
2,客户端將需要訂閲的主題經過SUBSCRIBE報文發送給代理服務,代理服務則將這個主題記錄到該客户端ID下(以後有這個主題發佈就會發送給該客户端),然後回覆確認消息SUBACK報文,客户端接到SUBACK報文後知道已經訂閲成功,則處於等待監聽代理服務推送的消息,也可以繼續訂閲其他主題或發佈主題,如下圖所示:
圖55.1.1.2 客户端向服務器訂閲示意圖
3,當某一客户端發佈一個主題到代理服務後,代理服務先回復該客户端收到主題的確認消息,該客户端收到確認後就可以繼續自己的邏輯了。但這時主題消息還沒有發給訂閲了這個主題的客户端,代理要根據質量級別(QoS)來決定怎樣處理這個主題。所以這裏充分體現了是MQTT協議是異步通信模式,不是立即端到端反應的,如下圖所示:
圖55.1.1.3 客户端向代理服務器發送主題
如果發佈和訂閲時的質量級別QoS都是至多一次,那代理服務則檢查當前訂閲這個主題的客户端是否在線,在線則轉發一次,收到與否不再做任何處理。這種質量對系統壓力最小。
如果發佈和訂閲時的質量級別QoS都是至少一次,那要保證代理服務和訂閲的客户端都有成功收到才可以,否則會嘗試補充發送(具體機制後面討論)。這也可能會出現同一主題多次重複發送的情況。這種質量對系統壓力較大。
如果發佈和訂閲時的質量級別QoS都是隻有一次,那要保證代理服務和訂閲的客户端都有成功收到,並只收到一次不會重複發送(具體機制後面討論)。這種質量對系統壓力最大。
55.1.2 配置遠程服務器
配置阿里雲服務器步驟 ,如下所示。
第一步:註冊阿里雲平台,打開產品分類/物聯網/物聯網平台,如下圖所示。
圖55.1.2.1 打開物聯網應用開發
點擊上圖中的“管理控制枱”按鍵進去物聯網平台頁面。
第二步:在物聯網平台頁面下點擊公共實例/設備管理/產品/創建設備,在此界面下填寫項目名稱等相關信息,如下圖所示:
圖55.1.2.4 產品參數填寫
注:上圖中的節點類型、連網方式、數據格式以及認證模式的選擇,其他產品參數根據用户愛好設置。
第三步:創建產品之後點擊設備管理添加設備,如下圖所示。
圖55.1.2.5 填寫設備參數
第四步:進入創建的設備,點擊查看三元組內容,如下圖所示。
圖55.1.2.5 設備信息
這三個參數非常重要!!!!!!!!!!,在本章實驗中會用到。
第五步:打開“產品/查看/功能定義”路徑,在該路徑下添加功能定義,如下圖所示。
圖55.1.2.6 添加功能
第六步:打開自定義功能併發布上線,這裏我們添加了兩個CurrentTemperature和RelativeHumidity標籤。
55.2 硬件設計
1.例程功能
本章實驗功能簡介:lwIP連接阿里雲實現數據上存。
2.硬件資源
1)LED燈
LED-IO1
2)XL9555
IIC_INT-IO0(需在P5連接IO0)
IIC_SDA-IO41
IIC_SCL-IO42
3)SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在P5端口,使用跳線帽將IO_SET和LCD_DC相連)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
4)ESP32-S3內部WiFi
3.原理圖
本章實驗使用的WiFi為ESP32-S3的片上資源,因此並沒有相應的連接原理圖。
55.3 軟件設計
55.3.1 程序流程圖
程序流程圖能幫助我們更好的理解一個工程的功能和實現的過程,對學習和設計工程有很好的主導作用。下面看看本實驗的程序流程圖:
圖55.3.1.1 程序流程圖
55.3.2 程序解析
在本章節中,我們主要關注兩個文件:lwip_demo.c和lwip_demo.h。lwip_demo.h文件主要定義了阿里雲提供的三元組內容和計算得出的MQTT參數,這部分內容請參考阿里雲提供的手冊 “如何計算MQTT簽名參數”章節,所以作者暫不詳細解釋。主要關注點是lwip_demo.c文件中的函數。在lwip_demo函數中,我們配置了相關的MQTT參數,並創建了一個名為mqtt_event_handler的事件回調函數。這個事件回調函數通過獲取MQTT事件ID來處理連接過程中所需的操作。接下來,我們將分別詳細解釋lwip_demo函數和mqtt_event_handler事件回調函數。
/**
* @brief lwip_demo進程
* @param 無
* @retval 無
*/
void lwip_demo(void)
{
char mqtt_publish_data[] = "alientek esp32-s3";
/* 設置客户端的信息量 */
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.hostname = HOST_NAME, /* MQTT地址 */
.broker.address.port = HOST_PORT, /* MQTT端口號 */
.broker.address.transport = MQTT_TRANSPORT_OVER_TCP, /* TCP模式 */
.credentials.client_id = CLIENT_ID, /* 設備名稱 */
.credentials.username = (char*)USER_NAME, /* 產品ID */
.credentials.authentication.password = PASSWORD, /* 計算出來的密碼 */
};
esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID,
mqtt_event_handler, NULL);
esp_mqtt_client_start(client);
while(1)
{
if (g_publish_flag == 1)
{
esp_mqtt_client_publish(client,DEVICE_PUBLISH,
(char *)mqtt_publish_data,strlen(mqtt_publish_data),1,0);
}
vTaskDelay(1000);
}
}
這個函數主要負責MQTT的連接配置。它首先創建了一個MQTT控制塊,用於存儲配置參數以及發送和接收數據。接着,它定義了一個回調函數,用於處理和響應MQTT連接過程中的各種事件。最後,它啓動MQTT併發送連接請求到服務器。一旦成功連接到MQTT服務器,它就可以開始循環發佈數據。現在,讓我們深入瞭解這個回調函數的工作原理。
/**
* @brief 錯誤日記
* @param message :錯誤消息
* @param error_code :錯誤碼
* @retval 無
*/
static void log_error_if_nonzero(const char *message, int error_code)
{
if (error_code != 0)
{
ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code);
}
}
/**
* @brief 註冊接收MQTT事件的事件處理程序
* @param handler_args:註冊到事件的用户數據
* @param base :處理程序的事件庫
* @param event_id :接收到的事件的id
* @param event_data :事件的數據
* @retval 無
*/
static void mqtt_event_handler(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = event_data;
esp_mqtt_client_handle_t client = event->client;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id)
{
case MQTT_EVENT_CONNECTED: /* 連接事件 */
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
msg_id=esp_mqtt_client_publish(client,"/topic/qos1","data_3",0,1,0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1");
ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
g_publish_flag = 1;
/* 訂閲主題 */
msg_id = esp_mqtt_client_subscribe(client, DEVICE_SUBSCRIBE, 0);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_DISCONNECTED: /* 斷開連接事件 */
break;
case MQTT_EVENT_SUBSCRIBED: /* 訂閲事件 */
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
msg_id=esp_mqtt_client_publish(client,"/topic/qos0","data",0,0,0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED: /* 取消訂閲事件 */
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED: /* 發佈事件 */
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA: /* 接收數據事件 */
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
if (event->error_handle->error_type
== MQTT_ERROR_TYPE_TCP_TRANSPORT)
{
log_error_if_nonzero("reported from esp-tls",
event->error_handle->esp_tls_last_esp_err);
log_error_if_nonzero("reported from tls stack",
event->error_handle->esp_tls_stack_err);
log_error_if_nonzero("captured as transport's socket errno",
event->error_handle->esp_transport_sock_errno);
ESP_LOGI(TAG, "Last errno string (%s)",
strerror(event->error_handle->esp_transport_sock_errno));
}
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
在這個回調函數中,主要處理與MQTT的交互過程。當系統接收到MQTT服務器的連接應答時,它會發送訂閲主題報文。當系統接收到MQTT服務器的訂閲應答報文時,它會發佈一個訂閲完成報文。因此,每個狀態事件都需要讀者根據項目需求進行相應的修改。
55.4 下載驗證
程序下載成功後,打開阿里雲平台的物聯網平台設備管理,可以看到此時的設備處於連接狀態,如下圖所示。
圖55.4.1 設備處於連接
MQTT連接成功後,可在日記服務中找到ESP32-S3設備發佈的數據,如下圖所示。
圖55.4.2 設備發佈的數據
我們可點擊上圖中的查看,可看到設備發佈的消息內容,如下圖所示。
圖55.4.3 查看發佈的消息內容