2025年11月13日 星期四

Send command from Linebot (Messaging API) to ESP32 Line IoT

 Send command from Linebot (Messaging API) to ESP32 Line IoT



使用電腦上的 Python Tkinter GUI 應用程式,透過 USB 序列埠 (Serial) 將配置參數傳輸到 ESP32 並寫入其 EEPROM(或更常見的 NVS/Preferences 儲存)

由於這個程式涉及三個主要部分(Tkinter GUI、PySerial 序列通訊、ESP32 韌體),我將提供一個高層次的概念和兩個主要部分的程式碼骨架

  1. Python 端 (PC/Tkinter + PySerial):建立 GUI 介面、收集參數,並透過序列埠發送。

  2. ESP32 端 (Arduino/C++):讀取序列埠數據、解析數據,並使用 Preferences 庫(推薦替代傳統的 EEPROM 庫)將數據儲存到 Flash。



Python Tkinter 程式 (GUI):將您提供的 Line 和 Wi-Fi 參數作為 預設值,並通過 USB 序列埠將其傳輸到 ESP32。


            ("Channel Access Token:", "token_entry", DEFAULT_TOKEN),

            ("接收者 ID (User ID):", "user_id_entry", DEFAULT_USER_ID),

            ("Line 訊息內容:", "message_entry", DEFAULT_MESSAGE),

            ("Wi-Fi SSID:", "ssid_entry", DEFAULT_SSID),

            ("Wi-Fi Password:", "password_entry", DEFAULT_PASSWORD)


ESP32 Arduino 程式 (韌體):使用 Preferences.h 庫從 NVS 儲存中讀取這些參數,然後進行 Wi-Fi 連線和 Line 訊息發送。






ESP32 端:從 NVS 讀取配置並發送 Line 訊息程式
ESP32 啟動。請在 5 秒內發送配置,否則將使用儲存的數據。

#include <WiFi.h>

#include <HTTPClient.h>

#include <ArduinoJson.h> 

#include <Preferences.h> // 使用 NVS 儲存配置


// --- NVS/Preferences 儲存的 Key ---

#define BAUD_RATE 115200

const char* NVS_NAMESPACE = "config_data"; 

const char* KEY_TOKEN     = "line_token";

const char* KEY_USER_ID   = "line_uid";

const char* KEY_SSID      = "wifi_ssid";

const char* KEY_PASS      = "wifi_pass";

const char* KEY_MESSAGE   = "line_msg"; 


// --- Line API 參數 ---

const char* LINE_API_ENDPOINT = "https://api.line.me/v2/bot/message/push";


// --- 序列埠接收參數 ---

const String COMMAND_PREFIX = "CONFIG";

const char SEPARATOR = '|';


// --- 儲存配置的變數 (從 NVS 載入) ---

Preferences preferences;

String lineChannelAccessToken = "";

String lineUserID = "";

String wifiSSID = "";

String wifiPassword = "";

String lineMessageContent = ""; 


// --- 輔助函數 ---


/**

 * @brief 解析接收到的配置字串並將其儲存到 NVS

 * @param data 完整的配置字串 (Token|UserID|SSID|Password|MessageContent)

 * @return true 如果成功儲存所有數據

 */

