☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
iot 中級

IoT 通訊協定與感測器

GPIO 深入理解

💡 比喻:Raspberry Pi 的神經系統 人的神經系統從大腦延伸到手指、腳趾,感覺冷熱、控制肌肉。 GPIO 就是 Raspberry Pi 的神經系統——那 40 根針腳就是它的「手指」, 可以感測外界訊號(輸入),也可以控制外部裝置(輸出)。

GPIO 針腳圖

Raspberry Pi GPIO 針腳配置(40 Pin):
┌─────────────────────────────────────┐
│  3V3 (1)  (2)  5V                   │  // 第 1-2 腳:電源
│  GPIO2 (3)  (4)  5V                 │  // 第 3 腳:I2C SDA
│  GPIO3 (5)  (6)  GND               │  // 第 5 腳:I2C SCL
│  GPIO4 (7)  (8)  GPIO14            │  // 第 8 腳:UART TXD
│  GND (9)  (10) GPIO15              │  // 第 10 腳:UART RXD
│  GPIO17(11) (12) GPIO18            │  // 第 12 腳:PWM
│  GPIO27(13) (14) GND               │  // 第 13 腳:通用 GPIO
│  GPIO22(15) (16) GPIO23            │  // 第 15-16 腳:通用 GPIO
│  3V3 (17) (18) GPIO24              │  // 第 17 腳:3.3V 電源
│  GPIO10(19) (20) GND               │  // 第 19 腳:SPI MOSI
│  GPIO9 (21) (22) GPIO25            │  // 第 21 腳:SPI MISO
│  GPIO11(23) (24) GPIO8             │  // 第 23 腳:SPI SCLK
│  GND (25) (26) GPIO7               │  // 第 24 腳:SPI CE0
│  GPIO0 (27) (28) GPIO1             │  // 第 27-28 腳:I2C EEPROM
│  GPIO5 (29) (30) GND               │  // 第 29 腳:通用 GPIO
│  GPIO6 (31) (32) GPIO12            │  // 第 31 腳:通用 GPIO
│  GPIO13(33) (34) GND               │  // 第 33 腳:PWM1
│  GPIO19(35) (36) GPIO16            │  // 第 35 腳:PCM FS
│  GPIO26(37) (38) GPIO20            │  // 第 37 腳:通用 GPIO
│  GND (39) (40) GPIO21              │  // 第 39 腳:接地
└─────────────────────────────────────┘

用 C# 讀取 GPIO

安裝 NuGet 套件

# 安裝 GPIO 套件 // .NET IoT 官方函式庫
dotnet add package System.Device.Gpio   // 加入 GPIO 支援
dotnet add package Iot.Device.Bindings  // 加入感測器綁定

GPIO 基本控制

// 引用 GPIO 命名空間 // .NET IoT 核心
using System.Device.Gpio; // GPIO 控制類別

// GPIO 控制服務 // 管理所有 GPIO 針腳
public class GpioService : IDisposable // GPIO 服務(實作 IDisposable)
{
    private readonly GpioController _controller; // GPIO 控制器
    private readonly ILogger<GpioService> _logger; // 日誌記錄器

    // LED 針腳定義 // 定義各裝置的針腳號碼
    private const int LED_PIN = 17; // LED 接在 GPIO17
    private const int BUTTON_PIN = 27; // 按鈕接在 GPIO27
    private const int BUZZER_PIN = 22; // 蜂鳴器接在 GPIO22

    // 建構函式 // 初始化 GPIO 控制器
    public GpioService(ILogger<GpioService> logger) // 注入日誌服務
    {
        _logger = logger; // 儲存日誌記錄器
        _controller = new GpioController(); // 建立 GPIO 控制器

        // 設定針腳模式 // 輸出控制 LED,輸入讀取按鈕
        _controller.OpenPin(LED_PIN, PinMode.Output); // LED 設為輸出
        _controller.OpenPin(BUTTON_PIN, PinMode.InputPullUp); // 按鈕設為輸入(上拉)
        _controller.OpenPin(BUZZER_PIN, PinMode.Output); // 蜂鳴器設為輸出

        _logger.LogInformation("GPIO 初始化完成"); // 記錄初始化成功
    }

