diff --git a/.github/labeler.yml b/.github/labeler.yml index 3e0a4af1d..e0414405b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -13,6 +13,8 @@ - packages/firebase_core/**/* "p: flutter_app_badger": - packages/flutter_app_badger/**/* +"p: flutter_reactive_ble": + - packages/flutter_reactive_ble/**/* "p: flutter_secure_storage": - packages/flutter_secure_storage/**/* "p: flutter_tts": diff --git a/.github/recipe.yaml b/.github/recipe.yaml index 0ea4c5d66..f7ec7e8aa 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -23,6 +23,7 @@ plugins: wakelock: ["wearable-5.5"] # No tests. + flutter_reactive_ble: [] google_sign_in: [] image_picker: [] firebase_core: [] diff --git a/README.md b/README.md index c99cf8f18..4f269dd39 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**device_info_plus_tizen**](packages/device_info_plus) | [device_info_plus](https://pub.dev/packages/device_info_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/device_info_plus_tizen.svg)](https://pub.dev/packages/device_info_plus_tizen) | No | | [**firebase_core_tizen**](packages/firebase_core) | [firebase_core](https://pub.dev/packages/firebase_core) | [![pub package](https://img.shields.io/pub/v/firebase_core_tizen.svg)](https://pub.dev/packages/firebase_core_tizen) | No | | [**flutter_app_badger_tizen**](packages/flutter_app_badger) | [flutter_app_badger](https://pub.dev/packages/flutter_app_badger) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_app_badger_tizen.svg)](https://pub.dev/packages/flutter_app_badger_tizen) | No | +| [**flutter_reactive_ble_tizen**](packages/flutter_reactive_ble) | [flutter_reactive_ble](https://pub.dev/packages/flutter_reactive_ble) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_reactive_ble_tizen.svg)](https://pub.dev/packages/flutter_reactive_ble_tizen) | No | | [**flutter_secure_storage_tizen**](packages/flutter_secure_storage) | [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_secure_storage_tizen.svg)](https://pub.dev/packages/flutter_secure_storage_tizen) | No | | [**flutter_tts_tizen**](packages/flutter_tts) | [flutter_tts](https://pub.dev/packages/flutter_tts) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_tts_tizen.svg)](https://pub.dev/packages/flutter_tts_tizen) | No | | [**flutter_webrtc_tizen**](packages/flutter_webrtc) | [flutter_webrtc](https://pub.dev/packages/flutter_webrtc) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_webrtc_tizen.svg)](https://pub.dev/packages/flutter_webrtc_tizen) | No | @@ -63,6 +64,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**device_info_plus_tizen**](packages/device_info_plus) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | | [**firebase_core**](packages/firebase_core) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | | [**flutter_app_badger_tizen**](packages/flutter_app_badger) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | API not supported | +| [**flutter_reactive_ble_tizen**](packages/flutter_reactive_ble) | 4.0 | ✔️ | ❌ | ✔️ | ❌ | API not supported on emulator | | [**flutter_secure_storage_tizen**](packages/flutter_secure_storage) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | | [**flutter_tts_tizen**](packages/flutter_tts) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | | [**flutter_webrtc_tizen**](packages/flutter_webrtc) | 6.0 | ❌ | ❌ | ✔️ | ❌ | No camera | diff --git a/packages/flutter_reactive_ble/.gitignore b/packages/flutter_reactive_ble/.gitignore new file mode 100644 index 000000000..d87f6f060 --- /dev/null +++ b/packages/flutter_reactive_ble/.gitignore @@ -0,0 +1,28 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code related +.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/flutter_reactive_ble/CHANGELOG.md b/packages/flutter_reactive_ble/CHANGELOG.md new file mode 100644 index 000000000..607323422 --- /dev/null +++ b/packages/flutter_reactive_ble/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release. diff --git a/packages/flutter_reactive_ble/LICENSE b/packages/flutter_reactive_ble/LICENSE new file mode 100644 index 000000000..2ce17b2ff --- /dev/null +++ b/packages/flutter_reactive_ble/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2023 Samsung Electronics Co., Ltd. All rights reserved. +Copyright (c) 2019 Signify Holding. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the names of the copyright holders nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/flutter_reactive_ble/README.md b/packages/flutter_reactive_ble/README.md new file mode 100644 index 000000000..e9cdcaec0 --- /dev/null +++ b/packages/flutter_reactive_ble/README.md @@ -0,0 +1,41 @@ +# flutter_reactive_ble_tizen + +[![pub package](https://img.shields.io/pub/v/flutter_reactive_ble_tizen.svg)](https://pub.dev/packages/flutter_reactive_ble_tizen) + +The Tizen implementation of [`flutter_reactive_ble`](https://pub.dev/packages/flutter_reactive_ble). + +## Usage + +This package is not an _endorsed_ implementation of `flutter_reactive_ble`. Therefore, you have to include `flutter_reactive_ble_tizen` alongside `flutter_reactive_ble` as dependencies in your `pubspec.yaml` file. + +```yaml +dependencies: + flutter_reactive_ble: ^5.0.3 + flutter_reactive_ble_tizen: ^0.1.0 +``` + +Then you can import `flutter_reactive_ble` in your Dart code: + +```dart +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +``` + +For detailed usage, see https://pub.dev/packages/flutter_reactive_ble#usage. + +## Required privileges + +The bluetooth privilege must be added to your `tizen-manifest.xml` file to use this plugin. + +```xml + + http://tizen.org/privilege/bluetooth + +``` + +## Known issues + +- The following parameters are not supported and will be ignored on Tizen. + - `connectionTimeout` of `FlutterReactiveBle.connectToDevice()` and `FlutterReactiveBle.connectToAdvertisingDevice()` + - `scanMode` of `FlutterReactiveBle.scanForDevices()` +- The plugin sometimes doesn't respond to characteristic read or notify/indicate requests, requiring the user to press the same button twice in the example app. This looks like a bug in the frontend package. +- The plugin often fails to retrieve the names of discovered devices on Tizen more frequently than on other platforms. diff --git a/packages/flutter_reactive_ble/analysis_options.yaml b/packages/flutter_reactive_ble/analysis_options.yaml new file mode 100644 index 000000000..e8a8c4589 --- /dev/null +++ b/packages/flutter_reactive_ble/analysis_options.yaml @@ -0,0 +1,161 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false + exclude: + - "bin/cache/**" + - "**/*.freezed.dart" + - "**/*.g.dart" + - "**/*.mocks.dart" + - "**/generated/**" + +linter: + rules: + # Errors + - avoid_empty_else + - avoid_relative_lib_imports + - avoid_returning_null_for_future + - avoid_slow_async_io + - avoid_types_as_parameter_names + - cancel_subscriptions + - close_sinks + # - comment_references -- DISABLED: collision with mockito generation + - control_flow_in_finally + # - diagnostic_describe_all_properties -- DISABLED: experimental feature + - empty_statements + - hash_and_equals + - iterable_contains_unrelated_type + - list_remove_unrelated_type + - literal_only_boolean_expressions + - no_adjacent_strings_in_list + - no_duplicate_case_values + - prefer_void_to_null + - test_types_in_equals + - throw_in_finally + - unnecessary_statements + - unrelated_type_equality_checks + - valid_regexps + # Style checks + - always_declare_return_types + # - always_put_control_body_on_new_line -- DISABLED: DARTFMT INCOMPATIBLE + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + # - always_specify_types -- DISABLED: LEADS TO FLUFFY CODE + - annotate_overrides + # - avoid_annotating_with_dynamic -- DISABLED: Gives false positives for function arguments where it is needed + # - avoid_as + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_field_initializers_in_const_classes + # - avoid_function_literals_in_foreach_calls -- DISABLED: forEach is often clearer than for-loop + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_private_typedef_functions + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_void + - avoid_returning_this + # - avoid_setters_without_getters -- DISABLED: Sometimes it makes sense to have write-only member + # - avoid_shadowing_type_parameters -- TODO: Enable new option + - avoid_single_cascade_in_expression_statements + # - avoid_types_on_closure_parameters -- DISABLED: INCOMPATIBLE WITH IMPLICIT TYPE CASTS DISABLED + - avoid_unused_constructor_parameters + - avoid_void_async + - await_only_futures + - camel_case_types + - cascade_invocations + - constant_identifier_names + # - curly_braces_in_flow_control_structures -- DISABLED: WE DO NOT WANT THIS + - directives_ordering + - empty_catches + - empty_constructor_bodies + - file_names + # - flutter_style_todos -- DISABLED: TOO MUCH? + - implementation_imports + - join_return_with_assignment + - library_names + - library_prefixes + # - lines_longer_than_80_chars -- DISABLED: WE DO NOT WANT THIS + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + # - one_member_abstracts -- DISABLED: OBSTRUCTS OO DESIGN + - only_throw_errors + - overridden_fields + - package_api_docs + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + # - prefer_asserts_with_message -- DISABLED: BOOLEAN CONDITIONS ARE GOOD ENOUGH + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + # - prefer_double_quotes -- DISABLED: This rule is to be supported in a future Dart release + - prefer_equal_for_default_values + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_initializing_formals + - prefer_inlined_adds + # - prefer_int_literals -- DISABLED: ADDED VALUE IS UNCLEAR + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_operators + # - prefer_single_quotes -- DISABLED: ADDED VALUE IS UNCLEAR + - prefer_spread_collections + - prefer_typing_uninitialized_variables + # - provide_deprecation_messages -- DISABLED: This rule is to be supported in a future Dart release + # - public_member_api_docs + - recursive_getters + - slash_for_doc_comments + # - sort_child_properties_last -- This rule is to be supported in a future Dart release + # - sort_constructors_first -- DISABLED: ADDED VALUE IS UNCLEAR + # - sort_unnamed_constructors_first -- DISABLED: ADDED VALUE IS UNCLEAR + # - type_annotate_public_apis -- DISABLED: ADDED VALUE IS UNCLEAR + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_this + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_to_and_as_if_applicable + - void_checks + # Pub Rules + - package_names + - sort_pub_dependencies diff --git a/packages/flutter_reactive_ble/example/.gitignore b/packages/flutter_reactive_ble/example/.gitignore new file mode 100644 index 000000000..ad3c2ca04 --- /dev/null +++ b/packages/flutter_reactive_ble/example/.gitignore @@ -0,0 +1,42 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/flutter_reactive_ble/example/README.md b/packages/flutter_reactive_ble/example/README.md new file mode 100644 index 000000000..bb0a376dd --- /dev/null +++ b/packages/flutter_reactive_ble/example/README.md @@ -0,0 +1,7 @@ +# flutter_reactive_ble_tizen_example + +Demonstrates how to use the flutter_reactive_ble_tizen plugin. + +## Getting Started + +To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen). diff --git a/packages/flutter_reactive_ble/example/lib/main.dart b/packages/flutter_reactive_ble/example/lib/main.dart new file mode 100644 index 000000000..1e55b6969 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/main.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.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'; +import 'package:flutter_reactive_ble_example/src/ble/ble_status_monitor.dart'; +import 'package:flutter_reactive_ble_example/src/ui/ble_status_screen.dart'; +import 'package:flutter_reactive_ble_example/src/ui/device_list.dart'; +import 'package:provider/provider.dart'; + +import 'src/ble/ble_logger.dart'; + +const _themeColor = Colors.lightGreen; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + final _ble = FlutterReactiveBle(); + final _bleLogger = BleLogger(ble: _ble); + final _scanner = BleScanner(ble: _ble, logMessage: _bleLogger.addToLog); + final _monitor = BleStatusMonitor(_ble); + final _connector = BleDeviceConnector( + ble: _ble, + logMessage: _bleLogger.addToLog, + ); + final _serviceDiscoverer = BleDeviceInteractor( + bleDiscoverServices: _ble.discoverServices, + readCharacteristic: _ble.readCharacteristic, + writeWithResponse: _ble.writeCharacteristicWithResponse, + writeWithOutResponse: _ble.writeCharacteristicWithoutResponse, + subscribeToCharacteristic: _ble.subscribeToCharacteristic, + logMessage: _bleLogger.addToLog, + ); + runApp( + MultiProvider( + providers: [ + Provider.value(value: _scanner), + Provider.value(value: _monitor), + Provider.value(value: _connector), + Provider.value(value: _serviceDiscoverer), + Provider.value(value: _bleLogger), + StreamProvider( + create: (_) => _scanner.state, + initialData: const BleScannerState( + discoveredDevices: [], + scanIsInProgress: false, + ), + ), + StreamProvider( + create: (_) => _monitor.state, + initialData: BleStatus.unknown, + ), + StreamProvider( + create: (_) => _connector.state, + initialData: const ConnectionStateUpdate( + deviceId: 'Unknown device', + connectionState: DeviceConnectionState.disconnected, + failure: null, + ), + ), + ], + child: MaterialApp( + title: 'Flutter Reactive BLE example', + color: _themeColor, + theme: ThemeData(primarySwatch: _themeColor), + home: const HomeScreen(), + ), + ), + ); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Consumer( + builder: (_, status, __) { + if (status == BleStatus.ready) { + return const DeviceListScreen(); + } else { + return BleStatusScreen(status: status ?? BleStatus.unknown); + } + }, + ); +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_connector.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_connector.dart new file mode 100644 index 000000000..9494149c8 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_connector.dart @@ -0,0 +1,58 @@ +import 'dart:async'; + +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart'; + +class BleDeviceConnector extends ReactiveState { + BleDeviceConnector({ + required FlutterReactiveBle ble, + required Function(String message) logMessage, + }) : _ble = ble, + _logMessage = logMessage; + + final FlutterReactiveBle _ble; + final void Function(String message) _logMessage; + + @override + Stream get state => _deviceConnectionController.stream; + + final _deviceConnectionController = StreamController(); + + // ignore: cancel_subscriptions + late StreamSubscription _connection; + + Future connect(String deviceId) async { + _logMessage('Start connecting to $deviceId'); + _connection = _ble.connectToDevice(id: deviceId).listen( + (update) { + _logMessage( + 'ConnectionState for device $deviceId : ${update.connectionState}'); + _deviceConnectionController.add(update); + }, + onError: (Object e) => + _logMessage('Connecting to device $deviceId resulted in error $e'), + ); + } + + Future disconnect(String deviceId) async { + try { + _logMessage('disconnecting to device: $deviceId'); + await _connection.cancel(); + } on Exception catch (e, _) { + _logMessage("Error disconnecting from a device: $e"); + } finally { + // Since [_connection] subscription is terminated, the "disconnected" state cannot be received and propagated + _deviceConnectionController.add( + ConnectionStateUpdate( + deviceId: deviceId, + connectionState: DeviceConnectionState.disconnected, + failure: null, + ), + ); + } + } + + Future dispose() async { + await _deviceConnectionController.close(); + } +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_interactor.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_interactor.dart new file mode 100644 index 000000000..3d09b5094 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_interactor.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; + +class BleDeviceInteractor { + BleDeviceInteractor({ + required Future> Function(String deviceId) + bleDiscoverServices, + required Future> Function(QualifiedCharacteristic characteristic) + readCharacteristic, + required Future Function(QualifiedCharacteristic characteristic, + {required List value}) + writeWithResponse, + required Future Function(QualifiedCharacteristic characteristic, + {required List value}) + writeWithOutResponse, + required void Function(String message) logMessage, + required Stream> Function(QualifiedCharacteristic characteristic) + subscribeToCharacteristic, + }) : _bleDiscoverServices = bleDiscoverServices, + _readCharacteristic = readCharacteristic, + _writeWithResponse = writeWithResponse, + _writeWithoutResponse = writeWithOutResponse, + _subScribeToCharacteristic = subscribeToCharacteristic, + _logMessage = logMessage; + + final Future> Function(String deviceId) + _bleDiscoverServices; + + final Future> Function(QualifiedCharacteristic characteristic) + _readCharacteristic; + + final Future Function(QualifiedCharacteristic characteristic, + {required List value}) _writeWithResponse; + + final Future Function(QualifiedCharacteristic characteristic, + {required List value}) _writeWithoutResponse; + + final Stream> Function(QualifiedCharacteristic characteristic) + _subScribeToCharacteristic; + + final void Function(String message) _logMessage; + + Future> discoverServices(String deviceId) async { + try { + _logMessage('Start discovering services for: $deviceId'); + final result = await _bleDiscoverServices(deviceId); + _logMessage('Discovering services finished'); + return result; + } on Exception catch (e) { + _logMessage('Error occured when discovering services: $e'); + rethrow; + } + } + + Future> readCharacteristic( + QualifiedCharacteristic characteristic) async { + try { + final result = await _readCharacteristic(characteristic); + + _logMessage('Read ${characteristic.characteristicId}: value = $result'); + return result; + } on Exception catch (e, s) { + _logMessage( + 'Error occured when reading ${characteristic.characteristicId} : $e', + ); + // ignore: avoid_print + print(s); + rethrow; + } + } + + Future writeCharacterisiticWithResponse( + QualifiedCharacteristic characteristic, List value) async { + try { + _logMessage( + 'Write with response value : $value to ${characteristic.characteristicId}'); + await _writeWithResponse(characteristic, value: value); + } on Exception catch (e, s) { + _logMessage( + 'Error occured when writing ${characteristic.characteristicId} : $e', + ); + // ignore: avoid_print + print(s); + rethrow; + } + } + + Future writeCharacterisiticWithoutResponse( + QualifiedCharacteristic characteristic, List value) async { + try { + await _writeWithoutResponse(characteristic, value: value); + _logMessage( + 'Write without response value: $value to ${characteristic.characteristicId}'); + } on Exception catch (e, s) { + _logMessage( + 'Error occured when writing ${characteristic.characteristicId} : $e', + ); + // ignore: avoid_print + print(s); + rethrow; + } + } + + Stream> subScribeToCharacteristic( + QualifiedCharacteristic characteristic) { + _logMessage('Subscribing to: ${characteristic.characteristicId} '); + return _subScribeToCharacteristic(characteristic); + } +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_logger.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_logger.dart new file mode 100644 index 000000000..cfc3b6977 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_logger.dart @@ -0,0 +1,26 @@ +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:intl/intl.dart'; + +class BleLogger { + BleLogger({ + required FlutterReactiveBle ble, + }) : _ble = ble; + + final FlutterReactiveBle _ble; + final List _logMessages = []; + final DateFormat formatter = DateFormat('HH:mm:ss.SSS'); + + List get messages => _logMessages; + + void addToLog(String message) { + final now = DateTime.now(); + _logMessages.add('${formatter.format(now)} - $message'); + } + + void clearLogs() => _logMessages.clear(); + + bool get verboseLogging => _ble.logLevel == LogLevel.verbose; + + void toggleVerboseLogging() => + _ble.logLevel = verboseLogging ? LogLevel.none : LogLevel.verbose; +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_scanner.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_scanner.dart new file mode 100644 index 000000000..5efff29b5 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_scanner.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart'; +import 'package:meta/meta.dart'; + +class BleScanner implements ReactiveState { + BleScanner({ + required FlutterReactiveBle ble, + required Function(String message) logMessage, + }) : _ble = ble, + _logMessage = logMessage; + + final FlutterReactiveBle _ble; + final void Function(String message) _logMessage; + final StreamController _stateStreamController = + StreamController(); + + final _devices = []; + + @override + Stream get state => _stateStreamController.stream; + + void startScan(List serviceIds) { + _logMessage('Start ble discovery'); + _devices.clear(); + _subscription?.cancel(); + _subscription = + _ble.scanForDevices(withServices: serviceIds).listen((device) { + final knownDeviceIndex = _devices.indexWhere((d) => d.id == device.id); + if (knownDeviceIndex >= 0) { + _devices[knownDeviceIndex] = device; + } else { + _devices.add(device); + } + _pushState(); + }, onError: (Object e) => _logMessage('Device scan fails with error: $e')); + _pushState(); + } + + void _pushState() { + _stateStreamController.add( + BleScannerState( + discoveredDevices: _devices, + scanIsInProgress: _subscription != null, + ), + ); + } + + Future stopScan() async { + _logMessage('Stop ble discovery'); + + await _subscription?.cancel(); + _subscription = null; + _pushState(); + } + + Future dispose() async { + await _stateStreamController.close(); + } + + StreamSubscription? _subscription; +} + +@immutable +class BleScannerState { + const BleScannerState({ + required this.discoveredDevices, + required this.scanIsInProgress, + }); + + final List discoveredDevices; + final bool scanIsInProgress; +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_status_monitor.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_status_monitor.dart new file mode 100644 index 000000000..cebbec76d --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_status_monitor.dart @@ -0,0 +1,11 @@ +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart'; + +class BleStatusMonitor implements ReactiveState { + const BleStatusMonitor(this._ble); + + final FlutterReactiveBle _ble; + + @override + Stream get state => _ble.statusStream; +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/reactive_state.dart b/packages/flutter_reactive_ble/example/lib/src/ble/reactive_state.dart new file mode 100644 index 000000000..f3441a399 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ble/reactive_state.dart @@ -0,0 +1,3 @@ +abstract class ReactiveState { + Stream get state; +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/ble_status_screen.dart b/packages/flutter_reactive_ble/example/lib/src/ui/ble_status_screen.dart new file mode 100644 index 000000000..81ccc7a54 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ui/ble_status_screen.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; + +class BleStatusScreen extends StatelessWidget { + const BleStatusScreen({required this.status, Key? key}) : super(key: key); + + final BleStatus status; + + String determineText(BleStatus status) { + switch (status) { + case BleStatus.unsupported: + return "This device does not support Bluetooth"; + case BleStatus.unauthorized: + return "Authorize the FlutterReactiveBle example app to use Bluetooth and location"; + case BleStatus.poweredOff: + return "Bluetooth is powered off on your device turn it on"; + case BleStatus.locationServicesDisabled: + return "Enable location services"; + case BleStatus.ready: + return "Bluetooth is up and running"; + default: + return "Waiting to fetch Bluetooth status $status"; + } + } + + @override + Widget build(BuildContext context) => Scaffold( + body: Center( + child: Text(determineText(status)), + ), + ); +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/characteristic_interaction_dialog.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/characteristic_interaction_dialog.dart new file mode 100644 index 000000000..d2a252f52 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/characteristic_interaction_dialog.dart @@ -0,0 +1,229 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/ble_device_interactor.dart'; +import 'package:provider/provider.dart'; + +class CharacteristicInteractionDialog extends StatelessWidget { + const CharacteristicInteractionDialog({ + required this.characteristic, + Key? key, + }) : super(key: key); + final QualifiedCharacteristic characteristic; + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, interactor, _) => _CharacteristicInteractionDialog( + characteristic: characteristic, + readCharacteristic: interactor.readCharacteristic, + writeWithResponse: interactor.writeCharacterisiticWithResponse, + writeWithoutResponse: + interactor.writeCharacterisiticWithoutResponse, + subscribeToCharacteristic: interactor.subScribeToCharacteristic, + )); +} + +class _CharacteristicInteractionDialog extends StatefulWidget { + const _CharacteristicInteractionDialog({ + required this.characteristic, + required this.readCharacteristic, + required this.writeWithResponse, + required this.writeWithoutResponse, + required this.subscribeToCharacteristic, + Key? key, + }) : super(key: key); + + final QualifiedCharacteristic characteristic; + final Future> Function(QualifiedCharacteristic characteristic) + readCharacteristic; + final Future Function( + QualifiedCharacteristic characteristic, List value) + writeWithResponse; + + final Stream> Function(QualifiedCharacteristic characteristic) + subscribeToCharacteristic; + + final Future Function( + QualifiedCharacteristic characteristic, List value) + writeWithoutResponse; + + @override + _CharacteristicInteractionDialogState createState() => + _CharacteristicInteractionDialogState(); +} + +class _CharacteristicInteractionDialogState + extends State<_CharacteristicInteractionDialog> { + late String readOutput; + late String writeOutput; + late String subscribeOutput; + late TextEditingController textEditingController; + late StreamSubscription>? subscribeStream; + + @override + void initState() { + readOutput = ''; + writeOutput = ''; + subscribeOutput = ''; + textEditingController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + subscribeStream?.cancel(); + super.dispose(); + } + + Future subscribeCharacteristic() async { + subscribeStream = + widget.subscribeToCharacteristic(widget.characteristic).listen((event) { + setState(() { + subscribeOutput = event.toString(); + }); + }); + setState(() { + subscribeOutput = 'Notification set'; + }); + } + + Future readCharacteristic() async { + final result = await widget.readCharacteristic(widget.characteristic); + setState(() { + readOutput = result.toString(); + }); + } + + List _parseInput() => textEditingController.text + .split(',') + .map( + int.parse, + ) + .toList(); + + Future writeCharacteristicWithResponse() async { + await widget.writeWithResponse(widget.characteristic, _parseInput()); + setState(() { + writeOutput = 'Ok'; + }); + } + + Future writeCharacteristicWithoutResponse() async { + await widget.writeWithoutResponse(widget.characteristic, _parseInput()); + setState(() { + writeOutput = 'Done'; + }); + } + + Widget sectionHeader(String text) => Text( + text, + style: const TextStyle(fontWeight: FontWeight.bold), + ); + + List get writeSection => [ + sectionHeader('Write characteristic'), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextField( + controller: textEditingController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Value', + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: false, + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: writeCharacteristicWithResponse, + child: const Text('With response'), + ), + ElevatedButton( + onPressed: writeCharacteristicWithoutResponse, + child: const Text('Without response'), + ), + ], + ), + Padding( + padding: const EdgeInsetsDirectional.only(top: 8.0), + child: Text('Output: $writeOutput'), + ), + ]; + + List get readSection => [ + sectionHeader('Read characteristic'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: readCharacteristic, + child: const Text('Read'), + ), + Text('Output: $readOutput'), + ], + ), + ]; + + List get subscribeSection => [ + sectionHeader('Subscribe / notify'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: subscribeCharacteristic, + child: const Text('Subscribe'), + ), + Text('Output: $subscribeOutput'), + ], + ), + ]; + + Widget get divider => const Padding( + padding: EdgeInsets.symmetric(vertical: 12.0), + child: Divider(thickness: 2.0), + ); + + @override + Widget build(BuildContext context) => Dialog( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: ListView( + shrinkWrap: true, + children: [ + const Text( + 'Select an operation', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + widget.characteristic.characteristicId.toString(), + ), + ), + divider, + ...readSection, + divider, + ...writeSection, + divider, + ...subscribeSection, + divider, + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(top: 20.0), + child: ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('close')), + ), + ) + ], + ), + ), + ); +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_detail_screen.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_detail_screen.dart new file mode 100644 index 000000000..4a9df0b80 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_detail_screen.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/ble_device_connector.dart'; +import 'package:flutter_reactive_ble_example/src/ui/device_detail/device_log_tab.dart'; +import 'package:provider/provider.dart'; + +import 'device_interaction_tab.dart'; + +class DeviceDetailScreen extends StatelessWidget { + final DiscoveredDevice device; + + const DeviceDetailScreen({required this.device, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Consumer( + builder: (_, deviceConnector, __) => _DeviceDetail( + device: device, + disconnect: deviceConnector.disconnect, + ), + ); +} + +class _DeviceDetail extends StatelessWidget { + const _DeviceDetail({ + required this.device, + required this.disconnect, + Key? key, + }) : super(key: key); + + final DiscoveredDevice device; + final void Function(String deviceId) disconnect; + @override + Widget build(BuildContext context) => WillPopScope( + onWillPop: () async { + disconnect(device.id); + return true; + }, + child: DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: Text(device.name), + bottom: const TabBar( + tabs: [ + Tab( + icon: Icon( + Icons.bluetooth_connected, + ), + ), + Tab( + icon: Icon( + Icons.find_in_page_sharp, + ), + ), + ], + ), + ), + body: TabBarView( + children: [ + DeviceInteractionTab( + device: device, + ), + const DeviceLogTab(), + ], + ), + ), + ), + ); +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.dart new file mode 100644 index 000000000..4abe66d02 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.dart @@ -0,0 +1,284 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.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:functional_data/functional_data.dart'; +import 'package:provider/provider.dart'; + +import 'characteristic_interaction_dialog.dart'; + +part 'device_interaction_tab.g.dart'; +//ignore_for_file: annotate_overrides + +class DeviceInteractionTab extends StatelessWidget { + final DiscoveredDevice device; + + const DeviceInteractionTab({ + required this.device, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => + Consumer3( + builder: (_, deviceConnector, connectionStateUpdate, serviceDiscoverer, + __) => + _DeviceInteractionTab( + viewModel: DeviceInteractionViewModel( + deviceId: device.id, + connectionStatus: connectionStateUpdate.connectionState, + deviceConnector: deviceConnector, + discoverServices: () => + serviceDiscoverer.discoverServices(device.id)), + ), + ); +} + +@immutable +@FunctionalData() +class DeviceInteractionViewModel extends $DeviceInteractionViewModel { + const DeviceInteractionViewModel({ + required this.deviceId, + required this.connectionStatus, + required this.deviceConnector, + required this.discoverServices, + }); + + final String deviceId; + final DeviceConnectionState connectionStatus; + final BleDeviceConnector deviceConnector; + @CustomEquality(Ignore()) + final Future> Function() discoverServices; + + bool get deviceConnected => + connectionStatus == DeviceConnectionState.connected; + + void connect() { + deviceConnector.connect(deviceId); + } + + void disconnect() { + deviceConnector.disconnect(deviceId); + } +} + +class _DeviceInteractionTab extends StatefulWidget { + const _DeviceInteractionTab({ + required this.viewModel, + Key? key, + }) : super(key: key); + + final DeviceInteractionViewModel viewModel; + + @override + _DeviceInteractionTabState createState() => _DeviceInteractionTabState(); +} + +class _DeviceInteractionTabState extends State<_DeviceInteractionTab> { + late List discoveredServices; + + @override + void initState() { + discoveredServices = []; + super.initState(); + } + + Future discoverServices() async { + final result = await widget.viewModel.discoverServices(); + setState(() { + discoveredServices = result; + }); + } + + @override + Widget build(BuildContext context) => CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate.fixed( + [ + Padding( + 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), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text( + "Status: ${widget.viewModel.connectionStatus}", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: !widget.viewModel.deviceConnected + ? widget.viewModel.connect + : null, + child: const Text("Connect"), + ), + ElevatedButton( + onPressed: widget.viewModel.deviceConnected + ? widget.viewModel.disconnect + : null, + child: const Text("Disconnect"), + ), + ElevatedButton( + onPressed: widget.viewModel.deviceConnected + ? discoverServices + : null, + child: const Text("Discover Services"), + ), + ], + ), + ), + if (widget.viewModel.deviceConnected) + _ServiceDiscoveryList( + deviceId: widget.viewModel.deviceId, + discoveredServices: discoveredServices, + ), + ], + ), + ), + ], + ); +} + +class _ServiceDiscoveryList extends StatefulWidget { + const _ServiceDiscoveryList({ + required this.deviceId, + required this.discoveredServices, + Key? key, + }) : super(key: key); + + final String deviceId; + final List discoveredServices; + + @override + _ServiceDiscoveryListState createState() => _ServiceDiscoveryListState(); +} + +class _ServiceDiscoveryListState extends State<_ServiceDiscoveryList> { + late final List _expandedItems; + + @override + void initState() { + _expandedItems = []; + super.initState(); + } + + String _charactisticsSummary(DiscoveredCharacteristic c) { + final props = []; + if (c.isReadable) { + props.add("read"); + } + if (c.isWritableWithoutResponse) { + props.add("write without response"); + } + if (c.isWritableWithResponse) { + props.add("write with response"); + } + if (c.isNotifiable) { + props.add("notify"); + } + if (c.isIndicatable) { + props.add("indicate"); + } + + return props.join("\n"); + } + + Widget _characteristicTile( + DiscoveredCharacteristic characteristic, String deviceId) => + ListTile( + onTap: () => showDialog( + context: context, + builder: (context) => CharacteristicInteractionDialog( + characteristic: QualifiedCharacteristic( + characteristicId: characteristic.characteristicId, + serviceId: characteristic.serviceId, + deviceId: deviceId), + )), + title: Text( + '${characteristic.characteristicId}\n(${_charactisticsSummary(characteristic)})', + style: const TextStyle( + fontSize: 14, + ), + ), + ); + + List buildPanels() { + final panels = []; + + widget.discoveredServices.asMap().forEach( + (index, service) => panels.add( + ExpansionPanel( + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsetsDirectional.only(start: 16.0), + child: Text( + 'Characteristics', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ListView.builder( + shrinkWrap: true, + itemBuilder: (context, index) => _characteristicTile( + service.characteristics[index], + widget.deviceId, + ), + itemCount: service.characteristicIds.length, + ), + ], + ), + headerBuilder: (context, isExpanded) => ListTile( + title: Text( + '${service.serviceId}', + style: const TextStyle(fontSize: 14), + ), + ), + isExpanded: _expandedItems.contains(index), + ), + ), + ); + + return panels; + } + + @override + Widget build(BuildContext context) => widget.discoveredServices.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsetsDirectional.only( + top: 20.0, + start: 20.0, + end: 20.0, + ), + child: ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { + setState(() { + if (isExpanded) { + _expandedItems.remove(index); + } else { + _expandedItems.add(index); + } + }); + }); + }, + children: [ + ...buildPanels(), + ], + ), + ); +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.g.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.g.dart new file mode 100644 index 000000000..994c9ccee --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.g.dart @@ -0,0 +1,115 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_interaction_tab.dart'; + +// ************************************************************************** +// FunctionalDataGenerator +// ************************************************************************** + +abstract class $DeviceInteractionViewModel { + const $DeviceInteractionViewModel(); + + String get deviceId; + DeviceConnectionState get connectionStatus; + BleDeviceConnector get deviceConnector; + Future> Function() get discoverServices; + + DeviceInteractionViewModel copyWith({ + String? deviceId, + DeviceConnectionState? connectionStatus, + BleDeviceConnector? deviceConnector, + Future> Function()? discoverServices, + }) => + DeviceInteractionViewModel( + deviceId: deviceId ?? this.deviceId, + connectionStatus: connectionStatus ?? this.connectionStatus, + deviceConnector: deviceConnector ?? this.deviceConnector, + discoverServices: discoverServices ?? this.discoverServices, + ); + + DeviceInteractionViewModel copyUsing( + void Function(DeviceInteractionViewModel$Change change) mutator) { + final change = DeviceInteractionViewModel$Change._( + this.deviceId, + this.connectionStatus, + this.deviceConnector, + this.discoverServices, + ); + mutator(change); + return DeviceInteractionViewModel( + deviceId: change.deviceId, + connectionStatus: change.connectionStatus, + deviceConnector: change.deviceConnector, + discoverServices: change.discoverServices, + ); + } + + @override + String toString() => + "DeviceInteractionViewModel(deviceId: $deviceId, connectionStatus: $connectionStatus, deviceConnector: $deviceConnector, discoverServices: $discoverServices)"; + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) => + other is DeviceInteractionViewModel && + other.runtimeType == runtimeType && + deviceId == other.deviceId && + connectionStatus == other.connectionStatus && + deviceConnector == other.deviceConnector && + const Ignore().equals(discoverServices, other.discoverServices); + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode { + var result = 17; + result = 37 * result + deviceId.hashCode; + result = 37 * result + connectionStatus.hashCode; + result = 37 * result + deviceConnector.hashCode; + result = 37 * result + const Ignore().hash(discoverServices); + return result; + } +} + +class DeviceInteractionViewModel$Change { + DeviceInteractionViewModel$Change._( + this.deviceId, + this.connectionStatus, + this.deviceConnector, + this.discoverServices, + ); + + String deviceId; + DeviceConnectionState connectionStatus; + BleDeviceConnector deviceConnector; + Future> Function() discoverServices; +} + +// ignore: avoid_classes_with_only_static_members +class DeviceInteractionViewModel$ { + static final deviceId = Lens( + (deviceIdContainer) => deviceIdContainer.deviceId, + (deviceIdContainer, deviceId) => + deviceIdContainer.copyWith(deviceId: deviceId), + ); + + static final connectionStatus = + Lens( + (connectionStatusContainer) => connectionStatusContainer.connectionStatus, + (connectionStatusContainer, connectionStatus) => + connectionStatusContainer.copyWith(connectionStatus: connectionStatus), + ); + + static final deviceConnector = + Lens( + (deviceConnectorContainer) => deviceConnectorContainer.deviceConnector, + (deviceConnectorContainer, deviceConnector) => + deviceConnectorContainer.copyWith(deviceConnector: deviceConnector), + ); + + static final discoverServices = Lens> Function()>( + (discoverServicesContainer) => discoverServicesContainer.discoverServices, + (discoverServicesContainer, discoverServices) => + discoverServicesContainer.copyWith(discoverServices: discoverServices), + ); +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_log_tab.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_log_tab.dart new file mode 100644 index 000000000..08ab45dd5 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_log_tab.dart @@ -0,0 +1,32 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_reactive_ble_example/src/ble/ble_logger.dart'; +import 'package:provider/provider.dart'; + +class DeviceLogTab extends StatelessWidget { + const DeviceLogTab({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, logger, _) => _DeviceLogTab( + messages: logger.messages, + ), + ); +} + +class _DeviceLogTab extends StatelessWidget { + const _DeviceLogTab({ + required this.messages, + Key? key, + }) : super(key: key); + + final List messages; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(16.0), + child: ListView.builder( + itemBuilder: (context, index) => Text(messages[index]), + itemCount: messages.length, + ), + ); +} diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_list.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_list.dart new file mode 100644 index 000000000..a3fb58212 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_list.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:flutter_reactive_ble_example/src/ble/ble_scanner.dart'; +import 'package:provider/provider.dart'; + +import '../ble/ble_logger.dart'; +import '../widgets.dart'; +import 'device_detail/device_detail_screen.dart'; + +class DeviceListScreen extends StatelessWidget { + const DeviceListScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => + Consumer3( + builder: (_, bleScanner, bleScannerState, bleLogger, __) => _DeviceList( + scannerState: bleScannerState ?? + const BleScannerState( + discoveredDevices: [], + scanIsInProgress: false, + ), + startScan: bleScanner.startScan, + stopScan: bleScanner.stopScan, + toggleVerboseLogging: bleLogger.toggleVerboseLogging, + verboseLogging: bleLogger.verboseLogging, + ), + ); +} + +class _DeviceList extends StatefulWidget { + const _DeviceList({ + required this.scannerState, + required this.startScan, + required this.stopScan, + required this.toggleVerboseLogging, + required this.verboseLogging, + }); + + final BleScannerState scannerState; + final void Function(List) startScan; + final VoidCallback stopScan; + final VoidCallback toggleVerboseLogging; + final bool verboseLogging; + + @override + _DeviceListState createState() => _DeviceListState(); +} + +class _DeviceListState extends State<_DeviceList> { + late TextEditingController _uuidController; + + @override + void initState() { + super.initState(); + _uuidController = TextEditingController() + ..addListener(() => setState(() {})); + } + + @override + void dispose() { + widget.stopScan(); + _uuidController.dispose(); + super.dispose(); + } + + bool _isValidUuidInput() { + final uuidText = _uuidController.text; + if (uuidText.isEmpty) { + return true; + } else { + try { + Uuid.parse(uuidText); + return true; + } on Exception { + return false; + } + } + } + + void _startScanning() { + final text = _uuidController.text; + widget.startScan(text.isEmpty ? [] : [Uuid.parse(_uuidController.text)]); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Scan for devices'), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + const Text('Service UUID (2, 4, 16 bytes):'), + TextField( + controller: _uuidController, + enabled: !widget.scannerState.scanIsInProgress, + decoration: InputDecoration( + errorText: + _uuidController.text.isEmpty || _isValidUuidInput() + ? null + : 'Invalid UUID format'), + autocorrect: false, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + child: const Text('Scan'), + onPressed: !widget.scannerState.scanIsInProgress && + _isValidUuidInput() + ? _startScanning + : null, + ), + ElevatedButton( + child: const Text('Stop'), + onPressed: widget.scannerState.scanIsInProgress + ? widget.stopScan + : null, + ), + ], + ), + ], + ), + ), + const SizedBox(height: 8), + Flexible( + child: ListView( + children: [ + SwitchListTile( + title: const Text("Verbose logging"), + value: widget.verboseLogging, + onChanged: (_) => setState(widget.toggleVerboseLogging), + ), + ListTile( + title: Text( + !widget.scannerState.scanIsInProgress + ? 'Enter a UUID above and tap start to begin scanning' + : 'Tap a device to connect to it', + ), + trailing: (widget.scannerState.scanIsInProgress || + widget.scannerState.discoveredDevices.isNotEmpty) + ? Text( + 'count: ${widget.scannerState.discoveredDevices.length}', + ) + : null, + ), + ...widget.scannerState.discoveredDevices + .map( + (device) => ListTile( + title: Text(device.name), + subtitle: Text("${device.id}\nRSSI: ${device.rssi}"), + leading: const BluetoothIcon(), + onTap: () async { + widget.stopScan(); + await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + DeviceDetailScreen(device: device))); + }, + ), + ) + .toList(), + ], + ), + ), + ], + ), + ); +} diff --git a/packages/flutter_reactive_ble/example/lib/src/utils.dart b/packages/flutter_reactive_ble/example/lib/src/utils.dart new file mode 100644 index 000000000..a0293a035 --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/utils.dart @@ -0,0 +1,2 @@ +// ignore: avoid_print +void log(String text) => print("[FlutterReactiveBLEApp] $text"); diff --git a/packages/flutter_reactive_ble/example/lib/src/widgets.dart b/packages/flutter_reactive_ble/example/lib/src/widgets.dart new file mode 100644 index 000000000..a6ec0672a --- /dev/null +++ b/packages/flutter_reactive_ble/example/lib/src/widgets.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class BluetoothIcon extends StatelessWidget { + const BluetoothIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => const SizedBox( + width: 64, + height: 64, + child: Align(alignment: Alignment.center, child: Icon(Icons.bluetooth)), + ); +} + +class StatusMessage extends StatelessWidget { + const StatusMessage({ + required this.text, + Key? key, + }) : super(key: key); + + final String text; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Text(text, style: const TextStyle(fontWeight: FontWeight.bold)), + ); +} diff --git a/packages/flutter_reactive_ble/example/pubspec.yaml b/packages/flutter_reactive_ble/example/pubspec.yaml new file mode 100644 index 000000000..cefda07dd --- /dev/null +++ b/packages/flutter_reactive_ble/example/pubspec.yaml @@ -0,0 +1,18 @@ +name: flutter_reactive_ble_example +description: Demonstrates how to use the flutter_reactive_ble_tizen plugin. +publish_to: none + +environment: + sdk: ">=2.18.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_reactive_ble: ^5.0.3 + flutter_reactive_ble_tizen: + path: ../ + intl: ^0.17.0 + provider: ^6.0.4 + +flutter: + uses-material-design: true diff --git a/packages/flutter_reactive_ble/example/tizen/.gitignore b/packages/flutter_reactive_ble/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/flutter_reactive_ble/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/flutter_reactive_ble/example/tizen/App.cs b/packages/flutter_reactive_ble/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/flutter_reactive_ble/example/tizen/App.cs @@ -0,0 +1,20 @@ +using Tizen.Flutter.Embedding; + +namespace Runner +{ + public class App : FlutterApplication + { + protected override void OnCreate() + { + base.OnCreate(); + + GeneratedPluginRegistrant.RegisterPlugins(this); + } + + static void Main(string[] args) + { + var app = new App(); + app.Run(args); + } + } +} diff --git a/packages/flutter_reactive_ble/example/tizen/Runner.csproj b/packages/flutter_reactive_ble/example/tizen/Runner.csproj new file mode 100644 index 000000000..f4e369d0c --- /dev/null +++ b/packages/flutter_reactive_ble/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ + + + + Exe + tizen40 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/flutter_reactive_ble/example/tizen/shared/res/ic_launcher.png b/packages/flutter_reactive_ble/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/flutter_reactive_ble/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/flutter_reactive_ble/example/tizen/tizen-manifest.xml b/packages/flutter_reactive_ble/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..ca17f5dc8 --- /dev/null +++ b/packages/flutter_reactive_ble/example/tizen/tizen-manifest.xml @@ -0,0 +1,13 @@ + + + + + + ic_launcher.png + + + + + http://tizen.org/privilege/bluetooth + + diff --git a/packages/flutter_reactive_ble/lib/flutter_reactive_ble_tizen.dart b/packages/flutter_reactive_ble/lib/flutter_reactive_ble_tizen.dart new file mode 100644 index 000000000..c4d45ea91 --- /dev/null +++ b/packages/flutter_reactive_ble/lib/flutter_reactive_ble_tizen.dart @@ -0,0 +1,173 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: prefer_expression_function_bodies + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart'; + +import 'src/models.dart'; + +/// Tizen implementation of [ReactiveBlePlatform]. +class ReactiveBleTizen extends ReactiveBlePlatform { + /// Registers this class as the default instance of [ReactiveBlePlatform]. + static void register() { + ReactiveBlePlatform.instance = ReactiveBleTizen(); + } + + @visibleForTesting + final MethodChannel methodChannel = + const MethodChannel('flutter_reactive_ble_method'); + + @override + final Stream bleStatusStream = + const EventChannel('flutter_reactive_ble_status') + .receiveBroadcastStream() + .map((dynamic event) => BleStatus.values.byName(event as String)); + + @override + final Stream scanStream = const EventChannel( + 'flutter_reactive_ble_scan') + .receiveBroadcastStream() + .map((dynamic event) => + ScanResultExtension.fromMap((event as Map).cast())); + + @override + final Stream connectionUpdateStream = + const EventChannel('flutter_reactive_ble_connected_device') + .receiveBroadcastStream() + .map((dynamic event) => ConnectionStateUpdateExtension.fromMap( + (event as Map).cast())); + + @override + final Stream charValueUpdateStream = + const EventChannel('flutter_reactive_ble_char_update') + .receiveBroadcastStream() + .map((dynamic event) => CharacteristicValueExtension.fromMap( + (event as Map).cast())); + + @override + Future initialize() => methodChannel.invokeMethod('initialize'); + + @override + Future deinitialize() => methodChannel.invokeMethod('deinitialize'); + + @override + Stream scanForDevices({ + required List withServices, + required ScanMode scanMode, // ignored + required bool requireLocationServicesEnabled, // ignored + }) { + return methodChannel.invokeMethod( + 'scanForDevices', + { + kServiceIds: withServices.map((Uuid uuid) => '$uuid').toList(), + }, + ).asStream(); + } + + @override + Stream connectToDevice( + String id, + Map>? servicesWithCharacteristicsToDiscover, + Duration? connectionTimeout, + ) { + // TODO(swift-kim): Support the connectionTimeout parameter. + return methodChannel.invokeMethod( + 'connectToDevice', + {kDeviceId: id}, + ).asStream(); + } + + @override + Future disconnectDevice(String deviceId) => methodChannel + .invokeMethod('disconnectDevice', {kDeviceId: deviceId}); + + @override + Future> discoverServices(String deviceId) async { + final result = await methodChannel.invokeListMethod( + 'discoverServices', + {kDeviceId: deviceId}, + ); + return result!.map(DiscoveredServiceExtension.fromMap).toList(); + } + + @override + Future requestConnectionPriority( + String deviceId, + ConnectionPriority priority, + ) { + throw UnimplementedError(); + } + + @override + Future requestMtuSize(String deviceId, int? mtu) async { + final mtuSize = await methodChannel.invokeMethod( + 'requestMtuSize', + {kDeviceId: deviceId, kMtu: mtu}, + ); + return mtuSize!; + } + + @override + Stream readCharacteristic(QualifiedCharacteristic characteristic) { + return methodChannel.invokeMethod( + 'readCharacteristic', + {kQualifiedCharacteristic: characteristic.toMap()}, + ).asStream(); + } + + @override + Future writeCharacteristicWithResponse( + QualifiedCharacteristic characteristic, + List value, + ) async { + final result = await methodChannel.invokeMapMethod( + 'writeCharacteristicWithResponse', + { + kQualifiedCharacteristic: characteristic.toMap(), + kValue: Uint8List.fromList(value), + }, + ); + return WriteCharacteristicInfoExtension.fromMap(result!); + } + + @override + Future writeCharacteristicWithoutResponse( + QualifiedCharacteristic characteristic, + List value, + ) async { + final result = await methodChannel.invokeMapMethod( + 'writeCharacteristicWithoutResponse', + { + kQualifiedCharacteristic: characteristic.toMap(), + kValue: Uint8List.fromList(value), + }, + ); + return WriteCharacteristicInfoExtension.fromMap(result!); + } + + @override + Stream subscribeToNotifications( + QualifiedCharacteristic characteristic, + ) { + return methodChannel.invokeMethod( + 'subscribeToNotifications', + {kQualifiedCharacteristic: characteristic.toMap()}, + ).asStream(); + } + + @override + Future stopSubscribingToNotifications( + QualifiedCharacteristic characteristic, + ) { + return methodChannel.invokeMethod( + 'stopSubscribingToNotifications', + {kQualifiedCharacteristic: characteristic.toMap()}, + ); + } +} diff --git a/packages/flutter_reactive_ble/lib/src/models.dart b/packages/flutter_reactive_ble/lib/src/models.dart new file mode 100644 index 000000000..d553032d9 --- /dev/null +++ b/packages/flutter_reactive_ble/lib/src/models.dart @@ -0,0 +1,139 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart'; + +const String kDeviceId = 'device_id'; +const String kDeviceName = 'device_name'; +const String kServiceIds = 'service_ids'; +const String kServiceData = 'service_data'; +const String kManufacturerData = 'manufacturer_data'; +const String kRssi = 'rssi'; +const String kConnectionState = 'connection_state'; +const String kCharacteristicId = 'characteristic_id'; +const String kServiceId = 'service_id'; +const String kIsReadable = 'is_readable'; +const String kIsWritableWithResponse = 'is_writable_with_response'; +const String kIsWritableWithoutResponse = 'is_writable_without_response'; +const String kIsNotifiable = 'is_notifiable'; +const String kIsIndicatable = 'is_indicatable'; +const String kIncludedServices = 'included_services'; +const String kCharacteristicIds = 'characteristic_ids'; +const String kCharacteristics = 'characteristics'; +const String kQualifiedCharacteristic = 'qualified_characteristic'; +const String kResult = 'result'; +const String kMtu = 'mtu'; +const String kValue = 'value'; + +extension DiscoveredDeviceExtension on DiscoveredDevice { + static DiscoveredDevice fromMap(Map map) { + final serviceUuids = + (map[kServiceIds] as List).cast().map(Uuid.parse).toList(); + final serviceData = (map[kServiceData] as Map) + .cast() + .map((String key, Uint8List value) => MapEntry(Uuid.parse(key), value)); + return DiscoveredDevice( + id: map[kDeviceId] as String, + name: map[kDeviceName] as String? ?? '', + serviceData: serviceData, + manufacturerData: map[kManufacturerData] as Uint8List, + rssi: map[kRssi] as int, + serviceUuids: serviceUuids, + ); + } +} + +extension ScanResultExtension on ScanResult { + static ScanResult fromMap(Map map) { + final discoveredDevice = DiscoveredDeviceExtension.fromMap(map); + return ScanResult(result: Result.success(discoveredDevice)); + } +} + +extension ConnectionStateUpdateExtension on ConnectionStateUpdate { + static ConnectionStateUpdate fromMap(Map map) => + ConnectionStateUpdate( + deviceId: map[kDeviceId] as String, + connectionState: DeviceConnectionState.values + .byName(map[kConnectionState] as String), + failure: null, + ); +} + +extension DiscoveredCharacteristicExtension on DiscoveredCharacteristic { + static DiscoveredCharacteristic fromMap(Map map) => + DiscoveredCharacteristic( + characteristicId: Uuid.parse(map[kCharacteristicId] as String), + serviceId: Uuid.parse(map[kServiceId] as String), + isReadable: map[kIsReadable] as bool, + isWritableWithResponse: map[kIsWritableWithResponse] as bool, + isWritableWithoutResponse: map[kIsWritableWithoutResponse] as bool, + isNotifiable: map[kIsNotifiable] as bool, + isIndicatable: map[kIsIndicatable] as bool, + ); +} + +extension DiscoveredServiceExtension on DiscoveredService { + static DiscoveredService fromMap(Map map) { + final includedServices = (map[kIncludedServices] as List) + .cast() + .map(DiscoveredServiceExtension.fromMap) + .toList(); + final characteristics = (map[kCharacteristics] as List) + .cast() + .map(DiscoveredCharacteristicExtension.fromMap) + .toList(); + final characteristicIds = (map[kCharacteristicIds] as List) + .cast() + .map(Uuid.parse) + .toList(); + return DiscoveredService( + serviceId: Uuid.parse(map[kServiceId] as String), + characteristicIds: characteristicIds, + includedServices: includedServices, + characteristics: characteristics, + ); + } +} + +extension QualifiedCharacteristicExtension on QualifiedCharacteristic { + Map toMap() => { + kDeviceId: deviceId, + kServiceId: '$serviceId', + kCharacteristicId: '$characteristicId', + }; + + static QualifiedCharacteristic fromMap(Map map) => + QualifiedCharacteristic( + deviceId: map[kDeviceId] as String, + serviceId: Uuid.parse(map[kServiceId] as String), + characteristicId: Uuid.parse(map[kCharacteristicId] as String), + ); +} + +extension CharacteristicValueExtension on CharacteristicValue { + static CharacteristicValue fromMap(Map map) { + final characteristicMap = + (map[kQualifiedCharacteristic] as Map).cast(); + final valueList = (map[kResult] as List).cast(); + return CharacteristicValue( + characteristic: + QualifiedCharacteristicExtension.fromMap(characteristicMap), + result: Result.success(valueList), + ); + } +} + +extension WriteCharacteristicInfoExtension on WriteCharacteristicInfo { + static WriteCharacteristicInfo fromMap(Map map) { + final characteristicMap = + (map[kQualifiedCharacteristic] as Map).cast(); + return WriteCharacteristicInfo( + characteristic: + QualifiedCharacteristicExtension.fromMap(characteristicMap), + result: const Result.success(Unit()), + ); + } +} diff --git a/packages/flutter_reactive_ble/pubspec.yaml b/packages/flutter_reactive_ble/pubspec.yaml new file mode 100644 index 000000000..054925cdc --- /dev/null +++ b/packages/flutter_reactive_ble/pubspec.yaml @@ -0,0 +1,27 @@ +name: flutter_reactive_ble_tizen +description: Tizen implementation of the flutter_reactive_ble plugin. +homepage: https://github.com/flutter-tizen/plugins +repository: https://github.com/flutter-tizen/plugins/tree/master/packages/flutter_reactive_ble +version: 0.1.0 + +environment: + sdk: ">=2.18.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + reactive_ble_platform_interface: ^5.0.3 + +dev_dependencies: + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + tizen: + dartPluginClass: ReactiveBleTizen + pluginClass: FlutterReactiveBleTizenPlugin + fileName: flutter_reactive_ble_tizen_plugin.h diff --git a/packages/flutter_reactive_ble/test/flutter_reative_ble_tizen_test.dart b/packages/flutter_reactive_ble/test/flutter_reative_ble_tizen_test.dart new file mode 100644 index 000000000..e93055d83 --- /dev/null +++ b/packages/flutter_reactive_ble/test/flutter_reative_ble_tizen_test.dart @@ -0,0 +1,80 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_reactive_ble_tizen/flutter_reactive_ble_tizen.dart'; +import 'package:flutter_reactive_ble_tizen/src/models.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late ReactiveBleTizen plugin; + late List methodCalls; + + setUp(() { + plugin = ReactiveBleTizen(); + methodCalls = []; + + plugin.methodChannel.setMockMethodCallHandler((MethodCall methodCall) { + methodCalls.add(methodCall); + return null; + }); + }); + + test('scanForDevices', () { + final uuids = [ + // 16-bit UUID. + Uuid.parse('0A0A'), + // 32-bit UUID. + Uuid.parse('0A0A0A0A'), + // 128-bit UUID. + Uuid.parse('0A0A0A0A-0A0A-0A0A-0A0A-0A0A0A0A0A0A'), + ]; + plugin.scanForDevices( + withServices: uuids, + scanMode: ScanMode.balanced, + requireLocationServicesEnabled: true, + ); + + expect( + methodCalls.first, + isMethodCall( + 'scanForDevices', + arguments: { + kServiceIds: [ + '0a0a', + '0a0a0a0a', + '0a0a0a0a-0a0a-0a0a-0a0a-0a0a0a0a0a0a', + ], + }, + ), + ); + }); + + test('subscribeToNotifications', () { + plugin.subscribeToNotifications(QualifiedCharacteristic( + deviceId: '01:23:45:67:89:AB', + // Battery Service UUID. + serviceId: Uuid([0x18, 0x0f]), + // Battery Level Characteristic UUID. + characteristicId: Uuid([0x2a, 0x19]), + )); + + expect( + methodCalls.first, + isMethodCall( + 'subscribeToNotifications', + arguments: { + kQualifiedCharacteristic: { + 'device_id': '01:23:45:67:89:AB', + 'service_id': '180f', + 'characteristic_id': '2a19', + } + }, + ), + ); + }); +} diff --git a/packages/flutter_reactive_ble/tizen/.gitignore b/packages/flutter_reactive_ble/tizen/.gitignore new file mode 100644 index 000000000..a2a7d62b1 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/.gitignore @@ -0,0 +1,5 @@ +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/flutter_reactive_ble/tizen/inc/flutter_reactive_ble_tizen_plugin.h b/packages/flutter_reactive_ble/tizen/inc/flutter_reactive_ble_tizen_plugin.h new file mode 100644 index 000000000..c7c3b6c65 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/inc/flutter_reactive_ble_tizen_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_FLUTTER_REACTIVE_BLE_TIZEN_PLUGIN_H_ +#define FLUTTER_PLUGIN_FLUTTER_REACTIVE_BLE_TIZEN_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void FlutterReactiveBleTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FLUTTER_REACTIVE_BLE_TIZEN_PLUGIN_H_ diff --git a/packages/flutter_reactive_ble/tizen/project_def.prop b/packages/flutter_reactive_ble/tizen/project_def.prop new file mode 100644 index 000000000..9fdee0ed0 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/project_def.prop @@ -0,0 +1,24 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = flutter_reactive_ble_tizen_plugin +type = staticLib +profile = common-4.0 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# Compiler flags +USER_CFLAGS_MISC = +USER_CPPFLAGS_MISC = + +# User includes +USER_INC_DIRS = inc src +USER_INC_FILES = +USER_CPP_INC_FILES = diff --git a/packages/flutter_reactive_ble/tizen/src/ble_device.cc b/packages/flutter_reactive_ble/tizen/src/ble_device.cc new file mode 100644 index 000000000..d0f62c099 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/ble_device.cc @@ -0,0 +1,298 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ble_device.h" + +#include + +#include "log.h" + +namespace { + +Uuid GetUuid(const bt_gatt_h &gatt_handle) { + char *uuid = nullptr; + int ret = bt_gatt_get_uuid(gatt_handle, &uuid); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Failed to retrieve a UUID: %s", get_error_message(ret)); + return std::string(); + } + std::string result = std::string(uuid); + free(uuid); + return result; +} + +void AddDiscoveredService(bt_gatt_h service_handle, + std::vector *discovered_services) { + DiscoveredService discovered_service = {}; + discovered_service.service_id = GetUuid(service_handle); + + bt_gatt_service_foreach_included_services( + service_handle, + [](int total, int index, bt_gatt_h included_service_handle, + void *user_data) -> bool { + auto *discovered_service = static_cast(user_data); + AddDiscoveredService(included_service_handle, + &discovered_service->included_services); + return true; + }, + &discovered_service); + bt_gatt_service_foreach_characteristics( + service_handle, + [](int total, int index, bt_gatt_h characteristic_handle, + void *user_data) -> bool { + int properties = 0; + int ret = bt_gatt_characteristic_get_properties(characteristic_handle, + &properties); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Unable to get characteristic properties: %s", + get_error_message(ret)); + return true; + } + + DiscoveredCharacteristic characteristic = {}; + characteristic.characteristic_id = GetUuid(characteristic_handle); + characteristic.is_readable = properties & BT_GATT_PROPERTY_READ; + characteristic.is_writable_with_response = + properties & BT_GATT_PROPERTY_WRITE; + characteristic.is_writable_without_response = + properties & BT_GATT_PROPERTY_WRITE_WITHOUT_RESPONSE; + characteristic.is_notifiable = properties & BT_GATT_PROPERTY_NOTIFY; + characteristic.is_indicatable = properties & BT_GATT_PROPERTY_INDICATE; + + auto *discovered_service = static_cast(user_data); + characteristic.service_id = discovered_service->service_id; + discovered_service->characteristic_ids.emplace_back( + characteristic.characteristic_id); + discovered_service->characteristics.emplace_back(characteristic); + return true; + }, + &discovered_service); + + discovered_services->emplace_back(discovered_service); +} + +} // namespace + +BleDevice::BleDevice(const std::string &device_id) : device_id_(device_id) { + last_error_ = bt_gatt_client_create(device_id.c_str(), &handle_); + if (last_error_ != BT_ERROR_NONE) { + LOG_ERROR("Failed to create a client handle: %s", + get_error_message(last_error_)); + return; + } +} + +BleDevice::~BleDevice() { + if (handle_) { + bt_gatt_client_destroy(handle_); + } +} + +std::vector BleDevice::DiscoverServices() { + std::vector discovered_services; + last_error_ = bt_gatt_client_foreach_services( + handle_, + [](int total, int index, bt_gatt_h service_handle, void *user_data) { + auto *discovered_services = + static_cast *>(user_data); + AddDiscoveredService(service_handle, discovered_services); + return true; + }, + &discovered_services); + if (last_error_ != BT_ERROR_NONE) { + LOG_ERROR("Unable to look up services: %s", get_error_message(last_error_)); + } + return discovered_services; +} + +bool BleDevice::NegotiateMtuSize(int32_t request_mtu, + MtuNegotiationCallback on_done, + ErrorCallback on_error) { + struct UserData { + MtuNegotiationCallback on_done; + ErrorCallback on_error; + } *params = new UserData; + params->on_done = std::move(on_done); + params->on_error = std::move(on_error); + + // Unset any existing callback just before setting a new callback, because + // calling the unset function inside the callback will result in an internal + // memory corruption. + bt_gatt_client_unset_att_mtu_changed_cb(handle_); + + last_error_ = bt_gatt_client_set_att_mtu_changed_cb( + handle_, + [](bt_gatt_client_h client, const bt_gatt_client_att_mtu_info_s *mtu_info, + void *user_data) { + UserData *params = static_cast(user_data); + if (mtu_info->status == 0) { + params->on_done(mtu_info->mtu); + } else { + // TODO(swift-kim): What does the status value mean? + params->on_error(mtu_info->status, "Failed to update the MTU value."); + } + delete params; + }, + params); + if (last_error_ != BT_ERROR_NONE) { + LOG_ERROR("Could not set an MTU change callback: %s", + get_error_message(last_error_)); + delete params; + return false; + } + + last_error_ = bt_gatt_client_request_att_mtu_change(handle_, request_mtu); + if (last_error_ != BT_ERROR_NONE) { + LOG_ERROR("Could not request an MTU change: %s", + get_error_message(last_error_)); + delete params; + return false; + } + return true; +} + +bool BleDevice::ReadCharacteristic( + const QualifiedCharacteristic &characteristic, VoidCallback on_done, + ErrorCallback on_error) { + struct UserData { + BleDevice *self; + VoidCallback on_done; + ErrorCallback on_error; + } *params = new UserData; + params->self = this; + params->on_done = std::move(on_done); + params->on_error = std::move(on_error); + + last_error_ = bt_gatt_client_read_value( + characteristic.handle(), + [](int result, bt_gatt_h request_handle, void *user_data) { + UserData *params = static_cast(user_data); + if (result != BT_ERROR_NONE) { + LOG_ERROR("The read operation resulted in an error: %s", + get_error_message(result)); + if (params->on_error) { + params->on_error(result, get_error_message(result)); + } + delete params; + return; + } + + std::shared_ptr characteristic = + params->self->FindCharacteristic(request_handle); + if (!characteristic) { + LOG_ERROR("The characteristic is unexpectedly not found."); + if (params->on_error) { + params->on_error(BT_ERROR_NONE, "Something went wrong."); + } + delete params; + return; + } + + char *value = nullptr; + int length = 0; + int ret = bt_gatt_get_value(request_handle, &value, &length); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Failed to retrieve a value: %s", get_error_message(ret)); + if (params->on_error) { + params->on_error(ret, get_error_message(ret)); + } + delete params; + return; + } + std::vector bytes(value, value + length); + free(value); + + // The done callback must be called before the notification is sent. + if (params->on_done) { + params->on_done(); + } + if (params->self->notification_callback_) { + params->self->notification_callback_(*characteristic, bytes); + } + delete params; + }, + params); + if (last_error_ != BT_ERROR_NONE) { + LOG_ERROR("Could not read a value from the characteristic: %s", + get_error_message(last_error_)); + return false; + } + return true; +} + +bool BleDevice::WriteCharacteristic( + const QualifiedCharacteristic &characteristic, + const std::vector &value, VoidCallback on_done, + ErrorCallback on_error) { + last_error_ = bt_gatt_set_value(characteristic.handle(), + reinterpret_cast(value.data()), + value.size()); + if (last_error_ != BT_ERROR_NONE) { + LOG_ERROR("Unable to set a value to the characteristic: %s", + get_error_message(last_error_)); + return false; + } + + struct UserData { + VoidCallback on_done; + ErrorCallback on_error; + } *params = new UserData; + params->on_done = std::move(on_done); + params->on_error = std::move(on_error); + + last_error_ = bt_gatt_client_write_value( + characteristic.handle(), + [](int result, bt_gatt_h request_handle, void *user_data) { + UserData *params = static_cast(user_data); + if (result != BT_ERROR_NONE) { + LOG_ERROR("The write operation resulted in an error: %s", + get_error_message(result)); + if (params->on_error) { + params->on_error(result, get_error_message(result)); + } + } else if (params->on_done) { + params->on_done(); + } + delete params; + }, + params); + if (last_error_ != BT_ERROR_NONE) { + LOG_ERROR("Could not write to the characteristic: %s", + get_error_message(last_error_)); + delete params; + return false; + } + + return true; +} + +bool BleDevice::ListenNotifications( + const QualifiedCharacteristic &characteristic) { + last_error_ = bt_gatt_client_set_characteristic_value_changed_cb( + characteristic.handle(), + [](bt_gatt_h handle, char *value, int len, void *user_data) { + BleDevice *self = static_cast(user_data); + std::shared_ptr characteristic = + self->FindCharacteristic(handle); + if (self->notification_callback_ && characteristic) { + std::vector bytes(value, value + len); + self->notification_callback_(*characteristic, bytes); + } + }, + this); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + return true; +} + +bool BleDevice::StopNotifications( + const QualifiedCharacteristic &characteristic) { + last_error_ = bt_gatt_client_unset_characteristic_value_changed_cb( + characteristic.handle()); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + return true; +} diff --git a/packages/flutter_reactive_ble/tizen/src/ble_device.h b/packages/flutter_reactive_ble/tizen/src/ble_device.h new file mode 100644 index 000000000..042c2d433 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/ble_device.h @@ -0,0 +1,127 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_BLE_DEVICE_H_ +#define FLUTTER_PLUGIN_BLE_DEVICE_H_ + +#include + +#include +#include +#include +#include +#include + +#include "qualified_characteristic.h" + +struct DiscoveredCharacteristic { + Uuid characteristic_id; + Uuid service_id; + bool is_readable = false; + bool is_writable_with_response = false; + bool is_writable_without_response = false; + bool is_notifiable = false; + bool is_indicatable = false; +}; + +struct DiscoveredService { + Uuid service_id; + std::vector characteristic_ids; + std::vector characteristics; + std::vector included_services; +}; + +typedef std::function VoidCallback; + +typedef std::function + ErrorCallback; + +typedef std::function MtuNegotiationCallback; + +typedef std::function& bytes)> + NotificationCallback; + +// A wrapper around the bt_gatt_client module. +class BleDevice { + public: + explicit BleDevice(const std::string& device_id); + ~BleDevice(); + + // Discovers services adverstised by this device. + std::vector DiscoverServices(); + + // Requests a specific MTU for this device. + bool NegotiateMtuSize(int32_t request_mtu, MtuNegotiationCallback on_done, + ErrorCallback on_error); + + // Reads a value from the characteristic. + bool ReadCharacteristic(const QualifiedCharacteristic& characteristic, + VoidCallback on_done, ErrorCallback on_error); + + // Writes a value to the characteristic. + bool WriteCharacteristic(const QualifiedCharacteristic& characteristic, + const std::vector& value, + VoidCallback on_done, ErrorCallback on_error); + + // Subscribes to updates from the characteristic. + bool ListenNotifications(const QualifiedCharacteristic& characteristic); + + // Unsubscribes from the characteristic. + bool StopNotifications(const QualifiedCharacteristic& characteristic); + + void SetNotificationCallback(NotificationCallback on_event) { + notification_callback_ = std::move(on_event); + } + + std::shared_ptr FindCharacteristicById( + const Uuid& service_id, const Uuid& characteristic_id) { + for (const auto& characteristic : characteristics_) { + if (service_id == characteristic->service_id() && + characteristic_id == characteristic->characteristic_id()) { + return characteristic; + } + } + return RegisterCharacteristic(service_id, characteristic_id); + } + + std::string device_id() const { return device_id_; } + + bt_gatt_client_h handle() const { return handle_; } + + int GetLastError() { return last_error_; } + + std::string GetLastErrorString() { return get_error_message(last_error_); } + + private: + std::shared_ptr RegisterCharacteristic( + const Uuid& service_id, const Uuid& characteristic_id) { + auto characteristic = std::make_shared( + this, service_id, characteristic_id); + if (!characteristic->handle()) { + return nullptr; + } + return characteristics_.emplace_back(std::move(characteristic)); + } + + std::shared_ptr FindCharacteristic( + bt_gatt_h handle) { + for (const auto& characteristic : characteristics_) { + if (handle == characteristic->handle()) { + return characteristic; + } + } + return nullptr; + } + + std::string device_id_; + bt_gatt_client_h handle_ = nullptr; + + std::vector> characteristics_; + NotificationCallback notification_callback_; + + int last_error_ = BT_ERROR_NONE; +}; + +#endif // FLUTTER_PLUGIN_BLE_DEVICE_H_ diff --git a/packages/flutter_reactive_ble/tizen/src/ble_tizen.cc b/packages/flutter_reactive_ble/tizen/src/ble_tizen.cc new file mode 100644 index 000000000..98a081d33 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/ble_tizen.cc @@ -0,0 +1,295 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ble_tizen.h" + +#include + +#include "log.h" + +bool BleTizen::Initialize() { + last_error_ = bt_initialize(); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + return true; +} + +bool BleTizen::Deinitialize() { + last_error_ = bt_deinitialize(); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + return true; +} + +BleStatus BleTizen::GetBleStatus() { + bt_adapter_state_e adapter_state; + last_error_ = bt_adapter_get_state(&adapter_state); + if (last_error_ == BT_ERROR_NONE) { + return adapter_state == BT_ADAPTER_ENABLED ? BleStatus::kEnabled + : BleStatus::kDisabled; + } else if (last_error_ == BT_ERROR_NOT_SUPPORTED) { + return BleStatus::kNotSupported; + } + return BleStatus::kUnknown; +} + +bool BleTizen::SetBleStatusChangeCallback(BleStatusChangeCallback callback) { + last_error_ = bt_adapter_set_state_changed_cb( + [](int result, bt_adapter_state_e adapter_state, void *user_data) { + if (result != BT_ERROR_NONE) { + LOG_ERROR("The operation failed unexpectedly: %s", + get_error_message(result)); + return; + } + BleTizen *self = static_cast(user_data); + if (self->ble_status_change_callback_) { + BleStatus status = adapter_state == BT_ADAPTER_ENABLED + ? BleStatus::kEnabled + : BleStatus::kDisabled; + self->ble_status_change_callback_(status); + } + }, + this); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + ble_status_change_callback_ = std::move(callback); + return true; +} + +bool BleTizen::UnsetBleStatusChangeCallback() { + last_error_ = bt_adapter_unset_state_changed_cb(); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + ble_status_change_callback_ = nullptr; + return true; +} + +bool BleTizen::Scan(const std::vector &service_ids) { + if (is_scanning_) { + StopScan(); + } + + for (const Uuid &service_id : service_ids) { + ScanFilter filter = ScanFilter(service_id); + if (filter.Register()) { + filters_.emplace_back(std::move(filter)); + } else { + LOG_ERROR("Ignoring scan filter: %s", service_id.c_str()); + } + } + + last_error_ = bt_adapter_le_start_scan( + [](int result, bt_adapter_le_device_scan_result_info_s *info, + void *user_data) { + BleTizen *self = static_cast(user_data); + if (result != BT_ERROR_NONE) { + LOG_ERROR("The scan operation resulted in an error: %s", + get_error_message(result)); + if (self->device_scan_error_callback_) { + self->device_scan_error_callback_(result, + get_error_message(result)); + } + return; + } + + DiscoveredDevice device = {}; + device.device_id = std::string(info->remote_address); + device.rssi = info->rssi; + + auto iter = self->cached_device_names_.find(device.device_id); + if (iter != self->cached_device_names_.end()) { + device.name = iter->second; + } else { + char *name = nullptr; + int ret = bt_adapter_le_get_scan_result_device_name( + info, BT_ADAPTER_LE_PACKET_SCAN_RESPONSE, &name); + if (ret == BT_ERROR_NO_DATA) { + // Retrying with a different packet type. + ret = bt_adapter_le_get_scan_result_device_name( + info, BT_ADAPTER_LE_PACKET_ADVERTISING, &name); + } + if (ret == BT_ERROR_NONE) { + device.name = std::string(name); + free(name); + } else if (ret != BT_ERROR_NO_DATA) { + LOG_ERROR("Could not obtain name info from the device %s.", + info->remote_address); + if (self->device_scan_error_callback_) { + self->device_scan_error_callback_(ret, get_error_message(ret)); + } + return; + } + if (!device.name.empty()) { + self->cached_device_names_[device.device_id] = device.name; + } + } + + int manufacturer_id = 0; + char *manufacturer_data = nullptr; + int manufacturer_data_len = 0; + int ret = bt_adapter_le_get_scan_result_manufacturer_data( + info, BT_ADAPTER_LE_PACKET_SCAN_RESPONSE, &manufacturer_id, + &manufacturer_data, &manufacturer_data_len); + if (ret == BT_ERROR_NONE) { + device.manufacturer_data = std::vector( + manufacturer_data, manufacturer_data + manufacturer_data_len); + free(manufacturer_data); + } else if (ret != BT_ERROR_NO_DATA) { + LOG_ERROR("Could not obtain manufacturer info from the device %s.", + info->remote_address); + if (self->device_scan_error_callback_) { + self->device_scan_error_callback_(ret, get_error_message(ret)); + } + return; + } + + char **service_ids = nullptr; + int service_count = 0; + ret = bt_adapter_le_get_scan_result_service_uuids( + info, BT_ADAPTER_LE_PACKET_SCAN_RESPONSE, &service_ids, + &service_count); + if (ret == BT_ERROR_NONE) { + for (int i = 0; i < service_count; ++i) { + device.service_ids.emplace_back(std::string(service_ids[i])); + free(service_ids[i]); + } + free(service_ids); + } else if (ret != BT_ERROR_NO_DATA) { + LOG_ERROR("Could not obtain service UUIDs from the device %s.", + info->remote_address); + if (self->device_scan_error_callback_) { + self->device_scan_error_callback_(ret, get_error_message(ret)); + } + return; + } + + bt_adapter_le_service_data_s *service_data = nullptr; + int service_data_count = 0; + ret = bt_adapter_le_get_scan_result_service_data_list( + info, BT_ADAPTER_LE_PACKET_SCAN_RESPONSE, &service_data, + &service_data_count); + if (ret == BT_ERROR_NONE) { + for (int i = 0; i < service_data_count; ++i) { + std::string service_id = service_data[i].service_uuid; + device.service_data[service_id] = + std::vector(service_data[i].service_data, + service_data[i].service_data + + service_data[i].service_data_len); + } + bt_adapter_le_free_service_data_list(service_data, + service_data_count); + } else if (ret != BT_ERROR_NO_DATA) { + LOG_ERROR("Could not obtain service data from the device %s.", + info->remote_address); + if (self->device_scan_error_callback_) { + self->device_scan_error_callback_(ret, get_error_message(ret)); + } + return; + } + + if (self->device_scan_callback_) { + self->device_scan_callback_(device); + } + }, + this); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + + is_scanning_ = true; + return true; +} + +bool BleTizen::StopScan() { + if (!is_scanning_) { + return true; + } + + last_error_ = bt_adapter_le_stop_scan(); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + ScanFilter::UnregisterAll(); + filters_.clear(); + + is_scanning_ = false; + return true; +} + +bool BleTizen::ConnectToDevice(const std::string &device_id) { + last_error_ = bt_gatt_connect(device_id.c_str(), false); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + if (connection_state_change_callback_) { + connection_state_change_callback_(device_id, ConnectionState::kConnecting); + } + return true; +} + +bool BleTizen::DisconnectFromDevice(const std::string &device_id) { + last_error_ = bt_gatt_disconnect(device_id.c_str()); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + if (connection_state_change_callback_) { + connection_state_change_callback_(device_id, + ConnectionState::kDisconnecting); + } + return true; +} + +bool BleTizen::SetConnectionStateChangeCallback( + ConnectionStateChangeCallback on_change, ErrorCallback on_error) { + last_error_ = bt_gatt_set_connection_state_changed_cb( + [](int result, bool connected, const char *remote_address, + void *user_data) { + LOG_DEBUG( + "Connection state changed: device[%s], connected[%d], result[%s]", + remote_address, connected, get_error_message(result)); + BleTizen *self = static_cast(user_data); + if (remote_address) { + if (result != BT_ERROR_NONE) { + LOG_ERROR("The connection request for device %s failed: %s", + remote_address, get_error_message(result)); + } + std::string device_id = std::string(remote_address); + bool success = + result == BT_ERROR_NONE || result == BT_ERROR_ALREADY_DONE; + ConnectionState state = success && connected + ? ConnectionState::kConnected + : ConnectionState::kDisconnected; + if (self->connection_state_change_callback_) { + self->connection_state_change_callback_(device_id, state); + } + } else { + LOG_ERROR("The connection request failed: %s", + get_error_message(result)); + if (self->connection_error_callback_) { + self->connection_error_callback_(result, get_error_message(result)); + } + } + }, + this); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + connection_state_change_callback_ = std::move(on_change); + connection_error_callback_ = std::move(on_error); + return true; +} + +bool BleTizen::UnsetConnectionStateChangeCallback() { + last_error_ = bt_gatt_unset_connection_state_changed_cb(); + if (last_error_ != BT_ERROR_NONE) { + return false; + } + connection_state_change_callback_ = nullptr; + connection_error_callback_ = nullptr; + return true; +} diff --git a/packages/flutter_reactive_ble/tizen/src/ble_tizen.h b/packages/flutter_reactive_ble/tizen/src/ble_tizen.h new file mode 100644 index 000000000..16b433ba7 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/ble_tizen.h @@ -0,0 +1,129 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_BLE_TIZEN_H_ +#define FLUTTER_PLUGIN_BLE_TIZEN_H_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "ble_device.h" +#include "scan_filter.h" + +struct DiscoveredDevice { + std::string device_id; + std::string name; + std::vector manufacturer_data; + std::vector service_ids; + std::map> service_data; + int32_t rssi = 0; +}; + +enum class BleStatus { + kUnknown, + kEnabled, + kDisabled, + kNotSupported, +}; + +enum class ConnectionState { + kConnecting, + kConnected, + kDisconnecting, + kDisconnected, +}; + +typedef std::function BleStatusChangeCallback; + +typedef std::function DeviceScanCallback; + +typedef std::function + ConnectionStateChangeCallback; + +// A wrapper around the bt_gatt module. +class BleTizen { + public: + explicit BleTizen() {} + ~BleTizen() = default; + + // Initializes the BLE service. + bool Initialize(); + + // Deinitializes the BLE service. + bool Deinitialize(); + + // Returns the current BLE adapter status. + BleStatus GetBleStatus(); + + bool SetBleStatusChangeCallback(BleStatusChangeCallback callback); + + bool UnsetBleStatusChangeCallback(); + + // Starts scanning for devices with optional |service_ids|. + bool Scan(const std::vector& service_ids); + + // Stops scanning for devices. + bool StopScan(); + + void SetDeviceScanCallback(DeviceScanCallback on_done, + ErrorCallback on_error) { + device_scan_callback_ = std::move(on_done); + device_scan_error_callback_ = std::move(on_error); + } + + // Connects to a device identified by |device_id|. + bool ConnectToDevice(const std::string& device_id); + + // Disconnects from the device. + bool DisconnectFromDevice(const std::string& device_id); + + bool SetConnectionStateChangeCallback(ConnectionStateChangeCallback on_change, + ErrorCallback on_error); + + bool UnsetConnectionStateChangeCallback(); + + std::shared_ptr FindDeviceById(const std::string& device_id) { + auto iter = devices_.find(device_id); + if (iter != devices_.end()) { + return iter->second; + } + return RegisterDevice(device_id); + } + + int GetLastError() { return last_error_; } + + std::string GetLastErrorString() { return get_error_message(last_error_); } + + private: + std::shared_ptr RegisterDevice(const std::string& device_id) { + auto device = std::make_shared(device_id); + if (!device->handle()) { + return nullptr; + } + devices_[device_id] = std::move(device); + return devices_[device_id]; + } + + bool is_scanning_ = false; + + BleStatusChangeCallback ble_status_change_callback_; + DeviceScanCallback device_scan_callback_; + ErrorCallback device_scan_error_callback_; + ConnectionStateChangeCallback connection_state_change_callback_; + ErrorCallback connection_error_callback_; + + std::unordered_map> devices_; + std::unordered_map cached_device_names_; + std::vector filters_; + + int last_error_ = BT_ERROR_NONE; +}; + +#endif // FLUTTER_PLUGIN_BLE_TIZEN_H_ diff --git a/packages/flutter_reactive_ble/tizen/src/flutter_reactive_ble_tizen_plugin.cc b/packages/flutter_reactive_ble/tizen/src/flutter_reactive_ble_tizen_plugin.cc new file mode 100644 index 000000000..e33f90805 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/flutter_reactive_ble_tizen_plugin.cc @@ -0,0 +1,635 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_reactive_ble_tizen_plugin.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "ble_tizen.h" +#include "qualified_characteristic.h" + +namespace { + +typedef flutter::EventChannel FlEventChannel; +typedef flutter::EventSink FlEventSink; +typedef flutter::MethodCall FlMethodCall; +typedef flutter::MethodResult FlMethodResult; +typedef flutter::MethodChannel FlMethodChannel; +typedef flutter::StreamHandler FlStreamHandler; +typedef flutter::StreamHandlerError + FlStreamHandlerError; + +// String key constants for method call arguments and results. +// Keep these in sync with those in `models.dart`. +constexpr char kDeviceId[] = "device_id"; +constexpr char kDeviceName[] = "device_name"; +constexpr char kServiceIds[] = "service_ids"; +constexpr char kServiceData[] = "service_data"; +constexpr char kManufacturerData[] = "manufacturer_data"; +constexpr char kRssi[] = "rssi"; +constexpr char kConnectionState[] = "connection_state"; +constexpr char kCharacteristicId[] = "characteristic_id"; +constexpr char kServiceId[] = "service_id"; +constexpr char kIsReadable[] = "is_readable"; +constexpr char kIsWritableWithResponse[] = "is_writable_with_response"; +constexpr char kIsWritableWithoutResponse[] = "is_writable_without_response"; +constexpr char kIsNotifiable[] = "is_notifiable"; +constexpr char kIsIndicatable[] = "is_indicatable"; +constexpr char kIncludedServices[] = "included_services"; +constexpr char kCharacteristicIds[] = "characteristic_ids"; +constexpr char kCharacteristics[] = "characteristics"; +constexpr char kQualifiedCharacteristic[] = "qualified_characteristic"; +constexpr char kResult[] = "result"; +constexpr char kMtu[] = "mtu"; +constexpr char kValue[] = "value"; + +std::string MissingArgumentError(const char *name) { + std::stringstream stream; + stream << "Argument " << name << " not provided, or not of expected type."; + return stream.str(); +} + +template +bool GetValueFromEncodableMap(const flutter::EncodableMap *map, const char *key, + T &out) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto *value = std::get_if(&iter->second)) { + out = *value; + return true; + } + } + return false; +} + +// Packs a DiscoveredService into an EncodableValue recursively. +flutter::EncodableValue ServiceToEncodableValue( + const DiscoveredService &service) { + flutter::EncodableList characteristic_ids; + for (const Uuid &characteristic_id : service.characteristic_ids) { + characteristic_ids.emplace_back(flutter::EncodableValue(characteristic_id)); + } + flutter::EncodableList included_services; + for (const DiscoveredService &included_service : service.included_services) { + included_services.emplace_back(ServiceToEncodableValue(included_service)); + } + flutter::EncodableList characteristics; + for (const DiscoveredCharacteristic &characteristic : + service.characteristics) { + flutter::EncodableMap map = { + {flutter::EncodableValue(kCharacteristicId), + flutter::EncodableValue(characteristic.characteristic_id)}, + {flutter::EncodableValue(kServiceId), + flutter::EncodableValue(characteristic.service_id)}, + {flutter::EncodableValue(kIsReadable), + flutter::EncodableValue(characteristic.is_readable)}, + {flutter::EncodableValue(kIsWritableWithResponse), + flutter::EncodableValue(characteristic.is_writable_with_response)}, + {flutter::EncodableValue(kIsWritableWithoutResponse), + flutter::EncodableValue(characteristic.is_writable_without_response)}, + {flutter::EncodableValue(kIsNotifiable), + flutter::EncodableValue(characteristic.is_notifiable)}, + {flutter::EncodableValue(kIsIndicatable), + flutter::EncodableValue(characteristic.is_indicatable)}, + }; + characteristics.emplace_back(flutter::EncodableValue(map)); + } + flutter::EncodableMap map = { + {flutter::EncodableValue(kServiceId), + flutter::EncodableValue(service.service_id)}, + {flutter::EncodableValue(kCharacteristicIds), + flutter::EncodableValue(characteristic_ids)}, + {flutter::EncodableValue(kIncludedServices), + flutter::EncodableValue(included_services)}, + {flutter::EncodableValue(kCharacteristics), + flutter::EncodableValue(characteristics)}, + }; + return flutter::EncodableValue(map); +} + +// A callback shared by subscribeToNotifications and readCharacteristic. +NotificationCallback notification_callback_; + +class BleCharUpdateStreamHandler : public FlStreamHandler { + public: + explicit BleCharUpdateStreamHandler(std::shared_ptr ble_tizen) + : ble_tizen_(ble_tizen){}; + + protected: + std::unique_ptr OnListenInternal( + const flutter::EncodableValue *arguments, + std::unique_ptr &&events) override { + events_ = std::move(events); + notification_callback_ = [this]( + const QualifiedCharacteristic &characteristic, + const std::vector &data) { + flutter::EncodableMap characteristic_map = { + {flutter::EncodableValue(kDeviceId), + flutter::EncodableValue(characteristic.device_id())}, + {flutter::EncodableValue(kServiceId), + flutter::EncodableValue(characteristic.service_id())}, + {flutter::EncodableValue(kCharacteristicId), + flutter::EncodableValue(characteristic.characteristic_id())}, + }; + flutter::EncodableMap map = { + {flutter::EncodableValue(kQualifiedCharacteristic), + flutter::EncodableValue(characteristic_map)}, + {flutter::EncodableValue(kResult), + flutter::EncodableValue(flutter::EncodableValue(data))}, + }; + events_->Success(flutter::EncodableValue(map)); + }; + return nullptr; + } + + std::unique_ptr OnCancelInternal( + const flutter::EncodableValue *arguments) override { + notification_callback_ = nullptr; + events_.reset(); + return nullptr; + } + + private: + std::shared_ptr ble_tizen_; + std::unique_ptr events_; +}; + +class ConnectionUpdateStreamHandler : public FlStreamHandler { + public: + explicit ConnectionUpdateStreamHandler(std::shared_ptr ble_tizen) + : ble_tizen_(ble_tizen){}; + + protected: + std::unique_ptr OnListenInternal( + const flutter::EncodableValue *arguments, + std::unique_ptr &&events) override { + events_ = std::move(events); + ble_tizen_->SetConnectionStateChangeCallback( + [this](const std::string &device_id, ConnectionState state) { + std::string connection_state = "disconnected"; + if (state == ConnectionState::kConnecting) { + connection_state = "connecting"; + } else if (state == ConnectionState::kConnected) { + connection_state = "connected"; + } else if (state == ConnectionState::kDisconnecting) { + connection_state = "disconnecting"; + } + flutter::EncodableMap map = { + {flutter::EncodableValue(kDeviceId), + flutter::EncodableValue(device_id)}, + {flutter::EncodableValue(kConnectionState), + flutter::EncodableValue(connection_state)}, + }; + events_->Success(flutter::EncodableValue(map)); + }, + [this](int error_code, const std::string &error_message) { + events_->Error(std::to_string(error_code), error_message); + }); + return nullptr; + } + + std::unique_ptr OnCancelInternal( + const flutter::EncodableValue *arguments) override { + ble_tizen_->UnsetConnectionStateChangeCallback(); + events_.reset(); + return nullptr; + } + + private: + std::shared_ptr ble_tizen_; + std::unique_ptr events_; +}; + +class BleScanStreamHandler : public FlStreamHandler { + public: + explicit BleScanStreamHandler(std::shared_ptr ble_tizen) + : ble_tizen_(ble_tizen){}; + + protected: + std::unique_ptr OnListenInternal( + const flutter::EncodableValue *arguments, + std::unique_ptr &&events) override { + events_ = std::move(events); + ble_tizen_->SetDeviceScanCallback( + [this](const DiscoveredDevice &device) { + flutter::EncodableList service_ids; + for (const Uuid &service_id : device.service_ids) { + service_ids.emplace_back(flutter::EncodableValue(service_id)); + } + flutter::EncodableMap service_data; + for (const auto &[service_id, data] : device.service_data) { + service_data[flutter::EncodableValue(service_id)] = + flutter::EncodableValue(data); + } + flutter::EncodableMap map = { + {flutter::EncodableValue(kDeviceId), + flutter::EncodableValue(device.device_id)}, + {flutter::EncodableValue(kDeviceName), + flutter::EncodableValue(device.name)}, + {flutter::EncodableValue(kManufacturerData), + flutter::EncodableValue(device.manufacturer_data)}, + {flutter::EncodableValue(kServiceIds), + flutter::EncodableValue(service_ids)}, + {flutter::EncodableValue(kServiceData), + flutter::EncodableValue(service_data)}, + {flutter::EncodableValue(kRssi), + flutter::EncodableValue(device.rssi)}, + }; + events_->Success(flutter::EncodableValue(map)); + }, + [this](int error_code, const std::string &error_message) { + events_->Error(std::to_string(error_code), error_message); + }); + return nullptr; + } + + std::unique_ptr OnCancelInternal( + const flutter::EncodableValue *arguments) override { + ble_tizen_->SetDeviceScanCallback(nullptr, nullptr); + ble_tizen_->StopScan(); + events_.reset(); + return nullptr; + } + + private: + std::shared_ptr ble_tizen_; + std::unique_ptr events_; +}; + +class BleStatusStreamHandler : public FlStreamHandler { + public: + explicit BleStatusStreamHandler(std::shared_ptr ble_tizen) + : ble_tizen_(ble_tizen){}; + + protected: + std::unique_ptr OnListenInternal( + const flutter::EncodableValue *arguments, + std::unique_ptr &&events) override { + events_ = std::move(events); + + BleStatus status = ble_tizen_->GetBleStatus(); + std::string ble_status = "unknown"; + if (status == BleStatus::kEnabled) { + ble_status = "ready"; + } else if (status == BleStatus::kDisabled) { + ble_status = "poweredOff"; + } else if (status == BleStatus::kNotSupported) { + ble_status = "unsupported"; + } + events_->Success(flutter::EncodableValue(ble_status)); + + ble_tizen_->SetBleStatusChangeCallback([this](BleStatus status) { + std::string ble_status = + status == BleStatus::kEnabled ? "ready" : "poweredOff"; + events_->Success(flutter::EncodableValue(ble_status)); + }); + return nullptr; + } + + std::unique_ptr OnCancelInternal( + const flutter::EncodableValue *arguments) override { + ble_tizen_->UnsetBleStatusChangeCallback(); + events_.reset(); + return nullptr; + } + + private: + std::shared_ptr ble_tizen_; + std::unique_ptr events_; +}; + +class FlutterReactiveBleTizenPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar *registrar) { + auto plugin = std::make_unique(); + + auto method_channel = std::make_unique( + registrar->messenger(), "flutter_reactive_ble_method", + &flutter::StandardMethodCodec::GetInstance()); + method_channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto &call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + plugin->SetUpEventChannels(registrar); + + registrar->AddPlugin(std::move(plugin)); + } + + FlutterReactiveBleTizenPlugin() : ble_tizen_(std::make_shared()) {} + + virtual ~FlutterReactiveBleTizenPlugin() {} + + private: + void SetUpEventChannels(flutter::PluginRegistrar *registrar) { + ble_status_channel_ = std::make_unique( + registrar->messenger(), "flutter_reactive_ble_status", + &flutter::StandardMethodCodec::GetInstance()); + ble_status_channel_->SetStreamHandler( + std::make_unique(ble_tizen_)); + + scan_event_channel_ = std::make_unique( + registrar->messenger(), "flutter_reactive_ble_scan", + &flutter::StandardMethodCodec::GetInstance()); + scan_event_channel_->SetStreamHandler( + std::make_unique(ble_tizen_)); + + connected_device_channel_ = std::make_unique( + registrar->messenger(), "flutter_reactive_ble_connected_device", + &flutter::StandardMethodCodec::GetInstance()); + connected_device_channel_->SetStreamHandler( + std::make_unique(ble_tizen_)); + + char_event_channel_ = std::make_unique( + registrar->messenger(), "flutter_reactive_ble_char_update", + &flutter::StandardMethodCodec::GetInstance()); + char_event_channel_->SetStreamHandler( + std::make_unique(ble_tizen_)); + } + + void HandleMethodCall(const FlMethodCall &method_call, + std::unique_ptr result) { + const auto &method_name = method_call.method_name(); + + if (method_name == "initialize") { + if (!ble_tizen_->Initialize()) { + result->Error(std::to_string(ble_tizen_->GetLastError()), + ble_tizen_->GetLastErrorString()); + return; + }; + result->Success(); + } else if (method_name == "deinitialize") { + if (!ble_tizen_->Deinitialize()) { + result->Error(std::to_string(ble_tizen_->GetLastError()), + ble_tizen_->GetLastErrorString()); + return; + }; + result->Success(); + } else if (method_name == "scanForDevices") { + const auto *arguments = + std::get_if(method_call.arguments()); + if (!arguments) { + result->Error("Invalid arguments", "No arguments provided."); + return; + } + + flutter::EncodableList service_id_list; + if (!GetValueFromEncodableMap(arguments, kServiceIds, service_id_list)) { + result->Error("Invalid arguments", MissingArgumentError(kServiceIds)); + return; + } + std::vector service_ids; + for (const flutter::EncodableValue &service_id : service_id_list) { + if (std::holds_alternative(service_id)) { + service_ids.emplace_back(std::get(service_id)); + } + } + + if (!ble_tizen_->Scan(service_ids)) { + result->Error(std::to_string(ble_tizen_->GetLastError()), + ble_tizen_->GetLastErrorString()); + return; + }; + result->Success(); + } else if (method_name == "connectToDevice" || + method_name == "disconnectDevice" || + method_name == "discoverServices" || + method_name == "requestMtuSize") { + HandleDeviceOperation(method_call, std::move(result)); + } else if (method_name == "readCharacteristic" || + method_name == "writeCharacteristicWithResponse" || + method_name == "writeCharacteristicWithoutResponse" || + method_name == "subscribeToNotifications" || + method_name == "stopSubscribingToNotifications") { + HandleCharacteristicOperation(method_call, std::move(result)); + } else { + result->NotImplemented(); + } + } + + void HandleDeviceOperation(const FlMethodCall &method_call, + std::unique_ptr result) { + const auto &method_name = method_call.method_name(); + const auto *arguments = + std::get_if(method_call.arguments()); + if (!arguments) { + result->Error("Invalid arguments", "No arguments provided."); + return; + } + + std::string device_id; + if (!GetValueFromEncodableMap(arguments, kDeviceId, device_id)) { + result->Error("Invalid arguments", MissingArgumentError(kDeviceId)); + return; + } + + if (method_name == "connectToDevice") { + if (!ble_tizen_->ConnectToDevice(device_id)) { + result->Error(std::to_string(ble_tizen_->GetLastError()), + ble_tizen_->GetLastErrorString()); + return; + }; + result->Success(); + } else if (method_name == "disconnectDevice") { + if (!ble_tizen_->DisconnectFromDevice(device_id)) { + result->Error(std::to_string(ble_tizen_->GetLastError()), + ble_tizen_->GetLastErrorString()); + return; + }; + result->Success(); + } else if (method_name == "discoverServices") { + std::shared_ptr device = ble_tizen_->FindDeviceById(device_id); + if (!device) { + result->Error("Operation failed", "Failed to register a device."); + return; + } + + flutter::EncodableList services; + for (const DiscoveredService &service : device->DiscoverServices()) { + services.emplace_back(ServiceToEncodableValue(service)); + } + result->Success(flutter::EncodableValue(services)); + } else if (method_name == "requestMtuSize") { + std::shared_ptr device = ble_tizen_->FindDeviceById(device_id); + if (!device) { + result->Error("Operation failed", "Failed to register a device."); + return; + } + + int32_t request_mtu; + if (!GetValueFromEncodableMap(arguments, kMtu, request_mtu)) { + result->Error("Invalid arguments", MissingArgumentError(kMtu)); + return; + } + if (request_mtu < 23) { + result->Error("Invalid arguments", "The MTU cannot be less than 23."); + return; + } + + FlMethodResult *result_ptr = result.release(); + if (!device->NegotiateMtuSize( + request_mtu, + [result_ptr](int32_t result_mtu) { + result_ptr->Success(flutter::EncodableValue(result_mtu)); + delete result_ptr; + }, + [result_ptr](int error_code, const std::string &error_message) { + result_ptr->Error(std::to_string(error_code), error_message); + delete result_ptr; + })) { + result_ptr->Error(std::to_string(device->GetLastError()), + device->GetLastErrorString()); + delete result_ptr; + } + } + } + + void HandleCharacteristicOperation(const FlMethodCall &method_call, + std::unique_ptr result) { + const auto &method_name = method_call.method_name(); + const auto *arguments = + std::get_if(method_call.arguments()); + if (!arguments) { + result->Error("Invalid arguments", "No arguments provided."); + return; + } + + flutter::EncodableMap qualified; + if (!GetValueFromEncodableMap(arguments, kQualifiedCharacteristic, + qualified)) { + result->Error("Invalid arguments", + MissingArgumentError(kQualifiedCharacteristic)); + return; + } + std::string device_id; + if (!GetValueFromEncodableMap(&qualified, kDeviceId, device_id)) { + result->Error("Invalid arguments", MissingArgumentError(kDeviceId)); + return; + } + std::string service_id; + if (!GetValueFromEncodableMap(&qualified, kServiceId, service_id)) { + result->Error("Invalid arguments", MissingArgumentError(kServiceId)); + return; + } + std::string characteristic_id; + if (!GetValueFromEncodableMap(&qualified, kCharacteristicId, + characteristic_id)) { + result->Error("Invalid arguments", + MissingArgumentError(kCharacteristicId)); + return; + } + + std::shared_ptr device = ble_tizen_->FindDeviceById(device_id); + std::shared_ptr characteristic = + device->FindCharacteristicById(service_id, characteristic_id); + if (!characteristic) { + result->Error("Operation failed", "Failed to register a characteristic."); + return; + } + + if (method_name == "readCharacteristic") { + if (!characteristic->IsReadable()) { + result->Error("Operation failed", + "Operation not allowed on this characteristic."); + return; + } + device->SetNotificationCallback(notification_callback_); + + FlMethodResult *result_ptr = result.release(); + if (!device->ReadCharacteristic( + *characteristic, + [result_ptr]() { + result_ptr->Success(); + delete result_ptr; + }, + [result_ptr](int error_code, const std::string &error_message) { + result_ptr->Error(std::to_string(error_code), error_message); + delete result_ptr; + })) { + result_ptr->Error(std::to_string(device->GetLastError()), + device->GetLastErrorString()); + delete result_ptr; + } + } else if (method_name == "writeCharacteristicWithResponse" || + method_name == "writeCharacteristicWithoutResponse") { + bool no_response = method_name == "writeCharacteristicWithoutResponse"; + if ((no_response && !characteristic->IsWritableWithoutResponse()) || + (!no_response && !characteristic->IsWritable())) { + result->Error("Operation failed", + "Operation not allowed on this characteristic."); + return; + } + characteristic->SetWriteType(no_response); + + std::vector value; + if (!GetValueFromEncodableMap(arguments, kValue, value)) { + result->Error("Invalid arguments", MissingArgumentError(kValue)); + return; + } + + FlMethodResult *result_ptr = result.release(); + if (!device->WriteCharacteristic( + *characteristic, value, + [result_ptr, qualified]() { + flutter::EncodableMap map = { + {flutter::EncodableValue(kQualifiedCharacteristic), + flutter::EncodableValue(qualified)}, + }; + result_ptr->Success(flutter::EncodableValue(map)); + delete result_ptr; + }, + [result_ptr](int error_code, const std::string &error_message) { + result_ptr->Error(std::to_string(error_code), error_message); + delete result_ptr; + })) { + result_ptr->Error(std::to_string(device->GetLastError()), + device->GetLastErrorString()); + delete result_ptr; + } + } else if (method_name == "subscribeToNotifications") { + device->SetNotificationCallback(notification_callback_); + if (!device->ListenNotifications(*characteristic)) { + result->Error(std::to_string(device->GetLastError()), + device->GetLastErrorString()); + return; + } + result->Success(); + } else if (method_name == "stopSubscribingToNotifications") { + device->SetNotificationCallback(nullptr); + if (!device->StopNotifications(*characteristic)) { + result->Error(std::to_string(device->GetLastError()), + device->GetLastErrorString()); + return; + } + result->Success(); + } + } + + std::shared_ptr ble_tizen_; + std::unique_ptr ble_status_channel_; + std::unique_ptr scan_event_channel_; + std::unique_ptr connected_device_channel_; + std::unique_ptr char_event_channel_; +}; + +} // namespace + +void FlutterReactiveBleTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + FlutterReactiveBleTizenPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/flutter_reactive_ble/tizen/src/log.h b/packages/flutter_reactive_ble/tizen/src/log.h new file mode 100644 index 000000000..f2a6c65c2 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/log.h @@ -0,0 +1,24 @@ +#ifndef __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "FlutterReactiveBleTizenPlugin" + +#ifndef __MODULE__ +#define __MODULE__ strrchr("/" __FILE__, '/') + 1 +#endif + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __MODULE__, __func__, \ + __LINE__, ##arg) + +#define LOG_DEBUG(fmt, args...) LOG(DLOG_DEBUG, fmt, ##args) +#define LOG_INFO(fmt, args...) LOG(DLOG_INFO, fmt, ##args) +#define LOG_WARN(fmt, args...) LOG(DLOG_WARN, fmt, ##args) +#define LOG_ERROR(fmt, args...) LOG(DLOG_ERROR, fmt, ##args) + +#endif // __LOG_H__ diff --git a/packages/flutter_reactive_ble/tizen/src/qualified_characteristic.cc b/packages/flutter_reactive_ble/tizen/src/qualified_characteristic.cc new file mode 100644 index 000000000..1308f264c --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/qualified_characteristic.cc @@ -0,0 +1,51 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "qualified_characteristic.h" + +#include + +#include "ble_device.h" +#include "log.h" + +QualifiedCharacteristic::QualifiedCharacteristic(const BleDevice *device, + const Uuid &service_id, + const Uuid &characteristic_id) + : device_id_(device->device_id()), + service_id_(service_id), + characteristic_id_(characteristic_id) { + bt_gatt_h service_handle = nullptr; + int ret = bt_gatt_client_get_service(device->handle(), service_id.c_str(), + &service_handle); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Failed to create a service handle: %s", get_error_message(ret)); + return; + } + + ret = bt_gatt_service_get_characteristic(service_handle, + characteristic_id.c_str(), &handle_); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Failed to create a characteristic handle: %s", + get_error_message(ret)); + } +} + +int QualifiedCharacteristic::GetProperties() const { + int properties = 0; + int ret = bt_gatt_characteristic_get_properties(handle_, &properties); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Unable to get characteristic properties: %s", + get_error_message(ret)); + } + return properties; +} + +void QualifiedCharacteristic::SetWriteType(bool no_response) { + int ret = bt_gatt_characteristic_set_write_type( + handle_, no_response ? BT_GATT_WRITE_TYPE_WRITE_NO_RESPONSE + : BT_GATT_WRITE_TYPE_WRITE); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Failed to set write type: %s", get_error_message(ret)); + } +} diff --git a/packages/flutter_reactive_ble/tizen/src/qualified_characteristic.h b/packages/flutter_reactive_ble/tizen/src/qualified_characteristic.h new file mode 100644 index 000000000..34625e239 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/qualified_characteristic.h @@ -0,0 +1,57 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_QUALIFIED_CHARACTERISTIC_H_ +#define FLUTTER_PLUGIN_QUALIFIED_CHARACTERISTIC_H_ + +#include + +#include + +typedef std::string Uuid; + +class BleDevice; + +// A BLE characteristic characterised by device_id, service_id, and +// characteristic_id. +class QualifiedCharacteristic { + public: + explicit QualifiedCharacteristic(const BleDevice* device, + const Uuid& service_id, + const Uuid& characteristic_id); + ~QualifiedCharacteristic() = default; + + // Prevent copying. + QualifiedCharacteristic(QualifiedCharacteristic const&) = delete; + QualifiedCharacteristic& operator=(QualifiedCharacteristic const&) = delete; + + bool IsReadable() const { return GetProperties() & BT_GATT_PROPERTY_READ; } + + bool IsWritable() const { return GetProperties() & BT_GATT_PROPERTY_WRITE; } + + bool IsWritableWithoutResponse() const { + return GetProperties() & BT_GATT_PROPERTY_WRITE_WITHOUT_RESPONSE; + } + + void SetWriteType(bool no_response); + + std::string device_id() const { return device_id_; } + + Uuid service_id() const { return service_id_; } + + Uuid characteristic_id() const { return characteristic_id_; } + + bt_gatt_h handle() const { return handle_; } + + private: + int GetProperties() const; + + std::string device_id_; + Uuid service_id_; + Uuid characteristic_id_; + + bt_gatt_h handle_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_QUALIFIED_CHARACTERISTIC_H_ diff --git a/packages/flutter_reactive_ble/tizen/src/scan_filter.cc b/packages/flutter_reactive_ble/tizen/src/scan_filter.cc new file mode 100644 index 000000000..7fd9e749c --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/scan_filter.cc @@ -0,0 +1,59 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "scan_filter.h" + +#include + +#include "log.h" + +ScanFilter::ScanFilter(const Uuid &service_id) { + int ret = bt_adapter_le_scan_filter_create(&handle_); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Failed to create a scan filter handle."); + return; + } + + ret = bt_adapter_le_scan_filter_set_service_uuid(handle_, service_id.c_str()); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Failed to set a service UUID for the scan filter."); + return; + } +}; + +ScanFilter::~ScanFilter() { +#ifdef TV_PROFILE + // This call results in a crash on wearable and common devices. + if (handle_) { + bt_adapter_le_scan_filter_destroy(handle_); + } +#endif +}; + +bool ScanFilter::Register() { + int ret = bt_adapter_le_scan_filter_register(handle_); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Filter registration failed: %s", get_error_message(ret)); + return false; + } + return true; +}; + +bool ScanFilter::Unregister() { + int ret = bt_adapter_le_scan_filter_unregister(handle_); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Filter unregistration failed: %s", get_error_message(ret)); + return false; + } + return true; +}; + +bool ScanFilter::UnregisterAll() { + int ret = bt_adapter_le_scan_filter_unregister_all(); + if (ret != BT_ERROR_NONE) { + LOG_ERROR("Filter unregistration failed: %s", get_error_message(ret)); + return false; + } + return true; +} diff --git a/packages/flutter_reactive_ble/tizen/src/scan_filter.h b/packages/flutter_reactive_ble/tizen/src/scan_filter.h new file mode 100644 index 000000000..31fb13314 --- /dev/null +++ b/packages/flutter_reactive_ble/tizen/src/scan_filter.h @@ -0,0 +1,42 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_SCAN_FILTER_H_ +#define FLUTTER_PLUGIN_SCAN_FILTER_H_ + +#include + +#include + +typedef std::string Uuid; + +// A wrapper around the bt_scan_filter_h handle. +class ScanFilter { + public: + explicit ScanFilter(const Uuid &service_id); + ~ScanFilter(); + + // Prevent copying. + ScanFilter(ScanFilter const &) = delete; + ScanFilter &operator=(ScanFilter const &) = delete; + + ScanFilter(ScanFilter &&other) : handle_(other.handle_) {} + ScanFilter &operator=(ScanFilter &&other) { + handle_ = other.handle_; + return *this; + } + + bool Register(); + + bool Unregister(); + + static bool UnregisterAll(); + + bt_scan_filter_h handle() const { return handle_; } + + private: + bt_scan_filter_h handle_; +}; + +#endif // FLUTTER_PLUGIN_SCAN_FILTER_H_