Flutter代码范例 - 读取 OP-BT/OP-BTS 蓝牙光电头电池电压

Flutter 测试 - 读取蓝牙光电头电池电压

1.环境准备

2.程序说明

OPBT 电池电压读取示例程序说明文档

概述

本示例程序展示了如何使用 Flutter 和 BLE 技术与 OPBT 设备通信,并读取其电池电压信息。程序实现了设备扫描、连接和数据交换的完整流程。

主要功能

  1. 扫描周围的蓝牙设备
  2. 连接到选定的 OPBT 设备
  3. 与设备进行 BLE 通信,读取电池电压
  4. 显示电池电压信息

技术要点

  1. 蓝牙通信 :使用 flutter_blue_plus 库实现 BLE 通信
  2. 权限管理 :使用 permission_handler 库处理蓝牙和位置权限
  3. JSON 解析 :解析设备返回的 JSON 格式数据
  4. 异步编程 :使用 async/await 处理异步蓝牙操作

通信协议

OPBT 设备使用 JSON 格式的命令进行通信:

  1. 进入命令模式: {“AtCommandMode”:true}
  2. 查询电池电压: {“BatteryVoltage”:"?"}
  3. 退出命令模式: {“AtCommandMode”:false} 设备会返回包含电池电压的 JSON 响应,例如: {“BatteryVoltage”:3800}

开发注意事项

  1. UUID 常量需要根据实际设备进行配置
  2. 蓝牙通信需要适当的延迟以确保命令被正确处理
  3. 在 Android 12 及以上版本需要特定的蓝牙权限
  4. 确保在 AndroidManifest.xml 中配置了必要的蓝牙权限

扩展开发

本示例可以扩展用于:

  1. 读取设备的其他参数
  2. 发送控制命令到设备
  3. 实现设备固件更新
  4. 添加数据记录和分析功能

程序部分代码说明

  • 程序运行后,首先要申请蓝牙服务相关的权限
  /// 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 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');
    }
  }
  • 蓝牙设备连接上后,扫描相应的服务,并连接和监听
      // Get list of services provided by device
      List<BluetoothService> services = await device.discoverServices();
      // Find target service
      var service = services.firstWhere(
        (s) => s.uuid.toString().toUpperCase().contains(
          BluetoothConstants.SERVICE_UUID,
        ),
      );

      // Get write characteristic
      var writeCharacteristic = service.characteristics.firstWhere(
        (c) => c.uuid.toString().toUpperCase().contains(
          BluetoothConstants.CHARACTERISTIC_UUID_WRITE,
        ),
      );

      // Get notification characteristic
      var notifyCharacteristic = service.characteristics.firstWhere(
        (c) => c.uuid.toString().toUpperCase().contains(
          BluetoothConstants.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'),
      );
  • 命令响应部分代码
  /// 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 battery voltage field exists
      if (data.containsKey('BatteryVoltage')) {
        int voltage = data['BatteryVoltage'];
        print('Parse successful, battery voltage: $voltage mV');
        setState(() {
          // Update UI display, showing both millivolts and volts
          batteryVoltage =
              '${voltage}mV (${(voltage / 1000).toStringAsFixed(2)}V)';
        });
      } else {
        print('No BatteryVoltage field in response');
      }
    } catch (e) {
      print('Failed to parse battery voltage: $e');
    }
  }

3.完整的程序代码安装和运行

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

flutter create opbt_battery_voltage

上面的过程时间可能从几分钟到几个小时来下载依赖库,请耐心等待

cd opbt_battery_voltage

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

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

修改 Android 权限配置: 文件:opbt_battery_voltage/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_battery_voltage"
        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_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';

/// Bluetooth device related constants
class BluetoothConstants {
  /// Service UUID
  static const String SERVICE_UUID = "18F0";

  /// Notification characteristic UUID
  static const String CHARACTERISTIC_UUID_NOTIFY = "2AF0";

  /// Write characteristic UUID
  static const String CHARACTERISTIC_UUID_WRITE = "2AF1";
}

/// Application entry point
void main() {
  runApp(const MyApp());
}

/// Root widget 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: 'Example: Retrieve Device Voltage',
      theme: ThemeData(primarySwatch: Colors.blue),
      home:
          const BatteryVoltageScreen(), // Set home page to battery voltage screen
    );
  }
}

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

class _BatteryVoltageScreenState extends State<BatteryVoltageScreen> {
  // List of scanned Bluetooth devices
  List<ScanResult> scanResults = [];
  // Currently connected Bluetooth device
  BluetoothDevice? connectedDevice;
  // Battery voltage display text
  String batteryVoltage = "Not Read";
  // Whether currently scanning for devices
  bool isScanning = false;

  @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
  /// 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 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
  /// Interact with device through 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 device
      List<BluetoothService> services = await device.discoverServices();
      // Find target service
      var service = services.firstWhere(
        (s) => s.uuid.toString().toUpperCase().contains(
          BluetoothConstants.SERVICE_UUID,
        ),
      );

      // Get write characteristic
      var writeCharacteristic = service.characteristics.firstWhere(
        (c) => c.uuid.toString().toUpperCase().contains(
          BluetoothConstants.CHARACTERISTIC_UUID_WRITE,
        ),
      );

      // Get notification characteristic
      var notifyCharacteristic = service.characteristics.firstWhere(
        (c) => c.uuid.toString().toUpperCase().contains(
          BluetoothConstants.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 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 battery voltage field exists
      if (data.containsKey('BatteryVoltage')) {
        int voltage = data['BatteryVoltage'];
        print('Parse successful, battery voltage: $voltage mV');
        setState(() {
          // Update UI display, showing both millivolts and volts
          batteryVoltage =
              '${voltage}mV (${(voltage / 1000).toStringAsFixed(2)}V)';
        });
      } else {
        print('No BatteryVoltage field in response');
      }
    } catch (e) {
      print('Failed to parse battery voltage: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Example: Retrieve Device Voltage')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 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),
                  );
                },
              ),
            ),
            // Bottom control area
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                // Scan button
                ElevatedButton(
                  onPressed: isScanning ? null : startScan,
                  child: Text(isScanning ? 'Scanning...' : 'Scan Devices'),
                ),
                // Connection info and voltage display
                Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    Text(
                      connectedDevice != null
                          ? 'NAME: ${connectedDevice!.name}'
                          : 'NAME: N/A',
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      'VOLTAGE: ${batteryVoltage == "Not Read" ? "N/A" : batteryVoltage}',
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}