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