2026年1月26日 星期一

Flutter 寫一個 APP 透過 MQTT 控制 Wokwi DHT22 & LED

 Flutter 寫一個 APP  透過 MQTT 控制 Wokwi DHT22  &  LED




使用的是 Flutter 3.19.1(2024年初版本),但配合了 2025 年最新的 Android Studio這種配置最容易在 Java 版本Gradle 版本上產生衝突。

1. Android SDK 與 Java 版本管理 (核心關鍵)

最新的 Android Studio (2025.2.3) 預設搭載 Java 21,而 Flutter 3.19.1 預設的專案模板通常只支援到 Java 11 或 17。

  • 環境診斷:務必執行 flutter doctor -v 檢查 Java binary at: 指向的位置。

  • 指令工具:在 Android Studio 的 SDK Manager 中,一定要勾選 "Android SDK Command-line Tools (latest)",否則 VS Code 無法調用 SDK 指令。

  • 授權同意:安裝完工具後,必須在終端機執行 flutter doctor --android-licenses 並一路按 y


2. Gradle 版本相容性調教 (解決 Major version 65 錯誤)

當你執行 flutter build apk 遇到 Unsupported class file major version 65,這是因為 Gradle 7.x 不支援 Java 21。

必須修改專案內的這三個檔案:

  1. android/gradle/wrapper/gradle-wrapper.properties:

    • distributionUrl 升級至 gradle-8.5-all.zip

  2. android/settings.gradle:

    • com.android.application 版本改為 8.1.0 或以上。

  3. android/app/build.gradle:

    • android { ... } 內新增 namespace "你的包名"


3. Visual Studio Code 開發注意事項

VS Code 是輕量化首選,但在開發 Flutter 時需注意以下配置:

  • 必要擴充功能

    • Flutter (會自動帶動 Dart 擴充)。

    • Error Lens (強烈推薦,能直接在程式碼行末顯示語法錯誤)。

  • 設定存檔自動格式化

    • settings.json 加入 "editor.formatOnSave": true,這能幫你自動整理 Flutter 巢狀的 ) 括號。

  • Hot Reload 習慣

    • 習慣使用 Ctrl + F5 啟動,並在修改 UI 後直接 Ctrl + S 存檔觸發熱重載,速度比 Android Studio 快很多。


4. Android 權限與網路設定

開發 MQTT 相關 App 時,Android 10 (API 29) 以上有嚴格限制:

  • 清單檔位置android/app/src/main/AndroidManifest.xml

  • 網路權限<uses-permission> 標籤必須放在 <application> 標籤之外

  • 明文傳輸 (非加密 MQTT):如果你連線的 MQTT Broker 是 ws:// 或非加密的 1883 埠,需在 <application> 標籤內加入 android:usesCleartextTraffic="true"


5. Flutter 3.19.1 版本的限制

  • 套件版本:在 pubspec.yaml 引用套件(如 mqtt_client)時,若遇到版本衝突,建議不要使用 any,而是指定與 Dart 3.3.0 相容的版本。

  • Deprecation:3.19 版本已經捨棄了一些舊有的 API(如舊版的 FlatButton),請統一使用 ElevatedButtonTextButton





以下為你重新整理的開發注意事項與環境調教指南:

要開始使用 Flutter,你需要下載 Flutter SDK 並配置開發環境。以下是針對不同作業系統的下載與安裝指南:

1. 官方下載位址

請前往 Flutter 官網獲取最新穩定版本:


2. 安裝步驟 (以 Windows 為例)

步驟 A:解壓縮

  1. 將下載的 .zip 檔案解壓縮。

  2. flutter 資料夾放到一個路徑簡單的地方,例如 C:\src\flutter

    注意: 不要安裝在 C:\Program Files\,因為這需要系統管理員權限,會導致指令執行失敗。

步驟 B:設定環境變數 (Path)

