2026年10月7日 星期三

 


清除項目編譯快取
執行 flutter clean

重新取得套件依賴
執行 flutter pub get

完整重新編譯並執行
flutter run

1. 新增專案套件 (pubspec.yaml)

在 Flutter 專案根目錄下的 pubspec.yaml 中,新增 mqtt_client 套件 dependencies:

name: mqtt_dht_led
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.13.5

# 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.0.0 # 請使用最新版本
  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8

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: ^6.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/to/resolution-aware-images

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/to/asset-from-package

  # 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/to/font-from-package

2. Flutter 完整程式碼 (lib/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 MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ESP32 控制面板',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const ESP32ControllerScreen(),
    );
  }
}

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

  @override
  State<ESP32ControllerScreen> createState() => _ESP32ControllerScreenState();
}

class _ESP32ControllerScreenState extends State<ESP32ControllerScreen> {
  // MQTT 設定
  static const String broker = 'broker.hivemq.com';
  static const int port = 1883;

  static const String topicLedStatus = 'alex9ufo/2026/ledstatus';
  static const String topicTemp = 'alex9ufo/2026/temp';
  static const String topicHumi = 'alex9ufo/2026/humi';

  static const String topicLedCmd = 'alex9ufo/2026/led';
  static const String topicTempOk = 'alex9ufo/2026/tempOK';
  static const String topicHumiOk = 'alex9ufo/2026/humiOK';

  late MqttServerClient client;

  // 狀態變數
  String mqttStatus = 'MQTT 狀態: 連線中...';
  Color mqttStatusColor = Colors.orange;

  String ledStatus = 'LED 狀態: 待機中...';
  String tempStatus = '環境溫度: -- °C';
  String humiStatus = '環境濕度: -- %';

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

  @override
  void dispose() {
    client.disconnect();
    super.dispose();
  }

  // 初始化並連線 MQTT Broker
  Future<void> _connectMqtt() async {
    // 建立獨一無二的 Client Identifier
    final String clientId =
        'flutter_client_${DateTime.now().millisecondsSinceEpoch}';
    client = MqttServerClient(broker, clientId);
    client.port = port;
    client.keepAlivePeriod = 60;
    client.logging(on: false);

    // 設定連線訊息
    final connMess = MqttConnectMessage()
        .withClientIdentifier(clientId)
        .startClean()
        .withWillQos(MqttQos.atMostOnce);
    client.connectionMessage = connMess;

    // 設定 Callback 回呼函式
    client.onConnected = _onConnected;
    client.onDisconnected = _onDisconnected;

    try {
      await client.connect();
    } catch (e) {
      setState(() {
        mqttStatus = 'MQTT 狀態: 連線失敗';
        mqttStatusColor = Colors.red;
      });
      client.disconnect();
    }
  }

  // 連線成功回呼
  void _onConnected() {
    setState(() {
      mqttStatus = 'MQTT 狀態: 已連線 (Broker 線上)';
      mqttStatusColor = Colors.green;
    });

    // 訂閱主題
    client.subscribe(topicLedStatus, MqttQos.atMostOnce);
    client.subscribe(topicTemp, MqttQos.atMostOnce);
    client.subscribe(topicHumi, MqttQos.atMostOnce);

    // 監聽接收到的訊息 stream
    client.updates!.listen((List<MqttReceivedMessage<MqttMessage?>>? c) {
      final MqttPublishMessage recMess = c![0].payload as MqttPublishMessage;
      final String payload = MqttPublishPayload.bytesToStringAsString(
        recMess.payload.message,
      );
      final String topic = c[0].topic;

      _handleMessage(topic, payload);
    });
  }

  // 斷線回呼
  void _onDisconnected() {
    setState(() {
      mqttStatus = 'MQTT 狀態: 已離線';
      mqttStatusColor = Colors.red;
    });
  }

  // 處理收到的 MQTT 訊息 (對應原 Node-RED 的 Function 節點邏輯)
  void _handleMessage(String topic, String payload) {
    setState(() {
      if (topic == topicLedStatus) {
        // 格式化 LED 狀態
        ledStatus = 'LED 狀態: $payload';
      } else if (topic == topicTemp) {
        // 格式化溫度 + 自動回傳 TempOK
        tempStatus = '環境溫度: $payload °C';
        _sendCommand(topicTempOk, 'OK');
      } else if (topic == topicHumi) {
        // 格式化濕度 + 自動回傳 HumiOK
        humiStatus = '環境濕度: $payload %';
        _sendCommand(topicHumiOk, 'OK');
      }
    });
  }

  // 發送 MQTT 訊息
  void _sendCommand(String topic, String payload) {
    if (client.connectionStatus?.state == MqttConnectionState.connected) {
      final builder = MqttClientPayloadBuilder();
      builder.addString(payload);
      client.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ESP32 控制面板'), centerTitle: true),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Card(
          elevation: 4,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                // 標題 (對應 Group 名稱)
                const Text(
                  'ESP32 LED & DHT22 控制器',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 12),

                // MQTT 連線狀態
                Text(
                  mqttStatus,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.bold,
                    color: mqttStatusColor,
                  ),
                ),
                const Divider(height: 24),

                // 1. LED 狀態顯示
                Text(
                  ledStatus,
                  textAlign: TextAlign.center,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: Colors.blue,
                  ),
                ),
                const SizedBox(height: 12),