    // 控制 LED // 開啟或關閉 LED
    public void SetLed(bool on) // 設定 LED 狀態方法
    {
        var value = on ? PinValue.High : PinValue.Low; // 轉換布林為電位
        _controller.Write(LED_PIN, value); // 寫入 GPIO 電位
        _logger.LogDebug("LED 狀態:{State}", on ? "開" : "關"); // 記錄狀態
    }

    // 讀取按鈕 // 偵測按鈕是否被按下
    public bool IsButtonPressed() // 讀取按鈕狀態方法
    {
        var value = _controller.Read(BUTTON_PIN); // 讀取 GPIO 電位
        return value == PinValue.Low; // 上拉電路按下時為 Low
    }

    // 蜂鳴器嗶聲 // 發出提示音
    public async Task BeepAsync(int durationMs = 200) // 蜂鳴器方法
    {
        _controller.Write(BUZZER_PIN, PinValue.High); // 開啟蜂鳴器
        await Task.Delay(durationMs); // 等待指定毫秒數
        _controller.Write(BUZZER_PIN, PinValue.Low); // 關閉蜂鳴器
    }

    // 釋放資源 // 清理 GPIO 資源
    public void Dispose() // 實作 IDisposable
    {
        _controller.ClosePin(LED_PIN); // 關閉 LED 針腳
        _controller.ClosePin(BUTTON_PIN); // 關閉按鈕針腳
        _controller.ClosePin(BUZZER_PIN); // 關閉蜂鳴器針腳
        _controller.Dispose(); // 釋放控制器
    }
}

I2C 通訊(溫濕度感測器 DHT22)

讀取 DHT22 感測器

// 引用 IoT 感測器綁定 // DHT22 溫濕度感測器
using Iot.Device.DHTxx; // DHT 系列感測器
using System.Device.I2c; // I2C 通訊
using UnitsNet; // 物理單位換算

// 溫濕度監控服務 // 定期讀取環境溫濕度
public class TemperatureService // 溫濕度服務類別
{
    private readonly ILogger<TemperatureService> _logger; // 日誌記錄器
    private const int DHT_PIN = 4; // DHT22 資料線接 GPIO4

    // 建構函式 // 注入日誌服務
    public TemperatureService(ILogger<TemperatureService> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 讀取溫濕度 // 回傳溫度和濕度數值
    public (double Temperature, double Humidity)? ReadSensor() // 讀取感測器方法
    {
        try // 嘗試讀取感測器
        {
            using var dht = new Dht22(DHT_PIN); // 建立 DHT22 實例

            var temp = dht.Temperature; // 讀取溫度
            var humidity = dht.Humidity; // 讀取濕度

            if (temp.Equals(default(Temperature)) || // 檢查溫度是否有效
                humidity.Equals(default(RelativeHumidity))) // 檢查濕度是否有效
            {
                _logger.LogWarning("感測器讀取失敗"); // 記錄讀取失敗
                return null; // 回傳 null 表示失敗
            }

            var result = (temp.DegreesCelsius, humidity.Percent); // 組合結果
            _logger.LogInformation( // 記錄讀取結果
                "溫度:{Temp:F1}°C,濕度:{Hum:F1}%", // 格式化訊息
                result.DegreesCelsius, result.Percent); // 傳入參數

            return result; // 回傳溫濕度結果
        }
        catch (Exception ex) // 捕捉例外
        {
            _logger.LogError(ex, "DHT22 讀取錯誤"); // 記錄錯誤
            return null; // 回傳 null
        }
    }
}

SPI 通訊(RFID 讀卡器 RC522)

RFID 讀卡服務

// RFID 讀卡服務 // 使用 SPI 通訊讀取 RFID 卡片
public class RfidService // RFID 服務類別
{
    private readonly ILogger<RfidService> _logger; // 日誌記錄器

    // RFID 事件 // 當卡片被偵測到時觸發
    public event Action<string>? OnCardDetected; // 卡片偵測事件

