├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md └── workflows │ ├── ktlint.yml │ └── main.yml ├── .gitignore ├── .swiftlint.yml ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin ├── flutter_customer_test.bat ├── flutter_customer_test.sh └── quality_checks.sh ├── example ├── LICENSE ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── signify │ │ │ │ └── hue │ │ │ │ └── reactivebleexample │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ ├── build.gradle │ ├── config │ │ └── detekt │ │ │ └── detekt.yml │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── main.dart │ └── src │ │ ├── ble │ │ ├── ble_device_connector.dart │ │ ├── ble_device_interactor.dart │ │ ├── ble_logger.dart │ │ ├── ble_scanner.dart │ │ ├── ble_status_monitor.dart │ │ └── reactive_state.dart │ │ ├── ui │ │ ├── ble_status_screen.dart │ │ ├── device_detail │ │ │ ├── characteristic_interaction_dialog.dart │ │ │ ├── device_detail_screen.dart │ │ │ ├── device_interaction_tab.dart │ │ │ ├── device_interaction_tab.g.dart │ │ │ └── device_log_tab.dart │ │ └── device_list.dart │ │ ├── utils.dart │ │ └── widgets.dart ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ │ └── MainMenu.xib │ │ ├── Configs │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ └── RunnerTests │ │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml └── test │ └── example_test.dart ├── melos.yaml ├── packages ├── flutter_reactive_ble │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── lib │ │ ├── flutter_reactive_ble.dart │ │ └── src │ │ │ ├── connected_device_operation.dart │ │ │ ├── debug_logger.dart │ │ │ ├── device_connector.dart │ │ │ ├── device_scanner.dart │ │ │ ├── discovered_devices_registry.dart │ │ │ ├── reactive_ble.dart │ │ │ └── rx_ext │ │ │ ├── repeater.dart │ │ │ └── serial_disposable.dart │ ├── pubspec.lock │ ├── pubspec.yaml │ └── test │ │ ├── connected_device_operation_test.dart │ │ ├── connected_device_operation_test.mocks.dart │ │ ├── device_connector_test.dart │ │ ├── device_connector_test.mocks.dart │ │ ├── device_scanner_test.dart │ │ ├── device_scanner_test.mocks.dart │ │ ├── reactive_ble_test.dart │ │ ├── reactive_ble_test.mocks.dart │ │ └── rx_ext │ │ └── serial_disposable_test.dart ├── reactive_ble_mobile │ ├── .metadata │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── android │ │ ├── build.gradle │ │ ├── proguard-rules.txt │ │ ├── settings.gradle │ │ └── src │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── signify │ │ │ │ └── hue │ │ │ │ └── flutterreactiveble │ │ │ │ ├── PluginController.kt │ │ │ │ ├── ReactiveBlePlugin.kt │ │ │ │ ├── ble │ │ │ │ ├── BleClient.kt │ │ │ │ ├── BleWrapper.kt │ │ │ │ ├── ConnectionQueue.kt │ │ │ │ ├── DeviceConnector.kt │ │ │ │ ├── ReactiveBleClient.kt │ │ │ │ └── extensions │ │ │ │ │ └── RxBleConnectionExtension.kt │ │ │ │ ├── channelhandlers │ │ │ │ ├── BleStatusHandler.kt │ │ │ │ ├── CharNotificationHandler.kt │ │ │ │ ├── DeviceConnectionHandler.kt │ │ │ │ └── ScanDevicesHandler.kt │ │ │ │ ├── converters │ │ │ │ ├── ManufacturerDataConverter.kt │ │ │ │ ├── ProtobufMessageConverter.kt │ │ │ │ └── UuidConverter.kt │ │ │ │ ├── debugutils │ │ │ │ ├── HexStringConversion.kt │ │ │ │ └── PerformanceAnalyzer.kt │ │ │ │ ├── model │ │ │ │ ├── ConnectionState.kt │ │ │ │ ├── ErrorType.kt │ │ │ │ └── ScanMode.kt │ │ │ │ └── utils │ │ │ │ ├── BleWrapperExtensions.kt │ │ │ │ ├── Discard.kt │ │ │ │ └── Duration.kt │ │ │ └── test │ │ │ └── kotlin │ │ │ └── com │ │ │ └── signify │ │ │ └── hue │ │ │ └── flutterreactiveble │ │ │ ├── ble │ │ │ ├── DeviceConnectorTest.kt │ │ │ └── ReactiveBleClientTest.kt │ │ │ ├── channelhandlers │ │ │ └── ConnectionQueueTest.kt │ │ │ └── converters │ │ │ ├── ProtobufMessageConverterTest.kt │ │ │ ├── ServicesWithCharacteristicsConverterTest.kt │ │ │ └── UuidConverterTest.kt │ ├── darwin │ │ ├── .gitignore │ │ ├── Assets │ │ │ └── .gitkeep │ │ ├── Classes │ │ │ ├── BleData extras │ │ │ │ ├── BLEStatus.swift │ │ │ │ ├── CharacteristicInstance.swift │ │ │ │ ├── ConnectionState.swift │ │ │ │ └── FailureCodes.swift │ │ │ ├── BleData │ │ │ │ └── bledata.pb.swift │ │ │ ├── Plugin │ │ │ │ ├── Common │ │ │ │ │ ├── EventSink.swift │ │ │ │ │ ├── Failable.swift │ │ │ │ │ ├── MethodHandler.swift │ │ │ │ │ ├── PlatformMethod.swift │ │ │ │ │ └── StreamHandler.swift │ │ │ │ ├── PluginController.swift │ │ │ │ ├── PluginError.swift │ │ │ │ ├── StreamingTask.swift │ │ │ │ └── SwiftReactiveBlePlugin.swift │ │ │ ├── Prelude │ │ │ │ ├── CoreBluetooth+Extensions.swift │ │ │ │ ├── base.swift │ │ │ │ └── papply.swift │ │ │ ├── ReactiveBle │ │ │ │ ├── Central.swift │ │ │ │ ├── CentralManagerDelegate.swift │ │ │ │ ├── OnOff.swift │ │ │ │ ├── PeripheralDelegate.swift │ │ │ │ └── Tasks │ │ │ │ │ ├── CharacteristicNotify │ │ │ │ │ ├── CharacteristicNotifyTaskController.swift │ │ │ │ │ └── CharacteristicNotifyTaskSpec.swift │ │ │ │ │ ├── CharacteristicWrite │ │ │ │ │ ├── CharacteristicWriteTaskController.swift │ │ │ │ │ └── CharacteristicWriteTaskSpec.swift │ │ │ │ │ ├── Connect │ │ │ │ │ ├── ConnectTaskController.swift │ │ │ │ │ └── ConnectTaskSpec.swift │ │ │ │ │ ├── PeripheralTaskRegistry │ │ │ │ │ ├── PeripheralTask.swift │ │ │ │ │ ├── PeripheralTaskController.swift │ │ │ │ │ └── PeripheralTaskRegistry.swift │ │ │ │ │ ├── ReadRssi │ │ │ │ │ ├── ReadRssiTaskController.swift │ │ │ │ │ └── ReadRssiTaskSpec.swift │ │ │ │ │ └── ServicesWithCharacteristicsDiscovery │ │ │ │ │ ├── ServicesWithCharacteristicsDiscoveryTaskController.swift │ │ │ │ │ ├── ServicesWithCharacteristicsDiscoveryTaskSpec.swift │ │ │ │ │ └── ServicesWithCharacteristicsToDiscover.swift │ │ │ ├── ReactiveBlePlugin.h │ │ │ └── ReactiveBlePlugin.m │ │ └── reactive_ble_mobile.podspec │ ├── lib │ │ ├── reactive_ble_mobile.dart │ │ └── src │ │ │ ├── converter │ │ │ ├── args_to_protubuf_converter.dart │ │ │ └── protobuf_converter.dart │ │ │ ├── generated │ │ │ ├── bledata.pb.dart │ │ │ ├── bledata.pbenum.dart │ │ │ ├── bledata.pbjson.dart │ │ │ └── bledata.pbserver.dart │ │ │ ├── reactive_ble_mobile_platform.dart │ │ │ └── select_from.dart │ ├── protos │ │ ├── README.md │ │ └── bledata.proto │ ├── pubspec.lock │ ├── pubspec.yaml │ └── test │ │ ├── converter │ │ ├── args_to_protobuf_converter_test.dart │ │ └── protobuf_converter_test.dart │ │ ├── reactive_ble_platform_test.dart │ │ └── reactive_ble_platform_test.mocks.dart └── reactive_ble_platform_interface │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── lib │ ├── reactive_ble_platform_interface.dart │ └── src │ │ ├── logger.dart │ │ ├── model │ │ ├── ble_status.dart │ │ ├── characteristic_instance.dart │ │ ├── characteristic_value.dart │ │ ├── clear_gatt_cache_error.dart │ │ ├── connection_priority.dart │ │ ├── connection_state_update.dart │ │ ├── connection_state_update.g.dart │ │ ├── discovered_characteristic.dart │ │ ├── discovered_device.dart │ │ ├── discovered_device.g.dart │ │ ├── discovered_service.dart │ │ ├── discovered_service.g.dart │ │ ├── generic_failure.dart │ │ ├── log_level.dart │ │ ├── qualified_characteristic.dart │ │ ├── result.dart │ │ ├── scan_mode.dart │ │ ├── scan_session.dart │ │ ├── unit.dart │ │ ├── uuid.dart │ │ └── write_characteristic_info.dart │ │ ├── models.dart │ │ ├── reactive_ble_platform_interface.dart │ │ └── select_from.dart │ ├── pubspec.lock │ ├── pubspec.yaml │ └── test │ └── select_from_test.dart ├── pubspec.lock └── pubspec.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ktlint_standard_property-naming=disabled 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Connect to one / many devices using `connectTo()` / `connectToAdvertisingDevice()` 16 | 2. Read from / write to / subscribe to a protected / non-protected characteristic 17 | 3. Observe a failure with exception (including the part of the stack trace, belonging to this package) … 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | - [ ] I tried doing the same with a general BLE scanner application (e.g. [nRF Connect](https://duckduckgo.com/?q=nRF+connect)) and it exhibits the expected behavior as described above 23 | 24 | **Smartphone / tablet** 25 | - Device: [e.g. iPhone X] 26 | - OS: [e.g. iOS 13, Android 10] 27 | - Package version: [e.g. 1.0.0] 28 | 29 | **Peripheral device** 30 | - Vendor, model: [e.g. Fitbit Versa, iPhone SE, or CUSTOM] 31 | - Does it run a custom firmware / software: yes / no 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? (please describe)** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ktlint.yml: -------------------------------------------------------------------------------- 1 | name: ktlint 2 | on: [pull_request] 3 | jobs: 4 | ktlint: 5 | name: Check Code Quality 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Clone repo 10 | uses: actions/checkout@master 11 | with: 12 | fetch-depth: 1 13 | - name: ktlint 14 | uses: ScaCap/action-ktlint@master 15 | with: 16 | github_token: ${{ secrets.github_token }} 17 | reporter: github-pr-review # Change reporter 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Set Up Java 19 | uses: actions/setup-java@v3.12.0 20 | with: 21 | distribution: 'oracle' 22 | java-version: '19.0.2' 23 | 24 | - name: Set Up Flutter 25 | uses: subosito/flutter-action@v2 26 | with: 27 | flutter-version: '3.16.3' 28 | channel: 'stable' 29 | 30 | - name: Set up debug keystore 31 | run: | 32 | rm -f ~/.android/debug.keystore 33 | keytool -genkeypair \ 34 | -alias androiddebugkey \ 35 | -keypass android \ 36 | -keystore ~/.android/debug.keystore \ 37 | -storepass android \ 38 | -dname 'CN=Android Debug,O=Android,C=US' \ 39 | -keyalg 'RSA' \ 40 | -keysize 2048 \ 41 | -validity 10000 42 | 43 | - name: Quality checks monorepo 44 | run: | 45 | ./bin/quality_checks.sh 46 | 47 | - name: Build Android app & run native tests 48 | run: | 49 | #!/bin/bash -ex 50 | cd example 51 | flutter build apk --debug 52 | 53 | cd android 54 | ./gradlew detekt 55 | ./gradlew testDebugUnitTest 56 | 57 | - name: Build iOS app 58 | run: | 59 | find . -name "Podfile" -execdir pod install \; 60 | cd example && flutter build ios --debug --no-codesign 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | .vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | .flutter-plugins-dependencies 32 | Flutter.podspec 33 | # Android related 34 | .gradle/ 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/Flutter/flutter_export_environment.sh 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | 74 | pubspec_overrides.yaml 75 | .ruby-version -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - cyclomatic_complexity 3 | - identifier_name 4 | - multiple_closures_with_trailing_closure 5 | - nesting 6 | - type_name 7 | - unused_closure_parameter 8 | 9 | excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included` 10 | - Pods 11 | - "**/*.pb.swift" # exclude files with a wildcard 12 | 13 | line_length: 300 14 | type_body_length: 15 | - 600 16 | 17 | function_body_length: 18 | - 150 19 | 20 | file_length: 21 | warning: 800 22 | 23 | reporter: "xcode" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD license 2 | 3 | ©2019 Signify Holding. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 9 | and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /bin/flutter_customer_test.bat: -------------------------------------------------------------------------------- 1 | PUSHD packages\flutter_reactive_ble 2 | CALL flutter analyze --no-fatal-infos || GOTO :END 3 | CALL flutter test || GOTO :END 4 | POPD 5 | 6 | PUSHD packages\reactive_ble_mobile 7 | CALL flutter analyze --no-fatal-infos || GOTO :END 8 | CALL flutter test || GOTO :END 9 | POPD 10 | 11 | PUSHD packages\reactive_ble_platform_interface 12 | CALL flutter analyze --no-fatal-infos || GOTO :END 13 | CALL flutter test || GOTO :END 14 | POPD 15 | 16 | @ECHO. 17 | @ECHO. 18 | @ECHO Testing complete. 19 | GOTO :EOF 20 | 21 | :END 22 | @ECHO. 23 | @ECHO. 24 | @ECHO Testing failed. 25 | EXIT /B 1 26 | -------------------------------------------------------------------------------- /bin/flutter_customer_test.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | (cd packages/flutter_reactive_ble; flutter analyze --no-fatal-infos; flutter test) 4 | (cd packages/reactive_ble_mobile; flutter analyze --no-fatal-infos; flutter test) 5 | (cd packages/reactive_ble_platform_interface; flutter analyze --no-fatal-infos; flutter test) 6 | -------------------------------------------------------------------------------- /bin/quality_checks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | export PATH="$PATH:$FLUTTER_ROOT/.pub-cache/bin" 3 | export PATH="$PATH:$FLUTTER_ROOT/bin" 4 | 5 | dart pub global activate melos 6 | 7 | melos bootstrap 8 | melos run analyze 9 | melos run unittest 10 | -------------------------------------------------------------------------------- /example/LICENSE: -------------------------------------------------------------------------------- 1 | BSD license 2 | 3 | ©2019 Signify Holding. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 9 | and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_reactive_ble 2 | 3 | Demonstrates how to use the `flutter_reactive_ble` plugin. 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](http://flutter.dev/). -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | compileSdkVersion 33 27 | 28 | sourceSets { 29 | main.java.srcDirs += 'src/main/kotlin' 30 | } 31 | 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = JavaVersion.VERSION_1_8.toString() 40 | } 41 | 42 | defaultConfig { 43 | applicationId "com.signify.hue.reactivebleexample" 44 | minSdkVersion 21 45 | targetSdkVersion 33 46 | versionCode flutterVersionCode.toInteger() 47 | versionName flutterVersionName 48 | } 49 | 50 | buildTypes { 51 | release { 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | namespace 'com.signify.hue.reactivebleexample' 57 | 58 | lint { 59 | disable 'InvalidPackage' 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 12 | 13 | 16 | 19 | 20 | 21 | 22 | 25 | 26 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/signify/hue/reactivebleexample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.reactivebleexample 2 | 3 | import androidx.annotation.NonNull 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity : FlutterActivity() { 9 | override fun configureFlutterEngine( 10 | @NonNull flutterEngine: FlutterEngine, 11 | ) { 12 | GeneratedPluginRegistrant.registerWith(flutterEngine) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | } 5 | } 6 | 7 | rootProject.buildDir = '../build' 8 | subprojects { 9 | project.buildDir = "${rootProject.buildDir}/${project.name}" 10 | } 11 | subprojects { 12 | project.evaluationDependsOn(':app') 13 | } 14 | 15 | tasks.register("clean", Delete) { 16 | delete rootProject.buildDir 17 | } 18 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.defaults.buildfeatures.buildconfig=true 2 | android.enableJetifier=false 3 | android.nonFinalResIds=false 4 | android.nonTransitiveRClass=false 5 | android.useAndroidX=true 6 | org.gradle.jvmargs=-Xmx1536M 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.0.2" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.21" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | 4 | // See https://github.com/flutter/flutter/issues/17735#issuecomment-400837152 5 | FLUTTER_BUILD_MODE=debug 6 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - Protobuf (3.18.0) 4 | - reactive_ble_mobile (0.0.1): 5 | - Flutter 6 | - Protobuf (~> 3.5) 7 | - SwiftProtobuf (~> 1.0) 8 | - SwiftProtobuf (1.17.0) 9 | 10 | DEPENDENCIES: 11 | - Flutter (from `Flutter`) 12 | - reactive_ble_mobile (from `.symlinks/plugins/reactive_ble_mobile/ios`) 13 | 14 | SPEC REPOS: 15 | trunk: 16 | - Protobuf 17 | - SwiftProtobuf 18 | 19 | EXTERNAL SOURCES: 20 | Flutter: 21 | :path: Flutter 22 | reactive_ble_mobile: 23 | :path: ".symlinks/plugins/reactive_ble_mobile/ios" 24 | 25 | SPEC CHECKSUMS: 26 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 27 | Protobuf: 1a37ebea1338949e9ac35a3f06e80b3f536eec8d 28 | reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c 29 | SwiftProtobuf: 9c85136c6ba74b0a1b84279dbf0f6db8efb714e0 30 | 31 | PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 32 | 33 | COCOAPODS: 1.15.2 34 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | hue_ble_lib_example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | NSBluetoothAlwaysUsageDescription 28 | The app uses bluetooth to find, connect and transfer data between different devices 29 | NSBluetoothPeripheralUsageDescription 30 | The app uses bluetooth to find, connect and transfer data between different devices 31 | UIApplicationSupportsIndirectInputEvents 32 | 33 | UIBackgroundModes 34 | 35 | bluetooth-central 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 3 | import 'package:flutter_reactive_ble_example/src/ble/ble_device_connector.dart'; 4 | import 'package:flutter_reactive_ble_example/src/ble/ble_device_interactor.dart'; 5 | import 'package:flutter_reactive_ble_example/src/ble/ble_scanner.dart'; 6 | import 'package:flutter_reactive_ble_example/src/ble/ble_status_monitor.dart'; 7 | import 'package:flutter_reactive_ble_example/src/ui/ble_status_screen.dart'; 8 | import 'package:flutter_reactive_ble_example/src/ui/device_list.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | import 'src/ble/ble_logger.dart'; 12 | 13 | const _themeColor = Colors.lightGreen; 14 | 15 | void main() { 16 | WidgetsFlutterBinding.ensureInitialized(); 17 | 18 | final _ble = FlutterReactiveBle(); 19 | final _bleLogger = BleLogger(ble: _ble); 20 | final _scanner = BleScanner(ble: _ble, logMessage: _bleLogger.addToLog); 21 | final _monitor = BleStatusMonitor(_ble); 22 | final _connector = BleDeviceConnector( 23 | ble: _ble, 24 | logMessage: _bleLogger.addToLog, 25 | ); 26 | final _serviceDiscoverer = BleDeviceInteractor( 27 | bleDiscoverServices: (deviceId) async { 28 | await _ble.discoverAllServices(deviceId); 29 | return _ble.getDiscoveredServices(deviceId); 30 | }, 31 | logMessage: _bleLogger.addToLog, 32 | readRssi: _ble.readRssi, 33 | ); 34 | runApp( 35 | MultiProvider( 36 | providers: [ 37 | Provider.value(value: _scanner), 38 | Provider.value(value: _monitor), 39 | Provider.value(value: _connector), 40 | Provider.value(value: _serviceDiscoverer), 41 | Provider.value(value: _bleLogger), 42 | StreamProvider( 43 | create: (_) => _scanner.state, 44 | initialData: const BleScannerState( 45 | discoveredDevices: [], 46 | scanIsInProgress: false, 47 | ), 48 | ), 49 | StreamProvider( 50 | create: (_) => _monitor.state, 51 | initialData: BleStatus.unknown, 52 | ), 53 | StreamProvider( 54 | create: (_) => _connector.state, 55 | initialData: const ConnectionStateUpdate( 56 | deviceId: 'Unknown device', 57 | connectionState: DeviceConnectionState.disconnected, 58 | failure: null, 59 | ), 60 | ), 61 | ], 62 | child: MaterialApp( 63 | title: 'Flutter Reactive BLE example', 64 | color: _themeColor, 65 | theme: ThemeData(primarySwatch: _themeColor), 66 | home: const HomeScreen(), 67 | ), 68 | ), 69 | ); 70 | } 71 | 72 | class HomeScreen extends StatelessWidget { 73 | const HomeScreen({ 74 | Key? key, 75 | }) : super(key: key); 76 | 77 | @override 78 | Widget build(BuildContext context) => Consumer( 79 | builder: (_, status, __) { 80 | if (status == BleStatus.ready) { 81 | return const DeviceListScreen(); 82 | } else { 83 | return BleStatusScreen(status: status ?? BleStatus.unknown); 84 | } 85 | }, 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /example/lib/src/ble/ble_device_connector.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 4 | import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart'; 5 | 6 | class BleDeviceConnector extends ReactiveState { 7 | BleDeviceConnector({ 8 | required FlutterReactiveBle ble, 9 | required void Function(String message) logMessage, 10 | }) : _ble = ble, 11 | _logMessage = logMessage; 12 | 13 | final FlutterReactiveBle _ble; 14 | final void Function(String message) _logMessage; 15 | 16 | @override 17 | Stream get state => _deviceConnectionController.stream; 18 | 19 | final _deviceConnectionController = StreamController(); 20 | 21 | // ignore: cancel_subscriptions 22 | late StreamSubscription _connection; 23 | 24 | Future connect(String deviceId) async { 25 | _logMessage('Start connecting to $deviceId'); 26 | _connection = _ble.connectToDevice(id: deviceId).listen( 27 | (update) { 28 | _logMessage( 29 | 'ConnectionState for device $deviceId : ${update.connectionState}'); 30 | _deviceConnectionController.add(update); 31 | }, 32 | onError: (Object e) => 33 | _logMessage('Connecting to device $deviceId resulted in error $e'), 34 | ); 35 | } 36 | 37 | Future disconnect(String deviceId) async { 38 | try { 39 | _logMessage('disconnecting to device: $deviceId'); 40 | await _connection.cancel(); 41 | } on Exception catch (e, _) { 42 | _logMessage("Error disconnecting from a device: $e"); 43 | } finally { 44 | // Since [_connection] subscription is terminated, the "disconnected" state cannot be received and propagated 45 | _deviceConnectionController.add( 46 | ConnectionStateUpdate( 47 | deviceId: deviceId, 48 | connectionState: DeviceConnectionState.disconnected, 49 | failure: null, 50 | ), 51 | ); 52 | } 53 | } 54 | 55 | Future dispose() async { 56 | await _deviceConnectionController.close(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/lib/src/ble/ble_device_interactor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 4 | 5 | class BleDeviceInteractor { 6 | BleDeviceInteractor({ 7 | required Future> Function(String deviceId) bleDiscoverServices, 8 | required void Function(String message) logMessage, 9 | required this.readRssi, 10 | }) : _bleDiscoverServices = bleDiscoverServices, 11 | _logMessage = logMessage; 12 | 13 | final Future> Function(String deviceId) _bleDiscoverServices; 14 | 15 | final Future Function(String deviceId) readRssi; 16 | 17 | final void Function(String message) _logMessage; 18 | 19 | Future> discoverServices(String deviceId) async { 20 | try { 21 | _logMessage('Start discovering services for: $deviceId'); 22 | final result = await _bleDiscoverServices(deviceId); 23 | _logMessage('Discovering services finished'); 24 | return result; 25 | } on Exception catch (e) { 26 | _logMessage('Error occurred when discovering services: $e'); 27 | rethrow; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/lib/src/ble/ble_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 2 | import 'package:intl/intl.dart'; 3 | 4 | class BleLogger { 5 | BleLogger({ 6 | required FlutterReactiveBle ble, 7 | }) : _ble = ble; 8 | 9 | final FlutterReactiveBle _ble; 10 | final List _logMessages = []; 11 | final DateFormat formatter = DateFormat('HH:mm:ss.SSS'); 12 | 13 | List get messages => _logMessages; 14 | 15 | void addToLog(String message) { 16 | final now = DateTime.now(); 17 | _logMessages.add('${formatter.format(now)} - $message'); 18 | } 19 | 20 | void clearLogs() => _logMessages.clear(); 21 | 22 | bool get verboseLogging => _ble.logLevel == LogLevel.verbose; 23 | 24 | void toggleVerboseLogging() => 25 | _ble.logLevel = verboseLogging ? LogLevel.none : LogLevel.verbose; 26 | } 27 | -------------------------------------------------------------------------------- /example/lib/src/ble/ble_scanner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 4 | import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | class BleScanner implements ReactiveState { 8 | BleScanner({ 9 | required FlutterReactiveBle ble, 10 | required void Function(String message) logMessage, 11 | }) : _ble = ble, 12 | _logMessage = logMessage; 13 | 14 | final FlutterReactiveBle _ble; 15 | final void Function(String message) _logMessage; 16 | final StreamController _stateStreamController = 17 | StreamController(); 18 | 19 | final _devices = []; 20 | 21 | @override 22 | Stream get state => _stateStreamController.stream; 23 | 24 | void startScan(List serviceIds) { 25 | _logMessage('Start ble discovery'); 26 | _devices.clear(); 27 | _subscription?.cancel(); 28 | _subscription = 29 | _ble.scanForDevices(withServices: serviceIds).listen((device) { 30 | final knownDeviceIndex = _devices.indexWhere((d) => d.id == device.id); 31 | if (knownDeviceIndex >= 0) { 32 | _devices[knownDeviceIndex] = device; 33 | } else { 34 | _devices.add(device); 35 | } 36 | _pushState(); 37 | }, onError: (Object e) => _logMessage('Device scan fails with error: $e')); 38 | _pushState(); 39 | } 40 | 41 | void _pushState() { 42 | _stateStreamController.add( 43 | BleScannerState( 44 | discoveredDevices: _devices, 45 | scanIsInProgress: _subscription != null, 46 | ), 47 | ); 48 | } 49 | 50 | Future stopScan() async { 51 | _logMessage('Stop ble discovery'); 52 | 53 | await _subscription?.cancel(); 54 | _subscription = null; 55 | _pushState(); 56 | } 57 | 58 | Future dispose() async { 59 | await _stateStreamController.close(); 60 | } 61 | 62 | StreamSubscription? _subscription; 63 | } 64 | 65 | @immutable 66 | class BleScannerState { 67 | const BleScannerState({ 68 | required this.discoveredDevices, 69 | required this.scanIsInProgress, 70 | }); 71 | 72 | final List discoveredDevices; 73 | final bool scanIsInProgress; 74 | } 75 | -------------------------------------------------------------------------------- /example/lib/src/ble/ble_status_monitor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 2 | import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart'; 3 | 4 | class BleStatusMonitor implements ReactiveState { 5 | const BleStatusMonitor(this._ble); 6 | 7 | final FlutterReactiveBle _ble; 8 | 9 | @override 10 | Stream get state => _ble.statusStream; 11 | } 12 | -------------------------------------------------------------------------------- /example/lib/src/ble/reactive_state.dart: -------------------------------------------------------------------------------- 1 | abstract class ReactiveState { 2 | Stream get state; 3 | } 4 | -------------------------------------------------------------------------------- /example/lib/src/ui/ble_status_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 3 | 4 | class BleStatusScreen extends StatelessWidget { 5 | const BleStatusScreen({required this.status, Key? key}) : super(key: key); 6 | 7 | final BleStatus status; 8 | 9 | String determineText(BleStatus status) { 10 | switch (status) { 11 | case BleStatus.unsupported: 12 | return "This device does not support Bluetooth"; 13 | case BleStatus.unauthorized: 14 | return "Authorize the FlutterReactiveBle example app to use Bluetooth and location"; 15 | case BleStatus.poweredOff: 16 | return "Bluetooth is powered off on your device turn it on"; 17 | case BleStatus.locationServicesDisabled: 18 | return "Enable location services"; 19 | case BleStatus.ready: 20 | return "Bluetooth is up and running"; 21 | default: 22 | return "Waiting to fetch Bluetooth status $status"; 23 | } 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) => Scaffold( 28 | body: Center( 29 | child: Text(determineText(status)), 30 | ), 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /example/lib/src/ui/device_detail/device_detail_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 3 | import 'package:flutter_reactive_ble_example/src/ble/ble_device_connector.dart'; 4 | import 'package:flutter_reactive_ble_example/src/ui/device_detail/device_log_tab.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | import 'device_interaction_tab.dart'; 8 | 9 | class DeviceDetailScreen extends StatelessWidget { 10 | final DiscoveredDevice device; 11 | 12 | const DeviceDetailScreen({required this.device, Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) => Consumer( 16 | builder: (_, deviceConnector, __) => _DeviceDetail( 17 | device: device, 18 | disconnect: deviceConnector.disconnect, 19 | ), 20 | ); 21 | } 22 | 23 | class _DeviceDetail extends StatelessWidget { 24 | const _DeviceDetail({ 25 | required this.device, 26 | required this.disconnect, 27 | Key? key, 28 | }) : super(key: key); 29 | 30 | final DiscoveredDevice device; 31 | final void Function(String deviceId) disconnect; 32 | @override 33 | Widget build(BuildContext context) => PopScope( 34 | canPop: true, 35 | onPopInvoked: (_) async { 36 | disconnect(device.id); 37 | }, 38 | child: DefaultTabController( 39 | length: 2, 40 | child: Scaffold( 41 | appBar: AppBar( 42 | title: Text(device.name.isNotEmpty ? device.name : "Unnamed"), 43 | bottom: const TabBar( 44 | tabs: [ 45 | Tab( 46 | icon: Icon( 47 | Icons.bluetooth_connected, 48 | ), 49 | ), 50 | Tab( 51 | icon: Icon( 52 | Icons.find_in_page_sharp, 53 | ), 54 | ), 55 | ], 56 | ), 57 | ), 58 | body: TabBarView( 59 | children: [ 60 | DeviceInteractionTab( 61 | device: device, 62 | ), 63 | const DeviceLogTab(), 64 | ], 65 | ), 66 | ), 67 | ), 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /example/lib/src/ui/device_detail/device_log_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_reactive_ble_example/src/ble/ble_logger.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class DeviceLogTab extends StatelessWidget { 6 | const DeviceLogTab({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) => Consumer( 10 | builder: (context, logger, _) => _DeviceLogTab( 11 | messages: logger.messages, 12 | ), 13 | ); 14 | } 15 | 16 | class _DeviceLogTab extends StatelessWidget { 17 | const _DeviceLogTab({ 18 | required this.messages, 19 | Key? key, 20 | }) : super(key: key); 21 | 22 | final List messages; 23 | 24 | @override 25 | Widget build(BuildContext context) => Padding( 26 | padding: const EdgeInsets.all(16.0), 27 | child: ListView.builder( 28 | itemBuilder: (context, index) => Text(messages[index]), 29 | itemCount: messages.length, 30 | ), 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /example/lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // ignore: avoid_print 2 | void log(String text) => print("[FlutterReactiveBLEApp] $text"); 3 | -------------------------------------------------------------------------------- /example/lib/src/widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BluetoothIcon extends StatelessWidget { 4 | const BluetoothIcon({Key? key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) => const SizedBox( 8 | width: 32, 9 | height: 32, 10 | child: Align(alignment: Alignment.center, child: Icon(Icons.bluetooth)), 11 | ); 12 | } 13 | 14 | class StatusMessage extends StatelessWidget { 15 | const StatusMessage({ 16 | required this.text, 17 | Key? key, 18 | }) : super(key: key); 19 | 20 | final String text; 21 | 22 | @override 23 | Widget build(BuildContext context) => Padding( 24 | padding: const EdgeInsets.symmetric(vertical: 20), 25 | child: Text(text, style: const TextStyle(fontWeight: FontWeight.bold)), 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import reactive_ble_mobile 9 | 10 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 11 | ReactiveBlePlugin.register(with: registry.registrar(forPlugin: "ReactiveBlePlugin")) 12 | } 13 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /example/macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FlutterMacOS (1.0.0) 3 | - Protobuf (3.24.3) 4 | - reactive_ble_mobile (0.0.1): 5 | - Flutter 6 | - FlutterMacOS 7 | - Protobuf (~> 3.5) 8 | - SwiftProtobuf (~> 1.0) 9 | - SwiftProtobuf (1.23.0) 10 | 11 | DEPENDENCIES: 12 | - FlutterMacOS (from `Flutter/ephemeral`) 13 | - reactive_ble_mobile (from `Flutter/ephemeral/.symlinks/plugins/reactive_ble_mobile/darwin`) 14 | 15 | SPEC REPOS: 16 | trunk: 17 | - Protobuf 18 | - SwiftProtobuf 19 | 20 | EXTERNAL SOURCES: 21 | FlutterMacOS: 22 | :path: Flutter/ephemeral 23 | reactive_ble_mobile: 24 | :path: Flutter/ephemeral/.symlinks/plugins/reactive_ble_mobile/darwin 25 | 26 | SPEC CHECKSUMS: 27 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 28 | Protobuf: 970f7ee93a3a08e3cf64859b8efd95ee32b4f87f 29 | reactive_ble_mobile: 856183aeda09f2a676bbaecbea8915c3cb1d33df 30 | SwiftProtobuf: b70d65f419fbfe61a2d58003456ca5da58e337d6 31 | 32 | PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 33 | 34 | COCOAPODS: 1.15.2 35 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.signify.hue.example 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.signify.hue. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.device.bluetooth 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | NSBluetoothAlwaysUsageDescription 32 | The app uses bluetooth to find, connect and transfer data between different devices 33 | NSBluetoothPeripheralUsageDescription 34 | The app uses bluetooth to find, connect and transfer data between different devices 35 | UIApplicationSupportsIndirectInputEvents 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.device.bluetooth 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_reactive_ble_example 2 | description: Demonstrates how to use the flutter_reactive_ble plugin. 3 | version: 5.4.0 4 | publish_to: 'none' 5 | 6 | environment: 7 | sdk: '>=2.17.0 <3.0.0' 8 | flutter: ">=2.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_reactive_ble: ^5.4.0 14 | functional_data: ^1.0.0 15 | intl: ^0.17.0 16 | 17 | provider: ^6.0.1 18 | 19 | dev_dependencies: 20 | build_runner: ^2.3.3 21 | dependency_validator: ^3.1.0 22 | flutter_lints: ^1.0.4 23 | flutter_test: 24 | sdk: flutter 25 | functional_data_generator: ^1.1.2 26 | 27 | dependency_overrides: 28 | flutter_reactive_ble: 29 | path: ../packages/flutter_reactive_ble 30 | reactive_ble_mobile: 31 | path: ../packages/reactive_ble_mobile 32 | reactive_ble_platform_interface: 33 | path: ../packages/reactive_ble_platform_interface 34 | 35 | flutter: 36 | uses-material-design: true 37 | -------------------------------------------------------------------------------- /example/test/example_test.dart: -------------------------------------------------------------------------------- 1 | // needed to make CI pass 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | test('Some test to make CI pass', () { 7 | expect(1, 1); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_reactive_ble 2 | packages: 3 | - packages/* 4 | - example 5 | scripts: 6 | analyze: melos exec -- flutter analyze 7 | 8 | format: melos exec -- flutter format . 9 | 10 | get: melos exec -- flutter pub get 11 | 12 | upgrade: melos exec -- flutter packages upgrade 13 | 14 | unittest: 15 | run: melos exec -- flutter test 16 | 17 | generate: 18 | run: melos run generate:dart && melos run generate:flutter 19 | description: Build all generated files for Dart & Flutter packages in this project. 20 | 21 | generate:dart: 22 | run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs" 23 | description: Build all generated files for Dart packages in this project. 24 | 25 | generate:flutter: 26 | run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs" 27 | description: Build all generated files for Flutter packages in this project. 28 | -------------------------------------------------------------------------------- /packages/flutter_reactive_ble/LICENSE: -------------------------------------------------------------------------------- 1 | BSD license 2 | 3 | ©2019 Signify Holding. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 9 | and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /packages/flutter_reactive_ble/lib/flutter_reactive_ble.dart: -------------------------------------------------------------------------------- 1 | library flutter_reactive_ble; 2 | 3 | export 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart' hide CharacteristicInstance; 4 | 5 | export 'src/reactive_ble.dart'; 6 | export 'src/rx_ext/repeater.dart'; 7 | export 'src/rx_ext/serial_disposable.dart'; 8 | -------------------------------------------------------------------------------- /packages/flutter_reactive_ble/lib/src/debug_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart'; 2 | 3 | class DebugLogger implements Logger { 4 | DebugLogger( 5 | String tag, 6 | void Function(Object object) print, 7 | ) : _tag = tag, 8 | _print = print; 9 | 10 | @override 11 | set logLevel(LogLevel logLevel) => _logLevel = logLevel; 12 | 13 | @override 14 | LogLevel get logLevel => _logLevel; 15 | 16 | @override 17 | void log(Object message) { 18 | if (_logLevel == LogLevel.verbose) { 19 | _print('$_tag: $message'); 20 | } 21 | } 22 | 23 | final void Function(Object object) _print; 24 | final String _tag; 25 | LogLevel _logLevel = LogLevel.none; 26 | } 27 | -------------------------------------------------------------------------------- /packages/flutter_reactive_ble/lib/src/device_scanner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 4 | 5 | abstract class DeviceScanner { 6 | ScanSession? get currentScan; 7 | 8 | Stream scanForDevices({ 9 | required List withServices, 10 | ScanMode scanMode = ScanMode.balanced, 11 | bool requireLocationServicesEnabled = true, 12 | }); 13 | } 14 | 15 | class DeviceScannerImpl implements DeviceScanner { 16 | DeviceScannerImpl({ 17 | required ReactiveBlePlatform blePlatform, 18 | required bool Function() platformIsAndroid, 19 | required Future delayAfterScanCompletion, 20 | required this.addToScanRegistry, 21 | }) : _blePlatform = blePlatform, 22 | _platformIsAndroid = platformIsAndroid, 23 | _delayAfterScanCompletion = delayAfterScanCompletion; 24 | 25 | ScanSession? _currentScanSession; 26 | 27 | final ReactiveBlePlatform _blePlatform; 28 | final bool Function() _platformIsAndroid; 29 | final Future _delayAfterScanCompletion; 30 | final void Function(String deviceId) addToScanRegistry; 31 | 32 | Stream get _scanStream => _blePlatform.scanStream; 33 | 34 | final SerialDisposable> _scanStreamDisposable = 35 | SerialDisposable( 36 | (Repeater repeater) => repeater.dispose()); 37 | 38 | @override 39 | ScanSession? get currentScan => _currentScanSession; 40 | 41 | @override 42 | Stream scanForDevices({ 43 | required List withServices, 44 | ScanMode scanMode = ScanMode.balanced, 45 | bool requireLocationServicesEnabled = true, 46 | }) { 47 | final completer = Completer(); 48 | _currentScanSession = 49 | ScanSession(withServices: withServices, future: completer.future); 50 | // Make sure completing a future with an error does not lead to an unhandled exception. 51 | completer.future.catchError((Object e, StackTrace s) {}); 52 | 53 | final scanRepeater = Repeater( 54 | onListenEmitFrom: () => 55 | _scanStream.map((scan) => scan.result.dematerialize()).handleError( 56 | (Object e, StackTrace s) { 57 | if (!completer.isCompleted) { 58 | completer.completeError(e, s); 59 | if (e is Exception) { 60 | throw e; 61 | } else if (e is Error) { 62 | throw e; 63 | } else { 64 | throw Exception(e); 65 | } 66 | } 67 | }, 68 | ), 69 | onCancel: () async { 70 | if (_platformIsAndroid()) { 71 | // Some devices have issues with establishing connection directly after scan is stopped so we should wait here. 72 | // See https://github.com/googlesamples/android-BluetoothLeGatt/issues/44 73 | await _delayAfterScanCompletion; 74 | } 75 | _currentScanSession = null; 76 | if (!completer.isCompleted) { 77 | completer.complete(); 78 | } 79 | }, 80 | ); 81 | 82 | _scanStreamDisposable.set(scanRepeater); 83 | 84 | return _blePlatform 85 | .scanForDevices( 86 | withServices: withServices, 87 | scanMode: scanMode, 88 | requireLocationServicesEnabled: requireLocationServicesEnabled, 89 | ) 90 | .asyncExpand((_) => scanRepeater.stream.map((discoveredDevice) { 91 | addToScanRegistry(discoveredDevice.id); 92 | return discoveredDevice; 93 | })); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/flutter_reactive_ble/lib/src/discovered_devices_registry.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | abstract class DiscoveredDevicesRegistry { 4 | DiscoveredDevicesRegistry({required this.getTimestamp}); 5 | 6 | final DateTime Function() getTimestamp; 7 | 8 | void add(String deviceId); 9 | 10 | void remove(String deviceId); 11 | 12 | bool isEmpty(); 13 | 14 | bool deviceIsDiscoveredRecently( 15 | {required String deviceId, required Duration cacheValidity}); 16 | } 17 | 18 | class DiscoveredDevicesRegistryImpl implements DiscoveredDevicesRegistry { 19 | DiscoveredDevicesRegistryImpl({required this.getTimestamp}); 20 | 21 | DiscoveredDevicesRegistryImpl.standard() : this(getTimestamp: DateTime.now); 22 | 23 | @override 24 | final DateTime Function() getTimestamp; 25 | 26 | @visibleForTesting 27 | final discoveredDevices = {}; 28 | 29 | @override 30 | void add(String deviceId) { 31 | discoveredDevices[deviceId] = getTimestamp(); 32 | } 33 | 34 | @override 35 | void remove(String deviceId) { 36 | discoveredDevices.remove(deviceId); 37 | } 38 | 39 | @override 40 | bool isEmpty() => discoveredDevices.isEmpty; 41 | 42 | @override 43 | bool deviceIsDiscoveredRecently({ 44 | required String deviceId, 45 | required Duration cacheValidity, 46 | }) => 47 | discoveredDevices.containsKey(deviceId) && 48 | (discoveredDevices[deviceId] 49 | ?.isAfter(getTimestamp().subtract(cacheValidity)) ?? 50 | false); 51 | } 52 | -------------------------------------------------------------------------------- /packages/flutter_reactive_ble/lib/src/rx_ext/serial_disposable.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart'; 4 | 5 | /// A disposable resource whose underlying resource can be replaced by another resource 6 | /// causing automatic disposal of the previous underlying resource. 7 | class SerialDisposable { 8 | SerialDisposable(this._dispose); 9 | 10 | Future set(T newValue) async { 11 | if (_isDisposed) { 12 | throw _SerialAlreadyDisposed(runtimeType); 13 | } 14 | if (_value != null) await _dispose(_value!); 15 | _value = newValue; 16 | } 17 | 18 | /// Dispose underlying resource 19 | Future dispose() async { 20 | _isDisposed = true; 21 | if (_value != null) { 22 | await _dispose(_value!); 23 | } 24 | } 25 | 26 | /// Returns whether or not the underlying resource is disposed 27 | bool get isDisposed => _isDisposed; 28 | 29 | final Future Function(T) _dispose; 30 | bool _isDisposed = false; 31 | T? _value; 32 | } 33 | 34 | class _SerialAlreadyDisposed extends Error { 35 | final Type _type; 36 | 37 | _SerialAlreadyDisposed(this._type); 38 | 39 | @override 40 | String toString() => "An instance of $_type has already been disposed"; 41 | } 42 | 43 | /// A [SerialDisposable] that contains an underlying stream subscription. 44 | class StreamSubscriptionSerialDisposable 45 | extends SerialDisposable> { 46 | StreamSubscriptionSerialDisposable() 47 | : super((StreamSubscription subscription) async { 48 | await subscription.cancel(); 49 | return const Unit(); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/flutter_reactive_ble/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_reactive_ble 2 | description: Reactive Bluetooth Low Energy (BLE) plugin that can communicate with multiple devices 3 | version: 5.4.0 4 | homepage: https://github.com/PhilipsHue/flutter_reactive_ble 5 | 6 | environment: 7 | sdk: '>=2.17.0 <3.0.0' 8 | flutter: ">=2.0.0" 9 | 10 | flutter: 11 | plugin: 12 | platforms: 13 | android: 14 | default_package: reactive_ble_mobile 15 | ios: 16 | default_package: reactive_ble_mobile 17 | 18 | dependencies: 19 | collection: ^1.15.0 20 | flutter: 21 | sdk: flutter 22 | functional_data: ^1.0.0 23 | meta: ^1.3.0 24 | reactive_ble_mobile: ^5.4.0 25 | reactive_ble_platform_interface: ^5.4.0 26 | 27 | dependency_overrides: 28 | reactive_ble_mobile: 29 | path: ../reactive_ble_mobile 30 | reactive_ble_platform_interface: 31 | path: ../reactive_ble_platform_interface 32 | 33 | dev_dependencies: 34 | build_runner: ^2.3.3 35 | flutter_lints: ^1.0.4 36 | flutter_test: 37 | sdk: flutter 38 | functional_data_generator: ^1.1.2 39 | mockito: ^5.0.14 40 | 41 | -------------------------------------------------------------------------------- /packages/flutter_reactive_ble/test/rx_ext/serial_disposable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | group('Serialdisposable', () { 6 | late SerialDisposable sut; 7 | late _Disposer disposer; 8 | 9 | setUp(() { 10 | disposer = _Disposer(); 11 | sut = SerialDisposable(disposer.dispose); 12 | }); 13 | test('It emits last set value on dispose', () async { 14 | await sut.set(5); 15 | await sut.dispose(); 16 | expect(disposer.disposedValues, [5]); 17 | }); 18 | 19 | test('It disposes previous value when a new value is set', () async { 20 | await sut.set(4); 21 | await sut.set(5); 22 | expect(disposer.disposedValues, [4]); 23 | }); 24 | 25 | test('It never calls dispose method in case no value is set', () async { 26 | await sut.dispose(); 27 | expect(disposer.disposedValues, isEmpty); 28 | }); 29 | 30 | test('It returns false in case it is not disposed', () async { 31 | await sut.set(4); 32 | expect(sut.isDisposed, false); 33 | }); 34 | 35 | test('It returns true in case it is disposed', () async { 36 | await sut.set(4); 37 | await sut.dispose(); 38 | expect(sut.isDisposed, true); 39 | }); 40 | 41 | test('It throws in case setting a value on already disposed disposable', 42 | () async { 43 | await sut.dispose(); 44 | expect(sut.set(4), throwsA(anything)); 45 | }); 46 | }); 47 | } 48 | 49 | class _Disposer { 50 | final _values = []; 51 | Future dispose(int value) async { 52 | _values.add(value); 53 | return const Unit(); 54 | } 55 | 56 | List get disposedValues => _values; 57 | } 58 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 5.4.0 2 | 3 | * Support for MacOS #889 4 | * Fixes subscription to _deviceConnector.deviceConnectionStateUpdateStream leaking #876 5 | * Workspace upgrade (dependencies) & gitignore sync #857 6 | * Run ktlint -F #766 7 | 8 | ## 5.3.1 9 | 10 | * Use proper platform_interface dependency version #837 11 | 12 | ## 5.3.0 13 | 14 | * Readd accidentally removed version constraint #789 15 | * Add a placeholder implementation for non-mobile platforms #688 16 | * Add Github actions and Migrate to latest Android SDK, AGP and protobuf plugin #830 17 | * Include packages lock-files #797 18 | * Update Kotlin version in README #831 19 | * Migrate away from deprecated strong mode analysis options #832 20 | * Read RSSI #796 21 | 22 | ## 5.2.0 23 | 24 | * Bump the minimum requirement to Dart 2.17 and upgrade melos to 3.1.0 in #762 25 | * swiftlint config and inital formatting pass in #765 26 | * Cancel subscription when a disconnect event has been thrown in #769 27 | * Fix typos in #778 28 | * Update CI config to use Xcode 14 in #786 29 | * Support multiple services or characteristics with the same id in #776 30 | * Breaking change: If a device has multiple characteristics with the same ID, `readCharacteristic`, `writeCharacteristic` and `subscribeToCharacteristic` used to select the first of those characteristics. Now they will fail in this case. Use `resolve` or `getDiscoveredServices` instead. 31 | 32 | ## 5.1.1 33 | 34 | * Make Connectable backwards compatible #757, #750 35 | 36 | ## 5.1.0 37 | 38 | * Add IsConnectable to discovery data. #750 39 | * Upgraded build_runner. #750 40 | 41 | ## 5.0.3 42 | 43 | * Enable extended advertising on android. Fix #571 44 | * Add additional null checks to scanRecord. Fix #521 45 | * Restore support for Xcode 12 46 | * Add verbose debug logging Fix #583 47 | 48 | ## 5.0.2 49 | 50 | * Revert Queue up messages on iOS until event channel is ready. Fix #439 51 | 52 | ## 5.0.1 53 | 54 | * Bump protobuf so it includes binaries for Mac M1 #396. 55 | 56 | ## 5.0.0 57 | 58 | * `DiscoveredService` has now a new required property called `DiscoveredCharacteristic` which provides properties of the BLE characteristic. 59 | * Android 12 support. 60 | * Queue up messages on iOS until event channel is ready. Fixes #137, #251, #307, #385, #387. 61 | 62 | ## 4.1.0 63 | 64 | * Add support Android 12 65 | 66 | ## 4.0.1 67 | 68 | * Add support for iOS 15 69 | 70 | ## 4.0.0 71 | 72 | * Initial Open Source release. 73 | 74 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/LICENSE: -------------------------------------------------------------------------------- 1 | BSD license 2 | 3 | ©2019 Signify Holding. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 9 | and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/README.md: -------------------------------------------------------------------------------- 1 | # Reactive_ble_mobile 2 | 3 | Official Android and iOS implementation for the flutter_reactive_ble plugin. 4 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.signify.hue.flutterreactiveblelib' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.detekt_version = '1.23.0' 6 | ext.kotlin_version = '1.8.21' 7 | repositories { 8 | google() 9 | mavenCentral() 10 | maven { 11 | url "https://plugins.gradle.org/m2/" 12 | } 13 | } 14 | 15 | dependencies { 16 | classpath 'com.android.tools.build:gradle:8.0.2' 17 | classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.4' 18 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 19 | classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detekt_version" 20 | classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.1.1" 21 | } 22 | } 23 | 24 | rootProject.allprojects { 25 | repositories { 26 | google() 27 | mavenCentral() 28 | maven { 29 | url "https://plugins.gradle.org/m2/" 30 | } 31 | } 32 | } 33 | 34 | apply plugin: 'com.android.library' 35 | apply plugin: 'com.google.protobuf' 36 | apply plugin: 'kotlin-android' 37 | apply plugin: "io.gitlab.arturbosch.detekt" 38 | apply plugin: "de.mannodermaus.android-junit5" 39 | 40 | android { 41 | compileSdkVersion 33 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | test.java.srcDirs += 'src/test/kotlin' 45 | main { 46 | proto { 47 | srcDir '../protos/' 48 | } 49 | } 50 | } 51 | 52 | defaultConfig { 53 | minSdkVersion 21 54 | targetSdkVersion 33 55 | consumerProguardFiles 'proguard-rules.txt' 56 | } 57 | 58 | namespace 'com.signify.hue.flutterreactiveble' 59 | 60 | lint { 61 | disable 'InvalidPackage' 62 | } 63 | 64 | compileOptions { 65 | sourceCompatibility JavaVersion.VERSION_1_8 66 | targetCompatibility JavaVersion.VERSION_1_8 67 | } 68 | 69 | kotlinOptions { 70 | jvmTarget = JavaVersion.VERSION_1_8.toString() 71 | } 72 | } 73 | 74 | detekt { 75 | version = detekt_version 76 | input = files("src/main/kotlin") 77 | dependencies { 78 | detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version" 79 | detektPlugins "io.gitlab.arturbosch.detekt:detekt-rules-libraries:$detekt_version" 80 | } 81 | } 82 | 83 | protobuf { 84 | protoc { 85 | artifact = 'com.google.protobuf:protoc:3.25.3' 86 | } 87 | 88 | generateProtoTasks { 89 | all().each { task -> 90 | task.builtins { 91 | java { 92 | option "lite" 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | dependencies { 100 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 101 | implementation 'com.polidea.rxandroidble2:rxandroidble:1.16.0' 102 | implementation 'com.google.protobuf:protobuf-javalite:3.25.3' 103 | implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' 104 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 105 | 106 | testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.0" 107 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.0" 108 | testImplementation "io.mockk:mockk:1.11.0" 109 | testImplementation "com.google.truth:truth:1.1.4" 110 | } -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | -keep class com.signify.hue.** { *; } -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_reactive_ble' 2 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ReactiveBlePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble 2 | 3 | import android.content.Context 4 | import io.flutter.embedding.engine.plugins.FlutterPlugin 5 | import io.flutter.plugin.common.BinaryMessenger 6 | import io.flutter.plugin.common.MethodCall 7 | import io.flutter.plugin.common.MethodChannel 8 | import io.flutter.plugin.common.MethodChannel.Result 9 | 10 | class ReactiveBlePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { 11 | override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { 12 | initializePlugin(binding.binaryMessenger, binding.applicationContext, this) 13 | } 14 | 15 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 16 | deinitializePlugin() 17 | } 18 | 19 | companion object { 20 | lateinit var pluginController: PluginController 21 | 22 | @JvmStatic 23 | private fun initializePlugin( 24 | messenger: BinaryMessenger, 25 | context: Context, 26 | plugin: ReactiveBlePlugin, 27 | ) { 28 | val channel = MethodChannel(messenger, "flutter_reactive_ble_method") 29 | channel.setMethodCallHandler(plugin) 30 | pluginController = PluginController() 31 | pluginController.initialize(messenger, context) 32 | } 33 | 34 | @JvmStatic 35 | private fun deinitializePlugin() { 36 | pluginController.deinitialize() 37 | } 38 | } 39 | 40 | override fun onMethodCall( 41 | call: MethodCall, 42 | result: Result, 43 | ) { 44 | pluginController.execute(call, result) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.ble 2 | 3 | import android.os.ParcelUuid 4 | import com.polidea.rxandroidble2.RxBleDeviceServices 5 | import com.signify.hue.flutterreactiveble.model.ScanMode 6 | import com.signify.hue.flutterreactiveble.utils.Duration 7 | import io.reactivex.Completable 8 | import io.reactivex.Observable 9 | import io.reactivex.Single 10 | import io.reactivex.subjects.BehaviorSubject 11 | import java.util.UUID 12 | 13 | @Suppress("TooManyFunctions") 14 | interface BleClient { 15 | val connectionUpdateSubject: BehaviorSubject 16 | 17 | fun initializeClient() 18 | 19 | fun scanForDevices( 20 | services: List, 21 | scanMode: ScanMode, 22 | requireLocationServicesEnabled: Boolean, 23 | ): Observable 24 | 25 | fun connectToDevice( 26 | deviceId: String, 27 | timeout: Duration, 28 | ) 29 | 30 | fun disconnectDevice(deviceId: String) 31 | 32 | fun disconnectAllDevices() 33 | 34 | fun discoverServices(deviceId: String): Single 35 | 36 | fun clearGattCache(deviceId: String): Completable 37 | 38 | fun readCharacteristic( 39 | deviceId: String, 40 | characteristicId: UUID, 41 | characteristicInstanceId: Int, 42 | ): Single 43 | 44 | fun setupNotification( 45 | deviceId: String, 46 | characteristicId: UUID, 47 | characteristicInstanceId: Int, 48 | ): Observable 49 | 50 | fun writeCharacteristicWithResponse( 51 | deviceId: String, 52 | characteristicId: UUID, 53 | characteristicInstanceId: Int, 54 | value: ByteArray, 55 | ): Single 56 | 57 | fun writeCharacteristicWithoutResponse( 58 | deviceId: String, 59 | characteristicId: UUID, 60 | characteristicInstanceId: Int, 61 | value: ByteArray, 62 | ): Single 63 | 64 | fun negotiateMtuSize( 65 | deviceId: String, 66 | size: Int, 67 | ): Single 68 | 69 | fun observeBleStatus(): Observable 70 | 71 | fun requestConnectionPriority( 72 | deviceId: String, 73 | priority: ConnectionPriority, 74 | ): Single 75 | 76 | fun readRssi(deviceId: String): Single 77 | } 78 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.ble 2 | 3 | import com.polidea.rxandroidble2.RxBleConnection 4 | import java.util.UUID 5 | 6 | data class ScanInfo( 7 | val deviceId: String, 8 | val name: String, 9 | val rssi: Int, 10 | val connectable: Connectable, 11 | val serviceData: Map, 12 | val serviceUuids: List, 13 | val manufacturerData: ByteArray, 14 | ) { 15 | override fun equals(other: Any?): Boolean { 16 | if (this === other) return true 17 | if (javaClass != other?.javaClass) return false 18 | 19 | other as ScanInfo 20 | 21 | if (deviceId != other.deviceId) return false 22 | if (name != other.name) return false 23 | if (rssi != other.rssi) return false 24 | if (connectable != other.connectable) return false 25 | if (serviceData != other.serviceData) return false 26 | if (serviceUuids != other.serviceUuids) return false 27 | if (!manufacturerData.contentEquals(other.manufacturerData)) return false 28 | 29 | return true 30 | } 31 | 32 | override fun hashCode(): Int { 33 | var result = deviceId.hashCode() 34 | result = 31 * result + name.hashCode() 35 | result = 31 * result + rssi 36 | result = 31 * result + connectable.hashCode() 37 | result = 31 * result + serviceData.hashCode() 38 | result = 31 * result + serviceUuids.hashCode() 39 | result = 31 * result + manufacturerData.contentHashCode() 40 | return result 41 | } 42 | } 43 | 44 | sealed class ConnectionUpdate 45 | 46 | data class ConnectionUpdateSuccess(val deviceId: String, val connectionState: Int) : ConnectionUpdate() 47 | 48 | data class ConnectionUpdateError(val deviceId: String, val errorMessage: String) : ConnectionUpdate() 49 | 50 | sealed class EstablishConnectionResult 51 | 52 | data class EstablishedConnection(val deviceId: String, val rxConnection: RxBleConnection) : EstablishConnectionResult() 53 | 54 | data class EstablishConnectionFailure(val deviceId: String, val errorMessage: String) : EstablishConnectionResult() 55 | 56 | sealed class MtuNegotiateResult 57 | 58 | data class MtuNegotiateSuccessful(val deviceId: String, val size: Int) : MtuNegotiateResult() 59 | 60 | data class MtuNegotiateFailed(val deviceId: String, val errorMessage: String) : MtuNegotiateResult() 61 | 62 | sealed class CharOperationResult 63 | 64 | data class CharOperationSuccessful(val deviceId: String, val value: List) : CharOperationResult() 65 | 66 | data class CharOperationFailed(val deviceId: String, val errorMessage: String) : CharOperationResult() 67 | 68 | sealed class RequestConnectionPriorityResult 69 | 70 | data class RequestConnectionPrioritySuccess(val deviceId: String) : RequestConnectionPriorityResult() 71 | 72 | data class RequestConnectionPriorityFailed(val deviceId: String, val errorMessage: String) : RequestConnectionPriorityResult() 73 | 74 | enum class BleStatus(val code: Int) { 75 | UNKNOWN(code = 0), 76 | UNSUPPORTED(code = 1), 77 | UNAUTHORIZED(code = 2), 78 | POWERED_OFF(code = 3), 79 | LOCATION_SERVICES_DISABLED(code = 4), 80 | READY(code = 5), 81 | } 82 | 83 | enum class ConnectionPriority(val code: Int) { 84 | BALANCED(code = 0), 85 | HIGH_PERFORMACE(code = 1), 86 | LOW_POWER(code = 2), 87 | } 88 | 89 | enum class Connectable(val code: Int) { 90 | UNKNOWN(code = 0), 91 | NOT_CONNECTABLE(code = 1), 92 | CONNECTABLE(code = 2), 93 | } 94 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ConnectionQueue.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.ble 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import io.reactivex.subjects.BehaviorSubject 5 | 6 | internal class ConnectionQueue { 7 | private val queueSubject = BehaviorSubject.createDefault(listOf()) 8 | 9 | fun observeQueue() = queueSubject 10 | 11 | fun addToQueue(deviceId: String) { 12 | if (queueSubject.value?.find { it == deviceId } == null) { 13 | queueSubject.value?.let { currentQueue -> 14 | val newQueue = currentQueue.toMutableList() 15 | newQueue.add(deviceId) 16 | queueSubject.onNext(newQueue) 17 | } 18 | } 19 | } 20 | 21 | @VisibleForTesting 22 | internal fun getCurrentQueue() = queueSubject.value 23 | 24 | fun removeFromQueue(deviceId: String) { 25 | queueSubject.value?.let { currentQueue -> 26 | val newQueue = currentQueue.toMutableList() 27 | newQueue.remove(deviceId) 28 | queueSubject.onNext(newQueue) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/extensions/RxBleConnectionExtension.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.ble.extensions 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import com.polidea.rxandroidble2.RxBleConnection 5 | import io.reactivex.Single 6 | import java.util.UUID 7 | 8 | fun RxBleConnection.resolveCharacteristic( 9 | uuid: UUID, 10 | instanceId: Int, 11 | ): Single = 12 | discoverServices().flatMap { services -> 13 | Single.just( 14 | services.bluetoothGattServices.flatMap { service -> 15 | service.characteristics.filter { 16 | it.uuid == uuid && it.instanceId == instanceId 17 | } 18 | }.single(), 19 | ) 20 | } 21 | 22 | fun RxBleConnection.writeCharWithResponse( 23 | characteristic: BluetoothGattCharacteristic, 24 | value: ByteArray, 25 | ): Single { 26 | characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT 27 | return writeCharacteristic(characteristic, value) 28 | } 29 | 30 | fun RxBleConnection.writeCharWithoutResponse( 31 | characteristic: BluetoothGattCharacteristic, 32 | value: ByteArray, 33 | ): Single { 34 | characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE 35 | return writeCharacteristic(characteristic, value) 36 | } 37 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/BleStatusHandler.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.channelhandlers 2 | 3 | import com.signify.hue.flutterreactiveble.ble.BleClient 4 | import io.flutter.plugin.common.EventChannel 5 | import io.reactivex.Observable 6 | import io.reactivex.android.schedulers.AndroidSchedulers 7 | import io.reactivex.disposables.Disposable 8 | import io.reactivex.disposables.SerialDisposable 9 | import java.util.concurrent.TimeUnit 10 | import com.signify.hue.flutterreactiveble.ProtobufModel as pb 11 | 12 | class BleStatusHandler(private val bleClient: BleClient) : EventChannel.StreamHandler { 13 | companion object { 14 | private const val delayListenBleStatus = 500L 15 | } 16 | 17 | private val subscriptionDisposable = SerialDisposable() 18 | 19 | override fun onListen( 20 | arg: Any?, 21 | eventSink: EventChannel.EventSink?, 22 | ) { 23 | subscriptionDisposable.set(eventSink?.let(::listenToBleStatus)) 24 | } 25 | 26 | override fun onCancel(arg: Any?) { 27 | subscriptionDisposable.set(null) 28 | } 29 | 30 | private fun listenToBleStatus(eventSink: EventChannel.EventSink): Disposable = 31 | Observable.timer(delayListenBleStatus, TimeUnit.MILLISECONDS) 32 | .switchMap { bleClient.observeBleStatus() } 33 | .observeOn(AndroidSchedulers.mainThread()) 34 | .subscribe({ bleStatus -> 35 | val message = 36 | pb.BleStatusInfo.newBuilder() 37 | .setStatus(bleStatus.code) 38 | .build() 39 | eventSink.success(message.toByteArray()) 40 | }, { throwable -> 41 | eventSink.error("ObserveBleStatusFailure", throwable.message, null) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/DeviceConnectionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.channelhandlers 2 | 3 | import com.signify.hue.flutterreactiveble.converters.ProtobufMessageConverter 4 | import com.signify.hue.flutterreactiveble.utils.Duration 5 | import io.flutter.plugin.common.EventChannel 6 | import io.reactivex.android.schedulers.AndroidSchedulers 7 | import io.reactivex.disposables.Disposable 8 | import java.util.concurrent.TimeUnit 9 | import com.signify.hue.flutterreactiveble.ProtobufModel as pb 10 | 11 | class DeviceConnectionHandler(private val bleClient: com.signify.hue.flutterreactiveble.ble.BleClient) : EventChannel.StreamHandler { 12 | private var connectDeviceSink: EventChannel.EventSink? = null 13 | private val converter = ProtobufMessageConverter() 14 | 15 | private lateinit var connectionUpdatesDisposable: Disposable 16 | 17 | override fun onListen( 18 | objectSink: Any?, 19 | eventSink: EventChannel.EventSink?, 20 | ) { 21 | eventSink?.let { 22 | connectDeviceSink = eventSink 23 | connectionUpdatesDisposable = listenToConnectionChanges() 24 | } 25 | } 26 | 27 | override fun onCancel(objectSink: Any?) { 28 | disconnectAll() 29 | connectionUpdatesDisposable.dispose() 30 | } 31 | 32 | fun connectToDevice(connectToDeviceMessage: pb.ConnectToDeviceRequest) { 33 | bleClient.connectToDevice( 34 | connectToDeviceMessage.deviceId, 35 | Duration(connectToDeviceMessage.timeoutInMs.toLong(), TimeUnit.MILLISECONDS), 36 | ) 37 | } 38 | 39 | fun disconnectDevice(deviceId: String) { 40 | bleClient.disconnectDevice(deviceId) 41 | } 42 | 43 | fun disconnectAll() { 44 | connectDeviceSink = null 45 | bleClient.disconnectAllDevices() 46 | } 47 | 48 | private fun listenToConnectionChanges() = 49 | bleClient.connectionUpdateSubject 50 | .observeOn(AndroidSchedulers.mainThread()) 51 | .subscribe { update -> 52 | when (update) { 53 | is com.signify.hue.flutterreactiveble.ble.ConnectionUpdateSuccess -> { 54 | handleDeviceConnectionUpdateResult(converter.convertToDeviceInfo(update)) 55 | } 56 | is com.signify.hue.flutterreactiveble.ble.ConnectionUpdateError -> { 57 | handleDeviceConnectionUpdateResult( 58 | converter.convertConnectionErrorToDeviceInfo(update.deviceId, update.errorMessage), 59 | ) 60 | } 61 | } 62 | } 63 | 64 | private fun handleDeviceConnectionUpdateResult(connectionUpdateMessage: pb.DeviceInfo) { 65 | connectDeviceSink?.success(connectionUpdateMessage.toByteArray()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/ScanDevicesHandler.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.channelhandlers 2 | 3 | import android.os.ParcelUuid 4 | import com.signify.hue.flutterreactiveble.converters.ProtobufMessageConverter 5 | import com.signify.hue.flutterreactiveble.converters.UuidConverter 6 | import com.signify.hue.flutterreactiveble.model.ScanMode 7 | import com.signify.hue.flutterreactiveble.model.createScanMode 8 | import io.flutter.plugin.common.EventChannel 9 | import io.reactivex.android.schedulers.AndroidSchedulers 10 | import io.reactivex.disposables.Disposable 11 | import com.signify.hue.flutterreactiveble.ProtobufModel as pb 12 | 13 | class ScanDevicesHandler( 14 | private val bleClient: com.signify.hue.flutterreactiveble.ble.BleClient, 15 | ) : EventChannel.StreamHandler { 16 | private var scanDevicesSink: EventChannel.EventSink? = null 17 | private lateinit var scanForDevicesDisposable: Disposable 18 | private val converter = ProtobufMessageConverter() 19 | 20 | companion object { 21 | private var scanParameters: ScanParameters? = null 22 | } 23 | 24 | override fun onListen( 25 | objectSink: Any?, 26 | eventSink: EventChannel.EventSink?, 27 | ) { 28 | eventSink?.let { 29 | scanDevicesSink = eventSink 30 | startDeviceScan() 31 | } 32 | } 33 | 34 | override fun onCancel(objectSink: Any?) { 35 | stopDeviceScan() 36 | scanDevicesSink = null 37 | } 38 | 39 | private fun startDeviceScan() { 40 | scanParameters?.let { params -> 41 | scanForDevicesDisposable = 42 | bleClient.scanForDevices(params.filter, params.mode, params.locationServiceIsMandatory) 43 | .observeOn(AndroidSchedulers.mainThread()) 44 | .subscribe( 45 | { scanResult -> 46 | handleDeviceScanResult(converter.convertScanInfo(scanResult)) 47 | }, 48 | { throwable -> 49 | handleDeviceScanResult(converter.convertScanErrorInfo(throwable.message)) 50 | }, 51 | ) 52 | } 53 | ?: handleDeviceScanResult(converter.convertScanErrorInfo("Scanning parameters are not set")) 54 | } 55 | 56 | fun stopDeviceScan() { 57 | if (this::scanForDevicesDisposable.isInitialized) { 58 | scanForDevicesDisposable.let { 59 | if (!it.isDisposed) { 60 | it.dispose() 61 | scanParameters = null 62 | } 63 | } 64 | } 65 | } 66 | 67 | fun prepareScan(scanMessage: pb.ScanForDevicesRequest) { 68 | stopDeviceScan() 69 | val filter = 70 | scanMessage.serviceUuidsList 71 | .map { ParcelUuid(UuidConverter().uuidFromByteArray(it.data.toByteArray())) } 72 | val scanMode = createScanMode(scanMessage.scanMode) 73 | scanParameters = ScanParameters(filter, scanMode, scanMessage.requireLocationServicesEnabled) 74 | } 75 | 76 | private fun handleDeviceScanResult(discoveryMessage: pb.DeviceScanInfo) { 77 | scanDevicesSink?.success(discoveryMessage.toByteArray()) 78 | } 79 | } 80 | 81 | private data class ScanParameters( 82 | val filter: List, 83 | val mode: ScanMode, 84 | val locationServiceIsMandatory: Boolean, 85 | ) 86 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/converters/ManufacturerDataConverter.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.converters 2 | 3 | import android.util.SparseArray 4 | 5 | fun extractManufacturerData(manufacturerData: SparseArray?): ByteArray { 6 | val rawData = mutableListOf() 7 | 8 | if (manufacturerData != null && manufacturerData.size() > 0) { 9 | val companyId = manufacturerData.keyAt(0) 10 | val payload = manufacturerData.get(companyId) 11 | 12 | rawData.add((companyId.toByte())) 13 | rawData.add(((companyId.shr(Byte.SIZE_BITS)).toByte())) 14 | rawData.addAll(2, payload.asList()) 15 | } 16 | 17 | return rawData.toByteArray() 18 | } 19 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/converters/UuidConverter.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.converters 2 | 3 | import java.nio.ByteBuffer 4 | import java.util.UUID 5 | 6 | class UuidConverter { 7 | companion object { 8 | private const val byteSize16Bit = 2 9 | private const val byteSize32Bit = 4 10 | private const val byteBufferSize = 16 11 | } 12 | 13 | fun uuidFromByteArray(bytes: ByteArray): UUID { 14 | return when (bytes.size) { 15 | byteSize16Bit -> convert16BitToUuid(bytes) 16 | byteSize32Bit -> convert32BitToUuid(bytes) 17 | else -> convert128BitNotationToUuid(bytes) 18 | } 19 | } 20 | 21 | @Suppress("Detekt.MagicNumber") 22 | private fun convert16BitToUuid(bytes: ByteArray): UUID { 23 | // UUID construction is retrieved from BLE corespec v5.0 page 1917 24 | val uuidConstruct = 25 | byteArrayOf( 26 | 0x00, 27 | 0x00, 28 | bytes[0], 29 | bytes[1], 30 | 0x00, 31 | 0x00, 32 | 0x10, 33 | 0x00, 34 | 0x80.toByte(), 35 | 0x00, 36 | 0x00, 37 | 0x80.toByte(), 38 | 0x5F, 39 | 0x9B.toByte(), 40 | 0x34, 41 | 0xFB.toByte(), 42 | ) 43 | 44 | return convert128BitNotationToUuid(uuidConstruct) 45 | } 46 | 47 | @Suppress("Detekt.MagicNumber") 48 | private fun convert32BitToUuid(bytes: ByteArray): UUID { 49 | val uuidConstruct = 50 | byteArrayOf( 51 | bytes[0], 52 | bytes[1], 53 | bytes[2], 54 | bytes[3], 55 | 0x00, 56 | 0x00, 57 | 0x10, 58 | 0x00, 59 | 0x80.toByte(), 60 | 0x00, 61 | 0x00, 62 | 0x80.toByte(), 63 | 0x5F, 64 | 0x9B.toByte(), 65 | 0x34, 66 | 0xFB.toByte(), 67 | ) 68 | 69 | return convert128BitNotationToUuid(uuidConstruct) 70 | } 71 | 72 | private fun convert128BitNotationToUuid(bytes: ByteArray): UUID { 73 | val bb = ByteBuffer.wrap(bytes) 74 | val most = bb.long 75 | val least = bb.long 76 | return UUID(most, least) 77 | } 78 | 79 | fun byteArrayFromUuid(uuid: UUID): ByteArray { 80 | val bb = ByteBuffer.wrap(ByteArray(byteBufferSize)) 81 | bb.putLong(uuid.mostSignificantBits) 82 | bb.putLong(uuid.leastSignificantBits) 83 | 84 | return bb.array() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/debugutils/HexStringConversion.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.debugutils 2 | 3 | fun ByteArray.toHexString() = joinToString("") { String.format("%02x", it) } 4 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/debugutils/PerformanceAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.debugutils 2 | 3 | object PerformanceAnalyzer { 4 | var timer = Pair(0, 0) 5 | 6 | fun start(startTime: Long) { 7 | timer = timer.copy(first = startTime) 8 | } 9 | 10 | fun end(endTime: Long) { 11 | timer = timer.copy(second = endTime) 12 | } 13 | 14 | fun timeElapsed(): Long = timer.second - timer.first 15 | } 16 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/model/ConnectionState.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.model 2 | 3 | import com.polidea.rxandroidble2.RxBleConnection 4 | 5 | @Suppress("Detekt.MagicNumber") 6 | enum class ConnectionState(val code: Int) { 7 | CONNECTING(0), 8 | CONNECTED(1), 9 | DISCONNECTING(2), 10 | DISCONNECTED(3), 11 | UNKNOWN(4), 12 | } 13 | 14 | fun RxBleConnection.RxBleConnectionState.toConnectionState(): ConnectionState = 15 | when (this.name) { 16 | "DISCONNECTED" -> ConnectionState.DISCONNECTED 17 | "CONNECTING" -> ConnectionState.CONNECTING 18 | "CONNECTED" -> ConnectionState.CONNECTED 19 | "DISCONNECTING" -> ConnectionState.DISCONNECTING 20 | else -> ConnectionState.UNKNOWN 21 | } 22 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/model/ErrorType.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.model 2 | 3 | enum class ConnectionErrorType(val code: Int) { 4 | UNKNOWN(0), 5 | FAILEDTOCONNECT(1), 6 | } 7 | 8 | enum class ClearGattCacheErrorType(val code: Int) { 9 | UNKNOWN(0), 10 | } 11 | 12 | enum class CharacteristicErrorType(val code: Int) { 13 | UNKNOWN(0), 14 | } 15 | 16 | enum class NegotiateMtuErrorType(val code: Int) { 17 | UNKNOWN(0), 18 | } 19 | 20 | enum class ScanErrorType(val code: Int) { 21 | UNKNOWN(0), 22 | } 23 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/model/ScanMode.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.model 2 | 3 | import android.bluetooth.le.ScanSettings 4 | 5 | enum class ScanMode(val code: Int) { 6 | OPPORTUNISTIC(-1), 7 | LOW_POWER(0), 8 | BALANCED(1), 9 | LOW_LATENCY(2), 10 | } 11 | 12 | internal fun ScanMode.toScanSettings(): Int = 13 | when (this) { 14 | ScanMode.OPPORTUNISTIC -> ScanSettings.SCAN_MODE_OPPORTUNISTIC 15 | ScanMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER 16 | ScanMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED 17 | ScanMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY 18 | } 19 | 20 | internal fun createScanMode(mode: Int): ScanMode = 21 | when (mode) { 22 | -1 -> ScanMode.OPPORTUNISTIC 23 | 0 -> ScanMode.LOW_POWER 24 | 1 -> ScanMode.BALANCED 25 | 2 -> ScanMode.LOW_LATENCY 26 | else -> ScanMode.LOW_POWER 27 | } 28 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/utils/BleWrapperExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.utils 2 | 3 | import com.polidea.rxandroidble2.RxBleClient 4 | import com.polidea.rxandroidble2.RxBleClient.State.BLUETOOTH_NOT_AVAILABLE 5 | import com.polidea.rxandroidble2.RxBleClient.State.BLUETOOTH_NOT_ENABLED 6 | import com.polidea.rxandroidble2.RxBleClient.State.LOCATION_PERMISSION_NOT_GRANTED 7 | import com.polidea.rxandroidble2.RxBleClient.State.LOCATION_SERVICES_NOT_ENABLED 8 | import com.polidea.rxandroidble2.RxBleClient.State.READY 9 | import com.signify.hue.flutterreactiveble.ble.BleStatus 10 | import com.signify.hue.flutterreactiveble.ble.ConnectionPriority 11 | 12 | fun RxBleClient.State.toBleState(): BleStatus = 13 | when (this) { 14 | BLUETOOTH_NOT_AVAILABLE -> BleStatus.UNSUPPORTED 15 | LOCATION_PERMISSION_NOT_GRANTED -> BleStatus.UNAUTHORIZED 16 | BLUETOOTH_NOT_ENABLED -> BleStatus.POWERED_OFF 17 | LOCATION_SERVICES_NOT_ENABLED -> BleStatus.LOCATION_SERVICES_DISABLED 18 | READY -> BleStatus.READY 19 | } 20 | 21 | fun Int.toConnectionPriority() = 22 | when (this) { 23 | 0 -> ConnectionPriority.BALANCED 24 | 1 -> ConnectionPriority.HIGH_PERFORMACE 25 | 2 -> ConnectionPriority.LOW_POWER 26 | else -> ConnectionPriority.BALANCED 27 | } 28 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/utils/Discard.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.utils 2 | 3 | @Suppress("unused") 4 | fun Any?.discard() = Unit 5 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/main/kotlin/com/signify/hue/flutterreactiveble/utils/Duration.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.utils 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | data class Duration(val value: Long, val unit: TimeUnit) 6 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/test/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/ConnectionQueueTest.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.channelhandlers 2 | import com.google.common.truth.Truth.assertThat 3 | import io.reactivex.subjects.BehaviorSubject 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Test 6 | 7 | class ConnectionQueueTest { 8 | private lateinit var sut: com.signify.hue.flutterreactiveble.ble.ConnectionQueue 9 | 10 | @BeforeEach 11 | fun setup() { 12 | sut = com.signify.hue.flutterreactiveble.ble.ConnectionQueue() 13 | } 14 | 15 | @Test 16 | fun `should be able to add an item to the queue`() { 17 | sut.addToQueue("test") 18 | assertThat(sut.getCurrentQueue()?.size).isEqualTo(1) 19 | } 20 | 21 | @Test 22 | fun `should not be able to add an item to the queue twice`() { 23 | sut.addToQueue("test") 24 | sut.addToQueue("test") 25 | assertThat(sut.getCurrentQueue()?.size).isEqualTo(1) 26 | } 27 | 28 | @Test 29 | fun `should be able to see all devices in the queue`() { 30 | sut.addToQueue("device1") 31 | sut.addToQueue("device2") 32 | sut.addToQueue("device3") 33 | assertThat(sut.getCurrentQueue()?.size).isEqualTo(3) 34 | } 35 | 36 | @Test 37 | fun `should be able to remove an item to the queue`() { 38 | sut.addToQueue("test") 39 | sut.removeFromQueue("test") 40 | assertThat(sut.getCurrentQueue()?.size).isEqualTo(0) 41 | } 42 | 43 | @Test 44 | fun `should provide behavior subject for observing latest queue state`() { 45 | assertThat(sut.observeQueue()).isInstanceOf(BehaviorSubject::class.java) 46 | } 47 | 48 | @Test 49 | fun `should return an emptylist when queue is created`() { 50 | assertThat(sut.observeQueue().test().values().first().size).isEqualTo(0) 51 | } 52 | 53 | @Test 54 | fun `should provide me new queue each time something changed`() { 55 | val observable = sut.observeQueue().test() 56 | sut.addToQueue("test") 57 | 58 | assertThat(observable.valueCount()).isEqualTo(2) 59 | } 60 | 61 | @Test 62 | fun `should be ordened in the sequence of adding`() { 63 | val expectedQueue = listOf("test1", "test2") 64 | val observable = sut.observeQueue().test() 65 | sut.addToQueue("test1") 66 | sut.addToQueue("test2") 67 | 68 | val lastQueue = observable.values().last() 69 | assertThat(lastQueue).isEqualTo(expectedQueue) 70 | } 71 | 72 | @Test 73 | fun `should update queue after removal`() { 74 | val observable = sut.observeQueue().test() 75 | sut.addToQueue("test1") 76 | sut.addToQueue("test2") 77 | sut.removeFromQueue("test2") 78 | 79 | assertThat(observable.valueCount()).isEqualTo(4) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/test/kotlin/com/signify/hue/flutterreactiveble/converters/ServicesWithCharacteristicsConverterTest.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.converters 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import android.bluetooth.BluetoothGattService 5 | import com.google.common.truth.Truth.assertThat 6 | import com.polidea.rxandroidble2.RxBleDeviceServices 7 | import io.mockk.MockKAnnotations 8 | import io.mockk.every 9 | import io.mockk.impl.annotations.MockK 10 | import org.junit.Before 11 | import org.junit.Test 12 | import java.util.UUID 13 | import com.signify.hue.flutterreactiveble.ProtobufModel as pb 14 | 15 | class ServicesWithCharacteristicsConverterTest { 16 | private val serviceUuid = UUID.randomUUID() 17 | private val characteristicUuid = UUID.randomUUID() 18 | private val internalCharacteristicUuid = UUID.randomUUID() 19 | private val internalServiceUuid = UUID.randomUUID() 20 | private val internalCharacteristicUuidLevel2 = UUID.randomUUID() 21 | private val internalServiceUuidLevel2 = UUID.randomUUID() 22 | private val sut = ProtobufMessageConverter() 23 | private lateinit var conversionResult: pb.DiscoverServicesInfo 24 | 25 | @MockK 26 | lateinit var service: BluetoothGattService 27 | 28 | @MockK 29 | lateinit var characteristic: BluetoothGattCharacteristic 30 | 31 | @MockK 32 | lateinit var internalService: BluetoothGattService 33 | 34 | @MockK 35 | lateinit var internalCharacteristic: BluetoothGattCharacteristic 36 | 37 | @MockK 38 | lateinit var internalServiceLevel2: BluetoothGattService 39 | 40 | @MockK 41 | lateinit var internalCharacteristicLevel2: BluetoothGattCharacteristic 42 | 43 | @Before 44 | fun setUp() { 45 | MockKAnnotations.init(this) 46 | every { service.uuid }.returns(serviceUuid) 47 | every { characteristic.uuid }.returns(characteristicUuid) 48 | every { service.includedServices }.returns(listOf(internalService)) 49 | every { service.characteristics }.returns(listOf(characteristic)) 50 | 51 | every { internalCharacteristic.uuid }.returns(internalCharacteristicUuid) 52 | every { internalService.uuid }.returns(internalServiceUuid) 53 | every { internalService.includedServices }.returns(listOf(internalServiceLevel2, internalServiceLevel2)) 54 | every { internalService.characteristics }.returns(listOf(internalCharacteristic)) 55 | 56 | every { internalCharacteristicLevel2.uuid }.returns(internalCharacteristicUuidLevel2) 57 | every { internalServiceLevel2.uuid }.returns(internalServiceUuidLevel2) 58 | every { internalServiceLevel2.includedServices }.returns(listOf()) 59 | every { internalServiceLevel2.characteristics }.returns(listOf(internalCharacteristicLevel2)) 60 | 61 | conversionResult = 62 | sut.convertDiscoverServicesInfo( 63 | "test", 64 | RxBleDeviceServices(listOf(service)), 65 | ) 66 | } 67 | 68 | @Test 69 | fun `It converts total services`() { 70 | assertThat(conversionResult.servicesCount).isEqualTo(1) 71 | } 72 | 73 | @Test 74 | fun `It converts characteristic uuid`() { 75 | assertThat(conversionResult.getServices(0).characteristicUuidsCount).isEqualTo(1) 76 | } 77 | 78 | @Test 79 | fun `It converts nested internal services correctly`() { 80 | assertThat( 81 | conversionResult.getServices(0).getIncludedServices(0) 82 | .includedServicesCount, 83 | ).isEqualTo(2) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/android/src/test/kotlin/com/signify/hue/flutterreactiveble/converters/UuidConverterTest.kt: -------------------------------------------------------------------------------- 1 | package com.signify.hue.flutterreactiveble.converters 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.jupiter.api.Test 5 | import java.util.UUID 6 | 7 | class UuidConverterTest { 8 | val converter = UuidConverter() 9 | 10 | @Test 11 | fun `should convert uuid into bytearray`() { 12 | val uuid = UUID.randomUUID() 13 | val byteArray = converter.byteArrayFromUuid(uuid) 14 | val uuid2 = converter.uuidFromByteArray(byteArray) 15 | 16 | assertThat(uuid2).isEqualTo(uuid) 17 | } 18 | 19 | @Test 20 | fun `should be able to convert 16bit uuid`() { 21 | val array = byteArrayOf(0xFE.toByte(), 0x0F.toByte()) 22 | val uuid = converter.uuidFromByteArray(array) 23 | assertThat(uuid.toString().toUpperCase()).isEqualTo("0000FE0F-0000-1000-8000-00805F9B34FB") 24 | } 25 | 26 | @Test 27 | fun `should be able to convert 32bit uuid`() { 28 | val array = byteArrayOf(0xFE.toByte(), 0x0F.toByte(), 0x0F.toByte(), 0xFE.toByte()) 29 | val uuid = converter.uuidFromByteArray(array) 30 | assertThat(uuid.toString().toUpperCase()).isEqualTo("FE0F0FFE-0000-1000-8000-00805F9B34FB") 31 | } 32 | 33 | @Test 34 | fun `should convert bytearray into uuid`() { 35 | val uuid = UUID.randomUUID() 36 | 37 | val byteArray = converter.byteArrayFromUuid(uuid) 38 | assertThat(converter.uuidFromByteArray(byteArray).toString()).isEqualTo(uuid.toString()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilipsHue/flutter_reactive_ble/fab66c21ef8bc28402cf9715e630fd88c910ad97/packages/reactive_ble_mobile/darwin/Assets/.gitkeep -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/BleData extras/BLEStatus.swift: -------------------------------------------------------------------------------- 1 | import enum CoreBluetooth.CBManagerState 2 | 3 | func encode(_ centralState: CBManagerState) -> Int32 { 4 | switch centralState { 5 | case .unknown, .resetting: 6 | return 0 7 | case .unsupported: 8 | return 1 9 | case .unauthorized: 10 | return 2 11 | case .poweredOff: 12 | return 3 13 | case .poweredOn: 14 | return 5 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/BleData extras/ConnectionState.swift: -------------------------------------------------------------------------------- 1 | import enum CoreBluetooth.CBPeripheralState 2 | 3 | func encode(_ connectionState: CBPeripheralState) -> Int32 { 4 | switch connectionState { 5 | case .disconnected: 6 | return 3 7 | case .connecting: 8 | return 0 9 | case .connected: 10 | return 1 11 | case .disconnecting: 12 | return 2 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/BleData extras/FailureCodes.swift: -------------------------------------------------------------------------------- 1 | enum ConnectionFailure: Int { 2 | 3 | case unknown 4 | case failedToConnect 5 | } 6 | 7 | enum ClearGattCacheFailure: Int { 8 | 9 | case operationNotSupported = 1 10 | } 11 | 12 | enum CharacteristicValueUpdateFailure: Int { 13 | 14 | case unknown 15 | } 16 | 17 | enum WriteCharacteristicFailure: Int { 18 | 19 | case unknown 20 | } 21 | 22 | enum MaximumWriteValueLengthRetrieval: Int { 23 | 24 | case unknown 25 | } 26 | 27 | enum RequestConnectionPriorityFailure: Int { 28 | 29 | case operationNotSupported = 1 30 | } 31 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Plugin/Common/EventSink.swift: -------------------------------------------------------------------------------- 1 | import enum SwiftProtobuf.BinaryEncodingError 2 | 3 | struct EventSink { 4 | 5 | private let name: String 6 | private let sink: FlutterEventSink 7 | 8 | init(name: String, _ sink: @escaping FlutterEventSink) { 9 | self.name = name 10 | self.sink = sink 11 | } 12 | 13 | func add(_ event: PlatformMethodResult) { 14 | switch event { 15 | case .success(let message): 16 | if let message = message { 17 | do { 18 | sink(FlutterStandardTypedData(bytes: try message.serializedData())) 19 | } catch let error as BinaryEncodingError { 20 | sink(PluginError.messageSerializationFailure(type: type(of: message), underlyingError: error).asFlutterError) 21 | } catch { 22 | sink(PluginError.unknown(error).asFlutterError) 23 | } 24 | } else { 25 | sink(nil) 26 | } 27 | case .failure(let error): 28 | sink(error) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Plugin/Common/Failable.swift: -------------------------------------------------------------------------------- 1 | enum Failable { 2 | 3 | case success(T) 4 | case failure(Error) 5 | 6 | var value: T? { return iif(success: id, failure: const(nil)) } 7 | var error: Error? { return iif(success: const(nil), failure: id) } 8 | 9 | func iif(success: (T) -> U, failure: (Error) -> U) -> U { 10 | switch self { 11 | case .success(let value): 12 | return success(value) 13 | case .failure(let error): 14 | return failure(error) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Plugin/Common/MethodHandler.swift: -------------------------------------------------------------------------------- 1 | final class MethodHandler { 2 | 3 | private let methods: [String: AnyPlatformMethod] 4 | 5 | init(_ methods: [AnyPlatformMethod]) { 6 | self.methods = methods.reduce(into: [:], { $0[$1.name] = $1 }) 7 | } 8 | 9 | func handle(in context: Context, _ call: FlutterMethodCall, completion: @escaping FlutterResult) { 10 | guard let method = methods[call.method] 11 | else { 12 | completion(PluginError.unsupportedMethodCall(method: call.method).asFlutterError) 13 | return 14 | } 15 | 16 | method.call(in: context, arguments: call.arguments, completion: completion) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Plugin/Common/StreamHandler.swift: -------------------------------------------------------------------------------- 1 | final class StreamHandler: NSObject, FlutterStreamHandler { 2 | 3 | typealias OnListenHandler = (Context, EventSink) -> FlutterError? 4 | typealias OnCancelHandler = (Context) -> FlutterError? 5 | 6 | private let name: String 7 | private weak var context: Context? 8 | private let onListenBody: OnListenHandler 9 | private let onCancelBody: OnCancelHandler 10 | 11 | init(name: String, context: Context, onListen: @escaping OnListenHandler, onCancel: @escaping OnCancelHandler) { 12 | self.name = name 13 | self.context = context 14 | self.onListenBody = onListen 15 | self.onCancelBody = onCancel 16 | super.init() 17 | } 18 | 19 | func onListen(withArguments args: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? { 20 | assert(args == nil) 21 | 22 | guard let context = context 23 | else { 24 | return PluginError.internalInconcictency( 25 | details: "\(StreamHandler.self)(name: \"\(name)\").\(#function) is called when the context is destroyed" 26 | ) 27 | .asFlutterError 28 | } 29 | 30 | return onListenBody(context, EventSink(name: name, eventSink)) 31 | } 32 | 33 | func onCancel(withArguments args: Any?) -> FlutterError? { 34 | assert(args == nil) 35 | 36 | guard let context = context 37 | else { 38 | return PluginError.internalInconcictency( 39 | details: "\(StreamHandler.self)(name: \"\(name)\").\(#function) is called when the context is destroyed" 40 | ) 41 | .asFlutterError 42 | } 43 | 44 | return onCancelBody(context) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Plugin/PluginError.swift: -------------------------------------------------------------------------------- 1 | import protocol SwiftProtobuf.Message 2 | 3 | enum PluginError: Error { 4 | 5 | case unknown(Error) 6 | 7 | case internalInconcictency(details: String?) 8 | 9 | case unsupportedMethodCall(method: String) 10 | case invalidMethodCall(method: String, details: String?) 11 | 12 | case messageDeserializationFailure(type: Message.Type, underlyingError: Error) 13 | case messageSerializationFailure(type: Message.Type, underlyingError: Error) 14 | 15 | case notInitialized 16 | 17 | case connectionLost 18 | 19 | var asFlutterError: FlutterError { 20 | switch self { 21 | case .unknown(let error as NSError): 22 | return makeFlutterError(code: "\(error.domain):\(error.code)", message: error.localizedDescription, details: error.userInfo) 23 | case .internalInconcictency(let details): 24 | let extra = details.map { " (\($0))" } ?? "" 25 | return makeFlutterError(message: "internal inconsistency" + extra) 26 | case .unsupportedMethodCall(let method): 27 | return makeFlutterError(message: "the method \"\(method)\" is not supported") 28 | case .invalidMethodCall(let method, let details): 29 | let extra = details.map { " (\($0))" } ?? "" 30 | return makeFlutterError(message: "invalid \"\(method)\" method call" + extra) 31 | case .messageDeserializationFailure(let type, let underlyingError): 32 | return makeFlutterError(message: "failed to deserialize a message of type \(type) (\(underlyingError))") 33 | case .messageSerializationFailure(let type, let underlyingError): 34 | return makeFlutterError(message: "failed to serialize a message of type \(type) (\(underlyingError))") 35 | case .notInitialized: 36 | return makeFlutterError(message: "not initialized") 37 | case .connectionLost: 38 | return makeFlutterError(message: "connection lost") 39 | } 40 | } 41 | 42 | private func makeFlutterError(code: String? = nil, message: String?, details: Any? = nil) -> FlutterError { 43 | return FlutterError(code: code ?? "\(self)", message: message, details: details) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Plugin/StreamingTask.swift: -------------------------------------------------------------------------------- 1 | struct StreamingTask { 2 | let parameters: T 3 | let sink: EventSink? 4 | 5 | init(parameters: T) { 6 | self.parameters = parameters 7 | self.sink = nil 8 | } 9 | 10 | private init(parameters: T, sink: EventSink?) { 11 | self.parameters = parameters 12 | self.sink = sink 13 | } 14 | 15 | func with(sink: EventSink?) -> StreamingTask { 16 | return StreamingTask(parameters: parameters, sink: sink) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Prelude/CoreBluetooth+Extensions.swift: -------------------------------------------------------------------------------- 1 | #if compiler(<5.5) 2 | import class CoreBluetooth.CBCharacteristic 3 | import class CoreBluetooth.CBService 4 | import class CoreBluetooth.CBPeripheral 5 | 6 | // Extensions below are supposed to backport API breaking changes 7 | // in CoreBluetooth framework which Apple introduced with Xcode 13: 8 | // 9 | // ``` 10 | // + weak var peripheral: CBPeripheral? { get } 11 | // - unowned(unsafe) var peripheral: CBPeripheral { get } 12 | // ``` 13 | // 14 | // Thus this code shadows original property declarations in CoreBluetooth 15 | // and changes their semantics from `unsafe non-optional` to `weak optional` 16 | // to mimic Xcode 13 behavior. 17 | // 18 | // - Note: This code compiles only when using Xcode 12 and below. 19 | // - SeeAlso: https://forums.swift.org/t/is-unowned-unsafe-t-weak-t-a-breaking-change/49917 20 | 21 | extension CBCharacteristic { 22 | @nonobjc 23 | weak var service: CBService? { 24 | return value(forKey: #function) as? CBService 25 | } 26 | } 27 | 28 | extension CBService { 29 | @nonobjc 30 | weak var peripheral: CBPeripheral? { 31 | return value(forKey: #function) as? CBPeripheral 32 | } 33 | } 34 | #endif 35 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Prelude/base.swift: -------------------------------------------------------------------------------- 1 | func id(_ some: T) -> T { 2 | return some 3 | } 4 | 5 | func const(_ value: U) -> (T) -> U { 6 | return { (_: T) -> U in value } 7 | } 8 | 9 | func const(_ value: U) -> () -> U { 10 | return { () -> U in value } 11 | } 12 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/Prelude/papply.swift: -------------------------------------------------------------------------------- 1 | func papply(weak arg1: T1, _ f: @escaping (T1, T2, T3, T4, T5) -> Void) -> (T2, T3, T4, T5) -> Void { 2 | return { [weak arg1] (arg2: T2, arg3: T3, arg4: T4, arg5: T5) -> Void in 3 | guard let arg1 = arg1 4 | else { return } 5 | 6 | f(arg1, arg2, arg3, arg4, arg5) 7 | } 8 | } 9 | 10 | func papply(weak arg1: T1, _ f: @escaping (T1, T2, T3, T4) -> Void) -> (T2, T3, T4) -> Void { 11 | return { [weak arg1] (arg2: T2, arg3: T3, arg4: T4) -> Void in 12 | guard let arg1 = arg1 13 | else { return } 14 | 15 | f(arg1, arg2, arg3, arg4) 16 | } 17 | } 18 | 19 | func papply(weak arg1: T1, _ f: @escaping (T1, T2, T3) -> Void) -> (T2, T3) -> Void { 20 | return { [weak arg1] (arg2: T2, arg3: T3) -> Void in 21 | guard let arg1 = arg1 22 | else { return } 23 | 24 | f(arg1, arg2, arg3) 25 | } 26 | } 27 | 28 | func papply(weak arg1: T1, _ f: @escaping (T1, T2) -> Void) -> (T2) -> Void { 29 | return { [weak arg1] (arg2: T2) -> Void in 30 | guard let arg1 = arg1 31 | else { return } 32 | 33 | f(arg1, arg2) 34 | } 35 | } 36 | 37 | func papply(weak arg1: T1, _ f: @escaping (T1) -> Void) -> () -> Void { 38 | return { [weak arg1] () -> Void in 39 | guard let arg1 = arg1 40 | else { return } 41 | 42 | f(arg1) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/CentralManagerDelegate.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | 3 | enum ConnectionChange { 4 | case connected 5 | case failedToConnect(Error?) 6 | case disconnected(Error?) 7 | } 8 | 9 | final class CentralManagerDelegate: NSObject, CBCentralManagerDelegate { 10 | 11 | typealias StateChangeHandler = (CBManagerState) -> Void 12 | typealias DiscoveryHandler = (CBPeripheral, AdvertisementData, RSSI) -> Void 13 | typealias ConnectionChangeHandler = (CBPeripheral, ConnectionChange) -> Void 14 | 15 | private let onStateChange: StateChangeHandler 16 | private let onDiscovery: DiscoveryHandler 17 | private let onConnectionChange: ConnectionChangeHandler 18 | 19 | init( 20 | onStateChange: @escaping StateChangeHandler, 21 | onDiscovery: @escaping DiscoveryHandler, 22 | onConnectionChange: @escaping ConnectionChangeHandler 23 | ) { 24 | self.onStateChange = onStateChange 25 | self.onDiscovery = onDiscovery 26 | self.onConnectionChange = onConnectionChange 27 | } 28 | 29 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 30 | onStateChange(central.state) 31 | } 32 | 33 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber) { 34 | onDiscovery(peripheral, advertisementData, rssi.intValue) 35 | } 36 | 37 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 38 | onConnectionChange(peripheral, .connected) 39 | } 40 | 41 | func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 42 | onConnectionChange(peripheral, .failedToConnect(error)) 43 | } 44 | 45 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 46 | onConnectionChange(peripheral, .disconnected(error)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/OnOff.swift: -------------------------------------------------------------------------------- 1 | enum OnOff { 2 | 3 | case on 4 | case off 5 | 6 | var isOn: Bool { 7 | switch self { 8 | case .on: 9 | return true 10 | case .off: 11 | return false 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/PeripheralDelegate.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | 3 | final class PeripheralDelegate: NSObject, CBPeripheralDelegate { 4 | 5 | typealias ServicesDiscoveryHandler = (CBPeripheral, Error?) -> Void 6 | typealias CharacteristicsDiscoverHandler = (CBService, Error?) -> Void 7 | typealias CharacteristicNotificationStateUpdateHandler = (CBCharacteristic, Error?) -> Void 8 | typealias CharacteristicValueUpdateHandler = (CBCharacteristic, Error?) -> Void 9 | typealias CharacteristicValueWriteHandler = (CBCharacteristic, Error?) -> Void 10 | typealias ReadRssiHandler = (CBPeripheral, Int, Error?) -> Void 11 | 12 | private let onServicesDiscovery: ServicesDiscoveryHandler 13 | private let onCharacteristicsDiscovery: CharacteristicsDiscoverHandler 14 | private let onCharacteristicNotificationStateUpdate: CharacteristicNotificationStateUpdateHandler 15 | private let onCharacteristicValueUpdate: CharacteristicValueUpdateHandler 16 | private let onCharacteristicValueWrite: CharacteristicValueWriteHandler 17 | private let onReadRssi: ReadRssiHandler 18 | 19 | init( 20 | onServicesDiscovery: @escaping ServicesDiscoveryHandler, 21 | onCharacteristicsDiscovery: @escaping CharacteristicsDiscoverHandler, 22 | onCharacteristicNotificationStateUpdate: @escaping CharacteristicNotificationStateUpdateHandler, 23 | onCharacteristicValueUpdate: @escaping CharacteristicValueUpdateHandler, 24 | onCharacteristicValueWrite: @escaping CharacteristicValueWriteHandler, 25 | onReadRssi: @escaping ReadRssiHandler 26 | ) { 27 | self.onServicesDiscovery = onServicesDiscovery 28 | self.onCharacteristicsDiscovery = onCharacteristicsDiscovery 29 | self.onCharacteristicNotificationStateUpdate = onCharacteristicNotificationStateUpdate 30 | self.onCharacteristicValueUpdate = onCharacteristicValueUpdate 31 | self.onCharacteristicValueWrite = onCharacteristicValueWrite 32 | self.onReadRssi = onReadRssi 33 | } 34 | 35 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 36 | onServicesDiscovery(peripheral, error) 37 | } 38 | 39 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 40 | onCharacteristicsDiscovery(service, error) 41 | } 42 | 43 | func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { 44 | onCharacteristicNotificationStateUpdate(characteristic, error) 45 | } 46 | 47 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 48 | onCharacteristicValueUpdate(characteristic, error) 49 | } 50 | 51 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { 52 | onCharacteristicValueWrite(characteristic, error) 53 | } 54 | 55 | func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { 56 | onReadRssi(peripheral, RSSI.intValue, error) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/CharacteristicNotify/CharacteristicNotifyTaskController.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | 3 | struct CharacteristicNotifyTaskController: PeripheralTaskController { 4 | 5 | typealias TaskSpec = CharacteristicNotifyTaskSpec 6 | 7 | private let task: SubjectTask 8 | 9 | init(_ task: SubjectTask) { 10 | self.task = task 11 | } 12 | 13 | func start(characteristic: CBCharacteristic) -> SubjectTask { 14 | guard let peripheral = characteristic.service?.peripheral 15 | else { return task.with(state: task.state.finished(CharacteristicNotifyError.unExpected)) } 16 | 17 | peripheral.setNotifyValue(task.params.state.isOn, for: characteristic) 18 | return task.with(state: task.state.processing(.applying)) 19 | } 20 | 21 | func cancel(error: Error) -> SubjectTask { 22 | return task.with(state: task.state.finished(error)) 23 | } 24 | 25 | func complete(error: Error?) -> SubjectTask { 26 | return task.with(state: task.state.finished(error)) 27 | } 28 | 29 | private enum CharacteristicNotifyError: Error { 30 | case unExpected 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/CharacteristicNotify/CharacteristicNotifyTaskSpec.swift: -------------------------------------------------------------------------------- 1 | struct CharacteristicNotifyTaskSpec: PeripheralTaskSpec { 2 | 3 | typealias Key = CharacteristicInstance 4 | 5 | struct Params { 6 | 7 | let state: OnOff 8 | } 9 | 10 | enum Stage { 11 | 12 | case applying 13 | } 14 | 15 | typealias Result = Error? 16 | 17 | static let tag = "NOTIFY" 18 | 19 | static func isMember(_ key: Key, of group: PeripheralID) -> Bool { 20 | return key.peripheralID == group 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/CharacteristicWrite/CharacteristicWriteTaskController.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | 3 | struct CharacteristicWriteTaskController: PeripheralTaskController { 4 | 5 | typealias TaskSpec = CharacteristicWriteTaskSpec 6 | 7 | private let task: SubjectTask 8 | 9 | init(_ task: SubjectTask) { 10 | self.task = task 11 | } 12 | 13 | func start(peripheral: CBPeripheral) -> SubjectTask { 14 | guard 15 | peripheral.state == .connected, 16 | let service = peripheral.services?.filter({ $0.uuid == task.key.serviceID })[Int(task.key.serviceInstanceID) ?? 0], 17 | let characteristic = service.characteristics?.filter({ $0.uuid == task.key.id })[Int(task.key.instanceID) ?? 0], 18 | characteristic.properties.contains(.write) 19 | else { 20 | return task.with(state: task.state.finished(PluginError.internalInconcictency(details: nil))) 21 | } 22 | 23 | peripheral.writeValue(task.params.value, for: characteristic, type: .withResponse) 24 | 25 | return task.with(state: task.state.processing(.writing)) 26 | } 27 | 28 | func cancel(error: Error) -> SubjectTask { 29 | return task.with(state: task.state.finished(error)) 30 | } 31 | 32 | func handleWrite(error: Error?) -> SubjectTask { 33 | return task.with(state: task.state.finished(error)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/CharacteristicWrite/CharacteristicWriteTaskSpec.swift: -------------------------------------------------------------------------------- 1 | struct CharacteristicWriteTaskSpec: PeripheralTaskSpec { 2 | 3 | typealias Key = CharacteristicInstance 4 | 5 | struct Params { 6 | 7 | let value: Data 8 | } 9 | 10 | enum Stage { 11 | 12 | case writing 13 | } 14 | 15 | typealias Result = Error? 16 | 17 | static let tag = "WRITE" 18 | 19 | static func isMember(_ key: Key, of group: PeripheralID) -> Bool { 20 | return key.peripheralID == group 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/Connect/ConnectTaskController.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | 3 | struct ConnectTaskController: PeripheralTaskController { 4 | 5 | typealias TaskSpec = ConnectTaskSpec 6 | 7 | private let task: SubjectTask 8 | 9 | init(_ task: SubjectTask) { 10 | self.task = task 11 | } 12 | 13 | func connect(centralManager: CBCentralManager, peripheral: CBPeripheral) -> SubjectTask { 14 | guard case .pending = task.state 15 | else { 16 | assert(false) 17 | return task 18 | } 19 | 20 | centralManager.connect(peripheral) 21 | 22 | return task.with(state: task.state.processing(.connecting)) 23 | } 24 | 25 | func handleConnectionChange(_ connectionChange: ConnectionChange) -> SubjectTask { 26 | guard case .processing(since: _, .connecting) = task.state 27 | else { 28 | assert(false) 29 | return task 30 | } 31 | 32 | return task.with(state: task.state.finished(connectionChange)) 33 | } 34 | 35 | func cancel(centralManager: CBCentralManager, peripheral: CBPeripheral, error: Error?) -> SubjectTask { 36 | switch task.state { 37 | case .pending: 38 | return task.with(state: task.state.finished(.failedToConnect(error))) 39 | case .processing(since: _, .connecting): 40 | centralManager.cancelPeripheralConnection(peripheral) 41 | return task.with(state: task.state.finished(.failedToConnect(error))) 42 | case .finished: 43 | assert(false) 44 | return task 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/Connect/ConnectTaskSpec.swift: -------------------------------------------------------------------------------- 1 | struct ConnectTaskSpec: PeripheralTaskSpec { 2 | 3 | typealias Key = PeripheralID 4 | 5 | struct Params {} 6 | 7 | enum Stage { 8 | 9 | case connecting 10 | } 11 | 12 | typealias Result = ConnectionChange 13 | 14 | static let tag = "CONNECT" 15 | 16 | static func isMember(_ key: Key, of group: PeripheralID) -> Bool { 17 | return key == group 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/PeripheralTaskRegistry/PeripheralTask.swift: -------------------------------------------------------------------------------- 1 | protocol PeripheralTaskSpec { 2 | 3 | associatedtype Key: Equatable 4 | associatedtype Group = PeripheralID 5 | associatedtype Params 6 | associatedtype Stage 7 | associatedtype Result 8 | 9 | static var tag: String { get } 10 | 11 | static func isMember(_ key: Key, of group: Group) -> Bool 12 | } 13 | 14 | enum PeripheralTaskState { 15 | 16 | case pending 17 | case processing(since: Date, Stage) 18 | case finished(in: TimeInterval, Result) 19 | 20 | func processing(_ stage: Stage) -> PeripheralTaskState { 21 | switch self { 22 | case .pending: 23 | return .processing(since: Date(), stage) 24 | case .processing(let start, _): 25 | return .processing(since: start, stage) 26 | case .finished: 27 | assert(false, "Invalid transition: \(self) -> .processing") 28 | return .processing(since: .distantPast, stage) 29 | } 30 | } 31 | 32 | func finished(_ result: Result) -> PeripheralTaskState { 33 | switch self { 34 | case .pending: 35 | return .finished(in: 0, result) 36 | case .processing(let start, _): 37 | return .finished(in: -start.timeIntervalSinceNow, result) 38 | case .finished: 39 | assert(false, "Invalid transition: \(self) -> .finished") 40 | return .finished(in: 0, result) 41 | } 42 | } 43 | } 44 | 45 | struct PeripheralTask { 46 | 47 | typealias Key = Spec.Key 48 | typealias Group = Spec.Group 49 | typealias Params = Spec.Params 50 | typealias Stage = Spec.Stage 51 | typealias Result = Spec.Result 52 | 53 | typealias State = PeripheralTaskState 54 | typealias Timeout = (duration: TimeInterval, handler: () -> Void) 55 | typealias CompletionHandler = (Result) -> Void 56 | 57 | let key: Key 58 | let params: Params 59 | let state: State 60 | let timeout: Timeout? 61 | let completion: CompletionHandler 62 | 63 | init(key: Key, params: Params, timeout: Timeout?, completion: @escaping CompletionHandler) { 64 | self.init(key: key, params: params, state: .pending, timeout: timeout, completion: completion) 65 | } 66 | 67 | private init(key: Key, params: Params, state: State, timeout: Timeout?, completion: @escaping CompletionHandler) { 68 | self.key = key 69 | self.params = params 70 | self.state = state 71 | self.timeout = timeout 72 | self.completion = completion 73 | } 74 | 75 | func with(state newState: State) -> PeripheralTask { 76 | return PeripheralTask( 77 | key: key, 78 | params: params, 79 | state: newState, 80 | timeout: timeout, 81 | completion: completion 82 | ) 83 | } 84 | 85 | func isMember(of group: Group) -> Bool { 86 | return Spec.isMember(key, of: group) 87 | } 88 | 89 | func iif(finished: (TimeInterval, Result) -> T, otherwise: () -> T) -> T { 90 | switch state { 91 | case .pending, .processing: 92 | return otherwise() 93 | case .finished(let duration, let result): 94 | return finished(duration, result) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/PeripheralTaskRegistry/PeripheralTaskController.swift: -------------------------------------------------------------------------------- 1 | protocol PeripheralTaskController { 2 | 3 | associatedtype TaskSpec: PeripheralTaskSpec 4 | 5 | typealias SubjectTask = PeripheralTask 6 | 7 | init(_ task: SubjectTask) 8 | } 9 | 10 | extension PeripheralTaskController { 11 | 12 | func improperHandling(currentState: SubjectTask.State, handler: String = #function) -> Error { 13 | return PluginError.internalInconcictency(details: "for a task in state \(currentState) a forbidden handler \(handler) is called") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/ReadRssi/ReadRssiTaskController.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | 3 | struct ReadRssiTaskController: PeripheralTaskController { 4 | typealias TaskSpec = ReadRssiTaskSpec 5 | 6 | private let task: SubjectTask 7 | 8 | init(_ task: SubjectTask) { 9 | self.task = task 10 | } 11 | 12 | func start(peripheral: CBPeripheral) -> SubjectTask { 13 | // error if not connected 14 | guard peripheral.state == .connected 15 | else { 16 | return task.with( 17 | state: task.state.finished( 18 | .failure(PluginError.internalInconcictency(details: nil)) 19 | ) 20 | ) 21 | } 22 | 23 | peripheral.readRSSI() 24 | 25 | return task.with(state: task.state.processing(.readingRssi)) 26 | } 27 | 28 | func cancel(error: Error) -> SubjectTask { 29 | return task 30 | .with(state: task.state.finished(.failure(error))) 31 | } 32 | 33 | func handleReadRssi(rssi: Int, error: Error?) -> SubjectTask { 34 | if let error = error { 35 | return task 36 | .with(state: task.state.finished(.failure(error))) 37 | } else { 38 | return task 39 | .with(state: task.state.finished(.success(rssi))) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/ReadRssi/ReadRssiTaskSpec.swift: -------------------------------------------------------------------------------- 1 | struct ReadRssiTaskSpec: PeripheralTaskSpec { 2 | typealias Key = PeripheralID 3 | 4 | struct Params {} 5 | 6 | enum Stage { 7 | case readingRssi 8 | } 9 | 10 | typealias Result = Failable 11 | 12 | static let tag = "READ RSSI" 13 | 14 | static func isMember(_ key: Key, of group: PeripheralID) -> Bool { 15 | return key == group 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/ServicesWithCharacteristicsDiscovery/ServicesWithCharacteristicsDiscoveryTaskSpec.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | 3 | struct ServicesWithCharacteristicsDiscoveryTaskSpec: PeripheralTaskSpec { 4 | 5 | typealias Key = PeripheralID 6 | 7 | struct Params { 8 | 9 | let servicesWithCharacteristicsToDiscover: ServicesWithCharacteristicsToDiscover 10 | } 11 | 12 | enum Stage { 13 | 14 | case discoveringServices 15 | case discoveringCharacteristics(servicesLeft: Int, errors: [Error]) 16 | } 17 | 18 | typealias Result = [Error] 19 | 20 | static let tag = "DISCOVER S&C" 21 | 22 | static func isMember(_ key: Key, of group: PeripheralID) -> Bool { 23 | return key == group 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBle/Tasks/ServicesWithCharacteristicsDiscovery/ServicesWithCharacteristicsToDiscover.swift: -------------------------------------------------------------------------------- 1 | enum ServicesWithCharacteristicsToDiscover: Equatable { 2 | 3 | case all 4 | case some([ServiceID: CharacteristicsToDiscover]) 5 | 6 | var isEmpty: Bool { 7 | switch self { 8 | case .all: 9 | return false 10 | case .some(let servicesWithCharacteristicsToDiscover): 11 | return servicesWithCharacteristicsToDiscover.isEmpty 12 | } 13 | } 14 | 15 | var services: [ServiceID]? { 16 | switch self { 17 | case .all: 18 | return nil 19 | case .some(let servicesWithCharacteristicsToDiscover): 20 | return Array(servicesWithCharacteristicsToDiscover.keys) 21 | } 22 | } 23 | } 24 | 25 | enum CharacteristicsToDiscover: Equatable { 26 | 27 | case all 28 | case some([CharacteristicID]) 29 | 30 | var characteristics: [CharacteristicID]? { 31 | switch self { 32 | case .all: 33 | return nil 34 | case .some(let characteristicsToDiscover): 35 | return characteristicsToDiscover 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBlePlugin.h: -------------------------------------------------------------------------------- 1 | #if TARGET_OS_OSX 2 | #import 3 | #else 4 | #import 5 | #endif 6 | 7 | @interface ReactiveBlePlugin : NSObject 8 | @end 9 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/Classes/ReactiveBlePlugin.m: -------------------------------------------------------------------------------- 1 | #import "ReactiveBlePlugin.h" 2 | #import 3 | 4 | @implementation ReactiveBlePlugin 5 | + (void)registerWithRegistrar:(NSObject*)registrar { 6 | [SwiftReactiveBlePlugin registerWithRegistrar:registrar]; 7 | } 8 | @end 9 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/darwin/reactive_ble_mobile.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'reactive_ble_mobile' 3 | s.version = '0.0.1' 4 | s.summary = 'Bluetooth Low Energy (BLE) Flutter plug-in' 5 | s.description = <<-DESC 6 | Bluetooth Low Energy (BLE) Flutter plug-in 7 | DESC 8 | s.homepage = 'https://github.com/PhilipsHue/flutter_reactive_ble' 9 | s.license = { :file => '../LICENSE' } 10 | s.author = { 'Your Company' => 'email@example.com' } 11 | s.source = { :path => '.' } 12 | s.source_files = 'Classes/**/*' 13 | s.public_header_files = 'Classes/**/*.h' 14 | s.dependency 'Protobuf', '~> 3.5' 15 | s.dependency 'SwiftProtobuf', '~> 1.0' 16 | s.ios.dependency 'Flutter' 17 | s.osx.dependency 'FlutterMacOS' 18 | s.ios.deployment_target = '11.0' 19 | s.osx.deployment_target = '10.13' 20 | s.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1' } 21 | s.swift_version = '4.2' 22 | end 23 | 24 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/lib/reactive_ble_mobile.dart: -------------------------------------------------------------------------------- 1 | library reactive_ble_mobile; 2 | 3 | export 'src/reactive_ble_mobile_platform.dart'; 4 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/lib/src/generated/bledata.pbenum.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated code. Do not modify. 3 | // source: bledata.proto 4 | // 5 | // @dart = 2.12 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names, library_prefixes 9 | // ignore_for_file: non_constant_identifier_names, prefer_final_fields 10 | // ignore_for_file: unnecessary_import, unnecessary_this, unused_import 11 | 12 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/lib/src/generated/bledata.pbserver.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated code. Do not modify. 3 | // source: bledata.proto 4 | // 5 | // @dart = 2.12 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes 10 | // ignore_for_file: non_constant_identifier_names, prefer_final_fields 11 | // ignore_for_file: unnecessary_import, unnecessary_this, unused_import 12 | 13 | export 'bledata.pb.dart'; 14 | 15 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/lib/src/select_from.dart: -------------------------------------------------------------------------------- 1 | T selectFrom(List values, 2 | {required int? index, required T Function(int? index) fallback}) { 3 | if (index != null && index >= 0 && index < values.length) { 4 | return values[index]; 5 | } 6 | return fallback(index); 7 | } 8 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/protos/README.md: -------------------------------------------------------------------------------- 1 | # Protobuf code generation 2 | 3 | 1. Install the newest `protoc`. It can be done via `brew`: 4 | 5 | ```sh 6 | brew install protobuf 7 | ``` 8 | 9 | 2. For Swift code generation install : 10 | 11 | ```sh 12 | brew install swift-protobuf 13 | ``` 14 | 15 | 3. If you don't have Dart SDK on your computer please install it. 16 | 17 | ```sh 18 | brew install dart 19 | ``` 20 | 21 | 4. Run `dart pub global activate protoc_plugin` 22 | 5. OPTIONAL Add plugin path to `PATH` environment variable 23 | 6. Run the following command from the "protos" directory 24 | 25 | ```sh 26 | protoc --dart_out=../lib/src/generated ./bledata.proto 27 | protoc --swift_out=../darwin/Classes/BleData ./bledata.proto 28 | ``` 29 | 30 | NOTE: If directory `../lib/generated` or `./darwin/Classes/BleData` does not exist please create it. 31 | -------------------------------------------------------------------------------- /packages/reactive_ble_mobile/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: reactive_ble_mobile 2 | description: Official Android and iOS implementation for the flutter_reactive_ble plugin. 3 | version: 5.4.0 4 | homepage: https://github.com/PhilipsHue/flutter_reactive_ble 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | flutter: ">=2.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | protobuf: ^3.0.0 14 | reactive_ble_platform_interface: ^5.4.0 15 | 16 | dev_dependencies: 17 | build_runner: ^2.3.3 18 | flutter_lints: ^1.0.4 19 | flutter_test: 20 | sdk: flutter 21 | mockito: ^5.0.14 22 | 23 | dependency_overrides: 24 | reactive_ble_platform_interface: 25 | path: ../reactive_ble_platform_interface 26 | 27 | flutter: 28 | plugin: 29 | platforms: 30 | android: 31 | package: com.signify.hue.flutterreactiveble 32 | pluginClass: ReactiveBlePlugin 33 | ios: 34 | pluginClass: ReactiveBlePlugin 35 | sharedDarwinSource: true 36 | macos: 37 | pluginClass: ReactiveBlePlugin 38 | sharedDarwinSource: true 39 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 5.4.0 2 | 3 | * Support for MacOS #889 4 | * Fixes subscription to _deviceConnector.deviceConnectionStateUpdateStream leaking #876 5 | * Workspace upgrade (dependencies) & gitignore sync #857 6 | * Run ktlint -F #766 7 | 8 | ## 5.3.1 9 | 10 | * Use proper platform_interface dependency version #837 11 | 12 | ## 5.3.0 13 | 14 | * Readd accidentally removed version constraint #789 15 | * Add a placeholder implementation for non-mobile platforms #688 16 | * Add Github actions and Migrate to latest Android SDK, AGP and protobuf plugin #830 17 | * Include packages lock-files #797 18 | * Update Kotlin version in README #831 19 | * Migrate away from deprecated strong mode analysis options #832 20 | * Read RSSI #796 21 | 22 | ## 5.2.0 23 | 24 | * Bump the minimum requirement to Dart 2.17 and upgrade melos to 3.1.0 in #762 25 | * swiftlint config and inital formatting pass in #765 26 | * Cancel subscription when a disconnect event has been thrown in #769 27 | * Fix typos in #778 28 | * Update CI config to use Xcode 14 in #786 29 | * Support multiple services or characteristics with the same id in #776 30 | * Breaking change: If a device has multiple characteristics with the same ID, `readCharacteristic`, `writeCharacteristic` and `subscribeToCharacteristic` used to select the first of those characteristics. Now they will fail in this case. Use `resolve` or `getDiscoveredServices` instead. 31 | 32 | ## 5.1.1 33 | 34 | * Make Connectable backwards compatible #757, #750 35 | 36 | ## 5.1.0 37 | 38 | * Add IsConnectable to discovery data. #750 39 | * Upgraded build_runner. #750 40 | 41 | ## 5.0.3 42 | 43 | * Enable extended advertising on android. Fix #571 44 | * Add additional null checks to scanRecord. Fix #521 45 | * Restore support for Xcode 12 46 | * Add verbose debug logging Fix #583 47 | 48 | ## 5.0.2 49 | 50 | * Revert Queue up messages on iOS until event channel is ready. Fix #439 51 | 52 | ## 5.0.1 53 | 54 | * Bump protobuf so it includes binaries for Mac M1 #396. 55 | 56 | ## 5.0.0 57 | 58 | ** Breaking change ** 59 | * DiscoveredService has now a new required property called `DiscoveredCharacteristic` which provides properties of the BLE characteristic. 60 | 61 | Other changes 62 | * Android 12 support. 63 | * Queue up messages on iOS until event channel is ready. Fixes #137, #251, #307, #385, #387. 64 | 65 | ## 4.0.1 66 | 67 | * Add support for iOS 15 68 | 69 | ## 4.0.0 70 | 71 | * Initial Open Source release. 72 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/LICENSE: -------------------------------------------------------------------------------- 1 | BSD license 2 | 3 | ©2019 Signify Holding. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 9 | and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/README.md: -------------------------------------------------------------------------------- 1 | # Reactive_ble_platform_interface 2 | 3 | A common platform interface for the [reactive ble](https://github.com/PhilipsHue/flutter_reactive_ble/) plugin. This package ensures every platform specific implementation uses the same interface. 4 | 5 | ## Usage 6 | 7 | To implement a new platform specific implementation extend the `ReactiveBlePlatform` with an implementation that performs the platform specific behavior. -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/reactive_ble_platform_interface.dart: -------------------------------------------------------------------------------- 1 | library reactive_ble_platform_interface; 2 | 3 | export 'src/logger.dart'; 4 | export 'src/models.dart'; 5 | export 'src/reactive_ble_platform_interface.dart'; 6 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/logger.dart: -------------------------------------------------------------------------------- 1 | import 'model/log_level.dart'; 2 | 3 | abstract class Logger { 4 | set logLevel(LogLevel logLevel); 5 | LogLevel get logLevel; 6 | 7 | void log(Object message); 8 | } 9 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/ble_status.dart: -------------------------------------------------------------------------------- 1 | /// The status of the BLE subsystem as reported by the platform API. 2 | enum BleStatus { 3 | /// Status is not (yet) determined. 4 | unknown, 5 | 6 | /// BLE is not supported on this device. 7 | unsupported, 8 | 9 | /// BLE usage is not authorized for this app. 10 | unauthorized, 11 | 12 | /// BLE is turned off. 13 | poweredOff, 14 | 15 | /// Android only: Location services are disabled. 16 | locationServicesDisabled, 17 | 18 | /// BLE is fully operating for this app. 19 | ready 20 | } 21 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/characteristic_instance.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'uuid.dart'; 4 | 5 | @immutable 6 | class CharacteristicInstance { 7 | /// Unique uuid of the specific characteristic 8 | final Uuid characteristicId; 9 | 10 | /// The id that identifies the specific instance of all characteristics with [characteristicId] in a given service. 11 | /// This should no be exposed to user of the plugin as it may be different each time a device is connected to, so it 12 | /// should not be used to identify characteristics across sessions. 13 | final String characteristicInstanceId; 14 | 15 | /// Service uuid of the characteristic 16 | final Uuid serviceId; 17 | 18 | /// The id that identifies the specific instance of all services with [serviceId] in a given device. 19 | /// This should no be exposed to user of the plugin as it may be different each time a device is connected to, so it 20 | /// should not be used to identify characteristics across sessions. 21 | final String serviceInstanceId; 22 | 23 | /// Device id of the BLE device 24 | final String deviceId; 25 | 26 | const CharacteristicInstance({ 27 | required this.characteristicId, 28 | required this.characteristicInstanceId, 29 | required this.serviceId, 30 | required this.serviceInstanceId, 31 | required this.deviceId, 32 | }); 33 | 34 | @override 35 | String toString() => "$runtimeType(characteristicId: $characteristicId($characteristicInstanceId), " 36 | "serviceId: $serviceId($serviceInstanceId), deviceId: $deviceId)"; 37 | 38 | @override 39 | int get hashCode => Object.hash( 40 | characteristicId.expanded, 41 | characteristicInstanceId, 42 | serviceId.expanded, 43 | serviceInstanceId, 44 | deviceId, 45 | ); 46 | 47 | @override 48 | bool operator ==(Object other) => 49 | other is CharacteristicInstance && 50 | runtimeType == other.runtimeType && 51 | characteristicId.expanded == other.characteristicId.expanded && 52 | characteristicInstanceId == other.characteristicInstanceId && 53 | serviceId.expanded == other.serviceId.expanded && 54 | serviceInstanceId == other.serviceInstanceId && 55 | deviceId == other.deviceId; 56 | } 57 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/characteristic_value.dart: -------------------------------------------------------------------------------- 1 | import 'characteristic_instance.dart'; 2 | import 'generic_failure.dart'; 3 | import 'result.dart'; 4 | 5 | /// Value update for specific [CharacteristicInstance]. 6 | class CharacteristicValue { 7 | final CharacteristicInstance characteristic; 8 | final Result, GenericFailure?> 9 | result; 10 | 11 | const CharacteristicValue( 12 | {required this.characteristic, required this.result}); 13 | 14 | @override 15 | String toString() => 16 | "$runtimeType(characteristic: $characteristic, value: $result)"; 17 | } 18 | 19 | /// Error type for characteristic value update. 20 | enum CharacteristicValueUpdateError { unknown } 21 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/clear_gatt_cache_error.dart: -------------------------------------------------------------------------------- 1 | /// Error type of Clear Gatt cache operation. 2 | enum ClearGattCacheError { 3 | /// Reason of failure is unknown. 4 | unknown, 5 | 6 | /// Clear gatt cache is not supported on this OS. 7 | operationNotSupported 8 | } 9 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/connection_priority.dart: -------------------------------------------------------------------------------- 1 | import '../model/unit.dart'; 2 | import 'generic_failure.dart'; 3 | import 'result.dart'; 4 | 5 | /// The priority that can be requested to update the connection parameter. 6 | enum ConnectionPriority { 7 | /// connection with recommended parameters. 8 | balanced, 9 | 10 | /// high priority, low latency connection. 11 | highPerformance, 12 | // reduced power, low data rate connection. 13 | lowPower, 14 | } 15 | 16 | ///util function to convert priority to a integer. 17 | int convertPriorityToInt(ConnectionPriority priority) { 18 | switch (priority) { 19 | case ConnectionPriority.balanced: 20 | return 0; 21 | case ConnectionPriority.highPerformance: 22 | return 1; 23 | case ConnectionPriority.lowPower: 24 | return 2; 25 | default: 26 | assert(false); 27 | return -1000; 28 | } 29 | } 30 | 31 | /// Result of the connection priority request 32 | class ConnectionPriorityInfo { 33 | const ConnectionPriorityInfo({required this.result}); 34 | 35 | final Result?> result; 36 | } 37 | 38 | /// Error type for connection priority. 39 | enum ConnectionPriorityFailure { unknown } 40 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/connection_state_update.dart: -------------------------------------------------------------------------------- 1 | import 'package:functional_data/functional_data.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import 'generic_failure.dart'; 5 | 6 | part 'connection_state_update.g.dart'; 7 | //ignore_for_file: annotate_overrides 8 | 9 | ///Status update for a specific BLE device. 10 | @immutable 11 | @FunctionalData() 12 | class ConnectionStateUpdate extends $ConnectionStateUpdate { 13 | final String deviceId; 14 | final DeviceConnectionState connectionState; 15 | 16 | /// Field `error` is null if there is no error reported. 17 | final GenericFailure? failure; 18 | 19 | const ConnectionStateUpdate({ 20 | required this.deviceId, 21 | required this.connectionState, 22 | required this.failure, 23 | }); 24 | } 25 | 26 | /// Connection status. 27 | enum DeviceConnectionState { 28 | /// Currently establishing a connection. 29 | connecting, 30 | 31 | /// Connection is established. 32 | connected, 33 | 34 | /// Terminating the connection. 35 | disconnecting, 36 | 37 | /// Device is disconnected. 38 | disconnected 39 | } 40 | 41 | /// Type of connection error. 42 | enum ConnectionError { 43 | /// Connection failed for an unknown reason. 44 | unknown, 45 | 46 | /// An attempt to connect was made but it failed. 47 | failedToConnect, 48 | } 49 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/connection_state_update.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'connection_state_update.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalDataGenerator 7 | // ************************************************************************** 8 | 9 | abstract class $ConnectionStateUpdate { 10 | const $ConnectionStateUpdate(); 11 | 12 | String get deviceId; 13 | DeviceConnectionState get connectionState; 14 | GenericFailure? get failure; 15 | 16 | ConnectionStateUpdate copyWith({ 17 | String? deviceId, 18 | DeviceConnectionState? connectionState, 19 | GenericFailure? failure, 20 | }) => 21 | ConnectionStateUpdate( 22 | deviceId: deviceId ?? this.deviceId, 23 | connectionState: connectionState ?? this.connectionState, 24 | failure: failure ?? this.failure, 25 | ); 26 | 27 | ConnectionStateUpdate copyUsing( 28 | void Function(ConnectionStateUpdate$Change change) mutator) { 29 | final change = ConnectionStateUpdate$Change._( 30 | this.deviceId, 31 | this.connectionState, 32 | this.failure, 33 | ); 34 | mutator(change); 35 | return ConnectionStateUpdate( 36 | deviceId: change.deviceId, 37 | connectionState: change.connectionState, 38 | failure: change.failure, 39 | ); 40 | } 41 | 42 | @override 43 | String toString() => 44 | "ConnectionStateUpdate(deviceId: $deviceId, connectionState: $connectionState, failure: $failure)"; 45 | 46 | @override 47 | // ignore: avoid_equals_and_hash_code_on_mutable_classes 48 | bool operator ==(Object other) => 49 | other is ConnectionStateUpdate && 50 | other.runtimeType == runtimeType && 51 | deviceId == other.deviceId && 52 | connectionState == other.connectionState && 53 | failure == other.failure; 54 | 55 | @override 56 | // ignore: avoid_equals_and_hash_code_on_mutable_classes 57 | int get hashCode { 58 | var result = 17; 59 | result = 37 * result + deviceId.hashCode; 60 | result = 37 * result + connectionState.hashCode; 61 | result = 37 * result + failure.hashCode; 62 | return result; 63 | } 64 | } 65 | 66 | class ConnectionStateUpdate$Change { 67 | ConnectionStateUpdate$Change._( 68 | this.deviceId, 69 | this.connectionState, 70 | this.failure, 71 | ); 72 | 73 | String deviceId; 74 | DeviceConnectionState connectionState; 75 | GenericFailure? failure; 76 | } 77 | 78 | // ignore: avoid_classes_with_only_static_members 79 | class ConnectionStateUpdate$ { 80 | static final deviceId = Lens( 81 | (deviceIdContainer) => deviceIdContainer.deviceId, 82 | (deviceIdContainer, deviceId) => 83 | deviceIdContainer.copyWith(deviceId: deviceId), 84 | ); 85 | 86 | static final connectionState = 87 | Lens( 88 | (connectionStateContainer) => connectionStateContainer.connectionState, 89 | (connectionStateContainer, connectionState) => 90 | connectionStateContainer.copyWith(connectionState: connectionState), 91 | ); 92 | 93 | static final failure = 94 | Lens?>( 95 | (failureContainer) => failureContainer.failure, 96 | (failureContainer, failure) => failureContainer.copyWith(failure: failure), 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/discovered_characteristic.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'uuid.dart'; 4 | 5 | /// Specific BLE characteristic for a BLE device characterized by [deviceId], [serviceId] and 6 | /// [characteristicId]. 7 | @immutable 8 | class DiscoveredCharacteristic { 9 | /// Unique uuid of the specific characteristic 10 | final Uuid characteristicId; 11 | 12 | final String characteristicInstanceId; 13 | 14 | /// Service uuid of the characteristic 15 | final Uuid serviceId; 16 | 17 | /// Properties 18 | final bool isReadable; 19 | final bool isWritableWithResponse; 20 | final bool isWritableWithoutResponse; 21 | final bool isNotifiable; 22 | final bool isIndicatable; 23 | 24 | const DiscoveredCharacteristic({ 25 | required this.characteristicId, 26 | required this.characteristicInstanceId, 27 | required this.serviceId, 28 | required this.isReadable, 29 | required this.isWritableWithResponse, 30 | required this.isWritableWithoutResponse, 31 | required this.isNotifiable, 32 | required this.isIndicatable, 33 | }); 34 | 35 | @override 36 | String toString() => 37 | "$runtimeType(characteristicId: $characteristicId, serviceId: $serviceId)"; 38 | 39 | @override 40 | int get hashCode => 41 | (((17 * 37) + characteristicId.hashCode) * 37 + serviceId.hashCode) * 37; 42 | 43 | @override 44 | bool operator ==(Object other) => 45 | other is DiscoveredCharacteristic && 46 | runtimeType == other.runtimeType && 47 | characteristicId == other.characteristicId && 48 | serviceId == other.serviceId; 49 | } 50 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/discovered_device.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:functional_data/functional_data.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | import '../../reactive_ble_platform_interface.dart'; 8 | 9 | part 'discovered_device.g.dart'; 10 | 11 | // ignore_for_file: annotate_overrides, avoid_classes_with_only_static_members, non_constant_identifier_names 12 | 13 | /// Result of a scan interval. 14 | @immutable 15 | @FunctionalData() 16 | class ScanResult extends $ScanResult { 17 | final Result?> result; 18 | 19 | const ScanResult({required this.result}); 20 | 21 | @override 22 | String toString() => "$ScanResult(result: $result)"; 23 | } 24 | 25 | ///Ble device that is discovered during scanning. 26 | @immutable 27 | @FunctionalData() 28 | class DiscoveredDevice extends $DiscoveredDevice { 29 | /// The unique identifier of the device. 30 | final String id; 31 | final String name; 32 | @CustomEquality(DeepCollectionEquality()) 33 | final Map serviceData; 34 | 35 | /// Advertised services 36 | @CustomEquality(DeepCollectionEquality()) 37 | final List serviceUuids; 38 | 39 | /// Manufacturer specific data. The first 2 bytes are the Company Identifier Codes. 40 | @CustomEquality(DeepCollectionEquality()) 41 | final Uint8List manufacturerData; 42 | 43 | final int rssi; 44 | 45 | final Connectable connectable; 46 | 47 | const DiscoveredDevice({ 48 | required this.id, 49 | required this.name, 50 | required this.serviceData, 51 | required this.manufacturerData, 52 | required this.rssi, 53 | required this.serviceUuids, 54 | this.connectable = Connectable.unknown, 55 | }); 56 | } 57 | 58 | ///Connection status of the BLE device. 59 | enum ConnectionStatus { 60 | /// Device is disconnected. 61 | disconnected, 62 | 63 | /// A connection is being established. 64 | connecting, 65 | 66 | /// Connected with Device. 67 | connected, 68 | 69 | /// Device is being disconnected. 70 | disconnecting, 71 | } 72 | 73 | /// Failure type of device discovery. 74 | enum ScanFailure { unknown } 75 | 76 | /// Shows if the device is ready to be connected to from a discovery perspective 77 | enum Connectable { unknown, unavailable, available } 78 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/discovered_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | import 'package:reactive_ble_platform_interface/src/model/discovered_characteristic.dart'; 4 | 5 | import 'uuid.dart'; 6 | 7 | part 'discovered_service.g.dart'; 8 | 9 | //ignore_for_file: annotate_overrides 10 | 11 | @FunctionalData() 12 | class DiscoveredService extends $DiscoveredService { 13 | const DiscoveredService({ 14 | required this.serviceId, 15 | required this.serviceInstanceId, 16 | required this.characteristicIds, 17 | required this.characteristics, 18 | this.includedServices = const [], 19 | }); 20 | 21 | final Uuid serviceId; 22 | 23 | final String serviceInstanceId; 24 | 25 | @CustomEquality(DeepCollectionEquality()) 26 | final List characteristicIds; 27 | 28 | @CustomEquality(DeepCollectionEquality()) 29 | final List characteristics; 30 | 31 | @CustomEquality(DeepCollectionEquality()) 32 | final List includedServices; 33 | } 34 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/generic_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Error result of a BLE operation. 4 | @immutable 5 | class GenericFailure { 6 | /// Code that classifies the failure. 7 | final T code; 8 | 9 | /// String that provides additional context of the failure. 10 | final String message; 11 | 12 | const GenericFailure({required this.code, required this.message}); 13 | 14 | @override 15 | String toString() => "$runtimeType(code: $code, message: \"$message\")"; 16 | 17 | @override 18 | bool operator ==(Object other) => 19 | identical(this, other) || 20 | (other is GenericFailure && 21 | code == other.code && 22 | message == other.message); 23 | 24 | @override 25 | int get hashCode { 26 | var result = 17; 27 | result = 37 * result + code.hashCode; 28 | result = 37 * result + message.hashCode; 29 | return result; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/log_level.dart: -------------------------------------------------------------------------------- 1 | /// States the level of debug logging within this library 2 | enum LogLevel { 3 | /// No debugLogging at all. This is the default level within the library. 4 | none, 5 | 6 | /// Verbose logging, this can be useful when debugging issues. 7 | verbose, 8 | } 9 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/qualified_characteristic.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'uuid.dart'; 4 | 5 | /// Specific BLE characteristic for a BLE device characterized by [deviceId], [serviceId] and 6 | /// [characteristicId]. 7 | @immutable 8 | class QualifiedCharacteristic { 9 | /// Unique uuid of the specific characteristic 10 | final Uuid characteristicId; 11 | 12 | /// Service uuid of the characteristic 13 | final Uuid serviceId; 14 | 15 | /// Device id of the BLE device 16 | final String deviceId; 17 | 18 | const QualifiedCharacteristic({ 19 | required this.characteristicId, 20 | required this.serviceId, 21 | required this.deviceId, 22 | }); 23 | 24 | @override 25 | String toString() => 26 | "$runtimeType(characteristicId: $characteristicId, serviceId: $serviceId, deviceId: $deviceId)"; 27 | 28 | @override 29 | int get hashCode => 30 | (((17 * 37) + characteristicId.hashCode) * 37 + serviceId.hashCode) * 37 + 31 | deviceId.hashCode; 32 | 33 | @override 34 | bool operator ==(Object other) => 35 | other is QualifiedCharacteristic && 36 | runtimeType == other.runtimeType && 37 | characteristicId == other.characteristicId && 38 | serviceId == other.serviceId && 39 | deviceId == other.deviceId; 40 | } 41 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/result.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | ///Result of a ble operation. 4 | /// 5 | /// In case the result is successful [Failure] is null. 6 | @immutable 7 | class Result { 8 | const Result.success(this._value) 9 | : _failure = null; // ignore: avoid_field_initializers_in_const_classes 10 | const Result.failure(this._failure) 11 | : assert(_failure != null), 12 | _value = null; // ignore: avoid_field_initializers_in_const_classes 13 | 14 | /// Provides the value in case of success or throws [Exception] in case of failure. 15 | Value dematerialize() => iif( 16 | success: (value) => value!, 17 | failure: (failure) { 18 | if (failure is Exception) { 19 | // ignore: only_throw_errors 20 | throw failure; 21 | } else { 22 | throw Exception(failure); 23 | } 24 | }, 25 | ); 26 | 27 | /// Execute specific actions on success and on failure. 28 | T iif( 29 | {required T Function(Value value) success, 30 | required T Function(Failure failure) failure}) { 31 | assert(_value == null || _failure == null); 32 | 33 | if (_failure != null) { 34 | return failure(_failure!); 35 | } else if (_value != null) { 36 | return success(_value!); 37 | } else { 38 | throw Exception('Both value and failure cannot be null'); 39 | } 40 | } 41 | 42 | @override 43 | String toString() { 44 | if (_value != null && _failure == null) { 45 | return "$runtimeType.success($_value)"; 46 | } else if (_failure != null && _value == null) { 47 | return "$runtimeType.failure($_failure)"; 48 | } else { 49 | return "$runtimeType._unsafe(value: $_value, failure: $_failure)"; 50 | } 51 | } 52 | 53 | @override 54 | int get hashCode => 55 | ((17 * 37) + (_value?.hashCode ?? 0)) * 37 + (_failure?.hashCode ?? 0); 56 | 57 | @override 58 | bool operator ==(Object other) => 59 | other is Result && 60 | runtimeType == other.runtimeType && 61 | _value == other._value && 62 | _failure == other._failure; 63 | 64 | final Value? _value; 65 | final Failure? _failure; 66 | } 67 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/scan_mode.dart: -------------------------------------------------------------------------------- 1 | /// Android only: mode in which BLE discovery is executed. 2 | enum ScanMode { 3 | /// passively listen for other scan results without starting BLE scan itself. 4 | opportunistic, 5 | 6 | /// Scan mode which has the lowest battery consumption. 7 | lowPower, 8 | 9 | /// Scan mode that is a good compromise between battery consumption and latency. 10 | balanced, 11 | 12 | /// Scan mode with highest battery consumption and lowest latency. 13 | /// Should not be used when scanning for a long time. 14 | lowLatency, 15 | } 16 | 17 | /// Converts [ScanMode] to integer representation. 18 | int convertScanModeToArgs(ScanMode scanMode) { 19 | switch (scanMode) { 20 | case ScanMode.opportunistic: 21 | return -1; 22 | case ScanMode.lowPower: 23 | return 0; 24 | case ScanMode.balanced: 25 | return 1; 26 | case ScanMode.lowLatency: 27 | return 2; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/scan_session.dart: -------------------------------------------------------------------------------- 1 | import 'uuid.dart'; 2 | 3 | class ScanSession { 4 | final List withServices; 5 | final Future future; 6 | 7 | const ScanSession({required this.withServices, required this.future}); 8 | } 9 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/unit.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | @immutable 4 | class Unit { 5 | const Unit(); 6 | 7 | @override 8 | bool operator ==(Object other) => other.runtimeType == runtimeType; 9 | 10 | @override 11 | int get hashCode => 1; 12 | } 13 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/uuid.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | /// Representation of a 16, 32 or 128-bit number used to identify BLE services and characteristics. 7 | class Uuid { 8 | final Uint8List data; 9 | 10 | Uuid(List data) : data = Uint8List.fromList(data); 11 | 12 | factory Uuid.parse(String string) { 13 | final data = Uint8List(16); 14 | 15 | var byteOffset = 0; 16 | for (var substringStart = 0; substringStart < string.length;) { 17 | if (string[substringStart] == "-") { 18 | substringStart += 1; 19 | continue; 20 | } 21 | 22 | if (byteOffset >= 16 || substringStart + 2 > string.length) { 23 | throw _UuidParseFailure(string); 24 | } 25 | 26 | final byte = int.tryParse( 27 | string.substring(substringStart, substringStart + 2), 28 | radix: 16, 29 | ); 30 | if (byte == null) throw _UuidParseFailure(string); 31 | 32 | data[byteOffset] = byte; 33 | 34 | byteOffset += 1; 35 | substringStart += 2; 36 | } 37 | if (byteOffset == 2 || byteOffset == 4 || byteOffset == 16) { 38 | return Uuid(data.buffer.asUint8List(0, byteOffset)); 39 | } else { 40 | throw _UuidParseFailure(string); 41 | } 42 | } 43 | 44 | /// Expands a 16 bit uuid to a 128 bit one 45 | Uuid get expanded { 46 | if (data.length == 2) { 47 | return Uuid.parse("0000$this-0000-1000-8000-00805f9b34fb"); 48 | } else { 49 | return this; 50 | } 51 | } 52 | 53 | @override 54 | String toString() { 55 | String paddedHex(int num) { 56 | final text = num.toRadixString(16); 57 | return text.length < 2 ? "0$text" : text; 58 | } 59 | 60 | final buffer = StringBuffer(); 61 | final groupLength = [4, 2, 2, 2, 6]; 62 | var group = 0, inGroupOffset = 0; 63 | for (final octet in data) { 64 | if (group < groupLength.length && inGroupOffset >= groupLength[group]) { 65 | buffer.write("-"); 66 | group += 1; 67 | inGroupOffset = 0; 68 | } 69 | buffer.write(paddedHex(octet)); 70 | inGroupOffset += 1; 71 | } 72 | return buffer.toString(); 73 | } 74 | 75 | @override 76 | int get hashCode => 77 | data.fold(17, (hash, octet) => 37 * hash + octet.hashCode); 78 | 79 | @override 80 | bool operator ==(Object other) => 81 | other is Uuid && 82 | other.runtimeType == runtimeType && 83 | const DeepCollectionEquality().equals(other.data, data); 84 | } 85 | 86 | @immutable 87 | class _UuidParseFailure implements Exception { 88 | final String string; 89 | 90 | const _UuidParseFailure(this.string); 91 | 92 | @override 93 | String toString() => "$runtimeType(\"$string\")"; 94 | } 95 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/model/write_characteristic_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'characteristic_instance.dart'; 4 | import 'generic_failure.dart'; 5 | import 'result.dart'; 6 | import 'unit.dart'; 7 | 8 | @immutable 9 | class WriteCharacteristicInfo { 10 | final CharacteristicInstance characteristic; 11 | final Result?> result; 12 | 13 | const WriteCharacteristicInfo({ 14 | required this.characteristic, 15 | required this.result, 16 | }); 17 | 18 | @override 19 | String toString() => 20 | "$runtimeType(characteristic: $characteristic, result: $result)"; 21 | 22 | @override 23 | int get hashCode => 24 | ((17 * 37) + characteristic.hashCode) * 37 + result.hashCode; 25 | 26 | @override 27 | bool operator ==(Object other) => 28 | runtimeType == other.runtimeType && 29 | other is WriteCharacteristicInfo && 30 | characteristic == other.characteristic && 31 | result == other.result; 32 | } 33 | 34 | enum WriteCharacteristicFailure { unknown } 35 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/models.dart: -------------------------------------------------------------------------------- 1 | export './model/ble_status.dart'; 2 | export './model/characteristic_instance.dart'; 3 | export './model/characteristic_value.dart'; 4 | export './model/clear_gatt_cache_error.dart'; 5 | export './model/connection_priority.dart'; 6 | export './model/connection_state_update.dart'; 7 | export './model/discovered_characteristic.dart'; 8 | export './model/discovered_device.dart'; 9 | export './model/discovered_service.dart'; 10 | export './model/generic_failure.dart'; 11 | export './model/log_level.dart'; 12 | export './model/qualified_characteristic.dart'; 13 | export './model/result.dart'; 14 | export './model/scan_mode.dart'; 15 | export './model/scan_session.dart'; 16 | export './model/unit.dart'; 17 | export './model/uuid.dart'; 18 | export './model/write_characteristic_info.dart'; 19 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/lib/src/select_from.dart: -------------------------------------------------------------------------------- 1 | T selectFrom(List values, 2 | {required int? index, required T Function(int? index) fallback}) { 3 | if (index != null && index >= 0 && index < values.length) { 4 | return values[index]; 5 | } 6 | return fallback(index); 7 | } 8 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: reactive_ble_platform_interface 2 | description: Platform interface for the flutter_reactive_ble_project 3 | version: 5.4.0 4 | homepage: https://github.com/PhilipsHue/flutter_reactive_ble 5 | 6 | environment: 7 | sdk: '>=2.17.0 <3.0.0' 8 | flutter: ">=2.0.0" 9 | 10 | dependencies: 11 | collection: ^1.15.0 12 | flutter: 13 | sdk: flutter 14 | functional_data: ^1.0.0 15 | meta: ^1.7.0 16 | 17 | plugin_platform_interface: ^2.0.1 18 | 19 | dev_dependencies: 20 | build_runner: ^2.3.3 21 | flutter_lints: ^1.0.3 22 | flutter_test: 23 | sdk: flutter 24 | functional_data_generator: ^1.1.2 25 | mockito: ^5.0.2 26 | 27 | flutter: 28 | -------------------------------------------------------------------------------- /packages/reactive_ble_platform_interface/test/select_from_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:reactive_ble_platform_interface/src/select_from.dart'; 3 | 4 | void main() { 5 | group("selectFrom", () { 6 | _Enum sut(int? raw) => 7 | selectFrom(_Enum.values, index: raw, fallback: (raw) => _Enum.unknown); 8 | 9 | test("selects a value by index", () { 10 | expect(sut(1), _Enum.a); 11 | }); 12 | 13 | test("uses the fallback given a negative index", () { 14 | expect(sut(-1), _Enum.unknown); 15 | }); 16 | 17 | test("uses the fallback given an out of range index", () { 18 | expect(sut(3), _Enum.unknown); 19 | }); 20 | 21 | test("uses the fallback given a null index", () { 22 | expect(sut(null), _Enum.unknown); 23 | }); 24 | }); 25 | } 26 | 27 | enum _Enum { unknown, a } 28 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_reactive_ble_workspace 2 | 3 | environment: 4 | sdk: '>=2.17.0 <3.0.0' 5 | dev_dependencies: 6 | flutter_lints: ^1.0.4 7 | melos: ^3.1.0 8 | --------------------------------------------------------------------------------