為了讓你在任何地方都能使用 flutter 指令,必須設定環境變數:

  1. 在 Windows 搜尋列輸入「環境變數」,選擇「編輯系統環境變數」。

  2. 點擊「環境變數」按鈕。

  3. 在「使用者變數」中找到名為 Path 的變數,點擊「編輯」。

  4. 點擊「新增」,將 Flutter 裡面的 bin 資料夾路徑貼上去(例如:C:\src\flutter\bin)。

  5. 點擊「確定」儲存。

在設定環境變數時,「變數值」就是你電腦中 Flutter 資料夾內 bin 資料夾的完整路徑

具體操作時,你會看到兩個欄位:

  1. 變數名稱Path

  2. 變數值:這就是你要填入的內容。


如何確定你的「變數值」?

假設你將下載的 Flutter 解壓縮到了 C:\ 底下的 src 資料夾,那麼你的變數值通常如下:

C:\src\flutter\bin

具體操作細節:

  1. 開啟檔案總管,進入你的 flutter 資料夾,再點進去 bin 資料夾。

  2. 點擊檔案總管最上方顯示路徑的欄位,路徑會變成藍色選取狀態(例如:C:\flutter_windows\flutter\bin)。

  3. 按下 Ctrl + C 複製。

  4. 回到環境變數視窗,點擊「新增」,按下 Ctrl + V 貼上。



3. 環境檢查工具:Flutter Doctor

安裝好 SDK 並設定完變數後,請開啟「命令提示字元 (CMD)」,輸入以下指令:

Bash
flutter doctor

這個工具會自動檢查你的電腦還缺少什麼。通常它會提醒你:

  • Android Studio 是否安裝。

  • Android SDK 是否配置正確。

  • Visual Studio Code 是否安裝。


4. 推薦的開發工具 (IDE)

Flutter SDK 本身只是工具包,你還需要一個寫程式的地方。目前有兩個主流選擇:

  1. VS Code (推薦新手):輕量、速度快。

    • 下載後需在 Extensions 搜尋並安裝 "Flutter""Dart" 插件。




當你安裝好 Flutter 並執行過 flutter doctor 且全部打勾後,你就可以依照以下指令建立新專案

VS Code 是目前開發 Flutter 最受歡迎的工具,因為它比 Android Studio 輕巧得多,啟動速度也快。

雖然你剛才安裝 Android Studio 是為了獲取 SDK(後台地基),但以後你完全可以在 VS Code 中寫程式(前台工地)

我們需要利用它的「安裝功能」來把 SDK 補回來。請跟著以下步驟操作:


第一步:重新喚醒 Android Studio 並下載 SDK

VS Code 本身不具備下載 Android 底層驅動(SDK)的能力,所以請:

  1. 啟動 Android Studio:如果在搜尋列找不到,請重新安裝一次。

  2. 進入 SDK Manager

    • 在歡迎畫面點選 More Actions > SDK Manager

    • 關鍵動作:看最上方的 Android SDK Location,這就是我們要的「寶藏路徑」。

    • 確認安裝:確保 Android 14.0 (UpsideDownCake) 或類似的版本有被勾選並顯示 Installed

  3. 安裝 Command-line Tools(這步沒做,VS Code 會報錯):

    • 在同一視窗切換到 SDK Tools 分頁。

    • 勾選 Android SDK Command-line Tools (latest)

    • 點擊 Apply 開始下載。

在 VS Code 中「手動指路」

當你從 Android Studio 拿到正確路徑(假設是 C:\Android\Sdk)後,回到 VS Code 的終端機,依序輸入以下指令:

  1. 修正路徑

    PowerShell
    flutter config --android-sdk "這裡貼上你剛才看到的路徑"
    
  2. 同意授權(這最重要,沒做就不能打包):

    PowerShell
    flutter doctor --android-licenses
    

    畫面上會出現很多字,請一直輸入 y 並按 Enter,直到它說 All SDK package licenses accepted



請按照以下步驟設定 VS Code:


1. 在 VS Code 安裝 Flutter 插件

  1. 打開 VS Code

  2. 點擊左側邊欄的 Extensions 圖示(或是按 Ctrl+Shift+X)。

  3. 在搜尋框輸入 Flutter

  4. 找到由 Dart Code 發行的 Flutter 插件,點擊 Install(安裝 Flutter 會自動幫你把 Dart 插件也裝好)。


