---
# System prepended metadata

title: 第15週- IoT MQTT
tags: [遠距控制, MQTT, IoT, WiFi]

---

# 第15週- IoT MQTT
###### tags: `WiFi` `IoT` `遠距控制` `MQTT`
在了解各類 IoT 網路架構以及 ESP32 WiFi 網路伺服器管理資源後，學習 MQTT 如何破除不同網域的限制，進行遠端遙控。

---

## MQTT 背景簡介
![](https://hackmd.io/_uploads/rJ-sKEJwh.png)

### 發明及演進：
* 1999 年由 IBM 的 Andy Stanford-Clark 博士和 Arcom（已更名為 Eurotech）的 Arlen Nipper 博士發明的通訊協定。
>* 方便分佈廣闊、大量的石油管線感測器，透過昻貴的人造衛星通訊，傳遞**輕量、可靠**的資料，便於降低電力損耗和網路擁塞，達到**低頻寬、低硬體**的需求。
* 2011 年 11 月，捐贈給 Eclipse 基金會，目前已演變成 Open Source Code。
* 2019 年 MQTT 提出 v5 版本。(目前市面上有兩個版本，v3.1.1、v.5)

### 基礎功能架構：
* 使用 TCP/IP 
>1. (為何不用 UDP？)
* Publish/Scriber messaing transport (「發佈訂閱」機制)
>1. 透過 broker 執行一對多通訊 (MQTT Broker 就是 Server)
>1. Client 可以向 broker 的主動發佈 (當 publisher) 及訂閱 (當 subscriber) 內容
>![](https://hackmd.io/_uploads/BkYTP3CI2.png)
>1. topic message 的最大容量為 256MB

* QoS (傳送的服務品質等級)
>1. 代表的是發送與接收訊息的品質，可設定 0 ~ 2
>---
>![](https://hackmd.io/_uploads/SkeybTAUn.png)
>>1. 若 topic 沒人訂閱，則 payload 內容被丟棄
>---
>![](https://hackmd.io/_uploads/rkEg-TA8h.png)
>---
>![](https://hackmd.io/_uploads/BkYEZa083.png)
>---
* Topic (會談主題)
>1. Topic 命名原則：
>>a) 是由 utf-8 編碼組成，如同 Http 的 URL 的概念，但以 "/" 進行分階層
>>
>>b) 不可以使用 "$"、"#"、"+"、或空隔(space)
>>![](https://hackmd.io/_uploads/SJC6mpAU3.png)

>2. 利用特殊符號訂閱 Topic :
>>a) ”＃”: 代表的垂直的概念，指的是該階層以下的全部Topic都訂閱：
>>*ex: myhome/groundfloor/#*
>>![](https://hackmd.io/_uploads/B1vvN6A8n.png)

>>b) ”＋”: 代表水平的概念，使用該符號的階層所處的階層可以替換成任何字元：
>>*ex: myhome/groundfloor/+/temperature*
>>![](https://hackmd.io/_uploads/Hydo4aAIn.png)
>>[注意：myhome/#/temperature 是不符合規定的]

* Persistent Session (持續會談主題)
>1. 當 publisher 對 MQTT 連線斷掉時，Topic 還會自動保留。
>1. 重新連線後 Topic 還存在。
* Retained Messages (保留主題訊息)
>1. 發送訊息後會將訊息保持在 Topic 上，使的新的加入者也可以獲取最新的息。
>1. 若在沒有設定的情況下，新加入的 subscriber 不會收到上一個已發送過的訊息。
* Last Will & testament (lwt) (遺囑)
>1. 當 publisher 斷線的時候，可指定 lwt 的 Topic，與想要傳送的訊息。 

---
## 調用 PubSubClient 程式庫
接入服務語法指令
| 動作 |WiFi|WiFiMulti|WiFi Client|MQTT Client|
| -------- | -------- | -------- |--|--|
| 宣告物件||WiFiMulti wifiMulti|WiFiClient client|PubSubClient MQTTClient(client)
| 模式宣告| WiFi.mode(WIFI_?) | WiFi.mode(WIFI_?) |||
| 增加清單|| wifiMulti.addAP("SSID_1", "password_1")|||
| 連線伺服器|| | client.connect(ip, port) | MQTTClient.setServer(MQTTServer, MQTTPort) |
| 連線| WiFi.begin(ssid,password) | wifiMulti.run() | | MQTTClient.connect(id, MQTTUser, MQTTPassword) |
| 檢查狀態| WiFi.status()==WL_CONNECTED | wifiMulti.run()==WL_CONNECTED | |MQTTClient.connect() |
| 關閉(斷線)| WiFi.mode(WIFI_OFF)| WiFi.mode(WIFI_OFF)| client.stop();|MQTTClient.disconnect()|

## ESP32 mqtt 實作:
### 實作一：
啟動 ESP32 藍牙功能
>1. 依課本 p.208 的硬體需求
>1. 修改課本 p.210 程式中的 mqttserver 和 topic，並上傳



:::warning
hint:
>1. 先安裝 library Nick O'Leary 的 pubsubclient
>1. 程式先包含 library:
>#include <WiFi.h>
>#include <PubSubClient.h>
>1. broker (MQTTServer) 為 broker.emqx.io
MQTTPort 為 1883
MQTTUser 為 emqx
MQTTPassword 為 public
>1. 設定 publish 的 topic
>設定 subscribe 的 topic
>1. 設定 publish 的間隔時間(15秒以內)，並以 MQTTClient.loop() 更新訂閱狀態
>1. 建立 WiFiClient 物件
> >WiFiClient WifiClient;
>7. 基於 WiFiClient 物件，建立 MQTTClient 物件
> >PubSubClient MQTTClient(WifiClient);
>8. 連接 broker (MQTTServer):
> >MQTTClient.setServer(MQTTServer, MQTTPort);
> >MQTTClient.setCallback(MQTTCallback);
> > >[當 subscribe 的 topic 有更新時，執行MQTTCallback 副程式]
>9. 訂閱 topic 為 MQTTClient.subscribe(主題)
>發佈 topic 資料為 MQTTClient.publish(主題, 字元)
:::spoiler
```javascript=
#include <WiFi.h>
#include <WiFiMulti.h>    //多重連線
WiFiMulti wifiMulti;      //宣告多重連線

#include <PubSubClient.h> //請先安裝PubSubClient程式庫
#include <SimpleDHT.h>

// ------ 設定WiFi帳號密碼 ------
char ssid[] = "ssid_0";      //請改名
char password[] = "pw_0";  //請改名
char ssid1[] = "sside_1"; //請改名
char password1[] = "pw_0";  //請改名

//------ 設定DHT11腳位 ------
int pinDHT11 = 23;//
SimpleDHT11 dht11(pinDHT11);

// ------ 以下修改成你MQTT設定 ------
//char* MQTTServer = "mqtt.eclipseprojects.io";//免註冊MQTT伺服器
//int MQTTPort = 1883;      //MQTT Port
//char* MQTTUser = "";      //不須帳密
//char* MQTTPassword = "";  //不須帳密

char* MQTTServer = "broker.emqx.io";//免註冊MQTT伺服器
int MQTTPort = 1883;      //MQTT Port
char* MQTTUser = "emqx";
char* MQTTPassword = "public";

//推播主題1:推播溫度(記得改Topic)
char* MQTTPubTopic1 = "YourTopic/class205/temp";
//推播主題2:推播濕度(記得改Topic)
char* MQTTPubTopic2 = "YourTopic/class205/humi";
//訂閱主題1:改變LED燈號(記得改Topic)
char* MQTTSubTopic1 = "YourTopic/class205/led";
long MQTTLastPublishTime;//此變數用來記錄推播時間
long MQTTPublishInterval = 10000;//每10秒推撥一次

WiFiClient WifiClient; // 建立 WiFiClient 物件
PubSubClient MQTTClient(WifiClient); // 基於 WiFiClient 物件，建立 MQTTClient 物件

void setup() {
  Serial.begin(115200);
  pinMode(15, OUTPUT);  //綠色LED燈

  //開始WiFiMulti連線
  WifiMultiConnecte();

  //開始MQTT連線
  MQTTConnecte();
}

void loop() {
  //如果WiFi連線中斷，則重啟WiFi連線
  if (WiFi.status() != WL_CONNECTED) { WifiMultiConnecte(); }

  //如果MQTT連線中斷，則重啟MQTT連線
  if (!MQTTClient.connected()) {  MQTTConnecte(); }

  //如果距離上次傳輸已經超過10秒，則Publish溫溼度
  if ((millis() - MQTTLastPublishTime) >= MQTTPublishInterval ) {
    //讀取溫濕度
    byte temperature = 0;
    byte humidity = 0;
    ReadDHT(&temperature, &humidity);
    // ------ 將DHT11溫濕度發佈到MQTT主題 ------
    MQTTClient.publish(MQTTPubTopic1, String(temperature).c_str());
    MQTTClient.publish(MQTTPubTopic2, String(humidity).c_str());
    Serial.println("溫溼度已發佈到MQTT Broker");
    MQTTLastPublishTime = millis();   //更新最後傳輸時間
  }
  MQTTClient.loop();//更新訂閱狀態
  delay(50);
}

//自建函式，讀取DHT11溫濕度
void ReadDHT(byte * temperature, byte * humidity) {
  int err = SimpleDHTErrSuccess;
  if ((err = dht11.read(temperature, humidity, NULL)) !=
      SimpleDHTErrSuccess) {
    Serial.print("讀取失敗,錯誤訊息="); 
    Serial.print(SimpleDHTErrCode(err));
    Serial.print(","); 
    Serial.println(SimpleDHTErrDuration(err)); 
    delay(1000);
    return;
  }
  Serial.print("DHT讀取成功：");
  Serial.print((int)*temperature); 
  Serial.print(" *C, ");
  Serial.print((int)*humidity); 
  Serial.println(" H");
}

/*
//自建函式，開始WiFi連線
void WifiConnecte() {
  //開始WiFi連線
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi連線成功");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());
}
*/

//自建函式，開始WiFiMulti連線
void WifiMultiConnecte() {
  //開始WiFiMulti連線
  wifiMulti.addAP(ssid, password);
  wifiMulti.addAP(ssid1, password1);
  while(wifiMulti.run() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFiMulti連線成功");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
}

//自建函式，開始MQTT連線
void MQTTConnecte() {
  MQTTClient.setServer(MQTTServer, MQTTPort);
  MQTTClient.setCallback(MQTTCallback);
  while (!MQTTClient.connected()) {
    //以亂數為ClietID
    String  MQTTClientid = "esp32-" + String(random(1000000, 9999999));
    if (MQTTClient.connect(MQTTClientid.c_str(), MQTTUser, MQTTPassword)) {
      //連結成功，顯示「已連線」。
      Serial.println("MQTT已連線");
      //訂閱SubTopic1主題
      MQTTClient.subscribe(MQTTSubTopic1);
    } else {
      //若連線不成功，則顯示錯誤訊息，並重新連線
      Serial.print("MQTT連線失敗,狀態碼=");
      Serial.println(MQTTClient.state());
      Serial.println("十五秒後重新連線");
      delay(15000);
    }
  }
}

//自建函式，接收到訂閱時
void MQTTCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print(topic); Serial.print("訂閱通知:");
  String payloadString; //將接收的payload轉成字串
  //顯示訂閱內容
  for (int i = 0; i < length; i++) {
    payloadString = payloadString + (char)payload[i];
  }
  Serial.println(payloadString);
  //比對主題是否為訂閱主題1
  if (strcmp(topic, MQTTSubTopic1) == 0) {
    Serial.println("改變燈號：" + payloadString);
    if (payloadString == "ON") {
      digitalWrite(15, HIGH);
    }
    if (payloadString == "OFF") {
      digitalWrite(15, LOW);
    }
  }
}
```
:::
:::success
觀察：當打開 App terminal 後，連續收到 "Hello World"。
:::

## 手機 mqtt app 應用實作:
### 實作一：
ESP32 透過mqtt將溫濕度數值，向手機傳送
>1. 需一部 Android 手機，並下戴安裝 ["MQTT Dash"](https://play.google.com/store/apps/details?id=net.routix.mqttdash&hl=zh_TW&gl=US)
>依程式修改 mqttserver 連線 ，並訂閱 topic 

:::warning
hint: 
在 App 上輸入：
>1. MQTTServer connection 為 broker.emqx.io
>port 為 1883
>1. subscribe YourTopic/..../temp 或 humi
>publish YourTopic/..../led

:::

:::success
觀察：當打開 App terminal 後，連續收到溫濕度數值，並可以控制 LED 的啟閉。
:::

### 實作二：
跨網域訂閱其他同學的 ESP32 topic


:::warning
hint: 
在 App 上輸入：
>1. subscribe 其他同學的 YourTopic/..../temp 或 humi
>2. publish 其他同學的 YourTopic/..../led

:::

:::success
觀察：當打開 App terminal 後，收到其他同學 ESP32 的溫濕度數值連續收到溫濕度數值，並可以控制 LED 的啟閉。
:::

:::success
:wink: 延伸
安裝不同的 MQTT 手機 APP，體驗不同的 UX (使用者經驗) 設計，比較設計的差異性。
>1. ["MQTT Dashboard"](https://play.google.com/store/apps/details?id=com.app.vetru.mqttdashboard&hl=zh_TW&gl=US)
>2. ["MQTT Dashboard"](https://play.google.com/store/apps/details?id=com.lapetov.mqtt)
>3. ["mymqtt"](https://play.google.com/store/search?q=mymqtt+app&c=apps)
:::


:::info
:wink: 進階延伸
在MQTT中使用TLS可以保證信息的機密性和完整性，防止訊息洩露和篡改。
>1. #include <WiFiClientSecure.h>
>const char* ca_cert= \*****\n;
>2. 利用 server 的 CA certificate 建立加密的 WiFi 連線
>// init wifi secure client
>WiFiClientSecure espClient;
>espClient.setCACert(ca_cert);

~[參考]~[MQTT on ESP32: A Beginner's Guide](https://www.emqx.com/en/blog/esp32-connects-to-the-free-public-mqtt-broker)
:::

## 參考資料
>1）IOT物聯網應用第十四章 – 尤濬哲(夜市小霸王) 編著
>2）[What is MQTT?](https://www.twilio.com/blog/what-is-mqtt)
>3）[MQTT教學（五）：「保留」發布訊息以及QoS品質設定](https://swf.com.tw/?p=1015)
>4）[[深入淺出MQTT]: v3.1.1與v5 的差異](https://ithelp.ithome.com.tw/articles/10257223?sc=rss.qu)
>5）[MQTT on ESP32: A Beginner's Guide](https://www.emqx.com/en/blog/esp32-connects-to-the-free-public-mqtt-broker)