├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── pycampers │ └── rx_ble │ ├── ConnectMethods.kt │ ├── PermissionMethods.kt │ ├── ReadWriteMethods.kt │ ├── RxBlePlugin.kt │ ├── ScanMethods.kt │ └── serializer.kt ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── pycampers │ │ │ │ │ └── rx_ble_example │ │ │ │ │ └── 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 │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── fonts │ └── DejaVuSansMono.ttf ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── 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 │ └── fonts │ │ └── DejaVuSansMono.ttf ├── lib │ └── main.dart ├── pubspec.lock └── pubspec.yaml ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── ConnectMethods.swift │ ├── PermissionMethods.swift │ ├── ReadWriteMethods.swift │ ├── RxBlePlugin.h │ ├── RxBlePlugin.m │ ├── ScanMethods.swift │ └── SwiftRxBlePlugin.swift └── rx_ble.podspec ├── lib ├── rx_ble.dart └── src │ ├── exception_serializer.dart │ ├── exceptions.dart │ └── models.dart ├── pubspec.lock ├── pubspec.yaml └── rx_ble.iml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | 9 | .idea/ 10 | -------------------------------------------------------------------------------- /.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: 0b8abb4724aa590dd0f429683339b1e045a1594d 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Sponsor](https://img.shields.io/badge/Sponsor-jaaga_labs-red.svg?style=for-the-badge)](https://www.jaaga.in/labs) [![pub package](https://img.shields.io/pub/v/rx_ble.svg?style=for-the-badge)](https://pub.dartlang.org/packages/rx_ble) 2 | 3 | # Flutter Rx BLE 4 | 5 | A Flutter BLE plugin, based on the wonderful [RxAndroidBle](https://github.com/Polidea/RxAndroidBle) and [RxBluetoothKit](https://github.com/Polidea/RxBluetoothKit) libraries. 6 | 7 | ### Batteries included. 8 | 9 | - Acquire every permission and setting required for Bluetooth access, using a _single_ method - `RxBle.requestAccess()`. 10 | - No need to manually discover BLE services. 11 | - Automatically queues up GATT requests to avoid race conditions. 12 | 13 | 14 | ## Installation 15 | 16 | ### iOS 17 | 18 | 1. Open iOS module in XCode 19 | 2. Edit `Info.plist` 20 | 3. Right click > Enable show Raw Keys/Values 21 | 4. Add these entries 22 | - `NSBluetoothAlwaysUsageDescription` = `Please enable location to continue.` 23 | - `NSLocationWhenInUseUsageDescription` = `Please enable location to continue.` 24 | - `NSBluetoothPeripheralUsageDescription` = `Please enable bluetooth to continue.` 25 | 26 | Or, you may add these entries maually using your editor of choice: 27 | 28 | ```plist 29 | 30 | ... 31 | 32 | NSBluetoothAlwaysUsageDescription 33 | Please enable location to continue. 34 | NSLocationWhenInUseUsageDescription 35 | Please enable location to continue. 36 | NSBluetoothPeripheralUsageDescription 37 | Please enable bluetooth to continue. 38 | 39 | ``` -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.pycampers.rx_ble' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.3.50' 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.5.0' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | sourceSets { 31 | main.java.srcDirs += 'src/main/kotlin' 32 | } 33 | defaultConfig { 34 | minSdkVersion 19 35 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 36 | } 37 | lintOptions { 38 | disable 'InvalidPackage' 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 44 | 45 | implementation "com.google.android.gms:play-services-location:17.0.0" 46 | implementation "com.polidea.rxandroidble2:rxandroidble:1.11.1" 47 | } 48 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'rx_ble' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/pycampers/rx_ble/ConnectMethods.kt: -------------------------------------------------------------------------------- 1 | package com.pycampers.rx_ble 2 | 3 | import com.pycampers.plugin_scaffold.sendThrowable 4 | import io.flutter.plugin.common.EventChannel.EventSink 5 | import io.flutter.plugin.common.MethodCall 6 | import io.flutter.plugin.common.MethodChannel.Result 7 | 8 | interface ConnectInterface { 9 | fun disconnect(call: MethodCall, result: Result) 10 | fun getConnectionState(call: MethodCall, result: Result) 11 | fun connectOnCancel(id: Int, args: Any?) 12 | fun connectOnListen(id: Int, args: Any?, sink: EventSink) 13 | } 14 | 15 | class ConnectMethods : ConnectInterface { 16 | override fun connectOnListen(id: Int, args: Any?, sink: EventSink) { 17 | val map = args as Map<*, *> 18 | val deviceId = map["deviceId"] as String 19 | val waitForDevice = map["waitForDevice"] as Boolean 20 | val device = getBleDevice(deviceId) 21 | val state = getDeviceState(deviceId) 22 | 23 | state.disconnect() 24 | 25 | val stateDisposable = device.observeConnectionStateChanges() 26 | .subscribe { sink.success(it.ordinal) } 27 | 28 | state.disposable = device.establishConnection(waitForDevice) 29 | .doFinally { 30 | sink.endOfStream() 31 | stateDisposable.dispose() 32 | } 33 | .subscribe( 34 | { state.bleConnection = it }, 35 | { sendThrowable(sink, it) } 36 | ) 37 | } 38 | 39 | override fun connectOnCancel(id: Int, args: Any?) { 40 | val map = args as Map<*, *> 41 | val deviceId = map["deviceId"] as String 42 | devices[deviceId]?.disconnect() 43 | } 44 | 45 | override fun disconnect(call: MethodCall, result: Result) { 46 | val deviceId = call.arguments as String? 47 | 48 | val toDisconnect = if (deviceId != null) { 49 | listOf(deviceId) 50 | } else { 51 | devices.keys 52 | } 53 | for (it in toDisconnect) { 54 | println("disconnecting: $it") 55 | devices[it]?.disconnect() 56 | } 57 | 58 | result.success(null) 59 | } 60 | 61 | override fun getConnectionState(call: MethodCall, result: Result) { 62 | val deviceId = call.arguments as String 63 | result.success(getBleDevice(deviceId).connectionState.ordinal) 64 | } 65 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/pycampers/rx_ble/PermissionMethods.kt: -------------------------------------------------------------------------------- 1 | package com.pycampers.rx_ble 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.bluetooth.BluetoothAdapter 6 | import android.bluetooth.BluetoothManager 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.content.pm.PackageManager 10 | import android.location.LocationManager 11 | import android.net.Uri 12 | import android.os.Build 13 | import android.provider.Settings 14 | import androidx.annotation.RequiresApi 15 | import com.google.android.gms.common.ConnectionResult 16 | import com.google.android.gms.common.GoogleApiAvailability 17 | import com.google.android.gms.common.api.ApiException 18 | import com.google.android.gms.common.api.ResolvableApiException 19 | import com.google.android.gms.location.* 20 | import com.pycampers.plugin_scaffold.catchErrors 21 | import com.pycampers.plugin_scaffold.trySend 22 | import io.flutter.plugin.common.MethodCall 23 | import io.flutter.plugin.common.MethodChannel.Result 24 | import io.flutter.plugin.common.PluginRegistry.ActivityResultListener 25 | import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener 26 | import java.util.* 27 | 28 | const val REQUEST_ENABLE_BT = 1 29 | const val REQUEST_ENABLE_LOC = 2 30 | const val REQUEST_PERM_LOC = 3 31 | const val LOC_PERM = Manifest.permission.ACCESS_COARSE_LOCATION 32 | 33 | enum class AccessStatus { 34 | OK, 35 | BT_DISABLED, 36 | LOC_DISABLED, 37 | LOC_DENIED, 38 | LOC_DENIED_NEVER_ASK_AGAIN, 39 | BLUETOOTH_NOT_AVAILABLE, 40 | LOC_DENIED_SHOW_PERM_RATIONALE, 41 | } 42 | 43 | interface PermissionInterface { 44 | fun requestLocPerm(call: MethodCall, result: Result) 45 | fun hasAccess(call: MethodCall, result: Result) 46 | fun requestAccess(call: MethodCall, result: Result) 47 | fun openAppSettings(call: MethodCall, result: Result) 48 | } 49 | 50 | class PermissionMethods(val context: Context) : ActivityResultListener, 51 | RequestPermissionsResultListener, 52 | PermissionInterface { 53 | 54 | var activity: Activity? = null 55 | 56 | val btEnableReqQ = ArrayDeque() 57 | val locEnableReqQ = ArrayDeque() 58 | val locPermReqQ = ArrayDeque() 59 | 60 | fun hasLocPerm(): Boolean { 61 | return Build.VERSION.SDK_INT < Build.VERSION_CODES.M 62 | || context.checkSelfPermission(LOC_PERM) == PackageManager.PERMISSION_GRANTED 63 | } 64 | 65 | @RequiresApi(Build.VERSION_CODES.M) 66 | fun reallyRequestLocPerm(result: Result) { 67 | activity?.requestPermissions(arrayOf(LOC_PERM), REQUEST_PERM_LOC) 68 | locPermReqQ.add(result) 69 | } 70 | 71 | fun isGooglePlayServicesAvailable(): Boolean { 72 | return GoogleApiAvailability 73 | .getInstance() 74 | .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS 75 | } 76 | 77 | fun requestLocPerm(result: Result) { 78 | if (hasLocPerm()) { 79 | return result.success(AccessStatus.OK.ordinal) 80 | } 81 | if (activity?.shouldShowRequestPermissionRationale(LOC_PERM) == true) { 82 | result.success(AccessStatus.LOC_DENIED_SHOW_PERM_RATIONALE.ordinal) 83 | } else { 84 | reallyRequestLocPerm(result) 85 | } 86 | } 87 | 88 | fun isLocEnabled(result: Result, callback: (Boolean, (() -> Unit)?) -> Unit) { 89 | if (!isGooglePlayServicesAvailable()) { 90 | if (activity == null) { 91 | callback(false) { 92 | result.success(AccessStatus.LOC_DISABLED.ordinal) 93 | } 94 | } 95 | 96 | activity?.let { 97 | val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 98 | val manager: LocationManager = 99 | context.getSystemService(Context.LOCATION_SERVICE) as LocationManager 100 | manager.isLocationEnabled 101 | } else { 102 | Settings.Secure.getInt( 103 | it.contentResolver, Settings.Secure.LOCATION_MODE 104 | ) != Settings.Secure.LOCATION_MODE_OFF 105 | } 106 | 107 | callback(enabled) { 108 | it.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) 109 | result.success(AccessStatus.LOC_DISABLED.ordinal) 110 | } 111 | return 112 | } 113 | } 114 | 115 | val request = LocationSettingsRequest.Builder() 116 | .addLocationRequest(LocationRequest()) 117 | .setAlwaysShow(true) 118 | .build() 119 | 120 | val taskResult = LocationServices.getSettingsClient(context) 121 | .checkLocationSettings(request) 122 | 123 | taskResult.addOnCompleteListener { 124 | val response = try { 125 | taskResult.getResult(ApiException::class.java) 126 | } catch (e: ApiException) { 127 | when (e.statusCode) { 128 | LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> { 129 | callback(false) { 130 | val resolvable = e as ResolvableApiException 131 | resolvable.startResolutionForResult(activity, REQUEST_ENABLE_LOC) 132 | locEnableReqQ.add(result) 133 | } 134 | return@addOnCompleteListener 135 | } 136 | else -> null 137 | } 138 | } 139 | 140 | callback(response != null && response.locationSettingsStates.isLocationUsable, null) 141 | } 142 | } 143 | 144 | fun requestLocEnable(result: Result) { 145 | isLocEnabled(result) { enabled, requestEnable -> 146 | catchErrors(result) { 147 | when { 148 | enabled -> { 149 | requestLocPerm(result) 150 | } 151 | requestEnable != null -> { 152 | requestEnable() 153 | } 154 | else -> { 155 | result.success(AccessStatus.LOC_DISABLED.ordinal) 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean { 163 | when (requestCode) { 164 | REQUEST_ENABLE_BT -> { 165 | val result: Result 166 | try { 167 | result = btEnableReqQ.remove() 168 | } catch (e: NoSuchElementException) { 169 | return false 170 | } 171 | 172 | catchErrors(result) { 173 | when (resultCode) { 174 | Activity.RESULT_OK -> requestLocEnable(result) 175 | Activity.RESULT_CANCELED -> result.success(AccessStatus.BT_DISABLED.ordinal) 176 | else -> throw IllegalArgumentException( 177 | "unexpected \"resultCode\" for REQUEST_ENABLE_BT { $resultCode }" 178 | ) 179 | } 180 | } 181 | 182 | return true 183 | } 184 | REQUEST_ENABLE_LOC -> { 185 | val result: Result 186 | try { 187 | result = locEnableReqQ.remove() 188 | } catch (e: NoSuchElementException) { 189 | return false 190 | } 191 | 192 | catchErrors(result) { 193 | when (resultCode) { 194 | Activity.RESULT_OK -> { 195 | if (LocationSettingsStates.fromIntent(intent).isLocationUsable) { 196 | requestLocPerm(result) 197 | } else { 198 | result.success(AccessStatus.LOC_DISABLED.ordinal) 199 | } 200 | } 201 | Activity.RESULT_CANCELED -> result.success(AccessStatus.LOC_DISABLED.ordinal) 202 | else -> throw IllegalArgumentException( 203 | "unexpected \"resultCode\" for REQUEST_ENABLE_LOC { $resultCode }" 204 | ) 205 | } 206 | } 207 | 208 | return true 209 | } 210 | else -> return false 211 | } 212 | } 213 | 214 | override fun onRequestPermissionsResult( 215 | requestCode: Int, 216 | permissions: Array?, 217 | grantResults: IntArray? 218 | ): Boolean { 219 | if (requestCode != REQUEST_PERM_LOC) return false 220 | 221 | val result: Result 222 | try { 223 | result = locPermReqQ.remove() 224 | } catch (e: NoSuchElementException) { 225 | return false 226 | } 227 | 228 | trySend(result) { 229 | val granted = try { 230 | grantResults?.first() == PackageManager.PERMISSION_GRANTED 231 | } catch (e: NoSuchElementException) { 232 | false 233 | } 234 | 235 | val status = if (granted) { 236 | AccessStatus.OK 237 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 238 | if (activity?.shouldShowRequestPermissionRationale(LOC_PERM) == true) { 239 | AccessStatus.LOC_DENIED 240 | } else { 241 | AccessStatus.LOC_DENIED_NEVER_ASK_AGAIN 242 | } 243 | } else { 244 | AccessStatus.LOC_DENIED 245 | } 246 | 247 | status.ordinal 248 | } 249 | 250 | return true 251 | } 252 | 253 | fun isBluetoothAvailable(): Boolean { 254 | val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() 255 | return bluetoothAdapter != null 256 | } 257 | 258 | fun isBluetoothEnabled(): Boolean { 259 | val value by lazy(LazyThreadSafetyMode.NONE) { 260 | val bluetoothManager = 261 | context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager 262 | bluetoothManager?.adapter 263 | ?: throw RuntimeException("couldn't acquire an instance of BluetoothAdapter") 264 | } 265 | return value.isEnabled 266 | } 267 | 268 | override fun requestLocPerm(call: MethodCall, result: Result) { 269 | if (hasLocPerm()) { 270 | return result.success(AccessStatus.OK.ordinal) 271 | } 272 | reallyRequestLocPerm(result) 273 | } 274 | 275 | override fun hasAccess(call: MethodCall, result: Result) { 276 | if (!isBluetoothAvailable()) { 277 | result.success(false) 278 | } 279 | isLocEnabled(result) { enabled, _ -> 280 | trySend(result) { 281 | isBluetoothEnabled() && enabled && hasLocPerm() 282 | } 283 | } 284 | } 285 | 286 | 287 | override fun requestAccess(call: MethodCall, result: Result) { 288 | if (!isBluetoothAvailable()) { 289 | result.success(AccessStatus.BLUETOOTH_NOT_AVAILABLE.ordinal) 290 | } 291 | if (isBluetoothEnabled()) { 292 | return requestLocEnable(result) 293 | } 294 | // enable bluetooth and proceed to requestLocEnable() 295 | val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) 296 | activity?.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) 297 | btEnableReqQ.add(result) 298 | } 299 | 300 | override fun openAppSettings(call: MethodCall, result: Result) { 301 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 302 | val uri = Uri.fromParts("package", context.packageName, null) 303 | intent.data = uri 304 | activity?.startActivity(intent) 305 | result.success(null) 306 | } 307 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/pycampers/rx_ble/ReadWriteMethods.kt: -------------------------------------------------------------------------------- 1 | package com.pycampers.rx_ble 2 | 3 | import com.pycampers.plugin_scaffold.trySend 4 | import com.pycampers.plugin_scaffold.sendThrowable 5 | import io.flutter.plugin.common.EventChannel.EventSink 6 | import io.flutter.plugin.common.MethodCall 7 | import io.flutter.plugin.common.MethodChannel.Result 8 | import io.reactivex.Single 9 | import io.reactivex.disposables.Disposable 10 | import java.util.* 11 | 12 | interface ReadWriteInterface { 13 | fun discoverChars(call: MethodCall, result: Result) 14 | fun readChar(call: MethodCall, result: Result) 15 | fun writeChar(call: MethodCall, result: Result) 16 | fun requestMtu(call: MethodCall, result: Result) 17 | fun observeCharOnListen(id: Int, args: Any?, sink: EventSink) 18 | fun observeCharOnCancel(id: Int, args: Any?) 19 | } 20 | 21 | class ReadWriteMethods : ReadWriteInterface { 22 | val disposables = mutableMapOf() 23 | 24 | fun sendSingle(observable: Single, result: Result) { 25 | observable.run { 26 | subscribe( 27 | { trySend(result) { it } }, 28 | { sendThrowable(result, it) } 29 | ) 30 | } 31 | } 32 | 33 | override fun discoverChars(call: MethodCall, result: Result) { 34 | val deviceId = call.arguments as String 35 | val connection = getBleConnection(deviceId) 36 | val charMap = mutableMapOf>() 37 | 38 | connection.run { 39 | discoverServices().subscribe( 40 | { 41 | trySend(result) { 42 | for (service in it.bluetoothGattServices) { 43 | val serviceId = service.uuid.toString() 44 | charMap[serviceId] = service.characteristics.map { it.uuid.toString() } 45 | } 46 | charMap 47 | } 48 | }, 49 | { sendThrowable(result, it) } 50 | ) 51 | } 52 | } 53 | 54 | override fun readChar(call: MethodCall, result: Result) { 55 | val deviceId = call.argument("deviceId")!! 56 | val uuid = UUID.fromString(call.argument("uuid")!!) 57 | val connection = getBleConnection(deviceId) 58 | sendSingle(connection.readCharacteristic(uuid), result) 59 | } 60 | 61 | override fun writeChar(call: MethodCall, result: Result) { 62 | val deviceId = call.argument("deviceId")!! 63 | val uuid = UUID.fromString(call.argument("uuid")!!) 64 | val value = call.argument("value")!! 65 | val connection = getBleConnection(deviceId) 66 | sendSingle(connection.writeCharacteristic(uuid, value), result) 67 | } 68 | 69 | override fun requestMtu(call: MethodCall, result: Result) { 70 | val deviceId = call.argument("deviceId")!! 71 | val value = call.argument("value")!! 72 | val connection = getBleConnection(deviceId) 73 | sendSingle(connection.requestMtu(value), result) 74 | } 75 | 76 | override fun observeCharOnListen(id: Int, args: Any?, sink: EventSink) { 77 | val map = args as Map<*, *> 78 | val deviceId = map["deviceId"] as String 79 | val uuid = UUID.fromString(map["uuid"] as String) 80 | 81 | disposables[id] = getBleConnection(deviceId).setupNotification(uuid) 82 | .flatMap { it } 83 | .doFinally { sink.endOfStream() } 84 | .subscribe( 85 | { sink.success(it) }, 86 | { sendThrowable(sink, it) } 87 | ) 88 | } 89 | 90 | override fun observeCharOnCancel(id: Int, args: Any?) { 91 | disposables[id]?.dispose() 92 | disposables.remove(id) 93 | } 94 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/pycampers/rx_ble/RxBlePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.pycampers.rx_ble 2 | 3 | import androidx.annotation.NonNull 4 | import com.polidea.rxandroidble2.RxBleClient 5 | import com.polidea.rxandroidble2.RxBleConnection 6 | import com.polidea.rxandroidble2.RxBleDevice 7 | import com.polidea.rxandroidble2.exceptions.BleException 8 | import com.pycampers.plugin_scaffold.createPluginScaffold 9 | import io.flutter.embedding.engine.plugins.FlutterPlugin 10 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 11 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 12 | import io.flutter.plugin.common.PluginRegistry.Registrar 13 | import io.reactivex.disposables.Disposable 14 | import io.reactivex.exceptions.UndeliverableException 15 | import io.reactivex.plugins.RxJavaPlugins 16 | 17 | const val PKG_NAME = "com.pycampers.rx_ble" 18 | 19 | class RxBlePlugin : FlutterPlugin, ActivityAware { 20 | var permissionMethods: PermissionMethods? = null 21 | 22 | override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 23 | RxJavaPlugins.setErrorHandler { error -> 24 | if (error is UndeliverableException && error.cause is BleException) { 25 | // ignore BleExceptions as they were surely delivered at least once 26 | return@setErrorHandler 27 | } 28 | throw error 29 | } 30 | 31 | val bleClient = RxBleClient.create(flutterPluginBinding.applicationContext)!! 32 | 33 | permissionMethods = PermissionMethods(flutterPluginBinding.applicationContext) 34 | 35 | val plugin = RxBlePluginMethods( 36 | permissionMethods!!, 37 | ConnectMethods(), 38 | ScanMethods(bleClient), 39 | ReadWriteMethods() 40 | ) 41 | createPluginScaffold(flutterPluginBinding.binaryMessenger, PKG_NAME, plugin) 42 | } 43 | 44 | // This static function is optional and equivalent to onAttachedToEngine. It supports the old 45 | // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting 46 | // plugin registration via this function while apps migrate to use the new Android APIs 47 | // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. 48 | // 49 | // It is encouraged to share logic between onAttachedToEngine and registerWith to keep 50 | // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called 51 | // depending on the user's project. onAttachedToEngine or registerWith must both be defined 52 | // in the same class. 53 | companion object { 54 | @JvmStatic 55 | fun registerWith(registrar: Registrar) { 56 | RxJavaPlugins.setErrorHandler { error -> 57 | if (error is UndeliverableException && error.cause is BleException) { 58 | // ignore BleExceptions as they were surely delivered at least once 59 | return@setErrorHandler 60 | } 61 | throw error 62 | } 63 | 64 | val bleClient = RxBleClient.create(registrar.context())!! 65 | 66 | val permissionMethods = PermissionMethods(registrar.context()); 67 | registrar.addActivityResultListener(permissionMethods) 68 | registrar.addRequestPermissionsResultListener(permissionMethods) 69 | permissionMethods.activity = registrar.activity() 70 | 71 | val plugin = RxBlePluginMethods( 72 | permissionMethods, 73 | ConnectMethods(), 74 | ScanMethods(bleClient), 75 | ReadWriteMethods() 76 | ) 77 | createPluginScaffold(registrar.messenger(), PKG_NAME, plugin) 78 | } 79 | } 80 | 81 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 82 | permissionMethods?.let { 83 | binding.addActivityResultListener(it) 84 | binding.addRequestPermissionsResultListener(it) 85 | it.activity = binding.activity 86 | } 87 | } 88 | 89 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {} 90 | 91 | override fun onDetachedFromActivity() {} 92 | 93 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {} 94 | 95 | override fun onDetachedFromActivityForConfigChanges() {} 96 | } 97 | 98 | 99 | class DeviceState { 100 | var disposable: Disposable? = null 101 | var bleDevice: RxBleDevice? = null 102 | var bleConnection: RxBleConnection? = null 103 | 104 | fun disconnect() { 105 | print("disconnecting! ${bleDevice?.macAddress}") 106 | disposable?.dispose() 107 | disposable = null 108 | } 109 | } 110 | 111 | val devices = mutableMapOf() 112 | 113 | fun getDeviceState(deviceId: String): DeviceState { 114 | return devices.getOrPut(deviceId) { DeviceState() } 115 | } 116 | 117 | fun getBleDevice(deviceId: String): RxBleDevice { 118 | return devices[deviceId]?.bleDevice ?: throw IllegalArgumentException( 119 | "Device has not been initialized yet. " + 120 | "You must call \"startScan()\" and wait for " + 121 | "device to appear in ScanResults before accessing the device." 122 | ) 123 | } 124 | 125 | fun getBleConnection(deviceId: String): RxBleConnection { 126 | return devices[deviceId]?.bleConnection ?: throw IllegalArgumentException( 127 | "Connection to device has not been initialized yet. " + 128 | "You must call \"connect()\" and wait for " + 129 | "\"BleConnectionState.connected\" before doing any read/write operation." 130 | ) 131 | } 132 | 133 | class RxBlePluginMethods(p: PermissionInterface, c: ConnectInterface, s: ScanInterface, r: ReadWriteInterface) : 134 | PermissionInterface by p, ConnectInterface by c, ScanInterface by s, ReadWriteInterface by r 135 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/pycampers/rx_ble/ScanMethods.kt: -------------------------------------------------------------------------------- 1 | package com.pycampers.rx_ble 2 | 3 | import android.os.ParcelUuid 4 | import com.polidea.rxandroidble2.RxBleClient 5 | import com.polidea.rxandroidble2.scan.ScanFilter 6 | import com.polidea.rxandroidble2.scan.ScanSettings 7 | import com.pycampers.plugin_scaffold.sendThrowable 8 | import com.pycampers.plugin_scaffold.trySend 9 | import io.flutter.plugin.common.EventChannel.EventSink 10 | import io.flutter.plugin.common.MethodCall 11 | import io.flutter.plugin.common.MethodChannel 12 | import io.reactivex.disposables.Disposable 13 | 14 | interface ScanInterface { 15 | fun stopScan(call: MethodCall, result: MethodChannel.Result) 16 | fun scanOnCancel(id: Int, args: Any?) 17 | fun scanOnListen(id: Int, args: Any?, sink: EventSink) 18 | } 19 | 20 | class ScanMethods(val bleClient: RxBleClient) : ScanInterface { 21 | var disposable: Disposable? = null 22 | 23 | fun stopScan() { 24 | disposable?.dispose() 25 | disposable = null 26 | } 27 | 28 | override fun scanOnListen(id: Int, args: Any?, sink: EventSink) { 29 | val map = args as Map<*, *> 30 | val scanSettings = ScanSettings.Builder().setScanMode(map["scanMode"] as Int - 1).build() 31 | 32 | val filter = ScanFilter.Builder() 33 | map["deviceId"]?.let { filter.setDeviceAddress(it as String) } 34 | map["name"]?.let { filter.setDeviceName(it as String) } 35 | map["service"]?.let { filter.setServiceUuid(ParcelUuid.fromString(it as String)) } 36 | 37 | stopScan() 38 | 39 | disposable = bleClient.scanBleDevices(scanSettings, filter.build()) 40 | .doFinally { sink.endOfStream() } 41 | .subscribe( 42 | { 43 | trySend(sink) { 44 | val device = it.bleDevice 45 | val state = getDeviceState(device.macAddress) 46 | state.bleDevice = device 47 | dumpScanResult(it) 48 | } 49 | }, 50 | { sendThrowable(sink, it) } 51 | ) 52 | } 53 | 54 | override fun scanOnCancel(id: Int, args: Any?) { 55 | stopScan() 56 | } 57 | 58 | override fun stopScan(call: MethodCall, result: MethodChannel.Result) { 59 | stopScan() 60 | result.success(null) 61 | } 62 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/pycampers/rx_ble/serializer.kt: -------------------------------------------------------------------------------- 1 | package com.pycampers.rx_ble 2 | 3 | import android.os.SystemClock 4 | import com.polidea.rxandroidble2.scan.ScanResult 5 | 6 | fun dumpScanResult(result: ScanResult): List<*> { 7 | return result.run { 8 | listOf( 9 | bleDevice.name, 10 | bleDevice.macAddress, 11 | rssi, 12 | (System.currentTimeMillis() - SystemClock.elapsedRealtime() + timestampNanos / 1e6).toLong() 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/.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 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /example/.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: 0b8abb4724aa590dd0f429683339b1e045a1594d 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # rx_ble_example 2 | 3 | Demonstrates how to use the rx_ble plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.pycampers.rx_ble_example" 42 | minSdkVersion 19 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | } 68 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/pycampers/rx_ble_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.pycampers.rx_ble_example 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(@NonNull flutterEngine: FlutterEngine) { 10 | GeneratedPluginRegistrant.registerWith(flutterEngine); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /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-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/fonts/DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/example/fonts/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 8.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 | -------------------------------------------------------------------------------- /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, '9.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 parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | generated_key_values = {} 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) do |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | generated_key_values[podname] = podpath 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | end 32 | generated_key_values 33 | end 34 | 35 | target 'Runner' do 36 | use_frameworks! 37 | use_modular_headers! 38 | 39 | # Flutter Pod 40 | 41 | copied_flutter_dir = File.join(__dir__, 'Flutter') 42 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') 43 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') 44 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) 45 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. 46 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. 47 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. 48 | 49 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') 50 | unless File.exist?(generated_xcode_build_settings_path) 51 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" 52 | end 53 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) 54 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; 55 | 56 | unless File.exist?(copied_framework_path) 57 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) 58 | end 59 | unless File.exist?(copied_podspec_path) 60 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) 61 | end 62 | end 63 | 64 | # Keep pod path relative so it can be checked into Podfile.lock. 65 | pod 'Flutter', :path => 'Flutter' 66 | 67 | # Plugin Pods 68 | 69 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 70 | # referring to absolute paths on developers' machines. 71 | system('rm -rf .symlinks') 72 | system('mkdir -p .symlinks/plugins') 73 | plugin_pods = parse_KV_file('../.flutter-plugins') 74 | plugin_pods.each do |name, path| 75 | symlink = File.join('.symlinks', 'plugins', name) 76 | File.symlink(path, symlink) 77 | pod name, :path => File.join(symlink, 'ios') 78 | end 79 | end 80 | 81 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 82 | install! 'cocoapods', :disable_input_output_paths => true 83 | 84 | post_install do |installer| 85 | installer.pods_project.targets.each do |target| 86 | target.build_configurations.each do |config| 87 | config.build_settings['ENABLE_BITCODE'] = 'NO' 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - plugin_scaffold (0.0.1): 4 | - Flutter 5 | - rx_ble (0.0.1): 6 | - Flutter 7 | - plugin_scaffold 8 | - RxBluetoothKit 9 | - RxBluetoothKit (5.3.0): 10 | - RxSwift (~> 5.0) 11 | - RxSwift (5.1.1) 12 | 13 | DEPENDENCIES: 14 | - Flutter (from `Flutter`) 15 | - plugin_scaffold (from `.symlinks/plugins/plugin_scaffold/ios`) 16 | - rx_ble (from `.symlinks/plugins/rx_ble/ios`) 17 | 18 | SPEC REPOS: 19 | trunk: 20 | - RxBluetoothKit 21 | - RxSwift 22 | 23 | EXTERNAL SOURCES: 24 | Flutter: 25 | :path: Flutter 26 | plugin_scaffold: 27 | :path: ".symlinks/plugins/plugin_scaffold/ios" 28 | rx_ble: 29 | :path: ".symlinks/plugins/rx_ble/ios" 30 | 31 | SPEC CHECKSUMS: 32 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 33 | plugin_scaffold: e7c52bc1cfb6e672959f798dfc9dea8dff55daeb 34 | rx_ble: c1eb4ff06f835004dc7cbbb10a7130d9c74be917 35 | RxBluetoothKit: 0e781978e8a0df70ac3c611dcf44800b76c69f8a 36 | RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 37 | 38 | PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83 39 | 40 | COCOAPODS: 1.8.4 41 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 13 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 6E4D268E64934BF81151ADAF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 94B8FD97A42DBB7125EF9E0A /* Pods_Runner.framework */; }; 15 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 16 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 17 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 18 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 19 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 20 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXCopyFilesBuildPhase section */ 24 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 25 | isa = PBXCopyFilesBuildPhase; 26 | buildActionMask = 2147483647; 27 | dstPath = ""; 28 | dstSubfolderSpec = 10; 29 | files = ( 30 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 31 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 32 | ); 33 | name = "Embed Frameworks"; 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXCopyFilesBuildPhase section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 40 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 41 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 42 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 43 | 5D207793617185706BAA94EA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 44 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 45 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 46 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 47 | 94B8FD97A42DBB7125EF9E0A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 49 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 50 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 51 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 53 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 54 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 55 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | BA0B1598B8E958DAF4C8D16A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 57 | FCC841A8DD755F69A353B266 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 66 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 67 | 6E4D268E64934BF81151ADAF /* Pods_Runner.framework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 0239285A8F7C8AB2681FA685 /* Pods */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 5D207793617185706BAA94EA /* Pods-Runner.debug.xcconfig */, 78 | BA0B1598B8E958DAF4C8D16A /* Pods-Runner.release.xcconfig */, 79 | FCC841A8DD755F69A353B266 /* Pods-Runner.profile.xcconfig */, 80 | ); 81 | path = Pods; 82 | sourceTree = ""; 83 | }; 84 | 59F60178125410266EA3CA8C /* Frameworks */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 94B8FD97A42DBB7125EF9E0A /* Pods_Runner.framework */, 88 | ); 89 | name = Frameworks; 90 | sourceTree = ""; 91 | }; 92 | 9740EEB11CF90186004384FC /* Flutter */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 3B80C3931E831B6300D905FE /* App.framework */, 96 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 97 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 98 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 99 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 100 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 101 | ); 102 | name = Flutter; 103 | sourceTree = ""; 104 | }; 105 | 97C146E51CF9000F007C117D = { 106 | isa = PBXGroup; 107 | children = ( 108 | 9740EEB11CF90186004384FC /* Flutter */, 109 | 97C146F01CF9000F007C117D /* Runner */, 110 | 97C146EF1CF9000F007C117D /* Products */, 111 | 0239285A8F7C8AB2681FA685 /* Pods */, 112 | 59F60178125410266EA3CA8C /* Frameworks */, 113 | ); 114 | sourceTree = ""; 115 | }; 116 | 97C146EF1CF9000F007C117D /* Products */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 97C146EE1CF9000F007C117D /* Runner.app */, 120 | ); 121 | name = Products; 122 | sourceTree = ""; 123 | }; 124 | 97C146F01CF9000F007C117D /* Runner */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 128 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 129 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 130 | 97C147021CF9000F007C117D /* Info.plist */, 131 | 97C146F11CF9000F007C117D /* Supporting Files */, 132 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 133 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 134 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 135 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 136 | ); 137 | path = Runner; 138 | sourceTree = ""; 139 | }; 140 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | ); 144 | name = "Supporting Files"; 145 | sourceTree = ""; 146 | }; 147 | /* End PBXGroup section */ 148 | 149 | /* Begin PBXNativeTarget section */ 150 | 97C146ED1CF9000F007C117D /* Runner */ = { 151 | isa = PBXNativeTarget; 152 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 153 | buildPhases = ( 154 | D164DEA366A76AA436BC567A /* [CP] Check Pods Manifest.lock */, 155 | 9740EEB61CF901F6004384FC /* Run Script */, 156 | 97C146EA1CF9000F007C117D /* Sources */, 157 | 97C146EB1CF9000F007C117D /* Frameworks */, 158 | 97C146EC1CF9000F007C117D /* Resources */, 159 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 160 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 161 | 4BC9316954B79FB00BD01F66 /* [CP] Embed Pods Frameworks */, 162 | ); 163 | buildRules = ( 164 | ); 165 | dependencies = ( 166 | ); 167 | name = Runner; 168 | productName = Runner; 169 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 170 | productType = "com.apple.product-type.application"; 171 | }; 172 | /* End PBXNativeTarget section */ 173 | 174 | /* Begin PBXProject section */ 175 | 97C146E61CF9000F007C117D /* Project object */ = { 176 | isa = PBXProject; 177 | attributes = { 178 | LastUpgradeCheck = 1020; 179 | ORGANIZATIONNAME = "The Chromium Authors"; 180 | TargetAttributes = { 181 | 97C146ED1CF9000F007C117D = { 182 | CreatedOnToolsVersion = 7.3.1; 183 | DevelopmentTeam = 9Q9DBAM35G; 184 | LastSwiftMigration = 1100; 185 | }; 186 | }; 187 | }; 188 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 189 | compatibilityVersion = "Xcode 3.2"; 190 | developmentRegion = en; 191 | hasScannedForEncodings = 0; 192 | knownRegions = ( 193 | en, 194 | Base, 195 | ); 196 | mainGroup = 97C146E51CF9000F007C117D; 197 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 198 | projectDirPath = ""; 199 | projectRoot = ""; 200 | targets = ( 201 | 97C146ED1CF9000F007C117D /* Runner */, 202 | ); 203 | }; 204 | /* End PBXProject section */ 205 | 206 | /* Begin PBXResourcesBuildPhase section */ 207 | 97C146EC1CF9000F007C117D /* Resources */ = { 208 | isa = PBXResourcesBuildPhase; 209 | buildActionMask = 2147483647; 210 | files = ( 211 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 212 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 213 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 214 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | }; 218 | /* End PBXResourcesBuildPhase section */ 219 | 220 | /* Begin PBXShellScriptBuildPhase section */ 221 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 222 | isa = PBXShellScriptBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | ); 226 | inputPaths = ( 227 | ); 228 | name = "Thin Binary"; 229 | outputPaths = ( 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | shellPath = /bin/sh; 233 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 234 | }; 235 | 4BC9316954B79FB00BD01F66 /* [CP] Embed Pods Frameworks */ = { 236 | isa = PBXShellScriptBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | ); 240 | inputPaths = ( 241 | ); 242 | name = "[CP] Embed Pods Frameworks"; 243 | outputPaths = ( 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | shellPath = /bin/sh; 247 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 248 | showEnvVarsInLog = 0; 249 | }; 250 | 9740EEB61CF901F6004384FC /* Run Script */ = { 251 | isa = PBXShellScriptBuildPhase; 252 | buildActionMask = 2147483647; 253 | files = ( 254 | ); 255 | inputPaths = ( 256 | ); 257 | name = "Run Script"; 258 | outputPaths = ( 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | shellPath = /bin/sh; 262 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 263 | }; 264 | D164DEA366A76AA436BC567A /* [CP] Check Pods Manifest.lock */ = { 265 | isa = PBXShellScriptBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | ); 269 | inputFileListPaths = ( 270 | ); 271 | inputPaths = ( 272 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 273 | "${PODS_ROOT}/Manifest.lock", 274 | ); 275 | name = "[CP] Check Pods Manifest.lock"; 276 | outputFileListPaths = ( 277 | ); 278 | outputPaths = ( 279 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | shellPath = /bin/sh; 283 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 284 | showEnvVarsInLog = 0; 285 | }; 286 | /* End PBXShellScriptBuildPhase section */ 287 | 288 | /* Begin PBXSourcesBuildPhase section */ 289 | 97C146EA1CF9000F007C117D /* Sources */ = { 290 | isa = PBXSourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 294 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 295 | ); 296 | runOnlyForDeploymentPostprocessing = 0; 297 | }; 298 | /* End PBXSourcesBuildPhase section */ 299 | 300 | /* Begin PBXVariantGroup section */ 301 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 302 | isa = PBXVariantGroup; 303 | children = ( 304 | 97C146FB1CF9000F007C117D /* Base */, 305 | ); 306 | name = Main.storyboard; 307 | sourceTree = ""; 308 | }; 309 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 310 | isa = PBXVariantGroup; 311 | children = ( 312 | 97C147001CF9000F007C117D /* Base */, 313 | ); 314 | name = LaunchScreen.storyboard; 315 | sourceTree = ""; 316 | }; 317 | /* End PBXVariantGroup section */ 318 | 319 | /* Begin XCBuildConfiguration section */ 320 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 321 | isa = XCBuildConfiguration; 322 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 323 | buildSettings = { 324 | ALWAYS_SEARCH_USER_PATHS = NO; 325 | CLANG_ANALYZER_NONNULL = YES; 326 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 327 | CLANG_CXX_LIBRARY = "libc++"; 328 | CLANG_ENABLE_MODULES = YES; 329 | CLANG_ENABLE_OBJC_ARC = YES; 330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 331 | CLANG_WARN_BOOL_CONVERSION = YES; 332 | CLANG_WARN_COMMA = YES; 333 | CLANG_WARN_CONSTANT_CONVERSION = YES; 334 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 335 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INFINITE_RECURSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 342 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 343 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 344 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 345 | CLANG_WARN_STRICT_PROTOTYPES = YES; 346 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 347 | CLANG_WARN_UNREACHABLE_CODE = YES; 348 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 349 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 350 | COPY_PHASE_STRIP = NO; 351 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 352 | ENABLE_NS_ASSERTIONS = NO; 353 | ENABLE_STRICT_OBJC_MSGSEND = YES; 354 | GCC_C_LANGUAGE_STANDARD = gnu99; 355 | GCC_NO_COMMON_BLOCKS = YES; 356 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 357 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 358 | GCC_WARN_UNDECLARED_SELECTOR = YES; 359 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 360 | GCC_WARN_UNUSED_FUNCTION = YES; 361 | GCC_WARN_UNUSED_VARIABLE = YES; 362 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 363 | MTL_ENABLE_DEBUG_INFO = NO; 364 | SDKROOT = iphoneos; 365 | SUPPORTED_PLATFORMS = iphoneos; 366 | TARGETED_DEVICE_FAMILY = "1,2"; 367 | VALIDATE_PRODUCT = YES; 368 | }; 369 | name = Profile; 370 | }; 371 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 372 | isa = XCBuildConfiguration; 373 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 374 | buildSettings = { 375 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 376 | CLANG_ENABLE_MODULES = YES; 377 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 378 | DEVELOPMENT_TEAM = 9Q9DBAM35G; 379 | ENABLE_BITCODE = NO; 380 | FRAMEWORK_SEARCH_PATHS = ( 381 | "$(inherited)", 382 | "$(PROJECT_DIR)/Flutter", 383 | ); 384 | INFOPLIST_FILE = Runner/Info.plist; 385 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 386 | LIBRARY_SEARCH_PATHS = ( 387 | "$(inherited)", 388 | "$(PROJECT_DIR)/Flutter", 389 | ); 390 | PRODUCT_BUNDLE_IDENTIFIER = com.pycampers.rxBleExample; 391 | PRODUCT_NAME = "$(TARGET_NAME)"; 392 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 393 | SWIFT_VERSION = 5.0; 394 | VERSIONING_SYSTEM = "apple-generic"; 395 | }; 396 | name = Profile; 397 | }; 398 | 97C147031CF9000F007C117D /* Debug */ = { 399 | isa = XCBuildConfiguration; 400 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 401 | buildSettings = { 402 | ALWAYS_SEARCH_USER_PATHS = NO; 403 | CLANG_ANALYZER_NONNULL = YES; 404 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 405 | CLANG_CXX_LIBRARY = "libc++"; 406 | CLANG_ENABLE_MODULES = YES; 407 | CLANG_ENABLE_OBJC_ARC = YES; 408 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 409 | CLANG_WARN_BOOL_CONVERSION = YES; 410 | CLANG_WARN_COMMA = YES; 411 | CLANG_WARN_CONSTANT_CONVERSION = YES; 412 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 413 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 414 | CLANG_WARN_EMPTY_BODY = YES; 415 | CLANG_WARN_ENUM_CONVERSION = YES; 416 | CLANG_WARN_INFINITE_RECURSION = YES; 417 | CLANG_WARN_INT_CONVERSION = YES; 418 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 419 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 420 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 421 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 422 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 423 | CLANG_WARN_STRICT_PROTOTYPES = YES; 424 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 425 | CLANG_WARN_UNREACHABLE_CODE = YES; 426 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 427 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 428 | COPY_PHASE_STRIP = NO; 429 | DEBUG_INFORMATION_FORMAT = dwarf; 430 | ENABLE_STRICT_OBJC_MSGSEND = YES; 431 | ENABLE_TESTABILITY = YES; 432 | GCC_C_LANGUAGE_STANDARD = gnu99; 433 | GCC_DYNAMIC_NO_PIC = NO; 434 | GCC_NO_COMMON_BLOCKS = YES; 435 | GCC_OPTIMIZATION_LEVEL = 0; 436 | GCC_PREPROCESSOR_DEFINITIONS = ( 437 | "DEBUG=1", 438 | "$(inherited)", 439 | ); 440 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 441 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 442 | GCC_WARN_UNDECLARED_SELECTOR = YES; 443 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 444 | GCC_WARN_UNUSED_FUNCTION = YES; 445 | GCC_WARN_UNUSED_VARIABLE = YES; 446 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 447 | MTL_ENABLE_DEBUG_INFO = YES; 448 | ONLY_ACTIVE_ARCH = YES; 449 | SDKROOT = iphoneos; 450 | TARGETED_DEVICE_FAMILY = "1,2"; 451 | }; 452 | name = Debug; 453 | }; 454 | 97C147041CF9000F007C117D /* Release */ = { 455 | isa = XCBuildConfiguration; 456 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 457 | buildSettings = { 458 | ALWAYS_SEARCH_USER_PATHS = NO; 459 | CLANG_ANALYZER_NONNULL = YES; 460 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 461 | CLANG_CXX_LIBRARY = "libc++"; 462 | CLANG_ENABLE_MODULES = YES; 463 | CLANG_ENABLE_OBJC_ARC = YES; 464 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 465 | CLANG_WARN_BOOL_CONVERSION = YES; 466 | CLANG_WARN_COMMA = YES; 467 | CLANG_WARN_CONSTANT_CONVERSION = YES; 468 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 469 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 470 | CLANG_WARN_EMPTY_BODY = YES; 471 | CLANG_WARN_ENUM_CONVERSION = YES; 472 | CLANG_WARN_INFINITE_RECURSION = YES; 473 | CLANG_WARN_INT_CONVERSION = YES; 474 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 475 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 476 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 477 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 478 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 479 | CLANG_WARN_STRICT_PROTOTYPES = YES; 480 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 481 | CLANG_WARN_UNREACHABLE_CODE = YES; 482 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 483 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 484 | COPY_PHASE_STRIP = NO; 485 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 486 | ENABLE_NS_ASSERTIONS = NO; 487 | ENABLE_STRICT_OBJC_MSGSEND = YES; 488 | GCC_C_LANGUAGE_STANDARD = gnu99; 489 | GCC_NO_COMMON_BLOCKS = YES; 490 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 491 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 492 | GCC_WARN_UNDECLARED_SELECTOR = YES; 493 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 494 | GCC_WARN_UNUSED_FUNCTION = YES; 495 | GCC_WARN_UNUSED_VARIABLE = YES; 496 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 497 | MTL_ENABLE_DEBUG_INFO = NO; 498 | SDKROOT = iphoneos; 499 | SUPPORTED_PLATFORMS = iphoneos; 500 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 501 | TARGETED_DEVICE_FAMILY = "1,2"; 502 | VALIDATE_PRODUCT = YES; 503 | }; 504 | name = Release; 505 | }; 506 | 97C147061CF9000F007C117D /* Debug */ = { 507 | isa = XCBuildConfiguration; 508 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 509 | buildSettings = { 510 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 511 | CLANG_ENABLE_MODULES = YES; 512 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 513 | DEVELOPMENT_TEAM = 9Q9DBAM35G; 514 | ENABLE_BITCODE = NO; 515 | FRAMEWORK_SEARCH_PATHS = ( 516 | "$(inherited)", 517 | "$(PROJECT_DIR)/Flutter", 518 | ); 519 | INFOPLIST_FILE = Runner/Info.plist; 520 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 521 | LIBRARY_SEARCH_PATHS = ( 522 | "$(inherited)", 523 | "$(PROJECT_DIR)/Flutter", 524 | ); 525 | PRODUCT_BUNDLE_IDENTIFIER = com.pycampers.rxBleExample; 526 | PRODUCT_NAME = "$(TARGET_NAME)"; 527 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 528 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 529 | SWIFT_VERSION = 5.0; 530 | VERSIONING_SYSTEM = "apple-generic"; 531 | }; 532 | name = Debug; 533 | }; 534 | 97C147071CF9000F007C117D /* Release */ = { 535 | isa = XCBuildConfiguration; 536 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 537 | buildSettings = { 538 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 539 | CLANG_ENABLE_MODULES = YES; 540 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 541 | DEVELOPMENT_TEAM = 9Q9DBAM35G; 542 | ENABLE_BITCODE = NO; 543 | FRAMEWORK_SEARCH_PATHS = ( 544 | "$(inherited)", 545 | "$(PROJECT_DIR)/Flutter", 546 | ); 547 | INFOPLIST_FILE = Runner/Info.plist; 548 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 549 | LIBRARY_SEARCH_PATHS = ( 550 | "$(inherited)", 551 | "$(PROJECT_DIR)/Flutter", 552 | ); 553 | PRODUCT_BUNDLE_IDENTIFIER = com.pycampers.rxBleExample; 554 | PRODUCT_NAME = "$(TARGET_NAME)"; 555 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 556 | SWIFT_VERSION = 5.0; 557 | VERSIONING_SYSTEM = "apple-generic"; 558 | }; 559 | name = Release; 560 | }; 561 | /* End XCBuildConfiguration section */ 562 | 563 | /* Begin XCConfigurationList section */ 564 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 565 | isa = XCConfigurationList; 566 | buildConfigurations = ( 567 | 97C147031CF9000F007C117D /* Debug */, 568 | 97C147041CF9000F007C117D /* Release */, 569 | 249021D3217E4FDB00AE95B9 /* Profile */, 570 | ); 571 | defaultConfigurationIsVisible = 0; 572 | defaultConfigurationName = Release; 573 | }; 574 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 575 | isa = XCConfigurationList; 576 | buildConfigurations = ( 577 | 97C147061CF9000F007C117D /* Debug */, 578 | 97C147071CF9000F007C117D /* Release */, 579 | 249021D4217E4FDB00AE95B9 /* Profile */, 580 | ); 581 | defaultConfigurationIsVisible = 0; 582 | defaultConfigurationName = Release; 583 | }; 584 | /* End XCConfigurationList section */ 585 | }; 586 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 587 | } 588 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/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 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | rx_ble_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | NSBluetoothAlwaysUsageDescription 45 | Please enable location to continue. 46 | NSLocationWhenInUseUsageDescription 47 | Please enable location to continue. 48 | NSBluetoothPeripheralUsageDescription 49 | Please enable bluetooth to continue. 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /example/ios/fonts/DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/example/ios/fonts/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:rx_ble/rx_ble.dart'; 7 | 8 | int time() => DateTime.now().millisecondsSinceEpoch; 9 | 10 | void main() { 11 | runApp( 12 | MaterialApp( 13 | home: Scaffold( 14 | appBar: AppBar( 15 | title: const Text('Flutter Rx BLE example'), 16 | ), 17 | body: MyApp(), 18 | ), 19 | ), 20 | ); 21 | } 22 | 23 | class YesNoDialog extends StatelessWidget { 24 | @override 25 | Widget build(BuildContext context) { 26 | return AlertDialog( 27 | title: Text('Location Permission Required'), 28 | content: Text( 29 | "This app needs location permission in order to access Bluetooth.\n" 30 | "Continue?", 31 | ), 32 | actions: [ 33 | SimpleDialogOption( 34 | child: Text( 35 | "NO", 36 | style: TextStyle( 37 | color: Colors.red, 38 | fontWeight: FontWeight.bold, 39 | ), 40 | ), 41 | onPressed: () { 42 | Navigator.of(context).pop(false); 43 | }, 44 | ), 45 | SimpleDialogOption( 46 | child: Text( 47 | "YES", 48 | style: TextStyle( 49 | color: Colors.blue, 50 | fontWeight: FontWeight.bold, 51 | ), 52 | ), 53 | onPressed: () { 54 | Navigator.of(context).pop(true); 55 | }, 56 | ) 57 | ], 58 | ); 59 | } 60 | } 61 | 62 | class MyApp extends StatefulWidget { 63 | @override 64 | _MyAppState createState() => _MyAppState(); 65 | } 66 | 67 | class _MyAppState extends State { 68 | var returnValue; 69 | String deviceId; 70 | Exception returnError; 71 | final results = {}; 72 | var chars = Map>(); 73 | final uuidControl = TextEditingController(); 74 | final mtuControl = TextEditingController(); 75 | final writeCharValueControl = TextEditingController(); 76 | final randomWriteNum = TextEditingController(text: '100'); 77 | final randomWriteSize = TextEditingController(text: '100'); 78 | var connectionState = BleConnectionState.disconnected; 79 | var isWorking = false; 80 | 81 | Function wrapCall(Function fn) { 82 | return () async { 83 | var value, error; 84 | setState(() { 85 | returnError = returnValue = null; 86 | isWorking = true; 87 | }); 88 | try { 89 | value = await fn(); 90 | print('returnValue: $value'); 91 | } catch (e, trace) { 92 | print('returnError: $e\n$trace'); 93 | error = e; 94 | } finally { 95 | if (mounted) { 96 | setState(() { 97 | isWorking = false; 98 | returnError = error; 99 | returnValue = value; 100 | }); 101 | } 102 | } 103 | }; 104 | } 105 | 106 | Future requestAccessRationale() async { 107 | return await RxBle.requestAccess( 108 | showRationale: () async { 109 | return await showDialog( 110 | context: context, 111 | builder: (context) => YesNoDialog(), 112 | ) ?? 113 | false; 114 | }, 115 | ); 116 | } 117 | 118 | Future startScan() async { 119 | await for (final scanResult in RxBle.startScan()) { 120 | results[scanResult.deviceId] = scanResult; 121 | if (!mounted) return; 122 | setState(() { 123 | returnValue = JsonEncoder.withIndent(" " * 2, (o) { 124 | if (o is ScanResult) { 125 | return o.toString(); 126 | } else { 127 | return o; 128 | } 129 | }).convert(results); 130 | }); 131 | } 132 | } 133 | 134 | Future discoverChars() async { 135 | final value = await RxBle.discoverChars(deviceId); 136 | if (!mounted) return null; 137 | setState(() { 138 | chars = value; 139 | }); 140 | return JsonEncoder.withIndent(" " * 2).convert(chars); 141 | } 142 | 143 | Future readChar() async { 144 | final value = await RxBle.readChar(deviceId, uuidControl.text); 145 | return value.toString() + 146 | "\n\n" + 147 | RxBle.charToString(value, allowMalformed: true); 148 | } 149 | 150 | Future observeChar() async { 151 | var start = time(); 152 | await for (final value in RxBle.observeChar(deviceId, uuidControl.text)) { 153 | final end = time(); 154 | if (!mounted) return; 155 | setState(() { 156 | returnValue = value.toString() + 157 | "\n\n" + 158 | RxBle.charToString(value, allowMalformed: true) + 159 | "\n\nDelay: ${(end - start)} ms"; 160 | }); 161 | start = time(); 162 | } 163 | } 164 | 165 | Future writeChar() async { 166 | return await RxBle.writeChar( 167 | deviceId, 168 | uuidControl.text, 169 | RxBle.stringToChar(writeCharValueControl.text), 170 | ); 171 | } 172 | 173 | Future requestMtu() async { 174 | return await RxBle.requestMtu(deviceId, int.parse(mtuControl.text)); 175 | } 176 | 177 | Future randomWrite() async { 178 | final rand = new Random(); 179 | final futures = List.generate(int.parse(randomWriteNum.text), (_) { 180 | return RxBle.writeChar( 181 | deviceId, 182 | uuidControl.text, 183 | Uint8List.fromList( 184 | List.generate(int.parse(randomWriteSize.text), (_) { 185 | return rand.nextInt(33) + 89; 186 | }), 187 | ), 188 | ); 189 | }); 190 | final start = time(); 191 | await Future.wait(futures); 192 | final end = time(); 193 | return "${end - start} ms"; 194 | } 195 | 196 | Future continuousRead() async { 197 | while (true) { 198 | final start = time(); 199 | final value = await RxBle.readChar(deviceId, uuidControl.text); 200 | final end = time(); 201 | if (!mounted) return; 202 | setState(() { 203 | returnValue = value.toString() + 204 | "\n\n" + 205 | RxBle.charToString(value, allowMalformed: true) + 206 | "\n\nDelay: ${start - end} ms"; 207 | }); 208 | } 209 | } 210 | 211 | @override 212 | Widget build(BuildContext context) { 213 | return Stack( 214 | children: [ 215 | Padding( 216 | padding: const EdgeInsets.all(8.0), 217 | child: ListView( 218 | children: [ 219 | Text("Return Value:"), 220 | SingleChildScrollView( 221 | scrollDirection: Axis.horizontal, 222 | child: Container( 223 | color: Colors.black, 224 | child: Text( 225 | "$returnValue", 226 | style: TextStyle( 227 | fontFamily: 'DejaVuSansMono', 228 | color: Colors.white, 229 | ), 230 | ), 231 | ), 232 | ), 233 | Divider(), 234 | Text("Error:"), 235 | SingleChildScrollView( 236 | scrollDirection: Axis.horizontal, 237 | child: Container( 238 | color: Colors.black, 239 | child: Text( 240 | "$returnError", 241 | style: TextStyle( 242 | fontFamily: 'DejaVuSansMono', 243 | color: Colors.white, 244 | ), 245 | ), 246 | ), 247 | ), 248 | Divider(), 249 | Container( 250 | color: Colors.black, 251 | child: Text( 252 | connectionState.toString(), 253 | style: TextStyle( 254 | fontFamily: 'DejaVuSansMono', 255 | color: Colors.white, 256 | ), 257 | ), 258 | ), 259 | Divider(), 260 | RaisedButton( 261 | child: Text( 262 | "requestAccess()", 263 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 264 | ), 265 | onPressed: wrapCall(RxBle.requestAccess), 266 | ), 267 | RaisedButton( 268 | child: Text( 269 | "requestAccess(showRationale)", 270 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 271 | ), 272 | onPressed: wrapCall(requestAccessRationale), 273 | ), 274 | RaisedButton( 275 | child: Text( 276 | "hasAccess()", 277 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 278 | ), 279 | onPressed: wrapCall(RxBle.hasAccess), 280 | ), 281 | RaisedButton( 282 | child: Text( 283 | "openAppSettings()", 284 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 285 | ), 286 | onPressed: wrapCall(RxBle.openAppSettings), 287 | ), 288 | Divider(), 289 | RaisedButton( 290 | child: Text( 291 | "startScan()", 292 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 293 | ), 294 | onPressed: wrapCall(startScan), 295 | ), 296 | RaisedButton( 297 | child: Text( 298 | "stopScan()", 299 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 300 | ), 301 | onPressed: wrapCall(RxBle.stopScan), 302 | ), 303 | Divider(), 304 | if (results.isEmpty) 305 | Text('Start scanning to connect to a device'), 306 | for (final scanResult in results.values) 307 | RaisedButton( 308 | child: Text( 309 | "connect(${scanResult.deviceId})", 310 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 311 | ), 312 | onPressed: wrapCall(() async { 313 | await RxBle.stopScan(); 314 | setState(() { 315 | deviceId = scanResult.deviceId; 316 | }); 317 | await for (final state in RxBle.connect(deviceId)) { 318 | print("device state: $state"); 319 | if (!mounted) return; 320 | setState(() { 321 | connectionState = state; 322 | }); 323 | } 324 | }), 325 | ), 326 | Divider(), 327 | if (connectionState != BleConnectionState.connected) 328 | Text("Connect to a device to perform GATT operations.") 329 | else ...[ 330 | RaisedButton( 331 | child: Text( 332 | "discoverChars()", 333 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 334 | ), 335 | onPressed: wrapCall(discoverChars), 336 | ), 337 | RaisedButton( 338 | child: Text( 339 | "disconnect()", 340 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 341 | ), 342 | onPressed: wrapCall(RxBle.disconnect), 343 | ), 344 | Divider(), 345 | if (chars.isNotEmpty) Text("Characteristic Picker"), 346 | SingleChildScrollView( 347 | scrollDirection: Axis.horizontal, 348 | child: Row( 349 | children: [ 350 | for (final i in chars.values) 351 | for (final j in i) 352 | Padding( 353 | padding: EdgeInsets.all(5), 354 | child: RaisedButton( 355 | child: Text(j), 356 | onPressed: () { 357 | setState(() { 358 | uuidControl.text = j; 359 | }); 360 | }, 361 | ), 362 | ), 363 | ], 364 | ), 365 | ), 366 | Divider(), 367 | TextField( 368 | controller: uuidControl, 369 | decoration: InputDecoration( 370 | labelText: "uuid", 371 | ), 372 | ), 373 | RaisedButton( 374 | child: Text( 375 | "device.readChar()", 376 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 377 | ), 378 | onPressed: wrapCall(readChar), 379 | ), 380 | RaisedButton( 381 | child: Text( 382 | "device.observeChar()", 383 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 384 | ), 385 | onPressed: wrapCall(observeChar), 386 | ), 387 | TextField( 388 | controller: writeCharValueControl, 389 | decoration: InputDecoration( 390 | labelText: "writeChar value", 391 | ), 392 | ), 393 | RaisedButton( 394 | child: Text( 395 | "device.writeChar()", 396 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 397 | ), 398 | onPressed: wrapCall(writeChar), 399 | ), 400 | TextField( 401 | controller: mtuControl, 402 | decoration: InputDecoration( 403 | labelText: "mtu", 404 | ), 405 | ), 406 | RaisedButton( 407 | child: Text( 408 | "device.requestMtu()", 409 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 410 | ), 411 | onPressed: wrapCall(requestMtu), 412 | ), 413 | Divider(), 414 | TextField( 415 | controller: randomWriteSize, 416 | keyboardType: TextInputType.number, 417 | decoration: InputDecoration( 418 | labelText: "Random write batch size", 419 | ), 420 | ), 421 | TextField( 422 | controller: randomWriteNum, 423 | keyboardType: TextInputType.number, 424 | decoration: InputDecoration( 425 | labelText: "Random Write no of batches", 426 | ), 427 | ), 428 | RaisedButton( 429 | child: Text( 430 | 'Test random writes', 431 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 432 | ), 433 | onPressed: wrapCall(randomWrite), 434 | ), 435 | RaisedButton( 436 | child: Text( 437 | 'Test continuous read', 438 | style: TextStyle(fontFamily: 'DejaVuSansMono'), 439 | ), 440 | onPressed: wrapCall(continuousRead), 441 | ), 442 | ], 443 | ], 444 | ), 445 | ), 446 | Column( 447 | mainAxisAlignment: MainAxisAlignment.end, 448 | children: [ 449 | if (isWorking) LinearProgressIndicator(value: null), 450 | ], 451 | ), 452 | ], 453 | ); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.1.3" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | image: 71 | dependency: transitive 72 | description: 73 | name: image 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "2.1.4" 77 | matcher: 78 | dependency: transitive 79 | description: 80 | name: matcher 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "0.12.6" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.1.8" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.6.4" 98 | pedantic: 99 | dependency: transitive 100 | description: 101 | name: pedantic 102 | url: "https://pub.dartlang.org" 103 | source: hosted 104 | version: "1.8.0+1" 105 | petitparser: 106 | dependency: transitive 107 | description: 108 | name: petitparser 109 | url: "https://pub.dartlang.org" 110 | source: hosted 111 | version: "2.4.0" 112 | plugin_scaffold: 113 | dependency: transitive 114 | description: 115 | name: plugin_scaffold 116 | url: "https://pub.dartlang.org" 117 | source: hosted 118 | version: "3.0.1" 119 | quiver: 120 | dependency: transitive 121 | description: 122 | name: quiver 123 | url: "https://pub.dartlang.org" 124 | source: hosted 125 | version: "2.0.5" 126 | rx_ble: 127 | dependency: "direct main" 128 | description: 129 | path: ".." 130 | relative: true 131 | source: path 132 | version: "1.0.0" 133 | sky_engine: 134 | dependency: transitive 135 | description: flutter 136 | source: sdk 137 | version: "0.0.99" 138 | source_span: 139 | dependency: transitive 140 | description: 141 | name: source_span 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.5.5" 145 | stack_trace: 146 | dependency: transitive 147 | description: 148 | name: stack_trace 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "1.9.3" 152 | stream_channel: 153 | dependency: transitive 154 | description: 155 | name: stream_channel 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "2.0.0" 159 | string_scanner: 160 | dependency: transitive 161 | description: 162 | name: string_scanner 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "1.0.5" 166 | term_glyph: 167 | dependency: transitive 168 | description: 169 | name: term_glyph 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "1.1.0" 173 | test_api: 174 | dependency: transitive 175 | description: 176 | name: test_api 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "0.2.11" 180 | typed_data: 181 | dependency: transitive 182 | description: 183 | name: typed_data 184 | url: "https://pub.dartlang.org" 185 | source: hosted 186 | version: "1.1.6" 187 | vector_math: 188 | dependency: transitive 189 | description: 190 | name: vector_math 191 | url: "https://pub.dartlang.org" 192 | source: hosted 193 | version: "2.0.8" 194 | xml: 195 | dependency: transitive 196 | description: 197 | name: xml 198 | url: "https://pub.dartlang.org" 199 | source: hosted 200 | version: "3.5.0" 201 | sdks: 202 | dart: ">=2.4.0 <3.0.0" 203 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rx_ble_example 2 | description: Demonstrates how to use the rx_ble plugin. 3 | publish_to: 'none' 4 | 5 | environment: 6 | sdk: ">=2.2.2 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | 12 | rx_ble: 13 | path: ../ 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://www.dartlang.org/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | 25 | # The following line ensures that the Material Icons font is 26 | # included with your application, so that you can use the icons in 27 | # the material Icons class. 28 | uses-material-design: true 29 | 30 | # To add assets to your application, add an assets section, like this: 31 | # assets: 32 | # - images/a_dot_burr.jpeg 33 | # - images/a_dot_ham.jpeg 34 | 35 | # An image asset can refer to one or more resolution-specific "variants", see 36 | # https://flutter.dev/assets-and-images/#resolution-aware. 37 | 38 | # For details regarding adding assets from package dependencies, see 39 | # https://flutter.dev/assets-and-images/#from-packages 40 | 41 | # To add custom fonts to your application, add a fonts section here, 42 | # in this "flutter" section. Each entry in this list should have a 43 | # "family" key with the font family name, and a "fonts" key with a 44 | # list giving the asset and other descriptors for the font. For 45 | # example: 46 | # fonts: 47 | # - family: Schyler 48 | # fonts: 49 | # - asset: fonts/Schyler-Regular.ttf 50 | # - asset: fonts/Schyler-Italic.ttf 51 | # style: italic 52 | # - family: Trajan Pro 53 | # fonts: 54 | # - asset: fonts/TrajanPro.ttf 55 | # - asset: fonts/TrajanPro_Bold.ttf 56 | # weight: 700 57 | # 58 | # For details regarding fonts from package dependencies, 59 | # see https://flutter.dev/custom-fonts/#from-packages 60 | 61 | fonts: 62 | - family: DejaVuSansMono 63 | fonts: 64 | - asset: fonts/DejaVuSansMono.ttf 65 | -------------------------------------------------------------------------------- /ios/.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 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scientifichackers/flutter-rx-ble/7646a02312d05e562e75f30f5a2141477a643281/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/ConnectMethods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dev Aggarwal on 2019-05-19. 3 | // 4 | 5 | import Foundation 6 | import plugin_scaffold 7 | import RxBluetoothKit 8 | import RxSwift 9 | 10 | typealias CharMap = [String: [Characteristic]] 11 | 12 | class ConnectMethods: NSObject { 13 | func getCharObservable(_ peripheral: Peripheral) -> Observable { 14 | return peripheral 15 | .discoverServices(nil).asObservable() 16 | .flatMap { Observable.from($0) } 17 | .flatMap { $0.discoverCharacteristics(nil) }.asObservable() 18 | .flatMap { Observable.from($0) } 19 | } 20 | 21 | func _discoverChars(_ peripheral: Peripheral, onSuccess: @escaping (CharMap) -> Void, onError: @escaping (Error) -> Void) { 22 | var chars = CharMap() 23 | _ = getCharObservable(peripheral).subscribe( 24 | onNext: { 25 | let serviceId = $0.service.uuid.uuidString 26 | if chars[serviceId] == nil { 27 | chars[serviceId] = [] 28 | } 29 | chars[serviceId]?.append($0) 30 | }, 31 | onError: { onError($0) }, 32 | onDisposed: { onSuccess(chars) } 33 | ) 34 | } 35 | 36 | func onListen(id: Int, args: Any?, sink: @escaping FlutterEventSink) throws { 37 | let map = args as! [String: Any?] 38 | let deviceId = map["deviceId"] as! String 39 | let state = getDeviceState(deviceId) 40 | let peripheral = try getPeripheral(deviceId) 41 | 42 | state.disconnect() 43 | 44 | let stateDisposable = peripheral.observeConnection().subscribe( 45 | onNext: { 46 | if $0 { 47 | self._discoverChars( 48 | peripheral, 49 | onSuccess: { 50 | for chars in $0.values { 51 | for char in chars { 52 | charCache[deviceId + char.uuid.uuidString] = char 53 | } 54 | } 55 | sink(1) 56 | }, 57 | onError: { trySendError(sink, $0) } 58 | ) 59 | } else { 60 | sink(2) 61 | } 62 | }, 63 | onError: { trySendError(sink, $0) } 64 | ) 65 | 66 | state.disposable = peripheral.establishConnection().subscribe( 67 | onError: { trySendError(sink, $0) }, 68 | onDisposed: { 69 | sink(FlutterEndOfEventStream) 70 | stateDisposable.dispose() 71 | } 72 | ) 73 | } 74 | 75 | func onCancel(id: Int, args: Any?) { 76 | let map = args as! [String: Any?] 77 | let deviceId = map["deviceId"] as! String 78 | devices[deviceId]?.disconnect() 79 | } 80 | 81 | func disconnect(call: FlutterMethodCall, result: @escaping FlutterResult) { 82 | if let deviceId = call.arguments as? String { 83 | devices[deviceId]?.disconnect() 84 | } else { 85 | for it in devices.values { 86 | it.disconnect() 87 | } 88 | } 89 | result(nil) 90 | } 91 | 92 | func getConnectionState(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 93 | let deviceId = call.arguments as! String 94 | let peripheral = try getPeripheral(deviceId) 95 | 96 | trySend(result) { 97 | switch peripheral.state { 98 | case .connecting: 99 | return 0 100 | case .connected: 101 | return 1 102 | case .disconnected: 103 | return 2 104 | case .disconnecting: 105 | return 3 106 | } 107 | } 108 | } 109 | 110 | func discoverChars(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 111 | let deviceId = call.arguments as! String 112 | let peripheral = try getPeripheral(deviceId) 113 | 114 | _discoverChars( 115 | peripheral, 116 | onSuccess: { 117 | chars in trySend(result) { 118 | chars.mapValues { $0.map { $0.uuid.uuidString } } 119 | } 120 | }, 121 | onError: { trySendError(result, $0) } 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /ios/Classes/PermissionMethods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionMethods.swift 3 | // rx_ble 4 | // 5 | // Created by Dev Aggarwal on 18/05/19. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | import plugin_scaffold 11 | import RxBluetoothKit 12 | 13 | enum AccessStatus: Int { 14 | case ok = 0, btDisabled, locDisabled, locDenied 15 | } 16 | 17 | class PermissionMethods: NSObject, CLLocationManagerDelegate { 18 | let manager: CentralManager 19 | let locationManager = CLLocationManager() 20 | var bluetoothIsOn = false 21 | var result: FlutterResult? 22 | 23 | init(_ manager: CentralManager) { 24 | self.manager = manager 25 | 26 | super.init() 27 | 28 | locationManager.delegate = self 29 | _ = manager.observeState().subscribe(onNext: { self.bluetoothIsOn = $0 == .poweredOn }) 30 | } 31 | 32 | func locationManager(_: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 33 | switch status { 34 | case .authorizedWhenInUse, .authorizedAlways: 35 | result?(AccessStatus.ok.rawValue) 36 | default: 37 | result?(AccessStatus.locDenied.rawValue) 38 | } 39 | } 40 | 41 | func requestAccess(call: FlutterMethodCall, result: @escaping FlutterResult) { 42 | if !CLLocationManager.locationServicesEnabled() { 43 | result(AccessStatus.locDisabled.rawValue) 44 | return 45 | } 46 | switch CLLocationManager.authorizationStatus() { 47 | case .notDetermined: 48 | locationManager.requestWhenInUseAuthorization() 49 | self.result = result 50 | return 51 | case .restricted, .denied: 52 | result(AccessStatus.locDenied.rawValue) 53 | return 54 | default: 55 | break 56 | } 57 | if !bluetoothIsOn { 58 | result(AccessStatus.btDisabled.rawValue) 59 | return 60 | } 61 | result(AccessStatus.ok.rawValue) 62 | } 63 | 64 | func hasAccess(call: FlutterMethodCall, result: @escaping FlutterResult) { 65 | let status = CLLocationManager.authorizationStatus() 66 | result( 67 | CLLocationManager.locationServicesEnabled() 68 | && (status == .authorizedWhenInUse || status == .authorizedAlways) 69 | && bluetoothIsOn 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ios/Classes/ReadWriteMethods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadWriteMethods.swift 3 | // rx_ble 4 | // 5 | // Created by Dev Aggarwal on 21/05/19. 6 | // 7 | 8 | import Foundation 9 | import plugin_scaffold 10 | import RxBluetoothKit 11 | import RxSwift 12 | 13 | class ReadWriteMethods: NSObject { 14 | var disposables = [Int: Disposable]() 15 | 16 | func sendSingle(_ single: Single, _ result: @escaping FlutterResult, _ serializer: @escaping (T) -> Any?) { 17 | _ = single.subscribe( 18 | onSuccess: { it in trySend(result) { serializer(it) } }, 19 | onError: { trySendError(result, $0) } 20 | ) 21 | } 22 | 23 | func readChar(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 24 | let map = call.arguments as! [String: Any?] 25 | let deviceId = map["deviceId"] as! String 26 | let uuid = map["uuid"] as! String 27 | let peripheral = try getPeripheral(deviceId) 28 | let char = try getCharFromCache(deviceId, uuid) 29 | 30 | sendSingle(peripheral.readValue(for: char), result) { 31 | FlutterStandardTypedData(bytes: $0.value ?? Data()) 32 | } 33 | } 34 | 35 | func writeChar(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 36 | let map = call.arguments as! [String: Any?] 37 | let deviceId = map["deviceId"] as! String 38 | let uuid = map["uuid"] as! String 39 | let value = map["value"] as! FlutterStandardTypedData 40 | let peripheral = try getPeripheral(deviceId) 41 | let char = try getCharFromCache(deviceId, uuid) 42 | 43 | sendSingle(peripheral.writeValue(value.data, for: char, type: .withResponse), result) { 44 | FlutterStandardTypedData(bytes: $0.value ?? Data()) 45 | } 46 | } 47 | 48 | func onListen(id: Int, args: Any?, sink: @escaping FlutterEventSink) throws { 49 | let map = args as! [String: Any?] 50 | let deviceId = map["deviceId"] as! String 51 | let uuid = map["uuid"] as! String 52 | let peripheral = try getPeripheral(deviceId) 53 | let char = try getCharFromCache(deviceId, uuid) 54 | 55 | disposables[id] = peripheral 56 | .observeValueUpdateAndSetNotification(for: char) 57 | .subscribe( 58 | onNext: { char in trySend(sink) { FlutterStandardTypedData(bytes: char.value ?? Data()) } }, 59 | onError: { trySendError(sink, $0) }, 60 | onDisposed: { sink(FlutterEndOfEventStream) } 61 | ) 62 | } 63 | 64 | func onCancel(id: Int, args: Any?) { 65 | disposables[id]?.dispose() 66 | disposables.removeValue(forKey: id) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ios/Classes/RxBlePlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface RxBlePlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/RxBlePlugin.m: -------------------------------------------------------------------------------- 1 | #import "RxBlePlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "rx_ble-Swift.h" 9 | #endif 10 | 11 | @implementation RxBlePlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftRxBlePlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/ScanMethods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dev Aggarwal on 2019-05-18. 3 | // 4 | 5 | import Foundation 6 | import plugin_scaffold 7 | import CoreBluetooth 8 | import RxBluetoothKit 9 | import RxSwift 10 | 11 | class ScanMethods: NSObject { 12 | let manager: CentralManager 13 | var disposable: Disposable? 14 | 15 | init(_ manager: CentralManager) { 16 | self.manager = manager 17 | super.init() 18 | } 19 | 20 | func _stopScan() { 21 | disposable?.dispose() 22 | disposable = nil 23 | } 24 | 25 | func onListen(id: Int, args: Any?, sink: @escaping FlutterEventSink) { 26 | let map = args as! [String: Any?] 27 | let deviceIdFilter = map["deviceId"] as? String 28 | let deviceNameFilter = map["deviceName"] as? String 29 | let serviceFilter = map["service"] as? String 30 | let services: [CBUUID]? = 31 | serviceFilter != nil ? [CBUUID(string: map["service"] as! String)] : nil 32 | 33 | _stopScan() 34 | 35 | disposable = manager.scanForPeripherals(withServices: services).subscribe( 36 | onNext: { 37 | let peripheral = $0.peripheral 38 | let deviceName = peripheral.name 39 | let deviceId = peripheral.identifier.uuidString 40 | let state = getDeviceState(deviceId) 41 | state.peripheral = peripheral 42 | 43 | if (deviceIdFilter != nil && deviceIdFilter != deviceId) 44 | || (deviceNameFilter != nil && deviceNameFilter != deviceName) { 45 | return 46 | } 47 | 48 | sink([ 49 | deviceName as Any, 50 | deviceId, 51 | $0.rssi as Any, 52 | Int(Date().timeIntervalSince1970 * 1000), 53 | ]) 54 | }, 55 | onError: { trySendError(sink, $0) }, 56 | onDisposed: { sink(FlutterEndOfEventStream) } 57 | ) 58 | } 59 | 60 | func onCancel(id: Int, args: Any?) { 61 | _stopScan() 62 | } 63 | 64 | func stopScan(call _: FlutterMethodCall, result: @escaping FlutterResult) { 65 | _stopScan() 66 | result(nil) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ios/Classes/SwiftRxBlePlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import plugin_scaffold 3 | import RxBluetoothKit 4 | import RxSwift 5 | import UIKit 6 | 7 | let pkgName = "com.pycampers.rx_ble" 8 | 9 | public class SwiftRxBlePlugin: NSObject, FlutterPlugin { 10 | public static func register(with registrar: FlutterPluginRegistrar) { 11 | let messenger = registrar.messenger() 12 | let manager = CentralManager(queue: .main) 13 | let permissionMethods = PermissionMethods(manager) 14 | let scanMethods = ScanMethods(manager) 15 | let connectMethods = ConnectMethods() 16 | let readWriteMethods = ReadWriteMethods() 17 | 18 | _ = createPluginScaffold(messenger: messenger, channelName: pkgName, methodMap: [ 19 | "requestAccess": permissionMethods.requestAccess, 20 | "hasAccess": permissionMethods.hasAccess, 21 | "stopScan": scanMethods.stopScan, 22 | "disconnect": connectMethods.disconnect, 23 | "getConnectionState": connectMethods.getConnectionState, 24 | "discoverChars": connectMethods.discoverChars, 25 | "readChar": readWriteMethods.readChar, 26 | "writeChar": readWriteMethods.writeChar, 27 | "scanOnListen": scanMethods.onListen, 28 | "scanOnCancel": scanMethods.onCancel, 29 | "connectOnListen": connectMethods.onListen, 30 | "connectOnCancel": connectMethods.onCancel, 31 | "observeCharOnListen": readWriteMethods.onListen, 32 | "observeCharOnCancel": readWriteMethods.onCancel 33 | ]) 34 | } 35 | } 36 | 37 | class DeviceState { 38 | var disposable: Disposable? 39 | var peripheral: Peripheral? 40 | 41 | func disconnect() { 42 | disposable?.dispose() 43 | disposable = nil 44 | } 45 | } 46 | 47 | enum Errors: Error { 48 | case deviceNotInitialized, charNotFound 49 | } 50 | 51 | var devices: [String: DeviceState] = [:] 52 | 53 | func getPeripheral(_ deviceId: String) throws -> Peripheral { 54 | if let peripheral = devices[deviceId]?.peripheral { 55 | return peripheral 56 | } 57 | throw Errors.deviceNotInitialized 58 | } 59 | 60 | func getDeviceState(_ deviceId: String) -> DeviceState { 61 | if let state = devices[deviceId] { 62 | return state 63 | } 64 | let deviceState = DeviceState() 65 | devices[deviceId] = deviceState 66 | return deviceState 67 | } 68 | 69 | var charCache = [String: Characteristic]() 70 | 71 | func putCharInCache(_ deviceId: String, _ char: Characteristic) { 72 | let key = deviceId + char.uuid.uuidString 73 | charCache[key] = char 74 | } 75 | 76 | func getCharFromCache(_ deviceId: String, _ uuid: String) throws -> Characteristic { 77 | let key = deviceId + uuid 78 | guard let char = charCache[key] else { throw Errors.charNotFound } 79 | return char 80 | } 81 | 82 | -------------------------------------------------------------------------------- /ios/rx_ble.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint rx_ble.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'rx_ble' 7 | s.version = '0.0.1' 8 | s.summary = 'A new flutter plugin project.' 9 | s.description = <<-DESC 10 | A new flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '8.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 22 | s.swift_version = '5.0' 23 | 24 | s.dependency 'plugin_scaffold' 25 | s.dependency 'RxBluetoothKit', '~> 5.3.0' 26 | end 27 | -------------------------------------------------------------------------------- /lib/rx_ble.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'dart:typed_data'; 5 | 6 | import 'package:flutter/services.dart'; 7 | import 'package:plugin_scaffold/plugin_scaffold.dart'; 8 | import 'package:rx_ble/src/exception_serializer.dart'; 9 | import 'package:rx_ble/src/exceptions.dart'; 10 | import 'package:rx_ble/src/models.dart'; 11 | 12 | export 'package:rx_ble/src/exceptions.dart'; 13 | export 'package:rx_ble/src/models.dart'; 14 | 15 | /// A callback that allows showing a small message to user 16 | /// explaining the need for the location permission. 17 | /// 18 | /// This method should return a Boolean value, 19 | /// indicating whether the library should continue 20 | /// asking the user for permission or not. 21 | typedef Future ShowLocationPermissionRationale(); 22 | 23 | class RxBle { 24 | static const pkgName = "com.pycampers.rx_ble"; 25 | static const channel = MethodChannel(pkgName); 26 | 27 | static Future invokeMethod(String method, [dynamic args]) async { 28 | try { 29 | return await channel.invokeMethod(method, args); 30 | } catch (e) { 31 | rethrowException(e); 32 | } 33 | } 34 | 35 | static final uuidRegex = RegExp( 36 | '(0000)([0-9A-z]{4})(-0000-1000-8000-00805f9b34fb)', 37 | ); 38 | 39 | static String encodeUUID(String uuid) { 40 | if (Platform.isAndroid && uuid.length == 4) { 41 | return "0000${uuid.toLowerCase()}-0000-1000-8000-00805f9b34fb"; 42 | } 43 | return uuid; 44 | } 45 | 46 | static String decodeUUID(String uuid) { 47 | if (Platform.isAndroid) { 48 | return uuidRegex.firstMatch(uuid)?.group(2)?.toUpperCase() ?? uuid; 49 | } 50 | return uuid; 51 | } 52 | 53 | /// Check if the app has all the necessary permissions, settings, etc. 54 | /// that are needed for the plugin to function. 55 | static Future hasAccess() async { 56 | return await invokeMethod("hasAccess"); 57 | } 58 | 59 | /// Check if the app has all the necessary permissions, settings, etc. 60 | /// that are needed for the plugin to function. 61 | /// 62 | /// If necessary conditions are not met, 63 | /// request the user for any permissions/settings that are required, 64 | /// automatically. 65 | /// 66 | /// No need to call [hasAccess] when using this function. 67 | /// 68 | /// [showRationale] - (Optional but recommended) 69 | /// A callback function that gets called when the 70 | /// app should probably show a message explaining why it needs the permission, 71 | /// since the user keeps denying the permission request! 72 | /// 73 | /// It is left up-to the developer to show the message however they wish. 74 | /// The default behavior is to continue asking user for permission anyway. 75 | /// 76 | /// For more info, refer the [Android documentation](https://developer.android.com/training/permissions/requesting#explain). 77 | static Future requestAccess({ 78 | ShowLocationPermissionRationale showRationale, 79 | }) async { 80 | int index; 81 | AccessStatus status; 82 | 83 | index = await invokeMethod("requestAccess"); 84 | 85 | try { 86 | status = AccessStatus.values[index]; 87 | } on RangeError { 88 | // app should show location permission rationale to user 89 | if (!(await showRationale?.call() ?? true)) { 90 | return AccessStatus.locationDenied; 91 | } 92 | 93 | index = await invokeMethod("requestLocPerm"); 94 | status = AccessStatus.values[index]; 95 | } 96 | 97 | return status; 98 | } 99 | 100 | static Future openAppSettings() async { 101 | await invokeMethod("openAppSettings"); 102 | } 103 | 104 | /// Returns an infinite [Stream] emitting BLE [ScanResult]s. 105 | /// 106 | /// Scanning can be stopped by either cancelling the [StreamSubscription], 107 | /// or by calling [stopScan]. 108 | static Stream startScan({ 109 | ScanModes scanMode: ScanModes.lowPower, 110 | String deviceName, 111 | String deviceId, 112 | String service, 113 | }) { 114 | return PluginScaffold.createStream(channel, "scan", { 115 | "scanMode": scanMode.index, 116 | "deviceName": deviceName, 117 | "deviceId": deviceId, 118 | "service": service, 119 | }).map((event) { 120 | return ScanResult.fromJson(event); 121 | }).handleError((e) { 122 | rethrowException(e); 123 | }); 124 | } 125 | 126 | /// Stops the Scan started by [startScan]. 127 | static Future stopScan() async { 128 | await invokeMethod("stopScan"); 129 | } 130 | 131 | static Future getConnectionState( 132 | String deviceId, 133 | ) async { 134 | return BleConnectionState 135 | .values[await RxBle.invokeMethod("getConnectionState", deviceId)]; 136 | } 137 | 138 | /// Establish connection to the BLE device identified by [deviceId] 139 | /// and emit the [BleConnectionState] changes. 140 | /// 141 | /// 142 | /// In cases when the BLE device is _not_ available at the time of calling this function, 143 | /// enabling [waitForDevice] will make the framework wait for device to start advertising. 144 | /// 145 | /// 146 | /// [BleConnectionState] is just a convenience value for 147 | /// easy monitoring that may be useful in the UI. 148 | /// It is not meant to be a trigger for reconnecting to a 149 | /// particular device. For that, use the [BleException]s. 150 | /// 151 | /// 152 | /// Throws the following errors: 153 | /// - [BleDisconnectedException] 154 | /// - [BleGattException] 155 | /// - [BleGattCallbackTimeoutException]. 156 | /// 157 | /// Device can be disconnected by either cancelling the [StreamSubscription], 158 | /// or by calling [disconnect]. 159 | /// 160 | /// [autoConnect] will catch [BleDisconnectedException], 161 | /// do a scan for the [deviceId], and connect automatically in background. 162 | /// 163 | /// 164 | /// It is mandatory that you perform a scan before issuing a connect request. 165 | /// In cases where the caller hasn't done a scan, 166 | /// and is loading the [deviceId] from say, local storage, the connect will fail. 167 | /// This can usually be solved using some simple dart [Stream] methods :- 168 | /// 169 | /// ```dart 170 | /// final scanResult = await RxBle.startScan(deviceId: "XX:XX:XX:XX:XX:XX") 171 | /// .timeout(Duration(seconds: 5)) 172 | /// .first; 173 | /// ``` 174 | /// 175 | /// This is done automatically when [autoConnect] is enabled. 176 | static Stream connect( 177 | String deviceId, { 178 | bool waitForDevice: false, 179 | bool autoConnect: false, 180 | ScanModes scanMode: ScanModes.lowPower, 181 | }) async* { 182 | Future doScan() async { 183 | final scanStream = 184 | RxBle.startScan(deviceId: deviceId, scanMode: scanMode); 185 | await for (final _ in scanStream) break; 186 | } 187 | 188 | while (true) { 189 | if (autoConnect) { 190 | await doScan(); 191 | } 192 | 193 | final stream = PluginScaffold.createStream(channel, "connect", { 194 | "deviceId": deviceId, 195 | "waitForDevice": waitForDevice, 196 | }).map((it) { 197 | return BleConnectionState.values[it]; 198 | }).handleError((e) { 199 | rethrowException(e); 200 | }); 201 | 202 | try { 203 | await for (final state in stream) { 204 | yield state; 205 | } 206 | } on BleDisconnectedException { 207 | yield BleConnectionState.disconnected; 208 | if (!autoConnect) rethrow; 209 | continue; 210 | } 211 | 212 | yield BleConnectionState.disconnected; 213 | break; 214 | } 215 | } 216 | 217 | /// Disconnect with the device with [deviceId]. 218 | /// 219 | /// If [deviceId] is not provided, 220 | /// then all previously connected devices are disconnected. 221 | static Future disconnect({String deviceId}) async { 222 | await RxBle.invokeMethod("disconnect", deviceId); 223 | } 224 | 225 | static Future>> discoverChars( 226 | String deviceId, 227 | ) async { 228 | final value = await RxBle.invokeMethod("discoverChars", deviceId); 229 | return Map>.from( 230 | value.map((k, v) { 231 | return MapEntry( 232 | decodeUUID(k), 233 | List.from( 234 | v.map((it) { 235 | return decodeUUID(it); 236 | }), 237 | ), 238 | ); 239 | }), 240 | ); 241 | } 242 | 243 | /// Performs GATT read operation on a characteristic with given [uuid]. 244 | /// 245 | /// Throws the following errors: 246 | /// - [BleCharacteristicNotFoundException] 247 | /// - [BleGattCannotStartException] 248 | /// - [BleGattException]. 249 | static Future readChar(String deviceId, String uuid) async { 250 | return await RxBle.invokeMethod("readChar", { 251 | "deviceId": deviceId, 252 | "uuid": encodeUUID(uuid), 253 | }); 254 | } 255 | 256 | /// Set up BLE notifications, 257 | /// and emit the changes in the characteristic with the given [uuid]. 258 | static Stream observeChar(String deviceId, String uuid) { 259 | return PluginScaffold.createStream(channel, "observeChar", { 260 | "deviceId": deviceId, 261 | "uuid": encodeUUID(uuid), 262 | }).map((it) { 263 | return it as Uint8List; 264 | }).handleError((e) { 265 | rethrowException(e); 266 | }); 267 | } 268 | 269 | /// Performs GATT write operation on a characteristic with given [uuid] and [value]. 270 | /// 271 | /// Throws the following errors: 272 | /// - [BleCharacteristicNotFoundException] 273 | /// - [BleGattCannotStartException] 274 | /// - [BleGattException]. 275 | static Future writeChar( 276 | String deviceId, 277 | String uuid, 278 | Uint8List value, 279 | ) async { 280 | return await RxBle.invokeMethod("writeChar", { 281 | "deviceId": deviceId, 282 | "uuid": encodeUUID(uuid), 283 | "value": value, 284 | }); 285 | } 286 | 287 | /// Performs GATT MTU (Maximum Transfer Unit) request. 288 | /// 289 | /// Timeouts after 10 seconds. 290 | /// 291 | /// Throws the following errors: 292 | /// - [BleGattCannotStartException] 293 | /// - [BleGattException] 294 | static Future requestMtu(String deviceId, int value) async { 295 | return await RxBle.invokeMethod("requestMtu", { 296 | "deviceId": deviceId, 297 | "value": value, 298 | }); 299 | } 300 | 301 | /// Converts characteristic value returned by [readChar] into utf-8 [String], 302 | /// By removing zeros (null value) and trimming leading and trailing whitespace. 303 | static String charToString(Uint8List value, {bool allowMalformed: false}) { 304 | return utf8 305 | .decode( 306 | value.where((it) => it != 0).toList(), 307 | allowMalformed: allowMalformed, 308 | ) 309 | .trim(); 310 | } 311 | 312 | /// Converts utf-8 string to characteristic value 313 | static Uint8List stringToChar(String value) { 314 | return utf8.encode(value); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /lib/src/exception_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:rx_ble/src/exceptions.dart'; 5 | 6 | final exceptionRegex = RegExp( 7 | r'(com\.polidea\.rxandroidble2\.exceptions\.|java\.lang\.)(\w+)', 8 | ); 9 | 10 | void rethrowException(e) { 11 | if (e is! PlatformException) throw e; 12 | 13 | final message = e.message; 14 | final details = e.details; 15 | 16 | if (Platform.isIOS) { 17 | if(message.toString().contains("RxBluetoothKit.BluetoothError error 4")){ 18 | throw BleDisconnectedException(message, details); 19 | } 20 | throw BleException(message, details); 21 | } 22 | 23 | final code = exceptionRegex.firstMatch(e.code)?.group(2); 24 | 25 | switch (code) { 26 | case "BleDisconnectedException": 27 | throw BleDisconnectedException(message, details); 28 | case "BleGattException": 29 | throw BleGattException(message, details); 30 | case "BleGattCallbackTimeoutException": 31 | throw BleGattCallbackTimeoutException(message, details); 32 | case "BleCharacteristicNotFoundException": 33 | throw BleCharacteristicNotFoundException(message, details); 34 | case "BleGattCharacteristicException": 35 | throw BleGattCharacteristicException(message, details); 36 | case "BleGattCannotStartException": 37 | throw BleGattCannotStartException(message, details); 38 | case "BleScanException": 39 | throw BleScanException(message, details); 40 | case "IllegalArgumentException": 41 | throw IllegalArgumentException(message, details); 42 | default: 43 | throw e; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/exceptions.dart: -------------------------------------------------------------------------------- 1 | /// base class for all BLE exceptions 2 | class BleException implements Exception { 3 | final String message, details; 4 | 5 | BleException(this.message, this.details); 6 | 7 | @override 8 | String toString() { 9 | return "$runtimeType: $message\n$details"; 10 | } 11 | } 12 | 13 | /// emitted when the BLE link has been disconnected either when the connection 14 | /// was already established or was in pending connection state. This occurs when the 15 | /// connection was released as a part of expected behavior. 16 | class BleDisconnectedException extends BleException { 17 | BleDisconnectedException(String message, String details) : super(message, details); 18 | } 19 | 20 | /// emitted when the BLE link has been interrupted as a result of an error. 21 | /// The exception contains detailed explanation of the error source (type of operation) and 22 | /// the code proxied from the Android system. 23 | class BleGattException extends BleException { 24 | BleGattException(String message, String details) : super(message, details); 25 | } 26 | 27 | /// emitted when an internal timeout for connection has been reached. The operation will 28 | /// timeout in direct mode (autoConnect = false) after 35 seconds. 29 | class BleGattCallbackTimeoutException extends BleException { 30 | BleGattCallbackTimeoutException(String message, String details) 31 | : super(message, details); 32 | } 33 | 34 | /// An exception being emitted from any RxBleConnection function 35 | /// that accepts UUID in case the said 36 | /// UUID is not found in the discovered device 37 | class BleCharacteristicNotFoundException extends BleException { 38 | BleCharacteristicNotFoundException(String message, String details) 39 | : super(message, details); 40 | } 41 | 42 | /// An exception emitted from RxBleConnection functions when the underlying 43 | /// BluetoothGatt returns [false] from BluetoothGatt.readRemoteRssi() 44 | /// or other functions associated with device interaction 45 | class BleGattCannotStartException extends BleException { 46 | BleGattCannotStartException(String message, String details) : super(message, details); 47 | } 48 | 49 | class BleScanException extends BleException { 50 | BleScanException(String message, String details) : super(message, details); 51 | } 52 | 53 | class IllegalArgumentException extends BleException { 54 | IllegalArgumentException(String message, String details) : super(message, details); 55 | } 56 | 57 | class BleGattCharacteristicException extends BleException { 58 | BleGattCharacteristicException(String message, String details) 59 | : super(message, details); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:rx_ble/rx_ble.dart'; 2 | 3 | enum AccessStatus { 4 | ok, 5 | 6 | /// bluetooth is disabled. 7 | bluetoothDisabled, 8 | 9 | /// location is disabled. 10 | locationDisabled, 11 | 12 | /// location permission denied by user. 13 | locationDenied, 14 | 15 | /// location permission denied by user, 16 | /// and "Never ask again" checkbox was ticked. 17 | /// 18 | /// In this case, the only way to acquire permission, 19 | /// is to make the user manually go to app settings, 20 | /// and enable the switch. 21 | /// 22 | /// You may use the utility method [RxBle.openAppSettings] 23 | /// to open the settings page programmatically. 24 | locationDeniedNeverAskAgain, 25 | 26 | /// Bluetooth is not available on this device 27 | bluetoothNotAvailable 28 | } 29 | 30 | enum ScanModes { 31 | /// A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for 32 | /// other scan results without starting BLE scans themselves. 33 | opportunistic, 34 | 35 | /// Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the 36 | /// least power. This mode is enforced if the scanning application is not in foreground. 37 | lowPower, 38 | 39 | /// Perform Bluetooth LE scan in balanced power mode. Scan results are returned at a rate that 40 | /// provides a good trade-off between scan frequency and power consumption. 41 | balanced, 42 | 43 | /// Scan using highest duty cycle. It's recommended to only use this mode when the application is 44 | /// running in the foreground. 45 | lowLatency, 46 | } 47 | 48 | enum BleConnectionState { connecting, connected, disconnected, disconnecting } 49 | 50 | enum ScanCallbackType { 51 | allMatches, 52 | firstMatch, 53 | matchLost, 54 | batch, 55 | unspecified, 56 | unknown 57 | } 58 | 59 | class ScanResult { 60 | final String deviceName, deviceId; 61 | final int rssi; 62 | final DateTime time; 63 | 64 | ScanResult.fromJson(List msg) 65 | : deviceName = msg[0], 66 | deviceId = msg[1], 67 | rssi = msg[2], 68 | time = DateTime.fromMillisecondsSinceEpoch(msg[3]); 69 | 70 | @override 71 | String toString() { 72 | return "ScanResult { deviceName: $deviceName, deviceId: $deviceId, rssi: $rssi, time: $time }"; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.1.3" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | image: 71 | dependency: transitive 72 | description: 73 | name: image 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "2.1.4" 77 | matcher: 78 | dependency: transitive 79 | description: 80 | name: matcher 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "0.12.6" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.1.8" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.6.4" 98 | pedantic: 99 | dependency: transitive 100 | description: 101 | name: pedantic 102 | url: "https://pub.dartlang.org" 103 | source: hosted 104 | version: "1.8.0+1" 105 | petitparser: 106 | dependency: transitive 107 | description: 108 | name: petitparser 109 | url: "https://pub.dartlang.org" 110 | source: hosted 111 | version: "2.4.0" 112 | plugin_scaffold: 113 | dependency: "direct main" 114 | description: 115 | name: plugin_scaffold 116 | url: "https://pub.dartlang.org" 117 | source: hosted 118 | version: "3.0.1" 119 | quiver: 120 | dependency: transitive 121 | description: 122 | name: quiver 123 | url: "https://pub.dartlang.org" 124 | source: hosted 125 | version: "2.0.5" 126 | sky_engine: 127 | dependency: transitive 128 | description: flutter 129 | source: sdk 130 | version: "0.0.99" 131 | source_span: 132 | dependency: transitive 133 | description: 134 | name: source_span 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.5.5" 138 | stack_trace: 139 | dependency: transitive 140 | description: 141 | name: stack_trace 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.9.3" 145 | stream_channel: 146 | dependency: transitive 147 | description: 148 | name: stream_channel 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "2.0.0" 152 | string_scanner: 153 | dependency: transitive 154 | description: 155 | name: string_scanner 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "1.0.5" 159 | term_glyph: 160 | dependency: transitive 161 | description: 162 | name: term_glyph 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "1.1.0" 166 | test_api: 167 | dependency: transitive 168 | description: 169 | name: test_api 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "0.2.11" 173 | typed_data: 174 | dependency: transitive 175 | description: 176 | name: typed_data 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "1.1.6" 180 | vector_math: 181 | dependency: transitive 182 | description: 183 | name: vector_math 184 | url: "https://pub.dartlang.org" 185 | source: hosted 186 | version: "2.0.8" 187 | xml: 188 | dependency: transitive 189 | description: 190 | name: xml 191 | url: "https://pub.dartlang.org" 192 | source: hosted 193 | version: "3.5.0" 194 | sdks: 195 | dart: ">=2.4.0 <3.0.0" 196 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rx_ble 2 | description: A Flutter BLE plugin, based on RxAndroidBle and RxBluetoothKit. 3 | version: 1.0.0 4 | author: Dev Aggarwal 5 | homepage: https://github.com/pycampers/flutter-rx-ble 6 | 7 | environment: 8 | sdk: ">=2.1.0 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | plugin_scaffold: ^3.0.1 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | 20 | # For information on the generic Dart part of this file, see the 21 | # following page: https://www.dartlang.org/tools/pub/pubspec 22 | 23 | # The following section is specific to Flutter. 24 | flutter: 25 | # This section identifies this Flutter project as a plugin project. 26 | # The androidPackage and pluginClass identifiers should not ordinarily 27 | # be modified. They are used by the tooling to maintain consistency when 28 | # adding or updating assets for this project. 29 | plugin: 30 | androidPackage: com.pycampers.rx_ble 31 | pluginClass: RxBlePlugin 32 | 33 | # To add assets to your plugin package, add an assets section, like this: 34 | # assets: 35 | # - images/a_dot_burr.jpeg 36 | # - images/a_dot_ham.jpeg 37 | # 38 | # For details regarding assets in packages, see 39 | # https://flutter.dev/assets-and-images/#from-packages 40 | # 41 | # An image asset can refer to one or more resolution-specific "variants", see 42 | # https://flutter.dev/assets-and-images/#resolution-aware. 43 | 44 | # To add custom fonts to your plugin package, add a fonts section here, 45 | # in this "flutter" section. Each entry in this list should have a 46 | # "family" key with the font family name, and a "fonts" key with a 47 | # list giving the asset and other descriptors for the font. For 48 | # example: 49 | # fonts: 50 | # - family: Schyler 51 | # fonts: 52 | # - asset: fonts/Schyler-Regular.ttf 53 | # - asset: fonts/Schyler-Italic.ttf 54 | # style: italic 55 | # - family: Trajan Pro 56 | # fonts: 57 | # - asset: fonts/TrajanPro.ttf 58 | # - asset: fonts/TrajanPro_Bold.ttf 59 | # weight: 700 60 | # 61 | # For details regarding fonts in packages, see 62 | # https://flutter.dev/custom-fonts/#from-packages 63 | -------------------------------------------------------------------------------- /rx_ble.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------