Flutter代码范例 - 通过OP-BT / OP-BTS 蓝牙光电头读取 IEC 62056-21 规约的电表测试程序

Flutter读表测试 - 通过蓝牙光电头读取支持IEC62056-21规约的电表测试程序

1.环境准备

2.程序说明

概述

本程序演示了如何使用 Flutter 和 BLE 技术与 OPBT 设备进行 IEC 协议通信。程序实现了设备扫描、连接和数据交换的完整流程。

主要功能

  1. 扫描附近的蓝牙设备
  2. 连接到选定的 OPBT 设备
  3. 使用 IEC 协议与设备通信
  4. 实时显示接收到的数据
  5. 支持数据清除和持续监控

技术要点

  1. 蓝牙通信:使用 flutter_blue_plus 库实现
  2. 权限管理:使用 permission_handler 库处理
  3. 数据协议:实现 IEC 协议的电表读取
  4. 异步编程:使用 async/await 处理蓝牙操作
  5. 实时数据显示:持续监控和显示数据

通信协议

程序遵循 IEC 协议进行电表读取:

  1. 握手命令:/?!\r\n
  2. 设备响应例如:/LGZ5yyyyyyyyyyyyyyyyyyy\r\n(其中'5’为标识符)
  3. 数据请求命令:0x06 0x30 [标识符] 0x30 0x0D 0x0A
  4. 数据响应:来自电表的连续数据流

开发注意事项

  1. UUID 配置:

    • 服务 UUID:“18F0”
    • 通知特征 UUID:“2AF0”
    • 写入特征 UUID:“2AF1”
  2. 通信流程:

    • 建立连接
    • 发送握手命令
    • 接收并验证响应
    • 发送数据请求命令
    • 持续接收和显示数据
  3. 重要考虑事项:

    • 命令之间的适当时间间隔
    • 数据格式验证
    • 通信失败的错误处理
    • 连续数据的缓冲区管理
  4. Android 要求:

    • AndroidManifest.xml 中的蓝牙权限
    • 蓝牙扫描的位置权限
    • Android 12+ 特定权限

扩展开发

本程序可以扩展用于:

  1. 读取额外的电表参数
  2. 数据记录和分析
  3. 多设备管理
  4. 自定义命令实现
  5. 数据导出功能

界面组件

  1. 设备列表区域:

    • 设备扫描控制
    • 设备列表显示
    • 连接状态
  2. 数据显示区域:

    • 实时数据显示
    • 数据清除选项
    • 自动滚动到最新数据

错误处理

程序包含全面的错误处理,包括:

  1. 连接失败
  2. 通信超时
  3. 无效数据格式
  4. 权限问题
  5. 设备断开连接

程序部分代码说明

  • 与蓝牙有关的服务
  // UUIDs for Bluetooth service and characteristics
  final String SERVICE_UUID = "18F0"; // Service UUID
  final String CHARACTERISTIC_UUID_NOTIFY = "2AF0"; // Notification characteristic UUID
  final String CHARACTERISTIC_UUID_WRITE = "2AF1"; // Write characteristic UUID
  • 程序运行后,首先要申请蓝牙服务相关的权限
  /// Request Bluetooth and location permissions
  /// These permissions are required for Bluetooth scanning and connection
  Future<void> _requestPermissions() async {
    await Permission.bluetooth.request();
    await Permission.bluetoothScan.request();
    await Permission.bluetoothConnect.request();
    await Permission.location.request();
  }
  • 蓝牙扫描相关代码
  /// Start scanning for Bluetooth devices
  /// Use flutter_blue_plus library to scan for nearby Bluetooth devices
  void startScan() async {
    setState(() {
      scanResults.clear();
      isScanning = true;
    });

    // Ensure Bluetooth adapter is ready
    await FlutterBluePlus.adapterState.first;
    // Start scanning with 4 second timeout
    await FlutterBluePlus.startScan(timeout: const Duration(seconds: 4));

    // Listen for scan results
    FlutterBluePlus.scanResults.listen((results) {
      setState(() {
        scanResults = results;
      });
    });
    // Listen for scan status
    FlutterBluePlus.isScanning.listen((scanning) {
      setState(() {
        isScanning = scanning;
      });
    });
  }
  • 蓝牙设备连接相关代码,并在蓝牙设备连接上后,扫描相应的服务,并连接和监听
  /// Connect to a Bluetooth device
  /// [device] The device to connect to
  /// Sets up the connection and starts the communication process
  Future<void> connectToDevice(BluetoothDevice device) async {
    try {
      // Stop any ongoing data reception
      await stopReceiving();

      // Connect to the device
      await device.connect();

      // Discover services
      List<BluetoothService> services = await device.discoverServices();
      var service = services.firstWhere(
        (s) => s.uuid.toString().toUpperCase().contains(SERVICE_UUID),
      );

      // Get characteristics for communication
      writeCharacteristic = service.characteristics.firstWhere(
        (c) =>
            c.uuid.toString().toUpperCase().contains(CHARACTERISTIC_UUID_WRITE),
      );

      notifyCharacteristic = service.characteristics.firstWhere(
        (c) => c.uuid.toString().toUpperCase().contains(
          CHARACTERISTIC_UUID_NOTIFY,
        ),
      );

      // Enable notifications and start listening
      await notifyCharacteristic!.setNotifyValue(true);

      // Set up continuous notification listening
      dataSubscription = notifyCharacteristic!.value.listen((value) {
        if (value.isNotEmpty) {
          String data = String.fromCharCodes(value);
          // Save received data to buffer
          dataBuffer.write(data);
        }
      });

      setState(() {
        connectedDevice = device;
        receivedData = "Device connected";
      });

      // Start the communication process
      await startCommunication();
    } catch (e) {
      setState(() {
        receivedData = "Connection failed: $e";
      });
    }
  }
  • 当已经连接上相应的服务后,发送相应的命令给蓝牙设备,获取当前电池的电压
  /// Start communication with the device
  /// Implements the IEC protocol communication sequence:
  /// 1. Send handshake command
  /// 2. Receive and validate response
  /// 3. Send data request command
  /// 4. Continuously receive data
  Future<void> startCommunication() async {
    try {
      // Send handshake command
      bool sent = await sendData(utf8.encode('/?!\r\n'));
      if (!sent) {
        throw Exception('Failed to send handshake command');
      }
      await Future.delayed(const Duration(milliseconds: 1000));

      // Receive handshake response
      String? response = await receiveData();
      // Check received data
      if (response != null && response.isNotEmpty) {
        // Check if first character is '/'
        if (response.startsWith('/')) {
          // Ensure response length is sufficient to extract 5th character
          if (response.length >= 5) {
            // Extract 5th character as identifier
            String fifthChar = response[4];

            // Build and send data request command
            List<int> command = [
              0x06, // Start of frame
              0x30, // Data request
              fifthChar.codeUnitAt(0), // Identifier
              0x30, // Data request
              0x0D, // Carriage return
              0x0A, // Line feed
            ];

            bool sent = await sendData(command);
            if (!sent) {
              throw Exception('Failed to send data request command');
            }

            // Loop to receive data
            for (int i = 0; i < 20000; i++) {
              // Receive data with 5 second timeout
              String? receivedResponse = await receiveData(
                timeout: const Duration(milliseconds: 5000),
              );

              if (receivedResponse != null && receivedResponse.isNotEmpty) {
                // Update UI with received data
                setState(() {
                  receivedData = '$receivedData$receivedResponse';
                });
                // Scroll to show latest data
                _scrollToBottom();
              } else {
                break;
              }
            }
          } else {
            throw Exception('Response length insufficient');
          }
        } else {
          throw Exception('Invalid data format');
        }
      } else {
        throw Exception('No data received');
      }

      setState(() {
        isReceiving = true;
      });
    } catch (e) {
      setState(() {
        receivedData = "Communication error: $e";
      });
    }
  }
  • 发送数据模块
  /// Send data to the connected device
  /// [data] The data to send as a list of integers
  /// Returns true if the data was sent successfully, false otherwise
  Future<bool> sendData(List<int> data) async {
    try {
      if (writeCharacteristic == null) {
        return false;
      }
      await writeCharacteristic!.write(data);
      return true;
    } catch (e) {
      return false;
    }
  }
  • 接收数据模块

  /// Receive data from the device
  /// [timeout] Maximum time to wait for data, defaults to 5 seconds
  /// Returns the received data as a string, or null if timeout or error occurs
  Future<String?> receiveData({
    Duration timeout = const Duration(seconds: 5),
  }) async {
    try {
      if (notifyCharacteristic == null) {
        return null;
      }

      Completer<String?> completer = Completer<String?>();
      isWaitingForData = true;

      // Set up timeout
      Timer(timeout, () {
        if (!completer.isCompleted) {
          isWaitingForData = false;
          completer.complete(null);
        }
      });

      // Periodically check for new data
      Timer.periodic(const Duration(milliseconds: 100), (timer) {
        if (dataBuffer.isNotEmpty) {
          String data = dataBuffer.toString();
          dataBuffer.clear();
          isWaitingForData = false;
          timer.cancel();
          completer.complete(data);
        }
      });

      return await completer.future;
    } catch (e) {
      isWaitingForData = false;
      return null;
    }
  }

3.程序代码安装和运行

首先创建一个新的 Flutter 项目:

flutter create opbt_iec_meter_read
cd opbt_iec_meter_read

然后在 opbt_iec_meter_read/pubspec.yaml 中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_blue_plus: ^1.31.13
  permission_handler: ^11.3.0

修改 Android 权限配置: opbt_iec_meter_read/android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
        <!-- 添加蓝牙权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    
    <!-- Android 12+ 新增蓝牙权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" 
        android:usesPermissionFlags="neverForLocation"
        tools:targetApi="S" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    
    <!-- 声明蓝牙功能 -->
    <uses-feature
        android:name="android.hardware.bluetooth"
        android:required="true" />
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />

    <application
        android:label="opbt_iec_meter_read"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:taskAffinity=""
            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 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>

主程序代码: opbt_iec_meter_read/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:permission_handler/permission_handler.dart';

/// Application entry point
/// Initializes and runs the Flutter application
void main() {
  runApp(const MyApp());
}

/// Root widget of the application
/// Configures the application theme and sets up the home screen
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'OPBT Meter(IEC Protocol) Reader',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const DataReadScreen(),
    );
  }
}

