├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── pauldemarco │ └── flutter_blue │ ├── AdvertisementParser.java │ ├── FlutterBluePlugin.java │ └── ProtoMaker.java ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── pauldemarco │ │ │ │ │ └── flutter_blue_example │ │ │ │ │ ├── EmbeddingV1Activity.java │ │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── 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-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── flutter_blue_example.iml ├── 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 │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── 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 │ │ └── main.m ├── lib │ ├── main.dart │ └── widgets.dart ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ └── MainMenu.xib │ │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements ├── pubspec.yaml └── test │ └── widget_test.dart ├── flutter_blue.iml ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── FlutterBluePlugin.h │ └── FlutterBluePlugin.m ├── flutter_blue.podspec └── gen │ ├── Flutterblue.pbobjc.h │ └── Flutterblue.pbobjc.m ├── lib ├── flutter_blue.dart ├── gen │ ├── flutterblue.pb.dart │ ├── flutterblue.pbenum.dart │ ├── flutterblue.pbjson.dart │ └── flutterblue.pbserver.dart └── src │ ├── bluetooth_characteristic.dart │ ├── bluetooth_descriptor.dart │ ├── bluetooth_device.dart │ ├── bluetooth_service.dart │ ├── constants.dart │ ├── flutter_blue.dart │ └── guid.dart ├── macos ├── Classes │ ├── FlutterBluePlugin.h │ └── FlutterBluePlugin.m ├── flutter_blue.podspec └── gen │ ├── Flutterblue.pbobjc.h │ └── Flutterblue.pbobjc.m ├── protos ├── flutterblue.proto └── regenerate.md ├── pubspec.yaml ├── site ├── flutterblue.png └── flutterblue.svg └── test └── guid_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | pubspec.lock 7 | 8 | build/ 9 | 10 | .atom/ 11 | .idea/ 12 | .vscode/ -------------------------------------------------------------------------------- /.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: f53b32eb2317ba09137969999d130c24a6314997 8 | channel: master 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.8.0 2 | * Migrate the plugin to null safety. 3 | 4 | ## 0.7.3 5 | * Fix Android project template files to be compatible with protobuf-lite. 6 | * Add experimental support for MacOS. 7 | 8 | ## 0.7.2 9 | * Add `allowDuplicates` option to `startScan`. 10 | * Fix performance issue with GUID initializers. 11 | 12 | ## 0.7.1+1 13 | * Fix for FlutterBlue constructor when running on emulator. 14 | * Return error when attempting to `discoverServices` while not connected. 15 | 16 | ## 0.7.1 17 | * Fix incorrect value notification when write is performed. 18 | * Add `toString` to each bluetooth class. 19 | * Various other bug fixes. 20 | 21 | ## 0.7.0 22 | * Support v2 android embedding. 23 | * Various bug and documentation fixes. 24 | 25 | ## 0.6.3+1 26 | * Fix compilation issue with iOS. 27 | * Bump protobuf version to 1.0.0. 28 | 29 | ## 0.6.3 30 | * Update project files for Android and iOS. 31 | * Remove dependency on protoc for iOS. 32 | 33 | ## 0.6.2 34 | * Add `mtu` and `requestMtu` to BluetoothDevice. 35 | 36 | ## 0.6.0+4 37 | * Fix duplicate characteristic notifications when connection lost. 38 | * Fix duplicate characteristic notifications when reconnecting. 39 | * Add minimum SDK version of 18 for the plugin. 40 | * Documentation updates. 41 | 42 | ## 0.6.0 43 | * **Breaking change**. API refactoring with RxDart (see example). 44 | * Log a more detailed warning at build time about the previous AndroidX migration. 45 | * Ensure that all channel calls to the Dart side from the Java side are done on the UI thread. 46 | This change allows Transactions to work with upcoming Engine restrictions, which require 47 | channel calls be made on the UI thread. Note this is an Android only change, 48 | the iOS implementation was not impacted. 49 | 50 | ## 0.5.0 51 | * **Breaking change**. Migrate from the deprecated original Android Support 52 | Library to AndroidX. This shouldn't result in any functional changes, but it 53 | requires any Android apps using this plugin to [also 54 | migrate](https://developer.android.com/jetpack/androidx/migrate) if they're 55 | using the original support library. 56 | 57 | ## 0.4.2+1 58 | * Upgrade Android Gradle plugin to 3.3.0. 59 | * Refresh iOS build files. 60 | 61 | ## 0.4.2 62 | * Set the verbosity of log messages with `setLogLevel`. 63 | * Updated iOS and Android project files. 64 | * `autoConnect` now configurable for Android. 65 | * Various bug fixes. 66 | 67 | ## 0.4.1 68 | * Fixed bug where setNotifyValue wasn't properly awaitable. 69 | * Various UI bug fixes to example app. 70 | * Removed unnecessary intl dependencies in example app. 71 | 72 | ## 0.4.0 73 | * **Breaking change**. Manufacturer Data is now a `Map` of manufacturer ID's. 74 | * Service UUID's, service data, tx power level packets fixed in advertising data. 75 | * Example app updated to show advertising data. 76 | * Various other bug fixes. 77 | 78 | ## 0.3.4 79 | * Updated to use the latest protobuf (^0.9.0+1). 80 | * Updated other dependencies. 81 | 82 | ## 0.3.3 83 | * `scan` `withServices` to filter by service UUID's (iOS). 84 | * Error handled when trying to scan with adapter off (Android). 85 | 86 | ## 0.3.2 87 | * Runtime permissions for Android. 88 | * `scan` `withServices` to filter by service UUID's (Android). 89 | * Scan mode can be specified (Android). 90 | * Now targets the latest android SDK. 91 | * Dart 2 compatibility. 92 | 93 | ## 0.3.1 94 | * Now allows simultaneous notifications of characteristics. 95 | * Fixed bug on iOS that was returning `discoverServices` too early. 96 | 97 | ## 0.3.0 98 | * iOS support added. 99 | * Bug fixed in example causing discoverServices to be called multiple times. 100 | * Various other bug fixes. 101 | 102 | ## 0.2.4 103 | * **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin 104 | 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in 105 | order to use this version of the plugin. Instructions can be found 106 | [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). 107 | 108 | ## 0.2.3 109 | * Bug fixes 110 | 111 | ## 0.2.2 112 | * **Breaking changes**: 113 | * `startScan` renamed to `scan`. 114 | * `ScanResult` now returns a `BluetoothDevice`. 115 | * `connect` now takes a `BluetoothDevice` and returns Stream. 116 | * Added parameter `timeout` to `connect`. 117 | * Automatic disconnect on deviceConnection.cancel(). 118 | 119 | ## 0.2.1 120 | * **Breaking change**. Removed `stopScan` from API, use `scanSubscription.cancel()` instead. 121 | * Automatically stops scan when `startScan` subscription is canceled (thanks to @brianegan). 122 | * Added `timeout` parameter to `startScan`. 123 | * Updated example app to show new scan functionality. 124 | 125 | ## 0.2.0 126 | 127 | * Added state and onStateChanged for BluetoothDevice. 128 | * Updated example to show new functionality. 129 | 130 | ## 0.1.1 131 | 132 | * Fixed image for pub.dartlang.org. 133 | 134 | ## 0.1.0 135 | 136 | * Characteristic notifications/indications. 137 | * Merged in Guid library, removed from pubspec.yaml. 138 | 139 | ## 0.0.1 - September 1st, 2017 140 | 141 | * Initial Release. 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Paul DeMarco. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Buffalo PC Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pub package](https://img.shields.io/pub/v/flutter_blue.svg)](https://pub.dartlang.org/packages/flutter_blue) 2 | [![Chat](https://img.shields.io/discord/634853295160033301.svg?style=flat-square&colorB=758ED3)](https://discord.gg/Yk5Efra) 3 | 4 |
5 |

6 | FlutterBlue 7 |

8 |

