2026年6月9日 星期二

Node-Red +SQLite <範例2>

 Node-Red +SQLite <範例2>

直接共用原本的 student.db 設定,不需建立新資料庫檔案。






[{"id":"in_gr_subject_v6","type":"ui_dropdown","z":"d84044bddad55edc","name":"選擇輸入科目","label":"● 選擇欲輸入科目","tooltip":"","place":"請選擇科目","group":"ui_grp_grade_left_form_v6","order":1,"width":0,"height":0,"passthru":true,"multiple":false,"options":[{"label":"國文","value":"chinese","type":"str"},{"label":"英文","value":"english","type":"str"},{"label":"數學","value":"math","type":"str"}],"payload":"","topic":"selected_subject","topicType":"str","className":"","x":140,"y":80,"wires":[["change_save_grade_v6"]]},{"id":"in_gr_id_v6","type":"ui_text_input","z":"d84044bddad55edc","name":"成績學號輸入","label":"1. 學號 (輸入欲登記成績之學號)","tooltip":"","group":"ui_grp_grade_left_form_v6","order":3,"width":0,"height":0,"passthru":false,"mode":"text","delay":0,"topic":"student_id","sendOnBlur":true,"className":"","topicType":"str","x":140,"y":120,"wires":[["change_save_grade_v6"]]},{"id":"in_gr_score_v6","type":"ui_text_input","z":"d84044bddad55edc","name":"分數輸入","label":"2. 分數 (請輸入該科分數 0-100)","tooltip":"","group":"ui_grp_grade_left_form_v6","order":5,"width":0,"height":0,"passthru":false,"mode":"text","delay":0,"topic":"score","sendOnBlur":true,"className":"","topicType":"str","x":120,"y":160,"wires":[["change_save_grade_v6"]]},{"id":"change_save_grade_v6","type":"change","z":"d84044bddad55edc","name":"即時暫存成績欄位資料","rules":[{"t":"set","p":"grade_form[msg.topic]","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":120,"wires":[["cd1ed0c6ac95cc06"]]},{"id":"btn_gr_1_v6","type":"ui_button","z":"d84044bddad55edc","name":"鍵1_建立成績表","group":"ui_grp_grade_right_btn_v6","order":1,"width":0,"height":0,"passthru":false,"label":"建立成績資料表","tooltip":"","color":"#ffffff","bgcolor":"#17a2b8","className":"","icon":"","payload":"","payloadType":"str","topic":"CREATE_GRADE_TABLE","topicType":"str","x":140,"y":220,"wires":[["func_grade_core_v6"]]},{"id":"btn_gr_2_v6","type":"ui_button","z":"d84044bddad55edc","name":"鍵2_同步學生名單","group":"ui_grp_grade_right_btn_v6","order":2,"width":0,"height":0,"passthru":false,"label":"從學生系統同步(匯入)名單","tooltip":"","color":"#ffffff","bgcolor":"#ffc107","className":"","icon":"","payload":"","payloadType":"str","topic":"SYNC_STUDENTS","topicType":"str","x":160,"y":260,"wires":[["func_grade_core_v6"]]},{"id":"btn_gr_3_v6","type":"ui_button","z":"d84044bddad55edc","name":"鍵3_輸入更正科目成績","group":"ui_grp_grade_right_btn_v6","order":3,"width":0,"height":0,"passthru":false,"label":"登記 / 更正該科成績","tooltip":"","color":"#ffffff","bgcolor":"#28a745","className":"","icon":"","payload":"","payloadType":"str","topic":"UPDATE_GRADE","topicType":"str","x":170,"y":300,"wires":[["func_grade_core_v6"]]},{"id":"btn_gr_4_v6","type":"ui_button","z":"d84044bddad55edc","name":"鍵4_結算顯示所有成績","group":"ui_grp_grade_right_btn_v6","order":4,"width":0,"height":0,"passthru":false,"label":"結算並顯示所有成績","tooltip":"","color":"#ffffff","bgcolor":"#007bff","className":"","icon":"","payload":"","payloadType":"str","topic":"DISPLAY_ALL_GRADES","topicType":"str","x":170,"y":340,"wires":[["func_grade_core_v6"]]},{"id":"btn_gr_5_v6","type":"ui_button","z":"d84044bddad55edc","name":"鍵5_查詢單生健康成績","group":"ui_grp_grade_right_btn_v6","order":5,"width":0,"height":0,"passthru":false,"label":"查詢單生資料(依學號)","tooltip":"","color":"#ffffff","bgcolor":"#6c757d","className":"","icon":"","payload":"","payloadType":"str","topic":"QUERY_GRADE","topicType":"str","x":170,"y":380,"wires":[["func_grade_core_v6"]]},{"id":"func_grade_core_v6","type":"function","z":"d84044bddad55edc","name":"成績系統核心字串拼接邏輯","func":"var action = msg.topic;\nvar form = flow.get(\"grade_form\") || {};\n\nvar subject = form.selected_subject ? form.selected_subject.trim() : \"\";\nvar student_id = form.student_id ? form.student_id.trim() : \"\";\nvar score = (form.score !== undefined && form.score !== \"\") ? form.score.trim() : \"0\";\n\nswitch(action) {\n    case \"CREATE_GRADE_TABLE\":\n        msg.topic = `CREATE TABLE IF NOT EXISTS grades (\n            student_id TEXT PRIMARY KEY,\n            chinese REAL DEFAULT 0,\n            english REAL DEFAULT 0,\n            math REAL DEFAULT 0,\n            total REAL GENERATED ALWAYS AS (chinese + english + math),\n            average REAL GENERATED ALWAYS AS ((chinese + english + math) / 3.0),\n            FOREIGN KEY(student_id) REFERENCES students(student_id) ON DELETE CASCADE\n        );`;\n        break;\n        \n    case \"SYNC_STUDENTS\":\n        msg.topic = \"INSERT OR IGNORE INTO grades (student_id) SELECT student_id FROM students;\";\n        break;\n        \n    case \"UPDATE_GRADE\":\n        if (student_id === \"\") {\n            node.error(\"【系統警告】登記失敗:請輸入學號!\");\n            return null;\n        }\n        if (subject === \"\") {\n            node.error(\"【系統警告】登記失敗:請先在下拉選單選擇輸入科目!\");\n            return null;\n        }\n        // 純字串拼接,因為分數是數字型態,不需要包單引號\n        msg.topic = \"UPDATE grades SET \" + subject + \" = \" + score + \" WHERE student_id = '\" + student_id + \"';\";\n        break;\n        \n    case \"QUERY_GRADE\":\n        if (student_id === \"\") {\n            node.error(\"【系統警告】查詢失敗:請輸入學號!\");\n            return null;\n        }\n        // 使用 LEFT JOIN 實時跨表從 students 關聯抓取姓名 (s.name AS name)\n        msg.topic = `SELECT g.student_id, s.name AS name, g.chinese, g.english, g.math, g.total, ROUND(g.average, 2) AS average \n                     FROM grades g \n                     LEFT JOIN students s ON g.student_id = s.student_id \n                     WHERE g.student_id = '${student_id}';`;\n        break;\n        \n    case \"DISPLAY_ALL_GRADES\":\n        // 顯示所有資料共 7 欄位,姓名完全實時關聯取得\n        msg.topic = `SELECT g.student_id, s.name AS name, g.chinese, g.english, g.math, g.total, ROUND(g.average, 2) AS average \n                     FROM grades g \n                     LEFT JOIN students s ON g.student_id = s.student_id \n                     ORDER BY g.student_id ASC;`;\n        break;\n        \n    default:\n        return null;\n}\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":300,"wires":[["sqlite_node_grade_v6"]]},{"id":"sqlite_node_grade_v6","type":"sqlite","z":"d84044bddad55edc","mydb":"sqlite_config_switch_v2","sqlquery":"msg.topic","sql":"","name":"SQLite 成績庫","x":680,"y":300,"wires":[["ui_table_grade_v6"]]},{"id":"ui_table_grade_v6","type":"ui_table","z":"d84044bddad55edc","group":"ui_grp_grade_bottom_table_v6","name":"學生成績大面板","order":1,"width":12,"height":6,"columns":[{"field":"student_id","title":"學號","width":"14%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"name","title":"姓名","width":"14%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"chinese","title":"國文","width":"12%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"english","title":"英文","width":"12%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"math","title":"數學","width":"12%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"total","title":"總分","width":"18%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"average","title":"平均分數","width":"18%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}}],"outputs":0,"cts":false,"x":860,"y":300,"wires":[]},{"id":"cd1ed0c6ac95cc06","type":"debug","z":"d84044bddad55edc","name":"debug 391","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":610,"y":120,"wires":[]},{"id":"ui_grp_grade_left_form_v6","type":"ui_group","name":"1. 成績輸入與科目選擇","tab":"d0cd6479caf695f3","order":4,"disp":true,"width":"8","collapse":false,"className":""},{"id":"ui_grp_grade_right_btn_v6","type":"ui_group","name":"2. 成績操作快捷鍵","tab":"d0cd6479caf695f3","order":5,"disp":true,"width":"4","collapse":false,"className":""},{"id":"sqlite_config_switch_v2","type":"sqlitedb","db":"student.db","mode":"RWC"},{"id":"ui_grp_grade_bottom_table_v6","type":"ui_group","name":"3. 成績資料顯示面板 (自動關聯姓名、結算總分與平均)","tab":"d0cd6479caf695f3","order":6,"disp":true,"width":"12","collapse":false,"className":""},{"id":"d0cd6479caf695f3","type":"ui_tab","name":"學生成績管理系統","icon":"dashboard","disabled":false,"hidden":false}]


這份 JSON 程式碼構建了系統的 「第二部分:學生成績管理系統」。它與第一部分(學生資料系統)共享同一個 SQLite 資料庫檔案 student.db,並透過經典的 Node-RED Dashboard 元件,完美實現了前端數據收集、動態 SQL 拼接與後端關聯式資料庫的互動。

以下為您詳細拆解這段程式的架構與運作邏輯:

🏗️ 一、 前端網頁佈局 (UI Dashboard)

這段 Flow 同樣利用 Grid(網格)系統將網頁畫面精準地切分為三個區塊,維持系統操作的一致性:

  • 左側輸入區 (ui_grp_grade_left_form_v6,寬度 8): 包含 3 個核心元件:一個下拉選單(選擇國文、英文或數學)與兩個文字輸入框(學號、分數)。

  • 右側按鍵區 (ui_grp_grade_right_btn_v6,寬度 4): 垂直排列 5 個功能實體按鈕(建立成績表、同步名單、登記/更正、結算顯示、單生查詢)。

  • 下方大面板 (ui_grp_grade_bottom_table_v6,寬度 12): 放置一個 7 欄位的表格元件 (ui_table),用來完整呈現包含姓名與計算結果的成绩單。

🔄 二、 數據流程與暫存機制

與第一部分相同,為了避免使用者每打一個字就去衝擊資料庫,系統採用了「非同步暫存」設計:

  1. 當使用者在網頁左側操作「選擇科目」、「輸入學號」或「輸入分數」時,這三個節點會將各自的數值發送到 「即時暫存成績欄位資料」(Change 節點)。

  2. Change 節點會以各自的 msg.topicselected_subjectstudent_idscore)作為鑰匙,將數值存入 Node-RED 的記憶體環境變數 flow.grade_form 中。

  3. 同時,它會將資料複製一份送往右上方的 「debug 391」 節點,方便您在除錯視窗即時監看目前輸入的暫存內容是否正確。

🧠 三、 核心 Function 邏輯解析 (func_grade_core_v6)

當使用者點擊右側 5 個按鈕中的任意一個時,按鈕會發送專屬的 msg.topic 指令(如 ADDUPDATE_GRADE 等)進入大腦 Function 節點。

Function 節點會從記憶體中取出暫存的科目、學號與分數,並根據點擊的按鈕執行 switch(action) 進行 SQL 字串拼接。以下是各個按鍵觸發的 SQL 邏輯:

鍵 1:建立成績資料表 (CREATE_GRADE_TABLE)

SQL
CREATE TABLE IF NOT EXISTS grades (
    student_id TEXT PRIMARY KEY,
    chinese REAL DEFAULT 0,
    english REAL DEFAULT 0,
    math REAL DEFAULT 0,
    total REAL GENERATED ALWAYS AS (chinese + english + math),
    average REAL GENERATED ALWAYS AS ((chinese + english + math) / 3.0),
    FOREIGN KEY(student_id) REFERENCES students(student_id) ON DELETE CASCADE
);
  • 說明: 這裏完美應用了您指定的 SQLite 進階技術。totalaverage 欄位被設定為 GENERATED ALWAYS AS(產生欄位)。這代表總分與平均完全不需要寫程式去計算,SQLite 驅動層會在分數寫入的瞬間自動在底層算好。

鍵 2:從學生系統同步(匯入)名單 (SYNC_STUDENTS)

SQL
INSERT OR IGNORE INTO grades (student_id) SELECT student_id FROM students;
  • 說明: 這行指令解決了「名單必須由第一系統取得」的需求。它會直接去學生基本資料表(students)把所有人的學號撈出來並倒進成績表(grades)中。使用 INSERT OR IGNORE 可以確保重複點擊時不會因為主鍵衝突而報錯。

鍵 3:登記 / 更正該科成績 (UPDATE_GRADE)

JavaScript
msg.topic = "UPDATE grades SET " + subject + " = " + score + " WHERE student_id = '" + student_id + "';";
  • 說明: 這裏實現了「一科目一科目連續輸入」的設計。程式會動態讀取變數 subject 的值(chineseenglishmath),並動態拼接成 SQL 欄位。

  • 範例: 如果您選了國文,輸入學號 DB112203,分數 95,拼接出來就是: UPDATE grades SET chinese = 95 WHERE student_id = 'DB112203'; 因為 score 提取出來後是數字,所以拼接時它很聰明地沒有包單引號;而學號是文字,兩側確實包了單引號 '${student_id}'

鍵 4 & 鍵 5:結算顯示與單生查詢 (DISPLAY_ALL_GRADES / QUERY_GRADE)

SQL
SELECT g.student_id, s.name AS name, g.chinese, g.english, g.math, g.total, ROUND(g.average, 2) AS average 
FROM grades g 
LEFT JOIN students s ON g.student_id = s.student_id
  • 說明(核心技術): 因為成績表本身只存儲學號,不存姓名。為了動態得到姓名,這裏使用了 LEFT JOIN students s ON g.student_id = s.student_id(左外部連接)

  • 它會實時(Real-time)根據學號去第一部分的基本資料表撈出對應的 name(姓名),並利用 ROUND(g.average, 2) 將平均分數四捨五入到小數點後兩位。

🗄️ 四、 後端執行與前端表格顯示

  1. SQLite 成績庫節點 (sqlite_node_grade_v6): 其 SQL Type 設定為 via msg.topic(即 sqlquery: "msg.topic"),它會死死接收前面 Function 拼接好的純文字 SQL 命令並直接送入 student.db 執行。

  2. 學生成績大面板 (ui_table_grade_v6): 當執行查詢或顯示所有時,SQLite 回傳的 7 欄位數據(學號、姓名、國文、英文、數學、總分、平均分數)會精準流入表格元件。因為表格內部設定的欄位 Key(field: "name", field: "total" 等)與 SQL 查詢出來的別名完全對齊,畫面便能毫無時差地渲染出一張精美的成績大報表。

Node-Red +SQLite <範例1>

Node-Red +SQLite範例 

SQLite 是一款非常獨特且極受歡迎的關聯式資料庫管理系統(RDBMS)。與 MySQL、PostgreSQL 或 Oracle 等傳統資料庫不同,SQLite 的核心設計理念是輕量化、嵌入式且無需設定

以下為您詳細拆解 SQLite 的核心功能與特性:

💡 核心特性 (Key Features)

1. 嵌入式與伺服器無關 (Serverless)

傳統資料庫需要啟動一個獨立的伺服器行程(Process),並透過網路連接埠(如 MySQL 的 3306)進行通訊。

  • SQLite 沒有獨立的伺服器行程

  • 它是一個軟體庫(Library),直接被編譯並嵌入到你的應用程式中。應用程式透過直接呼叫函數來讀寫資料,完全省去了網路傳輸的開銷。

2. 單一檔案儲存 (Single-File Database)

  • SQLite 將整個資料庫(包含結構定義、資料表、索引、資料本身)完整地儲存在電腦硬碟中的單一個普通檔案中。

  • 這使得資料庫的備份、複製、共享變得極其簡單,就像移動一個 Word 文件一樣方便。

3. 零配置 (Zero-Configuration)

  • 使用 SQLite 不需要安裝、不需要設定設定檔、不需要管理使用者權限,更不需要啟動或停止服務。

  • 只要你的應用程式載入了 SQLite 庫,指定一個檔案路徑,資料庫就馬建好並可以使用了。

4. 跨平台與高相容性

  • SQLite 的資料庫檔案格式是跨平台的。你在 Windows 上生成的資料庫檔案,可以直接複製到 Mac、Linux、iOS 或 Android 上直接讀取,完全不需要轉換格式。

5. 動態型別系統 (Manifest Typing)

  • 多數 SQL 資料庫(如 MySQL)在建立欄位時如果指定了 INTEGER,就絕對不能存入字串。

  • SQLite 採用動態型別,資料的型別是跟著「資料本身」走,而不是跟著「欄位」走。你可以在一個 INTEGER 欄位裡存入字串(除了 INTEGER PRIMARY KEY 例外)。

🛠️ 主要功能 (Major Capabilities)

  • 完整的 SQL 支援: 支援絕大多數的 SQL-92 標準,包含複雜的查詢(JOINSUBQUERY)、檢視表(VIEW)、觸發器(TRIGGER)以及視窗函數(Window Functions)。

  • ACID 事務支援: 具備完整的原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)與持久性(Durability)。即使程式崩潰或突然斷電,也能確保資料不會損壞。

  • 記憶體資料庫 (In-Memory Database): 支援直接在記憶體中建立資料庫(使用 :memory: 作為檔名),讀寫速度極快,非常適合用於暫存資料或單元測試(Unit Test)。

  • 高效能的讀取: 對於中小型規模的資料,SQLite 的讀取速度往往比傳統客戶端/伺服器架構的資料庫還要快,因為它沒有網路延遲。

⚖️ 優缺點與適用場景

優點 (Pros)缺點與限制 (Cons)
極度輕量:核心庫大小通常小於 1MB。不適合高併發寫入:寫入時會鎖定整個資料庫檔案,若有大量同時寫入的需求會遭遇瓶頸。
維護成本極低:免去維護資料庫伺服器的煩惱。缺乏細粒度權限控制:無法針對單一使用者設定特定資料表的讀寫權限。
隨處可見:Android、iOS、Windows、Mac 內部都內建了 SQLite。不適合海量資料:雖然理論支援到 281 TB,但超大型數據更適合分布式資料庫。

🎯 最佳適用場景:

  1. 行動裝置與桌面軟體:App(iOS/Android)儲存用戶資料、瀏覽器(如 Chrome)儲存歷史紀錄。

  2. 物聯網與嵌入式設備 (IoT):無人機、智慧家電、車載系統。

  3. 軟體開發與測試:在開發階段快速驗證功能,或作為單元測試的隔離資料庫。

  4. 低流量網站/工具:個人部落格、企業內部小型管理系統。




數字
let temp="update Score set";
temp=temp+"  chinese= " + chin ;
temp=temp+", mathematics= "+ math ;
temp=temp+", english= " + engl ;
temp=temp+", total= " + tot ;
temp=temp+", average= " + avg ;
temp=temp+" where name= '"+ name +"'";
msg.topic=temp;
return msg;

文字
var action = msg.topic;
var form = flow.get("student_form") || {};

var student_id = form.student_id ? form.student_id.trim() : "";
var name = form.name ? form.name.trim() : "";
var gender = form.gender ? form.gender.trim() : "女";
var birth = form.birth_date || "";
var school = form.grad_school ? form.grad_school.trim() : "";
var note = form.note ? form.note.trim() : "";
if (student_id === "") {
    node.error("【系統警告】更正資料失敗:必須提供欲修改之『學號』!");
         return null;
    }

let temp = "update students set";
temp = temp + "  name= '" + name +"'";
temp = temp + ", gender= '" + gender + "'";
temp = temp + ", birth_date= '" + birth + "'";
temp = temp + ", grad_school= '" + school + "'";
temp = temp + ", note= '" + note + "'";
temp = temp + " where student_id= '" + student_id + "'";
msg.topic = temp;
return msg;




[{"id":"19c22864913f7b0e","type":"tab","label":"學生成績管理系統","disabled":false,"info":""},{"id":"c94f3be1821949c2","type":"debug","z":"19c22864913f7b0e","name":"debug 389","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":510,"y":200,"wires":[]},{"id":"3ed0de022680f2db","type":"debug","z":"19c22864913f7b0e","name":"debug 390","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":550,"y":400,"wires":[]},{"id":"in_sw_id_v2","type":"ui_text_input","z":"19c22864913f7b0e","name":"學號輸入","label":"1. 學號 (主鍵必填)","tooltip":"","group":"ui_grp_left_form_switch_v2","order":1,"width":0,"height":0,"passthru":false,"mode":"text","delay":0,"topic":"student_id","sendOnBlur":true,"x":80,"y":100,"wires":[["change_save_switch_v2"]]},{"id":"in_sw_name_v2","type":"ui_text_input","z":"19c22864913f7b0e","name":"姓名輸入","label":"2. 姓名 (新增時必填)","tooltip":"","group":"ui_grp_left_form_switch_v2","order":2,"width":0,"height":0,"passthru":false,"mode":"text","delay":0,"topic":"name","sendOnBlur":true,"x":80,"y":140,"wires":[["change_save_switch_v2"]]},{"id":"in_sw_gender_v2","type":"ui_switch","z":"19c22864913f7b0e","name":"性別開關","label":"3. 性別 (關閉:女 / 開啟:男)","tooltip":"","group":"ui_grp_left_form_switch_v2","order":3,"width":0,"height":0,"passthru":false,"decouple":"false","topic":"gender","style":"","onvalue":"男","onvalueType":"str","onicon":"","oncolor":"","offvalue":"女","offvalueType":"str","officon":"","offcolor":"","x":80,"y":180,"wires":[["change_save_switch_v2"]]},{"id":"in_sw_dob_v2","type":"ui_text_input","z":"19c22864913f7b0e","name":"生日輸入","label":"4. 出生年次","tooltip":"","group":"ui_grp_left_form_switch_v2","order":4,"width":0,"height":0,"passthru":false,"mode":"text","delay":0,"topic":"birth_date","sendOnBlur":true,"className":"","topicType":"str","x":80,"y":220,"wires":[["change_save_switch_v2"]]},{"id":"in_sw_school_v2","type":"ui_text_input","z":"19c22864913f7b0e","name":"畢業科系輸入","label":"5. 畢業科系","tooltip":"","group":"ui_grp_left_form_switch_v2","order":5,"width":0,"height":0,"passthru":false,"mode":"text","delay":0,"topic":"grad_school","sendOnBlur":true,"x":100,"y":260,"wires":[["change_save_switch_v2"]]},{"id":"in_sw_note_v2","type":"ui_text_input","z":"19c22864913f7b0e","name":"備註輸入","label":"6. 備註","tooltip":"","group":"ui_grp_left_form_switch_v2","order":6,"width":0,"height":0,"passthru":false,"mode":"text","delay":0,"topic":"note","sendOnBlur":true,"x":80,"y":300,"wires":[["change_save_switch_v2"]]},{"id":"change_save_switch_v2","type":"change","z":"19c22864913f7b0e","name":"即時暫存欄位資料","rules":[{"t":"set","p":"student_form[msg.topic]","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":200,"wires":[["c94f3be1821949c2"]]},{"id":"btn_sw_1_v2","type":"ui_button","z":"19c22864913f7b0e","name":"鍵1_建立資料表","group":"ui_grp_right_btn_switch_v2","order":1,"width":0,"height":0,"passthru":false,"label":"建立資料表","tooltip":"","color":"#ffffff","bgcolor":"#17a2b8","payload":"","payloadType":"str","topic":"CREATE_TABLE","x":100,"y":360,"wires":[["func_sw_core_v2"]]},{"id":"btn_sw_2_v2","type":"ui_button","z":"19c22864913f7b0e","name":"鍵2_新增","group":"ui_grp_right_btn_switch_v2","order":2,"width":0,"height":0,"passthru":false,"label":"新增資料","tooltip":"","color":"#ffffff","bgcolor":"#28a745","payload":"","payloadType":"str","topic":"ADD","x":80,"y":400,"wires":[["func_sw_core_v2"]]},{"id":"btn_sw_3_v2","type":"ui_button","z":"19c22864913f7b0e","name":"鍵3_更正","group":"ui_grp_right_btn_switch_v2","order":3,"width":0,"height":0,"passthru":false,"label":"更正資料","tooltip":"","color":"#ffffff","bgcolor":"#ffc107","payload":"","payloadType":"str","topic":"UPDATE","x":80,"y":440,"wires":[["func_sw_core_v2"]]},{"id":"btn_sw_4_v2","type":"ui_button","z":"19c22864913f7b0e","name":"鍵4_刪除","group":"ui_grp_right_btn_switch_v2","order":4,"width":0,"height":0,"passthru":false,"label":"刪除資料","tooltip":"","color":"#ffffff","bgcolor":"#dc3545","payload":"","payloadType":"str","topic":"DELETE","x":80,"y":480,"wires":[["func_sw_core_v2"]]},{"id":"btn_sw_5_v2","type":"ui_button","z":"19c22864913f7b0e","name":"鍵5_查詢","group":"ui_grp_right_btn_switch_v2","order":5,"width":0,"height":0,"passthru":false,"label":"查詢資料(依學號)","tooltip":"","color":"#ffffff","bgcolor":"#6c757d","payload":"","payloadType":"str","topic":"QUERY","x":80,"y":520,"wires":[["func_sw_core_v2"]]},{"id":"btn_sw_6_v2","type":"ui_button","z":"19c22864913f7b0e","name":"鍵6_顯示所有","group":"ui_grp_right_btn_switch_v2","order":6,"width":0,"height":0,"passthru":false,"label":"顯示所有資料","tooltip":"","color":"#ffffff","bgcolor":"#007bff","payload":"","payloadType":"str","topic":"DISPLAY_ALL","x":100,"y":560,"wires":[["func_sw_core_v2"]]},{"id":"func_sw_core_v2","type":"function","z":"19c22864913f7b0e","name":"6鍵核心冒號映射邏輯","func":"var action = msg.topic;\nvar form = flow.get(\"student_form\") || {};\n\nvar student_id = form.student_id ? form.student_id.trim() : \"\";\nvar name = form.name ? form.name.trim() : \"\";\nvar gender = form.gender ? form.gender.trim() : \"女\";\nvar birth = form.birth_date || \"\";\nvar school = form.grad_school ? form.grad_school.trim() : \"\";\nvar note = form.note ? form.note.trim() : \"\";\n\nswitch (action) {\n    case \"CREATE_TABLE\":\n        msg.topic = `CREATE TABLE IF NOT EXISTS students (\n            student_id TEXT PRIMARY KEY,\n            name TEXT NOT NULL,\n            gender TEXT,\n            birth_date TEXT,\n            grad_school TEXT,\n            note TEXT\n        );`;\n        msg.payload = {};\n        break;\n\n    case \"ADD\":\n        if (student_id === \"\" || name === \"\") {\n            node.error(\"【系統警告】新增資料失敗:學號與姓名欄位為必填項目!\");\n            return null;\n        }\n        msg.topic = `INSERT INTO students (student_id, name, gender, birth_date, grad_school, note) VALUES ('${student_id}','${name}','${gender}','${birth}','${school}','${note}')`;\n        break;\n        //msg.topic = \"INSERT INTO LEDSTATUS ( STATUS , Date , Time ) VALUES ($myLED,  $var_date ,  $var_time ) \";\n        //msg.payload = [myLED, var_date, var_time]\n        //return msg;\n\n    case \"UPDATE\":\n        if (student_id === \"\") {\n            node.error(\"【系統警告】更正資料失敗:必須提供欲修改之『學號』!\");\n            return null;\n        }\n        let temp = \"update students set\";\n        temp = temp + \"  name= '\" + name + \"'\";\n        temp = temp + \", gender= '\" + gender + \"'\";\n        temp = temp + \", birth_date= '\" + birth + \"'\";\n        temp = temp + \", grad_school= '\" + school + \"'\";\n        temp = temp + \", note= '\" + note + \"'\";\n        temp = temp + \" where student_id= '\" + student_id + \"'\";\n        msg.topic = temp;\n        return msg;\n\n    case \"DELETE\":\n        if (student_id === \"\") {\n            node.error(\"【系統警告】刪除資料失敗:請指定要刪除的『學號』!\");\n            return null;\n        }\n        let temp1 = \"DELETE FROM students \";\n        temp1 = temp1 + \" where student_id= '\" + student_id + \"'\";\n        msg.topic = temp1;   \n        break;\n\n    case \"QUERY\":\n        if (student_id === \"\") {\n            node.error(\"【系統警告】查詢失敗:請在左側輸入欲查詢的『學號』!\");\n            return null;\n        }\n        let temp3= \"SELECT * FROM students \";\n        temp3 = temp3 + \" where student_id= '\" + student_id + \"'\";\n        msg.topic = temp3; \n        break;\n\n    case \"DISPLAY_ALL\":\n        msg.topic = `SELECT * FROM students ORDER BY student_id ASC;`;\n        break;\n\n    default:\n        return null;\n}\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":460,"wires":[["3ed0de022680f2db","sqlite_node_switch_v2"]]},{"id":"sqlite_node_switch_v2","type":"sqlite","z":"19c22864913f7b0e","mydb":"sqlite_config_switch_v2","sqlquery":"msg.topic","sql":"","name":"SQLite 資料庫","x":540,"y":500,"wires":[["ui_table_switch_v2"]]},{"id":"ui_table_switch_v2","type":"ui_table","z":"19c22864913f7b0e","group":"ui_grp_bottom_table_switch_v2","name":"學生資料顯示面版","order":1,"width":"12","height":"6","columns":[{"field":"student_id","title":"學號","width":"15%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"name","title":"姓名","width":"15%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"gender","title":"性別","width":"10%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"birth_date","title":"出生年次","width":"20%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"grad_school","title":"畢業科系","width":"20%","align":"center","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"note","title":"備註","width":"20%","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}}],"outputs":0,"cts":false,"x":730,"y":500,"wires":[]},{"id":"f6a5c8e22f729820","type":"comment","z":"19c22864913f7b0e","name":"資料表 students  (stduent.db)","info":"CREATE TABLE \"students\" (\n\t\"student_id\"\tTEXT,\n\t\"name\"\tTEXT NOT NULL,\n\t\"gender\"\tTEXT,\n\t\"birth_date\"\tTEXT,\n\t\"grad_school\"\tTEXT,\n\t\"note\"\tTEXT,\n\tPRIMARY KEY(\"student_id\")\n);","x":340,"y":160,"wires":[]},{"id":"808d139d0ecb740b","type":"comment","z":"19c22864913f7b0e","name":"update","info":"let temp=\"update Score set\";\ntemp=temp+\"  chinese= \" + chin ;\ntemp=temp+\", mathematics= \"+ math ;\ntemp=temp+\", english= \" + engl ;\ntemp=temp+\", total= \" + tot ;\ntemp=temp+\", average= \" + avg ;\ntemp=temp+\" where name= '\"+ name +\"'\";\nmsg.topic=temp;\nreturn msg;","x":290,"y":340,"wires":[]},{"id":"cd9c3ea5f68887f0","type":"function","z":"19c22864913f7b0e","name":"更正 function","func":"var action = msg.topic;\nvar form = flow.get(\"student_form\") || {};\n\nvar student_id = form.student_id ? form.student_id.trim() : \"\";\nvar name = form.name ? form.name.trim() : \"\";\nvar gender = form.gender ? form.gender.trim() : \"女\";\nvar birth = form.birth_date || \"\";\nvar school = form.grad_school ? form.grad_school.trim() : \"\";\nvar note = form.note ? form.note.trim() : \"\";\nif (student_id === \"\") {\n    node.error(\"【系統警告】更正資料失敗:必須提供欲修改之『學號』!\");\n         return null;\n    }\n\nlet temp = \"update students set\";\ntemp = temp + \"  name= '\" + name +\"'\";\ntemp = temp + \", gender= '\" + gender + \"'\";\ntemp = temp + \", birth_date= '\" + birth + \"'\";\ntemp = temp + \", grad_school= '\" + school + \"'\";\ntemp = temp + \", note= '\" + note + \"'\";\ntemp = temp + \" where student_id= '\" + student_id + \"'\";\nmsg.topic = temp;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":540,"wires":[["sqlite_node_switch_v2"]]},{"id":"ui_grp_left_form_switch_v2","type":"ui_group","name":"1. 學生資料輸入欄位","tab":"stud_tab_main_switch_v2","order":1,"disp":true,"width":"8","collapse":false},{"id":"ui_grp_right_btn_switch_v2","type":"ui_group","name":"2. 功能操作快捷鍵","tab":"stud_tab_main_switch_v2","order":2,"disp":true,"width":"4","collapse":false},{"id":"sqlite_config_switch_v2","type":"sqlitedb","db":"student.db","mode":"RWC"},{"id":"ui_grp_bottom_table_switch_v2","type":"ui_group","name":"3. 資料顯示面板","tab":"stud_tab_main_switch_v2","order":3,"disp":true,"width":"12","collapse":false},{"id":"stud_tab_main_switch_v2","type":"ui_tab","name":"學生資料管理系統","icon":"dashboard","order":1,"disabled":false,"hidden":false}]



這份 JSON 程式碼構建了一個非常完整且設計精良的學生資料管理系統(第一部分)。它採用了 Node-RED Dashboard 作為前端視覺化網頁,並以 SQLite 作為後端實體資料庫。

以下為您詳細拆解與說明這個程式的核心架構資料流向節點分工

🏗️ 一、 系統視覺與佈局架構 (UI Dashboard)

整個系統在網頁畫面上被劃分為三個主要區塊,利用寬度配置(Grid 系統)實現了「左側表單、右側按鍵、下方看板」的經典佈局:

+-------------------------------------------------------------------------+
|                          學生資料管理系統 (Tab)                          |
+----------------------------------------------------+--------------------+
|                                                    |                    |
|  1. 學生資料輸入欄位 (Group A, 寬度: 8)             | 2. 功能操作快捷鍵  |
|     - 學號、姓名、性別開關(Switch)、                 |    (Group B, 寬度: 4)|
|       出生年次、畢業科系、備註                       |    - 6 個實體按鈕  |
|                                                    |                    |
+----------------------------------------------------+--------------------+
|  3. 資料顯示面板 (Group C, 寬度: 12)                                     |
|     - 學生資料表格 (ui_table)                                            |
+-------------------------------------------------------------------------+
  1. 左側:ui_grp_left_form_switch_v2(寬度 8) 容納 6 個輸入元件。特別的是「性別」使用了 ui_switch,開啟傳送 "男",關閉傳送 "女"

  2. 右側:ui_grp_right_btn_switch_v2(寬度 4) 垂直排列 6 個功能按鈕(建立資料表、新增、更正、刪除、查詢、顯示所有)。

  3. 下方:ui_grp_bottom_table_switch_v2(寬度 12) 橫跨整個畫面,用來放置 ui_table 表格,呈現從 SQLite 查詢出來的最新資料。

🔄 二、 核心資料流向 (Data Flow)

這個程式的精妙之處在於它解決了網頁表單常見的「即時觸發」問題。它將程序分為「輸入期」「執行期」:

1. 輸入期:資料即時暫存

當使用者在左側網頁的 6 個輸入框(如學號、姓名)中輸入文字或切換性別時:

  • 每個輸入節點都會發送一個帶有自己 msg.topic(例如 student_id)的事件。

  • 這些事件會流向中間的 Change 節點 「即時暫存欄位資料」

  • 關鍵邏輯: 它會執行 flow.student_form[msg.topic] = msg.payload,把資料寫入 Node-RED 的 Flow 記憶體環境變數 中。此時完全不觸發資料庫。

2. 執行期:多鍵合一的 SQL 映射

當使用者點擊右側 6 個功能鍵中的任意一個時:

  • 按鈕會發送它專屬的指令標籤(存放於 msg.topic),例如新增按鈕發送 "ADD",更正按鈕發送 "UPDATE"

  • 訊號進入核心 Function 節點 「6鍵核心冒號映射邏輯」

🧠 三、 核心 Function 節點邏輯解析 (func_sw_core_v2)

這個 Function 節點是全系統的大腦,它負責從 Flow 記憶體中撈出剛剛暫存的 6 個欄位資料,並根據點擊的按鈕(action),利用 JavaScript 的 樣板字串(Template Literals) 動態組裝出純文字的 SQL 命令:

JavaScript
var action = msg.topic;
var form = flow.get("student_form") || {};

// 1. 從記憶體提取 6 個欄位資料,並進行嚴謹的修剪(trim)與防錯預設
var student_id = form.student_id ? form.student_id.trim() : "";
var name = form.name ? form.name.trim() : "";
var gender = form.gender ? form.gender.trim() : "女"; // 若沒動過開關,預設為女
var birth = form.birth_date || "";
var school = form.grad_school ? form.grad_school.trim() : "";
var note = form.note ? form.note.trim() : "";

// 2. 依據按鈕動作,組裝對應的 SQL 語句,並加上單引號防止字串報錯
switch (action) {
    case "CREATE_TABLE":
        msg.topic = `CREATE TABLE IF NOT EXISTS students (...);`;
        break;

    case "ADD":
        // 欄位防空驗證
        if (student_id === "" || name === "") { node.error("..."); return null; }
        // 【已修正】將變數直接安全地嵌入單引號之中
        msg.topic = `INSERT INTO students (...) VALUES ('${student_id}','${name}','${gender}','${birth}','${school}','${note}')`;
        break;

    case "UPDATE":
        // 組合更正字串
        msg.topic = `update students set name='${name}', gender='${gender}', birth_date='${birth}', grad_school='${school}', note='${note}' where student_id='${student_id}'`;
        break;

    case "DELETE":
        msg.topic = `DELETE FROM students where student_id= '${student_id}'`;
        break;

    case "QUERY":
        msg.topic = `SELECT * FROM students where student_id= '${student_id}'`;
        break;

    case "DISPLAY_ALL":
        msg.topic = `SELECT * FROM students ORDER BY student_id ASC;`;
        break;
}
return msg;

🗄️ 四、 資料庫與後端顯示 (SQLite & Table)

  1. SQLite 設定節點 (sqlite_config_switch_v2)

    • 配置連接到本地的實體資料庫檔案 student.db,模式為 RWC(可讀、可寫、不存在則自動建立)。

  2. SQLite 實體節點 (sqlite_node_switch_v2)

    • 它的 SQL Type 被配置為 via msg.topic(即配合 sqlquery: "msg.topic")。這意味著它會直接接收前面 Function 節點組裝出來的字串命令並在資料庫內執行。

  3. 前端表格顯示 (ui_table_switch_v2)

    • 當執行的是 QUERYDISPLAY_ALL 時,SQLite 資料庫會回傳一個包含查詢結果的 JSON 陣列物件

    • 此資料流入 ui_table 節點後,表格會依據預設好的 6 個欄頭(學號、姓名、性別、出生年次、畢業科系、備註)自動將資料對齊、渲染並顯示在網頁最下方。

💡 系統優點總結

  • 資料與介面分離:輸入欄位時不會頻繁衝擊資料庫,只有點擊右側功能鍵時才進行單次、精確的資料處理。

  • 字串安全封裝:程式在 SQL 拼接過程中,為每個變數(如 nameschool)都外包了單引號 '${name}',完美解決了 SQLite 之前將中文誤判為 no such column 的錯誤。

  • 高相容性結構:利用 flow context 暫存,即使未來增加第 7、第 8 個科目或成績欄位,也只需要在 Change 節點與核心 Function 節點中擴充變數,整體架構極具彈性。


2026年6月2日 星期二

FUXA + WOKWI ESP32 (1)

FUXA 是一款基於 Web 的開源工業視覺化軟體(SCADA/HMI),能讓使用者透過瀏覽器快速搭建工業監控儀表板、連線 PLC 設備與物聯網裝置。


核心操作 5 大步驟
要讓 FUXA 成功運作並顯示數據,請遵循以下核心流程:  
1. 配置設備連線(Devices)
  • 點擊介面左側或上方的 Devices(設備)。
  • 新增連線並選擇您的通訊協議。FUXA 支援 Modbus RTU/TCP、Siemens S7、OPC UA、MQTT、BACnet 等多種工業與 IoT 協議。
  • 輸入設備的 IP 位址、連接埠(Port)或 Broker 資訊。 
2. 建立變數標籤(Tags)
  • 在已連線的設備下方建立 Tags
  • 設定標籤名稱,並對應實際設備的記憶體位址(例如 Modbus 的 Holding Register 40001,或 MQTT 的 Topic home/sensor/temp)。
  • 設定資料型態(如 INT、FLOAT、BOOLEAN)。 
3. 繪製 HMI 視覺化介面(Views)
  • 切換到 Views(視圖)編輯器。
  • 利用內建的豐富元件庫,將數值顯示框、儀表板、開關按鈕、趨勢圖表(Charts)、泵浦/風扇圖示拖放至畫布中。 
4. 綁定資料變數(Tag Binding)
  • 點擊畫布上的元件(例如一個溫度計圖表)。
  • 在右側屬性面板中,找到資料來源設定,並將其綁定(Bind)到您在步驟 2 建立的 Tag
  • 如此一來,當底層設備的數據變動時,網頁圖表就會即時更新。 
5. 設定報警與定時任務(Alarms & Scripts)
  • Alarms:可設定當某個 Tag 超過設定值(如溫度 > 80 度)時觸發報警,並記錄在系統中。
  • Scripts / Text:支援編寫 JavaScript 邏輯,可用來處理複雜的資料轉換或執行定時任務。


Windows 安裝步驟
  1. 前往 Node.js 官方網站
  2. 點擊 LTS 版本的安裝按鈕下載 .msi 檔案。
  3. 開啟下載的檔案,依據安裝精靈的指示(一路點擊「Next」或「下一步」)完成安裝。
安裝完成後,開啟您的終端機(Terminal)或命令提示字元(cmd),輸入以下指令檢查版本:

  • 檢查 Node.js 是否安裝成功: node -v
  • 檢查 npm 是否安裝成功:      npm -v

透過 Node.js (NPM) 安裝

使用 node.js (建議 v18.x) 安裝: 進入cmd 命令模式
  1. 安裝環境:執行 npm install -g @frangoteam/fuxa-min
  2. 啟動服務:輸入 fuxa

要啟動 FUXA,您可以根據您的安裝方式選擇對應的指令:
🖥️ 本地或 Desktop 安裝
  1. 執行程式:雙擊桌面 FUXA 圖示,系統會自動啟動後台服務。 [1]
  2. 開啟網頁:打開瀏覽器,輸入:
    http://localhost:1881  或 
    http://127.0.0.1:1881/
  3. 預設登入
    • 帳號admin
    • 密碼admin


FUXA設定



















WOKWI ESP32 程式




#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>

// Wi-Fi 設定 (Wokwi 虛擬環境專用)
const char* ssid = "Wokwi-GUEST";
const char* password = "";

// MQTT Broker 設定 (使用 EMQX 公共伺服器)
const char* mqtt_server = "broker.emqx.io";
const int mqtt_port = 1883;

// 定義 MQTT 主題 (已更新為 alex9ufo 專屬代稱)
const char* topic_led = "alex9ufo/esp32/led";
const char* topic_temp = "alex9ufo/esp32/temp";
const char* topic_hum = "alex9ufo/esp32/hum";

#define DHTPIN 15
#define DHTTYPE DHT22
#define LED_PIN 2

DHT dht(DHTPIN, DHTTYPE);
WiFiClient espClient;
PubSubClient client(espClient);

unsigned long lastMsg = 0;

void setup_wifi() {
  delay(10);
  Serial.println("\nConnecting to WiFi...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected!");
}

// 接收來自網頁端的指令
void callback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.print("收到主題 ["); Serial.print(topic); Serial.print("] 的訊息: "); Serial.println(message);

  if (String(topic) == topic_led) {
    if (message == "ON") {
      digitalWrite(LED_PIN, HIGH);
    } else if (message == "OFF") {
      digitalWrite(LED_PIN, LOW);
    }
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("嘗試 MQTT 連線...");
    String clientId = "ESP32Client-" + String(random(0, 10000));
    if (client.connect(clientId.c_str())) {
      Serial.println("已連線!");
      client.subscribe(topic_led); // 訂閱 LED 控制主題
    } else {
      Serial.print("失敗, rc="); Serial.print(client.state());
      Serial.println(" 5秒後重試");
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  dht.begin();
  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  // 每 5 秒定時發送一次溫濕度數據
  unsigned long now = millis();
  if (now - lastMsg > 5000) {
    lastMsg = now;
    float h = dht.readHumidity();
    float t = dht.readTemperature();

    if (!isnan(h) && !isnan(t)) {
      client.publish(topic_temp, String(t).c_str());
      client.publish(topic_hum, String(h).c_str());
      Serial.printf("已上傳 - 溫度: %.1f °C, 濕度: %.1f %%\n", t, h);
    }
  }
}



Node-Red +SQLite <範例2>

 Node-Red +SQLite <範例2> 直接共用原本的 student.db 設定,不需建立新資料庫檔案。 [{"id":"in_gr_subject_v6","type":"ui_dro...