2026年10月3日 星期六

ESP32-Flutter-MQTT-IoT-Controller

 ESP32-Flutter-MQTT-IoT-Controller

這套系統包含了 Flutter 跨平台 App、ESP32 韌體 與 MQTT 雲端傳輸機制。






專案中採用的 功能命名原則與對應關係 整理如下:

1. 核心功能名稱對照表

功能區塊Python / Flutter 變數與函式命名說明 / 作用
MQTT 連線設定_setupMqttClient()初始化 MQTT Client,設定 Broker、Port、Client ID 並進行連線。
發送控制指令_sendCmd(String cmd)將控制指令發送至 MQTT 主題(Topic: alex9ufo/led)。
接收狀態解析_handleStatusPayload(String payload)解析 ESP32 回傳的狀態字串(1on ~ 4off, allon, alloff),並同步更新介面上的燈態指示燈。
訊息接收監聽_onMessageReceived(...)監聽所有已訂閱主題(溫濕度、狀態)的最新訊息,並調用對應的處理程序。

2. MQTT Topic(主題)命名規範

  • alex9ufo/temp:溫度數據(ESP32 上傳至 App/Web)

  • alex9ufo/humi:濕度數據(ESP32 上傳至 App/Web)

  • alex9ufo/led:控制指令(App/Web 發送至 ESP32)

  • alex9ufo/status:燈態即時狀態(ESP32 回傳至 App/Web)

3. 控制指令 Payload(有效載荷)命名規範

  • 1on / 1off:第 1 顆 LED / Relay 開啟或關閉

  • 2on / 2off:第 2 顆 LED / Relay 開啟或關閉

  • 3on / 3off:第 3 顆 LED / Relay 開啟或關閉

  • 4on / 4off:第 4 顆 LED / Relay 開啟或關閉

  • allon:全開所有 LED / Relay

  • alloff:全關所有 LED / Relay


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

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

完整重新編譯並執行
flutter run

支援 Flutter Web (WebSocket) 的完整版 lib/main.dart 程式碼。此程式碼可在 Chrome / Edge 瀏覽器中透過 WebSocket (ws://[broker.hivemq.com:8000/mqtt](https://broker.hivemq.com:8000/mqtt)) 連線,並與 ESP32 (Wokwi) 的 TCP Port (1883) 互通數據。  (Chrome / Edge 瀏覽器 執行)
import 'dart:async';

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ESP32 MQTT 控制與燈態中心',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const MqttControlPage(),
    );
  }
}

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

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

class _MqttControlPageState extends State<MqttControlPage> {
  // --- MQTT 設定 (適用於 Web 的 WebSocket 埠號) ---
  static const String broker = 'broker.hivemq.com';
  static const int port = 8000; // HiveMQ WebSocket 專用 Port

  static const String topicTemp = 'alex9ufo/temp';
  static const String topicHumi = 'alex9ufo/humi';
  static const String topicLed = 'alex9ufo/led';
  static const String topicStatus = 'alex9ufo/status';

  // --- 狀態變數 ---
  String _temp = '--';
  String _humi = '--';
  String _latestStatus = '尚未接收';
  final List<bool> _ledStates = [false, false, false, false];

  late MqttBrowserClient _client;
  bool _isConnected = false;

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

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

  // 初始化並透過 WebSocket 連線 MQTT Broker
  Future<void> _setupMqttClient() async {
    final String clientIdentifier =
        'flutter_web_${DateTime.now().millisecondsSinceEpoch}';

    // 建立瀏覽器專用的 WebSocket Client
    _client = MqttBrowserClient('ws://$broker/mqtt', clientIdentifier);
    _client.port = port;
    _client.keepAlivePeriod = 60;

    // 開啟詳細除錯 Log
    _client.logging(on: true);

    // 註冊事件 Listener
    _client.onConnected = () {
      print('[MQTT 事件] Web 端 WebSocket 連線成功!');
      if (mounted) {
        setState(() => _isConnected = true);
      }
    };

    _client.onDisconnected = () {
      print('[MQTT 事件] Web 端已斷線');
      if (mounted) {
        setState(() => _isConnected = false);
      }
    };

    _client.onSubscribed = (String topic) {
      print('[MQTT 事件] 成功訂閱主題: $topic');
    };

    final connMessage = MqttConnectMessage()
        .withClientIdentifier(clientIdentifier)
        .startClean()
        .withWillQos(MqttQos.atMostOnce);
    _client.connectionMessage = connMessage;

    try {
      print('[MQTT] 正在透過 WebSocket 連線至 ws://$broker:$port/mqtt ...');
      await _client.connect();
    } catch (e) {
      print('[MQTT 連線例外] 錯誤訊息: $e');
      _client.disconnect();
      return;
    }

    if (_client.connectionStatus?.state == MqttConnectionState.connected) {
      // 訂閱主題
      _client.subscribe(topicTemp, MqttQos.atMostOnce);
      _client.subscribe(topicHumi, MqttQos.atMostOnce);
      _client.subscribe(topicStatus, MqttQos.atMostOnce);

      // 監聽接收訊息
      _client.updates?.listen(_onMessageReceived);
    } else {
      print('[MQTT 連線失敗] 狀態: ${_client.connectionStatus?.state}');
      _client.disconnect();
    }
  }

