2025年11月16日 星期日

Line 發報機

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;
}

沒有留言:

張貼留言

ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

 ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...