9 | 10 | ## Introduction 11 | 12 | FlutterBlue is a bluetooth plugin for [Flutter](https://flutter.dev), a new app SDK to help developers build modern multi-platform apps. 13 | 14 | ## Alpha version 15 | 16 | This library is actively developed alongside production apps, and the API will evolve as we continue our way to version 1.0. 17 | 18 | **Please be fully prepared to deal with breaking changes.** 19 | **This package must be tested on a real device.** 20 | 21 | Having trouble adapting to the latest API? I'd love to hear your use-case, please contact me. 22 | 23 | ## Cross-Platform Bluetooth LE 24 | FlutterBlue aims to offer the most from both platforms (iOS and Android). 25 | 26 | Using the FlutterBlue instance, you can scan for and connect to nearby devices ([BluetoothDevice](#bluetoothdevice-api)). 27 | Once connected to a device, the BluetoothDevice object can discover services ([BluetoothService](lib/src/bluetooth_service.dart)), characteristics ([BluetoothCharacteristic](lib/src/bluetooth_characteristic.dart)), and descriptors ([BluetoothDescriptor](lib/src/bluetooth_descriptor.dart)). 28 | The BluetoothDevice object is then used to directly interact with characteristics and descriptors. 29 | 30 | ## Usage 31 | ### Obtain an instance 32 | ```dart 33 | FlutterBlue flutterBlue = FlutterBlue.instance; 34 | ``` 35 | 36 | ### Scan for devices 37 | ```dart 38 | // Start scanning 39 | flutterBlue.startScan(timeout: Duration(seconds: 4)); 40 | 41 | // Listen to scan results 42 | var subscription = flutterBlue.scanResults.listen((results) { 43 | // do something with scan results 44 | for (ScanResult r in results) { 45 | print('${r.device.name} found! rssi: ${r.rssi}'); 46 | } 47 | }); 48 | 49 | // Stop scanning 50 | flutterBlue.stopScan(); 51 | ``` 52 | 53 | ### Connect to a device 54 | ```dart 55 | // Connect to the device 56 | await device.connect(); 57 | 58 | // Disconnect from device 59 | device.disconnect(); 60 | ``` 61 | 62 | ### Discover services 63 | ```dart 64 | List services = await device.discoverServices(); 65 | services.forEach((service) { 66 | // do something with service 67 | }); 68 | ``` 69 | 70 | ### Read and write characteristics 71 | ```dart 72 | // Reads all characteristics 73 | var characteristics = service.characteristics; 74 | for(BluetoothCharacteristic c in characteristics) { 75 | List value = await c.read(); 76 | print(value); 77 | } 78 | 79 | // Writes to a characteristic 80 | await c.write([0x12, 0x34]) 81 | ``` 82 | 83 | ### Read and write descriptors 84 | ```dart 85 | // Reads all descriptors 86 | var descriptors = characteristic.descriptors; 87 | for(BluetoothDescriptor d in descriptors) { 88 | List value = await d.read(); 89 | print(value); 90 | } 91 | 92 | // Writes to a descriptor 93 | await d.write([0x12, 0x34]) 94 | ``` 95 | 96 | ### Set notifications and listen to changes 97 | ```dart 98 | await characteristic.setNotifyValue(true); 99 | characteristic.value.listen((value) { 100 | // do something with new value 101 | }); 102 | ``` 103 | 104 | ### Read the MTU and request larger size 105 | ```dart 106 | final mtu = await device.mtu.first; 107 | await device.requestMtu(512); 108 | ``` 109 | Note that iOS will not allow requests of MTU size, and will always try to negotiate the highest possible MTU (iOS supports up to MTU size 185) 110 | 111 | ## Getting Started 112 | ### Change the minSdkVersion for Android 113 | 114 | Flutter_blue is compatible only from version 19 of Android SDK so you should change this in **android/app/build.gradle**: 115 | ```dart 116 | Android { 117 | defaultConfig { 118 | minSdkVersion: 19 119 | ``` 120 | ### Add permissions for Bluetooth 121 | We need to add the permission to use Bluetooth and access location: 122 | 123 | #### **Android** 124 | In the **android/app/src/main/AndroidManifest.xml** let’s add: 125 | 126 | ```xml 127 | 128 | 129 | 130 | 137 | NSBluetoothAlwaysUsageDescription 138 | Need BLE permission 139 | NSBluetoothPeripheralUsageDescription 140 | Need BLE permission 141 | NSLocationAlwaysAndWhenInUseUsageDescription 142 | Need Location permission 143 | NSLocationAlwaysUsageDescription 144 | Need Location permission 145 | NSLocationWhenInUseUsageDescription 146 | Need Location permission 147 | ``` 148 | 149 | For location permissions on iOS see more at: [https://developer.apple.com/documentation/corelocation/requesting_authorization_for_location_services](https://developer.apple.com/documentation/corelocation/requesting_authorization_for_location_services) 150 | 151 | 152 | ## Reference 153 | ### FlutterBlue API 154 | | | Android | iOS | Description | 155 | | :--------------- | :----------------: | :------------------: | :-------------------------------- | 156 | | scan | :white_check_mark: | :white_check_mark: | Starts a scan for Bluetooth Low Energy devices. | 157 | | state | :white_check_mark: | :white_check_mark: | Stream of state changes for the Bluetooth Adapter. | 158 | | isAvailable | :white_check_mark: | :white_check_mark: | Checks whether the device supports Bluetooth. | 159 | | isOn | :white_check_mark: | :white_check_mark: | Checks if Bluetooth functionality is turned on. | 160 | 161 | ### BluetoothDevice API 162 | | | Android | iOS | Description | 163 | | :-------------------------- | :------------------: | :------------------: | :-------------------------------- | 164 | | connect | :white_check_mark: | :white_check_mark: | Establishes a connection to the device. | 165 | | disconnect | :white_check_mark: | :white_check_mark: | Cancels an active or pending connection to the device. | 166 | | discoverServices | :white_check_mark: | :white_check_mark: | Discovers services offered by the remote device as well as their characteristics and descriptors. | 167 | | services | :white_check_mark: | :white_check_mark: | Gets a list of services. Requires that discoverServices() has completed. | 168 | | state | :white_check_mark: | :white_check_mark: | Stream of state changes for the Bluetooth Device. | 169 | | mtu | :white_check_mark: | :white_check_mark: | Stream of mtu size changes. | 170 | | requestMtu | :white_check_mark: | | Request to change the MTU for the device. | 171 | 172 | ### BluetoothCharacteristic API 173 | | | Android | iOS | Description | 174 | | :-------------------------- | :------------------: | :------------------: | :-------------------------------- | 175 | | read | :white_check_mark: | :white_check_mark: | Retrieves the value of the characteristic. | 176 | | write | :white_check_mark: | :white_check_mark: | Writes the value of the characteristic. | 177 | | setNotifyValue | :white_check_mark: | :white_check_mark: | Sets notifications or indications on the characteristic. | 178 | | value | :white_check_mark: | :white_check_mark: | Stream of characteristic's value when changed. | 179 | 180 | ### BluetoothDescriptor API 181 | | | Android | iOS | Description | 182 | | :-------------------------- | :------------------: | :------------------: | :-------------------------------- | 183 | | read | :white_check_mark: | :white_check_mark: | Retrieves the value of the descriptor. | 184 | | write | :white_check_mark: | :white_check_mark: | Writes the value of the descriptor. | 185 | 186 | ## Troubleshooting 187 | ### When I scan using a service UUID filter, it doesn't find any devices. 188 | Make sure the device is advertising which service UUID's it supports. This is found in the advertisement 189 | packet as **UUID 16 bit complete list** or **UUID 128 bit complete list**. 190 | -------------------------------------------------------------------------------- /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.pauldemarco.flutter_blue' 2 | version '1.0' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:4.1.0' 12 | classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.15' 13 | } 14 | } 15 | 16 | rootProject.allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | apply plugin: 'com.android.library' 24 | apply plugin: 'com.google.protobuf' 25 | 26 | android { 27 | compileSdkVersion 30 28 | 29 | defaultConfig { 30 | minSdkVersion 19 31 | } 32 | sourceSets { 33 | main { 34 | proto { 35 | srcDir '../protos' 36 | } 37 | } 38 | } 39 | } 40 | 41 | protobuf { 42 | protoc { 43 | artifact = 'com.google.protobuf:protoc:3.11.4' 44 | } 45 | generateProtoTasks { 46 | all().each { task -> 47 | task.builtins { 48 | java { 49 | option "lite" 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | dependencies { 57 | implementation 'com.google.protobuf:protobuf-javalite:3.11.4' 58 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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-6.7-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_blue' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/src/main/java/com/pauldemarco/flutter_blue/AdvertisementParser.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017, the Flutter project authors. All rights reserved. 2 | // Redistribution and use in source and binary forms, with or without 3 | // modification, are permitted provided that the following conditions are 4 | // met: 5 | // 6 | // * Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // * Redistributions in binary form must reproduce the above 9 | // copyright notice, this list of conditions and the following 10 | // disclaimer in the documentation and/or other materials provided 11 | // with the distribution. 12 | // * Neither the name of Google Inc. nor the names of its 13 | // contributors may be used to endorse or promote products derived 14 | // from this software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | package com.pauldemarco.flutter_blue; 29 | 30 | import com.google.protobuf.ByteString; 31 | import com.pauldemarco.flutter_blue.Protos.AdvertisementData; 32 | 33 | import java.io.UnsupportedEncodingException; 34 | import java.nio.ByteBuffer; 35 | import java.nio.ByteOrder; 36 | import java.util.UUID; 37 | 38 | /** 39 | * Parser of Bluetooth Advertisement packets. 40 | */ 41 | class AdvertisementParser { 42 | 43 | /** 44 | * Parses packet data into {@link AdvertisementData} structure. 45 | * 46 | * @param rawData The scan record data. 47 | * @return An AdvertisementData proto object. 48 | * @throws ArrayIndexOutOfBoundsException if the input is truncated. 49 | */ 50 | static AdvertisementData parse(byte[] rawData) { 51 | ByteBuffer data = ByteBuffer.wrap(rawData).asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); 52 | AdvertisementData.Builder ret = AdvertisementData.newBuilder(); 53 | boolean seenLongLocalName = false; 54 | do { 55 | int length = data.get() & 0xFF; 56 | if (length == 0) { 57 | break; 58 | } 59 | if (length > data.remaining()) { 60 | throw new ArrayIndexOutOfBoundsException("Not enough data."); 61 | } 62 | 63 | int type = data.get() & 0xFF; 64 | length--; 65 | 66 | switch (type) { 67 | case 0x08: // Short local name. 68 | case 0x09: { // Long local name. 69 | if (seenLongLocalName) { 70 | // Prefer the long name over the short. 71 | data.position(data.position() + length); 72 | break; 73 | } 74 | byte[] name = new byte[length]; 75 | data.get(name); 76 | try { 77 | ret.setLocalName(new String(name, "UTF-8")); 78 | } catch (UnsupportedEncodingException e) { 79 | throw new RuntimeException(e); 80 | } 81 | if (type == 0x09) { 82 | seenLongLocalName = true; 83 | } 84 | break; 85 | } 86 | case 0x0A: { // Power level. 87 | ret.setTxPowerLevel(Protos.Int32Value.newBuilder().setValue(data.get())); 88 | break; 89 | } 90 | case 0x16: // Service Data with 16 bit UUID. 91 | case 0x20: // Service Data with 32 bit UUID. 92 | case 0x21: { // Service Data with 128 bit UUID. 93 | UUID uuid; 94 | int remainingDataLength = 0; 95 | if (type == 0x16 || type == 0x20) { 96 | long uuidValue; 97 | if (type == 0x16) { 98 | uuidValue = data.getShort() & 0xFFFF; 99 | remainingDataLength = length - 2; 100 | } else { 101 | uuidValue = data.getInt() & 0xFFFFFFFF; 102 | remainingDataLength = length - 4; 103 | } 104 | uuid = UUID.fromString(String.format("%08x-0000-1000-8000-00805f9b34fb", uuidValue)); 105 | } else { 106 | long msb = data.getLong(); 107 | long lsb = data.getLong(); 108 | uuid = new UUID(msb, lsb); 109 | remainingDataLength = length - 16; 110 | } 111 | byte[] remainingData = new byte[remainingDataLength]; 112 | data.get(remainingData); 113 | ret.putServiceData(uuid.toString(), ByteString.copyFrom(remainingData)); 114 | break; 115 | } 116 | case 0xFF: {// Manufacturer specific data. 117 | if(length < 2) { 118 | throw new ArrayIndexOutOfBoundsException("Not enough data for Manufacturer specific data."); 119 | } 120 | int manufacturerId = data.getShort(); 121 | if((length - 2) > 0) { 122 | byte[] msd = new byte[length - 2]; 123 | data.get(msd); 124 | ret.putManufacturerData(manufacturerId, ByteString.copyFrom(msd)); 125 | } 126 | break; 127 | } 128 | default: { 129 | data.position(data.position() + length); 130 | break; 131 | } 132 | } 133 | } while (true); 134 | return ret.build(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /android/src/main/java/com/pauldemarco/flutter_blue/ProtoMaker.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | package com.pauldemarco.flutter_blue; 6 | 7 | import android.annotation.TargetApi; 8 | import android.bluetooth.BluetoothDevice; 9 | import android.bluetooth.BluetoothGatt; 10 | import android.bluetooth.BluetoothGattCharacteristic; 11 | import android.bluetooth.BluetoothGattDescriptor; 12 | import android.bluetooth.BluetoothGattService; 13 | import android.bluetooth.BluetoothProfile; 14 | import android.bluetooth.le.ScanRecord; 15 | import android.bluetooth.le.ScanResult; 16 | import android.os.Build; 17 | import android.os.Parcel; 18 | import android.os.ParcelUuid; 19 | import android.util.Log; 20 | import android.util.SparseArray; 21 | 22 | import com.google.protobuf.ByteString; 23 | 24 | import java.util.Iterator; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.UUID; 28 | 29 | /** 30 | * Created by paul on 8/31/17. 31 | */ 32 | 33 | public class ProtoMaker { 34 | 35 | private static final UUID CCCD_UUID = UUID.fromString("000002902-0000-1000-8000-00805f9b34fb"); 36 | 37 | static Protos.ScanResult from(BluetoothDevice device, byte[] advertisementData, int rssi) { 38 | Protos.ScanResult.Builder p = Protos.ScanResult.newBuilder(); 39 | p.setDevice(from(device)); 40 | if(advertisementData != null && advertisementData.length > 0) 41 | p.setAdvertisementData(AdvertisementParser.parse(advertisementData)); 42 | p.setRssi(rssi); 43 | return p.build(); 44 | } 45 | 46 | @TargetApi(21) 47 | static Protos.ScanResult from(BluetoothDevice device, ScanResult scanResult) { 48 | Protos.ScanResult.Builder p = Protos.ScanResult.newBuilder(); 49 | p.setDevice(from(device)); 50 | Protos.AdvertisementData.Builder a = Protos.AdvertisementData.newBuilder(); 51 | ScanRecord scanRecord = scanResult.getScanRecord(); 52 | if(Build.VERSION.SDK_INT >= 26) { 53 | a.setConnectable(scanResult.isConnectable()); 54 | } else { 55 | if(scanRecord != null) { 56 | int flags = scanRecord.getAdvertiseFlags(); 57 | a.setConnectable((flags & 0x2) > 0); 58 | } 59 | } 60 | if(scanRecord != null) { 61 | String deviceName = scanRecord.getDeviceName(); 62 | if(deviceName != null) { 63 | a.setLocalName(deviceName); 64 | } 65 | int txPower = scanRecord.getTxPowerLevel(); 66 | if(txPower != Integer.MIN_VALUE) { 67 | a.setTxPowerLevel(Protos.Int32Value.newBuilder().setValue(txPower)); 68 | } 69 | // Manufacturer Specific Data 70 | SparseArray msd = scanRecord.getManufacturerSpecificData(); 71 | for (int i = 0; i < msd.size(); i++) { 72 | int key = msd.keyAt(i); 73 | byte[] value = msd.valueAt(i); 74 | a.putManufacturerData(key, ByteString.copyFrom(value)); 75 | } 76 | // Service Data 77 | Map serviceData = scanRecord.getServiceData(); 78 | for (Map.Entry entry : serviceData.entrySet()) { 79 | ParcelUuid key = entry.getKey(); 80 | byte[] value = entry.getValue(); 81 | a.putServiceData(key.getUuid().toString(), ByteString.copyFrom(value)); 82 | } 83 | // Service UUIDs 84 | List serviceUuids = scanRecord.getServiceUuids(); 85 | if(serviceUuids != null) { 86 | for (ParcelUuid s : serviceUuids) { 87 | a.addServiceUuids(s.getUuid().toString()); 88 | } 89 | } 90 | } 91 | p.setRssi(scanResult.getRssi()); 92 | p.setAdvertisementData(a.build()); 93 | return p.build(); 94 | } 95 | 96 | static Protos.BluetoothDevice from(BluetoothDevice device) { 97 | Protos.BluetoothDevice.Builder p = Protos.BluetoothDevice.newBuilder(); 98 | p.setRemoteId(device.getAddress()); 99 | String name = device.getName(); 100 | if(name != null) { 101 | p.setName(name); 102 | } 103 | switch(device.getType()){ 104 | case BluetoothDevice.DEVICE_TYPE_LE: 105 | p.setType(Protos.BluetoothDevice.Type.LE); 106 | break; 107 | case BluetoothDevice.DEVICE_TYPE_CLASSIC: 108 | p.setType(Protos.BluetoothDevice.Type.CLASSIC); 109 | break; 110 | case BluetoothDevice.DEVICE_TYPE_DUAL: 111 | p.setType(Protos.BluetoothDevice.Type.DUAL); 112 | break; 113 | default: 114 | p.setType(Protos.BluetoothDevice.Type.UNKNOWN); 115 | break; 116 | } 117 | return p.build(); 118 | } 119 | 120 | static Protos.BluetoothService from(BluetoothDevice device, BluetoothGattService service, BluetoothGatt gatt) { 121 | Protos.BluetoothService.Builder p = Protos.BluetoothService.newBuilder(); 122 | p.setRemoteId(device.getAddress()); 123 | p.setUuid(service.getUuid().toString()); 124 | p.setIsPrimary(service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY); 125 | for(BluetoothGattCharacteristic c : service.getCharacteristics()) { 126 | p.addCharacteristics(from(device, c, gatt)); 127 | } 128 | for(BluetoothGattService s : service.getIncludedServices()) { 129 | p.addIncludedServices(from(device, s, gatt)); 130 | } 131 | return p.build(); 132 | } 133 | 134 | static Protos.BluetoothCharacteristic from(BluetoothDevice device, BluetoothGattCharacteristic characteristic, BluetoothGatt gatt) { 135 | Protos.BluetoothCharacteristic.Builder p = Protos.BluetoothCharacteristic.newBuilder(); 136 | p.setRemoteId(device.getAddress()); 137 | p.setUuid(characteristic.getUuid().toString()); 138 | p.setProperties(from(characteristic.getProperties())); 139 | if(characteristic.getValue() != null) 140 | p.setValue(ByteString.copyFrom(characteristic.getValue())); 141 | for(BluetoothGattDescriptor d : characteristic.getDescriptors()) { 142 | p.addDescriptors(from(device, d)); 143 | } 144 | if(characteristic.getService().getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY) { 145 | p.setServiceUuid(characteristic.getService().getUuid().toString()); 146 | } else { 147 | // Reverse search to find service 148 | for(BluetoothGattService s : gatt.getServices()) { 149 | for(BluetoothGattService ss : s.getIncludedServices()) { 150 | if(ss.getUuid().equals(characteristic.getService().getUuid())){ 151 | p.setServiceUuid(s.getUuid().toString()); 152 | p.setSecondaryServiceUuid(ss.getUuid().toString()); 153 | break; 154 | } 155 | } 156 | } 157 | } 158 | return p.build(); 159 | } 160 | 161 | static Protos.BluetoothDescriptor from(BluetoothDevice device, BluetoothGattDescriptor descriptor) { 162 | Protos.BluetoothDescriptor.Builder p = Protos.BluetoothDescriptor.newBuilder(); 163 | p.setRemoteId(device.getAddress()); 164 | p.setUuid(descriptor.getUuid().toString()); 165 | p.setCharacteristicUuid(descriptor.getCharacteristic().getUuid().toString()); 166 | p.setServiceUuid(descriptor.getCharacteristic().getService().getUuid().toString()); 167 | if(descriptor.getValue() != null) 168 | p.setValue(ByteString.copyFrom(descriptor.getValue())); 169 | return p.build(); 170 | } 171 | 172 | static Protos.CharacteristicProperties from(int properties) { 173 | return Protos.CharacteristicProperties.newBuilder() 174 | .setBroadcast((properties & 1) != 0) 175 | .setRead((properties & 2) != 0) 176 | .setWriteWithoutResponse((properties & 4) != 0) 177 | .setWrite((properties & 8) != 0) 178 | .setNotify((properties & 16) != 0) 179 | .setIndicate((properties & 32) != 0) 180 | .setAuthenticatedSignedWrites((properties & 64) != 0) 181 | .setExtendedProperties((properties & 128) != 0) 182 | .setNotifyEncryptionRequired((properties & 256) != 0) 183 | .setIndicateEncryptionRequired((properties & 512) != 0) 184 | .build(); 185 | } 186 | 187 | static Protos.DeviceStateResponse from(BluetoothDevice device, int state) { 188 | Protos.DeviceStateResponse.Builder p = Protos.DeviceStateResponse.newBuilder(); 189 | switch(state) { 190 | case BluetoothProfile.STATE_DISCONNECTING: 191 | p.setState(Protos.DeviceStateResponse.BluetoothDeviceState.DISCONNECTING); 192 | break; 193 | case BluetoothProfile.STATE_CONNECTED: 194 | p.setState(Protos.DeviceStateResponse.BluetoothDeviceState.CONNECTED); 195 | break; 196 | case BluetoothProfile.STATE_CONNECTING: 197 | p.setState(Protos.DeviceStateResponse.BluetoothDeviceState.CONNECTING); 198 | break; 199 | case BluetoothProfile.STATE_DISCONNECTED: 200 | p.setState(Protos.DeviceStateResponse.BluetoothDeviceState.DISCONNECTED); 201 | break; 202 | default: 203 | break; 204 | } 205 | p.setRemoteId(device.getAddress()); 206 | return p.build(); 207 | } 208 | } -------------------------------------------------------------------------------- /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: f53b32eb2317ba09137969999d130c24a6314997 8 | channel: master 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_blue_example 2 | 3 | Demonstrates how to use the flutter_blue 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 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 30 29 | 30 | defaultConfig { 31 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 32 | applicationId "com.pauldemarco.flutter_blue_example" 33 | minSdkVersion 19 34 | targetSdkVersion 30 35 | versionCode flutterVersionCode.toInteger() 36 | versionName flutterVersionName 37 | } 38 | 39 | buildTypes { 40 | release { 41 | // TODO: Add your own signing config for the release build. 42 | // Signing with the debug keys for now, so `flutter run --release` works. 43 | signingConfig signingConfigs.debug 44 | // FIXME: Work-around for https://github.com/pauldemarco/flutter_blue/issues/772 45 | shrinkResources false 46 | minifyEnabled false 47 | } 48 | } 49 | } 50 | 51 | flutter { 52 | source '../..' 53 | } 54 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/pauldemarco/flutter_blue_example/EmbeddingV1Activity.java: -------------------------------------------------------------------------------- 1 | package com.pauldemarco.flutter_blue_example; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import com.pauldemarco.flutter_blue.FlutterBluePlugin; 6 | 7 | public class EmbeddingV1Activity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | FlutterBluePlugin.registerWith(registrarFor("com.pauldemarco.flutter_blue.FlutterBluePlugin")); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/pauldemarco/flutter_blue_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.pauldemarco.flutter_blue_example; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /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 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.1.0' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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-6.7-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/flutter_blue_example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 | # Flutter Pod 37 | 38 | copied_flutter_dir = File.join(__dir__, 'Flutter') 39 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') 40 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') 41 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) 42 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. 43 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. 44 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. 45 | 46 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') 47 | unless File.exist?(generated_xcode_build_settings_path) 48 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" 49 | end 50 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) 51 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; 52 | 53 | unless File.exist?(copied_framework_path) 54 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) 55 | end 56 | unless File.exist?(copied_podspec_path) 57 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) 58 | end 59 | end 60 | 61 | # Keep pod path relative so it can be checked into Podfile.lock. 62 | pod 'Flutter', :path => 'Flutter' 63 | 64 | # Plugin Pods 65 | 66 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 67 | # referring to absolute paths on developers' machines. 68 | system('rm -rf .symlinks') 69 | system('mkdir -p .symlinks/plugins') 70 | plugin_pods = parse_KV_file('../.flutter-plugins') 71 | plugin_pods.each do |name, path| 72 | symlink = File.join('.symlinks', 'plugins', name) 73 | File.symlink(path, symlink) 74 | pod name, :path => File.join(symlink, 'ios') 75 | end 76 | end 77 | 78 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 79 | install! 'cocoapods', :disable_input_output_paths => true 80 | 81 | post_install do |installer| 82 | installer.pods_project.targets.each do |target| 83 | target.build_configurations.each do |config| 84 | config.build_settings['ENABLE_BITCODE'] = 'NO' 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - e2e (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - flutter_blue (0.0.1): 6 | - Flutter 7 | - flutter_blue/Protos (= 0.0.1) 8 | - flutter_blue/Protos (0.0.1): 9 | - Flutter 10 | - Protobuf (~> 3.11.4) 11 | - Protobuf (3.11.4) 12 | 13 | DEPENDENCIES: 14 | - e2e (from `.symlinks/plugins/e2e/ios`) 15 | - Flutter (from `Flutter`) 16 | - flutter_blue (from `.symlinks/plugins/flutter_blue/ios`) 17 | 18 | SPEC REPOS: 19 | trunk: 20 | - Protobuf 21 | 22 | EXTERNAL SOURCES: 23 | e2e: 24 | :path: ".symlinks/plugins/e2e/ios" 25 | Flutter: 26 | :path: Flutter 27 | flutter_blue: 28 | :path: ".symlinks/plugins/flutter_blue/ios" 29 | 30 | SPEC CHECKSUMS: 31 | e2e: 967b9b1fc533b7636a3b7a719f840c27f301fe1f 32 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 33 | flutter_blue: eeb381dc4727a0954dede73515f683865494b370 34 | Protobuf: 176220c526ad8bd09ab1fb40a978eac3fef665f7 35 | 36 | PODFILE CHECKSUM: 3dbe063e9c90a5d7c9e4e76e70a821b9e2c1d271 37 | 38 | COCOAPODS: 1.9.1 39 | -------------------------------------------------------------------------------- /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/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/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 | flutter_blue_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 | NSBluetoothAlwaysUsageDescription 43 | We need BT access because it's a BT App. 44 | NSBluetoothPeripheralUsageDescription 45 | We need BT access because it's a BT App. 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:math'; 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_blue/flutter_blue.dart'; 10 | import 'package:flutter_blue_example/widgets.dart'; 11 | 12 | void main() { 13 | runApp(FlutterBlueApp()); 14 | } 15 | 16 | class FlutterBlueApp extends StatelessWidget { 17 | @override 18 | Widget build(BuildContext context) { 19 | return MaterialApp( 20 | color: Colors.lightBlue, 21 | home: StreamBuilder( 22 | stream: FlutterBlue.instance.state, 23 | initialData: BluetoothState.unknown, 24 | builder: (c, snapshot) { 25 | final state = snapshot.data; 26 | if (state == BluetoothState.on) { 27 | return FindDevicesScreen(); 28 | } 29 | return BluetoothOffScreen(state: state); 30 | }), 31 | ); 32 | } 33 | } 34 | 35 | class BluetoothOffScreen extends StatelessWidget { 36 | const BluetoothOffScreen({Key? key, this.state}) : super(key: key); 37 | 38 | final BluetoothState? state; 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Scaffold( 43 | backgroundColor: Colors.lightBlue, 44 | body: Center( 45 | child: Column( 46 | mainAxisSize: MainAxisSize.min, 47 | children: [ 48 | Icon( 49 | Icons.bluetooth_disabled, 50 | size: 200.0, 51 | color: Colors.white54, 52 | ), 53 | Text( 54 | 'Bluetooth Adapter is ${state != null ? state.toString().substring(15) : 'not available'}.', 55 | style: Theme.of(context) 56 | .primaryTextTheme 57 | .subhead 58 | ?.copyWith(color: Colors.white), 59 | ), 60 | ], 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | 67 | class FindDevicesScreen extends StatelessWidget { 68 | @override 69 | Widget build(BuildContext context) { 70 | return Scaffold( 71 | appBar: AppBar( 72 | title: Text('Find Devices'), 73 | ), 74 | body: RefreshIndicator( 75 | onRefresh: () => 76 | FlutterBlue.instance.startScan(timeout: Duration(seconds: 4)), 77 | child: SingleChildScrollView( 78 | child: Column( 79 | children: [ 80 | StreamBuilder>( 81 | stream: Stream.periodic(Duration(seconds: 2)) 82 | .asyncMap((_) => FlutterBlue.instance.connectedDevices), 83 | initialData: [], 84 | builder: (c, snapshot) => Column( 85 | children: snapshot.data! 86 | .map((d) => ListTile( 87 | title: Text(d.name), 88 | subtitle: Text(d.id.toString()), 89 | trailing: StreamBuilder( 90 | stream: d.state, 91 | initialData: BluetoothDeviceState.disconnected, 92 | builder: (c, snapshot) { 93 | if (snapshot.data == 94 | BluetoothDeviceState.connected) { 95 | return RaisedButton( 96 | child: Text('OPEN'), 97 | onPressed: () => Navigator.of(context).push( 98 | MaterialPageRoute( 99 | builder: (context) => 100 | DeviceScreen(device: d))), 101 | ); 102 | } 103 | return Text(snapshot.data.toString()); 104 | }, 105 | ), 106 | )) 107 | .toList(), 108 | ), 109 | ), 110 | StreamBuilder>( 111 | stream: FlutterBlue.instance.scanResults, 112 | initialData: [], 113 | builder: (c, snapshot) => Column( 114 | children: snapshot.data! 115 | .map( 116 | (r) => ScanResultTile( 117 | result: r, 118 | onTap: () => Navigator.of(context) 119 | .push(MaterialPageRoute(builder: (context) { 120 | r.device.connect(); 121 | return DeviceScreen(device: r.device); 122 | })), 123 | ), 124 | ) 125 | .toList(), 126 | ), 127 | ), 128 | ], 129 | ), 130 | ), 131 | ), 132 | floatingActionButton: StreamBuilder( 133 | stream: FlutterBlue.instance.isScanning, 134 | initialData: false, 135 | builder: (c, snapshot) { 136 | if (snapshot.data!) { 137 | return FloatingActionButton( 138 | child: Icon(Icons.stop), 139 | onPressed: () => FlutterBlue.instance.stopScan(), 140 | backgroundColor: Colors.red, 141 | ); 142 | } else { 143 | return FloatingActionButton( 144 | child: Icon(Icons.search), 145 | onPressed: () => FlutterBlue.instance 146 | .startScan(timeout: Duration(seconds: 4))); 147 | } 148 | }, 149 | ), 150 | ); 151 | } 152 | } 153 | 154 | class DeviceScreen extends StatelessWidget { 155 | const DeviceScreen({Key? key, required this.device}) : super(key: key); 156 | 157 | final BluetoothDevice device; 158 | 159 | List _getRandomBytes() { 160 | final math = Random(); 161 | return [ 162 | math.nextInt(255), 163 | math.nextInt(255), 164 | math.nextInt(255), 165 | math.nextInt(255) 166 | ]; 167 | } 168 | 169 | List _buildServiceTiles(List services) { 170 | return services 171 | .map( 172 | (s) => ServiceTile( 173 | service: s, 174 | characteristicTiles: s.characteristics 175 | .map( 176 | (c) => CharacteristicTile( 177 | characteristic: c, 178 | onReadPressed: () => c.read(), 179 | onWritePressed: () async { 180 | await c.write(_getRandomBytes(), withoutResponse: true); 181 | await c.read(); 182 | }, 183 | onNotificationPressed: () async { 184 | await c.setNotifyValue(!c.isNotifying); 185 | await c.read(); 186 | }, 187 | descriptorTiles: c.descriptors 188 | .map( 189 | (d) => DescriptorTile( 190 | descriptor: d, 191 | onReadPressed: () => d.read(), 192 | onWritePressed: () => d.write(_getRandomBytes()), 193 | ), 194 | ) 195 | .toList(), 196 | ), 197 | ) 198 | .toList(), 199 | ), 200 | ) 201 | .toList(); 202 | } 203 | 204 | @override 205 | Widget build(BuildContext context) { 206 | return Scaffold( 207 | appBar: AppBar( 208 | title: Text(device.name), 209 | actions: [ 210 | StreamBuilder( 211 | stream: device.state, 212 | initialData: BluetoothDeviceState.connecting, 213 | builder: (c, snapshot) { 214 | VoidCallback? onPressed; 215 | String text; 216 | switch (snapshot.data) { 217 | case BluetoothDeviceState.connected: 218 | onPressed = () => device.disconnect(); 219 | text = 'DISCONNECT'; 220 | break; 221 | case BluetoothDeviceState.disconnected: 222 | onPressed = () => device.connect(); 223 | text = 'CONNECT'; 224 | break; 225 | default: 226 | onPressed = null; 227 | text = snapshot.data.toString().substring(21).toUpperCase(); 228 | break; 229 | } 230 | return FlatButton( 231 | onPressed: onPressed, 232 | child: Text( 233 | text, 234 | style: Theme.of(context) 235 | .primaryTextTheme 236 | .button 237 | ?.copyWith(color: Colors.white), 238 | )); 239 | }, 240 | ) 241 | ], 242 | ), 243 | body: SingleChildScrollView( 244 | child: Column( 245 | children: [ 246 | StreamBuilder( 247 | stream: device.state, 248 | initialData: BluetoothDeviceState.connecting, 249 | builder: (c, snapshot) => ListTile( 250 | leading: (snapshot.data == BluetoothDeviceState.connected) 251 | ? Icon(Icons.bluetooth_connected) 252 | : Icon(Icons.bluetooth_disabled), 253 | title: Text( 254 | 'Device is ${snapshot.data.toString().split('.')[1]}.'), 255 | subtitle: Text('${device.id}'), 256 | trailing: StreamBuilder( 257 | stream: device.isDiscoveringServices, 258 | initialData: false, 259 | builder: (c, snapshot) => IndexedStack( 260 | index: snapshot.data! ? 1 : 0, 261 | children: [ 262 | IconButton( 263 | icon: Icon(Icons.refresh), 264 | onPressed: () => device.discoverServices(), 265 | ), 266 | IconButton( 267 | icon: SizedBox( 268 | child: CircularProgressIndicator( 269 | valueColor: AlwaysStoppedAnimation(Colors.grey), 270 | ), 271 | width: 18.0, 272 | height: 18.0, 273 | ), 274 | onPressed: null, 275 | ) 276 | ], 277 | ), 278 | ), 279 | ), 280 | ), 281 | StreamBuilder( 282 | stream: device.mtu, 283 | initialData: 0, 284 | builder: (c, snapshot) => ListTile( 285 | title: Text('MTU Size'), 286 | subtitle: Text('${snapshot.data} bytes'), 287 | trailing: IconButton( 288 | icon: Icon(Icons.edit), 289 | onPressed: () => device.requestMtu(223), 290 | ), 291 | ), 292 | ), 293 | StreamBuilder>( 294 | stream: device.services, 295 | initialData: [], 296 | builder: (c, snapshot) { 297 | return Column( 298 | children: _buildServiceTiles(snapshot.data!), 299 | ); 300 | }, 301 | ), 302 | ], 303 | ), 304 | ), 305 | ); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /example/lib/widgets.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_blue/flutter_blue.dart'; 7 | 8 | class ScanResultTile extends StatelessWidget { 9 | const ScanResultTile({Key? key, required this.result, this.onTap}) 10 | : super(key: key); 11 | 12 | final ScanResult result; 13 | final VoidCallback? onTap; 14 | 15 | Widget _buildTitle(BuildContext context) { 16 | if (result.device.name.length > 0) { 17 | return Column( 18 | mainAxisAlignment: MainAxisAlignment.start, 19 | crossAxisAlignment: CrossAxisAlignment.start, 20 | children: [ 21 | Text( 22 | result.device.name, 23 | overflow: TextOverflow.ellipsis, 24 | ), 25 | Text( 26 | result.device.id.toString(), 27 | style: Theme.of(context).textTheme.caption, 28 | ) 29 | ], 30 | ); 31 | } else { 32 | return Text(result.device.id.toString()); 33 | } 34 | } 35 | 36 | Widget _buildAdvRow(BuildContext context, String title, String value) { 37 | return Padding( 38 | padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), 39 | child: Row( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | Text(title, style: Theme.of(context).textTheme.caption), 43 | SizedBox( 44 | width: 12.0, 45 | ), 46 | Expanded( 47 | child: Text( 48 | value, 49 | style: Theme.of(context) 50 | .textTheme 51 | .caption 52 | ?.apply(color: Colors.black), 53 | softWrap: true, 54 | ), 55 | ), 56 | ], 57 | ), 58 | ); 59 | } 60 | 61 | String getNiceHexArray(List bytes) { 62 | return '[${bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join(', ')}]' 63 | .toUpperCase(); 64 | } 65 | 66 | String getNiceManufacturerData(Map> data) { 67 | if (data.isEmpty) { 68 | return 'N/A'; 69 | } 70 | List res = []; 71 | data.forEach((id, bytes) { 72 | res.add( 73 | '${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}'); 74 | }); 75 | return res.join(', '); 76 | } 77 | 78 | String getNiceServiceData(Map> data) { 79 | if (data.isEmpty) { 80 | return 'N/A'; 81 | } 82 | List res = []; 83 | data.forEach((id, bytes) { 84 | res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}'); 85 | }); 86 | return res.join(', '); 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return ExpansionTile( 92 | title: _buildTitle(context), 93 | leading: Text(result.rssi.toString()), 94 | trailing: RaisedButton( 95 | child: Text('CONNECT'), 96 | color: Colors.black, 97 | textColor: Colors.white, 98 | onPressed: (result.advertisementData.connectable) ? onTap : null, 99 | ), 100 | children: [ 101 | _buildAdvRow( 102 | context, 'Complete Local Name', result.advertisementData.localName), 103 | _buildAdvRow(context, 'Tx Power Level', 104 | '${result.advertisementData.txPowerLevel ?? 'N/A'}'), 105 | _buildAdvRow(context, 'Manufacturer Data', 106 | getNiceManufacturerData(result.advertisementData.manufacturerData)), 107 | _buildAdvRow( 108 | context, 109 | 'Service UUIDs', 110 | (result.advertisementData.serviceUuids.isNotEmpty) 111 | ? result.advertisementData.serviceUuids.join(', ').toUpperCase() 112 | : 'N/A'), 113 | _buildAdvRow(context, 'Service Data', 114 | getNiceServiceData(result.advertisementData.serviceData)), 115 | ], 116 | ); 117 | } 118 | } 119 | 120 | class ServiceTile extends StatelessWidget { 121 | final BluetoothService service; 122 | final List characteristicTiles; 123 | 124 | const ServiceTile( 125 | {Key? key, required this.service, required this.characteristicTiles}) 126 | : super(key: key); 127 | 128 | @override 129 | Widget build(BuildContext context) { 130 | if (characteristicTiles.length > 0) { 131 | return ExpansionTile( 132 | title: Column( 133 | mainAxisAlignment: MainAxisAlignment.center, 134 | crossAxisAlignment: CrossAxisAlignment.start, 135 | children: [ 136 | Text('Service'), 137 | Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}', 138 | style: Theme.of(context).textTheme.body1?.copyWith( 139 | color: Theme.of(context).textTheme.caption?.color)) 140 | ], 141 | ), 142 | children: characteristicTiles, 143 | ); 144 | } else { 145 | return ListTile( 146 | title: Text('Service'), 147 | subtitle: 148 | Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}'), 149 | ); 150 | } 151 | } 152 | } 153 | 154 | class CharacteristicTile extends StatelessWidget { 155 | final BluetoothCharacteristic characteristic; 156 | final List descriptorTiles; 157 | final VoidCallback? onReadPressed; 158 | final VoidCallback? onWritePressed; 159 | final VoidCallback? onNotificationPressed; 160 | 161 | const CharacteristicTile( 162 | {Key? key, 163 | required this.characteristic, 164 | required this.descriptorTiles, 165 | this.onReadPressed, 166 | this.onWritePressed, 167 | this.onNotificationPressed}) 168 | : super(key: key); 169 | 170 | @override 171 | Widget build(BuildContext context) { 172 | return StreamBuilder>( 173 | stream: characteristic.value, 174 | initialData: characteristic.lastValue, 175 | builder: (c, snapshot) { 176 | final value = snapshot.data; 177 | return ExpansionTile( 178 | title: ListTile( 179 | title: Column( 180 | mainAxisAlignment: MainAxisAlignment.center, 181 | crossAxisAlignment: CrossAxisAlignment.start, 182 | children: [ 183 | Text('Characteristic'), 184 | Text( 185 | '0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}', 186 | style: Theme.of(context).textTheme.body1?.copyWith( 187 | color: Theme.of(context).textTheme.caption?.color)) 188 | ], 189 | ), 190 | subtitle: Text(value.toString()), 191 | contentPadding: EdgeInsets.all(0.0), 192 | ), 193 | trailing: Row( 194 | mainAxisSize: MainAxisSize.min, 195 | children: [ 196 | IconButton( 197 | icon: Icon( 198 | Icons.file_download, 199 | color: Theme.of(context).iconTheme.color?.withOpacity(0.5), 200 | ), 201 | onPressed: onReadPressed, 202 | ), 203 | IconButton( 204 | icon: Icon(Icons.file_upload, 205 | color: Theme.of(context).iconTheme.color?.withOpacity(0.5)), 206 | onPressed: onWritePressed, 207 | ), 208 | IconButton( 209 | icon: Icon( 210 | characteristic.isNotifying 211 | ? Icons.sync_disabled 212 | : Icons.sync, 213 | color: Theme.of(context).iconTheme.color?.withOpacity(0.5)), 214 | onPressed: onNotificationPressed, 215 | ) 216 | ], 217 | ), 218 | children: descriptorTiles, 219 | ); 220 | }, 221 | ); 222 | } 223 | } 224 | 225 | class DescriptorTile extends StatelessWidget { 226 | final BluetoothDescriptor descriptor; 227 | final VoidCallback? onReadPressed; 228 | final VoidCallback? onWritePressed; 229 | 230 | const DescriptorTile( 231 | {Key? key, 232 | required this.descriptor, 233 | this.onReadPressed, 234 | this.onWritePressed}) 235 | : super(key: key); 236 | 237 | @override 238 | Widget build(BuildContext context) { 239 | return ListTile( 240 | title: Column( 241 | mainAxisAlignment: MainAxisAlignment.center, 242 | crossAxisAlignment: CrossAxisAlignment.start, 243 | children: [ 244 | Text('Descriptor'), 245 | Text('0x${descriptor.uuid.toString().toUpperCase().substring(4, 8)}', 246 | style: Theme.of(context) 247 | .textTheme 248 | .body1 249 | ?.copyWith(color: Theme.of(context).textTheme.caption?.color)) 250 | ], 251 | ), 252 | subtitle: StreamBuilder>( 253 | stream: descriptor.value, 254 | initialData: descriptor.lastValue, 255 | builder: (c, snapshot) => Text(snapshot.data.toString()), 256 | ), 257 | trailing: Row( 258 | mainAxisSize: MainAxisSize.min, 259 | children: [ 260 | IconButton( 261 | icon: Icon( 262 | Icons.file_download, 263 | color: Theme.of(context).iconTheme.color?.withOpacity(0.5), 264 | ), 265 | onPressed: onReadPressed, 266 | ), 267 | IconButton( 268 | icon: Icon( 269 | Icons.file_upload, 270 | color: Theme.of(context).iconTheme.color?.withOpacity(0.5), 271 | ), 272 | onPressed: onWritePressed, 273 | ) 274 | ], 275 | ), 276 | ); 277 | } 278 | } 279 | 280 | class AdapterStateTile extends StatelessWidget { 281 | const AdapterStateTile({Key? key, required this.state}) : super(key: key); 282 | 283 | final BluetoothState state; 284 | 285 | @override 286 | Widget build(BuildContext context) { 287 | return Container( 288 | color: Colors.redAccent, 289 | child: ListTile( 290 | title: Text( 291 | 'Bluetooth adapter is ${state.toString().substring(15)}', 292 | style: Theme.of(context).primaryTextTheme.subhead, 293 | ), 294 | trailing: Icon( 295 | Icons.error, 296 | color: Theme.of(context).primaryTextTheme.subhead?.color, 297 | ), 298 | ), 299 | ); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/xcuserdata/ 7 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import flutter_blue 9 | 10 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 11 | FlutterBluePlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlugin")) 12 | } 13 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.13' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def parse_KV_file(file, separator='=') 13 | file_abs_path = File.expand_path(file) 14 | if !File.exists? file_abs_path 15 | return []; 16 | end 17 | pods_ary = [] 18 | skip_line_start_symbols = ["#", "/"] 19 | File.foreach(file_abs_path) { |line| 20 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 21 | plugin = line.split(pattern=separator) 22 | if plugin.length == 2 23 | podname = plugin[0].strip() 24 | path = plugin[1].strip() 25 | podpath = File.expand_path("#{path}", file_abs_path) 26 | pods_ary.push({:name => podname, :path => podpath}); 27 | else 28 | puts "Invalid plugin specification: #{line}" 29 | end 30 | } 31 | return pods_ary 32 | end 33 | 34 | def pubspec_supports_macos(file) 35 | file_abs_path = File.expand_path(file) 36 | if !File.exists? file_abs_path 37 | return false; 38 | end 39 | File.foreach(file_abs_path) { |line| 40 | return true if line =~ /^\s*macos:/ 41 | } 42 | return false 43 | end 44 | 45 | target 'Runner' do 46 | use_frameworks! 47 | use_modular_headers! 48 | 49 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 50 | # referring to absolute paths on developers' machines. 51 | ephemeral_dir = File.join('Flutter', 'ephemeral') 52 | symlink_dir = File.join(ephemeral_dir, '.symlinks') 53 | symlink_plugins_dir = File.join(symlink_dir, 'plugins') 54 | system("rm -rf #{symlink_dir}") 55 | system("mkdir -p #{symlink_plugins_dir}") 56 | 57 | # Flutter Pods 58 | generated_xcconfig = parse_KV_file(File.join(ephemeral_dir, 'Flutter-Generated.xcconfig')) 59 | if generated_xcconfig.empty? 60 | puts "Flutter-Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 61 | end 62 | generated_xcconfig.map { |p| 63 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 64 | symlink = File.join(symlink_dir, 'flutter') 65 | File.symlink(File.dirname(p[:path]), symlink) 66 | pod 'FlutterMacOS', :path => File.join(symlink, File.basename(p[:path])) 67 | end 68 | } 69 | 70 | # Plugin Pods 71 | plugin_pods = parse_KV_file('../.flutter-plugins') 72 | plugin_pods.map { |p| 73 | symlink = File.join(symlink_plugins_dir, p[:name]) 74 | File.symlink(p[:path], symlink) 75 | if pubspec_supports_macos(File.join(symlink, 'pubspec.yaml')) 76 | pod p[:name], :path => File.join(symlink, 'macos') 77 | end 78 | } 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 | -------------------------------------------------------------------------------- /example/macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - flutter_blue (0.0.1): 3 | - flutter_blue/Protos (= 0.0.1) 4 | - FlutterMacOS 5 | - flutter_blue/Protos (0.0.1): 6 | - FlutterMacOS 7 | - Protobuf (~> 3.11.4) 8 | - FlutterMacOS (1.0.0) 9 | - Protobuf (3.11.4) 10 | 11 | DEPENDENCIES: 12 | - flutter_blue (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue/macos`) 13 | - FlutterMacOS (from `Flutter/ephemeral/.symlinks/flutter/darwin-x64`) 14 | 15 | SPEC REPOS: 16 | trunk: 17 | - Protobuf 18 | 19 | EXTERNAL SOURCES: 20 | flutter_blue: 21 | :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue/macos 22 | FlutterMacOS: 23 | :path: Flutter/ephemeral/.symlinks/flutter/darwin-x64 24 | 25 | SPEC CHECKSUMS: 26 | flutter_blue: 2756506bb10a51e075832da287b48bf5377bdc28 27 | FlutterMacOS: 15bea8a44d2fa024068daa0140371c020b4b6ff9 28 | Protobuf: 176220c526ad8bd09ab1fb40a978eac3fef665f7 29 | 30 | PODFILE CHECKSUM: 91a45292e3c63eb73c6e52036eeb01a52e7ae4df 31 | 32 | COCOAPODS: 1.9.2 33 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/macos/Runner/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.pauldemarco.example 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2020 com.pauldemarco. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.device.bluetooth 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.device.bluetooth 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_blue_example 2 | description: Demonstrates how to use the flutter_blue plugin. 3 | publish_to: "none" 4 | 5 | environment: 6 | sdk: '>=2.12.0 <3.0.0' 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | 12 | dev_dependencies: 13 | flutter_test: 14 | sdk: flutter 15 | flutter_blue: 16 | path: ../ 17 | 18 | flutter: 19 | uses-material-design: true 20 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | void main() { 11 | testWidgets('First test', (WidgetTester tester) async { 12 | // TODO: Add tests 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /flutter_blue.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/FlutterBluePlugin.h: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | #if TARGET_OS_OSX 6 | #import 7 | #else 8 | #import 9 | #endif 10 | #import 11 | 12 | #define NAMESPACE @"plugins.pauldemarco.com/flutter_blue" 13 | 14 | @interface FlutterBluePlugin : NSObject 15 | @end 16 | 17 | @interface FlutterBlueStreamHandler : NSObject 18 | @property FlutterEventSink sink; 19 | @end 20 | -------------------------------------------------------------------------------- /ios/flutter_blue.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint flutter_blue.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'flutter_blue' 7 | s.version = '0.0.1' 8 | s.summary = 'Bluetooth Low Energy plugin for Flutter.' 9 | s.description = <<-DESC 10 | Bluetooth Low Energy plugin for Flutter. 11 | DESC 12 | s.homepage = 'https://github.com/pauldemarco/flutter_blue' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Paul DeMarco' => 'paulmdemarco@gmail.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*', 'gen/**/*' 17 | s.public_header_files = 'Classes/**/*.h', 'gen/**/*.h' 18 | s.dependency 'Flutter' 19 | s.platform = :ios, '8.0' 20 | s.framework = 'CoreBluetooth' 21 | 22 | s.subspec "Protos" do |ss| 23 | ss.source_files = "gen/*.pbobjc.{h,m}", "gen/**/*.pbobjc.{h,m}" 24 | ss.header_mappings_dir = "gen" 25 | ss.requires_arc = false 26 | ss.dependency "Protobuf", '~> 3.11.4' 27 | end 28 | 29 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. 30 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64', 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1', } 31 | 32 | end 33 | -------------------------------------------------------------------------------- /lib/flutter_blue.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | library flutter_blue; 6 | 7 | import 'dart:async'; 8 | 9 | import 'package:collection/collection.dart'; 10 | import 'package:convert/convert.dart'; 11 | import 'package:flutter/services.dart'; 12 | import 'package:meta/meta.dart'; 13 | import 'package:rxdart/rxdart.dart'; 14 | 15 | import 'gen/flutterblue.pb.dart' as protos; 16 | 17 | part 'src/bluetooth_characteristic.dart'; 18 | part 'src/bluetooth_descriptor.dart'; 19 | part 'src/bluetooth_device.dart'; 20 | part 'src/bluetooth_service.dart'; 21 | part 'src/constants.dart'; 22 | part 'src/flutter_blue.dart'; 23 | part 'src/guid.dart'; 24 | -------------------------------------------------------------------------------- /lib/gen/flutterblue.pbenum.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: flutterblue.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields 7 | 8 | // ignore_for_file: UNDEFINED_SHOWN_NAME 9 | import 'dart:core' as $core; 10 | import 'package:protobuf/protobuf.dart' as $pb; 11 | 12 | class BluetoothState_State extends $pb.ProtobufEnum { 13 | static const BluetoothState_State UNKNOWN = BluetoothState_State._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UNKNOWN'); 14 | static const BluetoothState_State UNAVAILABLE = BluetoothState_State._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UNAVAILABLE'); 15 | static const BluetoothState_State UNAUTHORIZED = BluetoothState_State._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UNAUTHORIZED'); 16 | static const BluetoothState_State TURNING_ON = BluetoothState_State._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'TURNING_ON'); 17 | static const BluetoothState_State ON = BluetoothState_State._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ON'); 18 | static const BluetoothState_State TURNING_OFF = BluetoothState_State._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'TURNING_OFF'); 19 | static const BluetoothState_State OFF = BluetoothState_State._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'OFF'); 20 | 21 | static const $core.List values = [ 22 | UNKNOWN, 23 | UNAVAILABLE, 24 | UNAUTHORIZED, 25 | TURNING_ON, 26 | ON, 27 | TURNING_OFF, 28 | OFF, 29 | ]; 30 | 31 | static final $core.Map<$core.int, BluetoothState_State> _byValue = $pb.ProtobufEnum.initByValue(values); 32 | static BluetoothState_State? valueOf($core.int value) => _byValue[value]; 33 | 34 | const BluetoothState_State._($core.int v, $core.String n) : super(v, n); 35 | } 36 | 37 | class BluetoothDevice_Type extends $pb.ProtobufEnum { 38 | static const BluetoothDevice_Type UNKNOWN = BluetoothDevice_Type._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UNKNOWN'); 39 | static const BluetoothDevice_Type CLASSIC = BluetoothDevice_Type._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CLASSIC'); 40 | static const BluetoothDevice_Type LE = BluetoothDevice_Type._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'LE'); 41 | static const BluetoothDevice_Type DUAL = BluetoothDevice_Type._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DUAL'); 42 | 43 | static const $core.List values = [ 44 | UNKNOWN, 45 | CLASSIC, 46 | LE, 47 | DUAL, 48 | ]; 49 | 50 | static final $core.Map<$core.int, BluetoothDevice_Type> _byValue = $pb.ProtobufEnum.initByValue(values); 51 | static BluetoothDevice_Type? valueOf($core.int value) => _byValue[value]; 52 | 53 | const BluetoothDevice_Type._($core.int v, $core.String n) : super(v, n); 54 | } 55 | 56 | class WriteCharacteristicRequest_WriteType extends $pb.ProtobufEnum { 57 | static const WriteCharacteristicRequest_WriteType WITH_RESPONSE = WriteCharacteristicRequest_WriteType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WITH_RESPONSE'); 58 | static const WriteCharacteristicRequest_WriteType WITHOUT_RESPONSE = WriteCharacteristicRequest_WriteType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WITHOUT_RESPONSE'); 59 | 60 | static const $core.List values = [ 61 | WITH_RESPONSE, 62 | WITHOUT_RESPONSE, 63 | ]; 64 | 65 | static final $core.Map<$core.int, WriteCharacteristicRequest_WriteType> _byValue = $pb.ProtobufEnum.initByValue(values); 66 | static WriteCharacteristicRequest_WriteType? valueOf($core.int value) => _byValue[value]; 67 | 68 | const WriteCharacteristicRequest_WriteType._($core.int v, $core.String n) : super(v, n); 69 | } 70 | 71 | class DeviceStateResponse_BluetoothDeviceState extends $pb.ProtobufEnum { 72 | static const DeviceStateResponse_BluetoothDeviceState DISCONNECTED = DeviceStateResponse_BluetoothDeviceState._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DISCONNECTED'); 73 | static const DeviceStateResponse_BluetoothDeviceState CONNECTING = DeviceStateResponse_BluetoothDeviceState._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CONNECTING'); 74 | static const DeviceStateResponse_BluetoothDeviceState CONNECTED = DeviceStateResponse_BluetoothDeviceState._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CONNECTED'); 75 | static const DeviceStateResponse_BluetoothDeviceState DISCONNECTING = DeviceStateResponse_BluetoothDeviceState._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DISCONNECTING'); 76 | 77 | static const $core.List values = [ 78 | DISCONNECTED, 79 | CONNECTING, 80 | CONNECTED, 81 | DISCONNECTING, 82 | ]; 83 | 84 | static final $core.Map<$core.int, DeviceStateResponse_BluetoothDeviceState> _byValue = $pb.ProtobufEnum.initByValue(values); 85 | static DeviceStateResponse_BluetoothDeviceState? valueOf($core.int value) => _byValue[value]; 86 | 87 | const DeviceStateResponse_BluetoothDeviceState._($core.int v, $core.String n) : super(v, n); 88 | } 89 | 90 | -------------------------------------------------------------------------------- /lib/gen/flutterblue.pbserver.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: flutterblue.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package 7 | 8 | export 'flutterblue.pb.dart'; 9 | 10 | -------------------------------------------------------------------------------- /lib/src/bluetooth_characteristic.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part of flutter_blue; 6 | 7 | class BluetoothCharacteristic { 8 | final Guid uuid; 9 | final DeviceIdentifier deviceId; 10 | final Guid serviceUuid; 11 | final Guid? secondaryServiceUuid; 12 | final CharacteristicProperties properties; 13 | final List descriptors; 14 | bool get isNotifying { 15 | try { 16 | var cccd = 17 | descriptors.singleWhere((d) => d.uuid == BluetoothDescriptor.cccd); 18 | return ((cccd.lastValue[0] & 0x01) > 0 || (cccd.lastValue[0] & 0x02) > 0); 19 | } catch (e) { 20 | return false; 21 | } 22 | } 23 | 24 | BehaviorSubject> _value; 25 | Stream> get value => Rx.merge([ 26 | _value.stream, 27 | _onValueChangedStream, 28 | ]); 29 | 30 | List get lastValue => _value.value ?? []; 31 | 32 | BluetoothCharacteristic.fromProto(protos.BluetoothCharacteristic p) 33 | : uuid = new Guid(p.uuid), 34 | deviceId = new DeviceIdentifier(p.remoteId), 35 | serviceUuid = new Guid(p.serviceUuid), 36 | secondaryServiceUuid = (p.secondaryServiceUuid.length > 0) 37 | ? new Guid(p.secondaryServiceUuid) 38 | : null, 39 | descriptors = p.descriptors 40 | .map((d) => new BluetoothDescriptor.fromProto(d)) 41 | .toList(), 42 | properties = new CharacteristicProperties.fromProto(p.properties), 43 | _value = BehaviorSubject.seeded(p.value); 44 | 45 | Stream get _onCharacteristicChangedStream => 46 | FlutterBlue.instance._methodStream 47 | .where((m) => m.method == "OnCharacteristicChanged") 48 | .map((m) => m.arguments) 49 | .map( 50 | (buffer) => new protos.OnCharacteristicChanged.fromBuffer(buffer)) 51 | .where((p) => p.remoteId == deviceId.toString()) 52 | .map((p) => new BluetoothCharacteristic.fromProto(p.characteristic)) 53 | .where((c) => c.uuid == uuid) 54 | .map((c) { 55 | // Update the characteristic with the new values 56 | _updateDescriptors(c.descriptors); 57 | return c; 58 | }); 59 | 60 | Stream> get _onValueChangedStream => 61 | _onCharacteristicChangedStream.map((c) => c.lastValue); 62 | 63 | void _updateDescriptors(List newDescriptors) { 64 | for (var d in descriptors) { 65 | for (var newD in newDescriptors) { 66 | if (d.uuid == newD.uuid) { 67 | d._value.add(newD.lastValue); 68 | } 69 | } 70 | } 71 | } 72 | 73 | /// Retrieves the value of the characteristic 74 | Future> read() async { 75 | var request = protos.ReadCharacteristicRequest.create() 76 | ..remoteId = deviceId.toString() 77 | ..characteristicUuid = uuid.toString() 78 | ..serviceUuid = serviceUuid.toString(); 79 | FlutterBlue.instance._log(LogLevel.info, 80 | 'remoteId: ${deviceId.toString()} characteristicUuid: ${uuid.toString()} serviceUuid: ${serviceUuid.toString()}'); 81 | 82 | await FlutterBlue.instance._channel 83 | .invokeMethod('readCharacteristic', request.writeToBuffer()); 84 | 85 | return FlutterBlue.instance._methodStream 86 | .where((m) => m.method == "ReadCharacteristicResponse") 87 | .map((m) => m.arguments) 88 | .map((buffer) => 89 | new protos.ReadCharacteristicResponse.fromBuffer(buffer)) 90 | .where((p) => 91 | (p.remoteId == request.remoteId) && 92 | (p.characteristic.uuid == request.characteristicUuid) && 93 | (p.characteristic.serviceUuid == request.serviceUuid)) 94 | .map((p) => p.characteristic.value) 95 | .first 96 | .then((d) { 97 | _value.add(d); 98 | return d; 99 | }); 100 | } 101 | 102 | /// Writes the value of a characteristic. 103 | /// [CharacteristicWriteType.withoutResponse]: the write is not 104 | /// guaranteed and will return immediately with success. 105 | /// [CharacteristicWriteType.withResponse]: the method will return after the 106 | /// write operation has either passed or failed. 107 | Future write(List value, {bool withoutResponse = false}) async { 108 | final type = withoutResponse 109 | ? CharacteristicWriteType.withoutResponse 110 | : CharacteristicWriteType.withResponse; 111 | 112 | var request = protos.WriteCharacteristicRequest.create() 113 | ..remoteId = deviceId.toString() 114 | ..characteristicUuid = uuid.toString() 115 | ..serviceUuid = serviceUuid.toString() 116 | ..writeType = 117 | protos.WriteCharacteristicRequest_WriteType.valueOf(type.index)! 118 | ..value = value; 119 | 120 | var result = await FlutterBlue.instance._channel 121 | .invokeMethod('writeCharacteristic', request.writeToBuffer()); 122 | 123 | if (type == CharacteristicWriteType.withoutResponse) { 124 | return result; 125 | } 126 | 127 | return FlutterBlue.instance._methodStream 128 | .where((m) => m.method == "WriteCharacteristicResponse") 129 | .map((m) => m.arguments) 130 | .map((buffer) => 131 | new protos.WriteCharacteristicResponse.fromBuffer(buffer)) 132 | .where((p) => 133 | (p.request.remoteId == request.remoteId) && 134 | (p.request.characteristicUuid == request.characteristicUuid) && 135 | (p.request.serviceUuid == request.serviceUuid)) 136 | .first 137 | .then((w) => w.success) 138 | .then((success) => (!success) 139 | ? throw new Exception('Failed to write the characteristic') 140 | : null) 141 | .then((_) => null); 142 | } 143 | 144 | /// Sets notifications or indications for the value of a specified characteristic 145 | Future setNotifyValue(bool notify) async { 146 | var request = protos.SetNotificationRequest.create() 147 | ..remoteId = deviceId.toString() 148 | ..serviceUuid = serviceUuid.toString() 149 | ..characteristicUuid = uuid.toString() 150 | ..enable = notify; 151 | 152 | await FlutterBlue.instance._channel 153 | .invokeMethod('setNotification', request.writeToBuffer()); 154 | 155 | return FlutterBlue.instance._methodStream 156 | .where((m) => m.method == "SetNotificationResponse") 157 | .map((m) => m.arguments) 158 | .map((buffer) => new protos.SetNotificationResponse.fromBuffer(buffer)) 159 | .where((p) => 160 | (p.remoteId == request.remoteId) && 161 | (p.characteristic.uuid == request.characteristicUuid) && 162 | (p.characteristic.serviceUuid == request.serviceUuid)) 163 | .first 164 | .then((p) => new BluetoothCharacteristic.fromProto(p.characteristic)) 165 | .then((c) { 166 | _updateDescriptors(c.descriptors); 167 | return (c.isNotifying == notify); 168 | }); 169 | } 170 | 171 | @override 172 | String toString() { 173 | return 'BluetoothCharacteristic{uuid: $uuid, deviceId: $deviceId, serviceUuid: $serviceUuid, secondaryServiceUuid: $secondaryServiceUuid, properties: $properties, descriptors: $descriptors, value: ${_value.value}'; 174 | } 175 | } 176 | 177 | enum CharacteristicWriteType { withResponse, withoutResponse } 178 | 179 | @immutable 180 | class CharacteristicProperties { 181 | final bool broadcast; 182 | final bool read; 183 | final bool writeWithoutResponse; 184 | final bool write; 185 | final bool notify; 186 | final bool indicate; 187 | final bool authenticatedSignedWrites; 188 | final bool extendedProperties; 189 | final bool notifyEncryptionRequired; 190 | final bool indicateEncryptionRequired; 191 | 192 | CharacteristicProperties( 193 | {this.broadcast = false, 194 | this.read = false, 195 | this.writeWithoutResponse = false, 196 | this.write = false, 197 | this.notify = false, 198 | this.indicate = false, 199 | this.authenticatedSignedWrites = false, 200 | this.extendedProperties = false, 201 | this.notifyEncryptionRequired = false, 202 | this.indicateEncryptionRequired = false}); 203 | 204 | CharacteristicProperties.fromProto(protos.CharacteristicProperties p) 205 | : broadcast = p.broadcast, 206 | read = p.read, 207 | writeWithoutResponse = p.writeWithoutResponse, 208 | write = p.write, 209 | notify = p.notify, 210 | indicate = p.indicate, 211 | authenticatedSignedWrites = p.authenticatedSignedWrites, 212 | extendedProperties = p.extendedProperties, 213 | notifyEncryptionRequired = p.notifyEncryptionRequired, 214 | indicateEncryptionRequired = p.indicateEncryptionRequired; 215 | 216 | @override 217 | String toString() { 218 | return 'CharacteristicProperties{broadcast: $broadcast, read: $read, writeWithoutResponse: $writeWithoutResponse, write: $write, notify: $notify, indicate: $indicate, authenticatedSignedWrites: $authenticatedSignedWrites, extendedProperties: $extendedProperties, notifyEncryptionRequired: $notifyEncryptionRequired, indicateEncryptionRequired: $indicateEncryptionRequired}'; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /lib/src/bluetooth_descriptor.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part of flutter_blue; 6 | 7 | class BluetoothDescriptor { 8 | static final Guid cccd = new Guid("00002902-0000-1000-8000-00805f9b34fb"); 9 | 10 | final Guid uuid; 11 | final DeviceIdentifier deviceId; 12 | final Guid serviceUuid; 13 | final Guid characteristicUuid; 14 | 15 | BehaviorSubject> _value; 16 | Stream> get value => _value.stream; 17 | 18 | List get lastValue => _value.value ?? []; 19 | 20 | BluetoothDescriptor.fromProto(protos.BluetoothDescriptor p) 21 | : uuid = new Guid(p.uuid), 22 | deviceId = new DeviceIdentifier(p.remoteId), 23 | serviceUuid = new Guid(p.serviceUuid), 24 | characteristicUuid = new Guid(p.characteristicUuid), 25 | _value = BehaviorSubject.seeded(p.value); 26 | 27 | /// Retrieves the value of a specified descriptor 28 | Future> read() async { 29 | var request = protos.ReadDescriptorRequest.create() 30 | ..remoteId = deviceId.toString() 31 | ..descriptorUuid = uuid.toString() 32 | ..characteristicUuid = characteristicUuid.toString() 33 | ..serviceUuid = serviceUuid.toString(); 34 | 35 | await FlutterBlue.instance._channel 36 | .invokeMethod('readDescriptor', request.writeToBuffer()); 37 | 38 | return FlutterBlue.instance._methodStream 39 | .where((m) => m.method == "ReadDescriptorResponse") 40 | .map((m) => m.arguments) 41 | .map((buffer) => new protos.ReadDescriptorResponse.fromBuffer(buffer)) 42 | .where((p) => 43 | (p.request.remoteId == request.remoteId) && 44 | (p.request.descriptorUuid == request.descriptorUuid) && 45 | (p.request.characteristicUuid == request.characteristicUuid) && 46 | (p.request.serviceUuid == request.serviceUuid)) 47 | .map((d) => d.value) 48 | .first 49 | .then((d) { 50 | _value.add(d); 51 | return d; 52 | }); 53 | } 54 | 55 | /// Writes the value of a descriptor 56 | Future write(List value) async { 57 | var request = protos.WriteDescriptorRequest.create() 58 | ..remoteId = deviceId.toString() 59 | ..descriptorUuid = uuid.toString() 60 | ..characteristicUuid = characteristicUuid.toString() 61 | ..serviceUuid = serviceUuid.toString() 62 | ..value = value; 63 | 64 | await FlutterBlue.instance._channel 65 | .invokeMethod('writeDescriptor', request.writeToBuffer()); 66 | 67 | return FlutterBlue.instance._methodStream 68 | .where((m) => m.method == "WriteDescriptorResponse") 69 | .map((m) => m.arguments) 70 | .map((buffer) => new protos.WriteDescriptorResponse.fromBuffer(buffer)) 71 | .where((p) => 72 | (p.request.remoteId == request.remoteId) && 73 | (p.request.descriptorUuid == request.descriptorUuid) && 74 | (p.request.characteristicUuid == request.characteristicUuid) && 75 | (p.request.serviceUuid == request.serviceUuid)) 76 | .first 77 | .then((w) => w.success) 78 | .then((success) => (!success) 79 | ? throw new Exception('Failed to write the descriptor') 80 | : null) 81 | .then((_) => _value.add(value)) 82 | .then((_) => null); 83 | } 84 | 85 | @override 86 | String toString() { 87 | return 'BluetoothDescriptor{uuid: $uuid, deviceId: $deviceId, serviceUuid: $serviceUuid, characteristicUuid: $characteristicUuid, value: ${_value.value}}'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/bluetooth_device.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part of flutter_blue; 6 | 7 | class BluetoothDevice { 8 | final DeviceIdentifier id; 9 | final String name; 10 | final BluetoothDeviceType type; 11 | 12 | BluetoothDevice.fromProto(protos.BluetoothDevice p) 13 | : id = new DeviceIdentifier(p.remoteId), 14 | name = p.name, 15 | type = BluetoothDeviceType.values[p.type.value]; 16 | 17 | BehaviorSubject _isDiscoveringServices = BehaviorSubject.seeded(false); 18 | Stream get isDiscoveringServices => _isDiscoveringServices.stream; 19 | 20 | /// Establishes a connection to the Bluetooth Device. 21 | Future connect({ 22 | Duration? timeout, 23 | bool autoConnect = true, 24 | }) async { 25 | var request = protos.ConnectRequest.create() 26 | ..remoteId = id.toString() 27 | ..androidAutoConnect = autoConnect; 28 | 29 | Timer? timer; 30 | if (timeout != null) { 31 | timer = Timer(timeout, () { 32 | disconnect(); 33 | throw TimeoutException('Failed to connect in time.', timeout); 34 | }); 35 | } 36 | 37 | await FlutterBlue.instance._channel 38 | .invokeMethod('connect', request.writeToBuffer()); 39 | 40 | await state.firstWhere((s) => s == BluetoothDeviceState.connected); 41 | 42 | timer?.cancel(); 43 | 44 | return; 45 | } 46 | 47 | /// Cancels connection to the Bluetooth Device 48 | Future disconnect() => 49 | FlutterBlue.instance._channel.invokeMethod('disconnect', id.toString()); 50 | 51 | BehaviorSubject> _services = 52 | BehaviorSubject.seeded([]); 53 | 54 | /// Discovers services offered by the remote device as well as their characteristics and descriptors 55 | Future> discoverServices() async { 56 | final s = await state.first; 57 | if (s != BluetoothDeviceState.connected) { 58 | return Future.error(new Exception( 59 | 'Cannot discoverServices while device is not connected. State == $s')); 60 | } 61 | var response = FlutterBlue.instance._methodStream 62 | .where((m) => m.method == "DiscoverServicesResult") 63 | .map((m) => m.arguments) 64 | .map((buffer) => new protos.DiscoverServicesResult.fromBuffer(buffer)) 65 | .where((p) => p.remoteId == id.toString()) 66 | .map((p) => p.services) 67 | .map((s) => s.map((p) => new BluetoothService.fromProto(p)).toList()) 68 | .first 69 | .then((list) { 70 | _services.add(list); 71 | _isDiscoveringServices.add(false); 72 | return list; 73 | }); 74 | 75 | await FlutterBlue.instance._channel 76 | .invokeMethod('discoverServices', id.toString()); 77 | 78 | _isDiscoveringServices.add(true); 79 | 80 | return response; 81 | } 82 | 83 | /// Returns a list of Bluetooth GATT services offered by the remote device 84 | /// This function requires that discoverServices has been completed for this device 85 | Stream> get services async* { 86 | yield await FlutterBlue.instance._channel 87 | .invokeMethod('services', id.toString()) 88 | .then((buffer) => 89 | new protos.DiscoverServicesResult.fromBuffer(buffer).services) 90 | .then((i) => i.map((s) => new BluetoothService.fromProto(s)).toList()); 91 | yield* _services.stream; 92 | } 93 | 94 | /// The current connection state of the device 95 | Stream get state async* { 96 | yield await FlutterBlue.instance._channel 97 | .invokeMethod('deviceState', id.toString()) 98 | .then((buffer) => new protos.DeviceStateResponse.fromBuffer(buffer)) 99 | .then((p) => BluetoothDeviceState.values[p.state.value]); 100 | 101 | yield* FlutterBlue.instance._methodStream 102 | .where((m) => m.method == "DeviceState") 103 | .map((m) => m.arguments) 104 | .map((buffer) => new protos.DeviceStateResponse.fromBuffer(buffer)) 105 | .where((p) => p.remoteId == id.toString()) 106 | .map((p) => BluetoothDeviceState.values[p.state.value]); 107 | } 108 | 109 | /// The MTU size in bytes 110 | Stream get mtu async* { 111 | yield await FlutterBlue.instance._channel 112 | .invokeMethod('mtu', id.toString()) 113 | .then((buffer) => new protos.MtuSizeResponse.fromBuffer(buffer)) 114 | .then((p) => p.mtu); 115 | 116 | yield* FlutterBlue.instance._methodStream 117 | .where((m) => m.method == "MtuSize") 118 | .map((m) => m.arguments) 119 | .map((buffer) => new protos.MtuSizeResponse.fromBuffer(buffer)) 120 | .where((p) => p.remoteId == id.toString()) 121 | .map((p) => p.mtu); 122 | } 123 | 124 | /// Request to change the MTU Size 125 | /// Throws error if request did not complete successfully 126 | Future requestMtu(int desiredMtu) async { 127 | var request = protos.MtuSizeRequest.create() 128 | ..remoteId = id.toString() 129 | ..mtu = desiredMtu; 130 | 131 | return FlutterBlue.instance._channel 132 | .invokeMethod('requestMtu', request.writeToBuffer()); 133 | } 134 | 135 | /// Indicates whether the Bluetooth Device can send a write without response 136 | Future get canSendWriteWithoutResponse => 137 | new Future.error(new UnimplementedError()); 138 | 139 | @override 140 | bool operator ==(Object other) => 141 | identical(this, other) || 142 | other is BluetoothDevice && 143 | runtimeType == other.runtimeType && 144 | id == other.id; 145 | 146 | @override 147 | int get hashCode => id.hashCode; 148 | 149 | @override 150 | String toString() { 151 | return 'BluetoothDevice{id: $id, name: $name, type: $type, isDiscoveringServices: ${_isDiscoveringServices.value}, _services: ${_services.value}'; 152 | } 153 | } 154 | 155 | enum BluetoothDeviceType { unknown, classic, le, dual } 156 | 157 | enum BluetoothDeviceState { disconnected, connecting, connected, disconnecting } 158 | -------------------------------------------------------------------------------- /lib/src/bluetooth_service.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part of flutter_blue; 6 | 7 | class BluetoothService { 8 | final Guid uuid; 9 | final DeviceIdentifier deviceId; 10 | final bool isPrimary; 11 | final List characteristics; 12 | final List includedServices; 13 | 14 | BluetoothService.fromProto(protos.BluetoothService p) 15 | : uuid = new Guid(p.uuid), 16 | deviceId = new DeviceIdentifier(p.remoteId), 17 | isPrimary = p.isPrimary, 18 | characteristics = p.characteristics 19 | .map((c) => new BluetoothCharacteristic.fromProto(c)) 20 | .toList(), 21 | includedServices = p.includedServices 22 | .map((s) => new BluetoothService.fromProto(s)) 23 | .toList(); 24 | 25 | @override 26 | String toString() { 27 | return 'BluetoothService{uuid: $uuid, deviceId: $deviceId, isPrimary: $isPrimary, characteristics: $characteristics, includedServices: $includedServices}'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/constants.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part of flutter_blue; 6 | 7 | const NAMESPACE = 'plugins.pauldemarco.com/flutter_blue'; 8 | -------------------------------------------------------------------------------- /lib/src/flutter_blue.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part of flutter_blue; 6 | 7 | class FlutterBlue { 8 | final MethodChannel _channel = const MethodChannel('$NAMESPACE/methods'); 9 | final EventChannel _stateChannel = const EventChannel('$NAMESPACE/state'); 10 | final StreamController _methodStreamController = 11 | new StreamController.broadcast(); // ignore: close_sinks 12 | Stream get _methodStream => _methodStreamController 13 | .stream; // Used internally to dispatch methods from platform. 14 | 15 | /// Singleton boilerplate 16 | FlutterBlue._() { 17 | _channel.setMethodCallHandler((MethodCall call) async { 18 | _methodStreamController.add(call); 19 | }); 20 | 21 | _setLogLevelIfAvailable(); 22 | } 23 | 24 | static FlutterBlue _instance = new FlutterBlue._(); 25 | static FlutterBlue get instance => _instance; 26 | 27 | /// Log level of the instance, default is all messages (debug). 28 | LogLevel _logLevel = LogLevel.debug; 29 | LogLevel get logLevel => _logLevel; 30 | 31 | /// Checks whether the device supports Bluetooth 32 | Future get isAvailable => 33 | _channel.invokeMethod('isAvailable').then((d) => d); 34 | 35 | /// Checks if Bluetooth functionality is turned on 36 | Future get isOn => _channel.invokeMethod('isOn').then((d) => d); 37 | 38 | BehaviorSubject _isScanning = BehaviorSubject.seeded(false); 39 | Stream get isScanning => _isScanning.stream; 40 | 41 | BehaviorSubject> _scanResults = BehaviorSubject.seeded([]); 42 | 43 | /// Returns a stream that is a list of [ScanResult] results while a scan is in progress. 44 | /// 45 | /// The list emitted is all the scanned results as of the last initiated scan. When a scan is 46 | /// first started, an empty list is emitted. The returned stream is never closed. 47 | /// 48 | /// One use for [scanResults] is as the stream in a StreamBuilder to display the 49 | /// results of a scan in real time while the scan is in progress. 50 | Stream> get scanResults => _scanResults.stream; 51 | 52 | PublishSubject _stopScanPill = new PublishSubject(); 53 | 54 | /// Gets the current state of the Bluetooth module 55 | Stream get state async* { 56 | yield await _channel 57 | .invokeMethod('state') 58 | .then((buffer) => new protos.BluetoothState.fromBuffer(buffer)) 59 | .then((s) => BluetoothState.values[s.state.value]); 60 | 61 | yield* _stateChannel 62 | .receiveBroadcastStream() 63 | .map((buffer) => new protos.BluetoothState.fromBuffer(buffer)) 64 | .map((s) => BluetoothState.values[s.state.value]); 65 | } 66 | 67 | /// Retrieve a list of connected devices 68 | Future> get connectedDevices { 69 | return _channel 70 | .invokeMethod('getConnectedDevices') 71 | .then((buffer) => protos.ConnectedDevicesResponse.fromBuffer(buffer)) 72 | .then((p) => p.devices) 73 | .then((p) => p.map((d) => BluetoothDevice.fromProto(d)).toList()); 74 | } 75 | 76 | _setLogLevelIfAvailable() async { 77 | if (await isAvailable) { 78 | // Send the log level to the underlying platforms. 79 | setLogLevel(logLevel); 80 | } 81 | } 82 | 83 | /// Starts a scan for Bluetooth Low Energy devices and returns a stream 84 | /// of the [ScanResult] results as they are received. 85 | /// 86 | /// timeout calls stopStream after a specified [Duration]. 87 | /// You can also get a list of ongoing results in the [scanResults] stream. 88 | /// If scanning is already in progress, this will throw an [Exception]. 89 | Stream scan({ 90 | ScanMode scanMode = ScanMode.lowLatency, 91 | List withServices = const [], 92 | List withDevices = const [], 93 | Duration? timeout, 94 | bool allowDuplicates = false, 95 | }) async* { 96 | var settings = protos.ScanSettings.create() 97 | ..androidScanMode = scanMode.value 98 | ..allowDuplicates = allowDuplicates 99 | ..serviceUuids.addAll(withServices.map((g) => g.toString()).toList()); 100 | 101 | if (_isScanning.value == true) { 102 | throw Exception('Another scan is already in progress.'); 103 | } 104 | 105 | // Emit to isScanning 106 | _isScanning.add(true); 107 | 108 | final killStreams = []; 109 | killStreams.add(_stopScanPill); 110 | if (timeout != null) { 111 | killStreams.add(Rx.timer(null, timeout)); 112 | } 113 | 114 | // Clear scan results list 115 | _scanResults.add([]); 116 | 117 | try { 118 | await _channel.invokeMethod('startScan', settings.writeToBuffer()); 119 | } catch (e) { 120 | print('Error starting scan.'); 121 | _stopScanPill.add(null); 122 | _isScanning.add(false); 123 | throw e; 124 | } 125 | 126 | yield* FlutterBlue.instance._methodStream 127 | .where((m) => m.method == "ScanResult") 128 | .map((m) => m.arguments) 129 | .takeUntil(Rx.merge(killStreams)) 130 | .doOnDone(stopScan) 131 | .map((buffer) => new protos.ScanResult.fromBuffer(buffer)) 132 | .map((p) { 133 | final result = new ScanResult.fromProto(p); 134 | final list = _scanResults.value ?? []; 135 | int index = list.indexOf(result); 136 | if (index != -1) { 137 | list[index] = result; 138 | } else { 139 | list.add(result); 140 | } 141 | _scanResults.add(list); 142 | return result; 143 | }); 144 | } 145 | 146 | /// Starts a scan and returns a future that will complete once the scan has finished. 147 | /// 148 | /// Once a scan is started, call [stopScan] to stop the scan and complete the returned future. 149 | /// 150 | /// timeout automatically stops the scan after a specified [Duration]. 151 | /// 152 | /// To observe the results while the scan is in progress, listen to the [scanResults] stream, 153 | /// or call [scan] instead. 154 | Future startScan({ 155 | ScanMode scanMode = ScanMode.lowLatency, 156 | List withServices = const [], 157 | List withDevices = const [], 158 | Duration? timeout, 159 | bool allowDuplicates = false, 160 | }) async { 161 | await scan( 162 | scanMode: scanMode, 163 | withServices: withServices, 164 | withDevices: withDevices, 165 | timeout: timeout, 166 | allowDuplicates: allowDuplicates) 167 | .drain(); 168 | return _scanResults.value; 169 | } 170 | 171 | /// Stops a scan for Bluetooth Low Energy devices 172 | Future stopScan() async { 173 | await _channel.invokeMethod('stopScan'); 174 | _stopScanPill.add(null); 175 | _isScanning.add(false); 176 | } 177 | 178 | /// The list of connected peripherals can include those that are connected 179 | /// by other apps and that will need to be connected locally using the 180 | /// device.connect() method before they can be used. 181 | // Stream> connectedDevices({ 182 | // List withServices = const [], 183 | // }) => 184 | // throw UnimplementedError(); 185 | 186 | /// Sets the log level of the FlutterBlue instance 187 | /// Messages equal or below the log level specified are stored/forwarded, 188 | /// messages above are dropped. 189 | void setLogLevel(LogLevel level) async { 190 | await _channel.invokeMethod('setLogLevel', level.index); 191 | _logLevel = level; 192 | } 193 | 194 | void _log(LogLevel level, String message) { 195 | if (level.index <= _logLevel.index) { 196 | print(message); 197 | } 198 | } 199 | } 200 | 201 | /// Log levels for FlutterBlue 202 | enum LogLevel { 203 | emergency, 204 | alert, 205 | critical, 206 | error, 207 | warning, 208 | notice, 209 | info, 210 | debug, 211 | } 212 | 213 | /// State of the bluetooth adapter. 214 | enum BluetoothState { 215 | unknown, 216 | unavailable, 217 | unauthorized, 218 | turningOn, 219 | on, 220 | turningOff, 221 | off 222 | } 223 | 224 | class ScanMode { 225 | const ScanMode(this.value); 226 | static const lowPower = const ScanMode(0); 227 | static const balanced = const ScanMode(1); 228 | static const lowLatency = const ScanMode(2); 229 | static const opportunistic = const ScanMode(-1); 230 | final int value; 231 | } 232 | 233 | class DeviceIdentifier { 234 | final String id; 235 | const DeviceIdentifier(this.id); 236 | 237 | @override 238 | String toString() => id; 239 | 240 | @override 241 | int get hashCode => id.hashCode; 242 | 243 | @override 244 | bool operator ==(other) => 245 | other is DeviceIdentifier && compareAsciiLowerCase(id, other.id) == 0; 246 | } 247 | 248 | class ScanResult { 249 | ScanResult.fromProto(protos.ScanResult p) 250 | : device = new BluetoothDevice.fromProto(p.device), 251 | advertisementData = 252 | new AdvertisementData.fromProto(p.advertisementData), 253 | rssi = p.rssi; 254 | 255 | final BluetoothDevice device; 256 | final AdvertisementData advertisementData; 257 | final int rssi; 258 | 259 | @override 260 | bool operator ==(Object other) => 261 | identical(this, other) || 262 | other is ScanResult && 263 | runtimeType == other.runtimeType && 264 | device == other.device; 265 | 266 | @override 267 | int get hashCode => device.hashCode; 268 | 269 | @override 270 | String toString() { 271 | return 'ScanResult{device: $device, advertisementData: $advertisementData, rssi: $rssi}'; 272 | } 273 | } 274 | 275 | class AdvertisementData { 276 | final String localName; 277 | final int? txPowerLevel; 278 | final bool connectable; 279 | final Map> manufacturerData; 280 | final Map> serviceData; 281 | final List serviceUuids; 282 | 283 | AdvertisementData.fromProto(protos.AdvertisementData p) 284 | : localName = p.localName, 285 | txPowerLevel = 286 | (p.txPowerLevel.hasValue()) ? p.txPowerLevel.value : null, 287 | connectable = p.connectable, 288 | manufacturerData = p.manufacturerData, 289 | serviceData = p.serviceData, 290 | serviceUuids = p.serviceUuids; 291 | 292 | @override 293 | String toString() { 294 | return 'AdvertisementData{localName: $localName, txPowerLevel: $txPowerLevel, connectable: $connectable, manufacturerData: $manufacturerData, serviceData: $serviceData, serviceUuids: $serviceUuids}'; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lib/src/guid.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | part of flutter_blue; 6 | 7 | class Guid { 8 | final List _bytes; 9 | final int _hashCode; 10 | 11 | Guid._internal(List bytes) 12 | : _bytes = bytes, 13 | _hashCode = _calcHashCode(bytes); 14 | 15 | Guid(String input) : this._internal(_fromString(input)); 16 | 17 | Guid.fromMac(String input) : this._internal(_fromMacString(input)); 18 | 19 | Guid.empty() : this._internal(new List.filled(16, 0)); 20 | 21 | static List _fromMacString(String input) { 22 | if (input == null) { 23 | throw new ArgumentError("Input was null"); 24 | } 25 | 26 | input = _removeNonHexCharacters(input); 27 | final bytes = hex.decode(input); 28 | 29 | if (bytes.length != 6) { 30 | throw new FormatException("The format is invalid: " + input); 31 | } 32 | 33 | return bytes + List.filled(10, 0); 34 | } 35 | 36 | static List _fromString(String input) { 37 | if (input == null) { 38 | throw new ArgumentError("Input was null"); 39 | } 40 | 41 | input = _removeNonHexCharacters(input); 42 | final bytes = hex.decode(input); 43 | 44 | if (bytes.length != 16) { 45 | throw new FormatException("The format is invalid"); 46 | } 47 | 48 | return bytes; 49 | } 50 | 51 | static String _removeNonHexCharacters(String sourceString) { 52 | return String.fromCharCodes(sourceString.runes.where((r) => 53 | (r >= 48 && r <= 57) // characters 0 to 9 54 | || (r >= 65 && r <= 70) // characters A to F 55 | || (r >= 97 && r <= 102) // characters a to f 56 | )); 57 | } 58 | 59 | static int _calcHashCode(List bytes) { 60 | const equality = const ListEquality(); 61 | return equality.hash(bytes); 62 | } 63 | 64 | @override 65 | String toString() { 66 | String one = hex.encode(_bytes.sublist(0, 4)); 67 | String two = hex.encode(_bytes.sublist(4, 6)); 68 | String three = hex.encode(_bytes.sublist(6, 8)); 69 | String four = hex.encode(_bytes.sublist(8, 10)); 70 | String five = hex.encode(_bytes.sublist(10, 16)); 71 | return "$one-$two-$three-$four-$five"; 72 | } 73 | 74 | String toMac() { 75 | String one = hex.encode(_bytes.sublist(0, 1)); 76 | String two = hex.encode(_bytes.sublist(1, 2)); 77 | String three = hex.encode(_bytes.sublist(2, 3)); 78 | String four = hex.encode(_bytes.sublist(3, 4)); 79 | String five = hex.encode(_bytes.sublist(4, 5)); 80 | String six = hex.encode(_bytes.sublist(5, 6)); 81 | return "$one:$two:$three:$four:$five:$six".toUpperCase(); 82 | } 83 | 84 | List toByteArray() { 85 | return _bytes; 86 | } 87 | 88 | operator ==(other) => other is Guid && this.hashCode == other.hashCode; 89 | 90 | int get hashCode => _hashCode; 91 | } 92 | -------------------------------------------------------------------------------- /macos/Classes/FlutterBluePlugin.h: -------------------------------------------------------------------------------- 1 | ../../ios/Classes/FlutterBluePlugin.h -------------------------------------------------------------------------------- /macos/Classes/FlutterBluePlugin.m: -------------------------------------------------------------------------------- 1 | ../../ios/Classes/FlutterBluePlugin.m -------------------------------------------------------------------------------- /macos/flutter_blue.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint flutter_blue.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'flutter_blue' 7 | s.version = '0.0.1' 8 | s.summary = 'Bluetooth Low Energy plugin for Flutter.' 9 | s.description = <<-DESC 10 | Bluetooth Low Energy plugin for Flutter. 11 | DESC 12 | s.homepage = 'https://github.com/pauldemarco/flutter_blue' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Paul DeMarco' => 'paulmdemarco@gmail.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*', 'gen/**/*' 17 | s.public_header_files = 'Classes/**/*.h', 'gen/**/*.h' 18 | s.dependency 'FlutterMacOS' 19 | s.platform = :osx, '10.13' 20 | s.framework = 'CoreBluetooth' 21 | 22 | s.subspec "Protos" do |ss| 23 | ss.source_files = "gen/*.pbobjc.{h,m}", "gen/**/*.pbobjc.{h,m}" 24 | ss.header_mappings_dir = "gen" 25 | ss.requires_arc = false 26 | ss.dependency "Protobuf", '~> 3.11.4' 27 | end 28 | 29 | s.pod_target_xcconfig = { 30 | 'DEFINES_MODULE' => 'YES', 31 | 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1', 32 | } 33 | end 34 | -------------------------------------------------------------------------------- /macos/gen/Flutterblue.pbobjc.h: -------------------------------------------------------------------------------- 1 | ../../ios/gen/Flutterblue.pbobjc.h -------------------------------------------------------------------------------- /macos/gen/Flutterblue.pbobjc.m: -------------------------------------------------------------------------------- 1 | ../../ios/gen/Flutterblue.pbobjc.m -------------------------------------------------------------------------------- /protos/flutterblue.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | syntax = "proto3"; 6 | 7 | option java_package = "com.pauldemarco.flutter_blue"; 8 | option java_outer_classname = "Protos"; 9 | option objc_class_prefix = "Protos"; 10 | 11 | // Wrapper message for `int32`. 12 | // 13 | // Allows for nullability of fields in messages 14 | message Int32Value { 15 | // The int32 value. 16 | int32 value = 1; 17 | } 18 | 19 | message BluetoothState { 20 | enum State { 21 | UNKNOWN = 0; 22 | UNAVAILABLE = 1; 23 | UNAUTHORIZED = 2; 24 | TURNING_ON = 3; 25 | ON = 4; 26 | TURNING_OFF = 5; 27 | OFF = 6; 28 | }; 29 | State state = 1; 30 | } 31 | 32 | message AdvertisementData { 33 | string local_name = 1; 34 | Int32Value tx_power_level = 2; 35 | bool connectable = 3; 36 | map manufacturer_data = 4; // Map of manufacturers to their data 37 | map service_data = 5; // Map of service UUIDs to their data. 38 | repeated string service_uuids = 6; 39 | } 40 | 41 | message ScanSettings { 42 | int32 android_scan_mode = 1; 43 | repeated string service_uuids = 2; 44 | bool allow_duplicates = 3; 45 | } 46 | 47 | message ScanResult { 48 | BluetoothDevice device = 1; // The received peer's ID. 49 | AdvertisementData advertisement_data = 2; 50 | int32 rssi = 3; 51 | } 52 | 53 | message ConnectRequest { 54 | string remote_id = 1; 55 | bool android_auto_connect = 2; 56 | } 57 | 58 | message BluetoothDevice { 59 | enum Type { 60 | UNKNOWN = 0; 61 | CLASSIC = 1; 62 | LE = 2; 63 | DUAL = 3; 64 | }; 65 | 66 | string remote_id = 1; 67 | string name = 2; 68 | Type type = 3; 69 | } 70 | 71 | message BluetoothService { 72 | string uuid = 1; 73 | string remote_id = 2; 74 | bool is_primary = 3; // Indicates whether the type of service is primary or secondary. 75 | repeated BluetoothCharacteristic characteristics = 4; // A list of characteristics that have been discovered in this service. 76 | repeated BluetoothService included_services = 5; // A list of included services that have been discovered in this service. 77 | } 78 | 79 | message BluetoothCharacteristic { 80 | string uuid = 1; 81 | string remote_id = 2; 82 | string serviceUuid = 3; // The service that this characteristic belongs to. 83 | string secondaryServiceUuid = 4; // The secondary service if nested 84 | repeated BluetoothDescriptor descriptors = 5; // A list of descriptors that have been discovered in this characteristic. 85 | CharacteristicProperties properties = 6; // The properties of the characteristic. 86 | bytes value = 7; 87 | } 88 | 89 | message BluetoothDescriptor { 90 | string uuid = 1; 91 | string remote_id = 2; 92 | string serviceUuid = 3; // The service that this descriptor belongs to. 93 | string characteristicUuid = 4; // The characteristic that this descriptor belongs to. 94 | bytes value = 5; 95 | } 96 | 97 | message CharacteristicProperties { 98 | bool broadcast = 1; 99 | bool read = 2; 100 | bool write_without_response = 3; 101 | bool write = 4; 102 | bool notify = 5; 103 | bool indicate = 6; 104 | bool authenticated_signed_writes = 7; 105 | bool extended_properties = 8; 106 | bool notify_encryption_required = 9; 107 | bool indicate_encryption_required = 10; 108 | } 109 | 110 | message DiscoverServicesResult { 111 | string remote_id = 1; 112 | repeated BluetoothService services = 2; 113 | } 114 | 115 | message ReadCharacteristicRequest { 116 | string remote_id = 1; 117 | string characteristic_uuid = 2; 118 | string service_uuid = 3; 119 | string secondary_service_uuid = 4; 120 | } 121 | 122 | message ReadCharacteristicResponse { 123 | string remote_id = 1; 124 | BluetoothCharacteristic characteristic = 2; 125 | } 126 | 127 | message ReadDescriptorRequest { 128 | string remote_id = 1; 129 | string descriptor_uuid = 2; 130 | string service_uuid = 3; 131 | string secondary_service_uuid = 4; 132 | string characteristic_uuid = 5; 133 | } 134 | 135 | message ReadDescriptorResponse { 136 | ReadDescriptorRequest request = 1; 137 | bytes value = 2; 138 | } 139 | 140 | message WriteCharacteristicRequest { 141 | enum WriteType { 142 | WITH_RESPONSE = 0; 143 | WITHOUT_RESPONSE = 1; 144 | } 145 | string remote_id = 1; 146 | string characteristic_uuid = 2; 147 | string service_uuid = 3; 148 | string secondary_service_uuid = 4; 149 | WriteType write_type = 5; 150 | bytes value = 6; 151 | } 152 | 153 | message WriteCharacteristicResponse { 154 | WriteCharacteristicRequest request = 1; 155 | bool success = 2; 156 | } 157 | 158 | message WriteDescriptorRequest { 159 | string remote_id = 1; 160 | string descriptor_uuid = 2; 161 | string service_uuid = 3; 162 | string secondary_service_uuid = 4; 163 | string characteristic_uuid = 5; 164 | bytes value = 6; 165 | } 166 | 167 | message WriteDescriptorResponse { 168 | WriteDescriptorRequest request = 1; 169 | bool success = 2; 170 | } 171 | 172 | message SetNotificationRequest { 173 | string remote_id = 1; 174 | string service_uuid = 2; 175 | string secondary_service_uuid = 3; 176 | string characteristic_uuid = 4; 177 | bool enable = 5; 178 | } 179 | 180 | message SetNotificationResponse { 181 | string remote_id = 1; 182 | BluetoothCharacteristic characteristic = 2; 183 | bool success = 3; 184 | } 185 | 186 | message OnCharacteristicChanged { 187 | string remote_id = 1; 188 | BluetoothCharacteristic characteristic = 2; 189 | } 190 | 191 | message DeviceStateResponse { 192 | enum BluetoothDeviceState { 193 | DISCONNECTED = 0; 194 | CONNECTING = 1; 195 | CONNECTED = 2; 196 | DISCONNECTING = 3; 197 | } 198 | string remote_id = 1; 199 | BluetoothDeviceState state = 2; 200 | } 201 | 202 | message ConnectedDevicesResponse { 203 | repeated BluetoothDevice devices = 1; 204 | } 205 | 206 | message MtuSizeRequest { 207 | string remote_id = 1; 208 | uint32 mtu = 2; 209 | } 210 | 211 | message MtuSizeResponse { 212 | string remote_id = 1; 213 | uint32 mtu = 2; 214 | } -------------------------------------------------------------------------------- /protos/regenerate.md: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco.\ 2 | // All rights reserved. Use of this source code is governed by a\ 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | # Generate protobuf files in Dart 6 | 1. If upgrading, delete all proto files from /home/.pub-cache/bin 7 | 1. Clone the latest dart-protoc-plugin from https://github.com/dart-lang/protobuf 8 | 1. Run `pub install` inside protobuf/protoc_plugin 9 | 1. Run `pub global activate protoc_plugin` to get .dart files into /home/.pub-cache/bin/ 10 | 1. Get the latest linux protoc compiler from https://github.com/google/protobuf/releases/ (protoc-X.X.X-linux-x86_64.zip) 11 | 1. Copy /bin/protoc into /home/.pub-cache/bin/ 12 | 1. Run the following commands from this project's protos folder 13 | ```protoc --dart_out=../lib/gen ./flutterblue.proto``` 14 | ```protoc --objc_out=../ios/gen ./flutterblue.proto``` -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_blue 2 | description: 3 | Flutter plugin for connecting and communicating with Bluetooth Low Energy devices, 4 | on Android and iOS 5 | version: 0.8.0 6 | homepage: https://github.com/pauldemarco/flutter_blue 7 | 8 | environment: 9 | sdk: '>=2.12.0 <3.0.0' 10 | flutter: ">=1.12.13+hotfix.6" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | convert: ^3.0.0 16 | protobuf: ^2.0.0 17 | rxdart: ^0.26.0 18 | collection: ^1.15.0 19 | meta: ^1.3.0 20 | 21 | dev_dependencies: 22 | flutter_test: 23 | sdk: flutter 24 | 25 | flutter: 26 | plugin: 27 | platforms: 28 | android: 29 | package: com.pauldemarco.flutter_blue 30 | pluginClass: FlutterBluePlugin 31 | ios: 32 | pluginClass: FlutterBluePlugin 33 | macos: 34 | pluginClass: FlutterBluePlugin 35 | -------------------------------------------------------------------------------- /site/flutterblue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/km1220/flutter-bluetooth/eb45624e6782ec8c349cee18929dba1508a564a3/site/flutterblue.png -------------------------------------------------------------------------------- /test/guid_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017, Paul DeMarco. 2 | // All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:flutter_blue/flutter_blue.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | main() { 9 | group("Guid", () { 10 | test('equality', () { 11 | var guid = new Guid("{00002a43-0000-1000-8000-00805f9b34fb}"); 12 | var guid2 = new Guid("00002a43-0000-1000-8000-00805f9b34fb"); 13 | expect(guid, guid2); 14 | 15 | var mac = new Guid.fromMac("01:02:03:04:05:06"); 16 | var mac2 = new Guid.fromMac("01:02:03:04:05:06"); 17 | expect(mac, mac2); 18 | }); 19 | 20 | test('empty()', () { 21 | var guid = new Guid.empty(); 22 | expect("[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]", 23 | guid.toByteArray().toString()); 24 | }); 25 | 26 | test('toByteArray()', () { 27 | var guid = new Guid("{00002a43-0000-1000-8000-00805f9b34fb}"); 28 | expect("[0, 0, 42, 67, 0, 0, 16, 0, 128, 0, 0, 128, 95, 155, 52, 251]", 29 | guid.toByteArray().toString()); 30 | }); 31 | 32 | test('toString()', () { 33 | var guid = new Guid("{00002a43-0000-1000-8000-00805f9b34fb}"); 34 | expect("00002a43-0000-1000-8000-00805f9b34fb", guid.toString()); 35 | }); 36 | 37 | test('fromMac()', () { 38 | var guid = new Guid.fromMac("24:0A:64:50:A4:67"); 39 | expect("[36, 10, 100, 80, 164, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]", 40 | guid.toByteArray().toString()); 41 | }); 42 | 43 | test('fromMac()', () { 44 | var guid = new Guid.fromMac("24:0A:64:50:A4:67"); 45 | expect("24:0A:64:50:A4:67", guid.toMac()); 46 | }); 47 | 48 | test('hashCode', () { 49 | var guid = new Guid.fromMac("24:0A:64:50:A4:67"); 50 | var guid2 = new Guid.fromMac("24:0A:64:50:A4:67"); 51 | expect(guid.hashCode, guid2.hashCode); 52 | }); 53 | 54 | test('empty() equality', () { 55 | var guid = new Guid.empty(); 56 | var guid2 = new Guid.empty(); 57 | var guid3 = new Guid.fromMac("24:0A:64:50:A4:67"); 58 | expect(guid == guid2, true); 59 | expect(guid == guid3, false); 60 | }); 61 | }); 62 | } 63 | --------------------------------------------------------------------------------