  // 處理收到的 MQTT 訊息
  void _onMessageReceived(List<MqttReceivedMessage<MqttMessage>> c) {
    final MqttPublishMessage recMessage = c[0].payload as MqttPublishMessage;
    final String payload = MqttPublishPayload.bytesToStringAsString(
      recMessage.payload.message,
    ).trim();
    final String topic = c[0].topic;

    print('[MQTT 收到] 主題: $topic | 內容: $payload');

    if (!mounted) return;
    setState(() {
      if (topic == topicTemp) {
        _temp = payload;
      } else if (topic == topicHumi) {
        _humi = payload;
      } else if (topic == topicStatus) {
        _latestStatus = payload;
        _handleStatusPayload(payload);
      }
    });
  }

  // 解析 Status Payload 並更新燈態
  void _handleStatusPayload(String payload) {
    print("[除錯] 解析狀態 Payload: '$payload'");

    if (['1on', '2on', '3on', '4on'].contains(payload)) {
      int idx = int.parse(payload[0]) - 1;
      _ledStates[idx] = true;
    } else if (['1off', '2off', '3off', '4off'].contains(payload)) {
      int idx = int.parse(payload[0]) - 1;
      _ledStates[idx] = false;
    } else if (payload == 'allon') {
      for (int i = 0; i < 4; i++) {
        _ledStates[i] = true;
      }
    } else if (payload == 'alloff') {
      for (int i = 0; i < 4; i++) {
        _ledStates[i] = false;
      }
    }
  }