2. 建立你的第一個 Flutter 專案

  1. 在 VS Code 中按快捷鍵 Ctrl + Shift + P 打開指令面板。

  2. 輸入 flutter 並選擇 Flutter: New Project

  3. 選擇 Application

  4. 選擇一個你要存檔的資料夾。

  5. 輸入專案名稱(例如:mqtt_led_control)。

  6. 等待幾秒鐘,VS Code 會自動幫你產生所有檔案。


√ Built 就代表你已經大功告成了。

你的 APK 檔案存放在專案資料夾深處,最快找到它的方式有兩種:

1. 檔案總管路徑(直接複製貼上)

請開啟 Windows 檔案總管,將以下路徑貼到網址列(請確認開頭的磁碟代號正確):

D:\2026 RFID\flutter\hello\flutter_application_1\build\app\outputs\flutter-apk\


2. 檔案說明

在這個資料夾中,你會看到剛才生成的成品。建議優先使用 app-arm64-v8a-release.apk

檔案名稱說明
app-arm64-v8a-release.apk最推薦! 適用於絕大多數現代 Android 手機(64位元)。
app-armeabi-v7a-release.apk適用於非常舊款的手機(32位元)。
app-x86_64-release.apk適用於電腦上的 Android 模擬器或少數平板。

建立一個全新的專案並寫入 MQTT 控制功能。請跟著以下步驟操作:


第一步:建立全新的 Flutter 專案

  1. 開啟 VS Code

  2. 按下 Ctrl + Shift + P 組合鍵,搜尋並選擇 Flutter: New Project

  3. 選擇 Application

  4. 選擇一個存放目錄(例如 D:\2026 RFID\flutter\)。

  5. 輸入專案名稱:mqtt_led_controller,然後按 Enter。

  6. 等待 VS Code 幫你建立好所有檔案。


第二步:添加 MQTT 通訊套件

在 Flutter 中,我們需要外掛程式來處理 MQTT 協定。

  1. 在左側檔案列中找到並打開 pubspec.yaml

  2. 找到 dependencies: 這一行,在 cupertino_icons: 下方加入 mqtt_client: ^10.1.0

    注意: 縮排必須與 flutter: 對齊(2 個空格)。

YAML
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.6
  mqtt_client: ^10.1.0  # 新增這行
  1. Ctrl + S 存檔,VS Code 會自動下載這個套件(你會看到 flutter pub get 的字樣)。


第三步:撰寫控制程式碼

  1. 打開 lib/main.dart

  2. 刪除裡面所有的程式碼,直接把下面這段完整內容貼進去:

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: MqttControlPage()));

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

  @override
  State<MqttControlPage> createState() => _MqttControlPageState();
}

class _MqttControlPageState extends State<MqttControlPage> {
  // 設定區域:你可以改成你自己的 Broker 或 Topic
  final String broker = 'broker.emqx.io'; 
  final String clientIdentifier = 'flutter_user_${DateTime.now().millisecond}';
  final String topic = 'esp32/led/control';
  late MqttServerClient client;
  String status = "正在初始化...";

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

  Future<void> setupMqtt() async {
    client = MqttServerClient(broker, clientIdentifier);
    client.port = 1883;
    client.keepAlivePeriod = 20;

    try {
      await client.connect();
      setState(() => status = "連線成功");
    } catch (e) {
      setState(() => status = "連 line 失敗: $e");
      client.disconnect();
    }
  }

