├── .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 | [](https://www.jaaga.in/labs) [](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