Flutter蓝牙框架-flutter_blue_plus使用及源码解析
前言
前段时间有朋友拜托我研究下flutter利用蓝牙与硬件交互的功能,我查阅了很多资料,目前市面上比较流行的第三方库有两个,一个是flutter_blue_plus,一个是flutter_reactive_ble,前一个比较轻量级,能满足大部分场景,后一个比较复杂,支持多个蓝牙设备同时连接。那么这一次我们先来研究下flutter_blue_plus,剩下的flutter_reactive_ble下次有机会再来看。
低功耗蓝牙(BLE)原理
博主好几年前还做Android原生开发时就接触并研究过BLE在Android平台上的使用与原理,写过一篇文章,大家感兴趣可以去看看。本次主要研究flutter_blue_plus(v1.6.1),对BLE原理不做过多描述。
使用及源码解析
要搞清楚如何使用flutter_blue_plus,最好的办法就是查阅文档或者查看flutter_reactive_ble的代码。这一次,我们就从flutter_reactive_ble库中example目录下的示例代码开始,一步一步看看如何使用flutter_blue_plus。
- 首先,我们打开main.dart文件。能够看到runApp里创建了我们示例的根组件-FlutterBlueApp。
runApp(const FlutterBlueApp());
我们来看看FlutterBlueApp是怎么写的:
class FlutterBlueApp extends StatelessWidget { const FlutterBlueApp({Key? key}) : super(key: key); Widget build(BuildContext context) { return MaterialApp( color: Colors.lightBlue, home: StreamBuilder<BluetoothState>( stream: FlutterBluePlus.instance.state, initialData: BluetoothState.unknown, builder: (c, snapshot) { final state = snapshot.data; if (state == BluetoothState.on) { return const FindDevicesScreen(); } return BluetoothOffScreen(state: state); }), ); }}
我们看到,这里利用了一个StreamBuilder去监听Stream的变化,主要是BluetoothState。蓝牙设备的状态,然后根据实时状态去变化展示的内容。
BluetoothState是一个枚举类,定义了几种可能的状态:
/// State of the bluetooth adapter.enum BluetoothState{ unknown, unavailable, unauthorized, turningOn, on, turningOff, off}
initialData是StreamBuilder中绘制第一帧的数据,由于是BluetoothState.unknown,所以第一帧应该显示BluetoothOffScreen。之后的状态由stream中的异步数据提供,即FlutterBluePlus.instance.state,我们看看FlutterBluePlus这个类:
class FlutterBluePlus{ static final FlutterBluePlus _instance = FlutterBluePlus._(); static FlutterBluePlus get instance => _instance; .... /// Singleton boilerplate FlutterBluePlus._() { .... }....}
可以看到,FlutterBluePlus是一个利用dart getter操作符实现的一个单例类,通过FlutterBluePlus.instance获取全局唯一的一个实例。
接着我们看下FlutterBluePlus.instance.state,这个state也是一个getter方法:
/// Gets the current state of the Bluetooth module Stream<BluetoothState> get state async* { BluetoothState initialState = await _channel .invokeMethod('state') .then((buffer) => protos.BluetoothState.fromBuffer(buffer)) .then((s) => BluetoothState.values[s.state.value]); yield initialState; _stateStream ??= _stateChannel .receiveBroadcastStream() .map((buffer) => protos.BluetoothState.fromBuffer(buffer)) .map((s) => BluetoothState.values[s.state.value]) .doOnCancel(() => _stateStream = null); yield* _stateStream!; }
可以看到,由于蓝牙涉及到原生操作系统底层的功能,所以需要利用平台通道(platform channel)机制,实现 Dart 代码与原生代码的交互,间接调用Android/IOS SDK的Api。
final MethodChannel _channel = const MethodChannel('flutter_blue_plus/methods'); final EventChannel _stateChannel = const EventChannel('flutter_blue_plus/state');
在FlutterBluePlus这个类中,首先构造一个方法通道(method channel)与一个事件通道(event channel),通道的客户端(flutter方)和宿主端(原生方)通过传递给通道构造函数的通道名称进行连接,这个名称必须是唯一的。之后就可以通过_channel.invokeMethod调用原生的方法了,当然前提是原生平台有对应的实现。接下来,我们看下state这个方法,原生端是如何实现的(以Android为例):
在flutter_blue_plus库的android目录下,能看到一个FlutterBluePlusPlugin.java文件:
public class FlutterBluePlusPlugin implements FlutterPlugin, MethodCallHandler,.... @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { setup(pluginBinding.getBinaryMessenger(), (Application) pluginBinding.getApplicationContext()); }.... private void setup(final BinaryMessenger messenger, final Application application) { .... channel = new MethodChannel(messenger, NAMESPACE + "/methods"); channel.setMethodCallHandler(this); stateChannel = new EventChannel(messenger, NAMESPACE + "/state"); stateChannel.setStreamHandler(stateHandler); .... } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { .... tearDown(); } private void tearDown() { .... channel.setMethodCallHandler(null); channel = null; stateChannel.setStreamHandler(null); stateChannel = null; .... } }
可以看到,FlutterBluePlusPlugin实现了FlutterPlugin与MethodCallHandler两个接口,实现FlutterPlugin的onAttachedToEngine与onDetachedFromEngine两个方法后,就可以将插件与flutter的engine关联起来。在这两个方法中,主要是构造了MethodChannel与EventChannel并在最后置为空,作用是在一开始注册通道并在最后销毁掉。
而实现MethodCallHandler的onMethodCall方法,即在原生端实现相应的功能方便flutter通道调用:
@Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { .... switch (call.method) { .... case "state": { try { // get state, if we can int state = -1; try { state = mBluetoothAdapter.getState(); } catch (Exception e) {} // convert to protobuf enum Protos.BluetoothState.State pbs; switch(state) { case BluetoothAdapter.STATE_OFF: pbs = Protos.BluetoothState.State.OFF; break; case BluetoothAdapter.STATE_ON: pbs = Protos.BluetoothState.State.ON; break; case BluetoothAdapter.STATE_TURNING_OFF: pbs = Protos.BluetoothState.State.TURNING_OFF; break; case BluetoothAdapter.STATE_TURNING_ON: pbs = Protos.BluetoothState.State.TURNING_ON; break; default: pbs = Protos.BluetoothState.State.UNKNOWN; break; } Protos.BluetoothState.Builder p = Protos.BluetoothState.newBuilder(); p.setState(pbs); result.success(p.build().toByteArray()); } catch(Exception e) { result.error("state", e.getMessage(), e); } break; } ....
可以看到,Android端拿到蓝牙状态后通过result.success返回结果。
state方法只能获取初始状态,后面状态的变化我们看到是通过EventChannel监听广播获取的,我们看看在原生端是怎么处理的。
在创建EventChannel时,首先将它的StreamHandler设置为我们自定义的StreamHandler函数:
private class MyStreamHandler implements StreamHandler { private final int STATE_UNAUTHORIZED = -1; private EventSink sink; public EventSink getSink() { return sink; } private int cachedBluetoothState; public void setCachedBluetoothState(int value) { cachedBluetoothState = value; } public void setCachedBluetoothStateUnauthorized() { cachedBluetoothState = STATE_UNAUTHORIZED; } @Override public void onListen(Object o, EventChannel.EventSink eventSink) { sink = eventSink; if (cachedBluetoothState != 0) { // convert to Protobuf enum Protos.BluetoothState.State pbs; switch (cachedBluetoothState) { case BluetoothAdapter.STATE_OFF: pbs = Protos.BluetoothState.State.OFF; break; case BluetoothAdapter.STATE_ON: pbs = Protos.BluetoothState.State.ON; break; case BluetoothAdapter.STATE_TURNING_OFF: pbs = Protos.BluetoothState.State.TURNING_OFF; break; case BluetoothAdapter.STATE_TURNING_ON: pbs = Protos.BluetoothState.State.TURNING_ON; break; case STATE_UNAUTHORIZED: pbs = Protos.BluetoothState.State.OFF; break; default: pbs = Protos.BluetoothState.State.UNKNOWN; break; } Protos.BluetoothState.Builder p = Protos.BluetoothState.newBuilder(); p.setState(pbs); sink.success(p.build().toByteArray()); } } @Override public void onCancel(Object o) { sink = null; } };
在MyStreamHandler的onListen方法里,我们拿到EventSink引用并保存,并查看是否有缓存未发送的蓝牙状态,有的话就利用EventSink发送给Stream。
之后,我们注册一个监听蓝牙状态变化的广播,将当前蓝牙状态设置为MyStreamHandler的缓存状态cachedBluetoothState:
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);context.registerReceiver(mBluetoothStateReceiver, filter);try { stateHandler.setCachedBluetoothState(mBluetoothAdapter.getState()); } catch (SecurityException e) { stateHandler.setCachedBluetoothStateUnauthorized(); }
注册的广播代码如下:
private final BroadcastReceiver mBluetoothStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); // no change? if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action) == false) { return; } final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); EventSink sink = stateHandler.getSink(); if (sink == null) { stateHandler.setCachedBluetoothState(state); return; } // convert to Protobuf enum Protos.BluetoothState.State pbs; switch (state) { case BluetoothAdapter.STATE_OFF: pbs = Protos.BluetoothState.State.OFF; break; case BluetoothAdapter.STATE_ON: pbs = Protos.BluetoothState.State.ON; break; case BluetoothAdapter.STATE_TURNING_OFF: pbs = Protos.BluetoothState.State.TURNING_OFF; break; case BluetoothAdapter.STATE_TURNING_ON: pbs = Protos.BluetoothState.State.TURNING_ON; break; default: pbs = Protos.BluetoothState.State.UNKNOWN; break; } Protos.BluetoothState.Builder p = Protos.BluetoothState.newBuilder(); p.setState(pbs); sink.success(p); } };
广播接收到蓝牙状态变化后,根据是否能获取到EventSink,看是缓存还是发送。
至此,蓝牙状态相关代码就分析完了。
- 之后,我们来看下BluetoothOffScreen,这个界面比较简单,除了展示蓝牙的状态之外,还提供了一个打开蓝牙的开关(只针对Android)。
onPressed: Platform.isAndroid ? () => FlutterBluePlus.instance.turnOn() : null,
看看turnOn这个方法,也是通过MethodChannel实现的:
Future<bool> turnOn() { return _channel.invokeMethod('turnOn').then<bool>((d) => d); }
我们再来FlutterPlugin的onMethodCall方法下找找原生对应的实现:
case "turnOn": { try { if (mBluetoothAdapter.isEnabled()) { result.success(true); // no work to do } Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); activityBinding.getActivity().startActivityForResult(enableBtIntent, enableBluetoothRequestCode); result.success(true); } catch(Exception e) { result.error("turnOn", e.getMessage(), e); } break; }
原生是通过Intent去打开系统服务蓝牙的,那么这里为了从插件中获取Activity用到的activityBinding是从哪里来的?
也是FlutterBluePlusPlugin通过实现ActivityAware这个接口,然后在onAttachedToActivity这个方法时获取到的ActivityPluginBinding引用,通过它我们就可以在插件中获取到FlutterActivity里的context和Activity了。
- 最后,我们看看FindDevicesScreen:
1)首先看看右下角的按钮
floatingActionButton: StreamBuilder<bool>( stream: FlutterBluePlus.instance.isScanning, initialData: false, builder: (c, snapshot) { if (snapshot.data!) { return FloatingActionButton( child: const Icon(Icons.stop), onPressed: () => FlutterBluePlus.instance.stopScan(), backgroundColor: Colors.red, ); } else { return FloatingActionButton( child: const Icon(Icons.search), onPressed: () => FlutterBluePlus.instance .startScan(timeout: const Duration(seconds: 4))); } }, ),
这个按钮根据当前蓝牙是否在扫描,会展示开始搜索/停止搜索按钮。
先来看看startScan这个方法:
/// Starts a scan and returns a future that will complete once the scan has finished. /// Once a scan is started, call [stopScan] to stop the scan and complete the returned future. /// timeout automatically stops the scan after a specified [Duration]. /// To observe the results while the scan is in progress, listen to the [scanResults] stream, /// or call [scan] instead. Future startScan({ ScanMode scanMode = ScanMode.lowLatency, List<Guid> withServices = const [], List<Guid> withDevices = const [], List<String> macAddresses = const [], Duration? timeout, bool allowDuplicates = false, }) async { await scan( scanMode: scanMode, withServices: withServices, withDevices: withDevices, macAddresses: macAddresses, timeout: timeout, allowDuplicates: allowDuplicates) .drain(); return _scanResults.value; }
再来看scan方法
/// Starts a scan for Bluetooth Low Energy devices and returns a stream /// of the [ScanResult] results as they are received. /// timeout calls stopStream after a specified [Duration]. /// You can also get a list of ongoing results in the [scanResults] stream. /// If scanning is already in progress, this will throw an [Exception]. Stream<ScanResult> scan({ ScanMode scanMode = ScanMode.lowLatency, List<Guid> withServices = const [], List<Guid> withDevices = const [], List<String> macAddresses = const [], Duration? timeout, bool allowDuplicates = false, }) async* { var settings = protos.ScanSettings.create() ..androidScanMode = scanMode.value ..allowDuplicates = allowDuplicates ..macAddresses.addAll(macAddresses) ..serviceUuids.addAll(withServices.map((g) => g.toString()).toList()); if (_isScanning.value == true) { throw Exception('Another scan is already in progress.'); } // push to isScanning stream _isScanning.add(true); // Clear scan results list _scanResults.add(<ScanResult>[]); Stream<ScanResult> scanResultsStream = FlutterBluePlus.instance._methodStream .where((m) => m.method == "ScanResult") .map((m) => m.arguments) .map((buffer) => protos.ScanResult.fromBuffer(buffer)) .map((p) => ScanResult.fromProto(p)) .takeWhile((element) => _isScanning.value) .doOnDone(stopScan); // Start listening now, before invokeMethod, to ensure we don't miss any results _scanResultsBuffer = _BufferStream.listen(scanResultsStream); // Start timer *after* stream is being listened to, to make sure we don't miss the timeout if (timeout != null) { _scanTimeout = Timer(timeout, () { _scanResultsBuffer?.close(); _isScanning.add(false); _channel.invokeMethod('stopScan'); }); } try { await _channel.invokeMethod('startScan', settings.writeToBuffer()); } catch (e) { print('Error starting scan.'); _isScanning.add(false); rethrow; } await for (ScanResult item in _scanResultsBuffer!.stream) { // update list of devices List<ScanResult> list = List<ScanResult>.from(_scanResults.value); if (list.contains(item)) { int index = list.indexOf(item); list[index] = item; } else { list.add(item); } _scanResults.add(list); yield item; } }
final StreamController<MethodCall> _methodStreamController = StreamController.broadcast(); final _BehaviorSubject<bool> _isScanning = _BehaviorSubject(false); final _BehaviorSubject<List<ScanResult>> _scanResults = _BehaviorSubject([]);Stream<bool> get isScanning => _isScanning.stream; /// Returns a stream that is a list of [ScanResult] results while a scan is in progress. /// The list emitted is all the scanned results as of the last initiated scan. When a scan is /// first started, an empty list is emitted. The returned stream is never closed. /// One use for [scanResults] is as the stream in a StreamBuilder to display the /// results of a scan in real time while the scan is in progress. Stream<List<ScanResult>> get scanResults => _scanResults.stream; // Used internally to dispatch methods from platform. Stream<MethodCall> get _methodStream => _methodStreamController.stream;
_isScanning是对StreamController的一个封装,FlutterBluePlus.instance.isScanning就是通过getter 拿到它的stream,_isScanning.add是往stream中添加一个布尔值,即当前是否正在扫描,然后_isScanning.value就可以拿到当前的状态。
_scanResults与_isScanning类似,但是它是放置扫描结果的。
_methodStream是用来监听MethodCall即通道方法调用的。
大概流程是先将扫描状态设置为true,然后清空扫描结果,接着监听一个叫ScanResult的通道方法调用(后面我们知道这个就是开始扫描后原生侧返回扫描结果的回调方法),然后设置一个定时器,如果有设置超时时间的话就停止扫描并还原状态,最后调用通道方法startScan开始扫描,并遍历我们监听的扫描结果的stream,将数据添加到_scanResults中去。
stopScan比较简单,就不解释了:
/// Stops a scan for Bluetooth Low Energy devices Future stopScan() async { await _channel.invokeMethod('stopScan'); _scanResultsBuffer?.close(); _scanTimeout?.cancel(); _isScanning.add(false); }
接着,我们看下原生侧是如何实现扫描的:
case "startScan": { byte[] data = call.arguments(); Protos.ScanSettings p = Protos.ScanSettings.newBuilder().mergeFrom(data).build(); macDeviceScanned.clear(); BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); if(scanner == null) {throw new Exception("getBluetoothLeScanner() is null. Is the Adapter on?"); } int scanMode = p.getAndroidScanMode(); List<ScanFilter> filters = fetchFilters(p); // scan settings ScanSettings settings; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {settings = new ScanSettings.Builder() .setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED) .setLegacy(false) .setScanMode(scanMode) .build(); } else {settings = new ScanSettings.Builder() .setScanMode(scanMode).build(); } scanner.startScan(filters, settings, getScanCallback()); result.success(null); } catch(Exception e) { result.error("startScan", e.getMessage(), e); } break; }
通过传入的参数对mac地址和uuid对扫描对象进行过滤,然后在getScanCallback里面返回:
private ScanCallback scanCallback; @TargetApi(21) private ScanCallback getScanCallback() { if(scanCallback == null) { scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); if(result != null){ if (!allowDuplicates && result.getDevice() != null && result.getDevice().getAddress() != null) {if (macDeviceScanned.contains(result.getDevice().getAddress())) { return;}macDeviceScanned.add(result.getDevice().getAddress()); } Protos.ScanResult scanResult = ProtoMaker.from(result.getDevice(), result); invokeMethodUIThread("ScanResult", scanResult.toByteArray()); } } @Override public void onBatchScanResults(List<ScanResult> results) { super.onBatchScanResults(results); } @Override public void onScanFailed(int errorCode) { super.onScanFailed(errorCode); } }; } return scanCallback; }
每次扫描到结果都会调用onScanResult方法,然后通过macDeviceScanned记录已经扫描到的数据,去重。invokeMethodUIThread这个方法是通过handler做线程切换,保证在主线程返回结果。
2) 接着,我们看下FindDevicesScreen里面的扫描结果列表:
StreamBuilder<List<ScanResult>>( stream: FlutterBluePlus.instance.scanResults, initialData: const [], builder: (c, snapshot) => Column( children: snapshot.data! .map( (r) => ScanResultTile( result: r, onTap: () => Navigator.of(context) .push(MaterialPageRoute(builder: (context) {r.device.connect();return DeviceScreen(device: r.device); })), ), ) .toList(), ), ),
ScanResultTile是显示的item组件,从左到右,依次是:rssi(信号强度),BluetoothDevice(设备数据)的name与id,根据AdvertisementData(广告数据)connectable(是否可连接)判断能否点击的按钮。
点击后展开的内容,从上到下,依次是:Complete Local Name(完整的本地名称),Tx Power Level(发射功率电平),Manufacturer Data(制造商数据),Service UUIDs,Service Data
点击Connect按钮逻辑:
onTap: () => Navigator.of(context).push(MaterialPageRoute( builder: (context) { r.device.connect(); return DeviceScreen(device: r.device); })),
一起看下BluetoothDevice的connect方法
/// Establishes a connection to the Bluetooth Device. Future<void> connect({ Duration? timeout, bool autoConnect = true, bool shouldClearGattCache = true, }) async { if (Platform.isAndroid && shouldClearGattCache) { clearGattCache(); } var request = protos.ConnectRequest.create() ..remoteId = id.toString() ..androidAutoConnect = autoConnect; var responseStream = state.where((s) => s == BluetoothDeviceState.connected); // Start listening now, before invokeMethod, to ensure we don't miss the response Future<BluetoothDeviceState> futureState = responseStream.first; await FlutterBluePlus.instance._channel .invokeMethod('connect', request.writeToBuffer()); // wait for connection if (timeout != null) { await futureState.timeout(timeout, onTimeout: () { throw TimeoutException('Failed to connect in time.', timeout); }); } else { await futureState; } }
首先看一下这个state,也是一个getter方法:
/// The current connection state of the device Stream<BluetoothDeviceState> get state async* { BluetoothDeviceState initialState = await FlutterBluePlus.instance._channel .invokeMethod('deviceState', id.toString()) .then((buffer) => protos.DeviceStateResponse.fromBuffer(buffer)) .then((p) => BluetoothDeviceState.values[p.state.value]); yield initialState; yield* FlutterBluePlus.instance._methodStream .where((m) => m.method == "DeviceState") .map((m) => m.arguments) .map((buffer) => protos.DeviceStateResponse.fromBuffer(buffer)) .where((p) => p.remoteId == id.toString()) .map((p) => BluetoothDeviceState.values[p.state.value]); }
可以看到,依然是类似的逻辑,通过通道方法deviceState拿到设备连接初始状态,然后在回调方法里通过DeviceState方法将状态变化通知到flutter:
case "deviceState": { try { String deviceId = (String)call.arguments; BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceId); int state = mBluetoothManager.getConnectionState(device, BluetoothProfile.GATT); result.success(ProtoMaker.from(device, state).toByteArray()); } catch(Exception e) { result.error("deviceState", e.getMessage(), e); } break; }
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if(newState == BluetoothProfile.STATE_DISCONNECTED) { if(!mDevices.containsKey(gatt.getDevice().getAddress())) { gatt.close(); } } invokeMethodUIThread("DeviceState", ProtoMaker.from(gatt.getDevice(), newState).toByteArray()); } .... }
看下原生实现的connect:
case "connect": { byte[] data = call.arguments(); Protos.ConnectRequest options = Protos.ConnectRequest.newBuilder().mergeFrom(data).build(); String deviceId = options.getRemoteId(); BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceId); boolean isConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT).contains(device); // If device is already connected, return error if(mDevices.containsKey(deviceId) && isConnected) {result.error("connect", "connection with device already exists", null);return; } // If device was connected to previously but // is now disconnected, attempt a reconnect BluetoothDeviceCache bluetoothDeviceCache = mDevices.get(deviceId); if(bluetoothDeviceCache != null && !isConnected) {if(bluetoothDeviceCache.gatt.connect() == false) { result.error("connect", "error when reconnecting to device", null);}result.success(null);return; } // New request, connect and add gattServer to Map BluetoothGatt gattServer; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {gattServer = device.connectGatt(context, options.getAndroidAutoConnect(), mGattCallback, BluetoothDevice.TRANSPORT_LE); } else {gattServer = device.connectGatt(context, options.getAndroidAutoConnect(), mGattCallback); } mDevices.put(deviceId, new BluetoothDeviceCache(gattServer)); result.success(null); } catch(Exception e) { result.error("connect", e.getMessage(), e); } }); break; }
检查是否已经正在连接其他设备,是则报错,否则继续。接着看是否之前连过这个设备,是则发起重连。否则发起一个新的连接请求。mDevices为连接过设备的Cache数据,根据deviceId记录,后面获取Gatt时提高效率。
接着我们看下点击按钮后跳转的DeviceScreen页面:
首先右上角会根据当前设备的连接状态显示连接/断开,连接看过了,看下断开:
/// Cancels connection to the Bluetooth Device Future<void> disconnect() async { await FlutterBluePlus.instance._channel .invokeMethod('disconnect', id.toString()); }
case "disconnect": { try { String deviceId = (String)call.arguments; BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceId); BluetoothDeviceCache cache = mDevices.remove(deviceId); if(cache != null) { BluetoothGatt gattServer = cache.gatt; gattServer.disconnect(); int state = mBluetoothManager.getConnectionState(device, BluetoothProfile.GATT); if(state == BluetoothProfile.STATE_DISCONNECTED) {gattServer.close(); } } result.success(null); } catch(Exception e) { result.error("disconnect", e.getMessage(), e); } break; }
第一行最后边有一个刷新按钮:
trailing: StreamBuilder<bool>( stream: device.isDiscoveringServices, initialData: false, builder: (c, snapshot) => IndexedStack( index: snapshot.data! ? 1 : 0, children: <Widget>[ IconButton( icon: const Icon(Icons.refresh), onPressed: () => device.discoverServices(), ), .... ], ), ),
这个按钮是在当前连接设备上搜索所有的Service。
BluetoothDevice的discoverServices方法:
/// Discovers services offered by the remote device /// as well as their characteristics and descriptors Future<List<BluetoothService>> discoverServices() async { final s = await state.first; if (s != BluetoothDeviceState.connected) { return Future.error(Exception('Cannot discoverServices while' 'device is not connected. State == $s')); } // signal that we have started _isDiscoveringServices.add(true); var responseStream = FlutterBluePlus.instance._methodStream .where((m) => m.method == "DiscoverServicesResult") .map((m) => m.arguments) .map((buffer) => protos.DiscoverServicesResult.fromBuffer(buffer)) .where((p) => p.remoteId == id.toString()) .map((p) => p.services) .map((s) => s.map((p) => BluetoothService.fromProto(p)).toList()); // Start listening now, before invokeMethod, to ensure we don't miss the response Future<List<BluetoothService>> futureResponse = responseStream.first; await FlutterBluePlus.instance._channel .invokeMethod('discoverServices', id.toString()); // wait for response List<BluetoothService> services = await futureResponse; _isDiscoveringServices.add(false); _services.add(services); return services; }
根据推断,DiscoverServicesResult是在回调方法里返回结果,discoverServices发起搜索服务:
@Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { Protos.DiscoverServicesResult.Builder p = Protos.DiscoverServicesResult.newBuilder(); p.setRemoteId(gatt.getDevice().getAddress()); for(BluetoothGattService s : gatt.getServices()) { p.addServices(ProtoMaker.from(gatt.getDevice(), s, gatt)); } invokeMethodUIThread("DiscoverServicesResult", p.build().toByteArray()); }
case "discoverServices": { try { String deviceId = (String)call.arguments; BluetoothGatt gatt = locateGatt(deviceId); if(gatt.discoverServices() == false) { result.error("discoverServices", "unknown reason", null); break; } result.success(null); } catch(Exception e) { result.error("discoverServices", e.getMessage(), e); } break; }
搜索完成后会展示服务列表:
StreamBuilder<List<BluetoothService>>( stream: device.services, initialData: const [], builder: (c, snapshot) { return Column( children: _buildServiceTiles(snapshot.data!), ); }, ),
BluetoothDevice的services方法:
/// Returns a list of Bluetooth GATT services offered by the remote device /// This function requires that discoverServices has been completed for this device Stream<List<BluetoothService>> get services async* { List<BluetoothService> initialServices = await FlutterBluePlus.instance._channel .invokeMethod('services', id.toString()) .then((buffer) => protos.DiscoverServicesResult.fromBuffer(buffer).services) .then((i) => i.map((s) => BluetoothService.fromProto(s)).toList()); yield initialServices; yield* _services.stream; }
原生端实现
case "services": { try { String deviceId = (String)call.arguments; BluetoothGatt gatt = locateGatt(deviceId); Protos.DiscoverServicesResult.Builder p = Protos.DiscoverServicesResult.newBuilder(); p.setRemoteId(deviceId); for(BluetoothGattService s : gatt.getServices()){ p.addServices(ProtoMaker.from(gatt.getDevice(), s, gatt)); } result.success(p.build().toByteArray()); } catch(Exception e) { result.error("services", e.getMessage(), e); } break; }
接着我们来看下Service的内容:
每个Service都有一个uuid,若干characteristics数据,每个characteristic也有一个uuid,此外characteristic还支持读,写,通知等操作:
先来看读:BluetoothCharacteristic.read
/// Retrieves the value of the characteristic Future<List<int>> read() async { List<int> responseValue = []; // Only allow a single read or write operation // at a time, to prevent race conditions. await _readWriteMutex.synchronized(() async { var request = protos.ReadCharacteristicRequest.create() ..remoteId = deviceId.toString() ..characteristicUuid = uuid.toString() ..serviceUuid = serviceUuid.toString(); FlutterBluePlus.instance._log(LogLevel.info, 'remoteId: ${deviceId.toString()}' 'characteristicUuid: ${uuid.toString()}' 'serviceUuid: ${serviceUuid.toString()}'); var responseStream = FlutterBluePlus.instance._methodStream .where((m) => m.method == "ReadCharacteristicResponse") .map((m) => m.arguments) .map((buffer) => protos.ReadCharacteristicResponse.fromBuffer(buffer)) .where((p) => (p.remoteId == request.remoteId) && (p.characteristic.uuid == request.characteristicUuid) && (p.characteristic.serviceUuid == request.serviceUuid)) .map((p) => p.characteristic.value); // Start listening now, before invokeMethod, to ensure we don't miss the response Future<List<int>> futureResponse = responseStream.first; await FlutterBluePlus.instance._channel .invokeMethod('readCharacteristic', request.writeToBuffer()); responseValue = await futureResponse; // push to stream _readValueController.add(responseValue); // cache latest value lastValue = responseValue; }).catchError((e, stacktrace) { throw Exception("$e $stacktrace"); }); return responseValue; }
@Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { Protos.ReadCharacteristicResponse.Builder p = Protos.ReadCharacteristicResponse.newBuilder(); p.setRemoteId(gatt.getDevice().getAddress()); p.setCharacteristic(ProtoMaker.from(gatt.getDevice(), characteristic, gatt)); invokeMethodUIThread("ReadCharacteristicResponse", p.build().toByteArray()); }
case "readCharacteristic": { try { byte[] data = call.arguments(); Protos.ReadCharacteristicRequest request = Protos.ReadCharacteristicRequest.newBuilder().mergeFrom(data).build(); BluetoothGatt gattServer = locateGatt(request.getRemoteId()); BluetoothGattCharacteristic characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); if(gattServer.readCharacteristic(characteristic) == false) { result.error("read_characteristic_error", "unknown reason, may occur if readCharacteristic was called before last read finished.", null); break; } result.success(null); } catch(Exception e) { result.error("read_characteristic_error", e.getMessage(), null); } break; }
再来看写操作:BluetoothCharacteristic.write
/// Writes the value of a characteristic. /// [CharacteristicWriteType.withoutResponse]: the write is not /// guaranteed and will return immediately with success. /// [CharacteristicWriteType.withResponse]: the method will return after the /// write operation has either passed or failed. Future<void> write(List<int> value, {bool withoutResponse = false}) async { // Only allow a single read or write operation // at a time, to prevent race conditions. await _readWriteMutex.synchronized(() async { final type = withoutResponse ? CharacteristicWriteType.withoutResponse : CharacteristicWriteType.withResponse; var request = protos.WriteCharacteristicRequest.create() ..remoteId = deviceId.toString() ..characteristicUuid = uuid.toString() ..serviceUuid = serviceUuid.toString() ..writeType = protos.WriteCharacteristicRequest_WriteType.valueOf(type.index)! ..value = value; if (type == CharacteristicWriteType.withResponse) { var responseStream = FlutterBluePlus.instance._methodStream .where((m) => m.method == "WriteCharacteristicResponse") .map((m) => m.arguments) .map((buffer) => protos.WriteCharacteristicResponse.fromBuffer(buffer)) .where((p) => (p.request.remoteId == request.remoteId) && (p.request.characteristicUuid == request.characteristicUuid) && (p.request.serviceUuid == request.serviceUuid)); // Start listening now, before invokeMethod, to ensure we don't miss the response Future<protos.WriteCharacteristicResponse> futureResponse = responseStream.first; await FlutterBluePlus.instance._channel .invokeMethod('writeCharacteristic', request.writeToBuffer()); // wait for response, so that we can check for success protos.WriteCharacteristicResponse response = await futureResponse; if (!response.success) { throw Exception('Failed to write the characteristic'); } return Future.value(); } else { // invoke without waiting for reply return FlutterBluePlus.instance._channel .invokeMethod('writeCharacteristic', request.writeToBuffer()); } }).catchError((e, stacktrace) { throw Exception("$e $stacktrace"); }); }
@Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { Protos.WriteCharacteristicRequest.Builder request = Protos.WriteCharacteristicRequest.newBuilder(); request.setRemoteId(gatt.getDevice().getAddress()); request.setCharacteristicUuid(characteristic.getUuid().toString()); request.setServiceUuid(characteristic.getService().getUuid().toString()); Protos.WriteCharacteristicResponse.Builder p = Protos.WriteCharacteristicResponse.newBuilder(); p.setRequest(request); p.setSuccess(status == BluetoothGatt.GATT_SUCCESS); invokeMethodUIThread("WriteCharacteristicResponse", p.build().toByteArray()); }
case "writeCharacteristic": { try { byte[] data = call.arguments(); Protos.WriteCharacteristicRequest request = Protos.WriteCharacteristicRequest.newBuilder().mergeFrom(data).build(); BluetoothGatt gattServer = locateGatt(request.getRemoteId()); BluetoothGattCharacteristic characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); // Set Value if(!characteristic.setValue(request.getValue().toByteArray())){ result.error("writeCharacteristic", "could not set the local value of characteristic", null); } // Write type if(request.getWriteType() == Protos.WriteCharacteristicRequest.WriteType.WITHOUT_RESPONSE) { characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); } else { characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); } // Write Char if(!gattServer.writeCharacteristic(characteristic)){ result.error("writeCharacteristic", "writeCharacteristic failed", null); break; } result.success(null); } catch(Exception e) { result.error("writeCharacteristic", e.getMessage(), null); } break; }
通知操作:
/// Sets notifications or indications for the value of a specified characteristic Future<bool> setNotifyValue(bool notify) async { var request = protos.SetNotificationRequest.create() ..remoteId = deviceId.toString() ..serviceUuid = serviceUuid.toString() ..characteristicUuid = uuid.toString() ..enable = notify; Stream<protos.SetNotificationResponse> responseStream = FlutterBluePlus.instance._methodStream .where((m) => m.method == "SetNotificationResponse") .map((m) => m.arguments) .map((buffer) => protos.SetNotificationResponse.fromBuffer(buffer)) .where((p) => (p.remoteId == request.remoteId) && (p.characteristic.uuid == request.characteristicUuid) && (p.characteristic.serviceUuid == request.serviceUuid)); // Start listening now, before invokeMethod, to ensure we don't miss the response Future<protos.SetNotificationResponse> futureResponse = responseStream.first; await FlutterBluePlus.instance._channel .invokeMethod('setNotification', request.writeToBuffer()); // wait for response, so that we can check for success protos.SetNotificationResponse response = await futureResponse; if (!response.success) { throw Exception('setNotifyValue failed'); } BluetoothCharacteristic c = BluetoothCharacteristic.fromProto(response.characteristic); _updateDescriptors(c.descriptors); return c.isNotifying == notify; }
@Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { .... if(descriptor.getUuid().compareTo(CCCD_ID) == 0) { // SetNotificationResponse Protos.SetNotificationResponse.Builder q = Protos.SetNotificationResponse.newBuilder(); q.setRemoteId(gatt.getDevice().getAddress()); q.setCharacteristic(ProtoMaker.from(gatt.getDevice(), descriptor.getCharacteristic(), gatt)); q.setSuccess(status == BluetoothGatt.GATT_SUCCESS); invokeMethodUIThread("SetNotificationResponse", q.build().toByteArray()); } }
case "setNotification": { try { byte[] data = call.arguments(); Protos.SetNotificationRequest request = Protos.SetNotificationRequest.newBuilder().mergeFrom(data).build(); BluetoothGatt gattServer = locateGatt(request.getRemoteId()); BluetoothGattCharacteristic characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); BluetoothGattDescriptor cccDescriptor = characteristic.getDescriptor(CCCD_ID); if(cccDescriptor == null) { //Some devices - including the widely used Bluno do not actually set the CCCD_ID. //thus setNotifications works perfectly (tested on Bluno) without cccDescriptor log(LogLevel.INFO, "could not locate CCCD descriptor for characteristic: " + characteristic.getUuid().toString()); } // start notifications if(!gattServer.setCharacteristicNotification(characteristic, request.getEnable())){ result.error("setNotification", "could not set characteristic notifications to :" + request.getEnable(), null); break; } // update descriptor value if(cccDescriptor != null) { byte[] value = null; // determine value if(request.getEnable()) {boolean canNotify = (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0;boolean canIndicate = (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0;if(!canIndicate && !canNotify) { result.error("setNotification", "characteristic cannot notify or indicate", null); break;}if(canIndicate) {value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;}if(canNotify) {value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;} } else {value = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; } if (!cccDescriptor.setValue(value)) {result.error("setNotification", "error setting descriptor value to: " + Arrays.toString(value), null);break; } if (!gattServer.writeDescriptor(cccDescriptor)) {result.error("setNotification", "error writing descriptor", null);break; } } result.success(null); } catch(Exception e) { result.error("setNotification", e.getMessage(), null); } break; }
可以看到,设置通知有两部,第一步是调用方法设置通知,第二部是获取CCCD类型的descriptor,识别出是Notify(没有应答)或是Indicate(需要应答)类型后写入descriptor,然后在onDescriptorWrite接收应答。
每个characteristic下面还有若干descriptor,也可以进行读写操作,与characteristic类似,就不重复说明了。
除此以外,还有MtuSize(设置最大传输单元),requestConnectionPriority(设置蓝牙设备请求连接的优先级),setPreferredPhy(设置接收和发送的速率),pair(配对)等等api,在此就不一一赘述了。