Learning Objectives
- 說明發布/訂閱的運作方式,以及訊息代理(broker)扮演的角色
- 設計主題階層,用萬用字元 + 與 # 一次訂閱多個主題
- 用 paho-mqtt 2.x 發布感測器資料(JSON),以及訂閱主題控制 LED
- 說明 QoS、保留訊息與連線安全的基本概念
很多台裝置要怎麼互相說話
上一課的 Flask 是「一問一答」:瀏覽器問,樹莓派答。如果一棟樓有 30 間教室,每間一個溫度感測器, 管理室的螢幕、手機 App、冷氣控制器都想知道溫度,讓每個人一直去問每一台感測器,很快就會亂成一團。
MQTT 換一個方式:裝置不直接互相連線,而是都連到一台訊息代理(broker),
- 感測器把資料發布(publish)到一個主題(topic),例如
classroom/101/temp; - 想要資料的程式向 broker 訂閱(subscribe)它關心的主題;
- broker 把每一則訊息轉送給所有訂閱者。
101 溫度感測器 ──publish classroom/101/temp──▶ broker ──▶ 管理室螢幕(訂閱 classroom/+/temp)
└─▶ 冷氣控制器(訂閱 classroom/101/temp)
發布者不需要知道有誰在聽,訂閱者也不需要知道資料從哪來;新增一台裝置,其他裝置都不用改。 MQTT 的訊息很小、連線維持很省資源,所以常用在物聯網。常見的 broker 軟體是開源的 Mosquitto,預設的連接埠是 1883。
主題與萬用字元
主題是用斜線 / 分層的字串,像檔案路徑一樣由大到小:校區/大樓/教室/測量項目。
訂閱時可以用兩種萬用字元:
| 寫法 | 意思 | classroom/+/temp 的例子 |
|---|---|---|
+ |
剛好一層,任何名稱 | 收得到 classroom/101/temp、classroom/102/temp |
# |
這一層與以下所有層(只能放在最後) | classroom/# 收得到 classroom 底下的全部 |
classroom/+/temp 收不到 classroom/101/2F/temp(多了一層);也收不到 classroom/101/humidity(最後一層不同)。
設計主題時,把「會想一起訂閱的東西」放在同一層:例如測量項目放最後一層,就能用 classroom/+/temp 收所有教室的溫度。
下面的模擬器把整個流程畫出來,試試不同的主題篩選。
在樹莓派上用 Python:paho-mqtt
Python 最常用的 MQTT 用戶端是 Eclipse 的 paho-mqtt。2.0 版(2024 年)改了回呼函式的寫法,
建立用戶端時要指定 CallbackAPIVersion,舊教材的 mqtt.Client("名字") 在新版會出錯:
import paho.mqtt.client as mqtt
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.connect("broker 的位址", 1883)
client.publish("主題", "內容")
發布的內容(payload)是字串或位元組。要送好幾個數值時,常用 JSON:json.dumps({"room": "101", "temp": 26.1}),
收到的一方用 json.loads 還原成字典。
client.loop_start() 在背景處理網路的收發(送出訊息、回應 broker 的心跳);程式結束前用 loop_stop() 與 disconnect() 收尾。
逐步看:每秒發布一次溫度
電路畫面下方會列出 MQTT 的連線與發布紀錄。注意第 17 行把字典變成 JSON 字串,第 18 行送出。
訂閱:用回呼函式處理收到的訊息
訂閱的一方不知道訊息什麼時候會來,所以用回呼函式(callback):先告訴 paho「發生某件事時呼叫這個函式」,
然後讓它一直等。這和第 06 課 gpiozero 的 when_pressed 是同一個想法。
| 回呼函式 | 什麼時候被呼叫 | VERSION2 的參數 |
|---|---|---|
on_connect |
連上 broker 之後 | client, userdata, flags, reason_code, properties |
on_message |
收到訂閱主題的訊息 | client, userdata, msg(msg.topic、msg.payload) |
兩個習慣:
- 在
on_connect裡訂閱:網路斷了、paho 自動重新連線之後,on_connect會再被呼叫,訂閱也跟著恢復。 msg.payload是位元組(bytes),要先.decode("utf-8")變成字串再比較。
client.loop_forever() 讓程式停在這裡一直處理網路,直到斷線或程式被停止。
逐步看:收到訊息就開關燈
訊息時間表:1 秒 classroom/101/led 送 on、2 秒 classroom/102/led 送 on、3 秒 classroom/101/led 送 off。只訂閱了 101,看 2 秒那一則有沒有進到 on_message。
QoS、保留訊息與安全
QoS(服務品質)決定一則訊息的送達保證,發布與訂閱時都可以指定:
| QoS | 保證 | 適合 |
|---|---|---|
| 0 | 最多一次:送出就不管,可能遺失 | 每秒更新的感測數值,少一筆沒關係 |
| 1 | 至少一次:確認收到為止,可能重複 | 重要的指令,接收端要能處理重複 |
| 2 | 剛好一次:多一次往返確認 | 不能重複也不能遺失的事件 |
保留訊息(retained message):發布時加 retain=True,broker 會記住這個主題最後一則保留訊息,
之後才訂閱的人會立刻收到它。適合「目前狀態」這種資料(例如燈現在是開還是關),新加入的 App 不用等下一次更新。
安全:
- 公開的測試用 broker 誰都可以訂閱,不要送任何個人或敏感資料。
- 自己架設的 broker 要設定帳號密碼(Mosquitto 2.0 起,有設定監聽埠時預設不允許匿名連線),並為連線加密(TLS)。
- 控制硬體的主題(例如
.../led)要限制誰能發布,否則任何人都能開關你的燈。
小測驗:主題與萬用字元
訂閱 campus/+/temp,下面哪一則訊息會收到?
下面的函式模擬 MQTT 主題比對(只處理 + 萬用字元),會印出什麼?
def match(pattern, topic):
p, t = pattern.split("/"), topic.split("/")
if len(p) != len(t):
return False
return all(a == "+" or a == b for a, b in zip(p, t))
print(match("classroom/+/led", "classroom/102/led"),
match("classroom/+/led", "classroom/102/2F/led"))
小測驗:paho-mqtt 與 QoS
為什麼建議把 client.subscribe(...) 寫在 on_connect 裡,而不是寫在 connect() 的下一行?
下面的程式會印出什麼?
import json
payload = b'{"room": "101", "temp": 26.5}'
data = json.loads(payload.decode("utf-8"))
print(data["temp"] + 1, type(payload).__name__)
教室溫度每 5 秒發布一次,偶爾少一筆沒有關係。最適合用哪一個 QoS?
重點整理
- MQTT 是發布/訂閱:裝置都連到 broker,發布者送到主題,broker 轉給所有訂閱者,彼此不必直接連線。
- 主題用
/分層;+代表剛好一層,#代表這一層以下全部(只能放最後)。 - paho-mqtt 2.x:
mqtt.Client(mqtt.CallbackAPIVersion.VERSION2);在on_connect裡訂閱,on_message收訊息,msg.payload要解碼。 - 多個數值用 JSON 傳送;QoS 0/1/2 決定送達保證;保留訊息讓新訂閱者立刻拿到目前狀態。
- 公開 broker 不送敏感資料;自己的 broker 要設帳號密碼與加密,限制誰能發布控制指令。
下一課讓記錄、網頁與 MQTT 程式在樹莓派開機時自動執行,當機了也會自己重新啟動。