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

### 發明及演進:
* 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) 內容
>
>1. topic message 的最大容量為 256MB
* QoS (傳送的服務品質等級)
>1. 代表的是發送與接收訊息的品質,可設定 0 ~ 2
>---
>
>>1. 若 topic 沒人訂閱,則 payload 內容被丟棄
>---
>
>---
>
>---
* Topic (會談主題)
>1. Topic 命名原則:
>>a) 是由 utf-8 編碼組成,如同 Http 的 URL 的概念,但以 "/" 進行分階層
>>
>>b) 不可以使用 "$"、"#"、"+"、或空隔(space)
>>
>2. 利用特殊符號訂閱 Topic :
>>a) ”#”: 代表的垂直的概念,指的是該階層以下的全部Topic都訂閱:
>>*ex: myhome/groundfloor/#*
>>
>>b) ”+”: 代表水平的概念,使用該符號的階層所處的階層可以替換成任何字元:
>>*ex: myhome/groundfloor/+/temperature*
>>
>>[注意: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)