文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-08-18 16:22

关注

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时提高效率。

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

来源地址:https://blog.csdn.net/Yaoobs/article/details/131570861

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