  // 發送 MQTT 指令
  void _sendCmd(String cmd) {
    if (!_isConnected) {
      print('[MQTT 錯誤] 未連線,無法發送指令');
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('MQTT 未連線,請點擊右上角重連'),
          duration: Duration(seconds: 2),
        ),
      );
      return;
    }
    print('[MQTT 發送] 指令: $cmd');
    final builder = MqttClientPayloadBuilder();
    builder.addString(cmd);
    _client.publishMessage(topicLed, MqttQos.atMostOnce, builder.payload!);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ESP32 MQTT 控制與燈態中心'),
        centerTitle: true,
        actions: [
          IconButton(
            icon: Icon(
              _isConnected ? Icons.cloud_done : Icons.cloud_off,
              color: _isConnected ? Colors.green : Colors.red,
            ),
            onPressed: () {
              if (!_isConnected) {
                _setupMqttClient();
              }
            },
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 1. 環境數據卡片
            _buildSensorCard(),
            const SizedBox(height: 15),

            // 2. LED 控制卡片
            _buildControlCard(),
            const SizedBox(height: 20),

            // 3. 最新狀態顯示
            Text(
              '最新狀態: $_latestStatus',
              style: const TextStyle(
                fontSize: 14,
                fontStyle: FontStyle.italic,
                color: Colors.grey,
              ),
            ),
          ],
        ),
      ),
    );
  }

  // 環境數據 UI 區塊
  Widget _buildSensorCard() {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '環境數據',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const Divider(),
            Row(
              children: [
                const Text('溫度:', style: TextStyle(fontSize: 16)),
                const SizedBox(width: 15),
                Text(
                  '$_temp °C',
                  style: const TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.red,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 10),
            Row(
              children: [
                const Text('濕度:', style: TextStyle(fontSize: 16)),
                const SizedBox(width: 15),
                Text(
                  '$_humi %',
                  style: const TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.blue,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  // LED 控制與實時狀態 UI 區塊
  Widget _buildControlCard() {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'LED 控制與實時狀態',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const Divider(),
            for (int i = 0; i < 4; i++) _buildLedRow(i),
            const SizedBox(height: 10),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: () => _sendCmd('allon'),
                    child: const Text('全開 (allon)'),
                  ),
                ),
                const SizedBox(width: 10),
                Expanded(
                  child: ElevatedButton(
                    onPressed: () => _sendCmd('alloff'),
                    child: const Text('全關 (alloff)'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  // 單排 LED 控制列
  Widget _buildLedRow(int index) {
    final bool isOn = _ledStates[index];
    final Color ledColor = isOn
        ? const Color(0xFF00FF00)
        : const Color(0xFF1E1E1E);

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6.0),
      child: Row(
        children: [
          Text(
            'LED ${index + 1}:',
            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
          ),
          const SizedBox(width: 10),

          // 模擬指示燈
          Container(
            width: 24,
            height: 24,
            decoration: BoxDecoration(
              color: ledColor,
              shape: BoxShape.circle,
              border: Border.all(color: Colors.grey, width: 1.5),
            ),
          ),
          const Spacer(),

          // 開啟按鈕
          OutlinedButton(
            onPressed: () => _sendCmd('${index + 1}on'),
            style: OutlinedButton.styleFrom(
              padding: const EdgeInsets.symmetric(horizontal: 12),
            ),
            child: const Text('開啟'),
          ),
          const SizedBox(width: 8),

          // 關閉按鈕
          OutlinedButton(
            onPressed: () => _sendCmd('${index + 1}off'),
            style: OutlinedButton.styleFrom(
              padding: const EdgeInsets.symmetric(horizontal: 12),
            ),
            child: const Text('關閉'),
          ),
        ],
      ),
    );
  }
}



不同版本 (手機版本)

import 'dart:async';

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 MQTT 控制與燈態中心',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const MqttControlPage(),
    );
  }
}

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

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

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

  static const String topicTemp = 'alex9ufo/temp';
  static const String topicHumi = 'alex9ufo/humi';
  static const String topicLed = 'alex9ufo/led';
  static const String topicStatus = 'alex9ufo/status';

  // --- 狀態變數 ---
  String _temp = '--';
  String _humi = '--';
  String _latestStatus = '尚未接收';
  final List<bool> _ledStates = [false, false, false, false];

  late MqttServerClient _client;
  bool _isConnected = false;

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

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

  // 初始化並連線 MQTT Broker
  Future<void> _setupMqttClient() async {
    final String clientIdentifier =
        'flutter_client_${DateTime.now().millisecondsSinceEpoch}';
    _client = MqttServerClient(broker, clientIdentifier);
    _client.port = port;
    _client.keepAlivePeriod = 60;

    // --- 開啟詳細的除錯 Log ---
    _client.logging(on: true);

    // --- 註冊生命週期 Callback ---
    _client.onConnected = () {
      print('[MQTT 事件] 連線成功!');
      if (mounted) {
        setState(() => _isConnected = true);
      }
    };

    _client.onDisconnected = () {
      print('[MQTT 事件] 已斷線,狀態碼: ${_client.connectionStatus?.returnCode}');
      if (mounted) {
        setState(() => _isConnected = false);
      }
    };

    _client.onSubscribed = (String topic) {
      print('[MQTT 事件] 成功訂閱主題: $topic');
    };

    final connMessage = MqttConnectMessage()
        .withClientIdentifier(clientIdentifier)
        .startClean()
        .withWillQos(MqttQos.atMostOnce);
    _client.connectionMessage = connMessage;

    try {
      print('[MQTT] 正在嘗試連線至 $broker:$port...');
      await _client.connect();
    } catch (e) {
      print('[MQTT 連線例外] 錯誤訊息: $e');
      _client.disconnect();
      return;
    }

    if (_client.connectionStatus?.state == MqttConnectionState.connected) {
      // 訂閱主題
      _client.subscribe(topicTemp, MqttQos.atMostOnce);
      _client.subscribe(topicHumi, MqttQos.atMostOnce);
      _client.subscribe(topicStatus, MqttQos.atMostOnce);

      // 監聽接收訊息
      _client.updates?.listen(_onMessageReceived);
    } else {
      print('[MQTT 連線失敗] 狀態: ${_client.connectionStatus?.state}');
      _client.disconnect();
    }
  }

  // 處理收到的 MQTT 訊息
  void _onMessageReceived(List<MqttReceivedMessage<MqttMessage>> c) {
    final MqttPublishMessage recMessage = c[0].payload as MqttPublishMessage;
    final String payload = MqttPublishPayload.bytesToStringAsString(
      recMessage.payload.message,
    ).trim();
    final String topic = c[0].topic;

    print('[MQTT 收到] 主題: $topic | 內容: $payload');

    if (!mounted) return;
    setState(() {
      if (topic == topicTemp) {
        _temp = payload;
      } else if (topic == topicHumi) {
        _humi = payload;
      } else if (topic == topicStatus) {
        _latestStatus = payload;
        _handleStatusPayload(payload);
      }
    });
  }

  // 解析 Status Payload 並更新燈態
  void _handleStatusPayload(String payload) {
    print("[除錯] 解析狀態 Payload: '$payload'");

    if (['1on', '2on', '3on', '4on'].contains(payload)) {
      int idx = int.parse(payload[0]) - 1;
      _ledStates[idx] = true;
    } else if (['1off', '2off', '3off', '4off'].contains(payload)) {
      int idx = int.parse(payload[0]) - 1;
      _ledStates[idx] = false;
    } else if (payload == 'allon') {
      for (int i = 0; i < 4; i++) {
        _ledStates[i] = true;
      }
    } else if (payload == 'alloff') {
      for (int i = 0; i < 4; i++) {
        _ledStates[i] = false;
      }
    }
  }

  // 發送 MQTT 指令
  void _sendCmd(String cmd) {
    if (!_isConnected) {
      print('[MQTT 錯誤] 未連線,無法發送指令');
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('MQTT 未連線,請確認網路或重新整理'),
          duration: Duration(seconds: 2),
        ),
      );
      return;
    }
    print('[MQTT 發送] 指令: $cmd');
    final builder = MqttClientPayloadBuilder();
    builder.addString(cmd);
    _client.publishMessage(topicLed, MqttQos.atMostOnce, builder.payload!);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ESP32 MQTT 控制與燈態中心'),
        centerTitle: true,
        actions: [
          IconButton(
            icon: Icon(
              _isConnected ? Icons.cloud_done : Icons.cloud_off,
              color: _isConnected ? Colors.green : Colors.red,
            ),
            onPressed: () {
              if (!_isConnected) {
                _setupMqttClient();
              }
            },
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 1. 環境數據卡片
            _buildSensorCard(),
            const SizedBox(height: 15),

            // 2. LED 控制卡片
            _buildControlCard(),
            const SizedBox(height: 20),

            // 3. 最新狀態顯示
            Text(
              '最新狀態: $_latestStatus',
              style: const TextStyle(
                fontSize: 14,
                fontStyle: FontStyle.italic,
                color: Colors.grey,
              ),
            ),
          ],
        ),
      ),
    );
  }

  // 環境數據 UI 區塊
  Widget _buildSensorCard() {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '環境數據',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const Divider(),
            Row(
              children: [
                const Text('溫度:', style: TextStyle(fontSize: 16)),
                const SizedBox(width: 15),
                Text(
                  '$_temp °C',
                  style: const TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.red,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 10),
            Row(
              children: [
                const Text('濕度:', style: TextStyle(fontSize: 16)),
                const SizedBox(width: 15),
                Text(
                  '$_humi %',
                  style: const TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.blue,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  // LED 控制與實時狀態 UI 區塊
  Widget _buildControlCard() {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'LED 控制與實時狀態',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const Divider(),
            for (int i = 0; i < 4; i++) _buildLedRow(i),
            const SizedBox(height: 10),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: () => _sendCmd('allon'),
                    child: const Text('全開 (allon)'),
                  ),
                ),
                const SizedBox(width: 10),
                Expanded(
                  child: ElevatedButton(
                    onPressed: () => _sendCmd('alloff'),
                    child: const Text('全關 (alloff)'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  // 單排 LED 控制列
  Widget _buildLedRow(int index) {
    final bool isOn = _ledStates[index];
    final Color ledColor = isOn
        ? const Color(0xFF00FF00)
        : const Color(0xFF1E1E1E);

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6.0),
      child: Row(
        children: [
          Text(
            'LED ${index + 1}:',
            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
          ),
          const SizedBox(width: 10),

          // 模擬指示燈
          Container(
            width: 24,
            height: 24,
            decoration: BoxDecoration(
              color: ledColor,
              shape: BoxShape.circle,
              border: Border.all(color: Colors.grey, width: 1.5),
            ),
          ),
          const Spacer(),

          // 開啟按鈕
          OutlinedButton(
            onPressed: () => _sendCmd('${index + 1}on'),
            style: OutlinedButton.styleFrom(
              padding: const EdgeInsets.symmetric(horizontal: 12),
            ),
            child: const Text('開啟'),
          ),
          const SizedBox(width: 8),

          // 關閉按鈕
          OutlinedButton(
            onPressed: () => _sendCmd('${index + 1}off'),
            style: OutlinedButton.styleFrom(
              padding: const EdgeInsets.symmetric(horizontal: 12),
            ),
            child: const Text('關閉'),
          ),
        ],
      ),
    );
  }
}

沒有留言:

張貼留言

ESP32-Flutter-MQTT-IoT-Controller

 ESP32-Flutter-MQTT-IoT-Controller 這套系統包含了 Flutter 跨平台 App 、 ESP32 韌體 與 MQTT 雲端傳輸機制 。 專案中採用的 功能命名原則與對應關係 整理如下: 1. 核心功能名稱對照表 功能區塊 Python / ...