diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 986ee288..38179f0e 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -45,7 +45,7 @@ android { defaultConfig { applicationId "com.signify.hue.reactivebleexample" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/example/android/build.gradle b/example/android/build.gradle index 016184d1..2353f21a 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.7.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 89e56bdb..09523c0e 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/example/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 2330e194..ab9d9ba4 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -4,7 +4,6 @@ classes = { }; objectVersion = 54; - objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -169,7 +168,6 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 29DK8LS8B3; LastSwiftMigration = 0910; }; }; @@ -431,7 +429,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 29DK8LS8B3; + DEVELOPMENT_TEAM = 5KK9N97KX3; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -446,7 +444,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.signify.hue.reactivebleExample; + PRODUCT_BUNDLE_IDENTIFIER = com.signify.hue.tangoReactiveBleExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; diff --git a/example/lib/main.dart b/example/lib/main.dart index aa73e5a7..97e2bf86 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/ble_device_bond_monitor.dart'; import 'package:flutter_reactive_ble_example/src/ble/ble_device_connector.dart'; import 'package:flutter_reactive_ble_example/src/ble/ble_device_interactor.dart'; import 'package:flutter_reactive_ble_example/src/ble/ble_scanner.dart'; @@ -19,8 +20,10 @@ void main() { final _bleLogger = BleLogger(ble: _ble); final _scanner = BleScanner(ble: _ble, logMessage: _bleLogger.addToLog); final _monitor = BleStatusMonitor(_ble); + final _bondMonitor = BleDeviceBondMonitor(_ble); final _connector = BleDeviceConnector( ble: _ble, + bondMonitor: _bondMonitor, logMessage: _bleLogger.addToLog, ); final _serviceDiscoverer = BleDeviceInteractor( @@ -39,6 +42,7 @@ void main() { Provider.value(value: _connector), Provider.value(value: _serviceDiscoverer), Provider.value(value: _bleLogger), + Provider.value(value: _bondMonitor), StreamProvider( create: (_) => _scanner.state, initialData: const BleScannerState( @@ -50,6 +54,10 @@ void main() { create: (_) => _monitor.state, initialData: BleStatus.unknown, ), + StreamProvider( + create: (_) => _bondMonitor.state, + initialData: DeviceBondState.unknown, + ), StreamProvider( create: (_) => _connector.state, initialData: const ConnectionStateUpdate( diff --git a/example/lib/src/ble/ble_device_bond_monitor.dart b/example/lib/src/ble/ble_device_bond_monitor.dart new file mode 100644 index 00000000..a79b2a1b --- /dev/null +++ b/example/lib/src/ble/ble_device_bond_monitor.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart'; + +class BleDeviceBondMonitor implements ReactiveState { + BleDeviceBondMonitor(this._ble); + + final FlutterReactiveBle _ble; + + @override + Stream get state => _bondStateController.stream; + + final StreamController _bondStateController = + StreamController(); + + StreamSubscription? _bondStateSubscription; + + void startMonitoringDevice(String deviceId) { + _bondStateSubscription ??= _ble.bondUpdateStream + .where((update) => update.deviceId == deviceId) + .map((update) => update.bondState) + .listen(_bondStateController.add); + } + + void stopMontoringDevice(String deviceId) { + _bondStateSubscription?.cancel(); + _bondStateSubscription = null; + } + + Future dispose() async { + await _bondStateController.close(); + } +} diff --git a/example/lib/src/ble/ble_device_connector.dart b/example/lib/src/ble/ble_device_connector.dart index 536ff8b0..642a41e1 100644 --- a/example/lib/src/ble/ble_device_connector.dart +++ b/example/lib/src/ble/ble_device_connector.dart @@ -1,16 +1,20 @@ import 'dart:async'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/ble_device_bond_monitor.dart'; import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart'; class BleDeviceConnector extends ReactiveState { BleDeviceConnector({ required FlutterReactiveBle ble, + required BleDeviceBondMonitor bondMonitor, required void Function(String message) logMessage, }) : _ble = ble, + _bondMonitor = bondMonitor, _logMessage = logMessage; final FlutterReactiveBle _ble; + final BleDeviceBondMonitor _bondMonitor; final void Function(String message) _logMessage; @override @@ -22,6 +26,8 @@ class BleDeviceConnector extends ReactiveState { late StreamSubscription _connection; Future connect(String deviceId) async { + _bondMonitor.startMonitoringDevice(deviceId); + _logMessage('Start connecting to $deviceId'); _connection = _ble.connectToDevice(id: deviceId).listen( (update) { @@ -35,6 +41,8 @@ class BleDeviceConnector extends ReactiveState { } Future disconnect(String deviceId) async { + _bondMonitor.stopMontoringDevice(deviceId); + try { _logMessage('disconnecting to device: $deviceId'); await _connection.cancel(); diff --git a/example/lib/src/ui/device_detail/device_interaction_tab.dart b/example/lib/src/ui/device_detail/device_interaction_tab.dart index 02a2fb98..3c1303f6 100644 --- a/example/lib/src/ui/device_detail/device_interaction_tab.dart +++ b/example/lib/src/ui/device_detail/device_interaction_tab.dart @@ -20,14 +20,19 @@ class DeviceInteractionTab extends StatelessWidget { final DiscoveredDevice device; @override - Widget build(BuildContext context) => Consumer3( - builder: (_, deviceConnector, connectionStateUpdate, serviceDiscoverer, __) => _DeviceInteractionTab( + Widget build(BuildContext context) => Consumer4( + builder: (_, deviceConnector, bondState, connectionStateUpdate, + serviceDiscoverer, __) => + _DeviceInteractionTab( viewModel: DeviceInteractionViewModel( deviceId: device.id, + bondState: bondState, connectableStatus: device.connectable, connectionStatus: connectionStateUpdate.connectionState, deviceConnector: deviceConnector, - discoverServices: () => serviceDiscoverer.discoverServices(device.id), + discoverServices: () => + serviceDiscoverer.discoverServices(device.id), readRssi: () => serviceDiscoverer.readRssi(device.id), ), ), @@ -40,6 +45,7 @@ class DeviceInteractionViewModel extends $DeviceInteractionViewModel { const DeviceInteractionViewModel({ required this.deviceId, required this.connectableStatus, + required this.bondState, required this.connectionStatus, required this.deviceConnector, required this.discoverServices, @@ -48,6 +54,7 @@ class DeviceInteractionViewModel extends $DeviceInteractionViewModel { final String deviceId; final Connectable connectableStatus; + final DeviceBondState bondState; final DeviceConnectionState connectionStatus; final BleDeviceConnector deviceConnector; final Future Function() readRssi; @@ -55,7 +62,8 @@ class DeviceInteractionViewModel extends $DeviceInteractionViewModel { @CustomEquality(Ignore()) final Future> Function() discoverServices; - bool get deviceConnected => connectionStatus == DeviceConnectionState.connected; + bool get deviceConnected => + connectionStatus == DeviceConnectionState.connected; void connect() { deviceConnector.connect(deviceId); @@ -110,7 +118,8 @@ class _DeviceInteractionTabState extends State<_DeviceInteractionTab> { delegate: SliverChildListDelegate.fixed( [ Padding( - padding: const EdgeInsetsDirectional.only(top: 8.0, bottom: 16.0, start: 16.0), + padding: const EdgeInsetsDirectional.only( + top: 8.0, bottom: 16.0, start: 16.0), child: Text( "ID: ${widget.viewModel.deviceId}", style: const TextStyle(fontWeight: FontWeight.bold), @@ -137,27 +146,39 @@ class _DeviceInteractionTabState extends State<_DeviceInteractionTab> { style: const TextStyle(fontWeight: FontWeight.bold), ), ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text( + "Bond status: ${widget.viewModel.bondState}", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: Wrap( alignment: WrapAlignment.spaceEvenly, children: [ ElevatedButton( - onPressed: !widget.viewModel.deviceConnected ? widget.viewModel.connect : null, + onPressed: !widget.viewModel.deviceConnected + ? widget.viewModel.connect + : null, child: const Text("Connect"), ), ElevatedButton( - onPressed: widget.viewModel.deviceConnected ? widget.viewModel.disconnect : null, + onPressed: widget.viewModel.deviceConnected + ? widget.viewModel.disconnect + : null, child: const Text("Disconnect"), ), ElevatedButton( - onPressed: widget.viewModel.deviceConnected ? discoverServices : null, + onPressed: widget.viewModel.deviceConnected + ? discoverServices + : null, child: const Text("Discover Services"), ), ElevatedButton( - onPressed: widget.viewModel.deviceConnected - ? readRssi - : null, + onPressed: + widget.viewModel.deviceConnected ? readRssi : null, child: const Text("Get RSSI"), ), ], @@ -222,7 +243,8 @@ class _ServiceDiscoveryListState extends State<_ServiceDiscoveryList> { Widget _characteristicTile(Characteristic characteristic) => ListTile( onTap: () => showDialog( context: context, - builder: (context) => CharacteristicInteractionDialog(characteristic: characteristic), + builder: (context) => + CharacteristicInteractionDialog(characteristic: characteristic), ), title: Text( '${characteristic.id}\n(${_characteristicSummary(characteristic)})', @@ -253,7 +275,9 @@ class _ServiceDiscoveryListState extends State<_ServiceDiscoveryList> { ), Column( mainAxisSize: MainAxisSize.min, - children: service.characteristics.map(_characteristicTile).toList(), + children: service.characteristics + .map(_characteristicTile) + .toList(), ), ], ), diff --git a/example/lib/src/ui/device_detail/device_interaction_tab.g.dart b/example/lib/src/ui/device_detail/device_interaction_tab.g.dart index b4f78b7c..f704061d 100644 --- a/example/lib/src/ui/device_detail/device_interaction_tab.g.dart +++ b/example/lib/src/ui/device_detail/device_interaction_tab.g.dart @@ -11,6 +11,7 @@ abstract class $DeviceInteractionViewModel { String get deviceId; Connectable get connectableStatus; + DeviceBondState get bondState; DeviceConnectionState get connectionStatus; BleDeviceConnector get deviceConnector; Future Function() get readRssi; @@ -19,6 +20,7 @@ abstract class $DeviceInteractionViewModel { DeviceInteractionViewModel copyWith({ String? deviceId, Connectable? connectableStatus, + DeviceBondState? bondState, DeviceConnectionState? connectionStatus, BleDeviceConnector? deviceConnector, Future Function()? readRssi, @@ -27,6 +29,7 @@ abstract class $DeviceInteractionViewModel { DeviceInteractionViewModel( deviceId: deviceId ?? this.deviceId, connectableStatus: connectableStatus ?? this.connectableStatus, + bondState: bondState ?? this.bondState, connectionStatus: connectionStatus ?? this.connectionStatus, deviceConnector: deviceConnector ?? this.deviceConnector, readRssi: readRssi ?? this.readRssi, @@ -38,6 +41,7 @@ abstract class $DeviceInteractionViewModel { final change = DeviceInteractionViewModel$Change._( this.deviceId, this.connectableStatus, + this.bondState, this.connectionStatus, this.deviceConnector, this.readRssi, @@ -47,6 +51,7 @@ abstract class $DeviceInteractionViewModel { return DeviceInteractionViewModel( deviceId: change.deviceId, connectableStatus: change.connectableStatus, + bondState: change.bondState, connectionStatus: change.connectionStatus, deviceConnector: change.deviceConnector, readRssi: change.readRssi, @@ -56,7 +61,7 @@ abstract class $DeviceInteractionViewModel { @override String toString() => - "DeviceInteractionViewModel(deviceId: $deviceId, connectableStatus: $connectableStatus, connectionStatus: $connectionStatus, deviceConnector: $deviceConnector, readRssi: $readRssi, discoverServices: $discoverServices)"; + "DeviceInteractionViewModel(deviceId: $deviceId, connectableStatus: $connectableStatus, bondState: $bondState, connectionStatus: $connectionStatus, deviceConnector: $deviceConnector, readRssi: $readRssi, discoverServices: $discoverServices)"; @override // ignore: avoid_equals_and_hash_code_on_mutable_classes @@ -65,6 +70,7 @@ abstract class $DeviceInteractionViewModel { other.runtimeType == runtimeType && deviceId == other.deviceId && connectableStatus == other.connectableStatus && + bondState == other.bondState && connectionStatus == other.connectionStatus && deviceConnector == other.deviceConnector && readRssi == other.readRssi && @@ -76,6 +82,7 @@ abstract class $DeviceInteractionViewModel { var result = 17; result = 37 * result + deviceId.hashCode; result = 37 * result + connectableStatus.hashCode; + result = 37 * result + bondState.hashCode; result = 37 * result + connectionStatus.hashCode; result = 37 * result + deviceConnector.hashCode; result = 37 * result + readRssi.hashCode; @@ -88,6 +95,7 @@ class DeviceInteractionViewModel$Change { DeviceInteractionViewModel$Change._( this.deviceId, this.connectableStatus, + this.bondState, this.connectionStatus, this.deviceConnector, this.readRssi, @@ -96,6 +104,7 @@ class DeviceInteractionViewModel$Change { String deviceId; Connectable connectableStatus; + DeviceBondState bondState; DeviceConnectionState connectionStatus; BleDeviceConnector deviceConnector; Future Function() readRssi; @@ -119,6 +128,12 @@ class DeviceInteractionViewModel$ { connectableStatus: connectableStatus), ); + static final bondState = Lens( + (bondStateContainer) => bondStateContainer.bondState, + (bondStateContainer, bondState) => + bondStateContainer.copyWith(bondState: bondState), + ); + static final connectionStatus = Lens( (connectionStatusContainer) => connectionStatusContainer.connectionStatus, diff --git a/packages/flutter_reactive_ble/lib/src/reactive_ble.dart b/packages/flutter_reactive_ble/lib/src/reactive_ble.dart index cf89ef05..9f0a64c2 100644 --- a/packages/flutter_reactive_ble/lib/src/reactive_ble.dart +++ b/packages/flutter_reactive_ble/lib/src/reactive_ble.dart @@ -65,6 +65,13 @@ class FlutterReactiveBle { /// Also see [statusStream]. BleStatus get status => _status; + /// A stream providing bond updates for all the connected BLE devices. + Stream get bondUpdateStream => + Repeater.broadcast(onListenEmitFrom: () async* { + await initialize(); + yield* _blePlatform.bondUpdateStream; + }).stream; + /// A stream providing connection updates for all the connected BLE devices. Stream get connectedDeviceStream => Repeater.broadcast(onListenEmitFrom: () async* { diff --git a/packages/flutter_reactive_ble/test/connected_device_operation_test.mocks.dart b/packages/flutter_reactive_ble/test/connected_device_operation_test.mocks.dart index 7e8c28a1..417f482e 100644 --- a/packages/flutter_reactive_ble/test/connected_device_operation_test.mocks.dart +++ b/packages/flutter_reactive_ble/test/connected_device_operation_test.mocks.dart @@ -65,6 +65,12 @@ class MockReactiveBlePlatform extends _i1.Mock _i1.throwOnMissingStub(this); } + @override + _i4.Stream<_i2.BondStateUpdate> get bondUpdateStream => (super.noSuchMethod( + Invocation.getter(#bondUpdateStream), + returnValue: _i4.Stream<_i2.BondStateUpdate>.empty(), + ) as _i4.Stream<_i2.BondStateUpdate>); + @override _i4.Stream<_i2.ScanResult> get scanStream => (super.noSuchMethod( Invocation.getter(#scanStream), diff --git a/packages/flutter_reactive_ble/test/device_connector_test.mocks.dart b/packages/flutter_reactive_ble/test/device_connector_test.mocks.dart index b90cb617..7ca7341f 100644 --- a/packages/flutter_reactive_ble/test/device_connector_test.mocks.dart +++ b/packages/flutter_reactive_ble/test/device_connector_test.mocks.dart @@ -77,6 +77,12 @@ class MockReactiveBlePlatform extends _i1.Mock _i1.throwOnMissingStub(this); } + @override + _i4.Stream<_i2.BondStateUpdate> get bondUpdateStream => (super.noSuchMethod( + Invocation.getter(#bondUpdateStream), + returnValue: _i4.Stream<_i2.BondStateUpdate>.empty(), + ) as _i4.Stream<_i2.BondStateUpdate>); + @override _i4.Stream<_i2.ScanResult> get scanStream => (super.noSuchMethod( Invocation.getter(#scanStream), diff --git a/packages/flutter_reactive_ble/test/device_scanner_test.mocks.dart b/packages/flutter_reactive_ble/test/device_scanner_test.mocks.dart index a540f293..4201ce16 100644 --- a/packages/flutter_reactive_ble/test/device_scanner_test.mocks.dart +++ b/packages/flutter_reactive_ble/test/device_scanner_test.mocks.dart @@ -65,6 +65,12 @@ class MockReactiveBlePlatform extends _i1.Mock _i1.throwOnMissingStub(this); } + @override + _i4.Stream<_i2.BondStateUpdate> get bondUpdateStream => (super.noSuchMethod( + Invocation.getter(#bondUpdateStream), + returnValue: _i4.Stream<_i2.BondStateUpdate>.empty(), + ) as _i4.Stream<_i2.BondStateUpdate>); + @override _i4.Stream<_i2.ScanResult> get scanStream => (super.noSuchMethod( Invocation.getter(#scanStream), diff --git a/packages/flutter_reactive_ble/test/reactive_ble_test.mocks.dart b/packages/flutter_reactive_ble/test/reactive_ble_test.mocks.dart index 46e5405f..dd4754eb 100644 --- a/packages/flutter_reactive_ble/test/reactive_ble_test.mocks.dart +++ b/packages/flutter_reactive_ble/test/reactive_ble_test.mocks.dart @@ -68,6 +68,12 @@ class MockReactiveBlePlatform extends _i1.Mock _i1.throwOnMissingStub(this); } + @override + _i4.Stream<_i2.BondStateUpdate> get bondUpdateStream => (super.noSuchMethod( + Invocation.getter(#bondUpdateStream), + returnValue: _i4.Stream<_i2.BondStateUpdate>.empty(), + ) as _i4.Stream<_i2.BondStateUpdate>); + @override _i4.Stream<_i2.ScanResult> get scanStream => (super.noSuchMethod( Invocation.getter(#scanStream), diff --git a/packages/reactive_ble_mobile/android/build.gradle b/packages/reactive_ble_mobile/android/build.gradle index e39fa4c3..0e1915d9 100644 --- a/packages/reactive_ble_mobile/android/build.gradle +++ b/packages/reactive_ble_mobile/android/build.gradle @@ -38,7 +38,7 @@ apply plugin: "io.gitlab.arturbosch.detekt" apply plugin: "de.mannodermaus.android-junit5" android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' test.java.srcDirs += 'src/test/kotlin' @@ -51,7 +51,7 @@ android { defaultConfig { minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 consumerProguardFiles 'proguard-rules.txt' } diff --git a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/PluginController.kt b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/PluginController.kt index 7a5a4993..e71e9750 100644 --- a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/PluginController.kt +++ b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/PluginController.kt @@ -4,6 +4,7 @@ import android.content.Context import com.signify.hue.flutterreactiveble.ble.RequestConnectionPriorityFailed import com.signify.hue.flutterreactiveble.channelhandlers.BleStatusHandler import com.signify.hue.flutterreactiveble.channelhandlers.CharNotificationHandler +import com.signify.hue.flutterreactiveble.channelhandlers.DeviceBondHandler import com.signify.hue.flutterreactiveble.channelhandlers.DeviceConnectionHandler import com.signify.hue.flutterreactiveble.channelhandlers.ScanDevicesHandler import com.signify.hue.flutterreactiveble.channelhandlers.RestoredDeviceHandler @@ -66,18 +67,21 @@ class PluginController { deviceConnectionChannel = EventChannel(messenger, "flutter_reactive_ble_connected_device") charNotificationChannel = EventChannel(messenger, "flutter_reactive_ble_char_update") val bleStatusChannel = EventChannel(messenger, "flutter_reactive_ble_status") + val bondUpdateChannel = EventChannel(messenger, "flutter_reactive_ble_bond_update") val restoredDeviceChannel = EventChannel(messenger, "flutter_reactive_ble_restored_device") scanDevicesHandler = ScanDevicesHandler(bleClient) deviceConnectionHandler = DeviceConnectionHandler(bleClient) charNotificationHandler = CharNotificationHandler(bleClient) val bleStatusHandler = BleStatusHandler(bleClient) + val bondUpdateHandler = DeviceBondHandler(bleClient) val restoredDeviceHandler = RestoredDeviceHandler() scanchannel.setStreamHandler(scanDevicesHandler) deviceConnectionChannel.setStreamHandler(deviceConnectionHandler) charNotificationChannel.setStreamHandler(charNotificationHandler) bleStatusChannel.setStreamHandler(bleStatusHandler) + bondUpdateChannel.setStreamHandler(bondUpdateHandler) restoredDeviceChannel.setStreamHandler(restoredDeviceHandler) } diff --git a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt index 75870941..98943e3e 100644 --- a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt +++ b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt @@ -13,6 +13,7 @@ import java.util.UUID @Suppress("TooManyFunctions") interface BleClient { val connectionUpdateSubject: BehaviorSubject + val bondUpdateSubject: BehaviorSubject fun initializeClient() diff --git a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt index d928f777..2d9ebc31 100644 --- a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt +++ b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt @@ -71,6 +71,11 @@ data class RequestConnectionPrioritySuccess(val deviceId: String) : RequestConne data class RequestConnectionPriorityFailed(val deviceId: String, val errorMessage: String) : RequestConnectionPriorityResult() +data class BondUpdate( + val deviceId: String, + val bondState: Int, +) + enum class BleStatus(val code: Int) { UNKNOWN(code = 0), UNSUPPORTED(code = 1), diff --git a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ReactiveBleClient.kt b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ReactiveBleClient.kt index 9fa076c1..d1126e88 100644 --- a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ReactiveBleClient.kt +++ b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ReactiveBleClient.kt @@ -1,8 +1,12 @@ package com.signify.hue.flutterreactiveble.ble +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.BOND_BONDING import android.bluetooth.BluetoothGattCharacteristic +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Build import android.os.ParcelUuid import androidx.annotation.VisibleForTesting @@ -20,7 +24,9 @@ import com.signify.hue.flutterreactiveble.ble.extensions.resolveCharacteristic import com.signify.hue.flutterreactiveble.ble.extensions.writeCharWithResponse import com.signify.hue.flutterreactiveble.ble.extensions.writeCharWithoutResponse import com.signify.hue.flutterreactiveble.converters.extractManufacturerData +import com.signify.hue.flutterreactiveble.model.BondState import com.signify.hue.flutterreactiveble.model.ScanMode +import com.signify.hue.flutterreactiveble.model.getBondState import com.signify.hue.flutterreactiveble.model.toScanSettings import com.signify.hue.flutterreactiveble.utils.Duration import com.signify.hue.flutterreactiveble.utils.toBleState @@ -45,17 +51,29 @@ open class ReactiveBleClient(private val context: Context) : BleClient { private val connectionUpdateBehaviorSubject: BehaviorSubject = BehaviorSubject.create() + private val bondUpdateBehaviorSubject: BehaviorSubject = + BehaviorSubject.create() + lateinit var rxBleClient: RxBleClient internal set + internal var activeConnections = mutableMapOf() } override val connectionUpdateSubject: BehaviorSubject get() = connectionUpdateBehaviorSubject + override val bondUpdateSubject: BehaviorSubject + get() = bondUpdateBehaviorSubject + override fun initializeClient() { activeConnections = mutableMapOf() rxBleClient = RxBleClient.create(context) + + context.applicationContext.registerReceiver( + bondStateReceiver, + IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED), + ) } /*yes spread operator is not performant but after kotlin v1.60 it is less bad and it is also the @@ -276,7 +294,7 @@ open class ReactiveBleClient(private val context: Context) : BleClient { internal open fun createDeviceConnector( device: RxBleDevice, timeout: Duration, - ) = DeviceConnector(device, timeout, connectionUpdateBehaviorSubject::onNext, connectionQueue) + ) = DeviceConnector(device, timeout, ::onConnectionUpdate, connectionQueue) private fun getConnection( deviceId: String, @@ -404,4 +422,43 @@ open class ReactiveBleClient(private val context: Context) : BleClient { .setShouldLogAttributeValues(true) .build(), ) + + private fun onConnectionUpdate(update: ConnectionUpdate) { + when (update) { + is ConnectionUpdateSuccess -> { + val device = rxBleClient.getBleDevice(update.deviceId) + bondUpdateBehaviorSubject.onNext( + BondUpdate(update.deviceId, device.getBondState().code), + ) + } + + is ConnectionUpdateError -> { + val device = rxBleClient.getBleDevice(update.deviceId) + bondUpdateBehaviorSubject.onNext( + BondUpdate(update.deviceId, device.getBondState().code), + ) + } + } + + connectionUpdateBehaviorSubject.onNext(update) + } + + private val bondStateReceiver = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + val device = + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + val state = + intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR) + + if (device != null && state != BluetoothDevice.ERROR) { + bondUpdateBehaviorSubject.onNext( + BondUpdate(device.address, BondState.fromRaw(state).code), + ) + } + } + } } diff --git a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/DeviceBondHandler.kt b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/DeviceBondHandler.kt new file mode 100644 index 00000000..554e118e --- /dev/null +++ b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/DeviceBondHandler.kt @@ -0,0 +1,35 @@ +package com.signify.hue.flutterreactiveble.channelhandlers + +import com.signify.hue.flutterreactiveble.converters.ProtobufMessageConverter +import io.flutter.plugin.common.EventChannel +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable + +class DeviceBondHandler( + private val bleClient: com.signify.hue.flutterreactiveble.ble.BleClient, +) : EventChannel.StreamHandler { + private var sink: EventChannel.EventSink? = null + private val converter = ProtobufMessageConverter() + + private lateinit var disposable: Disposable + + override fun onListen( + objectSink: Any?, + eventSink: EventChannel.EventSink?, + ) { + eventSink?.let { + sink = eventSink + disposable = + bleClient.bondUpdateSubject + .distinct() + .observeOn(AndroidSchedulers.mainThread()) + .map(converter::convertToBondInfo) + .map { it.toByteArray() } + .subscribe { sink?.success(it) } + } + } + + override fun onCancel(objectSink: Any?) { + disposable.dispose() + } +} diff --git a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/RestoredDeviceHandler.kt b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/RestoredDeviceHandler.kt index 699b62f6..6da142d6 100644 --- a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/RestoredDeviceHandler.kt +++ b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/RestoredDeviceHandler.kt @@ -3,7 +3,7 @@ package com.signify.hue.flutterreactiveble.channelhandlers import io.flutter.plugin.common.EventChannel import com.signify.hue.flutterreactiveble.ProtobufModel as pb -class RestoredDeviceHandler() : EventChannel.StreamHandler { +class RestoredDeviceHandler : EventChannel.StreamHandler { private var sink: EventChannel.EventSink? = null override fun onListen( @@ -16,10 +16,9 @@ class RestoredDeviceHandler() : EventChannel.StreamHandler { val message = pb.RestoredDeviceInfoCollection.newBuilder().build() eventSink.success(message.toByteArray()) } - } override fun onCancel(objectSink: Any?) { sink = null } -} \ No newline at end of file +} diff --git a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/converters/ProtobufMessageConverter.kt b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/converters/ProtobufMessageConverter.kt index 7c8077f0..51ea1a25 100644 --- a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/converters/ProtobufMessageConverter.kt +++ b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/converters/ProtobufMessageConverter.kt @@ -4,6 +4,7 @@ import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattService import com.google.protobuf.ByteString import com.polidea.rxandroidble2.RxBleDeviceServices +import com.signify.hue.flutterreactiveble.ble.BondUpdate import com.signify.hue.flutterreactiveble.ble.ConnectionUpdateSuccess import com.signify.hue.flutterreactiveble.ble.MtuNegotiateFailed import com.signify.hue.flutterreactiveble.ble.MtuNegotiateResult @@ -55,6 +56,13 @@ class ProtobufMessageConverter { ) .build() + fun convertToBondInfo(update: BondUpdate): pb.BondInfo = + pb.BondInfo + .newBuilder() + .setId(update.deviceId) + .setBondState(update.bondState) + .build() + fun convertToDeviceInfo(connection: ConnectionUpdateSuccess): pb.DeviceInfo = pb.DeviceInfo.newBuilder() .setId(connection.deviceId) diff --git a/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/model/BondState.kt b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/model/BondState.kt new file mode 100644 index 00000000..64f53377 --- /dev/null +++ b/packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/model/BondState.kt @@ -0,0 +1,24 @@ +package com.signify.hue.flutterreactiveble.model + +import android.bluetooth.BluetoothDevice +import com.polidea.rxandroidble2.RxBleDevice + +enum class BondState( + val code: Int, +) { + NONE(0), + BONDING(1), + BONDED(2), + ; + + companion object { + fun fromRaw(raw: Int): BondState = + when (raw) { + BluetoothDevice.BOND_BONDING -> BONDING + BluetoothDevice.BOND_BONDED -> BONDED + else -> NONE + } + } +} + +fun RxBleDevice.getBondState(): BondState = BondState.fromRaw(bluetoothDevice.bondState) diff --git a/packages/reactive_ble_mobile/example/android/gradle/wrapper/gradle-wrapper.jar b/packages/reactive_ble_mobile/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13372aef Binary files /dev/null and b/packages/reactive_ble_mobile/example/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/reactive_ble_mobile/example/android/gradlew b/packages/reactive_ble_mobile/example/android/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/packages/reactive_ble_mobile/example/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/packages/reactive_ble_mobile/example/android/gradlew.bat b/packages/reactive_ble_mobile/example/android/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/packages/reactive_ble_mobile/example/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/reactive_ble_mobile/ios/Classes/BleData/bledata.pb.swift b/packages/reactive_ble_mobile/ios/Classes/BleData/bledata.pb.swift index 598e2aa7..852fc85b 100644 --- a/packages/reactive_ble_mobile/ios/Classes/BleData/bledata.pb.swift +++ b/packages/reactive_ble_mobile/ios/Classes/BleData/bledata.pb.swift @@ -117,6 +117,20 @@ struct ConnectToDeviceRequest: Sendable { fileprivate var _servicesWithCharacteristicsToDiscover: ServicesWithCharacteristics? = nil } +struct BondInfo: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var id: String = String() + + var bondState: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + struct DeviceInfo: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -924,6 +938,44 @@ extension ConnectToDeviceRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageI } } +extension BondInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "BondInfo" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "bondState"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.id) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.bondState) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.id.isEmpty { + try visitor.visitSingularStringField(value: self.id, fieldNumber: 1) + } + if self.bondState != 0 { + try visitor.visitSingularInt32Field(value: self.bondState, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: BondInfo, rhs: BondInfo) -> Bool { + if lhs.id != rhs.id {return false} + if lhs.bondState != rhs.bondState {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension DeviceInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "DeviceInfo" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/packages/reactive_ble_mobile/ios/Classes/Plugin/SwiftReactiveBlePlugin.swift b/packages/reactive_ble_mobile/ios/Classes/Plugin/SwiftReactiveBlePlugin.swift index 7d44213a..057ae864 100644 --- a/packages/reactive_ble_mobile/ios/Classes/Plugin/SwiftReactiveBlePlugin.swift +++ b/packages/reactive_ble_mobile/ios/Classes/Plugin/SwiftReactiveBlePlugin.swift @@ -18,6 +18,21 @@ public class SwiftReactiveBlePlugin: NSObject, FlutterPlugin { .setStreamHandler(plugin.characteristicValueUpdateStreamHandler) FlutterEventChannel(name: "flutter_reactive_ble_restored_device", binaryMessenger: registrar.messenger()) .setStreamHandler(plugin.restoredDeviceStreamHandler) + FlutterEventChannel(name: "flutter_reactive_ble_bond_update", binaryMessenger: registrar.messenger()) + .setStreamHandler(plugin.bondUpdateStreamHandler) + } + + var bondUpdateStreamHandler: StreamHandler { + return StreamHandler( + name: "bond update stream handler", + context: context, + onListen: { context, sink in + return nil + }, + onCancel: { context in + return nil + } + ) } var statusStreamHandler: StreamHandler { diff --git a/packages/reactive_ble_mobile/lib/src/converter/protobuf_converter.dart b/packages/reactive_ble_mobile/lib/src/converter/protobuf_converter.dart index 8129ce09..6f3fa828 100644 --- a/packages/reactive_ble_mobile/lib/src/converter/protobuf_converter.dart +++ b/packages/reactive_ble_mobile/lib/src/converter/protobuf_converter.dart @@ -9,6 +9,8 @@ abstract class ProtobufConverter { ScanResult scanResultFrom(List data); + BondStateUpdate bondUpdateFrom(List data); + ConnectionStateUpdate connectionStateUpdateFrom(List data); List restoredDevicesFrom(List data); @@ -77,6 +79,19 @@ class ProtobufConverterImpl implements ProtobufConverter { ); } + @override + BondStateUpdate bondUpdateFrom(List data) { + final message = pb.BondInfo.fromBuffer(data); + return BondStateUpdate( + deviceId: message.id, + bondState: selectFrom( + DeviceBondState.values, + index: message.bondState, + fallback: (_) => DeviceBondState.unknown, + ), + ); + } + @override ConnectionStateUpdate connectionStateUpdateFrom(List data) { final deviceInfo = pb.DeviceInfo.fromBuffer(data); diff --git a/packages/reactive_ble_mobile/lib/src/generated/bledata.pb.dart b/packages/reactive_ble_mobile/lib/src/generated/bledata.pb.dart index 867c67cd..9891a605 100644 --- a/packages/reactive_ble_mobile/lib/src/generated/bledata.pb.dart +++ b/packages/reactive_ble_mobile/lib/src/generated/bledata.pb.dart @@ -355,6 +355,70 @@ class ConnectToDeviceRequest extends $pb.GeneratedMessage { void clearTimeoutInMs() => clearField(3); } +class BondInfo extends $pb.GeneratedMessage { + factory BondInfo({ + $core.String? id, + $core.int? bondState, + }) { + final $result = create(); + if (id != null) { + $result.id = id; + } + if (bondState != null) { + $result.bondState = bondState; + } + return $result; + } + BondInfo._() : super(); + factory BondInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BondInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BondInfo', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'id') + ..a<$core.int>(2, _omitFieldNames ? '' : 'bondState', $pb.PbFieldType.O3, protoName: 'bondState') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BondInfo clone() => BondInfo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BondInfo copyWith(void Function(BondInfo) updates) => super.copyWith((message) => updates(message as BondInfo)) as BondInfo; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BondInfo create() => BondInfo._(); + BondInfo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BondInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BondInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get id => $_getSZ(0); + @$pb.TagNumber(1) + set id($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get bondState => $_getIZ(1); + @$pb.TagNumber(2) + set bondState($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasBondState() => $_has(1); + @$pb.TagNumber(2) + void clearBondState() => clearField(2); +} + class DeviceInfo extends $pb.GeneratedMessage { factory DeviceInfo({ $core.String? id, diff --git a/packages/reactive_ble_mobile/lib/src/generated/bledata.pbjson.dart b/packages/reactive_ble_mobile/lib/src/generated/bledata.pbjson.dart index d09b466b..e9b83aaf 100644 --- a/packages/reactive_ble_mobile/lib/src/generated/bledata.pbjson.dart +++ b/packages/reactive_ble_mobile/lib/src/generated/bledata.pbjson.dart @@ -84,6 +84,20 @@ final $typed_data.Uint8List connectToDeviceRequestDescriptor = $convert.base64De 'aXRoQ2hhcmFjdGVyaXN0aWNzUiVzZXJ2aWNlc1dpdGhDaGFyYWN0ZXJpc3RpY3NUb0Rpc2Nvdm' 'VyEiAKC3RpbWVvdXRJbk1zGAMgASgFUgt0aW1lb3V0SW5Ncw=='); +@$core.Deprecated('Use bondInfoDescriptor instead') +const BondInfo$json = { + '1': 'BondInfo', + '2': [ + {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'}, + {'1': 'bondState', '3': 2, '4': 1, '5': 5, '10': 'bondState'}, + ], +}; + +/// Descriptor for `BondInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List bondInfoDescriptor = $convert.base64Decode( + 'CghCb25kSW5mbxIOCgJpZBgBIAEoCVICaWQSHAoJYm9uZFN0YXRlGAIgASgFUglib25kU3RhdG' + 'U='); + @$core.Deprecated('Use deviceInfoDescriptor instead') const DeviceInfo$json = { '1': 'DeviceInfo', diff --git a/packages/reactive_ble_mobile/lib/src/reactive_ble_mobile_platform.dart b/packages/reactive_ble_mobile/lib/src/reactive_ble_mobile_platform.dart index cb5e8419..02246116 100644 --- a/packages/reactive_ble_mobile/lib/src/reactive_ble_mobile_platform.dart +++ b/packages/reactive_ble_mobile/lib/src/reactive_ble_mobile_platform.dart @@ -9,6 +9,7 @@ class ReactiveBleMobilePlatform extends ReactiveBlePlatform { required ArgsToProtobufConverter argsToProtobufConverter, required ProtobufConverter protobufConverter, required MethodChannel bleMethodChannel, + required Stream> bondUpdateChannel, required Stream> connectedDeviceChannel, required Stream> restoredDeviceChannel, required Stream> charUpdateChannel, @@ -19,6 +20,7 @@ class ReactiveBleMobilePlatform extends ReactiveBlePlatform { }) : _argsToProtobufConverter = argsToProtobufConverter, _protobufConverter = protobufConverter, _bleMethodChannel = bleMethodChannel, + _bondUpdateRawStream = bondUpdateChannel, _connectedDeviceRawStream = connectedDeviceChannel, _restoredDeviceRawStream = restoredDeviceChannel, _charUpdateRawStream = charUpdateChannel, @@ -30,6 +32,7 @@ class ReactiveBleMobilePlatform extends ReactiveBlePlatform { final ArgsToProtobufConverter _argsToProtobufConverter; final ProtobufConverter _protobufConverter; final MethodChannel _bleMethodChannel; + final Stream> _bondUpdateRawStream; final Stream> _connectedDeviceRawStream; final Stream> _restoredDeviceRawStream; final Stream> _charUpdateRawStream; @@ -43,6 +46,17 @@ class ReactiveBleMobilePlatform extends ReactiveBlePlatform { Stream? _scanResultStream; Stream? _bleStatusStream; + @override + Stream get bondUpdateStream => + _bondUpdateRawStream.map(_protobufConverter.bondUpdateFrom).map( + (update) { + _logger?.log( + 'Received $BondStateUpdate(deviceId: ${update.deviceId}, connectionState: ${update.bondState})', + ); + return update; + }, + ); + @override Stream get connectionUpdateStream => _connectionUpdateStream ??= _connectedDeviceRawStream @@ -344,6 +358,7 @@ class ReactiveBleMobilePlatformFactory { EventChannel("flutter_reactive_ble_connected_device"); const restoredDeviceChannel = EventChannel("flutter_reactive_ble_restored_device"); + const bondEventChannel = EventChannel("flutter_reactive_ble_bond_update"); const charEventChannel = EventChannel("flutter_reactive_ble_char_update"); const scanEventChannel = EventChannel("flutter_reactive_ble_scan"); const bleStatusChannel = EventChannel("flutter_reactive_ble_status"); @@ -352,6 +367,8 @@ class ReactiveBleMobilePlatformFactory { protobufConverter: const ProtobufConverterImpl(), argsToProtobufConverter: const ArgsToProtobufConverterImpl(), bleMethodChannel: _bleMethodChannel, + bondUpdateChannel: + bondEventChannel.receiveBroadcastStream().cast>(), connectedDeviceChannel: connectedDeviceChannel.receiveBroadcastStream().cast>(), restoredDeviceChannel: diff --git a/packages/reactive_ble_mobile/protos/bledata.proto b/packages/reactive_ble_mobile/protos/bledata.proto index 60616097..0f090d38 100644 --- a/packages/reactive_ble_mobile/protos/bledata.proto +++ b/packages/reactive_ble_mobile/protos/bledata.proto @@ -29,6 +29,11 @@ message ConnectToDeviceRequest { int32 timeoutInMs = 3; } +message BondInfo { + string id = 1; + int32 bondState = 2; +} + message DeviceInfo { string id = 1; int32 connectionState = 2; diff --git a/packages/reactive_ble_mobile/test/reactive_ble_platform_test.dart b/packages/reactive_ble_mobile/test/reactive_ble_platform_test.dart index 198691da..42b4b1b7 100644 --- a/packages/reactive_ble_mobile/test/reactive_ble_platform_test.dart +++ b/packages/reactive_ble_mobile/test/reactive_ble_platform_test.dart @@ -28,6 +28,7 @@ void main() { late MockMethodChannel _methodChannel; late ArgsToProtobufConverter _argsConverter; late ProtobufConverter _protobufConverter; + late StreamController> _bondUpdateStreamController; late StreamController> _connectedDeviceStreamController; late StreamController> _restoredDeviceStreamController; late StreamController> _argsStreamController; @@ -38,6 +39,7 @@ void main() { _argsConverter = MockArgsToProtobufConverter(); _methodChannel = MockMethodChannel(); _protobufConverter = MockProtobufConverter(); + _bondUpdateStreamController = StreamController(); _connectedDeviceStreamController = StreamController(); _restoredDeviceStreamController = StreamController(); _argsStreamController = StreamController(); @@ -52,6 +54,7 @@ void main() { argsToProtobufConverter: _argsConverter, bleMethodChannel: _methodChannel, protobufConverter: _protobufConverter, + bondUpdateChannel: _bondUpdateStreamController.stream, connectedDeviceChannel: _connectedDeviceStreamController.stream, restoredDeviceChannel: _restoredDeviceStreamController.stream, charUpdateChannel: _argsStreamController.stream, @@ -62,6 +65,7 @@ void main() { }); tearDown(() { + _bondUpdateStreamController.close(); _connectedDeviceStreamController.close(); _restoredDeviceStreamController.close(); _argsStreamController.close(); @@ -621,6 +625,41 @@ void main() { }); }); + group('bond status', () { + const status1 = BondStateUpdate( + deviceId: '123', + bondState: DeviceBondState.unknown, + ); + + const status2 = BondStateUpdate( + deviceId: '123', + bondState: DeviceBondState.bonding, + ); + + Stream? _bondUpdateStream; + + setUp(() { + _bondUpdateStreamController.addStream( + Stream>.fromIterable([ + [1], + [0] + ]), + ); + + when(_protobufConverter.bondUpdateFrom([1])).thenReturn(status1); + when(_protobufConverter.bondUpdateFrom([0])).thenReturn(status2); + + _bondUpdateStream = _sut.bondUpdateStream; + }); + + test('It emits correct values', () { + expect( + _bondUpdateStream, + emitsInOrder([status1, status2]), + ); + }); + }); + group('Discover services', () { const deviceId = "testdevice"; late pb.DiscoverServicesRequest request; diff --git a/packages/reactive_ble_mobile/test/reactive_ble_platform_test.mocks.dart b/packages/reactive_ble_mobile/test/reactive_ble_platform_test.mocks.dart index f750cbd7..7fd4dd53 100644 --- a/packages/reactive_ble_mobile/test/reactive_ble_platform_test.mocks.dart +++ b/packages/reactive_ble_mobile/test/reactive_ble_platform_test.mocks.dart @@ -184,9 +184,20 @@ class _FakeScanResult_13 extends _i1.SmartFake implements _i3.ScanResult { ); } -class _FakeConnectionStateUpdate_14 extends _i1.SmartFake +class _FakeBondStateUpdate_14 extends _i1.SmartFake + implements _i3.BondStateUpdate { + _FakeBondStateUpdate_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeConnectionStateUpdate_15 extends _i1.SmartFake implements _i3.ConnectionStateUpdate { - _FakeConnectionStateUpdate_14( + _FakeConnectionStateUpdate_15( Object parent, Invocation parentInvocation, ) : super( @@ -195,9 +206,9 @@ class _FakeConnectionStateUpdate_14 extends _i1.SmartFake ); } -class _FakeResult_15 extends _i1.SmartFake +class _FakeResult_16 extends _i1.SmartFake implements _i3.Result { - _FakeResult_15( + _FakeResult_16( Object parent, Invocation parentInvocation, ) : super( @@ -206,9 +217,9 @@ class _FakeResult_15 extends _i1.SmartFake ); } -class _FakeCharacteristicValue_16 extends _i1.SmartFake +class _FakeCharacteristicValue_17 extends _i1.SmartFake implements _i3.CharacteristicValue { - _FakeCharacteristicValue_16( + _FakeCharacteristicValue_17( Object parent, Invocation parentInvocation, ) : super( @@ -217,9 +228,9 @@ class _FakeCharacteristicValue_16 extends _i1.SmartFake ); } -class _FakeWriteCharacteristicInfo_17 extends _i1.SmartFake +class _FakeWriteCharacteristicInfo_18 extends _i1.SmartFake implements _i3.WriteCharacteristicInfo { - _FakeWriteCharacteristicInfo_17( + _FakeWriteCharacteristicInfo_18( Object parent, Invocation parentInvocation, ) : super( @@ -228,9 +239,9 @@ class _FakeWriteCharacteristicInfo_17 extends _i1.SmartFake ); } -class _FakeConnectionPriorityInfo_18 extends _i1.SmartFake +class _FakeConnectionPriorityInfo_19 extends _i1.SmartFake implements _i3.ConnectionPriorityInfo { - _FakeConnectionPriorityInfo_18( + _FakeConnectionPriorityInfo_19( Object parent, Invocation parentInvocation, ) : super( @@ -239,8 +250,8 @@ class _FakeConnectionPriorityInfo_18 extends _i1.SmartFake ); } -class _FakeMethodCodec_19 extends _i1.SmartFake implements _i4.MethodCodec { - _FakeMethodCodec_19( +class _FakeMethodCodec_20 extends _i1.SmartFake implements _i4.MethodCodec { + _FakeMethodCodec_20( Object parent, Invocation parentInvocation, ) : super( @@ -249,9 +260,9 @@ class _FakeMethodCodec_19 extends _i1.SmartFake implements _i4.MethodCodec { ); } -class _FakeBinaryMessenger_20 extends _i1.SmartFake +class _FakeBinaryMessenger_21 extends _i1.SmartFake implements _i5.BinaryMessenger { - _FakeBinaryMessenger_20( + _FakeBinaryMessenger_21( Object parent, Invocation parentInvocation, ) : super( @@ -568,6 +579,21 @@ class MockProtobufConverter extends _i1.Mock implements _i7.ProtobufConverter { ), ) as _i3.ScanResult); + @override + _i3.BondStateUpdate bondUpdateFrom(List? data) => (super.noSuchMethod( + Invocation.method( + #bondUpdateFrom, + [data], + ), + returnValue: _FakeBondStateUpdate_14( + this, + Invocation.method( + #bondUpdateFrom, + [data], + ), + ), + ) as _i3.BondStateUpdate); + @override _i3.ConnectionStateUpdate connectionStateUpdateFrom(List? data) => (super.noSuchMethod( @@ -575,7 +601,7 @@ class MockProtobufConverter extends _i1.Mock implements _i7.ProtobufConverter { #connectionStateUpdateFrom, [data], ), - returnValue: _FakeConnectionStateUpdate_14( + returnValue: _FakeConnectionStateUpdate_15( this, Invocation.method( #connectionStateUpdateFrom, @@ -601,7 +627,7 @@ class MockProtobufConverter extends _i1.Mock implements _i7.ProtobufConverter { #clearGattCacheResultFrom, [data], ), - returnValue: _FakeResult_15<_i3.Unit, + returnValue: _FakeResult_16<_i3.Unit, _i3.GenericFailure<_i3.ClearGattCacheError>?>( this, Invocation.method( @@ -619,7 +645,7 @@ class MockProtobufConverter extends _i1.Mock implements _i7.ProtobufConverter { #characteristicValueFrom, [data], ), - returnValue: _FakeCharacteristicValue_16( + returnValue: _FakeCharacteristicValue_17( this, Invocation.method( #characteristicValueFrom, @@ -635,7 +661,7 @@ class MockProtobufConverter extends _i1.Mock implements _i7.ProtobufConverter { #writeCharacteristicInfoFrom, [data], ), - returnValue: _FakeWriteCharacteristicInfo_17( + returnValue: _FakeWriteCharacteristicInfo_18( this, Invocation.method( #writeCharacteristicInfoFrom, @@ -651,7 +677,7 @@ class MockProtobufConverter extends _i1.Mock implements _i7.ProtobufConverter { #connectionPriorityInfoFrom, [data], ), - returnValue: _FakeConnectionPriorityInfo_18( + returnValue: _FakeConnectionPriorityInfo_19( this, Invocation.method( #connectionPriorityInfoFrom, @@ -709,7 +735,7 @@ class MockMethodChannel extends _i1.Mock implements _i8.MethodChannel { @override _i4.MethodCodec get codec => (super.noSuchMethod( Invocation.getter(#codec), - returnValue: _FakeMethodCodec_19( + returnValue: _FakeMethodCodec_20( this, Invocation.getter(#codec), ), @@ -718,7 +744,7 @@ class MockMethodChannel extends _i1.Mock implements _i8.MethodChannel { @override _i5.BinaryMessenger get binaryMessenger => (super.noSuchMethod( Invocation.getter(#binaryMessenger), - returnValue: _FakeBinaryMessenger_20( + returnValue: _FakeBinaryMessenger_21( this, Invocation.getter(#binaryMessenger), ), diff --git a/packages/reactive_ble_platform_interface/lib/src/model/bond_state_update.dart b/packages/reactive_ble_platform_interface/lib/src/model/bond_state_update.dart new file mode 100644 index 00000000..cf434422 --- /dev/null +++ b/packages/reactive_ble_platform_interface/lib/src/model/bond_state_update.dart @@ -0,0 +1,33 @@ +import 'package:functional_data/functional_data.dart'; +import 'package:meta/meta.dart'; + +part 'bond_state_update.g.dart'; +//ignore_for_file: annotate_overrides + +///Status update for a specific BLE device. +@immutable +@FunctionalData() +class BondStateUpdate extends $BondStateUpdate { + final String deviceId; + final DeviceBondState bondState; + + const BondStateUpdate({ + required this.deviceId, + required this.bondState, + }); +} + +/// Connection status. +enum DeviceBondState { + /// Device is not bonded. + none, + + /// Device bonding is in progress. + bonding, + + /// Device is bonded. + bonded, + + /// Bond state is not (yet) determined. + unknown, +} diff --git a/packages/reactive_ble_platform_interface/lib/src/model/bond_state_update.g.dart b/packages/reactive_ble_platform_interface/lib/src/model/bond_state_update.g.dart new file mode 100644 index 00000000..bd1a32d8 --- /dev/null +++ b/packages/reactive_ble_platform_interface/lib/src/model/bond_state_update.g.dart @@ -0,0 +1,82 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bond_state_update.dart'; + +// ************************************************************************** +// FunctionalDataGenerator +// ************************************************************************** + +abstract class $BondStateUpdate { + const $BondStateUpdate(); + + String get deviceId; + DeviceBondState get bondState; + + BondStateUpdate copyWith({ + String? deviceId, + DeviceBondState? bondState, + }) => + BondStateUpdate( + deviceId: deviceId ?? this.deviceId, + bondState: bondState ?? this.bondState, + ); + + BondStateUpdate copyUsing( + void Function(BondStateUpdate$Change change) mutator) { + final change = BondStateUpdate$Change._( + this.deviceId, + this.bondState, + ); + mutator(change); + return BondStateUpdate( + deviceId: change.deviceId, + bondState: change.bondState, + ); + } + + @override + String toString() => + "BondStateUpdate(deviceId: $deviceId, bondState: $bondState)"; + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) => + other is BondStateUpdate && + other.runtimeType == runtimeType && + deviceId == other.deviceId && + bondState == other.bondState; + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode { + var result = 17; + result = 37 * result + deviceId.hashCode; + result = 37 * result + bondState.hashCode; + return result; + } +} + +class BondStateUpdate$Change { + BondStateUpdate$Change._( + this.deviceId, + this.bondState, + ); + + String deviceId; + DeviceBondState bondState; +} + +// ignore: avoid_classes_with_only_static_members +class BondStateUpdate$ { + static final deviceId = Lens( + (deviceIdContainer) => deviceIdContainer.deviceId, + (deviceIdContainer, deviceId) => + deviceIdContainer.copyWith(deviceId: deviceId), + ); + + static final bondState = Lens( + (bondStateContainer) => bondStateContainer.bondState, + (bondStateContainer, bondState) => + bondStateContainer.copyWith(bondState: bondState), + ); +} diff --git a/packages/reactive_ble_platform_interface/lib/src/models.dart b/packages/reactive_ble_platform_interface/lib/src/models.dart index a015b3e2..eb7de300 100644 --- a/packages/reactive_ble_platform_interface/lib/src/models.dart +++ b/packages/reactive_ble_platform_interface/lib/src/models.dart @@ -1,4 +1,5 @@ export './model/ble_status.dart'; +export './model/bond_state_update.dart'; export './model/characteristic_instance.dart'; export './model/characteristic_value.dart'; export './model/clear_gatt_cache_error.dart'; diff --git a/packages/reactive_ble_platform_interface/lib/src/reactive_ble_platform_interface.dart b/packages/reactive_ble_platform_interface/lib/src/reactive_ble_platform_interface.dart index 4028cc1a..e4a3c3a4 100644 --- a/packages/reactive_ble_platform_interface/lib/src/reactive_ble_platform_interface.dart +++ b/packages/reactive_ble_platform_interface/lib/src/reactive_ble_platform_interface.dart @@ -24,6 +24,14 @@ abstract class ReactiveBlePlatform extends PlatformInterface { _instance = instance; } + /// Stream that provides status updates regarding device bonding. + /// + /// It is important to subscribe to this stream before connecting to a device + /// since it can happen that some results are missed. + Stream get bondUpdateStream { + throw UnimplementedError('scanStream has not been implemented.'); + } + /// Stream providing ble scan results. /// /// It is important to subscribe to this stream before scanning for devices