/// Main screen for data reading
/// Handles device scanning, connection, and data display
class DataReadScreen extends StatefulWidget {
  const DataReadScreen({super.key});
  @override
  State<DataReadScreen> createState() => _DataReadScreenState();
}

/// State class for DataReadScreen
/// Manages the application state and implements the main functionality
class _DataReadScreenState extends State<DataReadScreen> {
  // List to store discovered Bluetooth devices
  List<ScanResult> scanResults = [];

  // Currently connected Bluetooth device
  BluetoothDevice? connectedDevice;

  // String to store and display received data
  String receivedData = "Waiting for data...";

  // Flag to track scanning status
  bool isScanning = false;

  // Flag to track data receiving status
  bool isReceiving = false;

  // Subscription for data notifications
  StreamSubscription? dataSubscription;

  // Controller for scrolling the data display
  final ScrollController _scrollController = ScrollController();

  // Bluetooth communication related variables
  BluetoothCharacteristic?
  writeCharacteristic; // Characteristic for sending data
  BluetoothCharacteristic?
  notifyCharacteristic; // Characteristic for receiving notifications

  // UUIDs for Bluetooth service and characteristics
  final String SERVICE_UUID = "18F0"; // Service UUID
  final String CHARACTERISTIC_UUID_NOTIFY = "2AF0"; // Notification characteristic UUID
  final String CHARACTERISTIC_UUID_WRITE = "2AF1"; // Write characteristic UUID

