Flutter中BLE蓝牙通讯的实现_flutter 蓝牙
背景:
之前用java写过安卓端的BLE蓝牙通讯测试的demo,最近学习了Flutter相关知识,准备以BLE蓝牙测试为例,写一个flutte的BLE蓝牙测试demo。之前的demo请参考:安卓BLE蓝牙通讯
实现:
① 在android下的AndroidManifest.xml文件中添加安卓设备所需权限。
② 在ios下的Info.plist文件中添加ios设备所需权限。
NSBluetoothAlwaysUsageDescription 需要使用蓝牙来连接设备 NSBluetoothPeripheralUsageDescription 需要使用蓝牙来连接设备 NSLocationWhenInUseUsageDescription 需要使用位置权限来搜索附近的蓝牙设备 NSLocationAlwaysAndWhenInUseUsageDescription 需要使用位置权限来搜索附近的蓝牙设备
- 动态权限申请
① 创建一个PermissionUtil来管理权限获取。
import \'package:permission_handler/permission_handler.dart\';class PermissionUtil { /// 请求蓝牙和位置信息权限 static Future requestBluetoothConnectPermission() async { Map permission = await [ Permission.bluetoothConnect, Permission.bluetoothScan, Permission.bluetoothAdvertise, Permission.location, ].request(); if (await Permission.bluetoothConnect.isGranted) { print(\"蓝牙连接权限申请通过\"); } else { print(\"蓝牙连接权限申请失败\"); return false; } if (await Permission.bluetoothScan.isGranted) { print(\"蓝牙扫描权限申请通过\"); } else { print(\"蓝牙扫描权限申请失败\"); return false; } if (await Permission.bluetoothAdvertise.isGranted) { print(\"蓝牙广播权限申请通过\"); } else { print(\"蓝牙广播权限申请失败\"); return false; } if (await Permission.location.isGranted) { print(\"位置权限申请通过\"); } else { print(\"位置权限申请失败\"); return false; } return true; }}
② 在主页面中调用动态权限获取
PermissionUtil.requestBluetoothConnectPermission();
③ 在蓝牙扫描时调用权限检测。
// 扫描蓝牙 void scanDevices() { PermissionUtil.requestBluetoothConnectPermission().then((hasPermission) { if(hasPermission) { // 权限获取成功 devices.clear(); BleManager.getInstance().setCallback(this); BleManager.getInstance().startScan( timeout: Duration(seconds: 10), ); } else { // 权限获取失败 SnackBarManager.instance.showSnackBar(\"权限获取失败\", \"请先授予权限\"); } }); }
④ ios设备无需进行动态权限获取。
3. 创建一个蓝牙管理类
① 创建一个蓝牙管理类来管理连接的扫描、连接、通讯等。
import \'package:bluetooth/util/constants/ble_config.dart\';import \'package:bluetooth/util/snack_bar_manager.dart\';import \'package:flutter_blue_plus/flutter_blue_plus.dart\';import \'../inter/ble_callback.dart\';class BleManager { static BleManager? _instance; BluetoothDevice? _device; BluetoothCharacteristic? _writeCharacteristic; BluetoothCharacteristic? _notifyCharacteristic; // 回调接口 BleCallback? _callback; // 设置回调 void setCallback(BleCallback callback) { _callback = callback; } // 私有构造函数 BleManager._(); // 单例模式 static BleManager getInstance() { _instance ??= BleManager._(); return _instance!; } // 检查蓝牙是否可用 Future isAvailable() async { return await FlutterBluePlus.isAvailable; } // 检查蓝牙是否开启 Future isOn() async { return await FlutterBluePlus.isOn; } // 开始扫描 Future startScan({ Duration? timeout, List? withServices, }) async { if (!(await isOn())) { SnackBarManager.instance.showSnackBar(\"蓝牙未开启\", \"请打开蓝牙\"); } // 停止之前的扫描 await stopScan(); // 监听扫描结果 FlutterBluePlus.scanResults.listen((results) { for (ScanResult result in results) { // print(\"扫描结果: ${result.device.name} - ${result.device.remoteId}\"); if (_callback != null) { _callback!.onScanResult(result.device); } } }); // 开始扫描 print(\"开始扫描\"); await FlutterBluePlus.startScan( timeout: timeout ?? const Duration(seconds: 4), withServices: withServices ?? [], ); } // 停止扫描 Future stopScan() async { await FlutterBluePlus.stopScan(); } BluetoothDevice? getDeviceFromAddress(String address) { try { BluetoothDevice device = BluetoothDevice.fromId(address); return device; } catch (e) { print(\'获取设备失败: $e\'); return null; } } // 连接设备 Future connect(BluetoothDevice device) async { try { await device.connect( timeout: const Duration(seconds: 4), autoConnect: false, ); _device = device; // 添加断开连接监听 device.connectionState.listen((BluetoothConnectionState state) { if (state == BluetoothConnectionState.disconnected) { // 设备断开连接 if (_callback != null) { _callback!.onDisconnected(); } _device = null; _writeCharacteristic = null; _notifyCharacteristic = null; } }); // 发现服务 List services = await device.discoverServices(); for (BluetoothService service in services) { if (service.uuid.toString() == BleConfig.SERVICE_UUID) { for (BluetoothCharacteristic characteristic in service.characteristics) { if (characteristic.uuid.toString() == BleConfig.WRITE_CHARACTERISTIC_UUID) { _writeCharacteristic = characteristic; } if (characteristic.uuid.toString() == BleConfig.NOTIFY_CHARACTERISTIC_UUID) { _notifyCharacteristic = characteristic; } } } } if (_notifyCharacteristic != null) { // 设置通知 await enableNotification(); if (_callback != null) { _callback!.onConnectSuccess(); } return true; } else { print(\"未找到指定特征值\"); return false; } } catch (e) { if (_callback != null) { _callback!.onConnectFailed(e.toString()); } disconnect(); return false; } } // 断开连接 Future disconnect() async { if (_device != null) { await _device!.disconnect(); _device = null; _notifyCharacteristic = null; _writeCharacteristic = null; } } // 发送数据 Future sendData(List data) async { if (_writeCharacteristic != null) { await _writeCharacteristic!.write(data); return true; } else { print(\"未持有WRITE_UUID\"); return false; } } // 启用通知 Future enableNotification() async { if (_notifyCharacteristic != null) { await _notifyCharacteristic!.setNotifyValue(true); _notifyCharacteristic!.value.listen((value) { if (_callback != null) { _callback!.onDataReceived(value); } }); } else { print(\"未持有NOTIFY_UUID\"); } } // 获取连接状态 bool isConnected() { return _device != null && _notifyCharacteristic != null; }}
② 创建BleCallback来处理连接回调。
import \'package:flutter_blue_plus/flutter_blue_plus.dart\';mixin BleCallback { // 扫描结果回调 void onScanResult(BluetoothDevice device); // 连接成功回调 void onConnectSuccess(); // 断开连接回调 void onDisconnected(); // 连接失败回调 void onConnectFailed(String error); // 数据接收回调 void onDataReceived(List data);}
③ 统一管理蓝牙的UUID。
class BleConfig { // 服务和特征值 UUID static const String SERVICE_UUID = \"0783b03e-8535-b5a0-7140-a304d2495cb7\"; static const String WRITE_CHARACTERISTIC_UUID = \"0783b03e-8535-b5a0-7140-a304d2495cba\"; static const String NOTIFY_CHARACTERISTIC_UUID = \"0783b03e-8535-b5a0-7140-a304d2495cb8\";}
- 创建页面来展示蓝牙通讯
① 创建一个页面来展示蓝牙的连接和通讯状况。
import \'package:flutter/material.dart\';import \'package:get/get.dart\';import \'../home/home_contolller.dart\';class BluetoothInfoPage extends StatelessWidget { const BluetoothInfoPage({super.key}); @override Widget build(BuildContext context) { return GetBuilder(builder: (controller) { return Scaffold( appBar: AppBar( title: Text(\'蓝牙信息\'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Obx(() => Text(\"连接状态: ${controller.bluetoothInfo.value.isConnected ? \"已连接\" : \"未连接\"}\")), SizedBox(height: 8), Obx(() => Text(\"蓝牙名称: ${controller.bluetoothInfo.value.name}\")), SizedBox(height: 8), Obx(() => Text(\"蓝牙地址: ${controller.bluetoothInfo.value.address}\")), SizedBox(height: 16), Row( children: [ Expanded( child: TextField(onChanged: (value) { controller.sentMessage.value = value; // 更新发送的消息},decoration: InputDecoration( labelText: \'发送的报文\',), ), ), IconButton( icon: Icon(Icons.send), onPressed: () {controller.sendMessage(); }, tooltip: \'发送消息\', ), ], ), SizedBox(height: 16), const Text(\"接收的报文:\", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 8), // 接收报文内容区域和按钮 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 接收报文内容 Expanded( child: Container(height: 200, // 固定高度decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(4),),child: Obx(() => SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Text(controller.receivedMessage.value), ),)), ), ), // 右侧按钮 SizedBox( height: 200, // 与内容区域等高 child: Center( // 使用 Center 包裹按钮child: IconButton( icon: Icon(Icons.delete), onPressed: () { controller.clearMessage(); }, tooltip: \'清空接收内容\',), ), ), ], ), ], ), ), ); }); }}
② 创建一个页面来实现蓝牙的扫描和连接。
import \'package:flutter/cupertino.dart\';import \'package:flutter/material.dart\';import \'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart\';import \'package:get/get_state_manager/src/simple/get_state.dart\';import \'../home/home_contolller.dart\';class BluetoothListPage extends StatelessWidget { const BluetoothListPage({super.key}); @override Widget build(BuildContext context) { return GetBuilder(builder: (controller) { return Scaffold( appBar: AppBar( title: Text(\'蓝牙设备列表\'), ), body: Obx(() { return RefreshIndicator( onRefresh: () async { controller.scanDevices(); // 下拉刷新时重新扫描 }, child: ListView.builder( itemCount: controller.devices.length, itemBuilder: (context, index) { final device = controller.devices[index]; return Card( margin: EdgeInsets.symmetric(horizontal: 12, vertical: 6), elevation: 2, child: ListTile( contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), title: Text(device.name,style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [ SizedBox(height: 4), Text( device.address, style: TextStyle(fontSize: 14) ),], ), trailing: Container(padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),decoration: BoxDecoration( color: device.isConnected ? Colors.green[100] : Colors.grey[200], borderRadius: BorderRadius.circular(12)),child: Text( device.isConnected ? \"已连接\" : \"未连接\", style: TextStyle( color: device.isConnected ? Colors.green[700] : Colors.grey[700], fontSize: 14 ),), ), onTap: () {controller.connectToDevice(context, device); }, ), ); }, ) ); }), ); }); }}
- 创建一个Controller来管理页面的数据。
① 创建一个HomeController来管理页面数据以及蓝牙的实际连接、通讯等。
import \'package:bluetooth/util/ble_manager.dart\';import \'package:bluetooth/util/permission_util.dart\';import \'package:bluetooth/util/ua200_receiver.dart\';import \'package:flutter/material.dart\';import \'package:flutter_blue_plus/flutter_blue_plus.dart\';import \'package:get/get.dart\';import \'../../data/bluetooth_device_info.dart\';import \'../../inter/ble_callback.dart\';import \'../../util/snack_bar_manager.dart\';import \'../../widget/loading_dialog.dart\';import \'../ble/bluetooth_info_page.dart\';import \'../ble/bluetooth_list_page.dart\';class HomeController extends GetxController with BleCallback { final List pageList = [ const BluetoothInfoPage(), const BluetoothListPage(), ]; /// 当前界面的索引值 int currentIndex = 0; var bluetoothInfo = BluetoothDeviceInfo( name: \"\", address: \"\", isConnected: false, ).obs; var sentMessage = \"\".obs; var receivedMessage = \"\".obs; var devices = [].obs; changeIndex(int index) { currentIndex = index; update(); if (devices.isEmpty && currentIndex == 1) { scanDevices(); } } @override void onInit() { super.onInit(); PermissionUtil.requestBluetoothConnectPermission(); } @override void onReady() { super.onReady(); } // 扫描蓝牙 void scanDevices() { PermissionUtil.requestBluetoothConnectPermission().then((hasPermission) { if(hasPermission) { // 权限获取成功 devices.clear(); BleManager.getInstance().setCallback(this); BleManager.getInstance().startScan( timeout: Duration(seconds: 10), ); } else { // 权限获取失败 SnackBarManager.instance.showSnackBar(\"权限获取失败\", \"请先授予权限\"); } }); } Future connectToDevice(BuildContext context, BluetoothDeviceInfo device) async { if (device.isConnected) { // 如果设备已连接,则断开连接 device.isConnected = false; // 断开连接 BleManager.getInstance().disconnect(); bluetoothInfo.value = device; devices.refresh(); return; } for (var dev in devices) { dev.isConnected = false; // 先将所有设备的连接状态设为 false } // 显示连接中的状态框 // showDialog( // context: context, // barrierDismissible: false, // builder: (context) { // return const LoadingDialog(); // }, // ); LoadingDialog.show(\"连接中...\"); // 这里可以判断连接是否成功 var dev = BleManager.getInstance().getDeviceFromAddress(device.address); if (dev != null) { BleManager.getInstance().connect(dev).then((success) { if (success) { // 连接成功 LoadingDialog.hide(); // 关闭连接中的状态框 device.isConnected = true; // 将选中的设备连接状态设为 true devices.remove(device); // 移除该设备 devices.insert(0, device); // 将设备插入到列表顶部 bluetoothInfo.value = device; } else { LoadingDialog.hide(); // 关闭连接中的状态框 SnackBarManager.instance.showSnackBar(\"连接失败\", \"无法连接到 ${device.name},请重试。\"); } }); } else { LoadingDialog.hide(); // 关闭连接中的状态框 SnackBarManager.instance.showSnackBar(\"连接异常\", \"\"); } } void sendMessage() { // 发送消息的逻辑 print(\"发送消息: ${sentMessage.value}\"); var data = sentMessage.value.codeUnits; BleManager.getInstance().sendData(data).then((result) { if (result) { print(\"消息发送成功\"); } else { print(\"消息发送失败\"); SnackBarManager.instance.showSnackBar(\"消息发送失败\", \"请检查蓝牙连接状态\"); } }); } void clearMessage() { receivedMessage.value = \"\"; } @override void onClose() { super.onClose(); } @override void onScanResult(BluetoothDevice device) { if (device.name.isEmpty) { return; } if (devices.any((dev) => dev.address == device.remoteId.toString())) { return; } print(\'扫描到设备: ${device.name}, 地址: ${device.remoteId}\'); var dev = BluetoothDeviceInfo(name: device.name, address: device.remoteId.toString()); devices.add(dev); } @override void onConnectSuccess() { print(\'连接成功\'); } @override void onDisconnected() { print(\'断开连接\'); SnackBarManager.instance.showSnackBar(\"蓝牙断开连接\", \"\"); bluetoothInfo.value.isConnected = false; for (var device in devices) { device.isConnected = false; } bluetoothInfo.refresh(); devices.refresh(); } @override void onConnectFailed(String error) { print(\'连接失败: $error\'); } @override void onDataReceived(List data) { print(\'收到数据: $data\'); var receivedData = Ua200Receiver.getBleData(data); if (receivedData != \"\") { print(\'收到数据: $receivedData\'); receivedMessage.value = \'$receivedData\\n${receivedMessage.value}\'; } }}
onDataReceived接收的数据为byte数组,可根据自己BLE蓝牙协议进行解析,此demo收发皆使用string转byte后进行通讯。
-
实现效果
-
demo地址:https://gitee.com/hfyangi/bluetooth