Arduino 與感測判斷:感測、決策、動作
這篇文章把「開發板負責量測與開關,電腦負責判斷」做成一個可以跟做的小專案。系統分成三層:感測層在 Arduino 讀溫濕度、距離和光線;決策層在電腦用 Python 判斷溫度是否超過 30°C;動作層再把結果送回開發板,開關 LED 與繼電器。
這裡的「判斷」先用清楚的門檻,而不是訓練好的大模型。好處是每一步都看得到:哪一筆資料送出來、為什麼觸發、哪一條指令送回去。後段會說明若以後有紀錄資料,要怎麼把同一條決策函式換成小分類器。
這次測試到哪裡
本篇的 Uno 韌體已用 arduino-cli、板型 arduino:avr:uno 編譯通過(約佔程式空間 25%、動態記憶體 12%)。三層流程則在沒有實體開發板的環境裡,用 Linux PTY 虛擬序列埠加「假裝置」測完:假裝置依照與韌體相同的 DATA/ALERT 文字協定送出溫度,Python 決策層判斷後送回指令,模擬的 LED 與繼電器狀態會跟著改變。低於 30°C、剛好等於 30°C、超過 30°C,以及溫度讀取失敗(nan),都有測到預期結果。
以下項目還沒有在實體硬體上驗證,不能當成硬體測試通過:實際接線、DHT11/DHT22、HC-SR04、光敏電阻的真實讀值、Arduino 透過 USB 序列埠的重設與穩定度、LED 實際亮度、繼電器觸發極性,以及繼電器接上風扇或馬達等負載。ESP32 的 Wi-Fi 路徑只測了電腦端的 TCP 協定;這份 ESP32 韌體在撰文環境中尚未編譯,也未實機測試。
需要的材料
- Arduino Uno,或相容的 ATmega328P 開發板(5V 邏輯)
- USB 線,以及已安裝驅動的電腦(Windows、macOS 或 Linux)
- DHT22 溫濕度模組(也可用 DHT11,程式只要改
DHT_TYPE) - HC-SR04 超音波距離模組
- 光敏電阻、10kΩ 電阻
- LED、220Ω 電阻
- 5V 繼電器模組(本篇預設低電平觸發;若你的模組相反,改程式裡的
RELAY_ACTIVE_LOW) - 麵包板與杜邦線
若要用繼電器帶動風扇或馬達,負載必須走繼電器的 COM/NO 與外接電源,不可直接接在 Arduino 的腳位上。這部分需要實體配線,本篇沒有實機驗證。
三層怎麼分工
| 層 | 跑在哪裡 | 做什麼 |
|---|---|---|
| 感測層 | Arduino | 每秒讀一次溫濕度、距離、光線,經序列埠送出一行 DATA,... |
| 決策層 | 電腦上的 Python | 解析該行資料。溫度超過 30°C(嚴格大於)就送回 ALERT,1,1,否則送 ALERT,0,0 |
| 動作層 | 同一份 Arduino 程式 | 收到指令後開關 D5 的 LED 與 D6 的繼電器 |
預設走 USB 序列埠(9600 8N1)。若以後改用 ESP32,協定不變,只是改走區域網路 TCP,見後段選用路徑。
接線與腳位
下列腳位與程式常數一致,請從頭到尾使用同一套接法。
| 元件 | 元件腳位 | Arduino 腳位 | 說明 |
|---|---|---|---|
| DHT22 | VCC | 5V | 改用 DHT11 時接線相同 |
| DHT22 | GND | GND | |
| DHT22 | DATA | D2 | 若模組沒有內建上拉,DATA 與 5V 之間加 10kΩ |
| HC-SR04 | VCC | 5V | |
| HC-SR04 | GND | GND | |
| HC-SR04 | TRIG | D8 | |
| HC-SR04 | ECHO | D9 | Uno 為 5V 邏輯,可直接接。3.3V 開發板需分壓,見 ESP32 段 |
| 光敏電阻分壓 | 光敏電阻一端 | 5V | 5V → 光敏電阻 → A0,A0 → 10kΩ → GND |
| 光敏電阻分壓 | 分壓中點 | A0 | Uno 讀值範圍 0–1023,依此接線通常愈亮愈大 |
| 光敏電阻分壓 | 10kΩ 另一端 | GND | |
| LED | 陽極(經 220Ω) | D5 | D5 → 220Ω → LED 陽極,陰極接 GND |
| LED | 陰極 | GND | |
| 繼電器模組 | VCC | 5V | 請確認模組適用 5V |
| 繼電器模組 | GND | GND | |
| 繼電器模組 | IN | D6 | 程式預設低電平觸發 |
| 繼電器負載 | COM/NO | 外接電源與負載 | 風扇或馬達不可直接接 Arduino 腳位 |
接線速記
5V ──┬── DHT22 VCC
├── HC-SR04 VCC
├── 繼電器 VCC
└── 光敏電阻一端
GND ──┬── DHT22 GND
├── HC-SR04 GND
├── 繼電器 GND
├── LED 陰極
└── 10kΩ 一端
D2 ──── DHT22 DATA(必要時 10kΩ 上拉至 5V)
D5 ──── 220Ω ──── LED 陽極
D6 ──── 繼電器 IN
D8 ──── HC-SR04 TRIG
D9 ──── HC-SR04 ECHO
A0 ──── 光敏電阻與 10kΩ 的交點感測層:把讀值送出序列埠
在 Arduino IDE 或 arduino-cli 安裝「DHT sensor library」(Adafruit)以及「Adafruit Unified Sensor」。把下面這份程式上傳到 Uno。開機後它會先關閉 LED 與繼電器,等待約兩秒讓 DHT22 穩定,然後每秒送出一行資料。
協定很短,方便用序列埠監控視窗或 Python 一起看:
- 裝置送到電腦:
DATA,<溫度>,<濕度>,<距離cm>,<光線>,例如DATA,28.50,61.20,45.3,512 - 某個感測器失敗時,該欄位寫
nan,例如DATA,nan,nan,45.3,512 - 光線在 Uno 上是
analogRead的 0–1023,沒有單位,需靠實機校正
/*
環境感測節點(感測層 + 動作層)
開發板:Arduino Uno(ATmega328P 相容板)
函式庫:Adafruit DHT sensor library(請一併安裝 Adafruit Unified Sensor)
腳位(請與接線表保持一致):
DHT22 DATA -> D2
HC-SR04 TRIG -> D8
HC-SR04 ECHO -> D9
光敏電阻分壓中點 -> A0
LED(經 220Ω) -> D5
繼電器模組 IN -> D6(預設低電平觸發)
序列埠協定(9600 8N1,每筆一行,以換行結尾)
裝置 → 電腦:DATA,<temp>,<humidity>,<distance_cm>,<light>
例:DATA,28.50,61.20,45.3,512
感測失敗時該欄位為 nan(例如 DATA,nan,nan,45.3,512)
電腦 → 裝置:ALERT,<led>,<relay>
例:ALERT,1,1
led、relay 為 0 或 1
*/
#include <DHT.h>
const uint8_t DHT_PIN = 2;
const uint8_t DHT_TYPE = DHT22;
const uint8_t TRIG_PIN = 8;
const uint8_t ECHO_PIN = 9;
const uint8_t LDR_PIN = A0;
const uint8_t LED_PIN = 5;
const uint8_t RELAY_PIN = 6;
// 多數 5V 繼電器模組為低電平觸發:IN 接到 LOW 時接通。
const bool RELAY_ACTIVE_LOW = true;
const unsigned long SAMPLE_INTERVAL_MS = 1000;
const unsigned long ECHO_TIMEOUT_US = 30000; // 約 5 公尺上限
DHT dht(DHT_PIN, DHT_TYPE);
bool ledOn = false;
bool relayOn = false;
unsigned long lastSampleMs = 0;
void applyOutputs() {
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
if (RELAY_ACTIVE_LOW) {
digitalWrite(RELAY_PIN, relayOn ? LOW : HIGH);
} else {
digitalWrite(RELAY_PIN, relayOn ? HIGH : LOW);
}
}
void setup() {
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(TRIG_PIN, LOW);
ledOn = false;
relayOn = false;
applyOutputs();
Serial.begin(9600);
dht.begin();
delay(2000); // DHT22 上電穩定期
}
float readDistanceCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long duration = pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT_US);
if (duration == 0) {
return NAN;
}
// 聲速約 343 m/s:距離(cm) = duration(us) * 0.0343 / 2
return (duration * 0.0343f) / 2.0f;
}
void handleCommand(const String& line) {
// 期望格式:ALERT,1,0
if (!line.startsWith("ALERT")) {
return;
}
int firstComma = line.indexOf(',');
int secondComma = line.indexOf(',', firstComma + 1);
if (firstComma < 0 || secondComma < 0) {
return;
}
int ledVal = line.substring(firstComma + 1, secondComma).toInt();
int relayVal = line.substring(secondComma + 1).toInt();
ledOn = ledVal != 0;
relayOn = relayVal != 0;
applyOutputs();
}
void loop() {
while (Serial.available() > 0) {
String line = Serial.readStringUntil('\n');
line.trim();
if (line.length() > 0) {
handleCommand(line);
}
}
unsigned long now = millis();
if (now - lastSampleMs < SAMPLE_INTERVAL_MS) {
return;
}
lastSampleMs = now;
float temperature = dht.readTemperature(); // 攝氏
float humidity = dht.readHumidity();
float distanceCm = readDistanceCm();
int light = analogRead(LDR_PIN); // 0–1023,依本篇接線,數值愈大通常愈亮
Serial.print("DATA,");
if (isnan(temperature)) {
Serial.print("nan");
} else {
Serial.print(temperature, 2);
}
Serial.print(",");
if (isnan(humidity)) {
Serial.print("nan");
} else {
Serial.print(humidity, 2);
}
Serial.print(",");
if (isnan(distanceCm)) {
Serial.print("nan");
} else {
Serial.print(distanceCm, 1);
}
Serial.print(",");
Serial.println(light);
}決策層:Python 判斷門檻
電腦這端只做一件事:讀一行 DATA,看溫度是否超過門檻(預設 30.0°C,採嚴格大於,剛好 30.00 不觸發),再寫回 ALERT,led,relay。溫度若是 nan,會先把輸出關掉。指令沒有改變時不會重複送,以免一直重設輸出。
先安裝依賴,再指定序列埠執行:
安裝與執行
python3 -m venv .venv
source .venv/bin/activate # Windows 可用 .venv\Scripts\activate
pip install -r requirements.txt
python decision.py --port /dev/ttyACM0 --threshold 30序列埠名稱因系統而異:Linux 常見 /dev/ttyACM0 或 /dev/ttyUSB0,macOS 常見 /dev/tty.usbmodem14101,Windows 則是 COM3 這類編號。若不加 --port,程式會嘗試挑一個比較像 Arduino 的埠;不確定時請自己指定。
#!/usr/bin/env python3
"""決策層:從序列埠讀取感測資料,依溫度門檻送回動作指令。
依賴:pip install pyserial
預設規則:溫度「超過」門檻(預設 30.0°C,採嚴格大於)時,
同時開啟 LED 與繼電器;溫度讀取失敗時關閉輸出。
"""
from __future__ import annotations
import argparse
import socket
import sys
import time
from typing import Optional
import serial
import serial.tools.list_ports
BAUD = 9600
TEMP_THRESHOLD_C = 30.0
Reading = dict[str, Optional[float] | Optional[int]]
def pick_port(explicit: Optional[str]) -> str:
if explicit:
return explicit
ports = list(serial.tools.list_ports.comports())
if not ports:
sys.exit("找不到序列埠。請用 --port 指定,例如 COM3 或 /dev/ttyACM0。")
keywords = (
"arduino",
"usb serial",
"ch340",
"cp210",
"wch",
"silicon labs",
"usbmodem",
"ttyacm",
"ttyusb",
)
for item in ports:
blob = f"{item.device} {item.description} {item.manufacturer or ''}".lower()
if any(key in blob for key in keywords):
return item.device
return ports[0].device
def parse_data(line: str) -> Optional[dict]:
"""解析 DATA,temp,humidity,distance_cm,light。失敗回傳 None。"""
parts = line.strip().split(",")
if len(parts) != 5 or parts[0] != "DATA":
return None
def to_float(value: str) -> Optional[float]:
if value.lower() == "nan":
return None
try:
return float(value)
except ValueError:
return None
def to_int(value: str) -> Optional[int]:
try:
return int(value)
except ValueError:
return None
return {
"temp": to_float(parts[1]),
"humidity": to_float(parts[2]),
"distance_cm": to_float(parts[3]),
"light": to_int(parts[4]),
}
def decide(reading: dict, temp_threshold: float) -> tuple[int, int, str]:
"""回傳 (led, relay, reason)。
溫度超過門檻時兩者同時開啟;剛好等於門檻不觸發。
溫度讀取失敗時關閉輸出。
"""
temp = reading["temp"]
if temp is None:
return 0, 0, "溫度讀取失敗,先關閉輸出"
if temp > temp_threshold:
return (
1,
1,
f"溫度 {temp:.2f}°C 超過門檻 {temp_threshold:.1f}°C,觸發警報",
)
return 0, 0, f"溫度 {temp:.2f}°C 未超過門檻,維持關閉"
def format_command(led: int, relay: int) -> str:
return f"ALERT,{led},{relay}"
def process_line(line: str, temp_threshold: float) -> Optional[tuple[dict, str, str]]:
"""處理一列輸入。成功時回傳 (reading, command, reason)。"""
reading = parse_data(line)
if reading is None:
return None
led, relay, reason = decide(reading, temp_threshold)
return reading, format_command(led, relay), reason
def run_serial(port: str, temp_threshold: float) -> None:
print(f"開啟序列埠 {port}({BAUD} bps),門檻 {temp_threshold:.1f}°C")
with serial.Serial(port, BAUD, timeout=1) as ser:
time.sleep(2.5) # 許多 Arduino 在開啟序列埠時會重設
ser.reset_input_buffer()
last_cmd: Optional[str] = None
while True:
raw = ser.readline()
if not raw:
continue
line = raw.decode("utf-8", errors="ignore").strip()
result = process_line(line, temp_threshold)
if result is None:
if line:
print(f"略過無法解析的列:{line}")
continue
reading, cmd, reason = result
print(
f"T={reading['temp']} H={reading['humidity']} "
f"D={reading['distance_cm']} L={reading['light']} → {cmd}({reason})"
)
if cmd != last_cmd:
ser.write((cmd + "\n").encode("ascii"))
last_cmd = cmd
def run_tcp_server(host: str, port: int, temp_threshold: float) -> None:
"""給 ESP32 選用路徑用的 TCP 決策伺服器,協定與序列埠版相同。"""
print(f"在 {host}:{port} 等待 TCP 連線,門檻 {temp_threshold:.1f}°C")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(1)
while True:
conn, addr = server.accept()
print(f"已連線:{addr[0]}:{addr[1]}")
last_cmd: Optional[str] = None
with conn:
buffer = b""
while True:
chunk = conn.recv(1024)
if not chunk:
print("對端關閉連線")
break
buffer += chunk
while b"\n" in buffer:
raw, buffer = buffer.split(b"\n", 1)
line = raw.decode("utf-8", errors="ignore").strip()
result = process_line(line, temp_threshold)
if result is None:
if line:
print(f"略過無法解析的列:{line}")
continue
reading, cmd, reason = result
print(
f"T={reading['temp']} H={reading['humidity']} "
f"D={reading['distance_cm']} L={reading['light']} "
f"→ {cmd}({reason})"
)
if cmd != last_cmd:
conn.sendall((cmd + "\n").encode("ascii"))
last_cmd = cmd
def main() -> None:
parser = argparse.ArgumentParser(description="Arduino / ESP32 感測資料決策層")
parser.add_argument(
"--port",
help="序列埠,例如 COM3、/dev/ttyACM0、/dev/tty.usbmodem14101",
)
parser.add_argument(
"--threshold",
type=float,
default=TEMP_THRESHOLD_C,
help="溫度門檻(攝氏),預設 30",
)
parser.add_argument(
"--tcp",
action="store_true",
help="改開 TCP 伺服器(ESP32 Wi-Fi 路徑)",
)
parser.add_argument("--bind", default="0.0.0.0", help="TCP 綁定位址")
parser.add_argument("--tcp-port", type=int, default=5000, help="TCP 連接埠")
args = parser.parse_args()
if args.tcp:
run_tcp_server(args.bind, args.tcp_port, args.threshold)
return
port = pick_port(args.port)
run_serial(port, args.threshold)
if __name__ == "__main__":
main()pyserial>=3.5動作層:依指令開關輸出
動作層寫在同一份 environment_node.ino 裡,不必再上傳另一個程式。開發板收到 ALERT,1,1 會點亮 D5 的 LED,並把 D6 的繼電器接通;收到 ALERT,0,0 則兩者關閉。
多數 5V 繼電器模組是低電平觸發,所以程式預設 RELAY_ACTIVE_LOW = true:要「接通」時,IN 腳反而輸出 LOW。若你接上之後動作相反,把這個常數改成 false 再上傳。繼電器有沒有真的吸合、接負載後能不能帶動風扇或馬達,都需要實體測試。
在瀏覽器裡先看門檻怎麼作用
若還沒接板,可以先用下面的拉桿看決策規則:溫度超過門檻時,畫面上的 LED 與繼電器會開啟,並顯示對應的 DATA/ALERT 列。這只是瀏覽器模擬,不是感測器讀值。
決策層模擬
拉桿調整溫度,看會不會觸發警報
溫度 28.00°C 未超過門檻,維持關閉。
DATA,28.00,61.20,45.3,512 ALERT,0,0
這是瀏覽器裡的決策模擬,濕度、距離與光線用固定示例值,只演示溫度門檻。它不是感測器的真實讀值,也不能代替實體接線測試。
從頭跑一次
- 依腳位表接線,檢查 5V/GND 沒有接反。馬達或風扇不要直接接腳位。
- 安裝 DHT 函式庫,上傳
environment_node.ino。 - 打開序列埠監控視窗,設定 9600。應看到每秒一行
DATA,...。若溫度一直是nan,先查 DHT 接線與上拉電阻。 - 關掉監控視窗(同一序列埠一次只能被一個程式占用),執行
python decision.py --port 你的埠。 - 用暖風或手握住 DHT 讓溫度超過 30°C,觀察終端機是否印出觸發原因,LED 是否亮起、繼電器是否吸合;離開熱源後應再關閉。
最後一步必須在實體硬體上做。撰文時只驗證了編譯,以及虛擬序列埠上的協定與狀態切換。
選用:改走 ESP32 與 Wi-Fi
若要把電腦和開發板分開,可改用 ESP32,仍使用同一套 DATA/ALERT 文字協定,只是改走 TCP。電腦執行 python decision.py --tcp --tcp-port 5000,開發板連到電腦在區域網路中的 IP。
ESP32 是 3.3V 邏輯,腳位也與 Uno 不同,請改用下表,不要把兩套腳位混在一起。
| 元件 | ESP32 腳位 | 注意 |
|---|---|---|
| DHT22 DATA | GPIO4 | 模組電源請依規格接 3.3V 或 5V |
| HC-SR04 TRIG | GPIO18 | |
| HC-SR04 ECHO | GPIO19 | ECHO 常為 5V,請先分壓再進腳位,例如 ECHO → 1kΩ → GPIO19 → 2kΩ → GND |
| 光敏電阻中點 | GPIO34 | 僅能輸入;ADC 預設 12 位元,光線欄位範圍是 0–4095,不是 Uno 的 0–1023 |
| LED | GPIO25 | 同樣經 220Ω;LED 電源不要超過 3.3V 邏輯可承受的範圍 |
| 繼電器 IN | GPIO26 | 模組 VCC 仍依規格接 5V 或 3.3V |
使用前把程式裡的 YOUR_SSID、YOUR_PASSWORD 和 SERVER_HOST 改成你的網路與電腦 IP。這份韌體在撰文環境中沒有編譯;決策層的 TCP 收送已在本機迴圈測試過,Wi-Fi 連線、分壓電路與實機讀值都還沒測。
/*
環境感測節點(ESP32 選用路徑:同一套 DATA/ALERT 協定,改走 Wi-Fi TCP)
開發板:ESP32 Dev Module
函式庫:Adafruit DHT sensor library、Arduino ESP32 內建 WiFi.h
腳位(與 Uno 版不同,請勿混用):
DHT22 DATA -> GPIO4
HC-SR04 TRIG -> GPIO18
HC-SR04 ECHO -> GPIO19(HC-SR04 的 ECHO 為 5V,請經分壓再進 ESP32)
光敏電阻分壓中點 -> GPIO34(僅輸入、可 ADC)
LED(經 220Ω,LED 另接 GND) -> GPIO25
繼電器模組 IN -> GPIO26(預設低電平觸發;模組 VCC 請依規格接 5V 或 3.3V)
協定與 Uno 版相同,只是改走 TCP:
裝置 → 電腦:DATA,<temp>,<humidity>,<distance_cm>,<light>
電腦 → 裝置:ALERT,<led>,<relay>
注意:ESP32 ADC 預設為 12 位元,光線欄位範圍是 0–4095,不是 Uno 的 0–1023。
*/
#include <WiFi.h>
#include <DHT.h>
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";
const char* SERVER_HOST = "192.168.50.10"; // 改成電腦在區域網路的 IP
const uint16_t SERVER_PORT = 5000;
const uint8_t DHT_PIN = 4;
const uint8_t DHT_TYPE = DHT22;
const uint8_t TRIG_PIN = 18;
const uint8_t ECHO_PIN = 19;
const uint8_t LDR_PIN = 34;
const uint8_t LED_PIN = 25;
const uint8_t RELAY_PIN = 26;
const bool RELAY_ACTIVE_LOW = true;
const unsigned long SAMPLE_INTERVAL_MS = 1000;
const unsigned long ECHO_TIMEOUT_US = 30000;
const unsigned long RECONNECT_INTERVAL_MS = 3000;
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient client;
bool ledOn = false;
bool relayOn = false;
unsigned long lastSampleMs = 0;
unsigned long lastReconnectMs = 0;
void applyOutputs() {
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
if (RELAY_ACTIVE_LOW) {
digitalWrite(RELAY_PIN, relayOn ? LOW : HIGH);
} else {
digitalWrite(RELAY_PIN, relayOn ? HIGH : LOW);
}
}
void handleCommand(const String& line) {
if (!line.startsWith("ALERT")) {
return;
}
int firstComma = line.indexOf(',');
int secondComma = line.indexOf(',', firstComma + 1);
if (firstComma < 0 || secondComma < 0) {
return;
}
int ledVal = line.substring(firstComma + 1, secondComma).toInt();
int relayVal = line.substring(secondComma + 1).toInt();
ledOn = ledVal != 0;
relayOn = relayVal != 0;
applyOutputs();
}
void ensureConnected() {
if (client.connected()) {
return;
}
unsigned long now = millis();
if (now - lastReconnectMs < RECONNECT_INTERVAL_MS) {
return;
}
lastReconnectMs = now;
client.connect(SERVER_HOST, SERVER_PORT);
}
void setup() {
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(TRIG_PIN, LOW);
ledOn = false;
relayOn = false;
applyOutputs();
Serial.begin(115200);
dht.begin();
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
delay(200);
}
delay(2000);
}
float readDistanceCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long duration = pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT_US);
if (duration == 0) {
return NAN;
}
return (duration * 0.0343f) / 2.0f;
}
void loop() {
ensureConnected();
while (client.connected() && client.available() > 0) {
String line = client.readStringUntil('\n');
line.trim();
if (line.length() > 0) {
handleCommand(line);
}
}
unsigned long now = millis();
if (now - lastSampleMs < SAMPLE_INTERVAL_MS) {
return;
}
lastSampleMs = now;
if (!client.connected()) {
return;
}
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
float distanceCm = readDistanceCm();
int light = analogRead(LDR_PIN); // 0–4095
String line = "DATA,";
if (isnan(temperature)) {
line += "nan";
} else {
line += String(temperature, 2);
}
line += ",";
if (isnan(humidity)) {
line += "nan";
} else {
line += String(humidity, 2);
}
line += ",";
if (isnan(distanceCm)) {
line += "nan";
} else {
line += String(distanceCm, 1);
}
line += ",";
line += String(light);
client.println(line);
Serial.println(line); // 本機除錯用
}選用:把門檻換成小模型
預設的 decide() 已經把「輸入讀值、輸出 led/relay」隔開。若你之後蒐集了實機紀錄,可以在同一個位置改呼叫小分類器,例如 scikit-learn 的決策樹,不必改 Arduino 協定。沒有自己的資料以前,不建議先導入別人訓練好的權重;門檻規則本身就可以完整跑完這個小專案。
把 decide() 換成已訓練模型的示意(需自行準備資料,本篇未訓練、也未執行)
import joblib
model = joblib.load("alert_model.joblib")
def decide(reading, temp_threshold):
temp = reading["temp"]
if temp is None:
return 0, 0, "溫度讀取失敗,先關閉輸出"
features = [[
temp,
reading["humidity"] or 0.0,
reading["distance_cm"] or 0.0,
reading["light"] or 0,
]]
label = int(model.predict(features)[0])
reason = "小模型判定觸發" if label == 1 else "小模型判定關閉"
return label, label, reason常見問題
序列埠一打開,Arduino 就重啟?
許多 Uno 在電腦開啟序列埠時會自動重設。decision.py 因此會先等 2.5 秒再開始讀資料。請先關上 Arduino IDE 的監控視窗,再執行 Python。
溫度一直是 nan?
查 DHT 的 VCC/GND/DATA、上拉電阻,以及 DHT_TYPE 是否與實際模組相符。DHT22 上電後也需要一兩秒才穩定。
距離一直是 nan?
查 TRIG/ECHO 是否接反,以及物體是否超出模組範圍。若改用 ESP32 卻沒做 ECHO 分壓,腳位可能無法正確讀到高電平,甚至有損壞風險。
繼電器的動作反了?
把 RELAY_ACTIVE_LOW 改成相反值後重新上傳。先不要接高壓負載,用模組上的指示燈確認極性。
Linux 提示沒有權限開序列埠?
把使用者加入 dialout 群組後重新登入,或暫時確認裝置節點的權限。這屬於實機環境設定,撰文時未在實體 USB 裝置上操作。
下載
下列檔案與文中程式碼相同,可直接下載後編譯或執行。