  // Data buffer for storing received data
  StringBuffer dataBuffer = StringBuffer();

  // Variable to store the last received data
  String? lastReceivedData;

  // Flag to track if we're waiting for data
  bool isWaitingForData = false;

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

  /// Request necessary permissions for Bluetooth and location
  /// Required for scanning and connecting to Bluetooth devices
  Future<void> _requestPermissions() async {
    await Permission.bluetooth.request();
    await Permission.bluetoothScan.request();
    await Permission.bluetoothConnect.request();
    await Permission.location.request();
  }

  /// Start scanning for nearby Bluetooth devices
  /// Updates the UI with discovered devices
  void startScan() async {
    setState(() {
      scanResults.clear();
      isScanning = true;
    });

    // Ensure Bluetooth adapter is ready
    await FlutterBluePlus.adapterState.first;
    // Start scanning with 4 second timeout
    await FlutterBluePlus.startScan(timeout: const Duration(seconds: 4));

    // Listen for scan results
    FlutterBluePlus.scanResults.listen((results) {
      setState(() {
        scanResults = results;
      });
    });

    // Listen for scan status
    FlutterBluePlus.isScanning.listen((scanning) {
      setState(() {
        isScanning = scanning;
      });
    });
  }

  /// Send data to the connected device
  /// [data] The data to send as a list of integers
  /// Returns true if the data was sent successfully, false otherwise
  Future<bool> sendData(List<int> data) async {
    try {
      if (writeCharacteristic == null) {
        return false;
      }
      await writeCharacteristic!.write(data);
      return true;
    } catch (e) {
      return false;
    }
  }

