第18章 Raspberry Pi IoT 入門:MQTT 遠端感測與裝置控制

發布日期: |更新日期:
文章主圖:第18章 Raspberry Pi IoT 入門:MQTT 遠端感測與裝置控制

上一章我們使用 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 控制

最後建立的系統將可以:

  1. Raspberry Pi 讀取 DHT11 / DHT22 溫溼度
  2. Python 將 Sensor Data 發布到 MQTT Broker
  3. 另一個 MQTT Client 訂閱溫溼度
  4. 遠端 Client 發布 LED 控制指令
  5. Raspberry Pi 訂閱控制 Topic
  6. 收到 ON 後開啟 LED
  7. 收到 OFF 後關閉 LED
  8. 建立完整雙向 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

進行教學。

十四、準備材料

本章實作需要:

第 18 章 MQTT IoT 實作材料
材料 數量
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 編號:

第 18 章 GPIO 規劃
功能 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

接線:

DHT11 / DHT22 與 Raspberry Pi 接線
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 功能比較
功能 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 控制

具備以下功能:

  1. Raspberry Pi 連接 DHT11 / DHT22
  2. GPIO4 讀取溫溼度
  3. Python 定期取得 Sensor Data
  4. Temperature 發布至 MQTT
  5. Humidity 發布至 MQTT
  6. Mosquitto 作為 MQTT Broker
  7. 遠端 Client 可 Subscribe Sensor Data
  8. 遠端 Client 可 Publish LED Command
  9. Raspberry Pi Subscribe LED Set Topic
  10. GPIO17 控制 LED
  11. LED 狀態重新 Publish
  12. 使用 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 智慧環境監控中心

準備好最後一個重要資料層。

下一步學習

不要停在這一篇,接著把內容做成自己的實作

目前主題:ESP32 / IoT。從完整學習路線、免費資源、會員下載,再接到實作所需材料。

同系列:Raspberry-Pi |  已是會員?直接前往 Getfile
相關資源與會員文章下載
文章可免費閱讀;下載完整 DOCX 或 PDF 需登入免費基本會員。
會員免費下載 DOCX 會員免費 PDF/列印

← 回文章列表