MQTT教學(六):使用PubSubClient程式庫開發Arduino MQTT應用

本文將示範使用Arduino Uno控制板搭載乙太網路擴展板,藉由Nick O’Leary先生開發的MQTT前端程式庫,叫做PubSubClient,從Arduino發送MQTT主題訊息給Mosquitto伺服器。

PubSubClient程式庫相容於下列擴展板(shield)和控制板,完整說明請參閱此程式庫的網頁說明。

  • Arduino Ethernet
  • Arduino Ethernet Shield
  • Arduino YUN
  • Arduino WiFi Shield
  • Sparkfun WiFly Shield
  • TI CC3000 WiFi
  • Intel Galileo/Edison
  • ESP8266

此程式庫有一些功能上的限制:

  • 只能發布QoS 0訊息,但可以訂閱QoS 0或QoS 1的主題。
  • 最大訊息長度(含標頭)預設為128位元組,可透過PubSubClient.h裡的MQTT_MAX_PACKET_SIZE常數值調整。
  • keepalive(保持連線)簡隔時間預設為15秒,可透過PubSubClient.h裡的MQTT_KEEPALIVE常數值調整。
  • 用戶端預設採用MQTT 3.1.1標準,如果你的MQTT伺服器不支援(Mosquitto有支援),可將PubSubClient.h裡的MQTT_VERSION值改成3.1。

安裝PubSubClient程式庫

Arduino IDE(整合開發工具)從1.6.2版開始,支援從「程式庫管理員(Library Manager)」新增與更新程式庫的功能。選擇Arduino IDE裡的「草稿碼→匯入程式庫→管理程式庫」,開啟「程式庫管理員」。在搜尋欄位輸入關鍵字“mqtt”,可找到許多相關程式庫,請安裝PubSubClient。

程式庫管理員(Library Manager)

如果你使用的是舊版的IDE,需要手動下載安裝程式庫,請到PubSubClient專案網頁下載.zip壓縮格式檔,或者直接按此連結下載

PubSubClient專案網頁

下載之後,將它解壓縮存入「文件\Arduino\libraries」路徑。

PubSubClient程式庫提供的函式指令介紹

本節介紹稍後將使用的PubSubClient程式庫函式,完整的指令請參閱官方API文件,請先略讀本節再閱讀下一節的程式碼。

MQTT的相關指令都要透過PubSubClient物件操作,因此MQTT程式最重要的一步是建立PubSubClient物件,程式指令如下:

PubSubClient 物件名稱(網路用戶端物件)

由於MQTT協定基於TCP/IP,因此網路層要透過其他程式庫實作。採用W5100乙太網路卡的場合,使用官方Ethernet程式庫建立TCP/IP連線,因此這裡的「網路用戶端」指的是EthernetClinet類型的物件。基於乙太網路卡,建立PubSubClient物件的敘述如下:

建立PubSubClient物件

底下是本文使用的函式指令:

setServer(MQTT伺服器, 埠號):指定欲連接的MQTT伺服器的IP位址或網域名稱,以及埠號。

connect(用戶端ID):連線到MQTT伺服器,並傳入自訂的唯一識別碼

每個MQTT用戶端都需要一個唯一的識別碼(Client ID,以下稱「用戶端ID」),MQTT伺服器透過用戶端ID來識別用戶並且紀錄個別用戶的狀態,像是訂閱的主題和通訊品質設定。根據MQTT 3.1.1規格書Client Identifier單元的說明,用戶端ID的長度為1~23個字元,並且只允許數字和英文字母。但實際的狀況視伺服器和用戶端使用的軟體而定,例如,HiveMQ公司的MQTT伺服器軟體允許用戶端ID最大長度為65535字元(參閱該公司的這篇文件說明),而MQTTLens軟體自動產生的用戶端ID長度則是32個字元(參閱下圖)。話說回來,寫程式的時候還是盡量遵循規格書訂定的規範,以減少相容性的問題。

MQTTLens軟體自動產生的用戶端ID

connected():檢查用戶端是否和伺服器連線,傳回true代表仍處於連線狀態;false代表已斷線。

publish(主題, 內容):發布主題和內容,「主題」與「內容」參數值都是字元陣列類型。此函數會傳回一個布林值,true代表發布成功,false代表不成功,可能是斷線或者訊息內容太長。

