Flutter蓝牙框架-flutter

2024-04-08 1214阅读

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

  • 前言
  • 低功耗蓝牙(BLE)原理
  • 使用及源码解析

    前言

    前段时间有朋友拜托我研究下flutter利用蓝牙与硬件交互的功能,我查阅了很多资料,目前市面上比较流行的第三方库有两个,一个是flutter_blue_plus,一个是flutter_reactive_ble,前一个比较轻量级,能满足大部分场景,后一个比较复杂,支持多个蓝牙设备同时连接。那么这一次我们先来研究下flutter_blue_plus,剩下的flutter_reactive_ble下次有机会再来看。

    Flutter蓝牙框架-flutter
    (图片来源网络,侵删)

    低功耗蓝牙(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);
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          color: Colors.lightBlue,
          home: StreamBuilder(
              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 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 turnOn()
        {
            return _channel.invokeMethod('turnOn').then((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(
            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 withServices = const [],
            List withDevices = const [],
            List 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 scan({
            ScanMode scanMode = ScanMode.lowLatency,
            List withServices = const [],
            List withDevices = const [],
            List 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([]);
            Stream 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 list = List.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 _methodStreamController = StreamController.broadcast();
        final _BehaviorSubject _isScanning = _BehaviorSubject(false);
        final _BehaviorSubject _scanResults = _BehaviorSubject([]);
    	Stream 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 get scanResults => _scanResults.stream;
        // Used internally to dispatch methods from platform.
        Stream 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 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 results)
                    {
                        super.onBatchScanResults(results);
                    }
                    @Override
                    public void onScanFailed(int errorCode)
                    {
                        super.onScanFailed(errorCode);
                    }
                };
            }
            return scanCallback;
        }
    

    每次扫描到结果都会调用onScanResult方法,然后通过macDeviceScanned记录已经扫描到的数据,去重。invokeMethodUIThread这个方法是通过handler做线程切换,保证在主线程返回结果。

    2) 接着,我们看下FindDevicesScreen里面的扫描结果列表:

                  StreamBuilder(
                    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 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 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 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 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(
                      stream: device.isDiscoveringServices,
                      initialData: false,
                      builder: (c, snapshot) => IndexedStack(
                        index: snapshot.data! ? 1 : 0,
                        children: [
                          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 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 futureResponse = responseStream.first;
            await FlutterBluePlus.instance._channel
                .invokeMethod('discoverServices', id.toString());
            // wait for response
            List 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(
                  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 get services async*
        {
            List 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 read() async
        {
            List 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 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 write(List 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 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 setNotifyValue(bool notify) async
        {
            var request = protos.SetNotificationRequest.create()
            ..remoteId = deviceId.toString()
            ..serviceUuid = serviceUuid.toString()
            ..characteristicUuid = uuid.toString()
            ..enable = notify;
            Stream 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 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,在此就不一一赘述了。

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]