Flutter蓝牙框架-flutter_blue_plus使用及源码解析

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。

  1. 首先,我们打开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,看是缓存还是发送。
至此,蓝牙状态相关代码就分析完了。

  1. 之后,我们来看下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了。

  1. 最后,我们看看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时提高效率。

4.接着我们看下点击按钮后跳转的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,在此就不一一赘述了。

猜你喜欢

转载自blog.csdn.net/Yaoobs/article/details/131570861