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。
必須修改專案內的這三個檔案:
android/gradle/wrapper/gradle-wrapper.properties:
android/settings.gradle:
android/app/build.gradle:
3. Visual Studio Code 開發注意事項
VS Code 是輕量化首選,但在開發 Flutter 時需注意以下配置:
必要擴充功能:
設定存檔自動格式化:
Hot Reload 習慣:
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 版本的限制
以下為你重新整理的開發注意事項與環境調教指南:
要開始使用 Flutter,你需要下載 Flutter SDK 並配置開發環境。以下是針對不同作業系統的下載與安裝指南:
1. 官方下載位址
請前往 Flutter 官網獲取最新穩定版本:
2. 安裝步驟 (以 Windows 為例)
步驟 A:解壓縮
將下載的 .zip 檔案解壓縮。
將 flutter 資料夾放到一個路徑簡單的地方,例如 C:\src\flutter。
注意: 不要安裝在 C:\Program Files\,因為這需要系統管理員權限,會導致指令執行失敗。
步驟 B:設定環境變數 (Path)
為了讓你在任何地方都能使用 flutter 指令,必須設定環境變數:
在 Windows 搜尋列輸入「環境變數」,選擇「編輯系統環境變數」。
點擊「環境變數」按鈕。
在「使用者變數」中找到名為 Path 的變數,點擊「編輯」。
點擊「新增」,將 Flutter 裡面的 bin 資料夾路徑貼上去(例如:C:\src\flutter\bin)。
點擊「確定」儲存。
在設定環境變數時,「變數值」就是你電腦中 Flutter 資料夾內 bin 資料夾的完整路徑。
具體操作時,你會看到兩個欄位:
變數名稱:Path
變數值:這就是你要填入的內容。
如何確定你的「變數值」?
假設你將下載的 Flutter 解壓縮到了 C:\ 底下的 src 資料夾,那麼你的變數值通常如下:
C:\src\flutter\bin
具體操作細節:
開啟檔案總管,進入你的 flutter 資料夾,再點進去 bin 資料夾。
點擊檔案總管最上方顯示路徑的欄位,路徑會變成藍色選取狀態(例如:C:\flutter_windows\flutter\bin)。
按下 Ctrl + C 複製。
回到環境變數視窗,點擊「新增」,按下 Ctrl + V 貼上。
3. 環境檢查工具:Flutter Doctor
安裝好 SDK 並設定完變數後,請開啟「命令提示字元 (CMD)」,輸入以下指令:
這個工具會自動檢查你的電腦還缺少什麼。通常它會提醒你:
Android Studio 是否安裝。
Android SDK 是否配置正確。
Visual Studio Code 是否安裝。
4. 推薦的開發工具 (IDE)
Flutter SDK 本身只是工具包,你還需要一個寫程式的地方。目前有兩個主流選擇:
VS Code (推薦新手):輕量、速度快。
當你安裝好 Flutter 並執行過 flutter doctor 且全部打勾後,你就可以依照以下指令建立新專案。
VS Code 是目前開發 Flutter 最受歡迎的工具,因為它比 Android Studio 輕巧得多,啟動速度也快。
雖然你剛才安裝 Android Studio 是為了獲取 SDK(後台地基),但以後你完全可以在 VS Code 中寫程式(前台工地)。
我們需要利用它的「安裝功能」來把 SDK 補回來。請跟著以下步驟操作:
第一步:重新喚醒 Android Studio 並下載 SDK
VS Code 本身不具備下載 Android 底層驅動(SDK)的能力,所以請:
啟動 Android Studio:如果在搜尋列找不到,請重新安裝一次。
進入 SDK Manager:
在歡迎畫面點選 More Actions > SDK Manager。
關鍵動作:看最上方的 Android SDK Location,這就是我們要的「寶藏路徑」。
確認安裝:確保 Android 14.0 (UpsideDownCake) 或類似的版本有被勾選並顯示 Installed。
安裝 Command-line Tools(這步沒做,VS Code 會報錯):
在 VS Code 中「手動指路」
當你從 Android Studio 拿到正確路徑(假設是 C:\Android\Sdk)後,回到 VS Code 的終端機,依序輸入以下指令:
修正路徑:
同意授權(這最重要,沒做就不能打包):
畫面上會出現很多字,請一直輸入 y 並按 Enter,直到它說 All SDK package licenses accepted。
請按照以下步驟設定 VS Code:
1. 在 VS Code 安裝 Flutter 插件
打開 VS Code。
點擊左側邊欄的 Extensions 圖示(或是按 Ctrl+Shift+X)。
在搜尋框輸入 Flutter。
找到由 Dart Code 發行的 Flutter 插件,點擊 Install(安裝 Flutter 會自動幫你把 Dart 插件也裝好)。
2. 建立你的第一個 Flutter 專案
在 VS Code 中按快捷鍵 Ctrl + Shift + P 打開指令面板。
輸入 flutter 並選擇 Flutter: New Project。
選擇 Application。
選擇一個你要存檔的資料夾。
輸入專案名稱(例如:mqtt_led_control)。
等待幾秒鐘,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 專案
開啟 VS Code。
按下 Ctrl + Shift + P 組合鍵,搜尋並選擇 Flutter: New Project。
選擇 Application。
選擇一個存放目錄(例如 D:\2026 RFID\flutter\)。
輸入專案名稱:mqtt_led_controller,然後按 Enter。
等待 VS Code 幫你建立好所有檔案。
第二步:添加 MQTT 通訊套件
在 Flutter 中,我們需要外掛程式來處理 MQTT 協定。
在左側檔案列中找到並打開 pubspec.yaml。
找到 dependencies: 這一行,在 cupertino_icons: 下方加入 mqtt_client: ^10.1.0。
注意: 縮排必須與 flutter: 對齊(2 個空格)。
按 Ctrl + S 存檔,VS Code 會自動下載這個套件(你會看到 flutter pub get 的字樣)。
第三步:撰寫控制程式碼
打開 lib/main.dart。
刪除裡面所有的程式碼,直接把下面這段完整內容貼進去:
第四步:權限設定 (Android 網路權限)
雖然現在打包通常會過,但保險起見,我們確認一下網路權限:
打開 android/app/src/main/AndroidManifest.xml。
確保在 <manifest ...> 標籤下方有一行:
<uses-permission android:name="android.permission.INTERNET" />
第五步:正式打包 APK
在 VS Code 底部開啟 Terminal。
輸入以下指令開始打包:
等待 1-3 分鐘。當你看到 √ Built build\app\outputs\flutter-apk\app-arm64-v8a-release.apk 時,就完成了!
第六步:取出 APK 安裝
在 VS Code 檔案清單中,對著 build/app/outputs/flutter-apk/app-arm64-v8a-release.apk 按右鍵。
選擇 「在檔案總管中顯示」。
將這個檔案傳送到你的手機安裝。
完整的 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>


















接線檢查:
#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;
}
}
沒有留言:
張貼留言