# [ESP32] MQTT ## 什麼是 MQTT? MQTT(Message Queuing Telemetry Transport)是一種輕量級的通訊協議,設計用於低帶寬、高延遲或不穩定的網路環境。它採用了發布/訂閱(Publish/Subscribe)模型,該協定建構於TCP/IP協定上,由IBM在1999年發布,適合物聯網(IoT)設備的資料傳輸。 **MQTT 的特點** + 輕量級:封包大小較小,適合嵌入式設備。 + 低帶寬:最小的開銷讓它能在受限的網路環境中高效運作。 + 可靠性:支持多種訊息品質(QoS)。 + 靈活性:採用發布/訂閱模式,易於擴展。 + 即時性:提供即時的雙向通訊。 ### MQTT工作流程 1. **訂閱(Subscribe)**: - 客戶端向 Broker 訂閱特定主題(Topic)。 2. **發布(Publish)**: - 客戶端向特定主題發布訊息。 3. **分發(Distribute)**: - Broker 將訊息轉發給所有訂閱該主題的客戶端。 ![image](https://hackmd.io/_uploads/r1L72BwGkg.png) ## HTTP和MQTT通訊協定 MQTT和HTTP的底層都是TCP/IP,也就是物聯網裝置可以沿用既有的網路架構和設備,只是在網路上流通的「訊息格式」以及應用程式的處理機制不同。 ![image](https://hackmd.io/_uploads/BywD1SwGye.png) **HTTP 請求與 MQTT 的差異** HTTP: - 面向請求/響應模式。 - 每次請求需要建立一個連線。 - 適合間歇性的數據上傳。 MQTT: - 面向持久連線的發布/訂閱模式。 - 更適合頻繁的數據傳輸和即時性要求。 假設某個裝置透過Web瀏覽器,以HTTP協定傳送溫度值給網站伺服器,此HTTP POST訊息內容大概像這樣: ![http_post](https://hackmd.io/_uploads/HkCRSrwfyl.png) ## MQTT 封包結構 MQTT 訊息由多個欄位組成: 1. **固定標頭**: - 每個封包必備,用於描述訊息類型與長度。 2. **可變標頭**: - 僅部分訊息需要,包含主題名稱等資訊。 3. **有效載荷(Payload)**: - 實際的訊息內容。 ![mqtt_message_format](https://hackmd.io/_uploads/S12wUEDMyl.png) ### MQTT 訊息類型表 | 位元值 | 十進位值 | 訊息名稱 | 描述 | |--------|----------|---------------|-------------------------------------| | `0001` | 1 | CONNECT | 客戶端請求連線到 Broker。 | | `0010` | 2 | CONNACK | 伺服器確認連線請求。 | | `0011` | 3 | PUBLISH | 發布訊息到指定主題。 | | `0100` | 4 | PUBACK | 確認已接收到發布的訊息(QoS 1)。 | | `0101` | 5 | PUBREC | 確認已接收到發布的訊息(QoS 2 第一步)。 | | `0110` | 6 | PUBREL | 確認消息已準備好發布(QoS 2 第二步)。 | | `0111` | 7 | PUBCOMP | 確認消息發布完成(QoS 2 第三步)。 | | `1000` | 8 | SUBSCRIBE | 客戶端請求訂閱主題。 | | `1001` | 9 | SUBACK | 伺服器確認訂閱請求。 | | `1010` | 10 | UNSUBSCRIBE | 客戶端請求取消訂閱主題。 | | `1011` | 11 | UNSUBACK | 伺服器確認取消訂閱請求。 | | `1100` | 12 | PINGREQ | 客戶端發送的心跳請求(保持連線)。 | | `1101` | 13 | PINGRESP | 伺服器的心跳響應(保持連線)。 | | `1110` | 14 | DISCONNECT | 客戶端或伺服器主動請求斷線。 | ### DUP 的值與含義 | **DUP 值** | **含義** | |------------|----------------------------------------| | `0` | 這是第一次傳送的訊息(非重發)。 | | `1` | 這是重發的訊息,表示之前的訊息未被確認。 | ### MQTT 的連線品質(QoS) MQTT 提供三種品質保證服務(QoS): | QoS 等級 | 描述 | 適用場景 | |----------|-------------------------------------------|---------------------------| | 0 | 訊息最多送一次(At most once),不保證送達 | 即使丟失部分訊息,系統仍能正常運行。<br>例如:溫度、濕度、光照等頻繁傳輸的數據。 | | 1 | 訊息至少送一次(At least once) | 非即時但重要的數據。<br>例如:家庭安全設備的狀態通知(門鎖是否開啟)。 | | 2 | 訊息只送一次(Exactly once) | 金融交易,資料丟失或重複都會產生重大問題。<br>例如:支付請求、交易確認。<br>命令控制,重複執行指令會導致設備異常。<br>例如:智慧家居中的設備控制(如開關燈) | ![image](https://hackmd.io/_uploads/SkY3mzofye.png) ___ ![image](https://hackmd.io/_uploads/BJ0UojufJe.png) ![image](https://hackmd.io/_uploads/HkjmjodzJe.png) ___ ![image](https://hackmd.io/_uploads/S1KlssuG1e.png) ### broker保留 | 特性 | retain = 1 | retain = 0 | | -------- | -------- | -------- | | 訊息保存 | Broker 保存最後一條訊息 | Broker 不保存訊息 | |新訂閱者行為|新訂閱者會收到最新的保留訊息|新訂閱者不會收到任何訊息| |適用場景|記錄設備狀態、重要的持續訊息|實時訊息傳輸,無需歷史訊息| ### 傳輸範例 ![image](https://hackmd.io/_uploads/SJa54MsM1x.png) 假設向topic:`hpcEsp32/data` PUBLISH資料:`temp=21` 完整的 MQTT 訊息應該是: ``` 0011 0000 0001 0110 0000 1101 68 70 63 45 73 70 33 32 2F 64 61 74 61 74 65 6D 70 3D 32 31 ``` | 區塊 | 值 | 描述 | |----------------|--------------------------------------|----------------------------------------------------------------------| | **固定標頭** | `0011 0000` | 訊息類型:PUBLISH,QoS:0,無 DUP 和保留位元。 | | | `0001 0110` | 剩餘資料長度:22 個字節。 | | **可變標頭** | `0000 1101` | 主題名稱長度:13(以 2 個字節表示)。 | | | `68 70 63 45 73 70 33 32 2F 64 61 74 61` | 主題名稱 `hpcEsp32/data` 的 UTF-8 編碼。 | | **訊息本體** | `74 65 6D 70 3D 32 31` | 訊息內容 `temp=21` 的 UTF-8 編碼。 | --- ## Esp32 MQTT ### Arduino設定 **開發版下載** ![image](https://hackmd.io/_uploads/BJFhEXjGkl.png) **程式庫安裝** ![image](https://hackmd.io/_uploads/BkZm5Qszkg.png) **設定** ![image](https://hackmd.io/_uploads/BkOdcXjzJe.png) ### 流程圖 ![mqttesp32](https://hackmd.io/_uploads/S1bTkQiMJg.jpg) ### 程式碼 :::spoiler esp32mqtt.ino ```c= #include <esp_camera.h> // 引入ESP32相機功能庫 #include <WiFi.h> // 引入WiFi功能庫 #include <PubSubClient.h> // 引入MQTT功能庫 // ------ WiFi帳號密碼 ------ const char* ssid = "HPC_1"; // 定義WiFi名稱 const char* password = "HPC200-1"; // 定義WiFi密碼 // ------ MQTT設定 ------ const char* mqttServer = "mqtt.eclipseprojects.io"; // 定義MQTT伺服器位址 const unsigned int mqttPort = 1883; // 定義MQTT伺服器的通訊埠 const char* clientId = "test"; // 設定MQTT客戶端ID #define MQTT_PUBLISH_Monitor "hpcEsp32/data" // 定義發布文字訊息的Topic名稱 WiFiClient wifiClient; // 定義WiFi客戶端 PubSubClient mqttClient(mqttServer, mqttPort, wifiClient); // 定義MQTT客戶端 // Wi-Fi 連線函數 void setup_wifi() { if (WiFi.status() != WL_CONNECTED) { // 檢查是否已連線 Serial.printf("\nConnecting to Wi-Fi: %s", ssid); // 顯示正在連接的Wi-Fi名稱 WiFi.begin(ssid, password); // 嘗試連接Wi-Fi int retry_count = 0; // 記錄重試次數 while (WiFi.status() != WL_CONNECTED && retry_count < 10) { // 最多重試10次 delay(1000); Serial.print("."); retry_count++; } if (WiFi.status() == WL_CONNECTED) { Serial.print("\nWi-Fi Connected. IP Address: "); Serial.println(WiFi.localIP()); // 顯示取得的IP位址 } else { Serial.println("\nWi-Fi Connection Failed. Retrying..."); } } } // MQTT 連線函數 boolean mqtt_nonblock_reconnect() { if (!mqttClient.connected()) { // 檢查MQTT連線狀態 if (mqttClient.connect(clientId)) { // 嘗試連線到MQTT伺服器 Serial.println("MQTT Client Connected Successfully!"); return true; } else { Serial.println("MQTT Connection Failed!"); return false; } } return true; // 已連線 } // MQTT 傳遞文字訊息 void MQTT_message(int val) { if (mqtt_nonblock_reconnect()) { // 確保MQTT已連線 char message[50]; sprintf(message, "temp=%d", val); // 格式化訊息 boolean isPublished = mqttClient.publish(MQTT_PUBLISH_Monitor, message); // 傳送訊息 if (isPublished) { Serial.println("Message published to MQTT successfully!"); // 成功訊息 } else { Serial.println("Message failed to publish!"); // 失敗訊息 } } else { Serial.println("MQTT connection not established. Message not sent."); // 連線未建立 } } // 初始化函數 void setup() { Serial.begin(115200); // 初始化序列埠 setup_wifi(); // 啟動Wi-Fi連線 mqtt_nonblock_reconnect(); // 啟動MQTT連線 } // 主循環函數 void loop() { setup_wifi(); // 每次進入循環檢查Wi-Fi連線 static int val = 0; // 定義靜態變數作為計數器 MQTT_message(val); // 傳遞格式化後的文字訊息 val++; // 計數器加一 delay(1000); // 延遲1秒 } ``` ::: ___ 參考資料: https://hackmd.io/J0BpBTk5RWy78OVy24LOhQ?view https://cedalo.com/blog/mqtt-packet-guide/