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
- Scan bluetooth devices nearby
- Eastablish connection with specific device
- Communicate with meter by IEC standard protocal
- Display data received
Librarires
- Bluetooth Communication: flutter_blue_plus
- Permission: permission_handler
IEC Protocal
- Handshake:
/?!\r\n
- Device Response:
/LGZ5yyyyyyyyyyyyyyyyyyy\r\n
- 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:


Then, you can run following command from powershell:
flutter run -v