一、写在前面的话
本文主要介绍在Flutter环境下开发BLE应用。主要包含以下内容:判断蓝牙是否开启、判断是否有位置权限、扫描设备、
连接设备、监听设备上报的数据(Notify)、向设备发送数据、监听设备的断开等。内容虽然简单,但是很详细。
二、开始
1、使用到的库
? flutter_blue: ^0.7.1+1 ? permission_handler: "^3.2.0" # 权限 ? 众所周知,Flutter要使用原生的能力,就需要有原生库的支持,这里我们使用了flutter_blue来开发跟BLE相关的功能,permission_handler来开发权限检测以及申请权限的功能。在使用flutter_blue开启扫描的时候,插件会弹出系统的权限申请窗口,但是点击授权以后报了一个空指针,可能是插件存在的一个BUG,所以这里我们用permission_handler插件来处理权限相关的问题。(友情提示:在Android上扫描Ble需要位置权限)
2、flutter_blue的用法
import 'package:flutter_blue/flutter_blue.dart'; FlutterBlue flutterBlue = FlutterBlue.instance; 导包和初始化。
3、判断蓝牙是否开启
@override ? void initState() { ? ? super.initState(); ? ? flutterBlue.state.listen((state){ ? ? ? if(state == BluetoothState.on){ ? ? ? ? print('蓝牙状态为开启'); ? ? ? ? isBleOn = true; ? ? ? }else if(state == BluetoothState.off){ ? ? ? ? print('蓝牙状态为关闭'); ? ? ? ? isBleOn = false; ? ? ? } ? ? }); ? } 在路由初始化的时候开启对蓝牙状态的监听。
?if(!isBleOn){ ? ? ? ToastUtils.toast(context, "手机蓝牙未打开,请打开后再扫描设备"); ? ? ? return; ? ? } 在开始之前对蓝牙开启状态进行判断。
4、判断位置权限
PermissionUtils.checkPermissions(PermissionGroup.location).then((v) { ? ? ? if (v) { ? ? ? ? Navigator.pushNamed(context, "/device_page"); ? ? ? } else { ? ? ? ? PermissionUtils.showDialog(context, "提示", "扫描蓝牙需要位置权限", () async { ? ? ? ? ? Navigator.pop(context); ? ? ? ? ? await PermissionHandler() ? ? ? ? ? ? ? .requestPermissions([PermissionGroup.location]); ? ? ? ? ? PermissionStatus permission = await PermissionHandler() ? ? ? ? ? ? ? .checkPermissionStatus(PermissionGroup.location); ? ? ? ? ? if (permission == PermissionStatus.granted) { ? ? ? ? ? ? Navigator.pushNamed(context, "/device_page"); ? ? ? ? ? } else { ? ? ? ? ? ? print("no Permission to scan ble"); ? ? ? ? ? ? ToastUtils.toast(context, "权限开启失败,请在系统设置中开启!"); ? ? ? ? ? } ? ? ? ? }, () { ? ? ? ? ? Navigator.pop(context); ? ? ? ? }); ? ? ? } ? ? }); ?以上是对位置权限的处理,如果有,就进入扫描设备页面,没有的话申请权限,申请完之后在对其进行判断。因为本文的重点是ble,这里不对权限申请插件做过多的介绍。(PermissionUtils,dialog的源码会在文末附上)
5、扫描设备
?flutterBlue.scan().listen((scanResult) { ? ? ? // do something with scan result ? ? ? var device = scanResult.device; ? ? ? if (device.name.length > 10) { ? ? ? ? if (deviceSet.indexOf(device) == -1) { ? ? ? ? ? setState(() { ? ? ? ? ? ? deviceSet.add(device); ? ? ? ? ? }); ? ? ? ? } ? ? ? ? print( ? ? ? ? ? ? '${device.name} found! rssi: ${scanResult.rssi},address:${device.id}'); ? ? ? } ? ? }); ? ? 使用api进行ble扫描,我对蓝牙名进行了过滤,记得要存起来哦,连接的时候要用的。
6、连接设备
? ? ? await device.connect(autoConnect: false, timeout: Duration(seconds: 10)); 这里的连接参数可以根据需要自己进行设置,我这里设置了10秒连接超时。这里我们一般还不能认为连接成功,还需要找到对应的读写服务和特征值。
BluetoothCharacteristic mCharacteristic; List<BluetoothService> services = await device.discoverServices(); ? ? ? services.forEach((service) { ? ? ? ? if (service.uuid.toString() == GattAttributes.BLE_SPP_SERVICE_READ) { ? ? ? ? ? List<BluetoothCharacteristic> characteristics = ? ? ? ? ? ? ? service.characteristics; ? ? ? ? ? characteristics.forEach((characteristic) { ? ? ? ? ? ? if (characteristic.uuid.toString() == ? ? ? ? ? ? ? ? GattAttributes.BLE_SPP_NOTIFY_CHARACTERISTIC) { ? ? ? ? ? ? ? mCharacteristic = characteristic; ? ? ? ? ? ? } ? ? ? ? ? }); ? ? ? ? } ? ? ? ? // do something with service ? ? ? }); 以上便是根据uuid在对应的读写服务中找到对应Characteristic,至此,连接过程便已经完成了,可以进行页面跳转了。
7、读取设备的心跳
if (mNotifyCharacteristic != null) { ? ? ? mNotifyCharacteristic.setNotifyValue(true); ? ? ? mNotifyCharacteristic.value ? ? ? ? ? .listen((value) => {print("device is online: " + value.toString())}); ? ? } 通过第6点的方法,找到可以notify的Characteristic,开启notify,然后监听其值。这里的setNotifyValue可能会有异常,具体请
参考我的前篇文章点我点我。下图是蓝牙设备传回的心跳。
8、向设备发送数据
? Future<Null> write(List<int> value, {bool withoutResponse = false}) 我先查看源码可知write方法的参数是一个int型的list,那么我们只需把我们需要发送的数据放进这个list中就行了。
? ? ? mWriteCharacteristic.write([0x00,0x01]); 同样,获取mWriteCharacteristic的方法如上第6点所示。
9、监听设备的断开
? device.state.listen((state){ ? ? ? ? if(state == BluetoothDeviceState.disconnected){ ? ? ? ? ? DialogUtils.showOneDialog(context, "提示", "设备已断开连接", (){ //do something ? ? ? ? ? }); ? ? ? ? } ? ? ? }); 10、在不用页面获取device
?await flutterBlue.connectedDevices.then((list) => { ? ? ? ? ? if (list.length == 0) {Navigator.pop(context)} else {device = list[0]} ? ? ? ? }); 在不同的页面(路由)需要对设备进行读写操作,在设备的连接池中获取。
三、总结
本文归纳了flutter下使用ble对设备进行读写的详细步骤以及一些基本方法和注意事项,如果有问题的同学欢迎留言,博主会一一解答的。技术在于分享,开源的乐趣也在于此,如果本文有不够严谨的地方还望大佬支持。
附录
import 'package:flutter/cupertino.dart'; ? class DialogUtils{ ? static showDialog(BuildContext cxt, String title, String content, ? ? ? ok(), cancel()) { ? ? showCupertinoDialog<int>( ? ? ? ? context: cxt, ? ? ? ? builder: (cxt) { ? ? ? ? ? return CupertinoAlertDialog( ? ? ? ? ? ? title: Text(title), ? ? ? ? ? ? content: Text(content), ? ? ? ? ? ? actions: <Widget>[ ? ? ? ? ? ? ? CupertinoDialogAction( ? ? ? ? ? ? ? ? child: Text("确定"), ? ? ? ? ? ? ? ? onPressed: () { ? ? ? ? ? ? ? ? ? ok(); ? ? ? ? ? ? ? ? }, ? ? ? ? ? ? ? ), ? ? ? ? ? ? ? CupertinoDialogAction( ? ? ? ? ? ? ? ? child: Text("取消"), ? ? ? ? ? ? ? ? onPressed: () { ? ? ? ? ? ? ? ? ? cancel(); ? ? ? ? ? ? ? ? }, ? ? ? ? ? ? ? ) ? ? ? ? ? ? ], ? ? ? ? ? ); ? ? ? ? }); ? } ? static showOneDialog(BuildContext cxt, String title, String content, ? ? ? ok()) { ? ? showCupertinoDialog<int>( ? ? ? ? context: cxt, ? ? ? ? builder: (cxt) { ? ? ? ? ? return CupertinoAlertDialog( ? ? ? ? ? ? title: Text(title), ? ? ? ? ? ? content: Text(content), ? ? ? ? ? ? actions: <Widget>[ ? ? ? ? ? ? ? CupertinoDialogAction( ? ? ? ? ? ? ? ? child: Text("确定"), ? ? ? ? ? ? ? ? onPressed: () { ? ? ? ? ? ? ? ? ? ok(); ? ? ? ? ? ? ? ? }, ? ? ? ? ? ? ? ) ? ? ? ? ? ? ], ? ? ? ? ? ); ? ? ? ? }); ? } } import 'package:flutter/cupertino.dart'; import 'package:permission_handler/permission_handler.dart'; ? /// 权限管理工具类 class PermissionUtils { ? /// 检测相关权限是否已经打开(根据已有状态值) ? static bool checkPermissionsByStatus(List<PermissionStatus> lists) { ? ? bool result = true; ? ? ? for (PermissionStatus permissionStatus in lists) { ? ? ? if (permissionStatus != PermissionStatus.granted) { ? ? ? ? result = false; ? ? ? ? break; ? ? ? } ? ? } ? ? ? return result; ? } ? ? /// 检测相关权限是否已经打开(根据已有权限名称) ? static Future<bool> checkPermissionsByGroup( ? ? ? List<PermissionGroup> lists) async { ? ? bool result = true; ? ? ? for (PermissionGroup permissionGroup in lists) { ? ? ? PermissionStatus checkPermissionStatus = ? ? ? await PermissionHandler().checkPermissionStatus(permissionGroup); ? ? ? ? if (checkPermissionStatus != PermissionStatus.granted) { ? ? ? ? result = false; ? ? ? ? break; ? ? ? } ? ? } ? ? ? return result; ? } ? static Future<bool> checkPermissions(PermissionGroup permissionGroup) async{ ? ? bool result = true; ? ? ? PermissionStatus checkPermissionStatus = ? ? await PermissionHandler().checkPermissionStatus(permissionGroup); ? ? ? if (checkPermissionStatus != PermissionStatus.granted) { ? ? ? result = false; ? ? ? } ? ? return result; ? ? } ? ? /// 权限提示对话款 ? static showDialog(BuildContext cxt, String title, String content, ? ? ? ?ok(), cancel()) { ? ? showCupertinoDialog<int>( ? ? ? ? context: cxt, ? ? ? ? builder: (cxt) { ? ? ? ? ? return CupertinoAlertDialog( ? ? ? ? ? ? title: Text(title), ? ? ? ? ? ? content: Text(content), ? ? ? ? ? ? actions: <Widget>[ ? ? ? ? ? ? ? CupertinoDialogAction( ? ? ? ? ? ? ? ? child: Text("去开启"), ? ? ? ? ? ? ? ? onPressed: () { ? ? ? ? ? ? ? ? ? ok(); ? ? ? ? ? ? ? ? }, ? ? ? ? ? ? ? ), ? ? ? ? ? ? ? CupertinoDialogAction( ? ? ? ? ? ? ? ? child: Text("取消"), ? ? ? ? ? ? ? ? onPressed: () { ? ? ? ? ? ? ? ? ? cancel(); ? ? ? ? ? ? ? ? }, ? ? ? ? ? ? ? ) ? ? ? ? ? ? ], ? ? ? ? ? ); ? ? ? ? }); ? } }
|