  /// Receive data from the device
  /// [timeout] Maximum time to wait for data, defaults to 5 seconds
  /// Returns the received data as a string, or null if timeout or error occurs
  Future<String?> receiveData({
    Duration timeout = const Duration(seconds: 5),
  }) async {
    try {
      if (notifyCharacteristic == null) {
        return null;
      }

      Completer<String?> completer = Completer<String?>();
      isWaitingForData = true;

      // Set up timeout
      Timer(timeout, () {
        if (!completer.isCompleted) {
          isWaitingForData = false;
          completer.complete(null);
        }
      });

      // Periodically check for new data
      Timer.periodic(const Duration(milliseconds: 100), (timer) {
        if (dataBuffer.isNotEmpty) {
          String data = dataBuffer.toString();
          dataBuffer.clear();
          isWaitingForData = false;
          timer.cancel();
          completer.complete(data);
        }
      });

      return await completer.future;
    } catch (e) {
      isWaitingForData = false;
      return null;
    }
  }

  /// Connect to a Bluetooth device
  /// [device] The device to connect to
  /// Sets up the connection and starts the communication process
  Future<void> connectToDevice(BluetoothDevice device) async {
    try {
      // Stop any ongoing data reception
      await stopReceiving();

      // Connect to the device
      await device.connect();

      // Discover services
      List<BluetoothService> services = await device.discoverServices();
      var service = services.firstWhere(
        (s) => s.uuid.toString().toUpperCase().contains(SERVICE_UUID),
      );

      // Get characteristics for communication
      writeCharacteristic = service.characteristics.firstWhere(
        (c) =>
            c.uuid.toString().toUpperCase().contains(CHARACTERISTIC_UUID_WRITE),
      );

      notifyCharacteristic = service.characteristics.firstWhere(
        (c) => c.uuid.toString().toUpperCase().contains(
          CHARACTERISTIC_UUID_NOTIFY,
        ),
      );

      // Enable notifications and start listening
      await notifyCharacteristic!.setNotifyValue(true);

      // Set up continuous notification listening
      dataSubscription = notifyCharacteristic!.value.listen((value) {
        if (value.isNotEmpty) {
          String data = String.fromCharCodes(value);
          // Save received data to buffer
          dataBuffer.write(data);
        }
      });

      setState(() {
        connectedDevice = device;
        receivedData = "Device connected";
      });

      // Start the communication process
      await startCommunication();
    } catch (e) {
      setState(() {
        receivedData = "Connection failed: $e";
      });
    }
  }

  /// Start communication with the device
  /// Implements the IEC protocol communication sequence:
  /// 1. Send handshake command
  /// 2. Receive and validate response
  /// 3. Send data request command
  /// 4. Continuously receive data
  Future<void> startCommunication() async {
    try {
      // Send handshake command
      bool sent = await sendData(utf8.encode('/?!\r\n'));
      if (!sent) {
        throw Exception('Failed to send handshake command');
      }
      await Future.delayed(const Duration(milliseconds: 1000));

      // Receive handshake response
      String? response = await receiveData();
      // Check received data
      if (response != null && response.isNotEmpty) {
        // Check if first character is '/'
        if (response.startsWith('/')) {
          // Ensure response length is sufficient to extract 5th character
          if (response.length >= 5) {
            // Extract 5th character as identifier
            String fifthChar = response[4];

            // Build and send data request command
            List<int> command = [
              0x06, // Start of frame
              0x30, // Data request
              fifthChar.codeUnitAt(0), // Identifier
              0x30, // Data request
              0x0D, // Carriage return
              0x0A, // Line feed
            ];

            bool sent = await sendData(command);
            if (!sent) {
              throw Exception('Failed to send data request command');
            }

            // Loop to receive data
            for (int i = 0; i < 20000; i++) {
              // Receive data with 5 second timeout
              String? receivedResponse = await receiveData(
                timeout: const Duration(milliseconds: 5000),
              );

              if (receivedResponse != null && receivedResponse.isNotEmpty) {
                // Update UI with received data
                setState(() {
                  receivedData = '$receivedData$receivedResponse';
                });
                // Scroll to show latest data
                _scrollToBottom();
              } else {
                break;
              }
            }
          } else {
            throw Exception('Response length insufficient');
          }
        } else {
          throw Exception('Invalid data format');
        }
      } else {
        throw Exception('No data received');
      }

      setState(() {
        isReceiving = true;
      });
    } catch (e) {
      setState(() {
        receivedData = "Communication error: $e";
      });
    }
  }

