Line 發報機
Python TKinter (CONFIG_LINE Message API_2.py)
import serial
import serial.tools.list_ports
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import threading
import time
# --- 預設值 ---
# 請在這裡填入您最新有效的 Line Token 和 User ID
LINE_CHANNEL_ACCESS_TOKEN = "請在此填入您的 Line Channel Access Token" # 這裡填入長效 Token
LINE_TARGET_ID = "Uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 這裡填入您的 Line User ID (U...) 或 Group ID (C/R...)
WIFI_SSID = "ASUS_D0"
WIFI_PASSWORD = "staff@54569"
DEFAULT_ALARM_MESSAGES = [f"警報觸發測試第0{i}組" for i in range(1, 9)]
NUM_INPUTS = 8
BAUD_RATE = 115200
# --- 序列埠通訊狀態 ---
ser = None
stop_thread = threading.Event()
def get_ports():
"""獲取所有可用的序列埠"""
return [port.device for port in serial.tools.list_ports.comports()]
def connect_serial():
"""連接序列埠並啟動讀取執行緒"""
global ser
port_name = combo_port.get()
if ser and ser.is_open:
disconnect_serial()
try:
ser = serial.Serial(port_name, BAUD_RATE, timeout=0.1)
btn_connect.config(text="斷開", command=disconnect_serial)
log_message(f"已成功連接到 {port_name}")
stop_thread.clear()
threading.Thread(target=read_serial, daemon=True).start()
# 連接成功後立即嘗試讀取配置
load_config()
except Exception as e:
messagebox.showerror("連接錯誤", f"無法連接到序列埠 {port_name}: {e}")
def disconnect_serial():
"""斷開序列埠連線"""
global ser
if ser and ser.is_open:
stop_thread.set()
time.sleep(0.5)
ser.close()
btn_connect.config(text="連接", command=connect_serial)
log_message("已斷開序列埠連接")
def log_message(msg):
"""在日誌文本框中顯示訊息"""
log_text.insert(tk.END, f"[{time.strftime('%H:%M:%S')}] {msg}\n")
log_text.see(tk.END) # 滾動到底部
def read_serial():
"""在單獨執行緒中讀取序列埠資料"""
global ser
if not ser: return
# 讀取緩衝區,用於處理多行輸出
buffer = ""
log_message("開始讀取 ESP32 數據...")
while not stop_thread.is_set():
try:
if ser.in_waiting > 0:
# 使用 readline 讀取一行,並解碼
line = ser.readline().decode('utf-8', errors='ignore').strip()
if line:
log_message(f"Received: {line}")
buffer += line + "\n"
# 檢查是否接收到完整的載入響應
if line == "100%" and "LOAD_RESP:" in buffer:
parse_load_response(buffer)
buffer = "" # 清空緩衝區
time.sleep(0.01)
except serial.SerialException:
# 序列埠斷開
root.after(0, lambda: messagebox.showerror("錯誤", "序列埠連線中斷"))
root.after(0, disconnect_serial)
break
except Exception as e:
log_message(f"讀取錯誤: {e}")
time.sleep(0.1)
def parse_load_response(response):
"""解析從 ESP32 載入的配置數據並更新 GUI"""
start_index = response.find("LOAD_RESP:")
if start_index == -1:
return
data_str = response[start_index + len("LOAD_RESP:"):].split('\n')[0].strip()
# 分割數據
parts = data_str.split('|')
if len(parts) >= 6 + NUM_INPUTS:
# 1. Token
token_entry.delete(0, tk.END)
token_entry.insert(0, parts[0])
# 2. Target ID
target_id_entry.delete(0, tk.END)
target_id_entry.insert(0, parts[1])
# 3. SSID
ssid_entry.delete(0, tk.END)
ssid_entry.insert(0, parts[2])
# 4. Password
password_entry.delete(0, tk.END)
password_entry.insert(0, parts[3])
# 5. Flags
release_flag.set(int(parts[4]))
persist_flag.set(int(parts[5]))
# 6. Messages
for i in range(NUM_INPUTS):
msg_entries[i].delete(0, tk.END)
msg_entries[i].insert(0, parts[6 + i])
log_message("成功載入配置數據並更新 GUI。")
else:
log_message(f"載入數據格式錯誤或數據不完整。Parts len: {len(parts)}")
def save_config():
"""收集 GUI 數據並發送 SAV_CFG 命令到 ESP32"""
if not ser or not ser.is_open:
messagebox.showerror("錯誤", "請先連接到序列埠")
return
# 收集數據
token = token_entry.get().strip()
target_id = target_id_entry.get().strip()
ssid = ssid_entry.get().strip()
password = password_entry.get().strip()
flag_r = "1" if release_flag.get() else "0"
flag_p = "1" if persist_flag.get() else "0"
messages = [e.get().strip() for e in msg_entries]
# 組裝 Payload (使用 | 分隔符)
payload = "|".join([token, target_id, ssid, password, flag_r, flag_p] + messages)
command = f"SAVE_CFG:{payload}"
# 發送命令
try:
log_message(f"Sent: {command[:100]}...") # 只顯示部分命令避免暴露 Token
ser.write(command.encode('utf-8') + b'\n')
# 設定逾時,等待 ESP32 重啟
root.after(5000, check_save_status)
log_message("等待 ESP32 響應和重啟...")
except Exception as e:
messagebox.showerror("寫入錯誤", f"發送命令失敗: {e}")
def check_save_status():
"""簡單檢查,如果 5 秒後仍無響應,則認為寫入超時"""
# 由於 ESP32 寫入後會重啟,最好的檢查就是看它是否成功重啟
if btn_connect['text'] == "斷開" and ser and ser.is_open:
log_message("警告: 寫入命令可能超時。請檢查序列埠是否有 'Config data saved. Rebooting...' 訊息。")
# messagebox.showerror("儲存失敗", "寫入命令超時,請檢查序列埠連接和 ESP32 是否正在運行。")
def load_config():
"""發送 LOAD_CFG 命令到 ESP32"""
if not ser or not ser.is_open:
messagebox.showerror("錯誤", "請先連接到序列埠")
return
command = "LOAD_CFG\n"
try:
log_message("Sent: LOAD_CFG")
ser.write(command.encode('utf-8'))
except Exception as e:
messagebox.showerror("讀取錯誤", f"發送讀取命令失敗: {e}")
# --- GUI 介面設定 ---
root = tk.Tk()
root.title("ESP32 Line/Telegram 告警機配置工具")
root.geometry("800x750")
# 框架設置
frame_main = ttk.Frame(root, padding="10")
frame_main.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
# 序列埠控制
frame_serial = ttk.LabelFrame(frame_main, text="序列埠控制", padding="10")
frame_serial.grid(row=0, column=0, columnspan=2, sticky=tk.W + tk.E, pady=5)
ports = get_ports()
combo_port = ttk.Combobox(frame_serial, values=ports, width=15)
combo_port.grid(row=0, column=0, padx=5, pady=5)
if ports:
combo_port.set(ports[0])
btn_connect = ttk.Button(frame_serial, text="連接", command=connect_serial)
btn_connect.grid(row=0, column=1, padx=5, pady=5)
btn_save = ttk.Button(frame_serial, text="儲存", command=save_config)
btn_save.grid(row=0, column=2, padx=5, pady=5)
btn_load = ttk.Button(frame_serial, text="讀取", command=load_config)
btn_load.grid(row=0, column=3, padx=5, pady=5)
# 左右佈局
frame_left = ttk.Frame(frame_main, padding="5")
frame_left.grid(row=1, column=0, sticky=(tk.W, tk.N), padx=5)
frame_right = ttk.Frame(frame_main, padding="5")
frame_right.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=5)
# --- 左側:配置輸入 ---
# Line Configuration
frame_line = ttk.LabelFrame(frame_left, text="Line Messaging API 配置", padding="10")
frame_line.grid(row=0, column=0, sticky=tk.W + tk.E, pady=10)
ttk.Label(frame_line, text="Channel Access Token (長 Token):").grid(row=0, column=0, sticky=tk.W, pady=2)
token_entry = ttk.Entry(frame_line, width=50)
token_entry.grid(row=1, column=0, sticky=tk.W, padx=5, pady=2)
token_entry.insert(0, LINE_CHANNEL_ACCESS_TOKEN)
ttk.Label(frame_line, text="Target User/Group ID (U.../C...):").grid(row=2, column=0, sticky=tk.W, pady=2)
target_id_entry = ttk.Entry(frame_line, width=50)
target_id_entry.grid(row=3, column=0, sticky=tk.W, padx=5, pady=2)
target_id_entry.insert(0, LINE_TARGET_ID)
# 警報訊息配置
frame_messages = ttk.LabelFrame(frame_left, text="警報訊息內容 (8 組)", padding="10")
frame_messages.grid(row=1, column=0, sticky=tk.W + tk.E, pady=10)
msg_entries = []
for i in range(NUM_INPUTS):
ttk.Label(frame_messages, text=f"Port {i+1} 訊息:").grid(row=i, column=0, sticky=tk.W, padx=5, pady=2)
entry = ttk.Entry(frame_messages, width=35)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=2)
entry.insert(0, DEFAULT_ALARM_MESSAGES[i])
msg_entries.append(entry)
# Wi-Fi 和旗標
frame_settings = ttk.LabelFrame(frame_left, text="網路與發送設置", padding="10")
frame_settings.grid(row=2, column=0, sticky=tk.W + tk.E, pady=10)
ttk.Label(frame_settings, text="Wi-Fi SSID:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=2)
ssid_entry = ttk.Entry(frame_settings, width=20)
ssid_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
ssid_entry.insert(0, WIFI_SSID)
ttk.Label(frame_settings, text="Wi-Fi Password:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=2)
password_entry = ttk.Entry(frame_settings, width=20, show="*")
password_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
password_entry.insert(0, WIFI_PASSWORD)
release_flag = tk.IntVar()
ttk.Checkbutton(frame_settings, text="警報解除是否發送", variable=release_flag).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=5)
persist_flag = tk.IntVar()
ttk.Checkbutton(frame_settings, text="警報持續是否發送", variable=persist_flag).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=5)
# --- 右側:日誌輸出 ---
frame_log = ttk.LabelFrame(frame_right, text="序列埠日誌輸出", padding="10")
frame_log.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
frame_right.columnconfigure(0, weight=1)
frame_right.rowconfigure(0, weight=1)
log_text = scrolledtext.ScrolledText(frame_log, width=70, height=40, wrap=tk.WORD)
log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 關閉視窗時執行斷開序列埠
def on_closing():
disconnect_serial()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
Arduino ESP32程式 (8Port_IN_2.ino)
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <Preferences.h>
#include <ArduinoJson.h>
// ----------------------------------------------------
// 【配置參數與宏定義】
// ----------------------------------------------------
#define LINE_TOKEN_LEN 200 // 增大到 200 以容納長 Token (172個字元)
#define LINE_TARGET_ID_LEN 40
#define SSID_LEN 32
#define PASS_LEN 32
#define ALARM_MSG_LEN 40
#define NUM_INPUTS 8
const char* PREFS_NAME = "alarm_config";
// Line Messaging API 相關
const char* LINE_HOST = "api.line.me";
const int LINE_PORT = 443;
const char* LINE_PUSH_PATH = "/v2/bot/message/push";
const unsigned long PERSIST_INTERVAL = 60000; // 警報持續發送間隔 (60 秒)
// ----------------------------------------------------
// 【狀態/配置變數】
// ----------------------------------------------------
Preferences preferences;
char lineAccessToken[LINE_TOKEN_LEN];
char lineTargetID[LINE_TARGET_ID_LEN];
char wifiSSID[SSID_LEN];
char wifiPassword[PASS_LEN];
bool sendOnRelease = false;
bool sendOnPersist = false;
char alarmMessages[NUM_INPUTS][ALARM_MSG_LEN];
bool serialConfigMode = false;
// --- 硬體定義 ---
struct InputPin {
int id;
int pin;
};
InputPin inputMap[NUM_INPUTS] = {
{1, 13}, {2, 12}, {3, 14}, {4, 27},
{5, 16}, {6, 17}, {7, 25}, {8, 26}
};
int lastPinState[NUM_INPUTS];
unsigned long lastTriggerTime[NUM_INPUTS] = {0};
// --- 程式庫實例化 ---
WiFiClientSecure client;
// ----------------------------------------------------
// 【函式宣告 (Function Prototypes)】
// ----------------------------------------------------
void loadConfig();
void saveConfig(const String& payload);
void initializeInputs();
void connectWifi();
void checkSerialConfig();
bool sendLineMessage(const String& message);
void checkAndSendAlarm(int index);
// ----------------------------------------------------
// 【SETUP】
// ----------------------------------------------------
void setup() {
Serial.begin(115200);
Serial.setRxBufferSize(1024); // 增大序列埠接收緩衝區
delay(500);
Serial.println("\n--- ESP32 Line Messaging Alarm System Booting ---");
loadConfig();
initializeInputs();
connectWifi();
Serial.println("System Ready.");
if (String(lineAccessToken).length() > 5 && WiFi.status() == WL_CONNECTED) {
if (String(lineTargetID).length() > 5) {
// 嘗試發送系統啟動訊息 (將失敗,因為 Token 載入是錯的,但用於測試)
sendLineMessage("【系統啟動】ESP32 Line 告警機已連線並啟動。");
} else {
Serial.println("Line Messaging API Warning: Target ID is missing. Cannot send messages.");
}
}
}
// ----------------------------------------------------
// 【LOOP】
// ----------------------------------------------------
void loop() {
checkSerialConfig();
if (WiFi.status() != WL_CONNECTED) {
connectWifi();
delay(1000);
return;
}
if (!serialConfigMode) {
for (int i = 0; i < NUM_INPUTS; i++) {
checkAndSendAlarm(i);
}
}
delay(10);
}
// ----------------------------------------------------
// 【函式定義】
// ----------------------------------------------------
// Preferences 配置載入
void loadConfig() {
preferences.begin(PREFS_NAME, true);
preferences.getString("token", lineAccessToken, LINE_TOKEN_LEN);
preferences.getString("target_id", lineTargetID, LINE_TARGET_ID_LEN);
preferences.getString("ssid", wifiSSID, SSID_LEN);
preferences.getString("pass", wifiPassword, PASS_LEN);
if (String(lineAccessToken).length() == 0) strncpy(lineAccessToken, "", LINE_TOKEN_LEN);
if (String(lineTargetID).length() == 0) strncpy(lineTargetID, "", LINE_TARGET_ID_LEN);
if (String(wifiSSID).length() == 0) strncpy(wifiSSID, "ASUS_D0", SSID_LEN);
if (String(wifiPassword).length() == 0) strncpy(wifiPassword, "staff@54569", PASS_LEN);
sendOnRelease = preferences.getBool("flag_r", false);
sendOnPersist = preferences.getBool("flag_p", false);
for (int i = 0; i < NUM_INPUTS; i++) {
String key = "msg" + String(i + 1);
size_t len = preferences.getString(key.c_str(), alarmMessages[i], ALARM_MSG_LEN);
if (len == 0) {
String defaultMsg = String("警報觸發測試第") + (i + 1 < 10 ? "0" : "") + String(i + 1) + String("組");
strncpy(alarmMessages[i], defaultMsg.c_str(), ALARM_MSG_LEN);
}
}
preferences.end();
Serial.println("Config loaded.");
// 輸出當前載入的 Token 長度以供偵錯
Serial.printf("SSID: %s, Line Token Len: %d, Target ID Len: %d\n", wifiSSID, String(lineAccessToken).length(), String(lineTargetID).length());
}
// Preferences 配置儲存
void saveConfig(const String& payload) {
preferences.begin(PREFS_NAME, false);
String data = payload;
int next;
// 1. Line Access Token
next = data.indexOf('|');
String token = data.substring(0, next);
data = data.substring(next + 1);
// *** 修正:使用 strncpy 確保完整複製且結尾為 '\0' ***
const char* token_c_str = token.c_str();
size_t len = strlen(token_c_str);
size_t copy_len = (len < LINE_TOKEN_LEN) ? len : (LINE_TOKEN_LEN - 1);
strncpy(lineAccessToken, token_c_str, copy_len);
lineAccessToken[copy_len] = '\0'; // 強制添加終止符
preferences.putString("token", lineAccessToken);
// ********************************************************
// 2. Line Target ID
next = data.indexOf('|');
String targetID = data.substring(0, next);
targetID.toCharArray(lineTargetID, LINE_TARGET_ID_LEN);
preferences.putString("target_id", lineTargetID);
data = data.substring(next + 1);
// 3. SSID
next = data.indexOf('|');
String ssid = data.substring(0, next);
ssid.toCharArray(wifiSSID, SSID_LEN);
preferences.putString("ssid", wifiSSID);
data = data.substring(next + 1);
// 4. Password
next = data.indexOf('|');
String pass = data.substring(0, next);
pass.toCharArray(wifiPassword, PASS_LEN);
preferences.putString("pass", wifiPassword);
data = data.substring(next + 1);
// 5. Flags
next = data.indexOf('|');
sendOnRelease = (data.substring(0, next) == "1");
preferences.putBool("flag_r", sendOnRelease);
data = data.substring(next + 1);
next = data.indexOf('|');
sendOnPersist = (data.substring(0, next) == "1");
preferences.putBool("flag_p", sendOnPersist);
data = data.substring(next + 1);
// 6. Messages
for (int i = 0; i < NUM_INPUTS; i++) {
next = data.indexOf('|');
String msg = (next == -1) ? data : data.substring(0, next);
if (msg.length() >= ALARM_MSG_LEN) {
msg = msg.substring(0, ALARM_MSG_LEN - 1);
}
msg.toCharArray(alarmMessages[i], ALARM_MSG_LEN);
preferences.putString(("msg" + String(i+1)).c_str(), alarmMessages[i]);
if (next != -1) {
data = data.substring(next + 1);
}
}
preferences.end();
Serial.println("Config data saved. Rebooting...");
ESP.restart();
}
// 序列埠命令處理 (加入字串淨化和偵錯)
void checkSerialConfig() {
if (Serial.available()) {
// 使用 readString() 確保讀取所有數據
String input = Serial.readString();
input.trim();
// **修正:強制移除所有 Null 字符 (ASCII 0),防止字串提前截斷**
input.replace(String((char)0), "");
if (input.startsWith("SAVE_CFG:")) {
serialConfigMode = true;
Serial.println("50%");
String payload = input.substring(9);
// **偵錯:輸出接收到的 Payload 長度**
Serial.printf("DEBUG: Received Payload Length: %d\n", payload.length());
saveConfig(payload);
return;
} else if (input.startsWith("LOAD_CFG")) {
serialConfigMode = true;
Serial.println("50%");
preferences.begin(PREFS_NAME, true);
String response = "";
response += preferences.getString("token", "") + String("|");
response += preferences.getString("target_id", "") + String("|");
response += preferences.getString("ssid", "ASUS_D0") + String("|");
response += preferences.getString("pass", "staff@54569") + String("|");
response += (preferences.getBool("flag_r", false) ? "1" : "0") + String("|");
response += (preferences.getBool("flag_p", false) ? "1" : "0");
for (int i = 0; i < NUM_INPUTS; i++) {
String key = "msg" + String(i + 1);
String defaultMsg = String("警報觸發測試第") + (i + 1 < 10 ? "0" : "") + String(i + 1) + String("組");
response += String("|") + preferences.getString(key.c_str(), defaultMsg.c_str());
}
preferences.end();
Serial.print("LOAD_RESP:");
Serial.println(response);
Serial.println("100%");
serialConfigMode = false;
}
}
}
// Line Messaging API 實作
bool sendLineMessage(const String& message) {
if (WiFi.status() != WL_CONNECTED || String(lineAccessToken).length() < 5 || String(lineTargetID).length() < 5) {
Serial.println("Line Messaging failed: Check WiFi, Token, or Target ID.");
return false;
}
// 1. 建構 JSON 消息體
StaticJsonDocument<256> doc;
doc["to"] = lineTargetID;
JsonArray messages = doc.createNestedArray("messages");
JsonObject msg = messages.createNestedObject();
msg["type"] = "text";
msg["text"] = message;
String jsonMessage;
serializeJson(doc, jsonMessage);
// 2. 建立連線
client.setInsecure();
if (!client.connect(LINE_HOST, LINE_PORT)) {
Serial.println("Line Messaging connection failed");
return false;
}
// 3. 建立 POST 請求
String request = "POST " + String(LINE_PUSH_PATH) + " HTTP/1.1\r\n";
request += "Host: " + String(LINE_HOST) + "\r\n";
request += "Authorization: Bearer " + String(lineAccessToken) + "\r\n";
request += "Content-Type: application/json\r\n";
request += "Content-Length: " + String(jsonMessage.length()) + "\r\n";
request += "Connection: close\r\n\r\n";
request += jsonMessage;
client.print(request);
Serial.printf("Sent Line Message to %s. Size: %d\n", lineTargetID, jsonMessage.length());
// 4. 等待響應
unsigned long timeout = millis();
while (client.connected() && millis() - timeout < 5000) {
if (client.available()) {
String response = client.readStringUntil('\n');
Serial.print("Line Response: ");
Serial.println(response);
if (response.startsWith("HTTP/1.1 200 OK")) {
client.stop();
return true;
} else if (response.startsWith("HTTP/1.1 400") || response.startsWith("HTTP/1.1 401")) {
Serial.println("Line Messaging Error: Bad Request/Unauthorized (Check Token/Target ID).");
while(client.available()){
Serial.print((char)client.read());
}
Serial.println();
client.stop();
return false;
}
}
}
Serial.println("Line Messaging Timeout.");
client.stop();
return false;
}
// 輸入腳位初始化
void initializeInputs() {
for (int i = 0; i < NUM_INPUTS; i++) {
// 使用 INPUT_PULLUP,當腳位接地 (LOW) 時觸發警報
pinMode(inputMap[i].pin, INPUT_PULLUP);
lastPinState[i] = digitalRead(inputMap[i].pin);
Serial.printf("Initializing Input %d on GPIO %d\n", inputMap[i].id, inputMap[i].pin);
}
}
// Wi-Fi 連線
void connectWifi() {
if (WiFi.status() == WL_CONNECTED) {
return;
}
Serial.print("Connecting to Wi-Fi SSID: ");
Serial.println(wifiSSID);
WiFi.begin(wifiSSID, wifiPassword);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi Connected!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nWiFi Connection Failed! Will retry in loop.");
}
}
// 硬體與警報處理
void checkAndSendAlarm(int index) {
if (String(lineAccessToken).length() < 5 || String(lineTargetID).length() < 5) return;
int currentState = digitalRead(inputMap[index].pin);
String message = alarmMessages[index];
unsigned long currentTime = millis();
String finalMsg;
// 警報觸發 (狀態從 HIGH 變為 LOW)
if (lastPinState[index] == HIGH && currentState == LOW) {
finalMsg = "[🚨 警報觸發] " + message + " (Port " + String(inputMap[index].id) + ")";
sendLineMessage(finalMsg);
lastTriggerTime[index] = currentTime;
Serial.println(finalMsg);
// 警報解除 (狀態從 LOW 變為 HIGH)
} else if (lastPinState[index] == LOW && currentState == HIGH) {
if (sendOnRelease) {
finalMsg = "[✅ 警報解除] " + message + " (Port " + String(inputMap[index].id) + ")";
sendLineMessage(finalMsg);
Serial.println(finalMsg);
}
lastTriggerTime[index] = 0;
// 警報持續 (狀態持續為 LOW)
} else if (lastPinState[index] == LOW && currentState == LOW) {
if (sendOnPersist && (currentTime - lastTriggerTime[index] >= PERSIST_INTERVAL)) {
finalMsg = "[⚠️ 持續警告] " + message + " (Port " + String(inputMap[index].id) + ")";
sendLineMessage(finalMsg);
lastTriggerTime[index] = currentTime;
Serial.println(finalMsg);
}
}
lastPinState[index] = currentState;
}
沒有留言:
張貼留言