Flutter Demo: Retrieve Meter Information from a IEC 62056-21 Standard Meter

Once you have flutter installed and configured. You can try to create your first project. Following code demonstrates how to create a flutter project, communicate with OP-BT/BTS bluetooth optical probe by using BLE and retrieve basic information from a IEC standard meter.

Code Features

  1. Scan bluetooth devices nearby
  2. Eastablish connection with specific device
  3. Communicate with meter by IEC standard protocal
  4. Display data received

Librarires

  1. Bluetooth Communication: flutter_blue_plus
  2. Permission: permission_handler

IEC Protocal

  1. Handshake:/?!\r\n
  2. Device Response:/LGZ5yyyyyyyyyyyyyyyyyyy\r\n
  3. Data Request: 0x06 0x30 [identifier] 0x30 0x0D 0x0A

UUID

  • Service UUID:“18F0”
  • Notification UUID:“2AF0”
  • Writing UUID:“2AF1”

1. Create a new project

flutter create opbt_iec_meter_read
cd opbt_iec_meter_read

Please note, this step may take a few minutes to a few hours, depends on if you already have dependent library installed.

2. add following to pubspec.yaml:

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

3. Modify Android Permission

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">
        <!-- Add bluetooth permission -->
    <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+ Add bluetooth permission -->
    <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" />
    
    <!-- declare bluetooth service -->
    <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>

4. Copy following code to your project

opbt_iec_meter_read/lib/main.dart

Here's your Flutter code with all comments translated to English (code remains unchanged):

```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
void main() {
  runApp(const MyApp());
}

/// Root component of the application
/// Sets the application theme and home page
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'OPBT Battery Voltage Reader',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const BatteryVoltageScreen(), // Set home page as battery voltage reading screen
    );
  }
}

/// Battery voltage reading screen
/// Used to scan, connect to Bluetooth devices and read battery voltage
class BatteryVoltageScreen extends StatefulWidget {
  const BatteryVoltageScreen({super.key});
  @override
  State<BatteryVoltageScreen> createState() => _BatteryVoltageScreenState();
}

class _BatteryVoltageScreenState extends State<BatteryVoltageScreen> {
  // Stores the list of scanned Bluetooth devices
  List<ScanResult> scanResults = [];
  // Currently connected Bluetooth device
  BluetoothDevice? connectedDevice;
  // Battery voltage display text
  String batteryVoltage = "Not read";
  // Whether device scanning is in progress
  bool isScanning = false;
  
  // Bluetooth UUID constants for OPBT devices
  // These UUIDs are device-specific and need to be configured according to the actual device
  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
  @override
  void initState() {
    super.initState();
    // Request necessary permissions during initialization
    _requestPermissions();
  }

  /// 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
  /// Uses flutter_blue_plus library to scan 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 selected Bluetooth device
  /// @param device Bluetooth device to connect to
  Future<void> connectToDevice(BluetoothDevice device) async {
    try {
      // Connect to device
      await device.connect();
      setState(() {
        connectedDevice = device;
      });
      
      // Read battery voltage after successful connection
      await readBatteryVoltage(device);
    } catch (e) {
      print('Connection failed: $e');
    }
  }

  /// Read device battery voltage
  /// Interacts with device through BLE communication protocol to obtain battery voltage information
  /// @param device Connected Bluetooth device
  Future<void> readBatteryVoltage(BluetoothDevice device) async {
    try {
      // Get list of services provided by the device
      List<BluetoothService> services = await device.discoverServices();
      // Find target service
      var service = services.firstWhere(
        (s) => s.uuid.toString().toUpperCase().contains(SERVICE_UUID)
      );
      
      // Get write characteristic
      var writeCharacteristic = service.characteristics.firstWhere(
        (c) => c.uuid.toString().toUpperCase().contains(CHARACTERISTIC_UUID_WRITE)
      );
      
      // Get notification characteristic
      var notifyCharacteristic = service.characteristics.firstWhere(
        (c) => c.uuid.toString().toUpperCase().contains(CHARACTERISTIC_UUID_NOTIFY)
      );
      
      // Set up notification listener to receive data from device
      await notifyCharacteristic.setNotifyValue(true);
      notifyCharacteristic.value.listen((value) {
        if (value.isNotEmpty) {
          String response = String.fromCharCodes(value);
          print('Received: $response');
          _parseBatteryVoltage(response);
        }
      });
      
      // Step 1: Enter command mode
      print('Sending: {"AtCommandMode":true}');
      await writeCharacteristic.write(utf8.encode('{"AtCommandMode":true}\r\n'));
      await Future.delayed(const Duration(milliseconds: 1000));
      
      // Step 2: Query battery voltage
      print('Sending: {"BatteryVoltage":"?"}');
      await writeCharacteristic.write(utf8.encode('{"BatteryVoltage":"?"}\r\n'));
      await Future.delayed(const Duration(milliseconds: 1000));
      
      // Step 3: Try other possible command formats
      print('Sending: {"GetBatteryVoltage":true}');
      await writeCharacteristic.write(utf8.encode('{"GetBatteryVoltage":true}\r\n'));
      await Future.delayed(const Duration(milliseconds: 1000));
      
      // Step 4: Exit command mode
      print('Sending: {"AtCommandMode":false}');
      await writeCharacteristic.write(utf8.encode('{"AtCommandMode":false}\r\n'));
      
    } catch (e) {
      print('Failed to read battery voltage: $e');
    }
  }

  /// Parse battery voltage data returned by device
  /// @param response JSON format string returned by device
  void _parseBatteryVoltage(String response) {
    try {
      print('Attempting to parse: $response');
      // Parse response into JSON object
      Map<String, dynamic> data = json.decode(response);
      // Check if it contains battery voltage field
      if (data.containsKey('BatteryVoltage')) {
        int voltage = data['BatteryVoltage'];
        print('Parsing successful, battery voltage: $voltage mV');
        setState(() {
          // Update UI display, showing both millivolts and volts
          batteryVoltage = '${voltage}mV (${(voltage/1000).toStringAsFixed(2)}V)';
        });
      } else {
        print('Response does not contain BatteryVoltage field');
      }
    } catch (e) {
      print('Failed to parse battery voltage: $e');
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('OPBT Battery Voltage Reader'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Display battery voltage
            Text('Battery Voltage: $batteryVoltage'),
            const SizedBox(height: 20),
            // Display connection status
            if (connectedDevice != null)
              Text('Connected device: ${connectedDevice!.name}')
            else
              const Text('No device connected'),
            const SizedBox(height: 20),
            // Scan button
            ElevatedButton(
              onPressed: isScanning ? null : startScan,
              child: Text(isScanning ? 'Scanning...' : 'Scan Devices'),
            ),
            const SizedBox(height: 20),
            // Device list
            Expanded(
              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),
                    subtitle: Text(result.device.id.toString()),
                    onTap: () => connectToDevice(result.device),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Run

Connect your phone with computer by USB cable, you’ll see a debbuging permission request. As following screen shot:

Enable USB debugging on Android
Android debugging permissions

Then, you can run following command from powershell:

flutter run -v