    // 建構函式 // 注入日誌
    public RfidService(ILogger<RfidService> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 啟動 RFID 監聽 // 持續偵測卡片
    public async Task StartListeningAsync( // 開始監聽方法
        CancellationToken ct) // 取消令牌
    {
        _logger.LogInformation("RFID 讀卡器已啟動"); // 記錄啟動

        while (!ct.IsCancellationRequested) // 持續監聽直到取消
        {
            try // 嘗試讀取卡片
            {
                var cardId = await ReadCardAsync(); // 讀取卡片 ID
                if (cardId != null) // 如果有偵測到卡片
                {
                    _logger.LogInformation("偵測到卡片:{CardId}", cardId); // 記錄卡片
                    OnCardDetected?.Invoke(cardId); // 觸發卡片偵測事件
                }
            }
            catch (Exception ex) // 捕捉例外
            {
                _logger.LogError(ex, "RFID 讀取錯誤"); // 記錄錯誤
            }

            await Task.Delay(500, ct); // 每 0.5 秒偵測一次
        }
    }

    // 讀取卡片 ID // 透過 SPI 通訊取得 UID
    private Task<string?> ReadCardAsync() // 讀取卡片非同步方法
    {
        // 實際實作需要 SPI 通訊 // 這裡是模擬框架
        // 使用 System.Device.Spi 進行通訊 // SPI 匯流排
        return Task.FromResult<string?>(null); // 回傳讀取結果
    }
}

UART 通訊(條碼掃描模組)

串口條碼讀取

// 引用串口通訊命名空間 // UART 序列埠
using System.IO.Ports; // 串口通訊類別

// 條碼掃描服務 // 透過 UART 讀取條碼
public class BarcodeService : IDisposable // 條碼服務(可釋放)
{
    private readonly SerialPort _serialPort; // 串口物件
    private readonly ILogger<BarcodeService> _logger; // 日誌記錄器

    // 條碼掃描事件 // 掃到條碼時觸發
    public event Action<string>? OnBarcodeScanned; // 條碼掃描事件

    // 建構函式 // 初始化串口設定
    public BarcodeService(ILogger<BarcodeService> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器

        _serialPort = new SerialPort // 建立串口物件
        {
            PortName = "/dev/ttyUSB0", // Linux 串口路徑
            BaudRate = 9600, // 鮑率 9600
            DataBits = 8, // 資料位元 8
            Parity = Parity.None, // 無同位元檢查
            StopBits = StopBits.One, // 停止位元 1
            ReadTimeout = 1000 // 讀取超時 1 秒
        };

        // 註冊資料接收事件 // 當串口收到資料時觸發
        _serialPort.DataReceived += OnDataReceived; // 綁定接收事件
    }

    // 開啟串口 // 開始接收條碼資料
    public void Open() // 開啟串口方法
    {
        _serialPort.Open(); // 開啟串口連線
        _logger.LogInformation("條碼掃描器已連線:{Port}", // 記錄連線
            _serialPort.PortName); // 顯示埠名
    }

    // 串口資料接收處理 // 解析條碼字串
    private void OnDataReceived(object sender, // 發送者參數
        SerialDataReceivedEventArgs e) // 事件參數
    {
        var barcode = _serialPort.ReadLine().Trim(); // 讀取一行並去空白
        if (!string.IsNullOrEmpty(barcode)) // 如果條碼不為空
        {
            _logger.LogInformation("掃描到條碼:{Barcode}", barcode); // 記錄條碼
            OnBarcodeScanned?.Invoke(barcode); // 觸發掃描事件
        }
    }

    // 釋放資源 // 關閉串口
    public void Dispose() // 實作 IDisposable
    {
        if (_serialPort.IsOpen) // 如果串口是開的
            _serialPort.Close(); // 關閉串口
        _serialPort.Dispose(); // 釋放串口資源
    }
}

MQTT 訊息佇列

💡 比喻:物聯網的 LINE 群組 MQTT 就像一個 LINE 群組——你發一則訊息(Publish), 所有加入群組的人(Subscribe)都會收到。 不用知道對方是誰,只要在同一個「主題」(Topic)下就能通訊。

MQTT 客戶端

// 引用 MQTT 套件 // 安裝 MQTTnet NuGet
// dotnet add package MQTTnet // 加入 MQTT 支援

// MQTT 通訊服務 // 物聯網訊息發布與訂閱
public class MqttService // MQTT 服務類別
{
    private readonly ILogger<MqttService> _logger; // 日誌記錄器
    private readonly string _brokerHost; // MQTT Broker 位址
    private readonly int _brokerPort; // MQTT Broker 埠號