bool parseAndSaveConfig(String data) {

    // 移除 COMMAND_PREFIX 和第一個分隔符

    int start_index = data.indexOf(SEPARATOR);

    if (start_index == -1) return false;

    String params = data.substring(start_index + 1);


    // 順序: Token | UserID | SSID | Password | MessageContent

    String values[5]; 

    int current_index = 0;

    int array_index = 0;


    // 解析字串

    while (current_index >= 0 && array_index < 5) {

        int next_index = params.indexOf(SEPARATOR, current_index);

        

        if (next_index == -1) {

            // 處理最後一個參數 (MessageContent)

            values[array_index++] = params.substring(current_index);

            break;

        }

        

        values[array_index++] = params.substring(current_index, next_index);

        current_index = next_index + 1;

    }


    if (array_index < 5) { 

        Serial.println("Error: 參數數量不足 (預期 5 個)!");

        return false;

    }

    

    // 儲存數據到 NVS

    preferences.putString(KEY_TOKEN, values[0]);

    preferences.putString(KEY_USER_ID, values[1]);

    preferences.putString(KEY_SSID, values[2]);

    preferences.putString(KEY_PASS, values[3]);

    preferences.putString(KEY_MESSAGE, values[4]);

    

    Serial.println("--- 新配置已儲存到 NVS ---");

    Serial.print("SSID: "); Serial.println(values[2]);

    Serial.print("Line Message: "); Serial.println(values[4]);

    Serial.println("--------------------");


    // 發送回覆給 Python 端

    Serial.println("OK"); 

    

    return true;

}


/**

 * @brief 從 NVS (非揮發性儲存) 載入所有配置參數

 */

void loadConfig() {

    preferences.begin(NVS_NAMESPACE, true); // true 表示只讀模式


    lineChannelAccessToken = preferences.getString(KEY_TOKEN, "");

    lineUserID = preferences.getString(KEY_USER_ID, "");

    wifiSSID = preferences.getString(KEY_SSID, "");

    wifiPassword = preferences.getString(KEY_PASS, "");

    lineMessageContent = preferences.getString(KEY_MESSAGE, "");


    preferences.end();

    

    Serial.println("--- NVS 載入完成 ---");

}


/**

 * @brief 連接 Wi-Fi

 */

void connectWiFi() {

    if (wifiSSID == "") return;


    Serial.print("正在連線到 Wi-Fi: ");

    Serial.println(wifiSSID);

    

    WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str());


    int attempts = 0;

    while (WiFi.status() != WL_CONNECTED && attempts < 40) {

        delay(500);

        Serial.print(".");

        attempts++;

    }


    if (WiFi.status() == WL_CONNECTED) {

        Serial.println("\nWi-Fi 連線成功!");

    } else {

        Serial.println("\nWi-Fi 連線失敗!");

    }

}


/**

 * @brief 向 Line Message API 發送推播訊息

 */

void sendLineMessage() {

    if (WiFi.status() != WL_CONNECTED) {

        Serial.println("Line 訊息發送失敗:Wi-Fi 未連線。");

        return;

    }

    if (lineChannelAccessToken == "" || lineUserID == "" || lineMessageContent == "") {

         Serial.println("Line 訊息發送失敗:缺少 Token, User ID, 或 Message 內容。");

        return;

    }


    HTTPClient http;

    http.begin(LINE_API_ENDPOINT);

    

    // 設定 HTTP 標頭

    http.addHeader("Content-Type", "application/json");

    String authHeader = String("Bearer ") + lineChannelAccessToken;

    http.addHeader("Authorization", authHeader);


    // 建立 JSON 請求體

    StaticJsonDocument<256> doc;

    doc["to"] = lineUserID;

    JsonArray messages = doc.createNestedArray("messages");

    JsonObject msg = messages.createNestedObject();

    msg["type"] = "text";

    msg["text"] = lineMessageContent; 


    String requestBody;

    serializeJson(doc, requestBody);


    Serial.println("\n--- 正在發送 Line 訊息 ---");

    Serial.print("發送內容: "); Serial.println(lineMessageContent);


    // 發送 POST 請求

    int httpResponseCode = http.POST(requestBody);


    if (httpResponseCode > 0) {

        Serial.printf("[HTTP] POST... code: %d\n", httpResponseCode);

        if (httpResponseCode == HTTP_CODE_OK || httpResponseCode == HTTP_CODE_ACCEPTED) {

            Serial.println("Line 訊息發送成功!");

        } else {

            String payload = http.getString();

            Serial.println("Line 訊息發送失敗,伺服器回應錯誤。");

            Serial.print("錯誤詳情: "); Serial.println(payload);

        }

    } else {

        Serial.printf("[HTTP] POST... 失敗,錯誤碼: %s\n", http.errorToString(httpResponseCode).c_str());

    }


    http.end();

}


