# 第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)