    // MQTT 主題定義 // 依功能分類的主題
    public static class Topics // 主題常數類別
    {
        public const string Temperature = "pos/sensors/temperature"; // 溫度主題
        public const string Humidity = "pos/sensors/humidity"; // 濕度主題
        public const string Barcode = "pos/scanner/barcode"; // 條碼主題
        public const string Receipt = "pos/printer/receipt"; // 收據主題
        public const string Alert = "pos/system/alert"; // 系統警報主題
    }

    // 建構函式 // 設定 Broker 連線資訊
    public MqttService( // 建構 MQTT 服務
        ILogger<MqttService> logger, // 注入日誌
        string brokerHost = "localhost", // 預設本地 Broker
        int brokerPort = 1883) // 預設 MQTT 埠
    {
        _logger = logger; // 儲存日誌記錄器
        _brokerHost = brokerHost; // 儲存 Broker 位址
        _brokerPort = brokerPort; // 儲存 Broker 埠號
    }

    // 發布訊息 // 將資料發送到指定主題
    public async Task PublishAsync( // 發布訊息方法
        string topic, string message) // 主題和訊息內容
    {
        _logger.LogInformation( // 記錄發布動作
            "發布到 {Topic}: {Message}", // 格式化訊息
            topic, message); // 傳入參數

        // 實際發布邏輯 // 透過 MQTTnet 發送
        await Task.CompletedTask; // 非同步發布完成
    }