  void sendCommand(String cmd) {
    if (client.connectionStatus!.state == MqttConnectionState.connected) {
      final builder = MqttClientPayloadBuilder();
      builder.addString(cmd);
      client.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("ESP32 MQTT 控制")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("連線狀態: $status", style: const TextStyle(fontSize: 18, color: Colors.blue)),
            const SizedBox(height: 40),
            Wrap(
              spacing: 20,
              runSpacing: 20,
              children: [
                _btn("ON", Colors.green, "on"),
                _btn("OFF", Colors.red, "off"),
                _btn("FLASH", Colors.blue, "flash"),
                _btn("TIMER", Colors.orange, "timer"),
              ],
            )
          ],
        ),
      ),
    );
  }

  Widget _btn(String label, Color color, String cmd) {
    return SizedBox(
      width: 150,
      height: 60,
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(backgroundColor: color, foregroundColor: Colors.white),
        onPressed: () => sendCommand(cmd),
        child: Text(label, style: const TextStyle(fontSize: 20)),
      ),
    );
  }
}

第四步:權限設定 (Android 網路權限)

雖然現在打包通常會過,但保險起見,我們確認一下網路權限:

  1. 打開 android/app/src/main/AndroidManifest.xml

  2. 確保在 <manifest ...> 標籤下方有一行: <uses-permission android:name="android.permission.INTERNET" />


第五步:正式打包 APK

  1. 在 VS Code 底部開啟 Terminal

  2. 輸入以下指令開始打包:

PowerShell
flutter build apk --release --split-per-abi
  1. 等待 1-3 分鐘。當你看到 √ Built build\app\outputs\flutter-apk\app-arm64-v8a-release.apk 時,就完成了!


第六步:取出 APK 安裝

  1. 在 VS Code 檔案清單中,對著 build/app/outputs/flutter-apk/app-arm64-v8a-release.apk 按右鍵。

  2. 選擇 「在檔案總管中顯示」

  3. 將這個檔案傳送到你的手機安裝。


完整的 lib/main.dart



import 'dart:convert';
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: SmartHomeApp(),
      debugShowCheckedModeBanner: false,
    ));

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

  @override
  State<SmartHomeApp> createState() => _SmartHomeAppState();
}

class _SmartHomeAppState extends State<SmartHomeApp> {
  // --- MQTT 設定區域 (依據您的資料) ---
  final String broker = 'mqttgo.io';
  final int port = 1883;
  final String clientIdentifier = 'flutter_dht22_${DateTime.now().millisecondsSinceEpoch}';

  // 主題設定
  final String topicLedControl = "wokwi/led/control";
  final String topicTemp = "wokwi/dht/temperature";
  final String topicHumi = "wokwi/dht/humidity";

  late MqttServerClient client;
 
  // 數據狀態
  String temperature = "--";
  String humidity = "--";
  String connectionStatus = "正在初始化...";
  bool isConnected = false;

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

  Future<void> setupMqtt() async {
    client = MqttServerClient(broker, clientIdentifier);
    client.port = port;
    client.keepAlivePeriod = 20;
    client.onConnected = onConnected;
    client.onDisconnected = onDisconnected;

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

    try {
      await client.connect();
    } catch (e) {
      setState(() => connectionStatus = "連線失敗: $e");
      client.disconnect();
    }

    // 監聽並解析訂閱的主題訊息
    client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> c) {
      final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage;
      final String payload = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);

