Flutter 测试 - 读取蓝牙光电头电池电压
1.环境准备
- 安装 [Flutter SDK]环境(https://flutter.dev/docs/get-started/install)
2.程序说明
OPBT 电池电压读取示例程序说明文档
概述
本示例程序展示了如何使用 Flutter 和 BLE 技术与 OPBT 设备通信,并读取其电池电压信息。程序实现了设备扫描、连接和数据交换的完整流程。
主要功能
- 扫描周围的蓝牙设备
- 连接到选定的 OPBT 设备
- 与设备进行 BLE 通信,读取电池电压
- 显示电池电压信息
技术要点
- 蓝牙通信 :使用 flutter_blue_plus 库实现 BLE 通信
- 权限管理 :使用 permission_handler 库处理蓝牙和位置权限
- JSON 解析 :解析设备返回的 JSON 格式数据
- 异步编程 :使用 async/await 处理异步蓝牙操作
通信协议
OPBT 设备使用 JSON 格式的命令进行通信:
- 进入命令模式: {“AtCommandMode”:true}
- 查询电池电压: {“BatteryVoltage”:"?"}
- 退出命令模式: {“AtCommandMode”:false} 设备会返回包含电池电压的 JSON 响应,例如: {“BatteryVoltage”:3800}
开发注意事项
- UUID 常量需要根据实际设备进行配置
- 蓝牙通信需要适当的延迟以确保命令被正确处理
- 在 Android 12 及以上版本需要特定的蓝牙权限
- 确保在 AndroidManifest.xml 中配置了必要的蓝牙权限
扩展开发
本示例可以扩展用于:
- 读取设备的其他参数
- 发送控制命令到设备
- 实现设备固件更新
- 添加数据记录和分析功能
程序部分代码说明
- 程序运行后,首先要申请蓝牙服务相关的权限
/// 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),
),
],
),
],
),
],
),
),
);
}
}