-
Notifications
You must be signed in to change notification settings - Fork 339
/
reactive_ble.dart
343 lines (303 loc) · 13.8 KB
/
reactive_ble.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import 'dart:async';
import 'dart:io';
import 'package:flutter_reactive_ble/src/connected_device_operation.dart';
import 'package:flutter_reactive_ble/src/debug_logger.dart';
import 'package:flutter_reactive_ble/src/device_connector.dart';
import 'package:flutter_reactive_ble/src/device_scanner.dart';
import 'package:flutter_reactive_ble/src/discovered_devices_registry.dart';
import 'package:flutter_reactive_ble/src/rx_ext/repeater.dart';
import 'package:meta/meta.dart';
import 'package:reactive_ble_mobile/reactive_ble_mobile.dart';
import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart';
/// [FlutterReactiveBle] is the facade of the library. Its interface allows to
/// perform all the supported BLE operations.
class FlutterReactiveBle {
static final FlutterReactiveBle _sharedInstance = FlutterReactiveBle._();
factory FlutterReactiveBle() => _sharedInstance;
///Create a new instance where injected depedencies are used.
@visibleForTesting
FlutterReactiveBle.witDependencies({
required DeviceScanner deviceScanner,
required DeviceConnector deviceConnector,
required ConnectedDeviceOperation connectedDeviceOperation,
required Logger debugLogger,
required Future<void> initialization,
required ReactiveBlePlatform reactiveBlePlatform,
}) {
_deviceScanner = deviceScanner;
_deviceConnector = deviceConnector;
_connectedDeviceOperator = connectedDeviceOperation;
_debugLogger = debugLogger;
_initialization = initialization;
_blePlatform = reactiveBlePlatform;
_trackStatus();
}
FlutterReactiveBle._() {
_trackStatus();
}
/// Registry that keeps track of all BLE devices found during a BLE scan.
final scanRegistry = DiscoveredDevicesRegistryImpl.standard();
/// A stream providing the host device BLE subsystem status updates.
///
/// Also see [status].
Stream<BleStatus> get statusStream => Repeater(onListenEmitFrom: () async* {
await initialize();
yield _status;
yield* _statusStream;
}).stream;
/// Returns the current status of the BLE subsystem of the host device.
///
/// Also see [statusStream].
BleStatus get status => _status;
/// A stream providing connection updates for all the connected BLE devices.
Stream<ConnectionStateUpdate> get connectedDeviceStream =>
Repeater(onListenEmitFrom: () async* {
await initialize();
yield* _deviceConnector.deviceConnectionStateUpdateStream;
}).stream.asBroadcastStream()
..listen((_) {});
/// A stream providing value updates for all the connected BLE devices.
///
/// The updates include read responses as well as notifications.
Stream<CharacteristicValue> get characteristicValueStream async* {
await initialize();
yield* _connectedDeviceOperator.characteristicValueStream;
}
late ReactiveBlePlatform _blePlatform;
BleStatus _status = BleStatus.unknown;
Stream<BleStatus> get _statusStream => _blePlatform.bleStatusStream;
Future<void> _trackStatus() async {
await initialize();
_statusStream.listen((status) => _status = status);
}
Future<void>? _initialization;
late DeviceConnector _deviceConnector;
late ConnectedDeviceOperation _connectedDeviceOperator;
late DeviceScanner _deviceScanner;
late Logger _debugLogger;
/// Initializes this [FlutterReactiveBle] instance and its platform-specific
/// counterparts.
///
/// The initialization is performed automatically the first time any BLE
/// operation is triggered.
Future<void> initialize() async {
if (_initialization == null) {
_debugLogger = DebugLogger(
'REACTIVE_BLE',
print,
);
ReactiveBlePlatform.instance =
const ReactiveBleMobilePlatformFactory().create(
logger: _debugLogger,
);
_blePlatform = ReactiveBlePlatform.instance;
_initialization ??= _blePlatform.initialize();
_connectedDeviceOperator = ConnectedDeviceOperationImpl(
blePlatform: _blePlatform,
);
_deviceScanner = DeviceScannerImpl(
blePlatform: _blePlatform,
platformIsAndroid: () => Platform.isAndroid,
delayAfterScanCompletion: Future<void>.delayed(
const Duration(milliseconds: 300),
),
addToScanRegistry: scanRegistry.add,
);
_deviceConnector = DeviceConnectorImpl(
blePlatform: _blePlatform,
deviceIsDiscoveredRecently: scanRegistry.deviceIsDiscoveredRecently,
deviceScanner: _deviceScanner,
delayAfterScanFailure: const Duration(seconds: 10),
);
await _initialization;
}
}
/// Deinitializes this [FlutterReactiveBle] instance and its platform-specific
/// counterparts.
///
/// The deinitialization is automatically performed on Flutter Hot Restart.
Future<void> deinitialize() async {
if (_initialization != null) {
_initialization = null;
await _blePlatform.deinitialize();
}
}
/// Reads the value of the specified characteristic.
///
/// The returned future completes with an error in case of a failure during reading.
///
/// Be aware that a read request could be satisfied by a notification delivered
/// for the same characteristic via [characteristicValueStream] before the actual
/// read response arrives (due to the design of iOS BLE API).
Future<List<int>> readCharacteristic(
QualifiedCharacteristic characteristic) async {
await initialize();
return _connectedDeviceOperator.readCharacteristic(characteristic);
}
/// Writes a value to the specified characteristic awaiting for an acknowledgement.
///
/// The returned future completes with an error in case of a failure during writing.
Future<void> writeCharacteristicWithResponse(
QualifiedCharacteristic characteristic, {
required List<int> value,
}) async {
await initialize();
return _connectedDeviceOperator.writeCharacteristicWithResponse(
characteristic,
value: value,
);
}
/// Writes a value to the specified characteristic without waiting for an acknowledgement.
///
/// Use this method in case the client does not need an acknowledgement
/// that the write was successfully performed. For subsequent write operations it is
/// recommended to execute a [writeCharacteristicWithResponse] each n times to make sure
/// the BLE device is still responsive.
///
/// The returned future completes with an error in case of a failure during writing.
Future<void> writeCharacteristicWithoutResponse(
QualifiedCharacteristic characteristic, {
required List<int> value,
}) async {
await initialize();
return _connectedDeviceOperator.writeCharacteristicWithoutResponse(
characteristic,
value: value,
);
}
/// Request a specific MTU for a connected device.
///
/// Returns the actual MTU negotiated.
///
/// For reference:
///
/// * BLE 4.0–4.1 max ATT MTU is 23 bytes
/// * BLE 4.2–5.1 max ATT MTU is 247 bytes
Future<int> requestMtu({required String deviceId, required int mtu}) async {
await initialize();
return _connectedDeviceOperator.requestMtu(deviceId, mtu);
}
/// Requests for a connection parameter update on the connected device.
///
/// Always completes with an error on iOS, as there is no way (and no need) to perform this operation on iOS.
Future<void> requestConnectionPriority(
{required String deviceId, required ConnectionPriority priority}) async {
await initialize();
return _connectedDeviceOperator.requestConnectionPriority(
deviceId, priority);
}
/// Scan for BLE peripherals advertising the services specified in [withServices]
/// or for all BLE peripherals, if no services is specified. It is recommended to always specify some services.
///
/// There are two Android specific parameters that are ignored on iOS:
///
/// - [scanMode] allows to choose between different levels of power efficient and/or low latency scan modes.
/// - [requireLocationServicesEnabled] specifies whether to check if location services are enabled before scanning.
/// When set to true and location services are disabled, an exception is thrown. Default is true.
/// Setting the value to false can result in not finding BLE peripherals on some Android devices.
Stream<DiscoveredDevice> scanForDevices({
required List<Uuid> withServices,
ScanMode scanMode = ScanMode.balanced,
bool requireLocationServicesEnabled = true,
}) async* {
await initialize();
yield* _deviceScanner.scanForDevices(
withServices: withServices,
scanMode: scanMode,
requireLocationServicesEnabled: requireLocationServicesEnabled,
);
}
/// Establishes a connection to a BLE device.
///
/// Disconnecting the device is achieved by cancelling the stream subscription.
///
/// [id] is the unique device id of the BLE device: in iOS this is a uuid and on Android this is
/// a Mac-Adress.
/// Use [servicesWithCharacteristicsToDiscover] to scan only for the specific services mentioned in this map,
/// this can improve the connection speed on iOS since no full service discovery will be executed. On Android
/// this variable is ignored since partial discovery is not possible.
/// If [connectionTimeout] parameter is supplied and a connection is not established before [connectionTimeout] expires,
/// the pending connection attempt will be cancelled and a [TimeoutException] error will be emitted into the returned stream.
/// On Android when no timeout is specified the `autoConnect` flag is set in the [connectGatt()](https://developer.android.com/reference/android/bluetooth/BluetoothDevice#connectGatt(android.content.Context,%20boolean,%20android.bluetooth.BluetoothGattCallback)) call, otherwise it is cleared.
Stream<ConnectionStateUpdate> connectToDevice({
required String id,
Map<Uuid, List<Uuid>>? servicesWithCharacteristicsToDiscover,
Duration? connectionTimeout,
}) =>
initialize().asStream().asyncExpand(
(_) => _deviceConnector.connect(
id: id,
servicesWithCharacteristicsToDiscover:
servicesWithCharacteristicsToDiscover,
connectionTimeout: connectionTimeout,
),
);
/// Scans for a specific device and connects to it in case a device containing the specified [id]
/// is found and that is advertising the services specified in [withServices].
///
/// Disconnecting the device is achieved by cancelling the stream subscription.
///
/// The [prescanDuration] is the amount of time BLE disovery should run in order to find the device.
/// Use [servicesWithCharacteristicsToDiscover] to scan only for the specific services mentioned in this map,
/// this can improve the connection speed on iOS since no full service discovery will be executed. On Android
/// this variable is ignored since partial discovery is not possible.
/// If [connectionTimeout] parameter is supplied and a connection is not established before [connectionTimeout] expires,
/// the pending connection attempt will be cancelled and a [TimeoutException] error will be emitted into the returned stream.
/// On Android when no timeout is specified the `autoConnect` flag is set in the [connectGatt()](https://developer.android.com/reference/android/bluetooth/BluetoothDevice#connectGatt(android.content.Context,%20boolean,%20android.bluetooth.BluetoothGattCallback)) call, otherwise it is cleared.
Stream<ConnectionStateUpdate> connectToAdvertisingDevice({
required String id,
required List<Uuid> withServices,
required Duration prescanDuration,
Map<Uuid, List<Uuid>>? servicesWithCharacteristicsToDiscover,
Duration? connectionTimeout,
}) =>
initialize().asStream().asyncExpand(
(_) => _deviceConnector.connectToAdvertisingDevice(
id: id,
withServices: withServices,
prescanDuration: prescanDuration,
servicesWithCharacteristicsToDiscover:
servicesWithCharacteristicsToDiscover,
connectionTimeout: connectionTimeout,
),
);
/// Performs service discovery on the peripheral and returns the discovered services.
///
/// When discovery fails this method throws an [Exception].
Future<List<DiscoveredService>> discoverServices(String deviceId) =>
_connectedDeviceOperator.discoverServices(deviceId);
/// Clears GATT attribute cache on Android using undocumented API. Completes with an error in case of a failure.
///
/// Always completes with an error on iOS, as there is no way (and no need) to perform this operation on iOS.
///
/// The connection may need to be reestablished after successful GATT attribute cache clearing.
Future<void> clearGattCache(String deviceId) => _blePlatform
.clearGattCache(deviceId)
.then((info) => info.dematerialize());
/// Subscribes to updates from the characteristic specified.
///
/// This stream terminates automatically when the device is disconnected.
Stream<List<int>> subscribeToCharacteristic(
QualifiedCharacteristic characteristic,
) {
final isDisconnected = connectedDeviceStream
.where((update) =>
update.deviceId == characteristic.deviceId &&
(update.connectionState == DeviceConnectionState.disconnecting ||
update.connectionState == DeviceConnectionState.disconnected))
.cast<void>()
.firstWhere((_) => true, orElse: () {});
return initialize().asStream().asyncExpand(
(_) => _connectedDeviceOperator.subscribeToCharacteristic(
characteristic,
isDisconnected,
),
);
}
/// Sets the verbosity of debug output.
///
/// Use [LogLevel.verbose] for full debug output. Make sure to run this only for debugging purposes.
/// Use [LogLevel.none] to disable logging. This is also the default.
set logLevel(LogLevel logLevel) => _debugLogger.logLevel = logLevel;
LogLevel get logLevel => _debugLogger.logLevel;
}