                // 2. 溫度顯示
                Text(
                  tempStatus,
                  textAlign: TextAlign.center,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: Colors.brown,
                  ),
                ),
                const SizedBox(height: 12),

                // 3. 濕度顯示
                Text(
                  humiStatus,
                  textAlign: TextAlign.center,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: Colors.green,
                  ),
                ),
                const Divider(height: 32),

                // 控制按鈕區域 (對應 UI Button 節點)
                ElevatedButton(
                  onPressed: () => _sendCommand(topicLedCmd, 'on'),
                  child: const Text(
                    'ON (開啟)',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                ),
                const SizedBox(height: 8),

                ElevatedButton(
                  onPressed: () => _sendCommand(topicLedCmd, 'off'),
                  child: const Text(
                    'OFF (關閉)',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                ),
                const SizedBox(height: 8),

                ElevatedButton(
                  onPressed: () => _sendCommand(topicLedCmd, 'flash'),
                  child: const Text(
                    'FLASH (閃爍)',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                ),
                const SizedBox(height: 8),

                ElevatedButton(
                  onPressed: () => _sendCommand(topicLedCmd, 'timer(5Sec)'),
                  child: const Text(
                    'Timer (定時5秒)',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}






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

#define DHTPIN 4
#define DHTTYPE DHT22

const char* ssid = "Wokwi-GUEST";
const char* password = "";

const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;

const char* sub_topic = "alex9ufo/2026/led";
const char* pub_topic = "alex9ufo/2026/ledstatus";
const char* temp_topic = "alex9ufo/2026/temp";
const char* humi_topic = "alex9ufo/2026/humi";
const char* temp_ok_topic = "alex9ufo/2026/tempOK";
const char* humi_ok_topic = "alex9ufo/2026/humiOK";

const int ledPin = 2;

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

enum Mode { OFF, ON, FLASH, TIMER };
Mode currentMode = OFF;

unsigned long lastFlashTime = 0;
unsigned long timerStartTime = 0;
unsigned long lastDHTTime = 0;
bool ledState = LOW;

void setup_wifi() {
  delay(10);
  Serial.print("[Wi-Fi] 正在連線至 ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\n[Wi-Fi] 已成功連線!");
}

void publishStatus(const char* payload) {
  client.publish(pub_topic, payload);
  Serial.print("[MQTT 發行] LED 狀態: ");
  Serial.println(payload);
}

void callback(char* topic, byte* payload, unsigned int length) {
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }

  Serial.print("[MQTT 接收] 主題: ");
  Serial.print(topic);
  Serial.print(" | Payload: ");
  Serial.println(message);

  // 接收 tempOK 主題
  if (String(topic) == temp_ok_topic) {
    Serial.println("temp_Rx_OK");
    return;
  }
 
  // 接收 humiOK 主題
  if (String(topic) == humi_ok_topic) {
    Serial.println("humi_Rx_OK");
    return;
  }

  if (message == "on") {
    currentMode = ON;
    digitalWrite(ledPin, HIGH);
    publishStatus("on_led");
  } else if (message == "off") {
    currentMode = OFF;
    digitalWrite(ledPin, LOW);
    publishStatus("off_led");
  } else if (message == "flash") {
    currentMode = FLASH;
    publishStatus("flash_led");
  } else if (message == "timer(5Sec)") {
    currentMode = TIMER;
    timerStartTime = millis();
    digitalWrite(ledPin, HIGH);
    publishStatus("timer(5Sec)_led");
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("[MQTT] 正在嘗試連線至 Broker: ");
    Serial.println(mqtt_server);
    String clientId = "ESP32Client-" + String(random(0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("[MQTT] 連線成功!");
      client.subscribe(sub_topic);
      client.subscribe(temp_ok_topic); // 訂閱 tempOK
      client.subscribe(humi_ok_topic); // 訂閱 humiOK
    } else {
      Serial.print("[MQTT] 連線失敗, rc=");
      Serial.print(client.state());
      Serial.println(" 5 秒後重試...");
      delay(5000);
    }
  }
}

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

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

  unsigned long currentMillis = millis();

  // 每 2 秒讀取並發行溫濕度
  if (currentMillis - lastDHTTime >= 5000) {
    lastDHTTime = currentMillis;
    float h = dht.readHumidity();
    float t = dht.readTemperature();

    if (!isnan(h) && !isnan(t)) {
      client.publish(temp_topic, String(t, 1).c_str());
      client.publish(humi_topic, String(h, 1).c_str());
      Serial.printf("[DHT22] 溫度: %.1f °C | 濕度: %.1f %%\n", t, h);
    }
  }

  if (currentMode == FLASH) {
    if (currentMillis - lastFlashTime >= 500) {
      lastFlashTime = currentMillis;
      ledState = !ledState;
      digitalWrite(ledPin, ledState);
    }
  } else if (currentMode == TIMER) {
    if (currentMillis - timerStartTime >= 5000) {
      digitalWrite(ledPin, LOW);
      currentMode = OFF;
      publishStatus("off_led");
    }
  }
}

沒有留言:

張貼留言

  清除項目編譯快取 執行 flutter clean 重新取得套件依賴 執行 flutter pub get 完整重新編譯並執行 flutter run 1. 新增專案套件 (pubspec.yaml) 在 Flutter 專案根目錄下的 pubspec.yaml 中,新增 ...