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; // 實際連線邏輯
}