Flutter Demo: Retrieve OP-BT/BTS Battery Voltage

Once you have flutter installed and configured. You can try to create your first project. Following code demonstrates how to establish connection with OP-BT/BTS Bluetooth Optical Probes and retrive its battery voltage.

Code Features

  1. Scan bluetooth devices nearby
  2. Eastablish connection with chosen device
  3. Communicate with optical probe and retrieve its battery voltage
  4. Display battery voltage

Librarires

  1. Bluetooth Communication: flutter_blue_plus
  2. Permission: permission_handler

Communication Protocal

OP-BT/BTS communicate by JSON:

  1. Enable command mode: {“AtCommandMode”:true}
  2. Retreive battery voltage: {“BatteryVoltage”:"?"}
  3. Exit command mode: {“AtCommandMode”:false}

Device responde battery volate in JSON format: {“BatteryVoltage”:3800}

UUID

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

1. Create a new project

flutter create opbt_battery_voltage
cd opbt_battery_voltage

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:

Find the location of dependencies:

dependencies:
  flutter:
    sdk: flutter

Add following:

  flutter_blue_plus: ^1.31.13
  permission_handler: ^11.3.0

After adding dependencies, it should appear to be following:

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

3. Modify Android Permission

opbt_battery_voltage/android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 添加蓝牙权限 -->
    <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"/>
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    
    <application>
        <!-- ... 其他配置 ... -->
    </application>
</manifest>

4. Copy following code to your project

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

/// Root application component
/// 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 to 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 via BLE communication protocol to get 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 alternative command format
      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 from device
  /// @param response JSON-formatted string returned from 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, run following command from powershell:

flutter run -v