2026年2月16日 星期一

DISCORD ESP32

DISCORD  ESP32 


 

    準備工作:取得 Discord Webhook URL

  1. 開啟 Discord 並進入你的伺服器。

  2. 進入頻道設定(齒輪圖示) > 整合 (Integrations) > Webhook

  3. 點擊「建立 Webhook」,設定好名字後點擊 「複製 Webhook URL」





main.ino

#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h> // 必須包含此庫以處理 HTTPS
#include <ArduinoJson.h>
#include <DHT.h>

// --- Discord Webhook 設定 ---
// 請務必確認此 URL 正確且包含 ID 與 Token
const char* DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1472888818762907763/SpUB8clIuuvq54_ZCtEDaYaVRiDSPQJNIiX80JbWleFP8fUA79WqnXnJEeSBDC4-yfVk";

// --- Wokwi 模擬 Wi-Fi 憑證 ---
const char* WIFI_SSID     = "Wokwi-GUEST";
const char* WIFI_PASSWORD = "";

// --- 硬體腳位定義 ---
#define DHTPIN 15
#define DHTTYPE DHT22
#define LED_PIN 2
DHT dht(DHTPIN, DHTTYPE);

/**
 * @brief 向 Discord 發送訊息 (支援 HTTPS)
 */
void sendDiscordMessage(String content) {
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi 未連線,無法發送訊息");
        return;
    }

    WiFiClientSecure *client = new WiFiClientSecure;
    if(client) {
        // 重要:在 Wokwi 中必須跳過 SSL 憑證檢查
        client->setInsecure();

        HTTPClient http;
        Serial.print("\n[HTTP] 開始發送至 Discord...");
       
        if (http.begin(*client, DISCORD_WEBHOOK_URL)) {
            http.addHeader("Content-Type", "application/json");

            StaticJsonDocument<512> doc;
            doc["content"] = content;

            String requestBody;
            serializeJson(doc, requestBody);
           
            int httpResponseCode = http.POST(requestBody);
           
            if (httpResponseCode > 0) {
                Serial.printf(" 狀態碼: %d\n", httpResponseCode);
                if (httpResponseCode == 204) {
                    Serial.println("✅ Discord 訊息發送成功!");
                } else if (httpResponseCode == 400) {
                    Serial.println("❌ 錯誤 400:請檢查 Webhook URL 或 JSON 格式");
                    Serial.println(http.getString()); // 印出 Discord 的錯誤訊息
                }
            } else {
                Serial.printf(" 失敗,錯誤原因: %s\n", http.errorToString(httpResponseCode).c_str());
            }
            http.end();
        } else {
            Serial.println("無法連接伺服器");
        }
        delete client; // 釋放記憶體
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(LED_PIN, OUTPUT);
    dht.begin();

    // 啟動提示
    digitalWrite(LED_PIN, HIGH); delay(200);
    digitalWrite(LED_PIN, LOW);  delay(200);

    // 1. 連接 Wi-Fi
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    Serial.print("連線至 Wi-Fi...");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nWi-Fi 已連線! IP: " + WiFi.localIP().toString());

    // 2. 初始讀取數據並發送
    delay(2000); // 等待 DHT 穩定
    float h = dht.readHumidity();
    float t = dht.readTemperature();

    if (isnan(h) || isnan(t)) {
        Serial.println("DHT22 讀取失敗!");
        sendDiscordMessage("⚠️ **ESP32 警告**:無法讀取 DHT22 感測器數據!");
    } else {
        String report = "🚀 **ESP32 監控站啟動**\n🌡️ 溫度: `" + String(t, 1) + "°C`\n💧 濕度: `" + String(h, 1) + "%`";
        sendDiscordMessage(report);
       
        if (t > 30.0) digitalWrite(LED_PIN, HIGH);
    }
}
void loop() {
    static unsigned long lastUpdate = 0;
    // 將更新頻率稍微拉長到 60 秒,避免被 Discord 判定為惡意攻擊
    if (millis() - lastUpdate > 60000) {
        lastUpdate = millis();
       
        float t = dht.readTemperature();
        float h = dht.readHumidity();
       
        if (!isnan(t) && !isnan(h)) {
            // 先確認 WiFi 是否還在,不在就重連
            if (WiFi.status() != WL_CONNECTED) {
                WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
                return;
            }
           
            String msg = "🔄 定時回報 - 溫度: " + String(t, 1) + "°C, 濕度: " + String(h, 1) + "%";
            sendDiscordMessage(msg);
        }
    }
}

arduino_secrets.h

