Send command from Linebot (Messaging API) to ESP32 Line IoT
使用電腦上的 Python Tkinter GUI 應用程式,透過 USB 序列埠 (Serial) 將配置參數傳輸到 ESP32 並寫入其 EEPROM(或更常見的 NVS/Preferences 儲存)。
由於這個程式涉及三個主要部分(Tkinter GUI、PySerial 序列通訊、ESP32 韌體),我將提供一個高層次的概念和兩個主要部分的程式碼骨架:
Python 端 (PC/Tkinter + PySerial):建立 GUI 介面、收集參數,並透過序列埠發送。
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 訊息程式
#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()




沒有留言:
張貼留言