2026年4月22日 星期三

ESP32 and Node-RED with MQTT (Publish and Subscribe)

 

ESP32 and Node-RED with MQTT (Publish and Subscribe)

參考來源  https://randomnerdtutorials.com/esp8266-and-node-red-with-mqtt/



mqtt_server = "broker.mqtt-dashboard.com"

client.publish("alex9ufo/Thinkitive/temp"

client.publish("alex9ufo/Thinkitive/hum"

client.subscribe("alex9ufo/room/lamp");








WOKWI ESP32程式

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

// Uncomment one of the lines bellow for whatever DHT sensor type you're using!
#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321
const int DHT_PIN = 15;  // DHT 感測器的連接 PIN
const int lamp = 4;
DHTesp dht;
const char* ssid = "Wokwi-GUEST"; // WiFi SSID
const char* password = ""; // WiFi 密碼
const char* mqtt_server = "broker.mqtt-dashboard.com"; // MQTT 服務器地址

WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastMsg = 0; // 記錄上一次消息發送的時間
float temp = 0; // 溫度變量
float hum = 0; // 濕度變量

// 設置 WiFi 連接
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

// MQTT 消息回調函數
// This function is executed when some device publishes a message to a topic that your ESP8266 is subscribed to
// Change the function below to add logic to your program, so when a device publishes a message to a topic that
// your ESP8266 is subscribed you can actually do something
//==============================================================
void callback(char* topic, byte* payload, unsigned int length) {
  payload[length] = '\0';
  String message = String((char*)payload);
  Serial.print("Message arrived in topic: ");
  Serial.println(topic);
  Serial.print("Message: ");
  Serial.println( message);

  if (String(topic) == "alex9ufo/room/lamp") {

  if (message == "on")
    digitalWrite(lamp, HIGH); // LOW to activate

  if (message == "off")
    digitalWrite(lamp, LOW); // LOW to activate
  Serial.println();
  Serial.println("-----------------------");
  }
}
//==============================================================

// This functions reconnects your ESP8266 to your MQTT broker
// Change the function below if you want to subscribe to more topics with your ESP8266

// 重連 MQTT 服務器
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    String clientId = "ESP32Client-";
    clientId += String(random(0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("Connected");
     
      // 關鍵修正:必須訂閱你想要接收的主題
      client.subscribe("alex9ufo/room/lamp");
     
      // 這是您原本訂閱的主題(如果沒用到可以移除)
      client.subscribe("alex9ufo/ThinkIOT/Subscribe");
     
      client.publish("alex9ufo/ThinkIOT/Publish", "Welcome");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

// 初始化設置
void setup() {
  pinMode(lamp, OUTPUT);
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
  dht.setup(DHT_PIN, DHTesp::DHT22); // 初始化 DHT22
}

// 主循環
void loop() {
  // 如果未連接 MQTT,則嘗試重新連接
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  unsigned long now = millis();
  if (now - lastMsg > 5000) { // 每 2 秒發布一次數據
    lastMsg = now;
    TempAndHumidity  data = dht.getTempAndHumidity();

    String temp = String(data.temperature, 2);
    client.publish("alex9ufo/Thinkitive/temp", temp.c_str()); // 發布溫度數據
    String hum = String(data.humidity, 1);
    client.publish("alex9ufo/Thinkitive/hum", hum.c_str());   // 發布濕度數據

    Serial.print("Temperature: ");
    Serial.println(temp);
    Serial.print("Humidity: ");
    Serial.println(hum);
  }
}

Node-Red 程式

[{"id":"3322eee9576783df","type":"mqtt in","z":"4b78f7e5d13e6604","name":"濕度","topic":"alex9ufo/Thinkitive/hum","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":120,"wires":[["c955bb867c3764eb"]]},{"id":"e10e2c7d299e3c2c","type":"mqtt out","z":"4b78f7e5d13e6604","name":"燈泡","topic":"alex9ufo/room/lamp","qos":"1","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"b9efc827e98bf7f9","x":410,"y":180,"wires":[]},{"id":"df3ef8083edb59a3","type":"mqtt in","z":"4b78f7e5d13e6604","name":"溫度","topic":"alex9ufo/Thinkitive/temp","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":60,"wires":[["88fc71e9f52b3d9e"]]},{"id":"4619b3053d03f1a8","type":"ui_switch","z":"4b78f7e5d13e6604","name":"","label":"switch","tooltip":"","group":"3ac0cc1879cac657","order":0,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"topic","topicType":"msg","style":"","onvalue":"on","onvalueType":"str","onicon":"","oncolor":"","offvalue":"off","offvalueType":"str","officon":"","offcolor":"","animate":false,"className":"","x":230,"y":180,"wires":[["e10e2c7d299e3c2c"]]},{"id":"88fc71e9f52b3d9e","type":"ui_chart","z":"4b78f7e5d13e6604","name":"溫度","group":"9cb87f6334be2cce","order":1,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"step","nodata":"","dot":false,"ymin":"0","ymax":"40","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":410,"y":60,"wires":[[]]},{"id":"c955bb867c3764eb","type":"ui_gauge","z":"4b78f7e5d13e6604","name":"濕度'","group":"9cb87f6334be2cce","order":2,"width":0,"height":0,"gtype":"gage","title":"Humidity","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":410,"y":120,"wires":[]},{"id":"b9efc827e98bf7f9","type":"mqtt-broker","name":"broker.mqtt-dashboard.com","broker":"broker.mqtt-dashboard.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"3ac0cc1879cac657","type":"ui_group","name":"Lamp","tab":"0c1ba72ccee260d2","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"9cb87f6334be2cce","type":"ui_group","name":"Sensor","tab":"0c1ba72ccee260d2","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"0c1ba72ccee260d2","type":"ui_tab","name":"Room","icon":"dashboard","disabled":false,"hidden":false}]

Node Red MQTT

Node Red MQTT

https://hackmd.io/@joe94113/node-red_mqtt_esp32_and_open_api




























wokwi程式 修改mqtt_server = "broker.mqtt-dashboard.com"; // MQTT 服務器地址

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

const int DHT_PIN = 15;  // DHT 感測器的連接 PIN
DHTesp dht;
const char* ssid = "Wokwi-GUEST"; // WiFi SSID
const char* password = ""; // WiFi 密碼
const char* mqtt_server = "broker.mqtt-dashboard.com"; // MQTT 服務器地址

WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastMsg = 0; // 記錄上一次消息發送的時間
float temp = 0; // 溫度變量
float hum = 0; // 濕度變量

// 設置 WiFi 連接
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  // 等待 WiFi 連接
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  randomSeed(micros());

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

// MQTT 消息回調函數
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
}

// 重連 MQTT 服務器
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    String clientId = "ESP32Client-";
    clientId += String(random(0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("Connected");
      client.publish("/ThinkIOT/Publish", "Welcome");
      client.subscribe("/ThinkIOT/Subscribe");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

// 初始化設置
void setup() {
  pinMode(2, OUTPUT);    
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
  dht.setup(DHT_PIN, DHTesp::DHT22); // 初始化 DHT22
}

// 主循環
void loop() {
  // 如果未連接 MQTT,則嘗試重新連接
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  unsigned long now = millis();
  if (now - lastMsg > 2000) { // 每 2 秒發布一次數據
    lastMsg = now;
    TempAndHumidity  data = dht.getTempAndHumidity();

    String temp = String(data.temperature, 2);
    client.publish("/Thinkitive/temp", temp.c_str()); // 發布溫度數據
    String hum = String(data.humidity, 1);
    client.publish("/Thinkitive/hum", hum.c_str());   // 發布濕度數據

    Serial.print("Temperature: ");
    Serial.println(temp);
    Serial.print("Humidity: ");
    Serial.println(hum);
  }
}



看到這個提示別擔心,這通常是因為瀏覽器基於安全性考量暫時關閉了腳本功能。只要幾個簡單的步驟就能重新開啟。

以下是針對目前主流瀏覽器的開啟教學:


1. Google Chrome (電腦版)

  1. 點擊瀏覽器右上角的 「三個點」 圖示,選擇 「設定」

  2. 在左側選單點擊 「隱私權和安全性」

  3. 點擊 「網站設定」

  4. 向下滑動找到「內容」區塊,點擊 「JavaScript」

  5. 勾選 「網站可以使用 JavaScript」

  6. 回到原本的網頁並 重新整理 (F5)


2. Microsoft Edge

  1. 點擊右上角的 「三個點」 圖示,選擇 「設定」

  2. 在左側選單選擇 「Cookie 和網站權限」

  3. 在「所有權限」清單中找到並點擊 「JavaScript」

  4. 「允許 (建議)」 的開關切換為 開啟狀態

  5. 重新整理網頁。



Node-RED 中安裝 MQTT broker 的步驟,並可以開始使用 MQTT 進行消息傳輸

[{"id":"073cac6e5e434d41","type":"mqtt out","z":"de93e29c07cca82f","name":"","topic":"sensors/livingroom/temp","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"841df58d.ee5e98","x":310,"y":180,"wires":[]},{"id":"ceb8068819adc757","type":"inject","z":"de93e29c07cca82f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"22","payloadType":"num","x":110,"y":180,"wires":[["073cac6e5e434d41"]]},{"id":"5fc97feb1b566004","type":"mqtt in","z":"de93e29c07cca82f","name":"","topic":"sensors/livingroom/temp","qos":"2","datatype":"auto-detect","broker":"841df58d.ee5e98","nl":false,"rap":false,"inputs":0,"x":150,"y":260,"wires":[["6c6dc58f97ab8f86"]]},{"id":"6c6dc58f97ab8f86","type":"debug","z":"de93e29c07cca82f","name":"","active":true,"console":"false","complete":"false","x":350,"y":260,"wires":[]},{"id":"bcca028dd9cde448","type":"debug","z":"de93e29c07cca82f","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":40,"wires":[]},{"id":"20f600cc71d280e5","type":"debug","z":"de93e29c07cca82f","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":100,"wires":[]},{"id":"a4ee27d876e6c883","type":"aedes broker","z":"de93e29c07cca82f","name":"","mqtt_port":"1884","mqtt_ws_bind":"port","mqtt_ws_port":"","mqtt_ws_path":"","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","persistence_bind":"memory","dburl":"","persist_to_file":false,"usetls":false,"x":130,"y":80,"wires":[["bcca028dd9cde448"],["20f600cc71d280e5"]]},{"id":"841df58d.ee5e98","type":"mqtt-broker","name":"","broker":"broker.hivemq.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"compatmode":false,"protocolVersion":"4","keepalive":"15","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}]


Node-RED 中配置 MQTT 並訂閱 ESP32 發送資料

[{"id":"06616414d9f0a6f3","type":"aedes broker","z":"3b843e74e7ab6ef8","name":"","mqtt_port":"1884","mqtt_ws_bind":"port","mqtt_ws_port":"","mqtt_ws_path":"","cert":"","key":"","certname":"","keyname":"","dburl":"","usetls":false,"x":130,"y":80,"wires":[["866a2384f6992b64"],["b013bf4fca5a488e"]]},{"id":"866a2384f6992b64","type":"debug","z":"3b843e74e7ab6ef8","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":40,"wires":[]},{"id":"b013bf4fca5a488e","type":"debug","z":"3b843e74e7ab6ef8","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":100,"wires":[]},{"id":"ceb3112cd35e970e","type":"mqtt in","z":"3b843e74e7ab6ef8","name":"","topic":"/Thinkitive/temp","qos":"0","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":120,"y":180,"wires":[["7c6bdec976aab81d"]]},{"id":"a51c937face296dc","type":"mqtt in","z":"3b843e74e7ab6ef8","name":"","topic":"/Thinkitive/hum","qos":"0","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":120,"y":240,"wires":[["f06f1d6381b63b69"]]},{"id":"7c6bdec976aab81d","type":"ui_gauge","z":"3b843e74e7ab6ef8","name":"TRMP","group":"70d1d491db52e4fe","order":7,"width":6,"height":4,"gtype":"gage","title":"ESP32溫度","label":"","format":"{{value}}","min":0,"max":"150","colors":["#00b500","#e6e600","#ec2727"],"seg1":"","seg2":"","diff":false,"className":"","x":330,"y":180,"wires":[]},{"id":"f06f1d6381b63b69","type":"ui_gauge","z":"3b843e74e7ab6ef8","name":"","group":"70d1d491db52e4fe","order":12,"width":6,"height":4,"gtype":"gage","title":"ESP32濕度","label":"","format":"{{value}}","min":0,"max":"60","colors":["#15cb15","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":350,"y":240,"wires":[]},{"id":"b9efc827e98bf7f9","type":"mqtt-broker","name":"broker.mqtt-dashboard.com","broker":"broker.mqtt-dashboard.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"70d1d491db52e4fe","type":"ui_group","name":"esp32圖表","tab":"2156c1cf7f043d72","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"2156c1cf7f043d72","type":"ui_tab","name":"天氣圖表","icon":"dashboard","disabled":false,"hidden":false}]

Node-RED 中連接到一個公開的氣象資料 API,並抓取各地區的溫度等氣象資訊

[{"id":"84974e3b6b184603","type":"aedes broker","z":"ee949a679aeaac8d","name":"","mqtt_port":"1883","mqtt_ws_bind":"port","mqtt_ws_port":"","mqtt_ws_path":"","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","persistence_bind":"memory","dburl":"","persist_to_file":false,"usetls":false,"x":110,"y":60,"wires":[["d3d0db98037eb5a0"],["221c64cb507138ba"]]},{"id":"d3d0db98037eb5a0","type":"debug","z":"ee949a679aeaac8d","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":320,"y":40,"wires":[]},{"id":"221c64cb507138ba","type":"debug","z":"ee949a679aeaac8d","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":320,"y":80,"wires":[]},{"id":"7a50625e9de576f5","type":"mqtt out","z":"ee949a679aeaac8d","name":"","topic":"sensors/livingroom/temp","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"9aa0e63a7fe55694","x":590,"y":160,"wires":[]},{"id":"bf5d80bdee7081a3","type":"mqtt in","z":"ee949a679aeaac8d","name":"","topic":"sensors/livingroom/temp","qos":"2","datatype":"auto-detect","broker":"9aa0e63a7fe55694","nl":false,"rap":false,"inputs":0,"x":130,"y":220,"wires":[["b5a377175f3a3413","80fc5a39dcdee086","90567bb2394770f0","a54071ebf5b7899e","8210955ad042a18f"]]},{"id":"b5a377175f3a3413","type":"debug","z":"ee949a679aeaac8d","name":"mqTT_In","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":420,"y":220,"wires":[]},{"id":"ea0a92334afe9e73","type":"ui_dropdown","z":"ee949a679aeaac8d","name":"","label":"選擇地區","tooltip":"","place":"點擊選擇","group":"f1ec0c040bdd2739","order":37,"width":6,"height":1,"passthru":true,"multiple":false,"options":[{"label":"桃園","value":"桃園","type":"str"},{"label":"苗栗","value":"苗栗","type":"str"},{"label":"南投","value":"南投","type":"str"},{"label":"彰化","value":"埤頭","type":"str"},{"label":"台中","value":"中坑","type":"str"},{"label":"雲林","value":"斗六","type":"str"},{"label":"嘉義","value":"水上","type":"str"},{"label":"台南","value":"安南","type":"str"},{"label":"屏東","value":"九如","type":"str"},{"label":"台東","value":"延平","type":"str"},{"label":"花蓮","value":"豐濱","type":"str"},{"label":"宜蘭","value":"三星","type":"str"},{"label":"高雄","value":"三民","type":"str"},{"label":"台北","value":"平等","type":"str"}],"payload":"","topic":"topic","topicType":"msg","className":"","x":80,"y":160,"wires":[["1610a92a9b0c39de"]]},{"id":"1610a92a9b0c39de","type":"http request","z":"ee949a679aeaac8d","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=CWB-40C25FFF-1224-4250-B9D9-3735AAE17DBF&limit=10&format=JSON&StationName={{payload}}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":240,"y":160,"wires":[["aa1c124b347003ba"]]},{"id":"aa1c124b347003ba","type":"json","z":"ee949a679aeaac8d","name":"","property":"payload","action":"","pretty":false,"x":390,"y":160,"wires":[["7a50625e9de576f5"]]},{"id":"80fc5a39dcdee086","type":"ui_gauge","z":"ee949a679aeaac8d","name":"","group":"f1ec0c040bdd2739","order":39,"width":6,"height":4,"gtype":"gage","title":"氣溫","label":"度","format":"{{payload.records.Station[0].WeatherElement.AirTemperature}}","min":0,"max":"40","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":410,"y":260,"wires":[]},{"id":"90567bb2394770f0","type":"ui_gauge","z":"ee949a679aeaac8d","name":"","group":"f1ec0c040bdd2739","order":40,"width":6,"height":4,"gtype":"gage","title":"風速","label":"m/s","format":"{{payload.records.Station[0].WeatherElement.WindSpeed}}","min":0,"max":"10","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":410,"y":300,"wires":[]},{"id":"a54071ebf5b7899e","type":"ui_gauge","z":"ee949a679aeaac8d","name":"","group":"f1ec0c040bdd2739","order":41,"width":6,"height":4,"gtype":"gage","title":"相對濕度","label":"%","format":"{{payload.records.Station[0].WeatherElement.RelativeHumidity}}","min":"50","max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":420,"y":340,"wires":[]},{"id":"8210955ad042a18f","type":"ui_gauge","z":"ee949a679aeaac8d","name":"","group":"f1ec0c040bdd2739","order":42,"width":6,"height":4,"gtype":"gage","title":"空氣壓力","label":"atm","format":"{{payload.records.Station[0].WeatherElement.AirPressure}}","min":"900","max":"1100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":420,"y":380,"wires":[]},{"id":"9aa0e63a7fe55694","type":"mqtt-broker","name":"","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"f1ec0c040bdd2739","type":"ui_group","name":"天氣","tab":"2156c1cf7f043d72","order":1,"disp":true,"width":12,"collapse":false,"className":""},{"id":"2156c1cf7f043d72","type":"ui_tab","name":"天氣圖表","icon":"dashboard","disabled":false,"hidden":false}]

兩個圖表一起顯示 Node-Red

[{"id":"229d489a86aaea93","type":"mqtt out","z":"8c31de38cd0a6981","name":"","topic":"sensors/livingroom/temp","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"78f412356543fd12","x":630,"y":220,"wires":[]},{"id":"adaec6df5a6580fb","type":"mqtt in","z":"8c31de38cd0a6981","name":"","topic":"sensors/livingroom/temp","qos":"2","datatype":"auto-detect","broker":"78f412356543fd12","nl":false,"rap":false,"inputs":0,"x":630,"y":260,"wires":[["ae695b84fba5fc23","9ed7c60684d641e1","518b5e5ea5836bdd","61ae7765a9f9edf5","0b113fc7b52cbfdb"]]},{"id":"ae695b84fba5fc23","type":"debug","z":"8c31de38cd0a6981","name":"mqTT_In","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":880,"y":200,"wires":[]},{"id":"e2e6628b7bde7a1b","type":"aedes broker","z":"8c31de38cd0a6981","name":"","mqtt_port":1883,"mqtt_ws_bind":"port","mqtt_ws_port":"","mqtt_ws_path":"","cert":"","key":"","certname":"","keyname":"","dburl":"","usetls":false,"x":150,"y":60,"wires":[["f0ae9594e8fe0786"],["0243ee57c33cba12"]]},{"id":"f0ae9594e8fe0786","type":"debug","z":"8c31de38cd0a6981","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":360,"y":40,"wires":[]},{"id":"0243ee57c33cba12","type":"debug","z":"8c31de38cd0a6981","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":360,"y":80,"wires":[]},{"id":"f50edc83dba4da17","type":"ui_dropdown","z":"8c31de38cd0a6981","name":"","label":"選擇地區","tooltip":"","place":"點擊選擇","group":"f1ec0c040bdd2739","order":2,"width":6,"height":1,"passthru":true,"multiple":false,"options":[{"label":"桃園","value":"桃園","type":"str"},{"label":"苗栗","value":"苗栗","type":"str"},{"label":"南投","value":"南投","type":"str"},{"label":"彰化","value":"埤頭","type":"str"},{"label":"台中","value":"中坑","type":"str"},{"label":"雲林","value":"斗六","type":"str"},{"label":"嘉義","value":"水上","type":"str"},{"label":"台南","value":"安南","type":"str"},{"label":"屏東","value":"九如","type":"str"},{"label":"台東","value":"延平","type":"str"},{"label":"花蓮","value":"豐濱","type":"str"},{"label":"宜蘭","value":"三星","type":"str"},{"label":"高雄","value":"三民","type":"str"},{"label":"台北","value":"平等","type":"str"}],"payload":"","topic":"topic","topicType":"msg","className":"","x":120,"y":220,"wires":[["14e3088564d30c0c","cb6bc9284f648b45"]]},{"id":"e51d55930ce8853e","type":"json","z":"8c31de38cd0a6981","name":"","property":"payload","action":"","pretty":false,"x":430,"y":220,"wires":[["229d489a86aaea93"]]},{"id":"9ed7c60684d641e1","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"f1ec0c040bdd2739","order":6,"width":5,"height":3,"gtype":"gage","title":"氣溫","label":"度","format":"{{payload.records.Station[0].WeatherElement.AirTemperature}}","min":0,"max":"40","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":870,"y":240,"wires":[]},{"id":"518b5e5ea5836bdd","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"f1ec0c040bdd2739","order":4,"width":5,"height":3,"gtype":"gage","title":"風速","label":"m/s","format":"{{payload.records.Station[0].WeatherElement.WindSpeed}}","min":0,"max":"10","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":870,"y":280,"wires":[]},{"id":"61ae7765a9f9edf5","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"f1ec0c040bdd2739","order":10,"width":5,"height":3,"gtype":"gage","title":"相對濕度","label":"%","format":"{{payload.records.Station[0].WeatherElement.RelativeHumidity}}","min":"50","max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":880,"y":320,"wires":[]},{"id":"0b113fc7b52cbfdb","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"f1ec0c040bdd2739","order":12,"width":5,"height":3,"gtype":"gage","title":"空氣壓力","label":"atm","format":"{{payload.records.Station[0].WeatherElement.AirPressure}}","min":"900","max":"1100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":880,"y":360,"wires":[]},{"id":"14e3088564d30c0c","type":"debug","z":"8c31de38cd0a6981","name":"debug 22","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":280,"y":140,"wires":[]},{"id":"d17279b29e0e15aa","type":"http in","z":"8c31de38cd0a6981","name":"","url":"/weather-app","method":"get","upload":false,"swaggerDoc":"","x":150,"y":320,"wires":[["19625023b6920da9"]]},{"id":"19625023b6920da9","type":"template","z":"8c31de38cd0a6981","name":"JS版本氣象html","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Weather Display</title>\n    <style>\n        body {\n            font-family: 'Arial', sans-serif;\n            margin: 0;\n            padding: 0;\n            background-color: #f4f4f4;\n            color: #333;\n        }\n\n        .weather-container {\n            margin-top: 20px;\n            background-color: white;\n            padding: 20px;\n            border-radius: 8px;\n            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n            width: 300px; /* 固定寬度 */\n        }\n\n        #stationSelect {\n            padding: 8px;\n            border-radius: 4px;\n            border: 1px solid #ddd;\n            margin-top: 20px;\n        }\n\n        h1, p {\n            margin: 10px 0;\n        }\n\n        #city {\n            font-size: 1.5em;\n            color: #0275d8;\n        }\n\n        #weather {\n            font-size: 1.2em;\n            color: #5cb85c;\n        }\n\n        #temperature {\n            font-size: 1.4em;\n            color: #f0ad4e;\n        }\n\n        /* 響應式布局 */\n        @media (max-width: 600px) {\n            .weather-container {\n                width: 90%;\n                margin: 20px auto;\n            }\n        }\n\n    </style>\n</head>\n<body>\n    <div>\n        <label for=\"stationSelect\">Choose a station:</label>\n        <select id=\"stationSelect\">\n            <option value=\"桃園\">桃園</option>\n            <option value=\"苗栗\">苗栗</option>\n            <option value=\"南投\">南投</option>\n            <option value=\"埤頭\">彰化</option>\n            <option value=\"中坑\">台中</option>\n            <option value=\"安南\">台南</option>\n            <!-- 更多選項 -->\n        </select>\n    </div>\n    <div class=\"weather-container\">\n        <h1 id=\"city\"></h1>\n        <p id=\"weather\"></p>\n        <p id=\"temperature\"></p>\n    </div>\n    <script>\n        document.getElementById('stationSelect').addEventListener('change', (event) => {\n            fetchWeatherData(event.target.value);\n        });\n\n        function fetchWeatherData(stationName) {\n            const apiUrl = `https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=your-token&limit=10&format=JSON&StationName=${stationName}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName`;\n\n            fetch(apiUrl)\n                .then(response => {\n                    if (!response.ok) {\n                        throw new Error('Network response was not ok');\n                    }\n                    return response.json();\n                })\n                .then(data => {\n                    console.log(data)\n                    updateWeatherDisplay(data);\n                })\n                .catch(error => {\n                    console.error('There has been a problem with your fetch operation:', error);\n                });\n        }\n\n        function updateWeatherDisplay(data) {\n            const temperature = data.records.Station[0]['WeatherElement']['AirTemperature'];\n            document.getElementById('temperature').textContent = temperature;\n            const weather = data.records.Station[0]['WeatherElement']['Weather'];\n            document.getElementById('weather').textContent = weather;\n            const city = data.records.Station[0]['GeoInfo']['CountyName'];\n            document.getElementById('city').textContent = city;\n        }\n\n        // 初始化,載入頁面時自動加載預設站點的天氣數據\n        fetchWeatherData(document.getElementById('stationSelect').value);\n\n    </script>\n</body>\n</html>","output":"str","x":340,"y":320,"wires":[["1e0d037f5b8791d1"]]},{"id":"1e0d037f5b8791d1","type":"http response","z":"8c31de38cd0a6981","name":"","statusCode":"","headers":{},"x":510,"y":320,"wires":[]},{"id":"b9319af08c19eec4","type":"mqtt in","z":"8c31de38cd0a6981","name":"","topic":"/Thinkitive/temp","qos":"0","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":140,"y":400,"wires":[["da1f699dede14640"]]},{"id":"9aca9e818558523e","type":"mqtt in","z":"8c31de38cd0a6981","name":"","topic":"/Thinkitive/hum","qos":"0","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":140,"y":460,"wires":[["fff84846812b56a8"]]},{"id":"da1f699dede14640","type":"ui_gauge","z":"8c31de38cd0a6981","name":"TRMP","group":"70d1d491db52e4fe","order":1,"width":0,"height":0,"gtype":"gage","title":"ESP32溫度","label":"","format":"{{value}}","min":0,"max":"150","colors":["#00b500","#e6e600","#ec2727"],"seg1":"","seg2":"","diff":false,"className":"","x":350,"y":400,"wires":[]},{"id":"fff84846812b56a8","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"70d1d491db52e4fe","order":2,"width":0,"height":0,"gtype":"gage","title":"ESP32濕度","label":"","format":"{{value}}","min":0,"max":"60","colors":["#15cb15","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":370,"y":460,"wires":[]},{"id":"cb6bc9284f648b45","type":"http request","z":"8c31de38cd0a6981","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=CWB-40C25FFF-1224-4250-B9D9-3735AAE17DBF&limit=10&format=JSON&StationName={{payload}}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":280,"y":220,"wires":[["e51d55930ce8853e"]]},{"id":"78f412356543fd12","type":"mqtt-broker","name":"","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"f1ec0c040bdd2739","type":"ui_group","name":"天氣","tab":"2156c1cf7f043d72","order":1,"disp":true,"width":12,"collapse":false,"className":""},{"id":"b9efc827e98bf7f9","type":"mqtt-broker","name":"broker.mqtt-dashboard.com","broker":"broker.mqtt-dashboard.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"70d1d491db52e4fe","type":"ui_group","name":"esp32圖表","tab":"2156c1cf7f043d72","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"2156c1cf7f043d72","type":"ui_tab","name":"天氣圖表","icon":"dashboard","disabled":false,"hidden":false}]

2026年4月19日 星期日

2026 作業2 RFID+Node-Red+Python+SQLite 練習 (修正版 利用wokwi 內部元件)

2026 作業2  RFID+Node-Red+Python+SQLite 練習 (修正版  利用wokwi 內部元件)

使用 wokwi 上 的 元件   (原先尚未有)




硬體線路

 







不需要 自行建立元件  rfid-rc522.chip.json  rfid-rc522.chip.c   

其他的參考  原來的程式




SQLite 新增 (Create)、查詢 (Read)、更新 (Update)、刪除 (Delete) 資料

SQLite 新增 (Create)、查詢 (Read)、更新 (Update)、刪除 (Delete) 資料

SQLite 是一種輕量級、檔案型(單一檔案)的關聯式資料庫,非常適合嵌入式系統、行動應用程式(Android/iOS)和桌面軟體。其基本功能包含建立資料庫、建立資料表、新增 (Create)、查詢 (Read)、更新 (Update)、刪除 (Delete) 資料,簡稱 CRUD
以下是使用 Python sqlite3 模組的具體基本功能範例:
1. 建立/連接資料庫
SQLite 不需要啟動伺服器,直接開啟檔案即可。
python
import sqlite3

# 連接資料庫,若檔案不存在會自動建立
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
2. 建立資料表 (Create Table)
python
# 建立一個使用者資料表
cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        age INTEGER
    )
''')
conn.commit() # 提交事務
3. 新增資料 (Create)
python
# 插入單筆資料
cursor.execute("INSERT INTO users (name, age) VALUES ('Alice', 25)")

# 插入多筆資料
users = [('Bob', 30), ('Charlie', 35)]
cursor.executemany("INSERT INTO users (name, age) VALUES (?, ?)", users)

conn.commit()
4. 查詢資料 (Read)
python
# 查詢所有資料
cursor.execute("SELECT * FROM users")
rows = cursor.fetchall()
for row in rows:
    print(row)

# 查詢特定條件
cursor.execute("SELECT * FROM users WHERE age > ?", (30,))
print(cursor.fetchall())
5. 更新資料 (Update)
python
# 更新 Bob 的年齡
cursor.execute("UPDATE users SET age = ? WHERE name = ?", (31, 'Bob'))
conn.commit()
6. 刪除資料 (Delete)
python
# 刪除 Alice
cursor.execute("DELETE FROM users WHERE name = ?", ('Alice',))
conn.commit()
7. 關閉連接
python
conn.close()
常用工具與資源
  • 瀏覽器擴充功能: SQLite Browser 可用於查看和管理 SQLite 資料庫檔案。
  • 特點: 遵守 ACID 原則,檔案單一化,便於備份。


Node-RED 中使用 SQLite,通常會透過 node-red-node-sqlite 這個節點來達成。

以下是如何在 Node-RED 中實現 CRUD 操作的完整指南。


1. 環境準備

首先,你需要在 Node-RED 的「選單」>「管理調色盤」中搜尋並安裝: node-red-node-sqlite

安裝後,你會看到一個 sqlite 節點。你需要點擊該節點並設定一個 Database 檔案路徑(例如:/data/mydb.db)。


2. 建立資料表 (Setup)

在執行 CRUD 之前,我們需要一張表。通常會使用一個 Inject 節點觸發一個 Function 節點。

SQL 指令:

SQL
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    age INTEGER
)

3. CRUD 操作範例

你可以將 msg.topic 設為 SQL 語句,然後傳遞給 SQLite 節點。

C - 新增 (Create)

將資料寫入資料庫。

  • Function 節點內容:

    JavaScript
    msg.topic = "INSERT INTO users (name, age) VALUES ('Alice', 25)";
    return msg;
    

R - 查詢 (Read)

取出資料庫中的資料。

  • Function 節點內容:

    JavaScript
    msg.topic = "SELECT * FROM users WHERE age > 20";
    return msg;
    

    結果會以 Array (陣列) 形式出現在 msg.payload

U - 更新 (Update)

修改現有資料。

  • Function 節點內容:

    JavaScript
    msg.topic = "UPDATE users SET age = 26 WHERE name = 'Alice'";
    return msg;
    

D - 刪除 (Delete)

移除資料。

  • Function 節點內容:

    JavaScript
    msg.topic = "DELETE FROM users WHERE id = 1";
    return msg;
    

4. 進階技巧:預防 SQL 注入 (Prepared Statements)

直接在字串中拼接變數是很危險的。SQLite 節點支援使用陣列來帶入參數,這樣更安全且專業。

安全做法範例:

  1. Function 節點:

    JavaScript
    msg.topic = "INSERT INTO users (name, age) VALUES (?, ?)";
    msg.payload = ["Bob", 30]; // 依序對應問號
    return msg;
    

總結流程圖

  1. Inject 節點(觸發動作)

  2. Function 節點(撰寫 msg.topic 的 SQL 語句)

  3. SQLite 節點(執行資料庫讀寫)

  4. Debug 節點(查看執行結果)



[{"id":"60dcc3509d8bcea7","type":"inject","z":"2ec92e8e481b31c7","name":"建立資料表 (Setup)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":120,"wires":[["a8293005df7095ac"]]},{"id":"a8293005df7095ac","type":"function","z":"2ec92e8e481b31c7","name":"SETUP Table function  ","func":"msg.topic=\"CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)\";\nreturn msg;\n\n//SQL 指令:\n//CREATE TABLE IF NOT EXISTS users (\n//    id INTEGER PRIMARY KEY AUTOINCREMENT,\n//    name TEXT,\n//    age INTEGER\n//)\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":120,"wires":[["d517a660eaef169d"]]},{"id":"d517a660eaef169d","type":"sqlite","z":"2ec92e8e481b31c7","mydb":"1cb2040a1b54cb0d","sqlquery":"msg.topic","sql":"","name":"CRUD","x":650,"y":380,"wires":[["a969087543becd87"]]},{"id":"a969087543becd87","type":"debug","z":"2ec92e8e481b31c7","name":"debug  ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":830,"y":380,"wires":[]},{"id":"42bedc9b11726e3f","type":"inject","z":"2ec92e8e481b31c7","name":"新增一筆 (Create)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":200,"wires":[["cd426b5bf1c7752a"]]},{"id":"cd426b5bf1c7752a","type":"function","z":"2ec92e8e481b31c7","name":"Create function  ","func":"msg.topic = \"INSERT INTO users (name, age) VALUES ('Alice', 25)\";\nreturn msg;\n\n//SQL 指令:\n//CREATE TABLE IF NOT EXISTS users (\n//    id INTEGER PRIMARY KEY AUTOINCREMENT,\n//    name TEXT,\n//    age INTEGER\n//)\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":200,"wires":[["d517a660eaef169d"]]},{"id":"3ac57f7d2bbaedac","type":"comment","z":"2ec92e8e481b31c7","name":"資料表Table","info":"CREATE TABLE IF NOT EXISTS users (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n   name TEXT,\n   age INTEGER\n)\n","x":410,"y":80,"wires":[]},{"id":"210afd8e2601a3ce","type":"inject","z":"2ec92e8e481b31c7","name":"查詢 (Read)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":190,"y":360,"wires":[["3d3b9c38c623355c"]]},{"id":"3d3b9c38c623355c","type":"function","z":"2ec92e8e481b31c7","name":"Read function  ","func":"msg.topic = \"SELECT * FROM users WHERE age > 20\";\nreturn msg;\n\n//SQL 指令:\n//CREATE TABLE IF NOT EXISTS users (\n//    id INTEGER PRIMARY KEY AUTOINCREMENT,\n//    name TEXT,\n//    age INTEGER\n//)\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":360,"wires":[["d517a660eaef169d"]]},{"id":"9b8b4d857042e43a","type":"inject","z":"2ec92e8e481b31c7","name":"更新 (Update)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":190,"y":440,"wires":[["39f05041fd414bc1"]]},{"id":"39f05041fd414bc1","type":"function","z":"2ec92e8e481b31c7","name":"Update function  ","func":"msg.topic = \"UPDATE users SET age = 26 WHERE name = 'Alice'\";\nreturn msg;\n\n//SQL 指令:\n//CREATE TABLE IF NOT EXISTS users (\n//    id INTEGER PRIMARY KEY AUTOINCREMENT,\n//    name TEXT,\n//    age INTEGER\n//)\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":440,"wires":[["d517a660eaef169d"]]},{"id":"bf89364e88cea868","type":"inject","z":"2ec92e8e481b31c7","name":"刪除 (Delete)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":190,"y":520,"wires":[["a6d59667ce09088a"]]},{"id":"a6d59667ce09088a","type":"function","z":"2ec92e8e481b31c7","name":"Delete  function  ","func":"msg.topic = \"DELETE FROM users WHERE id = 1\";\nreturn msg;\n\n//SQL 指令:\n//CREATE TABLE IF NOT EXISTS users (\n//    id INTEGER PRIMARY KEY AUTOINCREMENT,\n//    name TEXT,\n//    age INTEGER\n//)\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":520,"wires":[["d517a660eaef169d"]]},{"id":"989491797b337a03","type":"inject","z":"2ec92e8e481b31c7","name":"新增一筆 (Create)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":280,"wires":[["4ac024a48fa44490"]]},{"id":"4ac024a48fa44490","type":"function","z":"2ec92e8e481b31c7","name":"Create function  ","func":"msg.topic = \"INSERT INTO users (name, age) VALUES (?, ?)\";\nmsg.payload = [\"Bob\", 30]; // 依序對應問號\nreturn msg;\n\n//SQL 指令:\n//CREATE TABLE IF NOT EXISTS users (\n//    id INTEGER PRIMARY KEY AUTOINCREMENT,\n//    name TEXT,\n//    age INTEGER\n//)\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":280,"wires":[["d517a660eaef169d"]]},{"id":"9483f811f3a38c69","type":"inject","z":"2ec92e8e481b31c7","name":"DROP (刪除資料表)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":600,"wires":[["cda3a22250276ed5"]]},{"id":"cda3a22250276ed5","type":"function","z":"2ec92e8e481b31c7","name":"DROP function  ","func":"msg.topic = \"DROP TABLE IF EXISTS users\";\nreturn msg;\n\n//SQL 指令:\n//CREATE TABLE IF NOT EXISTS users (\n//    id INTEGER PRIMARY KEY AUTOINCREMENT,\n//    name TEXT,\n//    age INTEGER\n//)\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":600,"wires":[["d517a660eaef169d"]]},{"id":"1cb2040a1b54cb0d","type":"sqlitedb","db":"CRUD.db","mode":"RWC"}]








[{"id":"940528c3e3697601","type":"sqlite","z":"6175eaf79545705a","mydb":"bc23914c8cab28bb","sqlquery":"msg.topic","sql":"","name":"SQLite 資料庫 (test.db)","x":670,"y":160,"wires":[["6c89c25ba1b79a98","c79aea60f978db5e"]]},{"id":"6c89c25ba1b79a98","type":"debug","z":"6175eaf79545705a","name":"SQLite 執行結果","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":900,"y":140,"wires":[]},{"id":"b320e61d0a2d30a5","type":"ui_button","z":"6175eaf79545705a","name":"CREATE (建立資料表)","group":"1963df87ae08178b","order":1,"width":3,"height":1,"passthru":false,"label":"(建立資料表)","tooltip":"建立 my_data 資料表","color":"","bgcolor":"green","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"msg","x":140,"y":80,"wires":[["5943ddf87dc315ac"]]},{"id":"5943ddf87dc315ac","type":"change","z":"6175eaf79545705a","name":"SQL: CREATE (建立)","rules":[{"t":"set","p":"topic","pt":"msg","to":"CREATE TABLE IF NOT EXISTS my_data (id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT, time TEXT, number INTEGER)","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":80,"wires":[["940528c3e3697601","606aeeb344c86ebb"]]},{"id":"f8fdbe63af332cf9","type":"ui_button","z":"6175eaf79545705a","name":"INSERT (插入)","group":"1963df87ae08178b","order":2,"width":3,"height":1,"passthru":false,"label":"INSERT (插入一筆)","tooltip":"插入一筆隨機資料","color":"","bgcolor":"blue","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"msg","x":120,"y":140,"wires":[["3d023e070064238f"]]},{"id":"3d023e070064238f","type":"function","z":"6175eaf79545705a","name":"產生 INSERT SQL","func":"// 獲取目前時間\nconst now = new Date();\n// 格式化日期:YYYY-MM-DD\nconst date = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2) + '-' + ('0' + now.getDate()).slice(-2);\n// 格式化時間:HH:mm:ss\nconst time = ('0' + now.getHours()).slice(-2) + ':' + ('0' + now.getMinutes()).slice(-2) + ':' + ('0' + now.getSeconds()).slice(-2);\n\n// 產生 0 到 100 之間的亂數\nconst randomNumber = Math.floor(Math.random() * 101);\n\n// 建構 INSERT INTO SQL 語句\nmsg.topic = `INSERT INTO my_data (date, time, number) VALUES ('${date}', '${time}', ${randomNumber})`;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":140,"wires":[["940528c3e3697601"]]},{"id":"d61eed9277aa032d","type":"ui_button","z":"6175eaf79545705a","name":"SELECT (查詢)","group":"1963df87ae08178b","order":3,"width":3,"height":1,"passthru":false,"label":"SELECT (查詢)","tooltip":"查詢所有資料並顯示","color":"","bgcolor":"orange","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"msg","x":120,"y":200,"wires":[["0925d7f1e6bf7d57"]]},{"id":"0925d7f1e6bf7d57","type":"change","z":"6175eaf79545705a","name":"SQL: SELECT (查詢)","rules":[{"t":"set","p":"topic","pt":"msg","to":"SELECT id, date, time, number FROM my_data","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":200,"wires":[["940528c3e3697601"]]},{"id":"c79aea60f978db5e","type":"ui_table","z":"6175eaf79545705a","group":"216515e83bf6ead4","name":"資料表格 (ui-table)","order":1,"width":10,"height":8,"columns":[{"field":"id","title":"序號","width":"40%","align":"left","formatter":"html","formatterParams":{"target":"_blank"}},{"field":"date","title":"日期","width":"20%","align":"left","formatter":"html","formatterParams":{"target":"_blank"}},{"field":"time","title":"時間","width":"20%","align":"left","formatter":"html","formatterParams":{"target":"_blank"}},{"field":"number","title":"亂數 (0-100)","width":"20%","align":"left","formatter":"html","formatterParams":{"target":"_blank"}}],"outputs":0,"cts":false,"x":910,"y":200,"wires":[]},{"id":"2650d6e9ad63a5c5","type":"ui_button","z":"6175eaf79545705a","name":"DELETE (刪除)","group":"1963df87ae08178b","order":4,"width":3,"height":1,"passthru":false,"label":"刪除 number < 50  ","tooltip":"刪除 number < 50 的紀錄,並自動刷新表格","color":"","bgcolor":"red","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"msg","x":120,"y":300,"wires":[["14f91b0687ef609b"]]},{"id":"14f91b0687ef609b","type":"change","z":"6175eaf79545705a","name":"SQL: DELETE (刪除)","rules":[{"t":"set","p":"topic","pt":"msg","to":"DELETE FROM my_data WHERE number < 50","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":300,"wires":[["940528c3e3697601","0925d7f1e6bf7d57"]]},{"id":"0c96e812e72f3f84","type":"ui_button","z":"6175eaf79545705a","name":"DROP(銷毀資料表)","group":"1963df87ae08178b","order":5,"width":3,"height":1,"passthru":false,"label":"DROP(銷毀資料表)","tooltip":"刪除 my_data 資料表 (請謹慎操作)","color":"","bgcolor":"darkred","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"msg","x":130,"y":400,"wires":[["ebd49403e7375a09"]]},{"id":"ebd49403e7375a09","type":"change","z":"6175eaf79545705a","name":"SQL: DROP (銷毀)","rules":[{"t":"set","p":"topic","pt":"msg","to":"DROP TABLE IF EXISTS my_data","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":400,"wires":[["940528c3e3697601"]]},{"id":"a0e788235c2d259a","type":"comment","z":"6175eaf79545705a","name":"","info":"DELETE FROM my_data WHERE number < 50","x":330,"y":340,"wires":[]},{"id":"51c5c320038f5c68","type":"comment","z":"6175eaf79545705a","name":"","info":"DROP TABLE IF EXISTS my_data","x":330,"y":440,"wires":[]},{"id":"bac6457e8cc2c3c5","type":"comment","z":"6175eaf79545705a","name":"","info":"CREATE TABLE IF NOT EXISTS my_data \n(\nid INTEGER PRIMARY KEY AUTOINCREMENT, \ndate TEXT, \ntime TEXT, \nnumber INTEGER\n)","x":370,"y":40,"wires":[]},{"id":"d005ea88d8cac469","type":"ui_button","z":"6175eaf79545705a","name":"刪除","group":"1963df87ae08178b","order":8,"width":3,"height":1,"passthru":false,"label":"DELETE by ID","tooltip":"執行單筆 ID 刪除,並刷新表格","color":"","bgcolor":"darkred","className":"","icon":"fa-eraser","payload":"1","payloadType":"num","topic":"","topicType":"str","x":90,"y":540,"wires":[["24cb44a4c6383834"]]},{"id":"9b934b2d46e35ffc","type":"function","z":"6175eaf79545705a","name":"function  (DELETE ID SQL)","func":"// 這個節點從 Dashboard 的輸入和按鈕接收訊息\n// 步驟一:儲存輸入的 ID\n// 如果 msg.topic 是 'deleteId' (來自輸入框),則儲存 payload (ID)\nif (msg.topic === 'deleteId') {\n    flow.set('deleteID', msg.payload);\n    // 不繼續傳遞,因為還沒按刪除按鈕\n    return msg;\n}\n\nreturn msg; // 其他情況不執行操作","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":480,"wires":[[]]},{"id":"333ccf32fcdfbf92","type":"ui_text_input","z":"6175eaf79545705a","name":"刪除一筆 id=","label":"刪除 ID = ","tooltip":"","group":"1963df87ae08178b","order":6,"width":3,"height":1,"passthru":true,"mode":"number","delay":0,"topic":"deleteId","sendOnBlur":true,"className":"","topicType":"str","x":110,"y":480,"wires":[["9b934b2d46e35ffc"]]},{"id":"24cb44a4c6383834","type":"function","z":"6175eaf79545705a","name":"function  (DELETE ID SQL)","func":"// 這個節點從 Dashboard 的輸入和按鈕接收訊息\n\n// 步驟二:執行刪除操作(來自按鈕)\n// 如果 msg.payload 是 1 (來自按鈕),則執行刪除\nif (msg.payload === 1) {\n    const idToDelete = flow.get('deleteID');\n    \n    // 檢查是否有有效的 ID\n    if (idToDelete && !isNaN(parseInt(idToDelete))) {\n        // 創建 DELETE SQL 語句\n        msg.topic = `DELETE FROM my_data WHERE id = ${parseInt(idToDelete)}`;\n        msg.payload = {}; // 清空 payload,確保後續操作正常\n        return msg;\n    } else {\n        node.warn(\"請先在輸入框中輸入有效的 ID 號碼!\");\n        return null; // 無效 ID,不執行操作\n    }\n}\n\nreturn msg; // 其他情況不執行操作","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":540,"wires":[["940528c3e3697601"]]},{"id":"ae02d51a98a2c189","type":"inject","z":"6175eaf79545705a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":40,"wires":[["5943ddf87dc315ac"]]},{"id":"606aeeb344c86ebb","type":"debug","z":"6175eaf79545705a","name":"debug 369","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":60,"wires":[]},{"id":"bc23914c8cab28bb","type":"sqlitedb","db":"1111.db","mode":"RWC"},{"id":"1963df87ae08178b","type":"ui_group","name":"Default","tab":"1871b13b611a78bb","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"216515e83bf6ead4","type":"ui_group","name":"SQLite 資料庫功能示範","tab":"1871b13b611a78bb","order":1,"disp":true,"width":10,"collapse":false,"className":""},{"id":"1871b13b611a78bb","type":"ui_tab","name":"資料庫示範面板","icon":"dashboard","order":1,"disabled":false,"hidden":false}]

ESP32 and Node-RED with MQTT (Publish and Subscribe)

  ESP32 and Node-RED with MQTT (Publish and Subscribe) 參考來源  https://randomnerdtutorials.com/esp8266-and-node-red-with-mqtt/ mqtt_server = ...