#define SECRET_SSID "Wokwi-GUEST"
#define SECRET_PASS ""
//Copy the webhook url here:
#define SECRET_WEBHOOK "https://discord.com/api/webhooks/1472888818762907763/SpUB8clIuuvq54_ZCtEDaYaVRiDSPQJNIiX80JbWleFP8fUA79WqnXnJEeSBDC4-yfVk"
#define SECRET_TTS "false"

discord.h

#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>

const char ssid[] = SECRET_SSID;    // Network SSID (name)
const char pass[] = SECRET_PASS;    // Network password (use for WPA, or use as key for WEP)
const String discord_webhook = SECRET_WEBHOOK;
const String discord_tts = SECRET_TTS;

WiFiMulti WiFiMulti;

void connectWIFI() {
  WiFiMulti.addAP(ssid, pass);
  WiFi.mode(WIFI_STA);
  Serial.print("[WiFi] Connecting to: ");
  Serial.println(ssid);
  // wait for WiFi connection
  while ((WiFiMulti.run() != WL_CONNECTED)) {
    Serial.print(".");
  }
  Serial.println("[WiFi] Connected");
}

void sendDiscord(String content) {
  WiFiClientSecure *client = new WiFiClientSecure;
  // WiFiClientSecure *client;

  if (client) {
    client->setInsecure(); // Disable SSL certificate verification
    {
      HTTPClient https;
      Serial.println("[HTTP] Connecting to Discord...");
      Serial.println("[HTTP] Message: " + content);
      Serial.println("[HTTP] TTS: " + discord_tts);
      if (https.begin(*client, discord_webhook)) {  // HTTPS
        // start connection and send HTTP header
        https.addHeader("Content-Type", "application/json");
        int httpCode = https.POST("{\"content\":\"" + content + "\",\"tts\":" + discord_tts +"}");

        // httpCode will be negative on error
        if (httpCode > 0) {
          // HTTP header has been send and Server response header has been handled
          Serial.print("[HTTP] Status code: ");
          Serial.println(httpCode);

          // file found at server
          if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
            String payload = https.getString();
            Serial.print("[HTTP] Response: ");
            Serial.println(payload);
          }
        } else {
          Serial.print("[HTTP] Post... failed, error: ");
          Serial.println(https.errorToString(httpCode).c_str());
        }

        https.end();
      } else {
        Serial.printf("[HTTP] Unable to connect\n");
      }

      // End extra scoping block
    }

    delete client;
  } else {
    Serial.println("[HTTP] Unable to create client");
  }
}





import machine
import dht
import network
import time
import urequests as requests
import ujson
import os
import gc

# --- 設定區 ---
WIFI_SSID = "Wokwi-GUEST"
WIFI_PASS = ""

# Discord Webhook 設定
DISCORD_ID = "1472888818762907763"
DISCORD_TOKEN = "SpUB8clIuuvq54_ZCtEDaYaVRiDSPQJNIiX80JbWleFP8fUA79WqnXnJEeSBDC4-yfVk"
URL = "https://discord.com/api/webhooks/{}/{}".format(DISCORD_ID, DISCORD_TOKEN)

# 硬體定義
led_pin = machine.Pin(2, machine.Pin.OUT)     # 你的 LED 腳位 (GPIO 2)
dht_sensor = dht.DHT22(machine.Pin(15))      # 你的 DHT22 腳位 (GPIO 15)
button_pin = machine.Pin(35, machine.Pin.IN, machine.Pin.PULL_UP) # 按鈕 (GPIO 35)

button_down = False

# --- 網路連線 ---
def connect_wifi():
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if not wifi.isconnected():
        print('正在連線到 WiFi...')
        wifi.connect(WIFI_SSID, WIFI_PASS)
        while not wifi.isconnected():
            time.sleep(1)
            print('.', end='')
    return wifi

# --- 按鈕中斷處理 ---
def button_handler(pin):
    global button_down
    # 簡單去彈跳處理
    button_down = True

# 綁定中斷
button_pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler)

# --- 發送 Discord 訊息 ---
def send_discord_message(content):
    connect_wifi()
    gc.collect() # 釋放記憶體,防止 SSL 連線記憶體不足
   
    # 修正重點:處理換行符號以符合 JSON 規範,避免 400 錯誤
    # Discord 的 JSON body 中,換行符號必須被轉義為 \\n
    json_content = content.replace('\n', '\\n')
    payload = '{"content": "' + json_content + '"}'
   
    headers = {
        'Content-Type': 'application/json'
    }

    try:
        print("\n 訊息發送中...")
        # 設定 timeout 防止模擬器連線卡死
        response = requests.post(URL, data=payload.encode('utf-8'), headers=headers, timeout=10)
       
        print("Discord 回應狀態碼:", response.status_code)
       
        if response.status_code == 400:
            print("❌ 格式錯誤原因:", response.text)
        elif response.status_code == 204:
            print("✅ 訊息發送成功!")
           
        response.close()
    except Exception as e:
        print("❌ 網路發送異常 (可能是 SSL EOF):", e)
   
    gc.collect()