void setup() {

    Serial.begin(BAUD_RATE);

    delay(1000);


    // 1. 初始化 NVS 讀寫模式

    preferences.begin(NVS_NAMESPACE, false); 

    

    Serial.println("ESP32 啟動。請在 5 秒內發送配置,否則將使用儲存的數據。");

    

    // 2. 檢查序列埠是否有命令傳入,並儲存

    long startTime = millis();

    while (millis() - startTime < 5000) { 

        if (Serial.available()) {

            String data = Serial.readStringUntil('\n');

            if (data.startsWith(COMMAND_PREFIX)) {

                parseAndSaveConfig(data); // 解析並儲存

                break; 

            }

        }

    }

    

    // 3. 載入配置 (無論是新配置還是舊配置)

    loadConfig();


    if (wifiSSID == "" || lineChannelAccessToken == "" || lineUserID == "" || lineMessageContent == "") {

        Serial.println("致命錯誤:缺少必要的配置。請執行 Python 程式並發送數據。");

        return;

    }


    // 4. 連接 Wi-Fi

    connectWiFi();


    // 5. 發送 Line 訊息

    sendLineMessage();

    

    preferences.end(); // 關閉 Preferences 連線

    Serial.println("\n--- 程式執行結束 ---");

}


void loop() {

    delay(1000); 

}

Python 端:配置傳輸 GUI 程式

import tkinter as tk

from tkinter import messagebox

import serial

import serial.tools.list_ports 


# --- 配置區 ---

BAUD_RATE = 115200

# 傳輸格式: COMMAND|Token|UserID|SSID|Password|MessageContent

COMMAND_PREFIX = "CONFIG"

SEPARATOR = "|"


# --- 預設值 (Default Values) ---

DEFAULT_TOKEN = "P7R4jd35usv1YhlJaC9HGXwcq6G0YknpvDxOb356AnOGHt5MPpzXmJrxj5L9OY5Z70h1DSdKRGw2/6Q8cN0bVoqh6PcUMISbfncKvnMmv2HG5GCR+HMgpPj2LQYqOLDKgDqUGchzrkgkrG1KhnhfnugdB04t89/1O/w1cDnyilFU="

DEFAULT_USER_ID = "U60f091awaaf1dq41e21ace45205bfd3cf"

DEFAULT_MESSAGE = "Hello World ESP32 從esp32送出"

DEFAULT_SSID = "ASUS_30"

DEFAULT_PASSWORD = "beard_7354"

# ----------------


