You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter BLE移动端设备通信读写数据异常问题求助

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 09:32:32