loop()程式應該要定期呼叫loop()函式,以便和伺服器保持連線並且處理接收到的訊息。loop()函式會傳回一個布林值,true代表仍與伺服器相連;false代表與伺服器斷線。

Arudino Uno搭載乙太網路擴展板發布MQTT主題訊息

本單元將以Arduino充當MQTT發布者,每隔5秒發布一則隨機溫度和濕度值(JSON格式)的“home/yard/DHT11”主題。

以Arduino充當MQTT發布者,每隔5秒發布一則隨機溫度和濕度值

實驗材料:

  • Arduino Uno控制板 × 1
  • 採用W5100晶片的乙太網路擴展板 × 1

實驗程式:

本實驗程式修改自PubSubClient程式庫內建的mqtt_basic範例,底下是程式的處理流程以及相關PubSubClient指令:

PubSubClient程式流程

此程式基於Ethernet程式庫,所以寫過Arduino HTTP伺服器或前端程式的讀者應該會感到熟悉,這是主程式部份:

#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>

// 設定MAC(實體)位址
const byte mac[] = {0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED};
// 設定用戶端和伺服器的IP位址,請自行修改成你的設備的IP位址。
const IPAddress ip(192, 168, 1, 25);
const IPAddress server(192, 168, 1, 19);

// 設定用戶端ID
const char clientID[] = "yard001";
// 設定主題名稱
const char topic[] = "home/yard/DHT11";
// 儲存訊息的字串變數
String msgStr = "";
// 儲存字元陣列格式的訊息字串(參閱下文說明)
char json[25];

EthernetClient ethClient;        // 建立乙太網路前端物件
PubSubClient client(ethClient);  // 基於乙太網路物件,建立MQTT前端物件

void setup(){
  Serial.begin(9600);

  // 設定MQTT代理人的網址和埠號
  client.setServer(server, 1883);
  Ethernet.begin(mac, ip);

  // 留點時間給乙太網路卡進行初始化
  delay(1500);
}

void loop(){
  // 確認用戶端是否已連上伺服器
  if (!client.connected()) {
    // 若沒有連上,則執行此自訂函式。
    reconnect();
  }
  // 更新用戶端狀態
  client.loop();
 
  // 建立MQTT訊息(JSON格式的字串)
  msgStr = msgStr + "{\"temp\":" + (19 + random(10)) + ",\"humid\":" + 20 + "}";
  // 把String字串轉換成字元陣列格式
  msgStr.toCharArray(json, 25);
  // 發布MQTT主題與訊息
  client.publish(topic, json);
  // 清空MQTT訊息內容
  msgStr = "";
  
  delay(5000);
}

底下連結MQTT伺服器的reconnect()自訂函式。

void reconnect() {
  // 若目前沒有和伺服器相連,則反覆執行直到連結成功…
  while (!client.connected()) {
    // 指定用戶端ID並連結MQTT伺服器
    if (client.connect(clientID)) {
      // 若連結成功,在序列埠監控視窗顯示「已連線」。
      Serial.println("connected");
    } else {
      // 若連線不成功,則顯示錯誤訊息
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // 等候5秒,再重新嘗試連線。
      delay(5000);
    }
  }
}

按此下載本單元的範例程式碼(.ZIP壓縮格式)

實驗結果:

我們將使用MQTTLens程式訂閱home/yard/DHT11主題,如果你之前沒有透過它訂閱過這個主題,請先訂閱,然後再把上面的程式上傳到Arduino控制板。每隔5秒,MQTTLens將顯示從Mosquitto伺服器轉送過來的訂閱訊息。

MQTTLens顯示訂閱訊息

使用String資料類型動態建立字串並轉換成字元陣列

補充說明一下建立MQTT訊息(JSON格式的字串)的敘述。這部份採用String資料類型動態建立字串,子字串和數字之間用“+”運算子相連。連接字串的最前面要加上這個String類型變數(msgStr),否則編譯過程將引發資料類型錯誤。由於publish()函式的參數值為字元陣列格式,因此程式要先透過String物件的toCharArray()方法,把String類型的字串轉換成字元陣列格式。

使用String資料類型動態建立字串並轉換成字元陣列