      setState(() {
        if (c[0].topic == topicTemp) {
          temperature = payload;
        } else if (c[0].topic == topicHumi) {
          humidity = payload;
        }
      });
    });
  }

  void onConnected() {
    setState(() {
      isConnected = true;
      connectionStatus = "已連線至 MQTT伺服器";
    });
    // 訂閱溫濕度主題
    client.subscribe(topicTemp, MqttQos.atMostOnce);
    client.subscribe(topicHumi, MqttQos.atMostOnce);
  }

  void onDisconnected() {
    setState(() {
      isConnected = false;
      connectionStatus = "連線中斷";
    });
  }

  // 發送 LED 指令 (JSON 格式)
  void sendControl(String action, int value) {
    if (isConnected) {
      final Map<String, dynamic> data = {'action': action, 'value': value};
      final String jsonString = jsonEncode(data);
      final builder = MqttClientPayloadBuilder();
      builder.addString(jsonString);
      client.publishMessage(topicLedControl, MqttQos.atMostOnce, builder.payload!);
      print("發送指令: $jsonString");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blueGrey[50],
      appBar: AppBar(
        title: const Text("DHT22 & LED 控制中心"),
        backgroundColor: Colors.blueGrey[900],
        foregroundColor: Colors.white,
      ),
      body: Column(
        children: [
          // 狀態提示列
          Container(
            padding: const EdgeInsets.all(10),
            color: isConnected ? Colors.green[400] : Colors.red[400],
            width: double.infinity,
            child: Text(
              connectionStatus,
              textAlign: TextAlign.center,
              style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
          ),
         
          // 溫濕度數據卡片
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                sensorCard("溫度", "$temperature°C", Icons.thermostat, Colors.deepOrange),
                const SizedBox(width: 12),
                sensorCard("濕度", "$humidity%", Icons.opacity, Colors.blueAccent),
              ],
            ),
          ),

          const Padding(
            padding: EdgeInsets.symmetric(horizontal: 20),
            child: Divider(thickness: 1),
          ),

          // LED 控制區域
          Expanded(
            child: GridView.count(
              padding: const EdgeInsets.all(20),
              crossAxisCount: 2,
              mainAxisSpacing: 20,
              crossAxisSpacing: 20,
              children: [
                controlBtn("LED ON", Icons.lightbulb, Colors.green, () => sendControl("on", 0)),
                controlBtn("LED OFF", Icons.lightbulb_outline, Colors.red, () => sendControl("off", 0)),
                controlBtn("FLASH", Icons.fluorescent, Colors.purple, () => sendControl("flash", 0)),
                controlBtn("TIMER 10s", Icons.timer, Colors.orange[800]!, () => sendControl("timer", 10)),
              ],
            ),
          ),
         
          if (!isConnected)
            Padding(
              padding: const EdgeInsets.only(bottom: 30),
              child: ElevatedButton.icon(
                onPressed: setupMqtt,
                icon: const Icon(Icons.refresh),
                label: const Text("手動重新連線"),
              ),
            ),
        ],
      ),
    );
  }

  Widget sensorCard(String title, String value, IconData icon, Color color) {
    return Expanded(
      child: Container(
        padding: const EdgeInsets.symmetric(vertical: 25),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(20),
          boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 10)],
        ),
        child: Column(
          children: [
            Icon(icon, color: color, size: 45),
            const SizedBox(height: 10),
            Text(title, style: const TextStyle(fontSize: 16, color: Colors.grey)),
            Text(value, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
          ],
        ),
      ),
    );
  }

  Widget controlBtn(String label, IconData icon, Color color, VoidCallback onTap) {
    return InkWell(
      onTap: onTap,
      borderRadius: BorderRadius.circular(20),
      child: Container(
        decoration: BoxDecoration(
          color: color,
          borderRadius: BorderRadius.circular(20),
          boxShadow: [BoxShadow(color: color.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4))],
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(icon, color: Colors.white, size: 40),
            const SizedBox(height: 8),
            Text(label, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
          ],
        ),
      ),
    );
  }
}


完整的 pubspec.yaml


name: flutter_dht22
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.1.0

  # 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


完整的 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

完整的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
    }
    settings.ext.flutterSdkPath = flutterSdkPath()

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

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

plugins {
    id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
    id "com.android.application" version "8.1.0" apply false  // 改成 8.1.0
    id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}

include ":app"

完整的android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:label="flutter_dht22"
        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>






























接線檢查:

  • DHT22 的 VCC3.3V

  • DHT22 的 SDA (Data)GPIO 15

  • DHT22 的 GNDGND



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

// --- 硬體定義 ---
#define DHTPIN 15
#define DHTTYPE DHT22
#define LED_PIN 2

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

// --- 模式定義 ---
enum Mode { IDLE, FLASHING, TIMER_COUNTDOWN };
Mode currentMode = IDLE;

unsigned long targetTime = 0;      
unsigned long lastFlashTime = 0;  
const int flashInterval = 300;    
unsigned long lastMsgTime = 0;    