class ESP32ConfigApp:

    def __init__(self, master):

        self.master = master

        master.title("ESP32 Line 訊息配置工具")


        master.protocol("WM_DELETE_WINDOW", self.on_closing) 


        # --- GUI 組件 ---


        # 1. 序列埠選擇

        tk.Label(master, text="序列埠 (COM/tty):").grid(row=0, column=0, sticky='w', padx=5, pady=5)

        

        self.port_list = [port.device for port in serial.tools.list_ports.comports()]

        if not self.port_list:

            self.port_list.append("無可用埠")

            

        self.selected_port = tk.StringVar(master)

        self.selected_port.set(self.port_list[0])

        

        self.port_menu = tk.OptionMenu(master, self.selected_port, *self.port_list)

        self.port_menu.grid(row=0, column=1, sticky='ew', padx=5, pady=5)

        

        # 2. 參數輸入欄位

        fields = [

            ("Channel Access Token:", "token_entry", DEFAULT_TOKEN),

            ("接收者 ID (User ID):", "user_id_entry", DEFAULT_USER_ID),

            ("Line 訊息內容:", "message_entry", DEFAULT_MESSAGE),

            ("Wi-Fi SSID:", "ssid_entry", DEFAULT_SSID),

            ("Wi-Fi Password:", "password_entry", DEFAULT_PASSWORD)

        ]

        

        self.entries = {}

        row_index = 1

        for label_text, entry_name, default_value in fields:

            tk.Label(master, text=label_text).grid(row=row_index, column=0, sticky='w', padx=5, pady=2)

            entry = tk.Entry(master, width=50)

            entry.grid(row=row_index, column=1, sticky='ew', padx=5, pady=2)

            entry.insert(0, default_value)  # 設置預設值

            self.entries[entry_name] = entry

            row_index += 1


        # --- 按鈕區域 ---

        

        # 3. 發送按鈕

        self.send_button = tk.Button(master, text="傳送配置到 ESP32 NVS", command=self.send_config, bg="lightblue")

        self.send_button.grid(row=row_index, column=0, pady=10, sticky='w', padx=5)

        

        # 4. 結束按鈕

        self.close_button = tk.Button(master, text="結束程式 (Exit)", command=self.on_closing, fg="white", bg="red")

        self.close_button.grid(row=row_index, column=1, pady=10, sticky='e', padx=5)

        

        row_index += 1


        # 5. 狀態訊息

        self.status_label = tk.Label(master, text="狀態: 待命", bd=1, relief=tk.SUNKEN, anchor='w')

        self.status_label.grid(row=row_index, column=0, columnspan=2, sticky='ew', padx=5, pady=2)

        

        master.grid_columnconfigure(1, weight=1)

        

    def update_status(self, message):

        """更新 GUI 底部的狀態訊息"""

        self.status_label.config(text=f"狀態: {message}")

        self.master.update()


    def on_closing(self):

        """處理結束程式的邏輯。"""

        if messagebox.askokcancel("退出", "確定要結束程式嗎?"):

            self.master.destroy() 

        

    def send_config(self):

        """處理按鈕點擊事件,發送數據"""

        

        port = self.selected_port.get()

        if port == "無可用埠":

            messagebox.showerror("錯誤", "請連接 ESP32 或刷新列表")

            return

            

        # 1. 收集數據

        token = self.entries["token_entry"].get().strip()

        user_id = self.entries["user_id_entry"].get().strip()

        message_content = self.entries["message_entry"].get().strip()

        ssid = self.entries["ssid_entry"].get().strip()

        password = self.entries["password_entry"].get().strip()

        

        # 2. 格式化數據 (格式必須與 ESP32 解析順序一致: Token|UserID|SSID|Password|MessageContent)

        data_string = f"{COMMAND_PREFIX}{SEPARATOR}{token}{SEPARATOR}{user_id}{SEPARATOR}{ssid}{SEPARATOR}{password}{SEPARATOR}{message_content}\n"

        

        self.update_status("正在嘗試連接和發送...")


        try:

            # 3. 建立並開啟序列埠連線

            with serial.Serial(port, BAUD_RATE, timeout=5) as ser:

                ser.write(data_string.encode('utf-8'))

                self.update_status(f"配置字串已發送。等待 ESP32 回覆...")

                

                # 讀取 ESP32 的回覆

                response = ser.readline().decode('utf-8').strip()

                if response == "OK":

                    self.update_status("✅ 發送成功!ESP32 確認配置已儲存。")

                else:

                    self.update_status(f"⚠️ 發送成功,但 ESP32 未回覆 OK。請檢查 Arduino Serial Monitor。")


        except serial.SerialException as e:

            messagebox.showerror("序列埠錯誤", f"無法連接到 {port} 或通訊失敗:\n{e}")

            self.update_status("❌ 發送失敗")

        except Exception as e:

            messagebox.showerror("未知錯誤", f"發生錯誤:\n{e}")

            self.update_status("❌ 發送失敗")


if __name__ == "__main__":

    root = tk.Tk()

    app = ESP32ConfigApp(root)

    root.mainloop()


沒有留言:

張貼留言

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...