清除項目編譯快取
執行 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");
}
}
}
沒有留言:
張貼留言