// --- MQTT 設定 ---
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "mqttgo.io";

const char* topic_led_control = "wokwi/led/control";
const char* topic_temp = "wokwi/dht/temperature";
const char* topic_humi = "wokwi/dht/humidity";

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  dht.begin();
 
  Serial.println("\n========================================");
  Serial.println("  系統初始化中...");
  Serial.println("========================================");
 
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

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 已連線");
  Serial.print("📍 IP 位址: "); Serial.println(WiFi.localIP());
}

// 處理從 App 傳來的訊息
void callback(char* topic, byte* payload, unsigned int length) {
  StaticJsonDocument<200> doc;
  deserializeJson(doc, payload, length);

  String action = doc["action"] | "";
  int value = doc["value"] | 0;

  Serial.println("\n📩 [收到 MQTT 指令]");
  Serial.print("   主題: "); Serial.println(topic);
  Serial.print("   動作: "); Serial.println(action);
  Serial.print("   數值: "); Serial.println(value);

  if (action == "on") {
    currentMode = IDLE;
    digitalWrite(LED_PIN, HIGH);
    Serial.println("💡 LED 已開啟");
  }
  else if (action == "off") {
    currentMode = IDLE;
    digitalWrite(LED_PIN, LOW);
    Serial.println("🌑 LED 已關閉");
  }
  else if (action == "flash") {
    currentMode = FLASHING;
    Serial.println("✨ 進入閃爍模式");
  }
  else if (action == "timer") {
    digitalWrite(LED_PIN, HIGH);
    targetTime = millis() + (value * 850);
    currentMode = TIMER_COUNTDOWN;
    Serial.printf("⏰ 啟動計時器: %d 秒後關閉\n", value);
  }
}

// 修正後的連線函式
void reconnect() {
  while (!client.connected()) {
    Serial.print("☁️ 正在連線至 MQTT 伺服器 ("); Serial.print(mqtt_server); Serial.print(")...");
   
    // 產生隨機 ID 避免衝突
    String clientId = "ESP32_Wokwi_" + String(random(0xffff), HEX);
   
    if (client.connect(clientId.c_str())) {
      Serial.println("\n✅ MQTT 伺服器連線成功!");
     
      // 訂閱主題
      client.subscribe(topic_led_control);
      Serial.print("📝 已成功訂閱主題: "); Serial.println(topic_led_control);
      Serial.println("----------------------------------------");
    } else {
      Serial.print("❌ 連線失敗, rc=");
      Serial.print(client.state());
      Serial.println(" (5 秒後重新嘗試)");
      delay(5000);
    }
  }
}

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

  unsigned long now = millis();

  // 1. 發送溫濕度 (每 5 秒)
  if (now - lastMsgTime > 5000) {
    lastMsgTime = now;
    float h = dht.readHumidity();
    float t = dht.readTemperature();
    if (!isnan(h) && !isnan(t)) {
      client.publish(topic_temp, String(t, 1).c_str());
      client.publish(topic_humi, String(h, 1).c_str());
      Serial.printf("📤 [發送數據] 溫度: %.1f°C, 濕度: %.1f%%\n", t, h);
    }
  }

  // 2. LED 模式邏輯
  switch (currentMode) {
    case FLASHING:
      if (now - lastFlashTime >= flashInterval) {
        lastFlashTime = now;
        digitalWrite(LED_PIN, !digitalRead(LED_PIN));
      }
      break;
    case TIMER_COUNTDOWN:
      if (now >= targetTime) {
        digitalWrite(LED_PIN, LOW);
        currentMode = IDLE;
        Serial.println("⌛ 計時時間到,LED 已關閉");
      }
      break;
    case IDLE:
      break;
  }
}

沒有留言:

張貼留言

設定並測試 Flutter

  設定並測試 Flutter https://docs.flutter.dev/install/quick 使用基於開源軟體的編輯器(例如 VS Code)在您的裝置上安裝 Flutter,即可開始使用 Flutter 開發您的第一個多平台應用程式! 學習如何使用任何基於開源軟...