# --- 主程式 ---
if __name__ == '__main__':
    print("系統啟動: " + os.uname().sysname)
    led_pin.value(0)
   
    # 啟動時發送一次通知
    send_discord_message("🚀 ESP32 MicroPython 監控站已啟動!")

    while True:
        if button_down:
            print("\n偵測到按鈕按下,讀取數據中...")
            led_pin.value(1) # 處理時亮燈
           
            try:
                dht_sensor.measure()
                temp = dht_sensor.temperature()
                humi = dht_sensor.humidity()
               
                # 建立多行訊息
                msg = "📊 **環境監測數據**\n🌡️ 溫度: {:.1f}°C\n💧 濕度: {:.1f}%".format(temp, humi)
                print(msg)
               
                # 發送至 Discord
                send_discord_message(msg)
               
                # 簡易 LED 控制邏輯:溫度大於 30 度維持亮燈
                if temp <= 30:
                    led_pin.value(0)
                else:
                    print("⚠️ 溫度過高!")
                   
            except OSError as e:
                print("DHT22 讀取失敗:", e)
                send_discord_message("⚠️ 無法讀取感測器數據")
                led_pin.value(0)
           
            # 處理完畢,重置按鈕狀態
            button_down = False
           
        # 讓 CPU 休息一下
        time.sleep_ms(200)

2026年1月28日 星期三

Fultter APP控制 WOKWI ESP32 RFID+LED (使用flutter_windows_3.19.1-stable Visual studio 1.108.2 )

Fultter APP控制 WOKWI ESP32 RFID+LED (使用flutter_windows_3.19.1-stable  Visual studio  1.108.2 )

1) Android studio 環境設定

2) Visual studio  1.108.2版本  環境設定+ 開發 Flutter APP程式

https://medium.com/%E5%BD%BC%E5%BE%97%E6%BD%98%E7%9A%84-flutter-app-%E9%96%8B%E7%99%BC%E5%95%8F%E9%A1%8C%E8%A7%A3%E7%AD%94%E9%9B%86/%E4%BD%BF%E7%94%A8-vs-code-%E9%96%8B%E7%99%BC-flutter-app-%E5%BB%BA%E7%AB%8B%E5%B0%88%E6%A1%88-%E6%89%93%E9%96%8B%E5%B0%88%E6%A1%88%E5%92%8C%E5%95%9F%E5%8B%95%E6%A8%A1%E6%93%AC%E5%99%A8%E5%9F%B7%E8%A1%8C-app-213054edc169

3)  安裝  Flutter  flutter_windows_3.19.1-stable  解壓縮後儲存 於 C:\src\flutter 目錄




WOKWI  ESP32 程式

#include <SPI.h>
#include <MFRC522.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// --- 硬體腳位 (您的指定) ---
#define SS_PIN    5
#define RST_PIN   22
#define LED_PIN   2
#define I2C_SDA   17
#define I2C_SCL   16

// --- 設定區 ---
const char* ssid = "Wokwi-GUEST";
const char* password = "";

// MQTT 設定 (與您的 Python Tkinter 程式對接)
const char* mqtt_server = "mqttgo.io";
const char* TOPIC_RFID_UID = "alex9ufo/rfid/UID";
const char* TOPIC_LED_CONTROL = "alex9ufo/led/control";
const char* TOPIC_LED_STATUS = "alex9ufo/led/status";

// 硬體物件
LiquidCrystal_I2C lcd(0x27, 16, 2);
MFRC522 mfrc522(SS_PIN, RST_PIN);
WiFiClient espClient;
PubSubClient mqttClient(espClient);

// --- FreeRTOS 隊列 ---
QueueHandle_t rfidQueue;
struct RfidMsg { char uid[20]; };

// 全域變數
bool isFlashing = false;