筆者將字元陣列的長度(元素數量)設定成25,足夠此範例的JSON字串使用,如有需要,請自行調整json陣列變數的長度。

延伸閱讀

Posts created 483

43 thoughts on “MQTT教學(六):使用PubSubClient程式庫開發Arduino MQTT應用

  1. 感謝分享!!
    想問https://swf.com.tw/images/books/IoT/MQTT/mqttLens_2.png 這張圖片的Client Id的功能是?

  2. 經實驗得到以下結果

    由Arduino 發佈 MQTTLens訂閱,MQTTLens可收到訊息
    但由MQTTLens發佈 Arduino訂閱,Arduino無法收到訊息
    由Arduino發佈 Arduino自己訂閱,Arduino可收到訊息

    可以確定Arduino收發正常,但為什麼由MQTTLens發佈訊息Arduino收不到?

    可否麻煩您解惑,並告之解決方法,感謝!

    1. 我測試使用MQTTLens發布和訂閱MQTT訊息都沒問題,話說回來,MQTTLens只是個測試工具,既然你的Arduino和伺服器程式都能運作無誤,那就沒問題啦~

      thanks,
      jeffrey

  3. 你好,我是来自大陆的电子爱好者,请教下你博客中的插图和流程图是使用什么软件编辑制作,感觉做的好精美好有创意,谢谢,你的博客内容十分给力。

    1. 感谢您的鼓励~我是用Adobe Flash(现已改名Animate) 软件,搭配鼠标(偶尔用第一代微软Surface Pro平板的Wacom笔)徒手绘制的。

      thanks,
      jeffrey

  4. 感謝教學!
    想請問大大如果我要用mqtt一次連兩個不同的server並分別傳直,這樣可行嗎?
    我自己實驗好久一直失敗,只能傳出第一組server,要進入server2時就卡住。
    謝謝

    1. 請問這兩個物件有分別建立兩組嗎?

      EthernetClient ethClient;        // 建立乙太網路前端物件
      PubSubClient client(ethClient);  // 基於乙太網路物件,建立MQTT前端物件
      

      不知為何你要連結兩個server,不過,你也可以寫個伺服器端程式,從一個server傳遞訊息給另一個server。

      thanks,
      jeffrey

  5. 請問老師SERVER IP是IPv4嗎? CLIENT IP是連接以太/WIFI 的IP嗎? 因為我出現了failed, rc=-2,不知道是哪邊溝通不良…

  6. 老師您好,請問在樹莓派上安裝了mqtt伺服器 並使用arduino uno +esp8266做連線一開始有回傳數值到樹莓派終端介面,但運作一陣子後就無回傳數值了是與keepalive 有關係嗎?

  7. 老師您好,請問如果keep a live 如果是18hr會斷開連線,請問我該如何讓18hr後esp8266 to raspberry pi連線持續運作呢?

    1. MQTT是為了窄頻寬、網路通訊品質不佳的環境而設計的協定,前後端斷線是很正常的情況。

      如果你需要前後端始終保持連線並即時通訊,可使用WebSocket技術,像Node.js有個socket.io模組,可容易達成這項需求。

      thanks,
      jeffrey

  8. 老師您好,我使用arduino uno +esp8266做連線一開始有回傳數值到AWS雲端BROKER,但運作一陣子後(大概12hr~18hr)就無回傳數值了是與keepalive 有關係嗎?
    或者會有其他原因嗎,非常需要您的幫助,謝謝~~!

    1. 請查看AWS伺服器的log,了解斷線的原因。

      問題應該不是出在keepAlive設定,因為pubsubclient程式預設每15秒會向MQTT broker發出ping訊息來維持連線狀態(參閱API文件的Configuration Options裡的MQTT_KEEPALIVE常數)。

      你也可以查看pubsubclient的state()方法的傳回值對照錯誤訊息。

      thanks,
      jeffrey

  9. 老師您好,想請問一下我看了「MQTT教學(二)」和「MQTT教學(四)」的介紹,還是搞不懂為甚麼出現failed, rc=-2,可以請老師解答嗎。
    另外想問,如果照著您的步驟做,一開始的:
    // 設定MAC(實體)位址
    const byte mac[] = {0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED};
    是要從哪裡查看自己的實體位址呢
    以及:
    // 設定用戶端和伺服器的IP位址,請自行修改成你的設備的IP位址。
    const IPAddress ip(192,168,1,25);
    const IPAddress server(172,168,1,19);
    IP指的是利用CMD–>ipconfig後查到的本機地址
    那server呢?

    1. MAC位址只要不跟區域網路的其他設備衝突即可,例如,用你的手機的MAC位址+1。
      伺服器IP位址就是你的電腦的IP位址。

      thanks,
      jeffrey

  10. 請問一下,arduino載入pubsubclient時會顯示strnlen was not decleared in the scope,請問這該如何解決? 謝謝

    1. 我剛剛用Arduino Uno編譯本文的範例檔,並沒有問題。請問你用那一款開發板?

      thanks,
      jeffrey

  11. 老師您好:
    請問mqtt server 的 ip是怎麼決定或設置的? 之前試過localhost and 127.0.0.1都能正常連接,若單純只有伺服器的話,如果從外部像是esp8266進行mqtt 連線,我總是失敗,ipv4位址也不例外,連伺服器都架不成。請教一下關於ip and server 問題,謝謝

  12. 老師您好,

    最近在開發ESP32連上AWS IOT,如果是以WIFI進行連線,與AWS IOT的連線狀況都很穩定。
    但如果是由SIM7020提供網路訊號的話,與AWS IOT的連線狀況就會變得非常不穩定,大約30-40分鐘就會斷線,重新連線一次,不過因為我們的需求需要ESP32時時刻刻在監聽的狀態,所以這麼頻繁的斷線會影響到正常運行。

    所以不曉得可否請教老師遇到此類問題,可從何下手改善呢?
    非常謝謝!

    1. 拍謝,沒用過GSM通訊模組,建議先用簡單的程式單獨測試GSM模組聯網的穩定性,也請留意GSM模組的供電是否充足。

    2. 每隔一段時間(3-5分鐘 或 更短時間) 發送Mqtt連線狀態偵測
      如果斷線 就重新建立連線

  13. 想請問老師Client ip要如何取得?
    MQTT Broker = server
    那老師定義的ip 位址是客戶端的ip囉?

    1. 你是指Arduino分配到的IP位址嗎?假設你採用乙太網路連線,程式像這樣:

      #include <SPI.h>
      #include <Ethernet.h>
      
      byte mac[] = { 0xF0, 0x7B, 0xCB, 0x4B, 0x58, 0xA5}; // 自訂的MAC位址
      
      void setup() {
        Serial.begin(9600);
      
        Ethernet.begin(mac);               // 以DHCP方式連線
        Serial.print(Ethernet.localIP());  // 顯示分配到的IP位址
      }
      
      void loop() {
      }
      
  14. 老師您好:
    因工作需求,原本我有使用ESP32 + DHT22溫濕度計使用MQTT上傳到Homeassistant,並放置外點機房。
    但因為無線訊號有時候會突然斷線,有時機器放置在外地無法遠端重開。
    因此最近想嘗試使用ESP32 + w5500(乙太網路) + DHT22溫濕度計,建置在麵包板上,
    但程式的撰寫上無法很完整的相容… 因此有許多不太懂的地方想跟您請教。
    如果您方便的話,我願意支付學費給您,請您協助我啟動這套建置。

    另外看完教學文後有個小問題,
    請問在
    『// 設定MAC(實體)位址
    const byte mac[] = {0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED};』
    這個部分,Mac地址是我要先確認W5500 MAC位址後,再填入其中還是這部分可以不用修改他呢?

    1. ESP32晶片有內建TCP/IP層,所以乙太網路不需要用W5500晶片,而是用LAN8720(廣告一下,《超圖解ESP32深度實作》第一章有講),也有現成的,結合ESP32和LAN8720的開發板,商品關鍵字是”WT32-ETH01″。

      至於MAC位址,理論上,網卡模組上面應該會貼一張註明MAC位址的貼紙,不過,絕大多數廠商都不願意支付權利金,所以你只要自行設定一個跟區域網路內的其他設備不同的MAC位址值即可。

    2. esp32 有 WatchDog功能 可以在特定條件下 硬體從新開機
      希望對此有幫助

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top