    // 訂閱主題 // 接收指定主題的訊息
    public async Task SubscribeAsync( // 訂閱主題方法
        string topic, // 要訂閱的主題
        Action<string> onMessage) // 收到訊息的回呼
    {
        _logger.LogInformation("訂閱主題:{Topic}", topic); // 記錄訂閱

        // 實際訂閱邏輯 // 透過 MQTTnet 訂閱
        await Task.CompletedTask; // 非同步訂閱完成
    }
}

WebSocket vs MQTT 比較

┌──────────────┬─────────────────┬─────────────────┐
│     比較項目  │   WebSocket     │     MQTT        │
├──────────────┼─────────────────┼─────────────────┤
│ 通訊模式     │ 點對點          │ 發布/訂閱       │
│ 協定         │ TCP(ws://)    │ TCP(mqtt://)  │
│ 適用場景     │ 即時聊天、遊戲  │ IoT 感測器      │
│ 訊息大小     │ 較大            │ 極小(2 bytes) │
│ 連線品質     │ 需要穩定網路    │ 支援離線佇列    │
│ 複雜度       │ 較簡單          │ 需要 Broker     │
│ 瀏覽器支援   │ 原生支援        │ 需要轉接        │
│ 耗電量       │ 較高            │ 極低            │
└──────────────┴─────────────────┴─────────────────┘

用 Python 讀取感測器(搭配 .NET API)

Python 感測器腳本

# sensor_reader.py // Python 感測器讀取腳本
import json          # JSON 序列化 // 轉換資料格式
import time          # 時間模組 // 控制讀取間隔
import requests      # HTTP 請求 // 發送到 .NET API

API_URL = "http://localhost:5000/api/sensors"  # .NET API 網址 // 感測器端點

def read_and_send():  # 讀取並發送函式 // 主要邏輯
    # 模擬讀取感測器 // 實際需連接硬體
    data = {  # 感測器資料字典 // 包含溫濕度
        "temperature": 25.5,  # 溫度(攝氏) // 來自 DHT22
        "humidity": 65.0,     # 濕度(百分比) // 來自 DHT22
        "timestamp": time.time()  # 時間戳記 // Unix 時間
    }

    try:  # 嘗試發送資料 // 可能網路斷線
        response = requests.post(  # 發送 POST 請求 // 到 .NET API
            API_URL,  # API 網址 // 感測器端點
            json=data,  # 以 JSON 傳送 // 自動序列化
            timeout=5  # 超時 5 秒 // 避免卡住
        )
        print(f"已發送:{response.status_code}")  # 印出狀態碼 // 確認成功
    except Exception as e:  # 捕捉例外 // 網路錯誤
        print(f"發送失敗:{e}")  # 印出錯誤 // 方便除錯

while True:  # 無限迴圈 // 持續讀取
    read_and_send()  # 執行讀取並發送 // 主要函式
    time.sleep(5)  # 等待 5 秒 // 控制讀取頻率

.NET API 接收端

// 感測器資料模型 // 接收 Python 傳來的資料
public class SensorData // 感測器資料類別
{
    public double Temperature { get; set; } // 溫度
    public double Humidity { get; set; } // 濕度
    public double Timestamp { get; set; } // 時間戳記
}

// 感測器 API 控制器 // 接收並儲存感測器資料
[ApiController] // 標記為 API 控制器
[Route("api/[controller]")] // 路由設定
public class SensorsController : ControllerBase // 感測器控制器
{
    private readonly ILogger<SensorsController> _logger; // 日誌記錄器

    // 建構函式 // 注入日誌
    public SensorsController(ILogger<SensorsController> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 接收感測器資料 // POST api/sensors
    [HttpPost] // HTTP POST 方法
    public IActionResult PostSensorData( // 接收資料方法
        [FromBody] SensorData data) // 從請求本體讀取
    {
        _logger.LogInformation( // 記錄接收到的資料
            "收到感測器資料:溫度={Temp}°C, 濕度={Hum}%", // 格式化訊息
            data.Temperature, data.Humidity); // 傳入參數

        // TODO: 儲存到資料庫 // 之後實作
        return Ok(new { status = "received" }); // 回傳成功
    }
}

感測器資料視覺化

SignalR 即時推送

// 感測器 SignalR Hub // 即時推送感測器資料到前端
public class SensorHub : Hub // 繼承 SignalR Hub
{
    // 推送溫度更新 // 前端即時收到溫度
    public async Task SendTemperature( // 發送溫度方法
        double temperature) // 溫度數值
    {
        await Clients.All.SendAsync( // 發送給所有連線的客戶端
            "ReceiveTemperature", temperature); // 觸發前端事件
    }

    // 推送濕度更新 // 前端即時收到濕度
    public async Task SendHumidity( // 發送濕度方法
        double humidity) // 濕度數值
    {
        await Clients.All.SendAsync( // 發送給所有客戶端
            "ReceiveHumidity", humidity); // 觸發前端事件
    }
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤:GPIO 用完沒有釋放

// 錯誤寫法 // 沒有 Dispose 會導致針腳被鎖住
public void BlinkLed() // 閃爍 LED 方法
{
    var controller = new GpioController(); // 每次都建新的控制器
    controller.OpenPin(17, PinMode.Output); // 開啟 GPIO17
    controller.Write(17, PinValue.High); // 點亮 LED
    // 沒有 Close 也沒有 Dispose ← 針腳被鎖住! // 忘記釋放資源
}

✅ 正確:使用 using 確保釋放

// 正確寫法 // 使用 using 自動釋放
public void BlinkLed() // 閃爍 LED 方法
{
    using var controller = new GpioController(); // using 確保釋放
    controller.OpenPin(17, PinMode.Output); // 開啟 GPIO17
    controller.Write(17, PinValue.High); // 點亮 LED
    Thread.Sleep(1000); // 亮 1 秒
    controller.Write(17, PinValue.Low); // 關閉 LED
} // 離開 using 範圍自動 Dispose // 自動釋放資源

❌ 錯誤:MQTT 沒有處理斷線重連

// 錯誤寫法 // 斷線後就再也收不到訊息
public class BadMqttService // 錯誤的 MQTT 服務
{
    public void Connect() // 連線方法
    {
        // 連線一次就不管了 // 斷線就完蛋
        // 沒有重連邏輯 // 網路不穩就掛掉
    }
}

✅ 正確:加入自動重連機制

// 正確寫法 // 斷線自動重連
public class ResilientMqttService // 有韌性的 MQTT 服務
{
    private int _retryCount = 0; // 重試計數器
    private const int MAX_RETRY = 10; // 最大重試次數

    public async Task ConnectWithRetryAsync() // 帶重試的連線方法
    {
        while (_retryCount < MAX_RETRY) // 在重試上限內迴圈
        {
            try // 嘗試連線
            {
                await ConnectAsync(); // 執行連線
                _retryCount = 0; // 成功後重置計數器
                break; // 跳出迴圈
            }
            catch // 連線失敗
            {
                _retryCount++; // 增加重試計數
                var delay = Math.Min(1000 * _retryCount, 30000); // 指數退避延遲
                await Task.Delay(delay); // 等待後重試
            }
        }
    }

    private Task ConnectAsync() => Task.CompletedTask; // 實際連線邏輯
}

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