// --- MQTT 接收處理 (來自 Python 控制台) ---
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (int i = 0; i < length; i++) message += (char)payload[i];
 
  Serial.printf("\n[MQTT CMD]: %s\n", message.c_str());
 
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("MQTT Command:");
  lcd.setCursor(0, 1);

  isFlashing = false;
  if (message == "on") {
    digitalWrite(LED_PIN, HIGH);
    lcd.print("LED: ON");
    mqttClient.publish(TOPIC_LED_STATUS, "ON");
  }
  else if (message == "off") {
    digitalWrite(LED_PIN, LOW);
    lcd.print("LED: OFF");
    mqttClient.publish(TOPIC_LED_STATUS, "OFF");
  }
  else if (message == "flash") {
    isFlashing = true;
    lcd.print("MODE: FLASHING");
    mqttClient.publish(TOPIC_LED_STATUS, "FLASHING");
  }
  else if (message == "timer") {
    digitalWrite(LED_PIN, HIGH);
    lcd.print("TIMER: 5 SEC");
    mqttClient.publish(TOPIC_LED_STATUS, "TIMER_START");
    vTaskDelay(5000 / portTICK_PERIOD_MS); // 在 Task 中使用 vTaskDelay 不會卡死整個系統
    digitalWrite(LED_PIN, LOW);
    mqttClient.publish(TOPIC_LED_STATUS, "OFF");
  }
}

// --- Core 0: 負責 WiFi 與 MQTT 通訊 ---
void mqttTask(void *pvParameters) {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { vTaskDelay(500 / portTICK_PERIOD_MS); }
 
  mqttClient.setServer(mqtt_server, 1883);
  mqttClient.setCallback(mqttCallback);

  RfidMsg rMsg;
  while (true) {
    // 維護 MQTT 連線
    if (!mqttClient.connected()) {
      Serial.print("Connecting to MQTT...");
      if (mqttClient.connect("ESP32_RFID_Gate_NoTG")) {
        Serial.println("Connected");
        mqttClient.subscribe(TOPIC_LED_CONTROL);
      } else {
        vTaskDelay(5000 / portTICK_PERIOD_MS);
      }
    }
    mqttClient.loop();

    // 接收來自 Core 1 的 RFID 訊息並發送到 MQTT
    if (xQueueReceive(rfidQueue, &rMsg, 0) == pdPASS) {
      mqttClient.publish(TOPIC_RFID_UID, rMsg.uid);
      Serial.printf("Sent UID to Python: %s\n", rMsg.uid);
    }

    // 處理閃爍邏輯
    if (isFlashing) {
      digitalWrite(LED_PIN, !digitalRead(LED_PIN));
      vTaskDelay(300 / portTICK_PERIOD_MS);
    }
   
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }
}

