MQTT不只是能傳:
QoS、Retain、LWT與斷線重連
感測值送得出去,只代表展示成功;真正能進入水井村智慧養殖、樹藝故事機或場域儀表板的 MQTT,還必須回答:訊息可否重複?新訂閱者需不需要立刻看到狀態?裝置突然離線時誰來通知?斷線後如何安全恢復?
一、MQTT為什麼適合IoT?
MQTT採用發布/訂閱模式。ESP32不必知道Django在哪裡,只要把訊息發布到Topic;Broker負責把訊息分送給訂閱者。這種解耦讓裝置、雲端與儀表板可以分別擴充。
site/pond01/water/temp訊息分類與路由名稱
{"value":28.4}真正傳送的資料內容
重點:Broker收到訊息,不等於資料一定寫入資料庫;Publisher成功送出,也不等於每個Subscriber都已完成業務處理。
二、QoS:不是越高越好,而是語意要正確
送出後不等待MQTT層確認;可能遺失,但額外負擔最低。
適合高頻、下一筆可取代上一筆的即時感測值。
未收到確認會重送,因此比較不易遺失,卻可能收到重複訊息。
適合警報、控制事件、播放紀錄等重要事件。
用更完整的交換流程降低重複交付,成本與延遲最高;裝置、Broker與函式庫均須支援。
適合極少數不能遺失也不能重複的交換。
| 資料情境 | 建議起點 | 為什麼 |
|---|---|---|
| 每5秒水溫 | QoS 0 | 少一筆通常可由下一筆補足;仍可在裝置端另做離線保存 |
| 溶氧過低警報 | QoS 1 | 不希望輕易遺失,但接收端必須能處理重複 |
| 故事播放開始事件 | QoS 1+事件UUID | 重送時以UUID去重,避免播放次數被重複計算 |
| 燈光即時亮度滑桿 | QoS 0 | 最新狀態通常比每次變化都抵達更重要 |
event_uuid、時間戳與應用層確認。三、Retain:讓新訂閱者立刻取得「最後狀態」
Publisher把訊息以Retain方式發布後,Broker會為該Topic保留最後一筆Retained Message。新的Subscriber一訂閱,就會立即收到它,不必等ESP32下一次發布。
適合Retain
裝置目前在線狀態、最後水溫、目前模式、韌體版本、開關的目標狀態。
不適合Retain
「餵魚一次」、「播放故事一次」、「重新啟動」等一次性命令。新裝置訂閱時若重播舊命令,可能造成危險。
| Topic | Retain | 理由 |
|---|---|---|
site/pond01/status/online | 是 | 儀表板訂閱後立即知道裝置狀態 |
site/pond01/telemetry/water_temp | 視需求 | 可顯示最後讀值,但介面必須同時呈現時間,避免把舊值誤認成即時值 |
site/pond01/cmd/feed_once | 否 | 避免斷線重連後再次執行舊命令 |
清除Retained Message:依MQTT慣例,對同一Topic發布空Payload並設定Retain,可要求Broker刪除該Retained Message;實際操作仍需依使用的Client函式庫確認。
四、LWT:裝置來不及說再見時,由Broker代為通知
LWT(Last Will and Testament,遺囑訊息)是在Client連線時,預先交給Broker的一則訊息。如果ESP32因斷電、訊號中斷或程式崩潰而非正常離線,Broker會代替它發布LWT。
- ESP32連線Broker時設定LWT:Topic為
site/pond01/status/online,Payload為offline,並設為Retain。 - 連線成功後,ESP32主動在同一Topic發布Retained
online。 - 若正常關機,可先發布
offline再主動Disconnect;若異常消失,由Broker發布LWT。
LWT不是即時斷線感測器:Broker通常要等到Keep Alive逾時或網路連線被判定中止後才發布,因此告警時間會受Keep Alive與網路狀況影響。
五、斷線重連:不要用while把整台ESP32卡死
初學範例常用無限while (!client.connected())反覆連線。場域系統若Broker故障,主迴圈就可能被卡住,感測、按鍵、JQ6500播放與離線保存全部停擺。
正確思路
採非阻塞重連、限制頻率、逐步增加等待時間;即使MQTT離線,裝置本地功能仍要運作。
恢復後要做
重新訂閱Topic、發布online、補送離線佇列、同步目標狀態,並防止重複事件。
| 重連次數 | 等待例 | 目的 |
|---|---|---|
| 第1次 | 1秒+隨機抖動 | 快速恢復短暫斷線 |
| 連續失敗 | 2、4、8、16…秒 | 指數退避,減少對Broker與網路的壓力 |
| 到達上限 | 例如60秒 | 避免等待時間無限增長 |
六、ESP32範例:LWT、Retain與非阻塞重連
#include <WiFi.h>
#include <PubSubClient.h>
WiFiClient net;
PubSubClient mqtt(net);
const char* broker = "192.168.1.10";
const char* statusTopic = "site/pond01/status/online";
const char* commandTopic = "site/pond01/cmd/#";
unsigned long nextRetryAt = 0;
unsigned long retryDelayMs = 1000;
const unsigned long maxRetryMs = 60000;
void onMessage(char* topic, byte* payload, unsigned int length) {
// 驗證Topic、Payload、長度與權限後再執行控制
}
bool connectMqtt() {
String clientId = "pond01-" + String((uint32_t)ESP.getEfuseMac(), HEX);
// clientId, user, password, willTopic, willQoS, willRetain, willMessage
bool ok = mqtt.connect(clientId.c_str(), "device_user", "device_password",
statusTopic, 1, true, "offline");
if (ok) {
mqtt.publish(statusTopic, "online", true); // Retained online
mqtt.subscribe(commandTopic, 1); // 重連後重新訂閱
retryDelayMs = 1000;
}
return ok;
}
void maintainMqtt() {
if (WiFi.status() != WL_CONNECTED) return;
if (mqtt.connected()) {
mqtt.loop();
return;
}
unsigned long now = millis();
if ((long)(now - nextRetryAt) < 0) return;
if (!connectMqtt()) {
unsigned long jitter = random(0, 500);
nextRetryAt = now + retryDelayMs + jitter;
retryDelayMs = min(retryDelayMs * 2, maxRetryMs);
}
}
void setup() {
Serial.begin(115200);
mqtt.setServer(broker, 1883);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
}
void loop() {
maintainMqtt();
// 感測、按鍵、播放與離線佇列仍持續執行
}
此程式著重架構,帳密與Broker位址僅為示意。不同版本的MQTT函式庫支援的QoS與API可能不同,正式使用前應查閱所採用函式庫文件;外網連線應使用TLS、憑證驗證與安全保存的憑證。
七、四個功能不能互相取代
| 機制 | 解決的問題 | 不能保證 |
|---|---|---|
| QoS | Client與Broker間的MQTT傳遞品質 | Django一定成功處理或寫入資料庫 |
| Retain | 新訂閱者立即取得某Topic最後保留值 | 訊息是最新現場狀態;必須搭配時間戳 |
| LWT | Client異常離線時由Broker發布預設訊息 | 零延遲偵測,也不代表設備硬體一定故障 |
| 重連 | 網路恢復後重新建立Session與訂閱 | 斷線期間資料自動存在;仍需離線佇列 |
八、常見錯誤與診斷線索
| 現象 | 可能原因 | 先檢查 |
|---|---|---|
| 連上Broker卻收不到命令 | 重連後忘記重新Subscribe、ACL拒絕、Topic拼錯 | CONNACK、訂閱結果、完整Topic與權限 |
| 儀表板一直顯示online | 未設LWT、LWT未Retain、狀態Topic不同 | 斷電測試與Broker日誌 |
| 一重連就再次啟動設備 | 一次性Command被設為Retain | 清除舊Retained Message並重整Topic設計 |
| 資料庫出現重複紀錄 | QoS 1重送或應用程式重試 | event_uuid、唯一約束、冪等寫入 |
| Broker恢復後大量設備仍離線 | 阻塞重連、重試太頻繁、驚群效應 | 指數退避、Jitter、資源使用量 |
| 顯示最後水溫但其實是昨天 | Retained值缺少時間 | Payload加入ts,UI顯示資料年齡 |
九、場域Topic設計範例
site/{site_id}/{device_id}/telemetry/water_temp
site/{site_id}/{device_id}/telemetry/dissolved_oxygen
site/{site_id}/{device_id}/event/story_start
site/{site_id}/{device_id}/status/online
site/{site_id}/{device_id}/status/firmware
site/{site_id}/{device_id}/cmd/set_mode
site/{site_id}/{device_id}/ack/{command_id}
- Telemetry:週期性量測,可依資料價值選QoS 0或1。
- Event:一次性事實,使用QoS 1、事件UUID及伺服器去重。
- Status:目前狀態,適合Retain並附時間戳。
- Command:控制要求,通常不Retain;加入command_id、期限與授權。
- Ack:設備實際接受或完成命令的應用層回覆,不能只看MQTT QoS。
十、學生實作:從「能傳」走向「可靠」
No-AI|觀察封包行為
用兩個MQTT Client測試QoS 0與1;比較Retain開關前後,新Subscriber第一次收到的內容,並記錄時間。
AI Pair|設計決策辯論
請AI分別扮演裝置工程師、Broker管理者與資料工程師,評論水溫、警報、控制命令應採用的QoS與Retain;學生查證後完成決策表。
Challenge AI|斷線演練
拔除網路、重啟Broker、讓ESP32斷電;驗證LWT、非阻塞重連、重新訂閱、離線補送、UUID去重與儀表板狀態是否正確。
十一、驗收清單
| 檢核項目 | 通過證據 |
|---|---|
| QoS符合資料語意 | 說得出可遺失、可重複與不可重複的差異 |
| Retain沒有誤用於一次性命令 | 新Client訂閱不會觸發舊動作 |
| LWT可辨識異常離線 | 直接斷電後,Broker會發布Retained offline |
| 斷線不阻塞本地功能 | Broker關閉時,感測與按鍵仍正常 |
| 恢復後不漏不重 | 完成重新訂閱、佇列補送與UUID去重 |
| 安全配置完成 | 獨立帳號、最小Topic ACL、TLS與憑證管理 |
沒有留言:
張貼留言