上一章我們使用 Flask Web Server,讓手機瀏覽器可以透過區域網路控制 Raspberry Pi GPIO。
整個架構是:
手機 Browser
│
│ HTTP
▼
Flask Web Server
│
▼
Python
│
▼
GPIO
│
▼
LED
這種方式非常適合建立:
- Web Dashboard
- 手機控制介面
- Raspberry Pi 管理頁面
- 遠端 GPIO 控制
但是如果系統開始出現很多 IoT 裝置,例如:
Raspberry Pi #1
Raspberry Pi #2
ESP32 #1
ESP32 #2
Sensor Node
Smart Relay
Web Dashboard
Database Server
如果全部裝置彼此直接建立 HTTP 連線,整個系統就會越來越複雜。
IoT 世界中有一個非常重要的通訊協定:
MQTT
MQTT 採用:
Publish / Subscribe
發布/訂閱模式。
裝置不需要知道其他裝置在哪裡,只需要把資料送到:
MQTT Broker
其他對這些資料有興趣的裝置,再向 Broker 訂閱對應的:
Topic
即可取得資料。
本章將使用 Raspberry Pi 安裝:
Eclipse Mosquitto MQTT Broker
並利用:
Python + Paho MQTT
完成:
Raspberry Pi MQTT 溫度監控與 LED 控制
最後建立的系統將可以:
- Raspberry Pi 讀取 DHT11 / DHT22 溫溼度
- Python 將 Sensor Data 發布到 MQTT Broker
- 另一個 MQTT Client 訂閱溫溼度
- 遠端 Client 發布 LED 控制指令
- Raspberry Pi 訂閱控制 Topic
- 收到 ON 後開啟 LED
- 收到 OFF 後關閉 LED
- 建立完整雙向 IoT 通訊架構
一、本章學習目標
完成本章後,你將了解:
- IoT 是什麼
- MQTT 是什麼
- MQTT Broker
- MQTT Client
- Publisher
- Subscriber
- Topic
- Payload
- Port 1883
- Mosquitto
- mosquitto_pub
- mosquitto_sub
- Python Paho MQTT
- MQTT Callback
- Sensor Data Publish
- GPIO Remote Control
- DHT11 / DHT22
- MQTT IoT 架構
- MQTT 安全基本觀念
最後完成:
Raspberry Pi MQTT 溫度監控與 LED 遠端控制系統
二、什麼是 IoT?
IoT 是:
Internet of Things
中文通常稱為:
物聯網
傳統電腦網路主要連接:
Computer
Notebook
Server
Smartphone
IoT 則把更多實體裝置接進網路,例如:
Temperature Sensor
Humidity Sensor
Smart Light
Smart Plug
Relay
Camera
Air Conditioner
Door Sensor
Motor
Pump
Industrial Machine
因此 IoT 的核心概念可以理解為:
Physical Device
│
▼
Sensor
│
▼
Controller
│
▼
Network
│
▼
Server / Cloud
│
▼
Application
Raspberry Pi 因為本身具有:
- Linux
- Python
- GPIO
- Wi-Fi
- Ethernet
- Database
- Web Server
- Camera
所以非常適合作為:
IoT Gateway
或:
IoT Edge Computer
三、IoT 系統需要解決什麼問題?
假設家中有:
客廳 Raspberry Pi
臥室 Raspberry Pi
工作室 Raspberry Pi
每台都有:
DHT22
LED
Relay
我們希望:
所有溫度
↓
中央監控系統
同時希望:
控制中心
↓
遠端控制每一台 Relay
如果使用傳統直接連線方式,可能變成:
Dashboard
├── Raspberry Pi #1
├── Raspberry Pi #2
├── Raspberry Pi #3
├── ESP32 #1
└── ESP32 #2
裝置增加後管理會變得複雜。
MQTT 則加入一個中央訊息交換角色:
MQTT Broker
/ | \
/ | \
▼ ▼ ▼
Sensor Dashboard Device
所有 Client 都與 Broker 溝通。
四、什麼是 MQTT?
MQTT 是一種輕量級的訊息傳遞協定,非常常見於:
- IoT
- Sensor Network
- Smart Home
- Industrial IoT
- Remote Monitoring
- Device Control
MQTT 最大特色之一就是使用:
Publish / Subscribe
也就是:
Publisher
│
│ Publish
▼
Broker
│
│ Deliver
▼
Subscriber
Publisher 不需要直接知道 Subscriber 的 IP。
Subscriber 也不需要直接連接 Publisher。
雙方只需要認識:
Broker。
Eclipse Mosquitto 是一套開放原始碼 MQTT Broker,目前支援 MQTT 5.0、3.1.1 與 3.1。
五、MQTT 最重要的四個角色
學 MQTT 一定要先理解:
Broker
Publisher
Subscriber
Topic
這四個概念。
六、什麼是 MQTT Broker?
Broker 可以理解為:
MQTT 訊息交換中心
例如:
Raspberry Pi Sensor
│
│ Publish
▼
Mosquitto
│
│ Forward
▼
Dashboard
Broker 的工作包括:
- 接收 Client Connection
- 接收 Publish Message
- 管理 Topic
- 尋找 Subscriber
- 將訊息轉送給 Subscriber
- 管理 Authentication
- 管理 Access Control
本章將直接讓 Raspberry Pi 安裝:
Mosquitto Broker
因此 Raspberry Pi 本身就可以同時扮演:
MQTT Broker
+
MQTT Client
+
Sensor Controller
七、什麼是 Publisher?
Publisher 就是:
發布訊息的一方
例如 DHT22 讀到:
Temperature = 28.6°C
Raspberry Pi 可以將:
28.6
發布到:
home/room1/temperature
完整概念:
DHT22
│
▼
Raspberry Pi
│
│ Publish
│
│ Topic:
│ home/room1/temperature
│
│ Payload:
│ 28.6
▼
MQTT Broker
此時 Raspberry Pi 就是:
Publisher。
八、什麼是 Subscriber?
Subscriber 就是:
訂閱訊息的一方
例如另一台電腦訂閱:
home/room1/temperature
當 Raspberry Pi 發布:
28.6
Broker 就會把:
28.6
傳送給 Subscriber。
架構:
Raspberry Pi
Publisher
│
│ 28.6
▼
Mosquitto Broker
│
│ 28.6
▼
Notebook
Subscriber
Paho Python Client 的官方 API 也是透過 publish() 發布訊息、subscribe() 訂閱 Topic,並利用 Network Loop 持續處理 MQTT 網路事件。
九、什麼是 MQTT Topic?
Topic 可以理解成:
訊息頻道名稱
例如:
home/room1/temperature
也可以設計:
home/room1/humidity
home/room1/led
home/room1/fan
home/room1/camera
甚至:
factory/line1/machine1/temperature
factory/line1/machine1/status
factory/line1/machine1/alarm
Topic 可以利用:
/
建立階層式結構。
十、良好的 MQTT Topic 設計
本章使用:
home/room1/temperature
home/room1/humidity
home/room1/led/set
home/room1/led/status
其中:
home
代表系統。
room1
代表設備位置。
temperature
代表溫度。
LED 則分成:
led/set
控制命令。
以及:
led/status
目前狀態。
因此資料流可以非常清楚。
十一、什麼是 Payload?
Topic 是頻道名稱。
Payload 才是真正傳送的內容。
例如:
Topic:
home/room1/temperature
Payload:
28.6
又例如:
Topic:
home/room1/led/set
Payload:
ON
所以 MQTT Message 可以理解為:
Topic + Payload
十二、MQTT 架構範例
假設 Raspberry Pi 讀取室內環境:
DHT22
│
▼
Raspberry Pi
│
├── Publish temperature
│
└── Publish humidity
│
▼
MQTT Broker
│
▼
Subscriber
│
▼
Dashboard
LED 控制則反過來:
Remote Client
│
│ Publish ON
▼
MQTT Broker
│
│ Deliver
▼
Raspberry Pi
│
▼
GPIO17
│
▼
LED ON
因此 MQTT 是:
雙向通訊。
十三、MQTT 常用 Port
未加密 MQTT 最常見的 TCP Port 是:
1883
因此:
Broker IP
+
Port 1883
就可以形成:
192.168.1.100:1883
如果使用 TLS 加密 MQTT,實際部署時則常另外設定 TLS Listener。
本章先以:
LAN + TCP 1883
進行教學。
十四、準備材料
本章實作需要:
| 材料 | 數量 |
|---|---|
| Raspberry Pi | 1 |
| Raspberry Pi OS | 1 |
| DHT11 或 DHT22 | 1 |
| LED | 1 |
| 220Ω~330Ω 電阻 | 1 |
| Breadboard | 1 |
| Dupont Wire | 數條 |
| Wi-Fi / Ethernet | 1 |
如果暫時沒有 DHT11 / DHT22,也可以先完成 Mosquitto 與 LED MQTT 控制。
十五、本章 GPIO 規劃
本章使用:
DHT Data → GPIO4
LED → GPIO17
也就是 BCM 編號:
| 功能 | BCM GPIO |
|---|---|
| DHT11 / DHT22 Data | GPIO4 |
| LED | GPIO17 |
注意:
BCM GPIO4 不等於 Physical Pin 4。
40-pin Header:
GPIO4 = Physical Pin 7
GPIO17 = Physical Pin 11
十六、LED 接線
LED:
GPIO17
│
▼
220Ω~330Ω
│
▼
LED Anode (+)
│
▼
LED Cathode (-)
│
▼
GND
十七、DHT11 / DHT22 接線
一般 DHT Module 常見接腳:
VCC
DATA
GND
接線:
| DHT | Raspberry Pi |
|---|---|
| VCC | 3.3V |
| DATA | GPIO4 |
| GND | GND |
概念:
Raspberry Pi
│
├── 3.3V ─────► VCC
│
├── GPIO4 ────► DATA
│
└── GND ──────► GND
如果使用的是裸 DHT Sensor,而不是已經帶有電阻的模組板,DATA 線通常還需要依模組規格加入 Pull-up Resistor。
初學者建議直接使用:
DHT11 / DHT22 Module
比較容易接線。
十八、安裝 Mosquitto
先更新 Raspberry Pi:
sudo apt update
安裝:
sudo apt install -y mosquitto mosquitto-clients
其中:
mosquitto
是 MQTT Broker。
而:
mosquitto-clients
提供:
mosquitto_pub
mosquitto_sub
兩個非常重要的測試工具。
十九、確認 Mosquitto 服務
可以使用:
systemctl status mosquitto
查看:
active (running)
如果尚未啟動:
sudo systemctl start mosquitto
如果希望開機自動啟動:
sudo systemctl enable mosquitto
二十、先不要急著寫 Python
在正式寫 Python 前,第一件事情應該先確認:
Broker 是否正常。
我們可以直接使用:
mosquitto_sub
mosquitto_pub
測試。
二十一、第一個 MQTT Subscriber
開啟 Terminal A:
mosquitto_sub \
-h localhost \
-t test/message
這個 Terminal 會等待:
test/message
的訊息。
此時看起來可能沒有任何輸出。
這是正常的。
因為 Subscriber 正在等待訊息。
二十二、第一個 MQTT Publisher
另外開啟 Terminal B。
執行:
mosquitto_pub \
-h localhost \
-t test/message \
-m "Hello MQTT"
此時 Terminal A 應該立刻出現:
Hello MQTT
恭喜你。
你已經完成第一個:
MQTT Publish / Subscribe
二十三、剛才到底發生什麼?
Terminal A:
mosquitto_sub
│
│ Subscribe
▼
test/message
Terminal B:
mosquitto_pub
│
│ Publish
▼
test/message
Broker:
Mosquitto
│
┌────────────┴────────────┐
│ │
Publisher Subscriber
│ ▲
│ Hello MQTT │
└─────────────────────────┘
Publisher 並沒有直接把訊息傳給 Subscriber。
中間一定經過:
Broker。
二十四、測試溫度 Topic
Terminal A:
mosquitto_sub \
-h localhost \
-t home/room1/temperature
Terminal B:
mosquitto_pub \
-h localhost \
-t home/room1/temperature \
-m "28.5"
Subscriber 就會看到:
28.5
這已經非常接近真正 Sensor Data。
二十五、一次監看很多 Topic
MQTT 支援 Wildcard。
例如:
#
可以用來訂閱多層 Topic。
測試:
mosquitto_sub \
-h localhost \
-t 'home/#' \
-v
-v 會同時顯示 Topic。
接著發布:
mosquitto_pub \
-h localhost \
-t home/room1/temperature \
-m "28.5"
再發布:
mosquitto_pub \
-h localhost \
-t home/room1/humidity \
-m "65"
Subscriber 可能看到:
home/room1/temperature 28.5
home/room1/humidity 65
這對 Debug MQTT 系統非常有用。
二十六、讓其他電腦連線 Mosquitto
這裡要注意新版 Mosquitto 的一個重要行為。
Mosquitto 2.x 在沒有額外 Listener Configuration 時,預設只允許本機 Loopback Connection,因此:
localhost
127.0.0.1
可以連線,但其他 LAN 電腦不一定可以直接連入。官方文件也說明,若要允許 Remote Client,需要建立 Listener;設定 Listener 後,匿名存取不會自動開啟。
所以如果我們希望:
Notebook
│
Wi-Fi
│
▼
Raspberry Pi MQTT Broker
就必須設定 Broker。
二十七、建立 LAN 測試用 Mosquitto 設定
建立:
sudo nano /etc/mosquitto/conf.d/raspberrypi.conf
教學環境先輸入:
listener 1883
allow_anonymous true
官方 Mosquitto 文件也以:
listener 1883
allow_anonymous true
作為允許未驗證 Client 連入的設定方式。
儲存後重新啟動:
sudo systemctl restart mosquitto
查看:
systemctl status mosquitto
二十八、重要安全警告
剛才:
allow_anonymous true
只適合:
- 家中實驗 LAN
- 隔離測試網路
- 教學環境
不要直接把:
TCP 1883
開放到 Internet。
因為任何能連線的人都可能:
Publish
Subscribe
Control Device
Read Sensor Data
正式 MQTT 系統至少應考慮:
- Username / Password
- ACL
- TLS
- Firewall
- Topic Permission
Mosquitto 官方提供 Password File、Authentication Plugin 與 Dynamic Security 等不同 Authentication / Authorization 機制。
二十九、查 Raspberry Pi IP
執行:
hostname -I
假設:
192.168.1.100
那麼 MQTT Broker 就是:
192.168.1.100
Port:
1883
三十、從另一台 Linux 電腦測試
Subscriber:
mosquitto_sub \
-h 192.168.1.100 \
-p 1883 \
-t test/message
Raspberry Pi 發布:
mosquitto_pub \
-h localhost \
-t test/message \
-m "Hello from Raspberry Pi"
遠端 Subscriber 如果看到:
Hello from Raspberry Pi
代表:
LAN
+
Mosquitto
+
MQTT
已經正常運作。
三十一、Python MQTT Library
接下來要讓 Python 自己 Publish / Subscribe。
我們使用:
Eclipse Paho MQTT Python
Paho MQTT Python Client 提供:
connect()
publish()
subscribe()
loop_start()
loop_forever()
disconnect()
等 MQTT Client 功能。
三十二、Python 套件安裝方式
Raspberry Pi OS Bookworm 之後,使用 pip 安裝第三方 Python Package 時,建議使用 Python Virtual Environment,以避免破壞系統管理的 Python 環境。Raspberry Pi 官方也特別說明 Bookworm 開始將 pip 套件放入 venv 是建議方式。
先安裝:
sudo apt install -y python3-full python3-venv
建立專案:
mkdir -p ~/mqtt_project
進入:
cd ~/mqtt_project
建立 Virtual Environment:
python3 -m venv --system-site-packages venv
啟用:
source venv/bin/activate
成功後命令提示字元前面通常會看到:
(venv)
三十三、安裝 Paho MQTT
在 Virtual Environment 中:
python -m pip install --upgrade pip
接著:
python -m pip install paho-mqtt
測試:
python
輸入:
import paho.mqtt.client as mqtt
如果沒有 Error:
exit()
就代表安裝完成。
三十四、第一個 Python MQTT Publisher
建立:
nano mqtt_publish.py
輸入:
import paho.mqtt.client as mqtt
BROKER = "localhost"
PORT = 1883
TOPIC = "home/room1/temperature"
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2
)
client.connect(
BROKER,
PORT,
60
)
client.publish(
TOPIC,
"28.5"
)
client.disconnect()
print("MQTT Publish 完成")
執行前先開 Subscriber:
mosquitto_sub \
-h localhost \
-t home/room1/temperature
接著:
python mqtt_publish.py
Subscriber 應顯示:
28.5
三十五、為什麼程式使用 CallbackAPIVersion.VERSION2?
較新的 Paho MQTT Python Client 已提供 Callback API Version 2,讓 MQTT 3.x 與 MQTT 5 的 Callback 參數行為更加一致;舊 Callback API Version 1 已被標示為 Deprecated,未來版本將移除。
因此新教學建議使用:
mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2
)
而不要完全照抄很多舊文章中的:
mqtt.Client()
三十六、第一個 Python Subscriber
建立:
nano mqtt_subscribe.py
輸入:
import paho.mqtt.client as mqtt
BROKER = "localhost"
PORT = 1883
TOPIC = "home/room1/temperature"
def on_connect(
client,
userdata,
flags,
reason_code,
properties
):
print(
"MQTT Connected:",
reason_code
)
client.subscribe(
TOPIC
)
def on_message(
client,
userdata,
message
):
payload = (
message.payload
.decode("utf-8")
)
print(
"Topic:",
message.topic
)
print(
"Message:",
payload
)
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2
)
client.on_connect = on_connect
client.on_message = on_message
client.connect(
BROKER,
PORT,
60
)
client.loop_forever()
執行:
python mqtt_subscribe.py
另一個 Terminal 發布:
mosquitto_pub \
-h localhost \
-t home/room1/temperature \
-m "29.1"
Python 顯示:
Topic: home/room1/temperature
Message: 29.1
三十七、什麼是 MQTT Callback?
例如:
client.on_connect = on_connect
表示:
MQTT Connection 成功
↓
執行 on_connect()
而:
client.on_message = on_message
表示:
收到 MQTT Message
↓
執行 on_message()
所以 MQTT 程式通常屬於:
Event Driven Programming
三十八、為什麼需要 loop_forever()?
MQTT Client 不只是:
Connect
Publish
Exit
Subscriber 必須一直等待 Broker 訊息。
因此需要 Network Loop:
client.loop_forever()
流程:
Connect Broker
↓
Subscribe Topic
↓
Network Loop
↓
等待 Message
↓
收到 Message
↓
Callback
↓
繼續等待
Paho 官方 API 也提供 loop_forever()、loop_start() 或定期呼叫 loop() 等方式維持 MQTT Network Processing。
三十九、MQTT 控制 LED
現在先不加入 Sensor。
我們先完成:
Remote Client
│
│ MQTT
▼
Broker
│
▼
Raspberry Pi
│
▼
GPIO17
│
▼
LED
控制 Topic:
home/room1/led/set
Payload:
ON
或:
OFF
四十、建立 MQTT LED Subscriber
建立:
nano mqtt_led.py
輸入:
import paho.mqtt.client as mqtt
from gpiozero import LED
BROKER = "localhost"
PORT = 1883
LED_SET_TOPIC = (
"home/room1/led/set"
)
LED_STATUS_TOPIC = (
"home/room1/led/status"
)
led = LED(17)
def on_connect(
client,
userdata,
flags,
reason_code,
properties
):
print(
"MQTT Connected:",
reason_code
)
client.subscribe(
LED_SET_TOPIC
)
def on_message(
client,
userdata,
message
):
command = (
message.payload
.decode("utf-8")
.strip()
.upper()
)
print(
"LED Command:",
command
)
if command == "ON":
led.on()
client.publish(
LED_STATUS_TOPIC,
"ON",
retain=True
)
elif command == "OFF":
led.off()
client.publish(
LED_STATUS_TOPIC,
"OFF",
retain=True
)
else:
print(
"Unknown Command:",
command
)
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2
)
client.on_connect = on_connect
client.on_message = on_message
client.connect(
BROKER,
PORT,
60
)
client.loop_forever()
四十一、測試 MQTT LED ON
執行:
python mqtt_led.py
另一個 Terminal:
mosquitto_pub \
-h localhost \
-t home/room1/led/set \
-m ON
LED 應該:
ON
四十二、測試 MQTT LED OFF
輸入:
mosquitto_pub \
-h localhost \
-t home/room1/led/set \
-m OFF
LED:
OFF
因此已完成:
MQTT 遠端 GPIO 控制
四十三、LED Status Topic
程式在收到:
ON
後,也會發布:
home/room1/led/status
Payload:
ON
因此可以另外開 Subscriber:
mosquitto_sub \
-h localhost \
-t home/room1/led/status
就能監看設備真正狀態。
四十四、set 與 status 為什麼分開?
這是很值得養成的 IoT 設計習慣。
控制 Topic:
home/room1/led/set
代表:
我要 LED 變成什麼狀態。
狀態 Topic:
home/room1/led/status
代表:
LED 現在是什麼狀態。
資料流:
Controller
│
│ led/set = ON
▼
Raspberry Pi
│
▼
LED ON
│
│ led/status = ON
▼
Broker
這比所有功能都塞在:
home/room1/led
更加清楚。
四十五、什麼是 Retain?
剛才程式:
client.publish(
LED_STATUS_TOPIC,
"ON",
retain=True
)
retain=True 表示 Broker 可以保留這個 Topic 最近一次的 Retained Message。
如此新的 Subscriber 剛訂閱:
home/room1/led/status
時,不必等 LED 下一次改變,就能取得最近的狀態。
對:
Device Status
Sensor Current State
Configuration State
非常有用。
四十六、加入 DHT11 / DHT22
接下來進入本章完整實作。
我們希望 Raspberry Pi:
DHT22
│
▼
Temperature
Humidity
│
▼
Python
│
▼
MQTT Broker
同時:
MQTT Command
│
▼
Python
│
▼
GPIO17
│
▼
LED
也就是一個程式同時:
Publish Sensor Data
以及:
Subscribe Device Command
四十七、安裝 DHT Python Library
如果使用 Adafruit CircuitPython DHT Library,可在前面建立的 Virtual Environment 中安裝:
source ~/mqtt_project/venv/bin/activate
然後:
python -m pip install \
adafruit-circuitpython-dht
依 Raspberry Pi OS / 硬體環境,DHT Library 所需的 GPIO Backend 套件可能有所差異,因此若安裝階段出現底層 GPIO 相依套件問題,應優先依目前 Raspberry Pi OS 與該 DHT Library 的版本文件處理,而不要直接照抄多年前的 Adafruit_DHT 舊版教學。
四十八、先測試 DHT22
建立:
nano dht_test.py
輸入:
import time
import board
import adafruit_dht
dht = adafruit_dht.DHT22(
board.D4,
use_pulseio=False
)
while True:
try:
temperature = (
dht.temperature
)
humidity = (
dht.humidity
)
print(
f"Temperature: "
f"{temperature:.1f} C"
)
print(
f"Humidity: "
f"{humidity:.1f} %"
)
except RuntimeError as error:
print(
"DHT read retry:",
error
)
time.sleep(2)
執行:
python dht_test.py
可能看到:
Temperature: 28.4 C
Humidity: 66.2 %
四十九、如果使用 DHT11
將:
dht = adafruit_dht.DHT22(
board.D4,
use_pulseio=False
)
改成:
dht = adafruit_dht.DHT11(
board.D4,
use_pulseio=False
)
即可。
因此:
DHT22 → adafruit_dht.DHT22
DHT11 → adafruit_dht.DHT11
五十、為什麼 DHT 偶爾讀取失敗?
DHT11 / DHT22 使用時序敏感的單線資料傳輸方式,實際使用時偶爾可能發生暫時性讀取錯誤。
所以不要因為一次:
RuntimeError
就立刻結束整個 IoT 程式。
比較好的方式是:
try:
# Read Sensor
except RuntimeError:
# Retry Later
如此 MQTT Service 才不會因為一次 Sensor Reading Error 就整個停止。
五十一、完整實作架構
現在正式建立:
Raspberry Pi MQTT 溫度監控與 LED 控制
完整硬體:
Raspberry Pi
│
┌──────────┴──────────┐
│ │
GPIO4 GPIO17
│ │
DHT22 LED
│
▼
Temperature
Humidity
軟體:
Python
│
┌───────────┴───────────┐
│ │
DHT Reading LED Control
│ ▲
│ Publish │ Subscribe
▼ │
Mosquitto Broker
五十二、本章 MQTT Topic
使用:
home/room1/temperature
home/room1/humidity
home/room1/led/set
home/room1/led/status
未來第 20 章還可以增加:
home/room1/buzzer/set
home/room1/buzzer/status
home/room1/fan/set
home/room1/fan/status
home/room1/camera/set
home/room1/system/status
五十三、完整 Python MQTT IoT 程式
建立:
cd ~/mqtt_project
建立:
nano mqtt_environment.py
輸入:
import time
import threading
import board
import adafruit_dht
import paho.mqtt.client as mqtt
from gpiozero import LED
# ============================================
# MQTT Settings
# ============================================
BROKER = "localhost"
PORT = 1883
KEEPALIVE = 60
# ============================================
# MQTT Topics
# ============================================
TEMP_TOPIC = (
"home/room1/temperature"
)
HUMIDITY_TOPIC = (
"home/room1/humidity"
)
LED_SET_TOPIC = (
"home/room1/led/set"
)
LED_STATUS_TOPIC = (
"home/room1/led/status"
)
# ============================================
# Sensor Settings
# ============================================
SENSOR_INTERVAL = 10
# ============================================
# GPIO
# ============================================
led = LED(17)
# DHT22 on GPIO4
dht = adafruit_dht.DHT22(
board.D4,
use_pulseio=False
)
# ============================================
# Program Control
# ============================================
running = True
# ============================================
# MQTT Connected Callback
# ============================================
def on_connect(
client,
userdata,
flags,
reason_code,
properties
):
print(
"MQTT Connected:",
reason_code
)
client.subscribe(
LED_SET_TOPIC
)
print(
"Subscribed:",
LED_SET_TOPIC
)
# Publish current LED status
if led.is_lit:
status = "ON"
else:
status = "OFF"
client.publish(
LED_STATUS_TOPIC,
status,
retain=True
)
# ============================================
# MQTT Message Callback
# ============================================
def on_message(
client,
userdata,
message
):
command = (
message.payload
.decode("utf-8")
.strip()
.upper()
)
print(
"Received:",
message.topic,
command
)
if message.topic == LED_SET_TOPIC:
if command == "ON":
led.on()
client.publish(
LED_STATUS_TOPIC,
"ON",
retain=True
)
print(
"LED -> ON"
)
elif command == "OFF":
led.off()
client.publish(
LED_STATUS_TOPIC,
"OFF",
retain=True
)
print(
"LED -> OFF"
)
else:
print(
"Unknown LED command"
)
# ============================================
# Sensor Publish Thread
# ============================================
def sensor_worker(
client
):
global running
while running:
try:
temperature = (
dht.temperature
)
humidity = (
dht.humidity
)
if (
temperature is not None
and
humidity is not None
):
temperature_text = (
f"{temperature:.1f}"
)
humidity_text = (
f"{humidity:.1f}"
)
client.publish(
TEMP_TOPIC,
temperature_text,
retain=True
)
client.publish(
HUMIDITY_TOPIC,
humidity_text,
retain=True
)
print(
"Temperature:",
temperature_text,
"C"
)
print(
"Humidity:",
humidity_text,
"%"
)
except RuntimeError as error:
print(
"DHT temporary error:",
error
)
except Exception as error:
print(
"Sensor error:",
error
)
time.sleep(
SENSOR_INTERVAL
)
# ============================================
# MQTT Client
# ============================================
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2
)
client.on_connect = on_connect
client.on_message = on_message
# ============================================
# Connect Broker
# ============================================
client.connect(
BROKER,
PORT,
KEEPALIVE
)
# ============================================
# Start MQTT Network Loop
# ============================================
client.loop_start()
# ============================================
# Start Sensor Thread
# ============================================
sensor_thread = threading.Thread(
target=sensor_worker,
args=(client,),
daemon=True
)
sensor_thread.start()
# ============================================
# Main Program
# ============================================
print(
"Raspberry Pi MQTT "
"Environment Monitor Started"
)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print(
"\nStopping..."
)
finally:
running = False
led.off()
dht.exit()
client.loop_stop()
client.disconnect()
print(
"MQTT IoT System Stopped"
)
五十四、執行完整系統
啟用 Virtual Environment:
cd ~/mqtt_project
source venv/bin/activate
執行:
python mqtt_environment.py
正常情況可能看到:
Raspberry Pi MQTT Environment Monitor Started
MQTT Connected: Success
Subscribed:
home/room1/led/set
Temperature: 28.5 C
Humidity: 67.2 %
Temperature: 28.6 C
Humidity: 67.1 %
五十五、監看所有 MQTT 資料
另外開 Terminal:
mosquitto_sub \
-h localhost \
-t 'home/room1/#' \
-v
可能看到:
home/room1/temperature 28.5
home/room1/humidity 67.2
home/room1/led/status OFF
每隔:
10 秒
又可能看到:
home/room1/temperature 28.6
home/room1/humidity 67.1
五十六、MQTT 遠端開啟 LED
輸入:
mosquitto_pub \
-h localhost \
-t home/room1/led/set \
-m ON
LED:
ON
同時 Status:
home/room1/led/status ON
五十七、MQTT 遠端關閉 LED
輸入:
mosquitto_pub \
-h localhost \
-t home/room1/led/set \
-m OFF
LED:
OFF
Status:
home/room1/led/status OFF
五十八、使用另一台電腦控制
假設 Raspberry Pi:
192.168.1.100
另一台安裝 Mosquitto Client 的電腦可以執行:
mosquitto_sub \
-h 192.168.1.100 \
-p 1883 \
-t 'home/room1/#' \
-v
就可以遠端看到:
home/room1/temperature 28.5
home/room1/humidity 67.2
home/room1/led/status OFF
遠端開 LED:
mosquitto_pub \
-h 192.168.1.100 \
-p 1883 \
-t home/room1/led/set \
-m ON
Raspberry Pi LED:
ON
這就是真正的:
LAN IoT Device Control
五十九、完整資料流程:溫度
DHT22:
DHT22
│
│ GPIO4
▼
Python
│
│ Read
▼
28.5°C
│
│ Publish
▼
home/room1/temperature
│
▼
Mosquitto Broker
│
▼
Subscriber
│
▼
28.5
六十、完整資料流程:濕度
DHT22
│
▼
Python
│
▼
67.2 %
│
│ Publish
▼
home/room1/humidity
│
▼
Broker
│
▼
Subscriber
六十一、完整資料流程:LED
控制:
Remote Client
│
│ Publish
│ ON
▼
home/room1/led/set
│
▼
Mosquitto
│
▼
Raspberry Pi
│
▼
on_message()
│
▼
led.on()
│
▼
GPIO17
│
▼
LED ON
接著 Raspberry Pi 回報:
LED ON
│
▼
home/room1/led/status
│
▼
Broker
│
▼
Subscriber
形成完整:
Command + Feedback
架構。
六十二、為什麼 Sensor Thread 與 MQTT Loop 分開?
程式同時要做兩件事情:
1. 等待 MQTT Command
2. 定時讀 Sensor
MQTT Network Loop:
client.loop_start()
會在背景處理 MQTT。
Sensor 則建立:
threading.Thread(...)
另一個 Thread 定期讀取 Sensor。
因此:
Main Program
│
├── MQTT Network Thread
│
└── Sensor Worker Thread
兩個工作可以同時進行。
六十三、不要用太快的 Sensor Publish Interval
目前設定:
SENSOR_INTERVAL = 10
代表:
10 秒發布一次
環境溫度通常不需要:
每 0.1 秒
更新一次。
因為這只會增加:
- Sensor Reading
- MQTT Message
- CPU Load
- Network Traffic
- Database Records
實際專案可依用途調整:
即時展示 2~10 秒
一般環境監控 30~60 秒
歷史趨勢 1~5 分鐘
六十四、Topic 命名不要隨便設計
不建議:
temp
hum
led
data1
data2
系統小的時候還能理解。
裝置多了就會不知道:
temp
到底是哪一台。
比較好的設計:
home/livingroom/temperature
home/bedroom/temperature
home/serverroom/temperature
甚至:
wcst/building1/floor2/room3/temperature
Topic 本身就是 IoT 系統的重要資料架構。
六十五、多台 Raspberry Pi MQTT 架構
假設有三台 Raspberry Pi:
room1
room2
room3
Topic:
home/room1/temperature
home/room2/temperature
home/room3/temperature
全部發布到:
MQTT Broker
中央系統只需要訂閱:
home/+/temperature
就可以取得不同房間的 Temperature。
架構:
Room1 Pi ──┐
│
Room2 Pi ──┼──► MQTT Broker ──► Dashboard
│
Room3 Pi ──┘
這就是 MQTT 比直接 HTTP Point-to-Point 更容易擴充的原因之一。
六十六、MQTT Wildcard
MQTT 常見兩種 Wildcard:
+
#
+
代表:
單一層級
例如:
home/+/temperature
可能匹配:
home/room1/temperature
home/room2/temperature
home/room3/temperature
#
代表:
後續所有層級
例如:
home/#
可以涵蓋:
home/room1/temperature
home/room1/humidity
home/room1/led/status
home/room2/temperature
Debug 時:
mosquitto_sub \
-h localhost \
-t 'home/#' \
-v
非常方便。
六十七、MQTT QoS 是什麼?
MQTT 還有一個重要概念:
QoS
Quality of Service。
基本可分為:
QoS 0
QoS 1
QoS 2
QoS 0
概念:
最多傳一次
速度快、負擔低。
一般即時 Sensor Data 很常使用。
QoS 1
概念:
至少傳一次
可靠度提高,但可能發生重複訊息,因此 Subscriber 設計時仍應考慮 Idempotency。
QoS 2
提供更完整的傳遞握手流程,用於要求更嚴格的訊息傳遞情境,但成本也更高。
對初學者而言:
Sensor Data → QoS 0 或依需求選擇 QoS 1
重要 Control → 依系統可靠性設計 QoS
即可先建立正確觀念。
六十八、JSON Sensor Data
目前我們分開發布:
temperature
humidity
大型系統也可以使用 JSON:
{
"temperature": 28.5,
"humidity": 67.2
}
例如 Topic:
home/room1/environment
Python:
import json
data = {
"temperature": 28.5,
"humidity": 67.2
}
payload = json.dumps(
data
)
client.publish(
"home/room1/environment",
payload
)
Subscriber 就可以一次取得多個 Sensor Value。
六十九、加入 Timestamp
還可以加入:
{
"temperature": 28.5,
"humidity": 67.2,
"timestamp": "2026-08-18T08:40:00+08:00"
}
這對下一章:
SQLite 歷史資料
非常重要。
因為 Database 需要知道:
哪一筆資料
是在什麼時間
產生的
七十、MQTT 與 Flask 有什麼不同?
第 17 章學 Flask。
本章學 MQTT。
兩者不是互相取代。
而是用途不同。
| 功能 | Flask / HTTP | MQTT |
|---|---|---|
| Web Browser | 非常適合 | 需 MQTT Client |
| Dashboard | 適合 | 通常搭配 Dashboard |
| REST API | 適合 | 不屬於主要用途 |
| Sensor Telemetry | 可以 | 非常適合 |
| Device Control | 可以 | 非常適合 |
| 多裝置訊息 | 較複雜 | 很適合 |
| Broker | 不需要 | 需要 |
| Pub/Sub | 非主要架構 | 核心架構 |
七十一、Flask + MQTT 一起使用
真正的 Raspberry Pi IoT 系統很常同時使用:
Flask
+
MQTT
例如:
Smartphone
│
▼
Flask
│
▼
Dashboard
│
│
▼
MQTT Broker
/ | \
/ | \
▼ ▼ ▼
Room1 Room2 Room3
Flask 負責:
UI
Web Page
User Control
MQTT 負責:
Device Message
Sensor Data
Remote Command
七十二、MQTT + SQLite
下一章加入 SQLite 後:
DHT22
│
▼
Raspberry Pi
│
├── MQTT Publish
│
└── SQLite INSERT
即可同時做到:
即時資料
+
歷史資料
例如:
MQTT
→ 現在溫度 28.5°C
SQLite
→ 過去 24 小時溫度
七十三、MQTT + Camera
第 16 章學過 Picamera2。
因此也可以增加:
home/room1/camera/set
發布:
PHOTO
Raspberry Pi 收到:
PHOTO
執行:
Picamera2
↓
Capture
↓
photo.jpg
架構:
Remote Client
│
│ PHOTO
▼
MQTT Broker
│
▼
Raspberry Pi
│
▼
Picamera2
│
▼
Camera
七十四、MQTT + Relay
LED 只是最安全的入門元件。
未來把:
LED
換成:
Relay
就可以形成:
MQTT
↓
Raspberry Pi
↓
Relay
↓
Fan
例如:
home/room1/fan/set
Payload:
ON
OFF
這就是第 20 章:
智慧環境監控中心
的重要功能。
七十五、溫度自動控制 + MQTT
甚至可以讓 Raspberry Pi 自己判斷:
Temperature >= 30°C
│
▼
Fan ON
同時 MQTT 只是負責回報:
home/room1/fan/status
這樣即使:
Network Down
Raspberry Pi 本機仍然可以繼續進行:
Temperature Control
這是 IoT 系統設計非常重要的原則:
基本安全控制不要完全依賴遠端網路。
七十六、本章常用指令整理
安裝 Mosquitto:
sudo apt update
sudo apt install -y \
mosquitto \
mosquitto-clients
查看服務:
systemctl status mosquitto
重新啟動:
sudo systemctl restart mosquitto
開機啟動:
sudo systemctl enable mosquitto
Subscriber:
mosquitto_sub \
-h localhost \
-t test/message
Publisher:
mosquitto_pub \
-h localhost \
-t test/message \
-m "Hello MQTT"
監看所有 Home Topic:
mosquitto_sub \
-h localhost \
-t 'home/#' \
-v
控制 LED:
mosquitto_pub \
-h localhost \
-t home/room1/led/set \
-m ON
關閉:
mosquitto_pub \
-h localhost \
-t home/room1/led/set \
-m OFF
七十七、常見問題:Connection Refused
如果看到:
Connection Refused
先確認:
systemctl status mosquitto
Broker 是否:
active (running)
如果 Localhost 正常,但其他電腦不能連:
檢查:
/etc/mosquitto/conf.d/
是否已設定 LAN Listener。
例如測試環境:
listener 1883
allow_anonymous true
然後:
sudo systemctl restart mosquitto
Mosquitto 2.x 的預設 Local-only 行為是常見的「本機能連、其他電腦不能連」原因。
七十八、常見問題:Port 1883 無法連線
確認 Broker Port:
ss -lnt
尋找:
1883
也可以查看 Mosquitto Service Log:
journalctl \
-u mosquitto \
--no-pager
若修改 Configuration 後 Mosquitto 無法啟動,優先檢查:
mosquitto.conf
conf.d/*.conf
是否存在語法錯誤。
七十九、常見問題:Python 找不到 paho
如果出現:
ModuleNotFoundError:
No module named 'paho'
先確認 Virtual Environment:
source ~/mqtt_project/venv/bin/activate
再:
python -m pip install paho-mqtt
查看:
python -m pip list
八十、常見問題:DHT 偶爾 Error
如果偶爾看到:
DHT temporary error
不一定代表硬體故障。
可以:
- 確認接線
- 確認 3.3V / GND
- 確認 GPIO4
- 檢查 Pull-up
- 不要讀取過快
- 程式捕捉 RuntimeError 後重新讀取
如果每次都失敗,才應進一步檢查:
Sensor
Wiring
Library
GPIO Backend
Raspberry Pi OS
八十一、常見問題:LED 沒有動作
先不要測 MQTT。
先用:
from gpiozero import LED
from time import sleep
led = LED(17)
led.on()
sleep(3)
led.off()
確認 LED 本身可以工作。
如果 GPIO Test 正常,而 MQTT 不正常,再檢查:
Topic
Payload
Subscriber
Callback
Broker Connection
八十二、Debug MQTT 最好用的方法
開一個專門的 Terminal:
mosquitto_sub \
-h localhost \
-t '#' \
-v
這會看到 Broker 中大量 Topic Message。
如果系統比較大,則縮小為:
mosquitto_sub \
-h localhost \
-t 'home/#' \
-v
可以很快知道:
Publisher 有沒有發布?
Topic 有沒有打錯?
Payload 是什麼?
LED status 有沒有回報?
八十三、MQTT 安全架構
正式環境不要只使用:
allow_anonymous true
較合理的方向是:
MQTT Client
│
│ Authentication
▼
Mosquitto
│
│ ACL
▼
Allowed Topic
例如:
sensor01
只能 Publish:
home/room1/#
而不能 Publish:
factory/admin/#
八十四、進一步加入 Username / Password
正式學習時可以進一步研究:
password_file
以及:
mosquitto_passwd
建立 MQTT Account。
概念:
Client
│
├── Username
└── Password
│
▼
Broker
但即使有 Password,如果透過不加密網路傳送,仍應考慮 TLS,因此真正 Internet-facing 的 MQTT 系統不能只停在 Username / Password。
八十五、不要直接把 MQTT 1883 暴露到 Internet
如果 Router 直接:
WAN
│
Port Forward 1883
│
▼
Raspberry Pi
而 Broker:
allow_anonymous true
這是非常不好的正式部署方式。
本章的:
1883 + anonymous
只用於:
LAN 教學實驗。
Internet IoT 應增加:
TLS
Authentication
ACL
Firewall
VPN
Logging
等保護。
八十六、本章與第 17 章最大的不同
第 17 章:
Browser
│
▼
HTTP
│
▼
Flask
第 18 章:
Publisher
│
▼
MQTT Broker
│
▼
Subscriber
Flask 是:
Request / Response
MQTT 則以:
Publish / Subscribe
作為核心。
八十七、本章完成後的 Raspberry Pi 架構
現在我們已經有:
Raspberry Pi
│
┌───────────┼───────────┐
│ │ │
GPIO Camera Network
│ │ │
▼ ▼ ▼
LED/Relay Picamera2 Flask
│
└── HTTP
│
▼
Mosquitto
│
▼
MQTT
Raspberry Pi 已經不再只是:
GPIO 開發板。
而是一台:
IoT Edge Server
八十八、第四篇目前進度
第 16 章:
Camera
↓
Picamera2
第 17 章:
Browser
↓
Flask
↓
GPIO
第 18 章:
Sensor / Device
↓
MQTT
↓
Mosquitto
下一章:
Sensor Data
↓
SQLite
↓
History Database
最後第 20 章將把全部功能整合。
八十九、本章重點整理
MQTT 最重要的架構是:
Publisher
│
│ Publish
▼
Topic
│
▼
Broker
│
│ Deliver
▼
Subscriber
其中:
Publisher
負責發送資料。
Subscriber
負責接收資料。
Broker
負責管理與轉送訊息。
Topic
負責分類訊息。
Payload
是真正的資料內容。
本章完成:
DHT22
│
▼
Raspberry Pi
│
│ MQTT Publish
▼
Mosquitto
│
▼
Temperature Monitor
以及:
Remote Controller
│
│ MQTT Publish
▼
Mosquitto
│
│ MQTT Subscribe
▼
Raspberry Pi
│
▼
GPIO17
│
▼
LED
因此已經完成:
Raspberry Pi MQTT 雙向 IoT 系統
九十、本章實作成果
最後完成的:
Raspberry Pi MQTT 溫度監控與 LED 控制
具備以下功能:
- Raspberry Pi 連接 DHT11 / DHT22
- GPIO4 讀取溫溼度
- Python 定期取得 Sensor Data
- Temperature 發布至 MQTT
- Humidity 發布至 MQTT
- Mosquitto 作為 MQTT Broker
- 遠端 Client 可 Subscribe Sensor Data
- 遠端 Client 可 Publish LED Command
- Raspberry Pi Subscribe LED Set Topic
- GPIO17 控制 LED
- LED 狀態重新 Publish
- 使用 Retained Status 保存目前設備狀態
整個系統已經具有:
Sensor
+
Network
+
Messaging
+
Remote Control
+
Device Feedback
這五個 IoT 基本元素。
九十一、下一章預告
下一章將進入:
第 19 章 Raspberry Pi 資料庫與環境監測:SQLite 儲存感測器歷史資料
目前 MQTT 可以告訴我們:
現在溫度 = 28.5°C
但是如果我們想知道:
昨天最高溫是多少?
今天凌晨溫度是多少?
過去 7 天平均濕度多少?
溫度什麼時候超過 30°C?
就必須:
保存歷史資料。
下一章會加入:
SQLite Database
架構將從:
DHT22
│
▼
Python
│
▼
MQTT
進一步變成:
DHT22
│
▼
Python
│
┌──────────┴──────────┐
│ │
▼ ▼
MQTT SQLite
│ │
▼ ▼
Real-Time Data History Data
並實作:
Raspberry Pi 溫溼度歷史資料庫
學習:
- Database
- SQLite
- Table
- INSERT
- SELECT
- Timestamp
- Python sqlite3
- Sensor Data Logging
- History Query
- CSV Export
這會替第 20 章完整:
Raspberry Pi 智慧環境監控中心
準備好最後一個重要資料層。