// --- Core 1: 專門負責 RFID 掃描 (不處理網路) ---
void rfidTask(void *pvParameters) {
  SPI.begin();
  mfrc522.PCD_Init();
  RfidMsg rMsg;
  while (true) {
    if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
      String uidStr = "";
      for (byte i = 0; i < mfrc522.uid.size; i++) {
        uidStr += (mfrc522.uid.uidByte[i] < 0x10 ? "0" : "");
        uidStr += String(mfrc522.uid.uidByte[i], HEX);
      }
      uidStr.toUpperCase();
     
      // 更新 LCD 顯示
      lcd.clear();
      lcd.setCursor(0, 0); lcd.print("RFID Detected!");
      lcd.setCursor(0, 1); lcd.print("ID: " + uidStr);
     
      // 將卡號打包送入隊列,交給 Core 0 發送
      uidStr.toCharArray(rMsg.uid, 20);
      xQueueSend(rfidQueue, &rMsg, portMAX_DELAY);

      mfrc522.PICC_HaltA();
      mfrc522.PCD_StopCrypto1();
    }
    vTaskDelay(200 / portTICK_PERIOD_MS);
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
 
  // 初始化 I2C LCD
  Wire.begin(I2C_SDA, I2C_SCL);
  lcd.init();
  lcd.backlight();
  lcd.print("MQTT Connecting...");

  // 建立隊列
  rfidQueue = xQueueCreate(10, sizeof(RfidMsg));

  if (rfidQueue != NULL) {
    // 建立雙核心任務
    xTaskCreatePinnedToCore(mqttTask, "MQTT_Task", 8192, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(rfidTask, "RFID_Task", 4096, NULL, 1, NULL, 1);
  }
}

void loop() {
  // FreeRTOS 架構下 loop 不需執行內容
  vTaskDelay(portMAX_DELAY);
}


VS Code pubspec.yaml

name: rfid
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=3.3.0 <4.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  mqtt_client: ^10.2.0  # Or latest version

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^3.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages


VS Code AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <application
        android:label="rfid"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <!-- Required to query activities that can process text, see:
         https://developer.android.com/training/package-visibility?hl=en and
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>


VS Code main.dart

import 'package:flutter/material.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';

void main() => runApp(const MaterialApp(home: RFIDControlApp()));

class RFIDControlApp extends StatefulWidget {
  const RFIDControlApp({super.key});

  @override
  State<RFIDControlApp> createState() => _RFIDControlAppState();
}

class _RFIDControlAppState extends State<RFIDControlApp> {
  final String server = "mqttgo.io";
  final String topicUID = "alex9ufo/rfid/UID";
  final String topicControl = "alex9ufo/led/control";
  final String topicStatus = "alex9ufo/led/status";
 
  late MqttServerClient client;
  String lastUID = "No Card Scanned";
  String ledStatus = "Unknown";
  bool isConnected = false;

  @override
  void initState() {
    super.initState();
    _setupMqtt();
  }

  Future<void> _setupMqtt() async {
    client = MqttServerClient(server, 'flutter_client_${DateTime.now().millisecondsSinceEpoch}');
    client.port = 1883;
    client.keepAlivePeriod = 20;
    client.onDisconnected = () => setState(() => isConnected = false);

    try {
      await client.connect();
      setState(() => isConnected = true);
     
      // Subscribe to UID and LED Status
      client.subscribe(topicUID, MqttQos.atMostOnce);
      client.subscribe(topicStatus, MqttQos.atMostOnce);

      client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> c) {
        final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage;
        final String pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);

        setState(() {
          if (c[0].topic == topicUID) {
            lastUID = pt;
          } else if (c[0].topic == topicStatus) {
            ledStatus = pt;
          }
        });
      });
    } catch (e) {
      print('Connection failed: $e');
      client.disconnect();
    }
  }

  void _sendCommand(String command) {
    if (!isConnected) return;
    final builder = MqttClientPayloadBuilder();
    builder.addString(command);
    client.publishMessage(topicControl, MqttQos.atLeastOnce, builder.payload!);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ESP32 RFID & LED Control"),
        backgroundColor: isConnected ? Colors.green : Colors.red,
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          children: [
            // RFID Display Card
            Card(
              elevation: 4,
              child: ListTile(
                leading: const Icon(Icons.credit_card, size: 40),
                title: const Text("Last Scanned UID"),
                subtitle: Text(lastUID, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.blue)),
              ),
            ),
            const SizedBox(height: 20),
            // LED Status Display
            Text("Current LED Status: $ledStatus", style: const TextStyle(fontSize: 18)),
            const Divider(height: 40),
            // Control Buttons
            Wrap(
              spacing: 20,
              runSpacing: 20,
              children: [
                _buildControlBtn("ON", Colors.green, () => _sendCommand("on")),
                _buildControlBtn("OFF", Colors.red, () => _sendCommand("off")),
                _buildControlBtn("FLASH", Colors.orange, () => _sendCommand("flash")),
                _buildControlBtn("TIMER (5s)", Colors.purple, () => _sendCommand("timer")),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildControlBtn(String label, Color color, VoidCallback onPressed) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(backgroundColor: color, minimumSize: const Size(120, 50)),
      onPressed: isConnected ? onPressed : null,
      child: Text(label, style: const TextStyle(color: Colors.white)),
    );
  }
}

VS Code android/gradle/wrapper/gradle-wrapper.properties

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip


VS Code android/settings.gradle
pluginManagement {
    def flutterSdkPath = {
        def properties = new Properties()
        file("local.properties").withInputStream { properties.load(it) }
        def flutterSdkPath = properties.getProperty("flutter.sdk")
        assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
        return flutterSdkPath
    }()

    includeBuild("${flutterSdkPath}/packages/flutter_tools/gradle")

    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

plugins {
    // 1. 將這裡的版本改為 8.2.1 或更高
    id "com.android.application" version "8.2.1" apply false
    // 2. 如果有 kotlin,建議也升級到 1.9.10 以上
    id "org.jetbrains.kotlin.android" version "1.9.10" apply false
    id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}


VS Code android/app/build.gradle

plugins {
    id "com.android.application"
    id "kotlin-android"
    id "dev.flutter.flutter-gradle-plugin"
}

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

android {
    namespace "com.example.rfid"
    compileSdk flutter.compileSdkVersion
    ndkVersion flutter.ndkVersion

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.rfid"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
        minSdkVersion flutter.minSdkVersion
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

flutter {
    source '../..'
}






PowerShell
# 1. 清除 Flutter 緩存
flutter clean

# 2. 重新取得套件
flutter pub get

# 3. 重新編譯
flutter build apk --release --split-per-abi


DISCORD ESP32

DISCORD  ESP32        準備工作:取得 Discord Webhook URL 開啟 Discord 並進入你的伺服器。 進入頻道設定(齒輪圖示) > 整合 (Integrations) > Webhook 。 點擊「建立 Webhook」,設定...