  /// Stop receiving data and clean up resources
  Future<void> stopReceiving() async {
    if (dataSubscription != null) {
      await dataSubscription!.cancel();
      dataSubscription = null;
    }
    dataBuffer.clear();
    lastReceivedData = null;
    isWaitingForData = false;
    setState(() {
      isReceiving = false;
    });
  }

  /// Scroll the data display to show the latest content
  void _scrollToBottom() {
    if (_scrollController.hasClients) {
      _scrollController.animateTo(
        _scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    }
  }

  @override
  void dispose() {
    _scrollController.dispose();
    stopReceiving();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('OPBT Meter(IEC Protocol) Reader'),
        backgroundColor: Colors.blue,
      ),
      body: Column(
        children: [
          // Upper part: Device list and control area
          Expanded(
            flex: 1,
            child: Container(
              width: double.infinity,
              margin: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.blue.shade200),
                borderRadius: BorderRadius.circular(8),
                color: Colors.blue.shade50,
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Display connection status
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Text(
                      connectedDevice != null
                          ? 'Connected device: ${connectedDevice!.name}'
                          : 'No device connected',
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: Colors.blue,
                      ),
                    ),
                  ),

                  // Scan button
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: ElevatedButton(
                      onPressed: isScanning ? null : startScan,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.blue,
                        foregroundColor: Colors.white,
                        padding: const EdgeInsets.symmetric(
                          horizontal: 32,
                          vertical: 12,
                        ),
                      ),
                      child: Text(isScanning ? 'Scanning...' : 'Scan Devices'),
                    ),
                  ),

                  // Device list
                  Expanded(
                    child: Container(
                      width: double.infinity,
                      margin: const EdgeInsets.all(8),
                      decoration: BoxDecoration(
                        border: Border.all(color: Colors.blue.shade200),
                        borderRadius: BorderRadius.circular(8),
                        color: Colors.white,
                      ),
                      child: ListView.builder(
                        itemCount: scanResults.length,
                        itemBuilder: (context, index) {
                          ScanResult result = scanResults[index];
                          return ListTile(
                            title: Text(
                              result.device.name.isEmpty
                                  ? 'Unknown Device'
                                  : result.device.name,
                              style: const TextStyle(
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            subtitle: Text(
                              result.device.id.toString(),
                              style: TextStyle(color: Colors.grey.shade600),
                            ),
                            onTap: () => connectToDevice(result.device),
                            tileColor: Colors.white,
                            shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(8),
                            ),
                          );
                        },
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),

          // Lower part: Receive information display area
          Expanded(
            flex: 2,
            child: Container(
              width: double.infinity,
              margin: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.blue.shade200),
                borderRadius: BorderRadius.circular(8),
                color: Colors.blue.shade50,
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      const Padding(
                        padding: EdgeInsets.all(8.0),
                        child: Text(
                          'Received Data',
                          style: TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                            color: Colors.blue,
                          ),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: ElevatedButton(
                          onPressed: () {
                            setState(() {
                              receivedData = "Waiting for data...";
                            });
                          },
                          style: ElevatedButton.styleFrom(
                            backgroundColor: Colors.blue,
                            foregroundColor: Colors.white,
                          ),
                          child: const Text('Clear'),
                        ),
                      ),
                    ],
                  ),
                  Expanded(
                    child: Container(
                      width: double.infinity,
                      margin: const EdgeInsets.all(8),
                      decoration: BoxDecoration(
                        border: Border.all(color: Colors.blue.shade200),
                        borderRadius: BorderRadius.circular(8),
                        color: Colors.white,
                      ),
                      child: SingleChildScrollView(
                        controller: _scrollController,
                        child: Padding(
                          padding: const EdgeInsets.all(12.0),
                          child: Text(
                            receivedData,
                            style: const TextStyle(fontSize: 16, height: 1.5),
                            textAlign: TextAlign.left,
                          ),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}