---
# System prepended metadata

title: '[ESP32] MQTT'
tags: [泰平]

---

# [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/