Flutter BLE移动端设备通信读写数据异常问题求助
问题背景
开发Flutter BLE移动端应用时,蓝牙扫描、连接功能正常,但数据读写操作无法正常工作。已封装BleController类管理BLE功能,但对BLE Service和Characteristic的概念与使用不熟悉。以下是相关代码:
相关代码
ble_controller.dart
import 'dart:convert'; import 'package:flutter_blue/flutter_blue.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; class DeviceInfo { final String name; final String id; DeviceInfo(this.name, this.id); // Convert DeviceInfo to JSON Map<String, dynamic> toJson() => { 'name': name, 'id': id, }; // Create DeviceInfo from JSON factory DeviceInfo.fromJson(Map<String, dynamic> json) => DeviceInfo( json['name'], json['id'], ); } class BleController extends GetxController { FlutterBlue ble = FlutterBlue.instance; BluetoothDevice? selectedDevice; List<BluetoothService> availableServices = []; List<BluetoothCharacteristic> availableCharacteristics = []; BluetoothCharacteristic? selectedCharacteristic; late String value = ""; void setSelectedDevice(BluetoothDevice? device) { selectedDevice = device; update(); } Future<void> connectToSelectedDevice() async { if (selectedDevice != null) { await connectToDevice(selectedDevice!); } } Future<void> connectToDevice(BluetoothDevice device) async { try { await device.connect(); await discoverServices(device); update(); // Notify listeners } catch (e) { print(e); } } Future<void> disconnectToSelectedDevice() async { if (selectedDevice != null) { await disconnectToDevice(selectedDevice!); } } Future<void> disconnectToDevice(BluetoothDevice device) async { try { await device.disconnect(); selectedDevice = null; // Reset selected device on disconnection availableServices.clear(); // Clear the list of services availableCharacteristics.clear(); // Clear the list of characteristics selectedCharacteristic = null; // Reset selected characteristic on disconnection update(); // Notify listeners } catch (e) { print(e); } } Future<void> discoverServices(BluetoothDevice device) async { try { List<BluetoothService> services = (await device.services.toList()).cast<BluetoothService>(); availableServices = services; // Clear the list of characteristics before adding new ones availableCharacteristics.clear(); for (BluetoothService service in services) { availableCharacteristics.addAll(service.characteristics); } } catch (e) { print(e); } } Future<void> saveSelectedDeviceToPrefs() async { SharedPreferences prefs = await SharedPreferences.getInstance(); if (selectedDevice != null) { String deviceJson = jsonEncode(DeviceInfo(selectedDevice!.name, selectedDevice!.id.id).toJson()); prefs.setString('selectedDevice', deviceJson); } } Future<BluetoothDevice?> loadSelectedDeviceFromPrefs() async { SharedPreferences prefs = await SharedPreferences.getInstance(); String? deviceJson = prefs.getString('selectedDevice'); if (deviceJson != null) { DeviceInfo deviceInfo = DeviceInfo.fromJson(jsonDecode(deviceJson)); // Fetch the connected devices and find the matching device List<BluetoothDevice> connectedDevices = await ble.connectedDevices; selectedDevice = connectedDevices.firstWhere( (device) => device.name == deviceInfo.name && device.id.id == deviceInfo.id, ); update(); // Notify listeners return selectedDevice; } return null; // Return null if no device is found in preferences } Future<String> readFromDevice(BluetoothDevice device) async { if (selectedCharacteristic == null) { try { value = (await selectedCharacteristic?.read()) as String; print('Read value: $value'); } catch (e) { print(e); } }else{ value = 'Read function is not performing'; } return value; } Future<void> writeToDevice(BluetoothDevice device, String data) async { if (device != null && selectedCharacteristic != null) { try { await selectedCharacteristic?.write(data.codeUnits, withoutResponse: false); print('Write successful'); } catch (e) { print(e); } } } BluetoothCharacteristic? findCharacteristicByUuid(BluetoothDevice device, String uuid) { for (BluetoothService service in availableServices) { for (BluetoothCharacteristic characteristic in service.characteristics) { if (characteristic.uuid.toString() == uuid) { return characteristic; } } } return null; } Future<void> scanDevices() async { if (await Permission.bluetoothScan.request().isGranted && await Permission.bluetoothConnect.request().isGranted) { ble.startScan(timeout: Duration(seconds: 1000)); } } void stopScan() { ble.stopScan(); } Stream<List<ScanResult>> get scanResults => ble.scanResults; }
basic_details.dart
import 'package:flutter/material.dart'; import 'package:flutter_blue/flutter_blue.dart'; import 'package:get/get.dart'; import 'ble_controller.dart'; class basicDetails extends StatefulWidget { const basicDetails({super.key}); @override State<basicDetails> createState() => _basicDetailsState(); } class _basicDetailsState extends State<basicDetails> { late BleController controller; @override void initState() { super.initState(); controller = BleController(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("BMC Configurator", style: TextStyle(color: Colors.white) ), backgroundColor: Color(0xFF0058DB), leading: Container( padding: EdgeInsets.all(10), child: Image.asset('assets/images/logo.jpg'), ), ), body: Padding( padding: const EdgeInsets.all(16.0), child: MyForm(), ), ); } } class MyForm extends StatefulWidget { @override _MyFormState createState() => _MyFormState(); } class _MyFormState extends State<MyForm> { final TextEditingController bmcVal = TextEditingController(); final TextEditingController deviceVal = TextEditingController(); final TextEditingController quantityVal = TextEditingController(); final TextEditingController recordTimeVal = TextEditingController(); final TextEditingController rtcMinVal = TextEditingController(); final TextEditingController rtcHrsVal = TextEditingController(); final TextEditingController rtcDateVal = TextEditingController(); final TextEditingController rtcMonVal = TextEditingController(); final TextEditingController rtcYrsVal = TextEditingController(); late BleController bleController; late Future<DeviceInfo> deviceInfoFuture; @override void initState() { super.initState(); bleController = BleController(); deviceInfoFuture = loadDeviceInfo() ; } Future<DeviceInfo> loadDeviceInfo() async { BluetoothDevice? selectedDevice = await bleController.loadSelectedDeviceFromPrefs(); return DeviceInfo(selectedDevice!); } @override Widget build(BuildContext context) { return FutureBuilder<DeviceInfo>( future: deviceInfoFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}')); } else { DeviceInfo deviceInfo = snapshot.data!; return SingleChildScrollView( child: Column( children: [ // Heading Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Text( 'Basic Details', style: TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, ), ), ), // Input Fields buildTextFieldWithButtons('BMC Code', bmcVal, '@BMCODE', bleController,deviceInfo.device), buildTextFieldWithButtons('Device ID', deviceVal, '@DEVICEID', bleController,deviceInfo.device), buildTextFieldWithButtons('Quantity', quantityVal, '@QUANTY', bleController,deviceInfo.device), buildTextFieldWithButtons('Record Saving Time', recordTimeVal, '@RECTIME', bleController,deviceInfo.device), buildTextFieldWithButtons('RTC Min', rtcMinVal, '@TIMEMIN', bleController,deviceInfo.device), buildTextFieldWithButtons('RTC Hrs', rtcHrsVal, '@TIMEHRS', bleController,deviceInfo.device), buildTextFieldWithButtons('RTC Date', rtcDateVal, '@TIMEDOM', bleController,deviceInfo.device), buildTextFieldWithButtons('RTC Month', rtcMonVal, '@TIMEMON', bleController,deviceInfo.device), buildTextFieldWithButtons('RTC Year', rtcYrsVal, '@TIMEYRS', bleController,deviceInfo.device), ], ), ); } }, ); } Widget buildTextFieldWithButtons(String labelText, TextEditingController controller, String data, BleController bleController, BluetoothDevice name) { late String dataToSend; var value; return GetBuilder<BleController>( init: bleController, builder: (bleController) { return Row( children: [ Expanded( child: TextFormField( controller: controller, decoration: InputDecoration(labelText: labelText), ), ), SizedBox(width: 8.0), ElevatedButton( onPressed: () async{ dataToSend = data + '?' + '\n\r'; bleController.writeToDevice(name, dataToSend); value = await bleController.readFromDevice(name ); print(value); setState(() { controller.text = value ; }); print('Receive $labelText: ${controller.text}'); }, child: Text('Receive'), ), SizedBox(width: 8.0), ElevatedButton( onPressed: () async{ dataToSend = data + ' ' + controller.text + '\n\r' ; controller.text = dataToSend; bleController.writeToDevice(name, dataToSend); value = await bleController.readFromDevice(name); print(value); print('Send $labelText: ${controller.text}'); }, child: Text('Send'), ), ], ); }, ); } } class DeviceInfo { final BluetoothDevice device; DeviceInfo(this.device); }
故障原因与修复方案
1. selectedCharacteristic 未被赋值
这是核心问题:读写操作依赖的selectedCharacteristic从未被设置,始终为null,导致读写逻辑根本无法执行。
修复步骤:
在设备连接并发现服务后,通过设备文档获取对应的读写Characteristic UUID,调用findCharacteristicByUuid方法设置selectedCharacteristic。修改connectToDevice方法:
Future<void> connectToDevice(BluetoothDevice device) async { try { await device.connect(); await discoverServices(device); // 替换为你的设备读写Characteristic的UUID const targetUuid = "0000ffe1-0000-1000-8000-00805f9b34fb"; selectedCharacteristic = findCharacteristicByUuid(device, targetUuid); if(selectedCharacteristic == null) { print("未找到目标Characteristic"); } update(); } catch (e) { print(e); } }
2. readFromDevice 方法逻辑错误
原方法条件判断颠倒,且直接将read()返回的List<int>强制转为String会导致类型错误。
修复代码:
Future<String> readFromDevice() async { if (selectedCharacteristic != null) { try { List<int> rawData = await selectedCharacteristic!.read(); value = utf8.decode(rawData); print('Read value: $value'); } catch (e) { print(e); value = '读取失败: $e'; } } else { value = '未选择Characteristic'; } return value; }
3. 多个BleController实例导致状态不一致
在basicDetails和MyForm中分别创建独立的BleController实例,导致状态不共享,已保存的设备信息无法同步。
修复步骤:
使用GetX的依赖注入,全局使用同一个BleController实例:
// 在basic_details.dart的_basicDetailsState中 @override void initState() { super.initState(); controller = Get.put(BleController()); // 全局注入 } // 在MyForm的_MyFormState中 @override void initState() { super.initState(); bleController = Get.find<BleController>(); // 获取全局实例 deviceInfoFuture = loadDeviceInfo(); }
4. loadDeviceInfo 方法潜在空指针风险
当从SharedPreferences加载不到设备时,selectedDevice为null,调用DeviceInfo(selectedDevice!)会崩溃。
修复代码:
Future<DeviceInfo?> loadDeviceInfo() async { BluetoothDevice? selectedDevice = await bleController.loadSelectedDeviceFromPrefs(); if(selectedDevice == null) { return null; } return DeviceInfo(selectedDevice); }
同时在FutureBuilder中处理null情况:
if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } else if (snapshot.hasError || snapshot.data == null) { return Center(child: Text('未找到已连接设备')); } else { // 原有渲染逻辑 }
5. 未检查Characteristic的读写权限
在读写前需要确认Characteristic支持对应的操作,避免无权限操作失败。
添加权限检查:
在writeToDevice和readFromDevice中添加判断:
// 写操作 if (selectedCharacteristic!.properties.write || selectedCharacteristic!.properties.writeWithoutResponse) { await selectedCharacteristic?.write(data.codeUnits, withoutResponse: false); } else { print('该Characteristic不支持写操作'); } // 读操作 if(selectedCharacteristic!.properties.read) { List<int> rawData = await selectedCharacteristic!.read(); } else { print('该Characteristic不支持读操作'); }
补充说明
BLE通信中,每个设备会暴露多个Service,每个Service包含多个Characteristic,只有具备读写权限的Characteristic才能进行数据交互。必须提前获取设备的Service和Characteristic UUID,才能正确指定通信通道。
内容的提问来源于stack exchange,提问作者Sakshi Kalpesh Brahmbhatt

