├── .github └── ISSUE_TEMPLATE │ └── Bugs.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── github │ └── edufolly │ └── flutterbluetoothserial │ ├── BluetoothConnection.java │ └── FlutterBluetoothSerialPlugin.java ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── arduino │ ├── Monitor.ino │ ├── Termometer.hpp │ └── phSensor.hpp ├── flutter_bluetooth_serial_example.iml ├── flutter_bluetooth_serial_example_android.iml ├── integration_test │ └── bluetooth_test.dart ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── 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 │ ├── BackgroundCollectedPage.dart │ ├── BackgroundCollectingTask.dart │ ├── BluetoothDeviceListEntry.dart │ ├── ChatPage.dart │ ├── DiscoveryPage.dart │ ├── MainPage.dart │ ├── SelectBondedDevicePage.dart │ ├── helpers │ │ ├── LineChart.dart │ │ └── PaintStyle.dart │ └── main.dart ├── pubspec.yaml └── test_driver │ └── integration_test.dart ├── flutter_bluetooth_serial.iml ├── flutter_bluetooth_serial_android.iml ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── FlutterBluetoothSerialPlugin.h │ └── FlutterBluetoothSerialPlugin.m └── flutter_bluetooth_serial.podspec ├── lib ├── BluetoothBondState.dart ├── BluetoothConnection.dart ├── BluetoothDevice.dart ├── BluetoothDeviceType.dart ├── BluetoothDiscoveryResult.dart ├── BluetoothPairingRequest.dart ├── BluetoothState.dart ├── FlutterBluetoothSerial.dart └── flutter_bluetooth_serial.dart └── pubspec.yaml /.github/ISSUE_TEMPLATE/Bugs.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | label: "Type: Bug" 5 | 6 | --- 7 | 8 | 17 | 18 | ### Problem summary 19 | 20 | 24 | 25 | ### Steps to reproduce 26 | 27 | 28 | 29 | 1. 30 | 2. 31 | 3. 32 | 4. 33 | 34 | 35 | 36 | ### Environment 37 | 38 | 39 | ``` 40 | ``` 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Miscellaneous 3 | *.class 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | /build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # Eclipse related 41 | example/android/\.project 42 | example/android/\.settings/org\.eclipse\.buildship\.core\.prefs 43 | example/android/app/\.settings/ 44 | example/android/app/\.classpath 45 | example/android/app/\.project 46 | android/\.settings/ 47 | android/\.project 48 | android/\.classpath 49 | 50 | # iOS/XCode related 51 | **/ios/**/*.mode1v3 52 | **/ios/**/*.mode2v3 53 | **/ios/**/*.moved-aside 54 | **/ios/**/*.pbxuser 55 | **/ios/**/*.perspectivev3 56 | **/ios/**/*sync/ 57 | **/ios/**/.sconsign.dblite 58 | **/ios/**/.tags* 59 | **/ios/**/.vagrant/ 60 | **/ios/**/DerivedData/ 61 | **/ios/**/Icon? 62 | **/ios/**/Pods/ 63 | **/ios/**/.symlinks/ 64 | **/ios/**/profile 65 | **/ios/**/xcuserdata 66 | **/ios/.generated/ 67 | **/ios/Flutter/App.framework 68 | **/ios/Flutter/Flutter.framework 69 | **/ios/Flutter/Generated.xcconfig 70 | **/ios/Flutter/app.flx 71 | **/ios/Flutter/app.zip 72 | **/ios/Flutter/flutter_assets/ 73 | **/ios/ServiceDefinitions.json 74 | **/ios/Runner/GeneratedPluginRegistrant.* 75 | 76 | # Exceptions to above rules. 77 | !**/ios/**/default.mode1v3 78 | !**/ios/**/default.mode2v3 79 | !**/ios/**/default.pbxuser 80 | !**/ios/**/default.perspectivev3 81 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 82 | 83 | # Library 84 | pubspec.lock 85 | 86 | # Scripts 87 | # None at the moment 88 | **/.flutter-plugins-dependencies 89 | **/flutter_export_environment.sh 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.4.0] - 2021-08-17 2 | 3 | Update flutter plugin v2. 4 | 5 | ## [0.3.2] - 2021-07-15 6 | 7 | Fixing pubspec.yaml typo. 8 | 9 | ## [0.3.1] - 2021-07-15 10 | 11 | Patch release. 12 | 13 | * Update example to sound null safety and new Material Buttons. 14 | * Changed FutureOr to FutureOr in getBondStateForAddress. 15 | * Fix an unhandled exception when trying to get bluetooth state. 16 | 17 | ## [0.3.0] - 2021-07-04 18 | 19 | Implementing null safety. 20 | 21 | ## [0.2.2] - 2019-08-19 22 | 23 | Patch release. 24 | 25 | * Fix closing connections which are not `listen`ing to `input` (solved #60), 26 | * Add more clear example of detecting which side caused disconnection, 27 | * Add exception on `add`ing to `output` if connection closed, 28 | * `BluetoothConnection` `cancel` is deprecated now, use `close` instead. It was 29 | stupid to name `cancel` something that `close`s (it was even documented that 30 | way, lol). 31 | 32 | ## [0.2.1] - 2019-08-05 33 | 34 | Patch release. 35 | 36 | * apply patch #48 for some disconnection issues, 37 | * update `ChatPage` to newer API, 38 | * fix and update AndroidX declaration. 39 | 40 | ## [0.2.0] - 2019-07-02 41 | 42 | Two big features packs: 43 | 44 | * Bonding (Pairing): 45 | - starting outgoing pairing requests, 46 | - handling incoming pairing requests, 47 | - remove current bondings to devices, 48 | * Discoverability! 49 | - requesting discoverable mode for specified duration, 50 | - example with timeout countdown, 51 | - checking discoverable mode. 52 | 53 | And few more features: 54 | 55 | * get/set for local (discoverable) device name, 56 | * getting local adapter address (with some hacks to work on newer Androids), 57 | * checking for `isConnected` for discovered or bonded devices, 58 | * fixed few broadcast receiver leaks. 59 | 60 | ## [0.1.1] - 2019-07-01 61 | 62 | * Patch #43 for "Error when meet unknown devices". 63 | 64 | ## [0.1.0] - 2019-06-19 65 | 66 | Pull request #35 by @PsychoXIVI changes a lot: 67 | 68 | * Discovering other devices, 69 | * Multiple connections, 70 | * Interesting example application, 71 | * Enabling/Disabling Bluetooth, 72 | * Byte streams, 73 | * Overall improvements and fixes. 74 | 75 | ## [0.0.5] - 2019-03-18 76 | 77 | * Upgrade for AndroidX support (thanks @akilinomendez) 78 | * New default constructor (thanks @MohiuddinM) 79 | * Added method write passing byte[] (thanks @rafaelterada) 80 | * Upgrade to Android Studio 3.3.2 81 | 82 | ## [0.0.4] - 2018-12-20 83 | 84 | * Unsupported value error correction (thanks @rafaelterada) 85 | * Added openSettings, which opens the bluetooth settings screen (thanks 86 | @rafaelterada) 87 | 88 | ## [0.0.3] - 2018-11-22 89 | 90 | * async connection and null exception handled (thanks @MohiuddinM) 91 | 92 | ## [0.0.2] - 2018-09-27 93 | 94 | * isConnected Implementation (thanks @Riscue) 95 | 96 | ## [0.0.1] - 2018-08-20 97 | 98 | * Only Android support. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Eduardo Folly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # `flutter_bluetooth_serial` 3 | 4 | [![pub package](https://img.shields.io/pub/v/flutter_bluetooth_serial.svg)](https://pub.dartlang.org/packages/flutter_bluetooth_serial) 5 | 6 | Flutter basic implementation for Classical Bluetooth (only RFCOMM for now). 7 | 8 | 9 | ## Features 10 | 11 | The first goal of this project, started by @edufolly was making an interface for Serial Port Protocol (HC-05 Adapter). Now the plugin features: 12 | 13 | + Adapter status monitoring, 14 | 15 | + Turning adapter on and off, 16 | 17 | + Opening settings, 18 | 19 | + Discovering devices (and requesting discoverability), 20 | 21 | + Listing bonded devices and pairing new ones, 22 | 23 | + Connecting to multiple devices at the same time, 24 | 25 | + Sending and receiving data (multiple connections). 26 | 27 | The plugin (for now) uses Serial Port profile for moving data over RFCOMM, so make sure there is running Service Discovery Protocol that points to SP/RFCOMM channel of the device. There could be [max up to 7 Bluetooth connections](https://stackoverflow.com/a/32149519/4880243). 28 | 29 | For now there is only Android support. 30 | 31 | 32 | ## Getting Started 33 | 34 | #### Depending 35 | ```yaml 36 | # Add dependency to `pubspec.yaml` of your project. 37 | dependencies: 38 | # ... 39 | flutter_bluetooth_serial: ^0.3.2 40 | ``` 41 | 42 | #### Installing 43 | 44 | ```bash 45 | # With pub manager 46 | pub get 47 | # or with Flutter 48 | flutter pub get 49 | ``` 50 | 51 | #### Importing 52 | ```dart 53 | import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; 54 | ``` 55 | 56 | #### Usage 57 | 58 | You should look to the Dart code of the library (mostly documented functions) or to the examples code. 59 | ```dart 60 | // Some simplest connection :F 61 | try { 62 | BluetoothConnection connection = await BluetoothConnection.toAddress(address); 63 | print('Connected to the device'); 64 | 65 | connection.input.listen((Uint8List data) { 66 | print('Data incoming: ${ascii.decode(data)}'); 67 | connection.output.add(data); // Sending data 68 | 69 | if (ascii.decode(data).contains('!')) { 70 | connection.finish(); // Closing connection 71 | print('Disconnecting by local host'); 72 | } 73 | }).onDone(() { 74 | print('Disconnected by remote request'); 75 | }); 76 | } 77 | catch (exception) { 78 | print('Cannot connect, exception occured'); 79 | } 80 | ``` 81 | 82 | Note: Work is underway to make the communication easier than operations on byte streams. See #41 for discussion about the topic. 83 | 84 | #### Examples 85 | 86 | Check out [example application](example/README.md) with connections with both Arduino HC-05 and Raspberry Pi (RFCOMM) Bluetooth interfaces. 87 | 88 | Main screen and options | Discovery and connecting | Simple chat with server | Background connection | 89 | :---:|:---:|:---:|:---:| 90 | ![](https://i.imgur.com/qeeMsVe.png) | ![](https://i.imgur.com/zruuelZ.png) | ![](https://i.imgur.com/y5mTUey.png) | ![](https://i.imgur.com/3wvwDVo.png) 91 | 92 | 93 | ## To-do list 94 | 95 | + Add some utils to easier manage `BluetoothConnection` (see discussion #41), 96 | + Allow connection method/protocol/UUID specification, 97 | + Listening/server mode, 98 | + Recognizing and displaying `BluetoothClass` of device, 99 | + Maybe integration with `flutter_blue` one day ;) 100 | 101 | You might also want to check [milestones](https://github.com/edufolly/flutter_bluetooth_serial/milestones). 102 | 103 | 104 | ## Credits 105 | 106 | - [Eduardo Folly](mailto:edufolly@gmail.com) 107 | - [Martin Mauch](mailto:martin.mauch@gmail.com) 108 | - [Patryk Ludwikowski](mailto:patryk.ludwikowski.7@gmail.com) 109 | 110 | After version 0.3.0 we have a lot of collaborators. If you would like to be credited, please send me an [email](mailto:edufolly@gmail.com). 111 | 112 | #### Thanks for all the support! -------------------------------------------------------------------------------- /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 'io.github.edufolly.flutterbluetoothserial' 2 | version '1.0-SNAPSHOT' 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.1.0' 11 | } 12 | } 13 | rootProject.allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | } 18 | } 19 | apply plugin: 'com.android.library' 20 | android { 21 | compileSdkVersion 30 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_8 24 | targetCompatibility JavaVersion.VERSION_1_8 25 | } 26 | defaultConfig { 27 | minSdkVersion 19 28 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 29 | } 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | dependencies { 34 | implementation 'androidx.appcompat:appcompat:1.3.0' 35 | } 36 | buildToolsVersion '30.0.3' 37 | } 38 | 39 | dependencies { 40 | } 41 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableJetifier=true 3 | android.useAndroidX=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_bluetooth_serial' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/java/io/github/edufolly/flutterbluetoothserial/BluetoothConnection.java: -------------------------------------------------------------------------------- 1 | package io.github.edufolly.flutterbluetoothserial; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.util.UUID; 7 | import java.util.Arrays; 8 | 9 | import android.bluetooth.BluetoothAdapter; 10 | import android.bluetooth.BluetoothDevice; 11 | import android.bluetooth.BluetoothSocket; 12 | 13 | /// Universal Bluetooth serial connection class (for Java) 14 | public abstract class BluetoothConnection 15 | { 16 | protected static final UUID DEFAULT_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); 17 | 18 | protected BluetoothAdapter bluetoothAdapter; 19 | 20 | protected ConnectionThread connectionThread = null; 21 | 22 | public boolean isConnected() { 23 | return connectionThread != null && connectionThread.requestedClosing != true; 24 | } 25 | 26 | 27 | 28 | public BluetoothConnection(BluetoothAdapter bluetoothAdapter) { 29 | this.bluetoothAdapter = bluetoothAdapter; 30 | } 31 | 32 | 33 | 34 | // @TODO . `connect` could be done perfored on the other thread 35 | // @TODO . `connect` parameter: timeout 36 | // @TODO . `connect` other methods than `createRfcommSocketToServiceRecord`, including hidden one raw `createRfcommSocket` (on channel). 37 | // @TODO ? how about turning it into factoried? 38 | /// Connects to given device by hardware address 39 | public void connect(String address, UUID uuid) throws IOException { 40 | if (isConnected()) { 41 | throw new IOException("already connected"); 42 | } 43 | 44 | BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address); 45 | if (device == null) { 46 | throw new IOException("device not found"); 47 | } 48 | 49 | BluetoothSocket socket = device.createRfcommSocketToServiceRecord(uuid); // @TODO . introduce ConnectionMethod 50 | if (socket == null) { 51 | throw new IOException("socket connection not established"); 52 | } 53 | 54 | // Cancel discovery, even though we didn't start it 55 | bluetoothAdapter.cancelDiscovery(); 56 | 57 | socket.connect(); 58 | 59 | connectionThread = new ConnectionThread(socket); 60 | connectionThread.start(); 61 | } 62 | /// Connects to given device by hardware address (default UUID used) 63 | public void connect(String address) throws IOException { 64 | connect(address, DEFAULT_UUID); 65 | } 66 | 67 | /// Disconnects current session (ignore if not connected) 68 | public void disconnect() { 69 | if (isConnected()) { 70 | connectionThread.cancel(); 71 | connectionThread = null; 72 | } 73 | } 74 | 75 | /// Writes to connected remote device 76 | public void write(byte[] data) throws IOException { 77 | if (!isConnected()) { 78 | throw new IOException("not connected"); 79 | } 80 | 81 | connectionThread.write(data); 82 | } 83 | 84 | /// Callback for reading data. 85 | protected abstract void onRead(byte[] data); 86 | 87 | /// Callback for disconnection. 88 | protected abstract void onDisconnected(boolean byRemote); 89 | 90 | /// Thread to handle connection I/O 91 | private class ConnectionThread extends Thread { 92 | private final BluetoothSocket socket; 93 | private final InputStream input; 94 | private final OutputStream output; 95 | private boolean requestedClosing = false; 96 | 97 | ConnectionThread(BluetoothSocket socket) { 98 | this.socket = socket; 99 | InputStream tmpIn = null; 100 | OutputStream tmpOut = null; 101 | 102 | try { 103 | tmpIn = socket.getInputStream(); 104 | tmpOut = socket.getOutputStream(); 105 | } catch (IOException e) { 106 | e.printStackTrace(); 107 | } 108 | 109 | this.input = tmpIn; 110 | this.output = tmpOut; 111 | } 112 | 113 | /// Thread main code 114 | public void run() { 115 | byte[] buffer = new byte[1024]; 116 | int bytes; 117 | 118 | while (!requestedClosing) { 119 | try { 120 | bytes = input.read(buffer); 121 | 122 | onRead(Arrays.copyOf(buffer, bytes)); 123 | } catch (IOException e) { 124 | // `input.read` throws when closed by remote device 125 | break; 126 | } 127 | } 128 | 129 | // Make sure output stream is closed 130 | if (output != null) { 131 | try { 132 | output.close(); 133 | } 134 | catch (Exception e) {} 135 | } 136 | 137 | // Make sure input stream is closed 138 | if (input != null) { 139 | try { 140 | input.close(); 141 | } 142 | catch (Exception e) {} 143 | } 144 | 145 | // Callback on disconnected, with information which side is closing 146 | onDisconnected(!requestedClosing); 147 | 148 | // Just prevent unnecessary `cancel`ing 149 | requestedClosing = true; 150 | } 151 | 152 | /// Writes to output stream 153 | public void write(byte[] bytes) { 154 | try { 155 | output.write(bytes); 156 | } catch (IOException e) { 157 | e.printStackTrace(); 158 | } 159 | } 160 | 161 | /// Stops the thread, disconnects 162 | public void cancel() { 163 | if (requestedClosing) { 164 | return; 165 | } 166 | requestedClosing = true; 167 | 168 | // Flush output buffers befoce closing 169 | try { 170 | output.flush(); 171 | } 172 | catch (Exception e) {} 173 | 174 | // Close the connection socket 175 | if (socket != null) { 176 | try { 177 | // Might be useful (see https://stackoverflow.com/a/22769260/4880243) 178 | Thread.sleep(111); 179 | 180 | socket.close(); 181 | } 182 | catch (Exception e) {} 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /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 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/**/GeneratedPluginRegistrant.java 38 | 39 | # iOS/XCode related 40 | **/ios/**/*.mode1v3 41 | **/ios/**/*.mode2v3 42 | **/ios/**/*.moved-aside 43 | **/ios/**/*.pbxuser 44 | **/ios/**/*.perspectivev3 45 | **/ios/**/*sync/ 46 | **/ios/**/.sconsign.dblite 47 | **/ios/**/.tags* 48 | **/ios/**/.vagrant/ 49 | **/ios/**/DerivedData/ 50 | **/ios/**/Icon? 51 | **/ios/**/Pods/ 52 | **/ios/**/.symlinks/ 53 | **/ios/**/profile 54 | **/ios/**/xcuserdata 55 | **/ios/.generated/ 56 | **/ios/Flutter/App.framework 57 | **/ios/Flutter/Flutter.framework 58 | **/ios/Flutter/Generated.xcconfig 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/ServiceDefinitions.json 63 | **/ios/Runner/GeneratedPluginRegistrant.* 64 | 65 | # Exceptions to above rules. 66 | !**/ios/**/default.mode1v3 67 | !**/ios/**/default.mode2v3 68 | !**/ios/**/default.pbxuser 69 | !**/ios/**/default.perspectivev3 70 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 71 | 72 | # Scripts 73 | # None at the moment 74 | 75 | -------------------------------------------------------------------------------- /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: c7ea3ca377e909469c68f2ab878a5bc53d3cf66b 8 | channel: beta 9 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # `flutter_bluetooth_serial_example` 2 | 3 | Example application demonstrates key features of the `flutter_bluetooth_serial` plugin: 4 | 5 | + Adapter status monitoring, 6 | 7 | + Turning adapter on and off, 8 | 9 | + Opening settings, 10 | 11 | + Discovering devices (and requesting discoverability), 12 | 13 | + Listing bonded devices and pairing new ones, 14 | 15 | + Connecting to multiple devices at the same time, 16 | 17 | + Sending and recieving data (multiple connections). 18 | 19 | The plugin (for now) uses Serial Port profile for moving data over RFCOMM, so make sure there is running Service Discovery Protocol that points to SP/RFCOMM channel of the device. 20 | 21 | #### Screens 22 | 23 | Main screen and options | Discovery and connecting | Simple chat with server | Background connection | 24 | :---:|:---:|:---:|:---:| 25 | ![](https://i.imgur.com/qeeMsVe.png) | ![](https://i.imgur.com/zruuelZ.png) | ![](https://i.imgur.com/y5mTUey.png) | ![](https://i.imgur.com/3wvwDVo.png) 26 | 27 | Note: There screen-shots might be out-dated. Build and see the example app for yourself, you won't regret it. :) 28 | 29 | #### Tests 30 | 31 | There is a recording of the tests (click for open video as WEBM version): 32 | 33 | [![Test with multiple connections](https://i.imgur.com/rDFrYcS.png)](https://webm.red/qpGg.webm) 34 | 35 | 36 | 37 | ## General 38 | 39 | The basics are simple, so there is no need to write about it so much. 40 | 41 | #### Discovery page 42 | 43 | On devices list you can long tap to start pairing process. If device is already paired, you can use long tap to unbond it. 44 | 45 | 46 | 47 | ## Chat example 48 | 49 | There is implemented simple chat. Client (the Flutter host) connects to selected from bonded devices server in order to exchange data - asynchronously. 50 | 51 | #### Simple (console) server on Raspberry Pi: 52 | 53 | 1. Enable Bluetooth and pair Raspberry with the Flutter host device (only first time) 54 | ``` 55 | $ sudo bluetoothctl 56 | # power on 57 | # agent on 58 | # scan on 59 | # pair [MAC of the Flutter host] 60 | # quit 61 | ``` 62 | 63 | 2. Add SP/RFCOMM entry to the SDP service 64 | ``` 65 | $ sudo sdptool add SP # There can be channel specified one of 79 channels by adding `--channel N`. 66 | $ sudo sdptool browse local # Check on which channel RFCOMM will be operating, to select in next step. 67 | ``` 68 | SDP tool tends to use good (and free) channels, so you don't have to keep track of other services if you let it decide. 69 | 70 | 3. Start RFCOMM listening 71 | ``` 72 | $ sudo killall rfcomm 73 | $ sudo rfcomm listen /dev/rfcomm0 N picocom -c /dev/rfcomm0 --omap crcrlf # `N` should be channel number on which SDP is pointing the SP. 74 | ``` 75 | 76 | 4. Now you can connect and chat to the server with example application using the console. Every character is send to your device and buffered. Only full messages, between new line characters (`\n`) are displayed. You can use `Ctrl+A` and `Ctrl+Q` to exit from `picocom` utility if you want to end stream from server side (and `Ctrl+C` for exit watch-mode of `rfcomm` utility). 77 | 78 | If you experiencing problems with your terminal (some `term_exitfunc` of `picocom` errors), you should try saving good terminal settings (`stty --save > someFile`) and loading them after picocom exits (adding ``; stty `cat someFile` `` to the second command of 3. should do the thing). 79 | 80 | You can also use the descriptor (`/dev/rfcomm0`) in other way, not necessarily to run interactive terminal on it, in order to chat. It can be used in various ways, providing more automation and/or abstraction. 81 | 82 | 83 | 84 | ## Background monitor example 85 | 86 | For testing multiple connections there were created background data collector, which connects to Arduino controller equiped with `HC-05` Bluetooth interface, 2 `DS18B20` termometers and water pH level meter. There are very nice graphs to displaying the recieved data. 87 | 88 | The example uses Celsius degree, which was chosen because it utilizes standard conditions of water freezing and ice melting points instead of just rolling a dice over periodic table of elements like a Fahrenheit do... 89 | 90 | Project of the Arduino side could be found in `/arduino` folder, but there is a note: **the code is prepared for testing in certain environment** and will not work without its hardware side (termometers, pH meter). If you can alter the real termometer code for example for random data generator or your own inputs. 91 | 92 | 93 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | -------------------------------------------------------------------------------- /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 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 31 19 | lintOptions { 20 | disable 'InvalidPackage' 21 | } 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId "io.github.edufolly.flutterbluetoothserialexample" 25 | minSdkVersion 19 26 | targetSdkVersion 30 27 | versionCode 1 28 | versionName "1.0" 29 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 30 | } 31 | buildTypes { 32 | release { 33 | // TODO: Add your own signing config for the release build. 34 | // Signing with the debug keys for now, so `flutter run --release` works. 35 | signingConfig signingConfigs.debug 36 | } 37 | } 38 | buildToolsVersion '30.0.3' 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | } 44 | 45 | flutter { 46 | source '../..' 47 | } 48 | 49 | dependencies { 50 | testImplementation 'junit:junit:4.13.2' 51 | androidTestImplementation 'androidx.test:runner:1.4.0' 52 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 53 | } 54 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | 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 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Mar 18 21:10:34 BRT 2019 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.1-all.zip 7 | -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/arduino/Monitor.ino: -------------------------------------------------------------------------------- 1 | /// Example temperature and ph level monitor 2 | /// 3 | /// Part of code of https://gitlab.com/PsychoXIVI/aquariumcontroller by Patryk (PsychoX) Ludwikowski 4 | /// Cutted to simple monitor for https://github.com/edufolly/flutter_bluetooth_serial/pull/35 5 | //////////////////////////////////////////////////////////////////////////////// 6 | #include 7 | 8 | #define DEBUG 1 9 | 10 | #include "Termometer.hpp" 11 | #include "phSensor.hpp" 12 | 13 | // There was so (stupidly) called `BluetoothQwertyServer` which provides simple packet system with data retransmission, but this should be simple example, so... bye friend :F 14 | 15 | // Library for virtual serial ports over normal pins 16 | #include 17 | 18 | SoftwareSerial bluetooth(7, 8); 19 | 20 | bool doUpdateStatus = false; 21 | 22 | 23 | 24 | //////////////////////////////////////////////////////////////////////////////// 25 | void loop(void) 26 | { 27 | delay(1); 28 | 29 | updateTermometer(); 30 | 31 | updatephSensorsSamplings(); 32 | updatephSensorsValues(); 33 | 34 | // Commands: "start", "stop" 35 | while (bluetooth.available() >= 4) { 36 | switch (bluetooth.read()) { 37 | case 's': 38 | bluetooth.read(); // Ignore (probably) 't' 39 | switch (bluetooth.read()) { 40 | case 'a': // "start" 41 | doUpdateStatus = true; 42 | digitalWrite(13, HIGH); 43 | 44 | bluetooth.read(); // Ignore 'r' 45 | while (bluetooth.available() == 0); 46 | bluetooth.read(); // Ignore 't' 47 | break; 48 | 49 | case 'o': // "stop" 50 | doUpdateStatus = false; 51 | digitalWrite(13, LOW); 52 | 53 | bluetooth.read(); // Ignore 'p' 54 | } 55 | break; 56 | } 57 | } 58 | 59 | static unsigned long lastRefreshTime = 0; 60 | if (millis() - lastRefreshTime >= 1000) { 61 | lastRefreshTime += 1000; 62 | 63 | if (doUpdateStatus) { 64 | // Every update there are temperatures and water pH level sent coded into binary form of: 65 | // 't', 66 | // integer part of value in Celcius of first termometer, 67 | // fractional part of value in Celcius of first termometer, 68 | // integer part of value in Celcius of second termometer, 69 | // fractional part of value in Celcius of second termometer, 70 | // 'w' 71 | // integer value of water pH level, 72 | // fractional part of water pH level. 73 | 74 | bluetooth.write('t'); 75 | for (byte i = 0; i < 2; i++) { 76 | bluetooth.write(static_cast(static_cast(DS18B20_value[i]))); 77 | bluetooth.write(static_cast(static_cast((DS18B20_value[i] - static_cast(DS18B20_value[i])) * 100))); 78 | } 79 | 80 | bluetooth.write('w'); 81 | bluetooth.write(static_cast(static_cast(phSensors[0].value))); 82 | bluetooth.write(static_cast(static_cast((phSensors[0].value - static_cast(phSensors[0].value)) * 100))); 83 | } 84 | } 85 | } 86 | 87 | 88 | 89 | //////////////////////////////////////////////////////////////////////////////// 90 | void setup(void) 91 | { 92 | Serial.begin(9600); 93 | 94 | bluetooth.begin(9600); 95 | 96 | setupTermometer(); 97 | 98 | setupphSensors(); 99 | 100 | pinMode(13, OUTPUT); 101 | digitalWrite(13, LOW); 102 | } 103 | 104 | 105 | -------------------------------------------------------------------------------- /example/arduino/Termometer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //////////////////////////////////////////////////////////////////////////////// 4 | // Heading 5 | 6 | // Library for DS18B20 termometer 7 | #include 8 | 9 | // OneWire protocol 10 | OneWire oneWire(9); 11 | 12 | // Number of DS18B20 13 | #define DS18B20_sensors_num 2 14 | 15 | // Adresses of DS18B20's 16 | const byte DS18B20_address[DS18B20_sensors_num][8] PROGMEM = { 17 | 0x28, 0x79, 0x84, 0x48, 0x4, 0x0, 0x0, 0xA6, 18 | 0x28, 0xEF, 0x19, 0x97, 0xA, 0x0, 0x0, 0x91 19 | }; 20 | 21 | // DS18B20 controllers 22 | DS18B20 DS18B20_controller(&oneWire); 23 | 24 | // Buffer for values 25 | float DS18B20_value[DS18B20_sensors_num]; 26 | 27 | 28 | 29 | //////////////////////////////////////////////////////////////////////////////// 30 | // Update 31 | 32 | inline void updateTermometer() 33 | { 34 | // Update values buffer 35 | if (DS18B20_controller.available()) { 36 | for (byte i = 0; i < DS18B20_sensors_num; i++) { 37 | DS18B20_value[i] = DS18B20_controller.readTemperature(FA(DS18B20_address[i])); 38 | } 39 | DS18B20_controller.request(); 40 | } 41 | } 42 | 43 | 44 | 45 | //////////////////////////////////////////////////////////////////////////////// 46 | // Setup 47 | 48 | inline void setupTermometer() 49 | { 50 | // Init controller 51 | DS18B20_controller.begin(); 52 | DS18B20_controller.request(); 53 | 54 | // First update 55 | updateTermometer(); 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /example/arduino/phSensor.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //////////////////////////////////////////////////////////////////////////////// 4 | // Heading 5 | 6 | #define phSensorDeviationOffset 0.00 //deviation compensate 7 | 8 | #define phSensorSamplingInterval 20 9 | #define phSensorSamplesNumber 40 10 | 11 | double avergearray(int*, int); 12 | 13 | struct phSensor 14 | { 15 | /* Fields */ 16 | float value; 17 | float voltage; 18 | unsigned long samplingTime = 0; 19 | 20 | /*unsigned*/ short lastValues[phSensorSamplesNumber]; 21 | unsigned char lastValueIndex = 0; 22 | 23 | const byte pin; // @TODO ? constexpr, skoro wszystkie piny i tak sa podawane compile-time 24 | 25 | /* Operators */ 26 | phSensor(const byte pin) 27 | : pin(pin) 28 | {} 29 | 30 | /* Methods */ 31 | void setup() 32 | { 33 | pinMode(pin, INPUT); 34 | } 35 | 36 | void updateSampling() 37 | { 38 | if (millis() - samplingTime > phSensorSamplingInterval) { 39 | lastValues[lastValueIndex++] = analogRead(pin); 40 | 41 | if (lastValueIndex == phSensorSamplesNumber) { 42 | lastValueIndex=0; 43 | } 44 | 45 | samplingTime = millis(); 46 | } 47 | } 48 | 49 | void updateValue() 50 | { 51 | voltage = avergearray(lastValues, phSensorSamplesNumber) * 5.0 / 1024; 52 | value = 3.5 * voltage + phSensorDeviationOffset; 53 | } 54 | }; 55 | 56 | 57 | // Number of ph sensors 58 | #define phSensors_Length 1 59 | 60 | // Structures constructed with ph sensors pin numbers. 61 | phSensor phSensors[phSensors_Length] = {{A6}}; 62 | 63 | 64 | 65 | //////////////////////////////////////////////////////////////////////////////// 66 | // Update 67 | 68 | inline void updatephSensorsSamplings() 69 | { 70 | phSensors[0].updateSampling(); 71 | } 72 | 73 | inline void updatephSensorsValues() 74 | { 75 | phSensors[0].updateValue(); 76 | } 77 | 78 | 79 | 80 | //////////////////////////////////////////////////////////////////////////////// 81 | // Setup 82 | 83 | inline void setupphSensors() 84 | { 85 | phSensors[0].setup(); 86 | } 87 | 88 | 89 | 90 | 91 | // Random code from internet below, sorry... 92 | double avergearray(int* arr, int number){ 93 | int i; 94 | int max,min; 95 | double avg; 96 | long amount = 0; 97 | 98 | if (number <= 0) { 99 | //Serial.println("Error number for the array to avraging!/n"); 100 | return 0; 101 | } 102 | if (number < 5) { //less than 5, calculated directly statistics 103 | for(i = 0; i < number; i++) { 104 | amount += arr[i]; 105 | } 106 | avg = amount/number; 107 | return avg; 108 | } 109 | else { // XDDD ? 110 | if (arr[0] < arr[1]) { 111 | min = arr[0]; 112 | max = arr[1]; 113 | } 114 | else { 115 | min = arr[1]; 116 | max = arr[0]; 117 | } 118 | for (i = 2; i < number; i++) { 119 | if (arr[i] < min) { 120 | amount += min; //arr max) { 125 | amount += max; //arr>max 126 | max = arr[i]; 127 | } 128 | else { 129 | amount += arr[i]; //min<=arr<=max 130 | } 131 | } 132 | } 133 | avg = (double)amount / (number - 2); 134 | } 135 | return avg; 136 | } 137 | 138 | 139 | -------------------------------------------------------------------------------- /example/flutter_bluetooth_serial_example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/flutter_bluetooth_serial_example_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/integration_test/bluetooth_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:integration_test/integration_test.dart'; 4 | 5 | void main() { 6 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 7 | 8 | testWidgets("failing test example", (WidgetTester tester) async { 9 | expect(2 + 2, equals(4)); 10 | expect( 11 | find.byWidgetPredicate( 12 | (Widget widget) => 13 | widget is Text && widget.data!.startsWith('Running on:'), 14 | ), 15 | findsOneWidget, 16 | ); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /example/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/app.flx 37 | /Flutter/app.zip 38 | /Flutter/flutter_assets/ 39 | /Flutter/App.framework 40 | /Flutter/Flutter.framework 41 | /Flutter/Generated.xcconfig 42 | /ServiceDefinitions.json 43 | 44 | Pods/ 45 | .symlinks/ 46 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 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 | def parse_KV_file(file, separator='=') 8 | file_abs_path = File.expand_path(file) 9 | if !File.exists? file_abs_path 10 | return []; 11 | end 12 | pods_ary = [] 13 | skip_line_start_symbols = ["#", "/"] 14 | File.foreach(file_abs_path) { |line| 15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 16 | plugin = line.split(pattern=separator) 17 | if plugin.length == 2 18 | podname = plugin[0].strip() 19 | path = plugin[1].strip() 20 | podpath = File.expand_path("#{path}", file_abs_path) 21 | pods_ary.push({:name => podname, :path => podpath}); 22 | else 23 | puts "Invalid plugin specification: #{line}" 24 | end 25 | } 26 | return pods_ary 27 | end 28 | 29 | target 'Runner' do 30 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 31 | # referring to absolute paths on developers' machines. 32 | system('rm -rf .symlinks') 33 | system('mkdir -p .symlinks/plugins') 34 | 35 | # Flutter Pods 36 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 37 | if generated_xcode_build_settings.empty? 38 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 39 | end 40 | generated_xcode_build_settings.map { |p| 41 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 42 | symlink = File.join('.symlinks', 'flutter') 43 | File.symlink(File.dirname(p[:path]), symlink) 44 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 45 | end 46 | } 47 | 48 | # Plugin Pods 49 | plugin_pods = parse_KV_file('../.flutter-plugins') 50 | plugin_pods.map { |p| 51 | symlink = File.join('.symlinks', 'plugins', p[:name]) 52 | File.symlink(p[:path], symlink) 53 | pod p[:name], :path => File.join(symlink, 'ios') 54 | } 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['ENABLE_BITCODE'] = 'NO' 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 13 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 14 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 16 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 18 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 19 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 20 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 21 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 22 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 23 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXCopyFilesBuildPhase section */ 27 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 28 | isa = PBXCopyFilesBuildPhase; 29 | buildActionMask = 2147483647; 30 | dstPath = ""; 31 | dstSubfolderSpec = 10; 32 | files = ( 33 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 34 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 35 | ); 36 | name = "Embed Frameworks"; 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXCopyFilesBuildPhase section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 43 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 44 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 45 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 46 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 47 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 48 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 49 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 50 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 51 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 52 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 53 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 55 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 57 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 58 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 67 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 9740EEB11CF90186004384FC /* Flutter */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 78 | 3B80C3931E831B6300D905FE /* App.framework */, 79 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 80 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 81 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 82 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 83 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 84 | ); 85 | name = Flutter; 86 | sourceTree = ""; 87 | }; 88 | 97C146E51CF9000F007C117D = { 89 | isa = PBXGroup; 90 | children = ( 91 | 9740EEB11CF90186004384FC /* Flutter */, 92 | 97C146F01CF9000F007C117D /* Runner */, 93 | 97C146EF1CF9000F007C117D /* Products */, 94 | CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, 95 | ); 96 | sourceTree = ""; 97 | }; 98 | 97C146EF1CF9000F007C117D /* Products */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 97C146EE1CF9000F007C117D /* Runner.app */, 102 | ); 103 | name = Products; 104 | sourceTree = ""; 105 | }; 106 | 97C146F01CF9000F007C117D /* Runner */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 110 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 111 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 112 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 113 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 114 | 97C147021CF9000F007C117D /* Info.plist */, 115 | 97C146F11CF9000F007C117D /* Supporting Files */, 116 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 117 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 118 | ); 119 | path = Runner; 120 | sourceTree = ""; 121 | }; 122 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 97C146F21CF9000F007C117D /* main.m */, 126 | ); 127 | name = "Supporting Files"; 128 | sourceTree = ""; 129 | }; 130 | /* End PBXGroup section */ 131 | 132 | /* Begin PBXNativeTarget section */ 133 | 97C146ED1CF9000F007C117D /* Runner */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 136 | buildPhases = ( 137 | 9740EEB61CF901F6004384FC /* Run Script */, 138 | 97C146EA1CF9000F007C117D /* Sources */, 139 | 97C146EB1CF9000F007C117D /* Frameworks */, 140 | 97C146EC1CF9000F007C117D /* Resources */, 141 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 142 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | name = Runner; 149 | productName = Runner; 150 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 151 | productType = "com.apple.product-type.application"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | 97C146E61CF9000F007C117D /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | LastUpgradeCheck = 0910; 160 | ORGANIZATIONNAME = "The Chromium Authors"; 161 | TargetAttributes = { 162 | 97C146ED1CF9000F007C117D = { 163 | CreatedOnToolsVersion = 7.3.1; 164 | }; 165 | }; 166 | }; 167 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 168 | compatibilityVersion = "Xcode 3.2"; 169 | developmentRegion = English; 170 | hasScannedForEncodings = 0; 171 | knownRegions = ( 172 | en, 173 | Base, 174 | ); 175 | mainGroup = 97C146E51CF9000F007C117D; 176 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 177 | projectDirPath = ""; 178 | projectRoot = ""; 179 | targets = ( 180 | 97C146ED1CF9000F007C117D /* Runner */, 181 | ); 182 | }; 183 | /* End PBXProject section */ 184 | 185 | /* Begin PBXResourcesBuildPhase section */ 186 | 97C146EC1CF9000F007C117D /* Resources */ = { 187 | isa = PBXResourcesBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 191 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 192 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 193 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 194 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 195 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 196 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | /* End PBXResourcesBuildPhase section */ 201 | 202 | /* Begin PBXShellScriptBuildPhase section */ 203 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 204 | isa = PBXShellScriptBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | ); 208 | inputPaths = ( 209 | ); 210 | name = "Thin Binary"; 211 | outputPaths = ( 212 | ); 213 | runOnlyForDeploymentPostprocessing = 0; 214 | shellPath = /bin/sh; 215 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 216 | }; 217 | 9740EEB61CF901F6004384FC /* Run Script */ = { 218 | isa = PBXShellScriptBuildPhase; 219 | buildActionMask = 2147483647; 220 | files = ( 221 | ); 222 | inputPaths = ( 223 | ); 224 | name = "Run Script"; 225 | outputPaths = ( 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | shellPath = /bin/sh; 229 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 230 | }; 231 | /* End PBXShellScriptBuildPhase section */ 232 | 233 | /* Begin PBXSourcesBuildPhase section */ 234 | 97C146EA1CF9000F007C117D /* Sources */ = { 235 | isa = PBXSourcesBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 239 | 97C146F31CF9000F007C117D /* main.m in Sources */, 240 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | /* End PBXSourcesBuildPhase section */ 245 | 246 | /* Begin PBXVariantGroup section */ 247 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 248 | isa = PBXVariantGroup; 249 | children = ( 250 | 97C146FB1CF9000F007C117D /* Base */, 251 | ); 252 | name = Main.storyboard; 253 | sourceTree = ""; 254 | }; 255 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 256 | isa = PBXVariantGroup; 257 | children = ( 258 | 97C147001CF9000F007C117D /* Base */, 259 | ); 260 | name = LaunchScreen.storyboard; 261 | sourceTree = ""; 262 | }; 263 | /* End PBXVariantGroup section */ 264 | 265 | /* Begin XCBuildConfiguration section */ 266 | 97C147031CF9000F007C117D /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 269 | buildSettings = { 270 | ALWAYS_SEARCH_USER_PATHS = NO; 271 | CLANG_ANALYZER_NONNULL = YES; 272 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 273 | CLANG_CXX_LIBRARY = "libc++"; 274 | CLANG_ENABLE_MODULES = YES; 275 | CLANG_ENABLE_OBJC_ARC = YES; 276 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 277 | CLANG_WARN_BOOL_CONVERSION = YES; 278 | CLANG_WARN_COMMA = YES; 279 | CLANG_WARN_CONSTANT_CONVERSION = YES; 280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 281 | CLANG_WARN_EMPTY_BODY = YES; 282 | CLANG_WARN_ENUM_CONVERSION = YES; 283 | CLANG_WARN_INFINITE_RECURSION = YES; 284 | CLANG_WARN_INT_CONVERSION = YES; 285 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 286 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 288 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 289 | CLANG_WARN_STRICT_PROTOTYPES = YES; 290 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 291 | CLANG_WARN_UNREACHABLE_CODE = YES; 292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 293 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 294 | COPY_PHASE_STRIP = NO; 295 | DEBUG_INFORMATION_FORMAT = dwarf; 296 | ENABLE_STRICT_OBJC_MSGSEND = YES; 297 | ENABLE_TESTABILITY = YES; 298 | GCC_C_LANGUAGE_STANDARD = gnu99; 299 | GCC_DYNAMIC_NO_PIC = NO; 300 | GCC_NO_COMMON_BLOCKS = YES; 301 | GCC_OPTIMIZATION_LEVEL = 0; 302 | GCC_PREPROCESSOR_DEFINITIONS = ( 303 | "DEBUG=1", 304 | "$(inherited)", 305 | ); 306 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 307 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 308 | GCC_WARN_UNDECLARED_SELECTOR = YES; 309 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 310 | GCC_WARN_UNUSED_FUNCTION = YES; 311 | GCC_WARN_UNUSED_VARIABLE = YES; 312 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 313 | MTL_ENABLE_DEBUG_INFO = YES; 314 | ONLY_ACTIVE_ARCH = YES; 315 | SDKROOT = iphoneos; 316 | TARGETED_DEVICE_FAMILY = "1,2"; 317 | }; 318 | name = Debug; 319 | }; 320 | 97C147041CF9000F007C117D /* Release */ = { 321 | isa = XCBuildConfiguration; 322 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 323 | buildSettings = { 324 | ALWAYS_SEARCH_USER_PATHS = NO; 325 | CLANG_ANALYZER_NONNULL = YES; 326 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 327 | CLANG_CXX_LIBRARY = "libc++"; 328 | CLANG_ENABLE_MODULES = YES; 329 | CLANG_ENABLE_OBJC_ARC = YES; 330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 331 | CLANG_WARN_BOOL_CONVERSION = YES; 332 | CLANG_WARN_COMMA = YES; 333 | CLANG_WARN_CONSTANT_CONVERSION = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_INFINITE_RECURSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 342 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 343 | CLANG_WARN_STRICT_PROTOTYPES = YES; 344 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 345 | CLANG_WARN_UNREACHABLE_CODE = YES; 346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 347 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 348 | COPY_PHASE_STRIP = NO; 349 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 350 | ENABLE_NS_ASSERTIONS = NO; 351 | ENABLE_STRICT_OBJC_MSGSEND = YES; 352 | GCC_C_LANGUAGE_STANDARD = gnu99; 353 | GCC_NO_COMMON_BLOCKS = YES; 354 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 355 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 356 | GCC_WARN_UNDECLARED_SELECTOR = YES; 357 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 358 | GCC_WARN_UNUSED_FUNCTION = YES; 359 | GCC_WARN_UNUSED_VARIABLE = YES; 360 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 361 | MTL_ENABLE_DEBUG_INFO = NO; 362 | SDKROOT = iphoneos; 363 | TARGETED_DEVICE_FAMILY = "1,2"; 364 | VALIDATE_PRODUCT = YES; 365 | }; 366 | name = Release; 367 | }; 368 | 97C147061CF9000F007C117D /* Debug */ = { 369 | isa = XCBuildConfiguration; 370 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 371 | buildSettings = { 372 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 373 | CURRENT_PROJECT_VERSION = 1; 374 | ENABLE_BITCODE = NO; 375 | FRAMEWORK_SEARCH_PATHS = ( 376 | "$(inherited)", 377 | "$(PROJECT_DIR)/Flutter", 378 | ); 379 | INFOPLIST_FILE = Runner/Info.plist; 380 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 381 | LIBRARY_SEARCH_PATHS = ( 382 | "$(inherited)", 383 | "$(PROJECT_DIR)/Flutter", 384 | ); 385 | PRODUCT_BUNDLE_IDENTIFIER = io.github.edufolly.flutterBluetoothSerialExample; 386 | PRODUCT_NAME = "$(TARGET_NAME)"; 387 | VERSIONING_SYSTEM = "apple-generic"; 388 | }; 389 | name = Debug; 390 | }; 391 | 97C147071CF9000F007C117D /* Release */ = { 392 | isa = XCBuildConfiguration; 393 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 394 | buildSettings = { 395 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 396 | CURRENT_PROJECT_VERSION = 1; 397 | ENABLE_BITCODE = NO; 398 | FRAMEWORK_SEARCH_PATHS = ( 399 | "$(inherited)", 400 | "$(PROJECT_DIR)/Flutter", 401 | ); 402 | INFOPLIST_FILE = Runner/Info.plist; 403 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 404 | LIBRARY_SEARCH_PATHS = ( 405 | "$(inherited)", 406 | "$(PROJECT_DIR)/Flutter", 407 | ); 408 | PRODUCT_BUNDLE_IDENTIFIER = io.github.edufolly.flutterBluetoothSerialExample; 409 | PRODUCT_NAME = "$(TARGET_NAME)"; 410 | VERSIONING_SYSTEM = "apple-generic"; 411 | }; 412 | name = Release; 413 | }; 414 | /* End XCBuildConfiguration section */ 415 | 416 | /* Begin XCConfigurationList section */ 417 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 418 | isa = XCConfigurationList; 419 | buildConfigurations = ( 420 | 97C147031CF9000F007C117D /* Debug */, 421 | 97C147041CF9000F007C117D /* Release */, 422 | ); 423 | defaultConfigurationIsVisible = 0; 424 | defaultConfigurationName = Release; 425 | }; 426 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 427 | isa = XCConfigurationList; 428 | buildConfigurations = ( 429 | 97C147061CF9000F007C117D /* Debug */, 430 | 97C147071CF9000F007C117D /* Release */, 431 | ); 432 | defaultConfigurationIsVisible = 0; 433 | defaultConfigurationName = Release; 434 | }; 435 | /* End XCConfigurationList section */ 436 | }; 437 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 438 | } 439 | -------------------------------------------------------------------------------- /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 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/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 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_bluetooth_serial_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /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/BackgroundCollectedPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import './BackgroundCollectingTask.dart'; 4 | import './helpers/LineChart.dart'; 5 | import './helpers/PaintStyle.dart'; 6 | 7 | class BackgroundCollectedPage extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | final BackgroundCollectingTask task = 11 | BackgroundCollectingTask.of(context, rebuildOnChange: true); 12 | 13 | // Arguments shift is needed for timestamps as miliseconds in double could loose precision. 14 | final int argumentsShift = 15 | task.samples.first.timestamp.millisecondsSinceEpoch; 16 | 17 | final Duration showDuration = 18 | Duration(hours: 2); // @TODO . show duration should be configurable 19 | final Iterable lastSamples = task.getLastOf(showDuration); 20 | 21 | final Iterable arguments = lastSamples.map((sample) { 22 | return (sample.timestamp.millisecondsSinceEpoch - argumentsShift) 23 | .toDouble(); 24 | }); 25 | 26 | // Step for argument labels 27 | final Duration argumentsStep = 28 | Duration(minutes: 15); // @TODO . step duration should be configurable 29 | 30 | // Find first timestamp floored to step before 31 | final DateTime beginningArguments = lastSamples.first.timestamp; 32 | DateTime beginningArgumentsStep = DateTime(beginningArguments.year, 33 | beginningArguments.month, beginningArguments.day); 34 | while (beginningArgumentsStep.isBefore(beginningArguments)) { 35 | beginningArgumentsStep = beginningArgumentsStep.add(argumentsStep); 36 | } 37 | beginningArgumentsStep = beginningArgumentsStep.subtract(argumentsStep); 38 | final DateTime endingArguments = lastSamples.last.timestamp; 39 | 40 | // Generate list of timestamps of labels 41 | final Iterable argumentsLabelsTimestamps = () sync* { 42 | DateTime timestamp = beginningArgumentsStep; 43 | yield timestamp; 44 | while (timestamp.isBefore(endingArguments)) { 45 | timestamp = timestamp.add(argumentsStep); 46 | yield timestamp; 47 | } 48 | }(); 49 | 50 | // Map strings for labels 51 | final Iterable argumentsLabels = 52 | argumentsLabelsTimestamps.map((timestamp) { 53 | return LabelEntry( 54 | (timestamp.millisecondsSinceEpoch - argumentsShift).toDouble(), 55 | ((timestamp.hour <= 9 ? '0' : '') + 56 | timestamp.hour.toString() + 57 | ':' + 58 | (timestamp.minute <= 9 ? '0' : '') + 59 | timestamp.minute.toString())); 60 | }); 61 | 62 | return Scaffold( 63 | appBar: AppBar( 64 | title: Text('Collected data'), 65 | actions: [ 66 | // Progress circle 67 | (task.inProgress 68 | ? FittedBox( 69 | child: Container( 70 | margin: new EdgeInsets.all(16.0), 71 | child: CircularProgressIndicator( 72 | valueColor: 73 | AlwaysStoppedAnimation(Colors.white)))) 74 | : Container(/* Dummy */)), 75 | // Start/stop buttons 76 | (task.inProgress 77 | ? IconButton(icon: Icon(Icons.pause), onPressed: task.pause) 78 | : IconButton( 79 | icon: Icon(Icons.play_arrow), onPressed: task.reasume)), 80 | ], 81 | ), 82 | body: ListView( 83 | children: [ 84 | Divider(), 85 | ListTile( 86 | leading: const Icon(Icons.brightness_7), 87 | title: const Text('Temperatures'), 88 | subtitle: const Text('In Celsius'), 89 | ), 90 | LineChart( 91 | constraints: const BoxConstraints.expand(height: 350), 92 | arguments: arguments, 93 | argumentsLabels: argumentsLabels, 94 | values: [ 95 | lastSamples.map((sample) => sample.temperature1), 96 | lastSamples.map((sample) => sample.temperature2), 97 | ], 98 | verticalLinesStyle: const PaintStyle(color: Colors.grey), 99 | additionalMinimalHorizontalLabelsInterval: 0, 100 | additionalMinimalVerticalLablesInterval: 0, 101 | seriesPointsStyles: [ 102 | null, 103 | null, 104 | //const PaintStyle(style: PaintingStyle.stroke, strokeWidth: 1.7*3, color: Colors.indigo, strokeCap: StrokeCap.round), 105 | ], 106 | seriesLinesStyles: [ 107 | const PaintStyle( 108 | style: PaintingStyle.stroke, 109 | strokeWidth: 1.7, 110 | color: Colors.indigoAccent), 111 | const PaintStyle( 112 | style: PaintingStyle.stroke, 113 | strokeWidth: 1.7, 114 | color: Colors.redAccent), 115 | ], 116 | ), 117 | Divider(), 118 | ListTile( 119 | leading: const Icon(Icons.filter_vintage), 120 | title: const Text('Water pH level'), 121 | ), 122 | LineChart( 123 | constraints: const BoxConstraints.expand(height: 200), 124 | arguments: arguments, 125 | argumentsLabels: argumentsLabels, 126 | values: [ 127 | lastSamples.map((sample) => sample.waterpHlevel), 128 | ], 129 | verticalLinesStyle: const PaintStyle(color: Colors.grey), 130 | additionalMinimalHorizontalLabelsInterval: 0, 131 | additionalMinimalVerticalLablesInterval: 0, 132 | seriesPointsStyles: [ 133 | null, 134 | ], 135 | seriesLinesStyles: [ 136 | const PaintStyle( 137 | style: PaintingStyle.stroke, 138 | strokeWidth: 1.7, 139 | color: Colors.greenAccent), 140 | ], 141 | ), 142 | ], 143 | )); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /example/lib/BackgroundCollectingTask.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; 6 | import 'package:scoped_model/scoped_model.dart'; 7 | 8 | class _Message { 9 | int whom; 10 | String text; 11 | 12 | _Message(this.whom, this.text); 13 | } 14 | class DataSample { 15 | double temperature1; 16 | double temperature2; 17 | double waterpHlevel; 18 | DateTime timestamp; 19 | 20 | DataSample({ 21 | required this.temperature1, 22 | required this.temperature2, 23 | required this.waterpHlevel, 24 | required this.timestamp, 25 | }); 26 | } 27 | 28 | class BackgroundCollectingTask extends Model { 29 | static BackgroundCollectingTask of( 30 | BuildContext context, { 31 | bool rebuildOnChange = false, 32 | }) => 33 | ScopedModel.of( 34 | context, 35 | rebuildOnChange: rebuildOnChange, 36 | ); 37 | 38 | final BluetoothConnection _connection; 39 | List _buffer = List.empty(growable: true); 40 | 41 | // @TODO , Such sample collection in real code should be delegated 42 | // (via `Stream` preferably) and then saved for later 43 | // displaying on chart (or even stright prepare for displaying). 44 | // @TODO ? should be shrinked at some point, endless colleting data would cause memory shortage. 45 | List samples = List.empty(growable: true); 46 | 47 | bool inProgress = false; 48 | 49 | List<_Message> messages = []; 50 | String _messageBuffer = ''; 51 | 52 | BackgroundCollectingTask._fromConnection(this._connection) { 53 | _connection.input!.listen((data) {_onDataReceived(data); 54 | }).onDone(() { 55 | 56 | inProgress = false; 57 | notifyListeners(); 58 | }); 59 | } 60 | void _onDataReceived(Uint8List data) { 61 | // Allocate buffer for parsed data 62 | int backspacesCounter = 0; 63 | data.forEach((byte) { 64 | if (byte == 8 || byte == 127) { 65 | backspacesCounter++; 66 | } 67 | }); 68 | Uint8List buffer = Uint8List(data.length - backspacesCounter); 69 | int bufferIndex = buffer.length; 70 | 71 | // Apply backspace control character 72 | backspacesCounter = 0; 73 | for (int i = data.length - 1; i >= 0; i--) { 74 | if (data[i] == 8 || data[i] == 127) { 75 | backspacesCounter++; 76 | } else { 77 | if (backspacesCounter > 0) { 78 | backspacesCounter--; 79 | } else { 80 | buffer[--bufferIndex] = data[i]; 81 | } 82 | } 83 | } 84 | 85 | // Create message if there is new line character 86 | String dataString = String.fromCharCodes(buffer); 87 | int index = buffer.indexOf(13); 88 | if (~index != 0) { 89 | messages.add( 90 | _Message( 91 | 1, 92 | backspacesCounter > 0 93 | ? _messageBuffer.substring( 94 | 0, _messageBuffer.length - backspacesCounter) 95 | : _messageBuffer + dataString.substring(0, index), 96 | ), 97 | ); 98 | _messageBuffer = dataString.substring(index); 99 | print(messages.length-1); 100 | } else { 101 | _messageBuffer = (backspacesCounter > 0 102 | ? _messageBuffer.substring( 103 | 0, _messageBuffer.length - backspacesCounter) 104 | : _messageBuffer + dataString); 105 | } 106 | } 107 | static Future connect( 108 | BluetoothDevice server) async { 109 | final BluetoothConnection connection = 110 | await BluetoothConnection.toAddress(server.address); 111 | return BackgroundCollectingTask._fromConnection(connection); 112 | } 113 | 114 | void dispose() { 115 | _connection.dispose(); 116 | } 117 | 118 | Future start() async { 119 | inProgress = true; 120 | _buffer.clear(); 121 | samples.clear(); 122 | notifyListeners(); 123 | _connection.output.add(ascii.encode('start')); 124 | await _connection.output.allSent; 125 | } 126 | 127 | Future cancel() async { 128 | inProgress = false; 129 | notifyListeners(); 130 | _connection.output.add(ascii.encode('stop')); 131 | await _connection.finish(); 132 | } 133 | 134 | Future pause() async { 135 | inProgress = false; 136 | notifyListeners(); 137 | _connection.output.add(ascii.encode('stop')); 138 | await _connection.output.allSent; 139 | } 140 | 141 | Future reasume() async { 142 | inProgress = true; 143 | notifyListeners(); 144 | _connection.output.add(ascii.encode('start')); 145 | await _connection.output.allSent; 146 | } 147 | 148 | Iterable getLastOf(Duration duration) { 149 | DateTime startingTime = DateTime.now().subtract(duration); 150 | int i = samples.length; 151 | do { 152 | i -= 1; 153 | if (i <= 0) { 154 | break; 155 | } 156 | } while (samples[i].timestamp.isAfter(startingTime)); 157 | return samples.getRange(i, samples.length); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /example/lib/BluetoothDeviceListEntry.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; 3 | 4 | class BluetoothDeviceListEntry extends ListTile { 5 | BluetoothDeviceListEntry({ 6 | required BluetoothDevice device, 7 | int? rssi, 8 | GestureTapCallback? onTap, 9 | GestureLongPressCallback? onLongPress, 10 | bool enabled = true, 11 | }) : super( 12 | onTap: onTap, 13 | onLongPress: onLongPress, 14 | enabled: enabled, 15 | leading: 16 | Icon(Icons.devices), // @TODO . !BluetoothClass! class aware icon 17 | title: Text(device.name ?? ""), 18 | subtitle: Text(device.address.toString()), 19 | trailing: Row( 20 | mainAxisSize: MainAxisSize.min, 21 | children: [ 22 | rssi != null 23 | ? Container( 24 | margin: new EdgeInsets.all(8.0), 25 | child: DefaultTextStyle( 26 | style: _computeTextStyle(rssi), 27 | child: Column( 28 | mainAxisSize: MainAxisSize.min, 29 | children: [ 30 | Text(rssi.toString()), 31 | Text('dBm'), 32 | ], 33 | ), 34 | ), 35 | ) 36 | : Container(width: 0, height: 0), 37 | device.isConnected 38 | ? Icon(Icons.import_export) 39 | : Container(width: 0, height: 0), 40 | device.isBonded 41 | ? Icon(Icons.link) 42 | : Container(width: 0, height: 0), 43 | ], 44 | ), 45 | ); 46 | 47 | static TextStyle _computeTextStyle(int rssi) { 48 | /**/ if (rssi >= -35) 49 | return TextStyle(color: Colors.greenAccent[700]); 50 | else if (rssi >= -45) 51 | return TextStyle( 52 | color: Color.lerp( 53 | Colors.greenAccent[700], Colors.lightGreen, -(rssi + 35) / 10)); 54 | else if (rssi >= -55) 55 | return TextStyle( 56 | color: Color.lerp( 57 | Colors.lightGreen, Colors.lime[600], -(rssi + 45) / 10)); 58 | else if (rssi >= -65) 59 | return TextStyle( 60 | color: Color.lerp(Colors.lime[600], Colors.amber, -(rssi + 55) / 10)); 61 | else if (rssi >= -75) 62 | return TextStyle( 63 | color: Color.lerp( 64 | Colors.amber, Colors.deepOrangeAccent, -(rssi + 65) / 10)); 65 | else if (rssi >= -85) 66 | return TextStyle( 67 | color: Color.lerp( 68 | Colors.deepOrangeAccent, Colors.redAccent, -(rssi + 75) / 10)); 69 | else 70 | /*code symmetry*/ 71 | return TextStyle(color: Colors.redAccent); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/lib/ChatPage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; 7 | 8 | class ChatPage extends StatefulWidget { 9 | final BluetoothDevice server; 10 | 11 | const ChatPage({required this.server}); 12 | 13 | @override 14 | _ChatPage createState() => new _ChatPage(); 15 | } 16 | 17 | class _Message { 18 | int whom; 19 | String text; 20 | 21 | _Message(this.whom, this.text); 22 | } 23 | 24 | class _ChatPage extends State { 25 | static final clientID = 0; 26 | BluetoothConnection? connection; 27 | 28 | List<_Message> messages = List<_Message>.empty(growable: true); 29 | String _messageBuffer = ''; 30 | 31 | final TextEditingController textEditingController = 32 | new TextEditingController(); 33 | final ScrollController listScrollController = new ScrollController(); 34 | 35 | bool isConnecting = true; 36 | bool get isConnected => (connection?.isConnected ?? false); 37 | 38 | bool isDisconnecting = false; 39 | 40 | @override 41 | void initState() { 42 | super.initState(); 43 | 44 | BluetoothConnection.toAddress(widget.server.address).then((_connection) { 45 | print('Connected to the device'); 46 | connection = _connection; 47 | setState(() { 48 | isConnecting = false; 49 | isDisconnecting = false; 50 | }); 51 | 52 | connection!.input!.listen(_onDataReceived).onDone(() { 53 | // Example: Detect which side closed the connection 54 | // There should be `isDisconnecting` flag to show are we are (locally) 55 | // in middle of disconnecting process, should be set before calling 56 | // `dispose`, `finish` or `close`, which all causes to disconnect. 57 | // If we except the disconnection, `onDone` should be fired as result. 58 | // If we didn't except this (no flag set), it means closing by remote. 59 | if (isDisconnecting) { 60 | print('Disconnecting locally!'); 61 | } else { 62 | print('Disconnected remotely!'); 63 | } 64 | if (this.mounted) { 65 | setState(() {}); 66 | } 67 | }); 68 | }).catchError((error) { 69 | print('Cannot connect, exception occured'); 70 | print(error); 71 | }); 72 | } 73 | 74 | @override 75 | void dispose() { 76 | // Avoid memory leak (`setState` after dispose) and disconnect 77 | if (isConnected) { 78 | isDisconnecting = true; 79 | connection?.dispose(); 80 | connection = null; 81 | } 82 | 83 | super.dispose(); 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | final List list = messages.map((_message) { 89 | return Row( 90 | children: [ 91 | Container( 92 | child: Text( 93 | (text) { 94 | return text == '/shrug' ? '¯\\_(ツ)_/¯' : text; 95 | }(_message.text.trim()), 96 | style: TextStyle(color: Colors.white)), 97 | padding: EdgeInsets.all(12.0), 98 | margin: EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0), 99 | width: 222.0, 100 | decoration: BoxDecoration( 101 | color: 102 | _message.whom == clientID ? Colors.blueAccent : Colors.grey, 103 | borderRadius: BorderRadius.circular(7.0)), 104 | ), 105 | ], 106 | mainAxisAlignment: _message.whom == clientID 107 | ? MainAxisAlignment.end 108 | : MainAxisAlignment.start, 109 | ); 110 | }).toList(); 111 | 112 | final serverName = widget.server.name ?? "Unknown"; 113 | return Scaffold( 114 | appBar: AppBar( 115 | title: (isConnecting 116 | ? Text('Connecting chat to ' + serverName + '...') 117 | : isConnected 118 | ? Text('Live chat with ' + serverName) 119 | : Text('Chat log with ' + serverName))), 120 | body: SafeArea( 121 | child: Column( 122 | children: [ 123 | Flexible( 124 | child: ListView( 125 | padding: const EdgeInsets.all(12.0), 126 | controller: listScrollController, 127 | children: list), 128 | ), 129 | Row( 130 | children: [ 131 | Flexible( 132 | child: Container( 133 | margin: const EdgeInsets.only(left: 16.0), 134 | child: TextField( 135 | style: const TextStyle(fontSize: 15.0), 136 | controller: textEditingController, 137 | decoration: InputDecoration.collapsed( 138 | hintText: isConnecting 139 | ? 'Wait until connected...' 140 | : isConnected 141 | ? 'Type your message...' 142 | : 'Chat got disconnected', 143 | hintStyle: const TextStyle(color: Colors.grey), 144 | ), 145 | enabled: isConnected, 146 | ), 147 | ), 148 | ), 149 | Container( 150 | margin: const EdgeInsets.all(8.0), 151 | child: IconButton( 152 | icon: const Icon(Icons.send), 153 | onPressed: isConnected 154 | ? () => _sendMessage(textEditingController.text) 155 | : null), 156 | ), 157 | ], 158 | ) 159 | ], 160 | ), 161 | ), 162 | ); 163 | } 164 | 165 | void _onDataReceived(Uint8List data) { 166 | // Allocate buffer for parsed data 167 | int backspacesCounter = 0; 168 | data.forEach((byte) { 169 | if (byte == 8 || byte == 127) { 170 | backspacesCounter++; 171 | } 172 | }); 173 | Uint8List buffer = Uint8List(data.length - backspacesCounter); 174 | int bufferIndex = buffer.length; 175 | 176 | // Apply backspace control character 177 | backspacesCounter = 0; 178 | for (int i = data.length - 1; i >= 0; i--) { 179 | if (data[i] == 8 || data[i] == 127) { 180 | backspacesCounter++; 181 | } else { 182 | if (backspacesCounter > 0) { 183 | backspacesCounter--; 184 | } else { 185 | buffer[--bufferIndex] = data[i]; 186 | } 187 | } 188 | } 189 | 190 | // Create message if there is new line character 191 | String dataString = String.fromCharCodes(buffer); 192 | int index = buffer.indexOf(13); 193 | if (~index != 0) { 194 | setState(() { 195 | messages.add( 196 | _Message( 197 | 1, 198 | backspacesCounter > 0 199 | ? _messageBuffer.substring( 200 | 0, _messageBuffer.length - backspacesCounter) 201 | : _messageBuffer + dataString.substring(0, index), 202 | ), 203 | ); 204 | _messageBuffer = dataString.substring(index); 205 | }); 206 | } else { 207 | _messageBuffer = (backspacesCounter > 0 208 | ? _messageBuffer.substring( 209 | 0, _messageBuffer.length - backspacesCounter) 210 | : _messageBuffer + dataString); 211 | } 212 | } 213 | 214 | void _sendMessage(String text) async { 215 | text = text.trim(); 216 | textEditingController.clear(); 217 | 218 | if (text.length > 0) { 219 | try { 220 | connection!.output.add(Uint8List.fromList(utf8.encode(text + "\r\n"))); 221 | await connection!.output.allSent; 222 | 223 | setState(() { 224 | messages.add(_Message(clientID, text)); 225 | }); 226 | 227 | Future.delayed(Duration(milliseconds: 333)).then((_) { 228 | listScrollController.animateTo( 229 | listScrollController.position.maxScrollExtent, 230 | duration: Duration(milliseconds: 333), 231 | curve: Curves.easeOut); 232 | }); 233 | } catch (e) { 234 | // Ignore error, but notify state 235 | setState(() {}); 236 | } 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /example/lib/DiscoveryPage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; 5 | 6 | import './BluetoothDeviceListEntry.dart'; 7 | 8 | class DiscoveryPage extends StatefulWidget { 9 | /// If true, discovery starts on page start, otherwise user must press action button. 10 | final bool start; 11 | 12 | const DiscoveryPage({this.start = true}); 13 | 14 | @override 15 | _DiscoveryPage createState() => new _DiscoveryPage(); 16 | } 17 | 18 | class _DiscoveryPage extends State { 19 | StreamSubscription? _streamSubscription; 20 | List results = 21 | List.empty(growable: true); 22 | bool isDiscovering = false; 23 | 24 | _DiscoveryPage(); 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | 30 | isDiscovering = widget.start; 31 | if (isDiscovering) { 32 | _startDiscovery(); 33 | } 34 | } 35 | 36 | void _restartDiscovery() { 37 | setState(() { 38 | results.clear(); 39 | isDiscovering = true; 40 | }); 41 | 42 | _startDiscovery(); 43 | } 44 | 45 | void _startDiscovery() { 46 | _streamSubscription = 47 | FlutterBluetoothSerial.instance.startDiscovery().listen((r) { 48 | setState(() { 49 | final existingIndex = results.indexWhere( 50 | (element) => element.device.address == r.device.address); 51 | if (existingIndex >= 0) 52 | results[existingIndex] = r; 53 | else 54 | results.add(r); 55 | }); 56 | }); 57 | 58 | _streamSubscription!.onDone(() { 59 | setState(() { 60 | isDiscovering = false; 61 | }); 62 | }); 63 | } 64 | 65 | // @TODO . One day there should be `_pairDevice` on long tap on something... ;) 66 | 67 | @override 68 | void dispose() { 69 | // Avoid memory leak (`setState` after dispose) and cancel discovery 70 | _streamSubscription?.cancel(); 71 | 72 | super.dispose(); 73 | } 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return Scaffold( 78 | appBar: AppBar( 79 | title: isDiscovering 80 | ? Text('Discovering devices') 81 | : Text('Discovered devices'), 82 | actions: [ 83 | isDiscovering 84 | ? FittedBox( 85 | child: Container( 86 | margin: new EdgeInsets.all(16.0), 87 | child: CircularProgressIndicator( 88 | valueColor: AlwaysStoppedAnimation(Colors.white), 89 | ), 90 | ), 91 | ) 92 | : IconButton( 93 | icon: Icon(Icons.replay), 94 | onPressed: _restartDiscovery, 95 | ) 96 | ], 97 | ), 98 | body: ListView.builder( 99 | itemCount: results.length, 100 | itemBuilder: (BuildContext context, index) { 101 | BluetoothDiscoveryResult result = results[index]; 102 | final device = result.device; 103 | final address = device.address; 104 | return BluetoothDeviceListEntry( 105 | device: device, 106 | rssi: result.rssi, 107 | onTap: () { 108 | Navigator.of(context).pop(result.device); 109 | }, 110 | onLongPress: () async { 111 | try { 112 | bool bonded = false; 113 | if (device.isBonded) { 114 | print('Unbonding from ${device.address}...'); 115 | await FlutterBluetoothSerial.instance 116 | .removeDeviceBondWithAddress(address); 117 | print('Unbonding from ${device.address} has succed'); 118 | } else { 119 | print('Bonding with ${device.address}...'); 120 | bonded = (await FlutterBluetoothSerial.instance 121 | .bondDeviceAtAddress(address))!; 122 | print( 123 | 'Bonding with ${device.address} has ${bonded ? 'succed' : 'failed'}.'); 124 | } 125 | setState(() { 126 | results[results.indexOf(result)] = BluetoothDiscoveryResult( 127 | device: BluetoothDevice( 128 | name: device.name ?? '', 129 | address: address, 130 | type: device.type, 131 | bondState: bonded 132 | ? BluetoothBondState.bonded 133 | : BluetoothBondState.none, 134 | ), 135 | rssi: result.rssi); 136 | }); 137 | } catch (ex) { 138 | showDialog( 139 | context: context, 140 | builder: (BuildContext context) { 141 | return AlertDialog( 142 | title: const Text('Error occured while bonding'), 143 | content: Text("${ex.toString()}"), 144 | actions: [ 145 | new TextButton( 146 | child: new Text("Close"), 147 | onPressed: () { 148 | Navigator.of(context).pop(); 149 | }, 150 | ), 151 | ], 152 | ); 153 | }, 154 | ); 155 | } 156 | }, 157 | ); 158 | }, 159 | ), 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /example/lib/MainPage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; 4 | import 'package:scoped_model/scoped_model.dart'; 5 | import './BackgroundCollectedPage.dart'; 6 | import './BackgroundCollectingTask.dart'; 7 | import './ChatPage.dart'; 8 | import './SelectBondedDevicePage.dart'; 9 | 10 | // import './helpers/LineChart.dart'; 11 | 12 | class MainPage extends StatefulWidget { 13 | @override 14 | _MainPage createState() => new _MainPage(); 15 | } 16 | 17 | class _MainPage extends State { 18 | BluetoothState _bluetoothState = BluetoothState.UNKNOWN; 19 | 20 | String _address = "..."; 21 | String _name = "..."; 22 | 23 | Timer? _discoverableTimeoutTimer; 24 | int _discoverableTimeoutSecondsLeft = 0; 25 | 26 | BackgroundCollectingTask? _collectingTask; 27 | 28 | bool _autoAcceptPairingRequests = false; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | 34 | // Get current state 35 | FlutterBluetoothSerial.instance.state.then((state) { 36 | setState(() { 37 | _bluetoothState = state; 38 | }); 39 | }); 40 | 41 | Future.doWhile(() async { 42 | // Wait if adapter not enabled 43 | if ((await FlutterBluetoothSerial.instance.isEnabled) ?? false) { 44 | return false; 45 | } 46 | await Future.delayed(Duration(milliseconds: 0xDD)); 47 | return true; 48 | }).then((_) { 49 | // Update the address field 50 | FlutterBluetoothSerial.instance.address.then((address) { 51 | setState(() { 52 | _address = address!; 53 | }); 54 | }); 55 | }); 56 | 57 | FlutterBluetoothSerial.instance.name.then((name) { 58 | setState(() { 59 | _name = name!; 60 | }); 61 | }); 62 | 63 | // Listen for futher state changes 64 | FlutterBluetoothSerial.instance 65 | .onStateChanged() 66 | .listen((BluetoothState state) { 67 | setState(() { 68 | _bluetoothState = state; 69 | 70 | // Discoverable mode is disabled when Bluetooth gets disabled 71 | _discoverableTimeoutTimer = null; 72 | _discoverableTimeoutSecondsLeft = 0; 73 | }); 74 | }); 75 | } 76 | 77 | @override 78 | void dispose() { 79 | FlutterBluetoothSerial.instance.setPairingRequestHandler(null); 80 | _collectingTask?.dispose(); 81 | _discoverableTimeoutTimer?.cancel(); 82 | super.dispose(); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return Scaffold( 88 | appBar: AppBar( 89 | title: const Text('Flutter Bluetooth Serial'), 90 | ), 91 | body: Container( 92 | child: ListView( 93 | children: [ 94 | Divider(), 95 | // ListTile(title: const Text('General')), 96 | SwitchListTile( 97 | title: const Text('Habillitar Bluetooth'), 98 | value: _bluetoothState.isEnabled, 99 | onChanged: (bool value) { 100 | // Do the request and update with the true value then 101 | future() async { 102 | // async lambda seems to not working 103 | if (value) 104 | await FlutterBluetoothSerial.instance.requestEnable(); 105 | else 106 | await FlutterBluetoothSerial.instance.requestDisable(); 107 | } 108 | 109 | future().then((_) { 110 | setState(() {}); 111 | }); 112 | }, 113 | ), 114 | Divider(), 115 | SwitchListTile( 116 | title: const Text('Colocar Senha Automaticamente'), 117 | subtitle: const Text('Pin 1234'), 118 | value: _autoAcceptPairingRequests, 119 | onChanged: (bool value) { 120 | setState(() { 121 | _autoAcceptPairingRequests = value; 122 | }); 123 | if (value) { 124 | FlutterBluetoothSerial.instance.setPairingRequestHandler( 125 | (BluetoothPairingRequest request) { 126 | print("Colocar Senha Automaticamente"); 127 | if (request.pairingVariant == PairingVariant.Pin) { 128 | return Future.value("1234"); 129 | } 130 | return Future.value(null); 131 | }); 132 | } else { 133 | FlutterBluetoothSerial.instance 134 | .setPairingRequestHandler(null); 135 | } 136 | }, 137 | ), 138 | Divider(), 139 | ListTile(title: const Text('Conectar e Receber dados')), 140 | ListTile( 141 | title: ElevatedButton( 142 | child: ((_collectingTask?.inProgress ?? false) 143 | ? const Text('Desconecte e interrompa a coleta em segundo plano') 144 | : const Text('Conecte-se para iniciar a coleta em segundo plano')), 145 | onPressed: () async { 146 | if (_collectingTask?.inProgress ?? false) { 147 | await _collectingTask!.cancel(); 148 | setState(() { 149 | /* Update for `_collectingTask.inProgress` */ 150 | }); 151 | } else { 152 | final BluetoothDevice? selectedDevice = 153 | await Navigator.of(context).push( 154 | MaterialPageRoute( 155 | builder: (context) { 156 | return SelectBondedDevicePage( 157 | checkAvailability: false); 158 | }, 159 | ), 160 | ); 161 | 162 | if (selectedDevice != null) { 163 | await _startBackgroundTask(context, selectedDevice); 164 | setState(() { 165 | /* Update for `_collectingTask.inProgress` */ 166 | }); 167 | } 168 | } 169 | }, 170 | ), 171 | ), 172 | ListTile( 173 | title: ElevatedButton( 174 | child: const Text('Ver Dados Coletados'), 175 | onPressed: (_collectingTask != null) 176 | ? () async { 177 | final valor = await _collectingTask; 178 | print(valor); 179 | // Navigator.of(context).push( 180 | // MaterialPageRoute( 181 | // builder: (context) { 182 | // return ScopedModel( 183 | // model: _collectingTask!, 184 | // child: BackgroundCollectedPage(), 185 | // ); 186 | // }, 187 | // ), 188 | // ); 189 | } 190 | : null, 191 | ), 192 | ), 193 | ], 194 | ), 195 | ), 196 | ); 197 | } 198 | 199 | void _startChat(BuildContext context, BluetoothDevice server) { 200 | Navigator.of(context).push( 201 | MaterialPageRoute( 202 | builder: (context) { 203 | return ChatPage(server: server); 204 | }, 205 | ), 206 | ); 207 | } 208 | 209 | Future _startBackgroundTask( 210 | BuildContext context, 211 | BluetoothDevice server, 212 | ) async { 213 | try { 214 | _collectingTask = await BackgroundCollectingTask.connect(server); 215 | await _collectingTask!.start(); 216 | } catch (ex) { 217 | _collectingTask?.cancel(); 218 | showDialog( 219 | context: context, 220 | builder: (BuildContext context) { 221 | return AlertDialog( 222 | title: const Text('Error occured while connecting'), 223 | content: Text("${ex.toString()}"), 224 | actions: [ 225 | new TextButton( 226 | child: new Text("Close"), 227 | onPressed: () { 228 | Navigator.of(context).pop(); 229 | }, 230 | ), 231 | ], 232 | ); 233 | }, 234 | ); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /example/lib/SelectBondedDevicePage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; 5 | 6 | import './BluetoothDeviceListEntry.dart'; 7 | 8 | class SelectBondedDevicePage extends StatefulWidget { 9 | /// If true, on page start there is performed discovery upon the bonded devices. 10 | /// Then, if they are not avaliable, they would be disabled from the selection. 11 | final bool checkAvailability; 12 | 13 | const SelectBondedDevicePage({this.checkAvailability = true}); 14 | 15 | @override 16 | _SelectBondedDevicePage createState() => new _SelectBondedDevicePage(); 17 | } 18 | 19 | enum _DeviceAvailability { 20 | no, 21 | maybe, 22 | yes, 23 | } 24 | 25 | class _DeviceWithAvailability { 26 | BluetoothDevice device; 27 | _DeviceAvailability availability; 28 | int? rssi; 29 | 30 | _DeviceWithAvailability(this.device, this.availability, [this.rssi]); 31 | } 32 | 33 | class _SelectBondedDevicePage extends State { 34 | List<_DeviceWithAvailability> devices = 35 | List<_DeviceWithAvailability>.empty(growable: true); 36 | 37 | // Availability 38 | StreamSubscription? _discoveryStreamSubscription; 39 | bool _isDiscovering = false; 40 | 41 | _SelectBondedDevicePage(); 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | 47 | _isDiscovering = widget.checkAvailability; 48 | 49 | if (_isDiscovering) { 50 | _startDiscovery(); 51 | } 52 | 53 | // Setup a list of the bonded devices 54 | FlutterBluetoothSerial.instance 55 | .getBondedDevices() 56 | .then((List bondedDevices) { 57 | setState(() { 58 | devices = bondedDevices 59 | .map( 60 | (device) => _DeviceWithAvailability( 61 | device, 62 | widget.checkAvailability 63 | ? _DeviceAvailability.maybe 64 | : _DeviceAvailability.yes, 65 | ), 66 | ) 67 | .toList(); 68 | }); 69 | }); 70 | } 71 | 72 | void _restartDiscovery() { 73 | setState(() { 74 | _isDiscovering = true; 75 | }); 76 | 77 | _startDiscovery(); 78 | } 79 | 80 | void _startDiscovery() { 81 | _discoveryStreamSubscription = 82 | FlutterBluetoothSerial.instance.startDiscovery().listen((r) { 83 | setState(() { 84 | Iterator i = devices.iterator; 85 | while (i.moveNext()) { 86 | var _device = i.current; 87 | if (_device.device == r.device) { 88 | _device.availability = _DeviceAvailability.yes; 89 | _device.rssi = r.rssi; 90 | } 91 | } 92 | }); 93 | }); 94 | 95 | _discoveryStreamSubscription?.onDone(() { 96 | setState(() { 97 | _isDiscovering = false; 98 | }); 99 | }); 100 | } 101 | 102 | @override 103 | void dispose() { 104 | // Avoid memory leak (`setState` after dispose) and cancel discovery 105 | _discoveryStreamSubscription?.cancel(); 106 | 107 | super.dispose(); 108 | } 109 | 110 | @override 111 | Widget build(BuildContext context) { 112 | List list = devices 113 | .map((_device) => BluetoothDeviceListEntry( 114 | device: _device.device, 115 | rssi: _device.rssi, 116 | enabled: _device.availability == _DeviceAvailability.yes, 117 | onTap: () { 118 | Navigator.of(context).pop(_device.device); 119 | }, 120 | )) 121 | .toList(); 122 | return Scaffold( 123 | appBar: AppBar( 124 | title: Text('Selecionar PLaca'), 125 | actions: [ 126 | _isDiscovering 127 | ? FittedBox( 128 | child: Container( 129 | margin: new EdgeInsets.all(16.0), 130 | child: CircularProgressIndicator( 131 | valueColor: AlwaysStoppedAnimation( 132 | Colors.white, 133 | ), 134 | ), 135 | ), 136 | ) 137 | : IconButton( 138 | icon: Icon(Icons.replay), 139 | onPressed: _restartDiscovery, 140 | ) 141 | ], 142 | ), 143 | body: ListView(children: list), 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /example/lib/helpers/LineChart.dart: -------------------------------------------------------------------------------- 1 | /// @name LineChart 2 | /// @version 0.0.5 3 | /// @description Simple line chart widget 4 | /// @author Patryk "PsychoX" Ludwikowski 5 | /// @license MIT License (see https://mit-license.org/) 6 | import 'dart:math' as math show min, max; 7 | import 'dart:ui' as ui; 8 | 9 | import 'package:flutter/material.dart'; 10 | 11 | import './PaintStyle.dart'; 12 | 13 | class LabelEntry { 14 | final double value; 15 | final String label; 16 | 17 | LabelEntry(this.value, this.label); 18 | } 19 | 20 | /// Widget that allows to show data on line chart. 21 | /// 22 | /// All arguments, values and labels data should be sorted! 23 | /// Since both the arguments and the values must be `double` type, 24 | /// be aware of the precision. 25 | class LineChart extends StatelessWidget { 26 | /// Constraints for the line chart. 27 | final BoxConstraints constraints; 28 | 29 | // @TODO ? Both `_LineChartPainter` and `LineChart` have most the same fields. 30 | // `LineChart` is just mainly passing them to the painter. Shouldn't there be 31 | // only one class containing these data? Some `LineChartData` forged inside here 32 | // and then passed and used by the painter? :thinking: 33 | 34 | /// Padding around main drawng area. Necessary for displaying labels (around the chart). 35 | final EdgeInsets padding; 36 | 37 | /* Arguments */ 38 | /// Collection of doubles as arguments. 39 | final Iterable arguments; 40 | 41 | /// Mappings of strings for doubles arguments, which allow to specify custom 42 | /// strings as labels for certain arguments. 43 | final Iterable argumentsLabels; 44 | 45 | /* Values */ 46 | /// Collection of data series as collections of next values on corresponding arguments. 47 | final Iterable> values; 48 | 49 | /// Mappings of string for doubles values, which allow to specify custom 50 | /// string as labels for certain values. 51 | final Iterable? valuesLabels; 52 | 53 | /* Labels & lines styles */ 54 | /// Style of horizontal lines labels 55 | final TextStyle? horizontalLabelsTextStyle; 56 | 57 | /// Style of vertical lines labels 58 | final TextStyle? verticalLabelsTextStyle; 59 | 60 | /// Defines style of horizontal lines. Might be null in order to prevent lines from drawing. 61 | final Paint? horizontalLinesPaint; 62 | 63 | /// Defines style of vertical lines. Might be null in order to prevent lines from drawing. 64 | final Paint? verticalLinesPaint; 65 | 66 | // @TODO . expose it 67 | final bool snapToLeftLabel = false; 68 | final bool snapToTopLabel = true; 69 | final bool snapToRightLabel = false; 70 | final bool snapToBottomLabel = true; 71 | 72 | /* Series points & lines styles */ 73 | /// List of paint styles for series values points. 74 | /// 75 | /// On whole list null would use predefined set of styles. 76 | /// On list entry null there will be no points for certain series. 77 | final List seriesPointsPaints; 78 | 79 | /// List of paint styles for lines between next series points. 80 | /// 81 | /// On null there will be no lines. 82 | final List? seriesLinesPaints; 83 | 84 | final double additionalMinimalHorizontalLabelsInterval; 85 | final double additionalMinimalVerticalLablesInterval; 86 | 87 | LineChart({ 88 | required this.constraints, 89 | this.padding = const EdgeInsets.fromLTRB(32, 12, 20, 28), 90 | required this.arguments, 91 | required this.argumentsLabels, 92 | required this.values, 93 | this.valuesLabels, 94 | this.horizontalLabelsTextStyle, 95 | this.verticalLabelsTextStyle, 96 | PaintStyle horizontalLinesStyle = const PaintStyle(color: Colors.grey), 97 | PaintStyle? verticalLinesStyle, // null for default 98 | 99 | this.additionalMinimalHorizontalLabelsInterval = 8, 100 | this.additionalMinimalVerticalLablesInterval = 8, 101 | Iterable? 102 | seriesPointsStyles, // null would use predefined set of styles 103 | Iterable? seriesLinesStyles, // null for default 104 | }) : horizontalLinesPaint = horizontalLinesStyle.toPaint(), 105 | verticalLinesPaint = verticalLinesStyle?.toPaint(), 106 | seriesPointsPaints = _prepareSeriesPointsPaints(seriesPointsStyles), 107 | seriesLinesPaints = _prepareSeriesLinesPaints(seriesLinesStyles) { 108 | if ((seriesPointsStyles?.length ?? values.length) < values.length && 109 | 12 /* default paints */ < values.length) { 110 | throw "Too few `seriesPointsPaintStyle`s! Try define more or limit number of displayed series"; 111 | } 112 | if ((seriesLinesStyles?.length ?? values.length) < values.length) { 113 | throw "Too few `seriesLinesStyles`s! Try define more or limit number of displayed series"; 114 | } 115 | } 116 | 117 | static List _prepareSeriesPointsPaints( 118 | Iterable? seriesPointsStyles) { 119 | if (seriesPointsStyles == null) { 120 | // Default paint for points 121 | return List.unmodifiable([ 122 | PaintStyle(strokeWidth: 1.7, color: Colors.blue).toPaint(), 123 | PaintStyle(strokeWidth: 1.7, color: Colors.red).toPaint(), 124 | PaintStyle(strokeWidth: 1.7, color: Colors.yellow).toPaint(), 125 | PaintStyle(strokeWidth: 1.7, color: Colors.green).toPaint(), 126 | 127 | PaintStyle(strokeWidth: 1.7, color: Colors.purple).toPaint(), 128 | PaintStyle(strokeWidth: 1.7, color: Colors.deepOrange).toPaint(), 129 | PaintStyle(strokeWidth: 1.7, color: Colors.brown).toPaint(), 130 | PaintStyle(strokeWidth: 1.7, color: Colors.lime).toPaint(), 131 | 132 | PaintStyle(strokeWidth: 1.7, color: Colors.indigo).toPaint(), 133 | PaintStyle(strokeWidth: 1.7, color: Colors.pink).toPaint(), 134 | PaintStyle(strokeWidth: 1.7, color: Colors.amber).toPaint(), 135 | PaintStyle(strokeWidth: 1.7, color: Colors.teal).toPaint(), 136 | 137 | // For more, user should specify them :F 138 | ]); 139 | } else { 140 | return seriesPointsStyles.map((style) => style?.toPaint()).toList(); 141 | } 142 | } 143 | 144 | static List? _prepareSeriesLinesPaints( 145 | Iterable? seriesLinesStyles) { 146 | if (seriesLinesStyles == null) { 147 | return null; 148 | } else { 149 | return seriesLinesStyles.map((style) => style.toPaint()).toList(); 150 | } 151 | } 152 | 153 | @override 154 | Widget build(BuildContext context) { 155 | return ConstrainedBox( 156 | constraints: this.constraints, 157 | child: CustomPaint( 158 | painter: _LineChartPainter( 159 | padding: padding, 160 | arguments: arguments, 161 | argumentsLabels: argumentsLabels, 162 | values: values, 163 | valuesLabels: valuesLabels, 164 | horizontalLabelsTextStyle: 165 | horizontalLabelsTextStyle ?? Theme.of(context).textTheme.caption, 166 | verticalLabelsTextStyle: 167 | verticalLabelsTextStyle ?? Theme.of(context).textTheme.caption, 168 | horizontalLinesPaint: horizontalLinesPaint, 169 | verticalLinesPaint: verticalLinesPaint, 170 | additionalMinimalHorizontalLabelsInterval: 171 | additionalMinimalHorizontalLabelsInterval, 172 | additionalMinimalVerticalLablesInterval: 173 | additionalMinimalVerticalLablesInterval, 174 | seriesPointsPaints: seriesPointsPaints, 175 | seriesLinesPaints: seriesLinesPaints, 176 | ))); 177 | } 178 | } 179 | 180 | class _LineChartPainter extends CustomPainter { 181 | /// Padding around main drawng area. Necessary for displaying labels (around the chart). 182 | final EdgeInsets padding; 183 | 184 | /* Arguments */ 185 | /// Collection of doubles as arguments. 186 | final Iterable arguments; 187 | 188 | /// Mappings of strings for doubles arguments, which allow to specify custom 189 | /// strings as labels for certain arguments. 190 | final Iterable? argumentsLabels; 191 | 192 | /* Values */ 193 | /// Collection of data series as collections of next values on corresponding arguments. 194 | final Iterable> values; 195 | 196 | /// Mappings of string for doubles values, which allow to specify custom 197 | /// string as labels for certain values. 198 | final Iterable? valuesLabels; 199 | 200 | /* Labels & lines styles */ 201 | /// Style of horizontal lines labels 202 | final TextStyle? horizontalLabelsTextStyle; 203 | 204 | /// Style of vertical lines labels 205 | final TextStyle? verticalLabelsTextStyle; 206 | 207 | /// Defines style of horizontal lines. Might be null in order to prevent lines from drawing. 208 | final Paint? horizontalLinesPaint; 209 | 210 | /// Defines style of vertical lines. Might be null in order to prevent lines from drawing. 211 | final Paint? verticalLinesPaint; 212 | 213 | // @TODO . expose it 214 | final bool snapToLeftLabel = false; 215 | final bool snapToTopLabel = true; 216 | final bool snapToRightLabel = false; 217 | final bool snapToBottomLabel = true; 218 | 219 | /* Series points & lines styles */ 220 | /// Collection of paint styles for series values points. 221 | /// 222 | /// On whole argument null would use predefined set of styles. 223 | /// On collection entry null there will be no points for certain series. 224 | final Iterable seriesPointsPaints; 225 | 226 | /// Collection of paint styles for lines between next series points. 227 | /// 228 | /// On null there will be no lines. 229 | final Iterable? seriesLinesPaints; 230 | 231 | /* Runtime */ 232 | /// Minimal allowed interval between horizontal lines. Calculated from font size. 233 | final double minimalHorizontalLabelsInterval; 234 | 235 | /// Maximal value of all data series values 236 | double maxValue = -double.maxFinite; 237 | 238 | /// Minimal value of all data series values 239 | double minValue = double.maxFinite; 240 | 241 | double _minimalHorizontalRatio = 0; 242 | double _minimalVerticalRatio = 0; 243 | 244 | /// Creates `_LineChartPainter` (`CustomPainter`) with given data and styling. 245 | _LineChartPainter({ 246 | this.padding = const EdgeInsets.fromLTRB(40, 8, 8, 32), 247 | required this.arguments, 248 | required this.argumentsLabels, 249 | required this.values, 250 | required this.valuesLabels, 251 | required this.horizontalLabelsTextStyle, 252 | required this.verticalLabelsTextStyle, 253 | required this.horizontalLinesPaint, 254 | required this.verticalLinesPaint, 255 | double additionalMinimalHorizontalLabelsInterval = 8, 256 | double additionalMinimalVerticalLablesInterval = 8, 257 | required this.seriesPointsPaints, 258 | required this.seriesLinesPaints, 259 | }) : this.minimalHorizontalLabelsInterval = 260 | (horizontalLabelsTextStyle?.fontSize ?? 12) + 261 | additionalMinimalHorizontalLabelsInterval { 262 | // Find max & min values of data to be show 263 | for (Iterable series in values) { 264 | for (double value in series) { 265 | if (value > maxValue) { 266 | maxValue = value; 267 | } else if (value < minValue) { 268 | minValue = value; 269 | } 270 | } 271 | } 272 | 273 | if (valuesLabels != null) { 274 | // Find minimal vertical ratio to fit all provided values labels 275 | Iterator entry = valuesLabels!.iterator; 276 | entry.moveNext(); 277 | double lastValue = entry.current.value; 278 | 279 | while (entry.moveNext()) { 280 | final double goodRatio = 281 | minimalHorizontalLabelsInterval / (entry.current.value - lastValue); 282 | if (goodRatio > _minimalVerticalRatio) { 283 | _minimalVerticalRatio = goodRatio; 284 | } 285 | 286 | lastValue = entry.current.value; 287 | } 288 | } 289 | 290 | if (argumentsLabels != null) { 291 | // Find minimal horizontal ratio to fit all provided arguments labels 292 | Iterator entry = argumentsLabels!.iterator; 293 | entry.moveNext(); 294 | double lastValue = entry.current.value; 295 | double lastWidth = 296 | _getLabelTextPainter(entry.current.label, verticalLabelsTextStyle) 297 | .width; 298 | 299 | while (entry.moveNext()) { 300 | final double nextValue = entry.current.value; 301 | final double nextWidth = 302 | _getLabelTextPainter(entry.current.label, verticalLabelsTextStyle) 303 | .width; 304 | 305 | final double goodRatio = ((lastWidth + nextWidth) / 2 + 306 | additionalMinimalVerticalLablesInterval) / 307 | (nextValue - lastValue); 308 | if (goodRatio > _minimalHorizontalRatio) { 309 | _minimalHorizontalRatio = goodRatio; 310 | } 311 | 312 | lastValue = nextValue; 313 | lastWidth = nextWidth; 314 | } 315 | } 316 | } 317 | 318 | @override 319 | void paint(Canvas canvas, Size size) { 320 | final double width = size.width - padding.left - padding.right; 321 | final double height = size.height - padding.top - padding.bottom; 322 | 323 | /* Horizontal lines with labels */ 324 | double valuesOffset = 0; // @TODO ? could be used in future for scrolling 325 | double verticalRatio; 326 | 327 | { 328 | Iterable labels; 329 | 330 | // If no labels provided - generate them! 331 | if (valuesLabels == null) { 332 | final double optimalStepValue = 333 | _calculateOptimalStepValue(maxValue - minValue, height); 334 | int stepsNumber = 1; 335 | 336 | // Find bottom line value 337 | double bottomValue = 0; 338 | if (minValue > 0) { 339 | while (bottomValue < minValue) { 340 | bottomValue += optimalStepValue; 341 | } 342 | bottomValue -= optimalStepValue; 343 | } else { 344 | while (bottomValue > minValue) { 345 | bottomValue -= optimalStepValue; 346 | } 347 | } 348 | valuesOffset = bottomValue; 349 | 350 | // Find top line value 351 | double topValue = bottomValue; 352 | while (topValue < maxValue) { 353 | topValue += optimalStepValue; 354 | stepsNumber += 1; 355 | } 356 | 357 | // Set labels iterable from prepared generator 358 | Iterable generator(double optimalStepValue, int stepsNumber, 359 | [double value = 0.0]) sync* { 360 | //double value = _bottomValue; 361 | for (int i = 0; i < stepsNumber; i++) { 362 | yield LabelEntry( 363 | value, 364 | value 365 | .toString()); // @TODO , choose better precision based on optimal step value while parsing to string 366 | value += optimalStepValue; 367 | } 368 | } 369 | 370 | labels = generator(optimalStepValue, stepsNumber, bottomValue); 371 | 372 | if (!snapToTopLabel) { 373 | topValue = maxValue; 374 | } 375 | if (!snapToBottomLabel) { 376 | bottomValue = valuesOffset = minValue; 377 | } 378 | 379 | // Calculate vertical ratio of pixels per value 380 | // Note: There is no empty space already 381 | verticalRatio = height / (topValue - bottomValue); 382 | } 383 | // If labels provided - use them 384 | else { 385 | // Set labels iterable as the provided list 386 | labels = valuesLabels!; 387 | 388 | // Use minimal visible value as offset. 389 | // Note: `minValue` is calculated in constructor and includes miniaml labels values. 390 | valuesOffset = minValue; 391 | 392 | // Calculate vertical ratio of pixels per value 393 | // Note: `_minimalVerticalRatio` is calculated in constructor 394 | final double topValue = 395 | snapToTopLabel ? math.max(maxValue, labels.last.value) : maxValue; 396 | final double bottomValue = snapToBottomLabel 397 | ? math.min(minValue, labels.first.value) 398 | : minValue; 399 | final double noEmptySpaceRatio = height / (topValue - bottomValue); 400 | verticalRatio = math.max(_minimalVerticalRatio, noEmptySpaceRatio); 401 | } 402 | 403 | // Draw the horizontal lines and labels 404 | for (LabelEntry tuple in labels) { 405 | if (tuple.value < valuesOffset) continue; 406 | final double yOffset = (size.height - 407 | padding.bottom - 408 | (tuple.value - valuesOffset) * verticalRatio); 409 | if (yOffset < padding.top) break; 410 | 411 | // Draw line 412 | if (horizontalLinesPaint != null) { 413 | canvas.drawLine( 414 | Offset(padding.left, yOffset), 415 | Offset(size.width - padding.right, yOffset), 416 | horizontalLinesPaint!); 417 | } 418 | 419 | // Draw label 420 | TextPainter( 421 | text: TextSpan(text: tuple.label, style: horizontalLabelsTextStyle), 422 | textAlign: TextAlign.right, 423 | textDirection: TextDirection.ltr) 424 | ..layout(minWidth: padding.left - 4) 425 | ..paint( 426 | canvas, 427 | Offset( 428 | 0, 429 | yOffset - 430 | (horizontalLabelsTextStyle?.fontSize ?? 12) / 2 - 431 | 1)); 432 | } 433 | } 434 | 435 | /* Vertical lines with labels */ 436 | double argumentsOffset = 0; 437 | final double xOffsetLimit = size.width - padding.right; 438 | double horizontalRatio; 439 | 440 | { 441 | Iterable labels; 442 | 443 | // If no labels provided - generate them! 444 | if (argumentsLabels == null) { 445 | throw "not implemented"; 446 | // @TODO . after few hot days of thinking about the problem for 1-2 hour a day, I just gave up. 447 | // The hardest in the problem is that there must be trade-off between space for labels and max lines, 448 | // but keep in mind that the label values should be in some human-readable steps (0.5, 10, 0.02...). 449 | } 450 | // If labels provided - use them 451 | else { 452 | // Set labels iterable as the provided list 453 | labels = argumentsLabels!; 454 | 455 | // Use first visible argument as arguments offset 456 | argumentsOffset = labels.first.value; 457 | 458 | if (!snapToLeftLabel) { 459 | argumentsOffset = arguments.first; 460 | } 461 | 462 | // Calculate vertical ratio of pixels per value 463 | // Note: `_minimalHorizontalRatio` is calculated in constructor 464 | final double leftMost = snapToLeftLabel 465 | ? math.min(arguments.first, labels.first.value) 466 | : arguments.first; 467 | final double rightMost = snapToRightLabel 468 | ? math.max(arguments.last, labels.last.value) 469 | : arguments.last; 470 | final double noEmptySpaceRatio = width / (rightMost - leftMost); 471 | horizontalRatio = math.max(_minimalHorizontalRatio, noEmptySpaceRatio); 472 | } 473 | 474 | // Draw the vertical lines and labels 475 | for (LabelEntry tuple in labels) { 476 | if (tuple.value < argumentsOffset) continue; 477 | final double xOffset = 478 | padding.left + (tuple.value - argumentsOffset) * horizontalRatio; 479 | if (xOffset > xOffsetLimit) break; 480 | 481 | // Draw line 482 | if (verticalLinesPaint != null) { 483 | canvas.drawLine( 484 | Offset(xOffset, padding.top), 485 | Offset(xOffset, size.height - padding.bottom), 486 | verticalLinesPaint!); 487 | } 488 | 489 | // Draw label 490 | final TextPainter textPainter = TextPainter( 491 | text: TextSpan(text: tuple.label, style: verticalLabelsTextStyle), 492 | textDirection: TextDirection.ltr) 493 | ..layout(); 494 | textPainter.paint( 495 | canvas, 496 | Offset(xOffset - textPainter.width / 2, 497 | size.height - (verticalLabelsTextStyle?.fontSize ?? 12) - 8)); 498 | } 499 | } 500 | 501 | /* Points and lines between subsequent */ 502 | Iterator> series = values.iterator; 503 | Iterator linesPaints = seriesLinesPaints == null 504 | ? [].iterator 505 | : (seriesLinesPaints ?? []).iterator; 506 | Iterator pointsPaints = seriesPointsPaints.iterator; 507 | while (series.moveNext()) { 508 | List points = []; 509 | Iterator value = series.current.iterator; 510 | Iterator argument = arguments.iterator; 511 | while (value.moveNext()) { 512 | argument.moveNext(); 513 | if (value.current == null || value.current == double.nan) continue; 514 | 515 | if (argument.current < argumentsOffset) continue; 516 | final double xOffset = padding.left + 517 | (argument.current - argumentsOffset) * horizontalRatio; 518 | if (xOffset > xOffsetLimit) break; 519 | 520 | if (value.current! < valuesOffset) continue; 521 | final double yOffset = size.height - 522 | padding.bottom - 523 | (value.current! - valuesOffset) * verticalRatio; 524 | if (yOffset < padding.top) continue; 525 | 526 | points.add(Offset(xOffset, yOffset)); 527 | } 528 | 529 | // Lines 530 | if (linesPaints.moveNext() && linesPaints.current != null) { 531 | canvas.drawPath( 532 | Path()..addPolygon(points, false), linesPaints.current!); 533 | } 534 | 535 | // Points 536 | if (pointsPaints.moveNext() && pointsPaints.current != null) { 537 | canvas.drawPoints(ui.PointMode.points, points, pointsPaints.current!); 538 | } 539 | } 540 | } 541 | 542 | @override 543 | bool shouldRepaint(_LineChartPainter old) => 544 | (this.arguments != old.arguments || 545 | this.values != old.values || 546 | this.argumentsLabels != old.argumentsLabels || 547 | this.valuesLabels != old.valuesLabels || 548 | this.seriesPointsPaints != old.seriesPointsPaints || 549 | this.seriesLinesPaints != old.seriesLinesPaints || 550 | this.horizontalLabelsTextStyle != old.horizontalLabelsTextStyle || 551 | this.verticalLabelsTextStyle != old.verticalLabelsTextStyle || 552 | this.padding != old.padding // 553 | ); 554 | 555 | // ..., 0.01, 0.02, 0.05, 0.1, [0.125], 0.2, [0.25], 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, ... 556 | double _calculateOptimalStepValue(double valueRange, double height) { 557 | final int maxSteps = height ~/ minimalHorizontalLabelsInterval; 558 | if (maxSteps <= 0) { 559 | throw "invalid max lines!"; 560 | } 561 | double interval = valueRange / maxSteps; 562 | if (interval > 1) { 563 | int zeros = 0; 564 | while (interval >= 10) { 565 | interval = interval / 10; 566 | zeros += 1; 567 | } 568 | /**/ if (interval <= 1) { 569 | interval = 1; 570 | } else if (interval <= 2) { 571 | interval = 2; 572 | } else if (interval <= 5) { 573 | interval = 5; 574 | } 575 | for (; zeros-- != 0;) { 576 | interval *= 10; 577 | } 578 | } else { 579 | // @TODO ! not working at all for lower :C 580 | int zeros = 0; 581 | while (interval < 0) { 582 | interval = interval * 10; 583 | zeros += 1; 584 | } 585 | /**/ if (interval <= 1) { 586 | interval = 1; 587 | } else if (interval <= 2) { 588 | interval = 2; 589 | } else if (interval <= 5) { 590 | interval = 5; 591 | } 592 | for (; zeros-- != 0;) { 593 | interval /= 10; 594 | } 595 | } 596 | return interval; 597 | } 598 | 599 | TextPainter _getLabelTextPainter(String text, TextStyle? style) { 600 | return TextPainter( 601 | text: TextSpan(text: text, style: style), 602 | textDirection: TextDirection.ltr) 603 | ..layout(); 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /example/lib/helpers/PaintStyle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | /// A description of the style to use when drawing on a [Canvas]. 4 | /// 5 | /// Most APIs on [Canvas] take a [Paint] object to describe the style 6 | /// to use for that operation. [PaintStyle] allows to be const 7 | /// constructed and later in runtime forged into the [Paint] object. 8 | class PaintStyle { 9 | /// Whether to apply anti-aliasing to lines and images drawn on the 10 | /// canvas. 11 | /// 12 | /// Defaults to true. 13 | final bool isAntiAlias; 14 | 15 | // Must be kept in sync with the default in paint.cc. 16 | static const int _kColorDefault = 0xFF000000; 17 | 18 | /// The color to use when stroking or filling a shape. 19 | /// 20 | /// Defaults to opaque black. 21 | /// 22 | /// See also: 23 | /// 24 | /// * [style], which controls whether to stroke or fill (or both). 25 | /// * [colorFilter], which overrides [color]. 26 | /// * [shader], which overrides [color] with more elaborate effects. 27 | /// 28 | /// This color is not used when compositing. To colorize a layer, use 29 | /// [colorFilter]. 30 | final Color? color; 31 | 32 | // Must be kept in sync with the default in paint.cc. 33 | static final int _kBlendModeDefault = BlendMode.srcOver.index; 34 | 35 | /// A blend mode to apply when a shape is drawn or a layer is composited. 36 | /// 37 | /// The source colors are from the shape being drawn (e.g. from 38 | /// [Canvas.drawPath]) or layer being composited (the graphics that were drawn 39 | /// between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying 40 | /// the [colorFilter], if any. 41 | /// 42 | /// The destination colors are from the background onto which the shape or 43 | /// layer is being composited. 44 | /// 45 | /// Defaults to [BlendMode.srcOver]. 46 | /// 47 | /// See also: 48 | /// 49 | /// * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite 50 | /// the layer when [restore] is called. 51 | /// * [BlendMode], which discusses the user of [saveLayer] with [blendMode]. 52 | final BlendMode blendMode; 53 | 54 | /// Whether to paint inside shapes, the edges of shapes, or both. 55 | /// 56 | /// Defaults to [PaintingStyle.fill]. 57 | final PaintingStyle style; 58 | 59 | /// How wide to make edges drawn when [style] is set to 60 | /// [PaintingStyle.stroke]. The width is given in logical pixels measured in 61 | /// the direction orthogonal to the direction of the path. 62 | /// 63 | /// Defaults to 0.0, which correspond to a hairline width. 64 | final double strokeWidth; 65 | 66 | /// The kind of finish to place on the end of lines drawn when 67 | /// [style] is set to [PaintingStyle.stroke]. 68 | /// 69 | /// Defaults to [StrokeCap.butt], i.e. no caps. 70 | final StrokeCap strokeCap; 71 | 72 | /// The kind of finish to place on the joins between segments. 73 | /// 74 | /// This applies to paths drawn when [style] is set to [PaintingStyle.stroke], 75 | /// It does not apply to points drawn as lines with [Canvas.drawPoints]. 76 | /// 77 | /// Defaults to [StrokeJoin.miter], i.e. sharp corners. 78 | /// 79 | /// Some examples of joins: 80 | /// 81 | /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4} 82 | /// 83 | /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/round_join.mp4} 84 | /// 85 | /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/bevel_join.mp4} 86 | /// 87 | /// The centers of the line segments are colored in the diagrams above to 88 | /// highlight the joins, but in normal usage the join is the same color as the 89 | /// line. 90 | /// 91 | /// See also: 92 | /// 93 | /// * [strokeMiterLimit] to control when miters are replaced by bevels when 94 | /// this is set to [StrokeJoin.miter]. 95 | /// * [strokeCap] to control what is drawn at the ends of the stroke. 96 | /// * [StrokeJoin] for the definitive list of stroke joins. 97 | final StrokeJoin strokeJoin; 98 | 99 | // Must be kept in sync with the default in paint.cc. 100 | static const double _kStrokeMiterLimitDefault = 4.0; 101 | 102 | /// The limit for miters to be drawn on segments when the join is set to 103 | /// [StrokeJoin.miter] and the [style] is set to [PaintingStyle.stroke]. If 104 | /// this limit is exceeded, then a [StrokeJoin.bevel] join will be drawn 105 | /// instead. This may cause some 'popping' of the corners of a path if the 106 | /// angle between line segments is animated, as seen in the diagrams below. 107 | /// 108 | /// This limit is expressed as a limit on the length of the miter. 109 | /// 110 | /// Defaults to 4.0. Using zero as a limit will cause a [StrokeJoin.bevel] 111 | /// join to be used all the time. 112 | /// 113 | /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_0_join.mp4} 114 | /// 115 | /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4} 116 | /// 117 | /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_6_join.mp4} 118 | /// 119 | /// The centers of the line segments are colored in the diagrams above to 120 | /// highlight the joins, but in normal usage the join is the same color as the 121 | /// line. 122 | /// 123 | /// See also: 124 | /// 125 | /// * [strokeJoin] to control the kind of finish to place on the joins 126 | /// between segments. 127 | /// * [strokeCap] to control what is drawn at the ends of the stroke. 128 | final double strokeMiterLimit; 129 | 130 | /// A mask filter (for example, a blur) to apply to a shape after it has been 131 | /// drawn but before it has been composited into the image. 132 | /// 133 | /// See [MaskFilter] for details. 134 | final MaskFilter? maskFilter; 135 | 136 | /// Controls the performance vs quality trade-off to use when applying 137 | /// filters, such as [maskFilter], or when drawing images, as with 138 | /// [Canvas.drawImageRect] or [Canvas.drawImageNine]. 139 | /// 140 | /// Defaults to [FilterQuality.none]. 141 | // TODO(ianh): verify that the image drawing methods actually respect this 142 | final FilterQuality filterQuality; 143 | 144 | /// The shader to use when stroking or filling a shape. 145 | /// 146 | /// When this is null, the [color] is used instead. 147 | /// 148 | /// See also: 149 | /// 150 | /// * [Gradient], a shader that paints a color gradient. 151 | /// * [ImageShader], a shader that tiles an [Image]. 152 | /// * [colorFilter], which overrides [shader]. 153 | /// * [color], which is used if [shader] and [colorFilter] are null. 154 | final Shader? shader; 155 | 156 | /// A color filter to apply when a shape is drawn or when a layer is 157 | /// composited. 158 | /// 159 | /// See [ColorFilter] for details. 160 | /// 161 | /// When a shape is being drawn, [colorFilter] overrides [color] and [shader]. 162 | final ColorFilter? colorFilter; 163 | 164 | /// Whether the colors of the image are inverted when drawn. 165 | /// 166 | /// Inverting the colors of an image applies a new color filter that will 167 | /// be composed with any user provided color filters. This is primarily 168 | /// used for implementing smart invert on iOS. 169 | final bool invertColors; 170 | 171 | const PaintStyle({ 172 | this.isAntiAlias = true, 173 | this.color = const Color(_kColorDefault), 174 | this.blendMode = BlendMode.srcOver, 175 | this.style = PaintingStyle.fill, 176 | this.strokeWidth = 0.0, 177 | this.strokeCap = StrokeCap.butt, 178 | this.strokeJoin = StrokeJoin.miter, 179 | this.strokeMiterLimit = 4.0, 180 | this.maskFilter, // null 181 | this.filterQuality = FilterQuality.none, 182 | this.shader, // null 183 | this.colorFilter, // null 184 | this.invertColors = false, 185 | }); 186 | 187 | @override 188 | String toString() { 189 | final StringBuffer result = StringBuffer(); 190 | String semicolon = ''; 191 | result.write('PaintStyle('); 192 | if (style == PaintingStyle.stroke) { 193 | result.write('$style'); 194 | if (strokeWidth != 0.0) 195 | result.write(' ${strokeWidth.toStringAsFixed(1)}'); 196 | else 197 | result.write(' hairline'); 198 | if (strokeCap != StrokeCap.butt) result.write(' $strokeCap'); 199 | if (strokeJoin == StrokeJoin.miter) { 200 | if (strokeMiterLimit != _kStrokeMiterLimitDefault) 201 | result.write( 202 | ' $strokeJoin up to ${strokeMiterLimit.toStringAsFixed(1)}'); 203 | } else { 204 | result.write(' $strokeJoin'); 205 | } 206 | semicolon = '; '; 207 | } 208 | if (isAntiAlias != true) { 209 | result.write('${semicolon}antialias off'); 210 | semicolon = '; '; 211 | } 212 | if (color != const Color(_kColorDefault)) { 213 | if (color != null) 214 | result.write('$semicolon$color'); 215 | else 216 | result.write('${semicolon}no color'); 217 | semicolon = '; '; 218 | } 219 | if (blendMode.index != _kBlendModeDefault) { 220 | result.write('$semicolon$blendMode'); 221 | semicolon = '; '; 222 | } 223 | if (colorFilter != null) { 224 | result.write('${semicolon}colorFilter: $colorFilter'); 225 | semicolon = '; '; 226 | } 227 | if (maskFilter != null) { 228 | result.write('${semicolon}maskFilter: $maskFilter'); 229 | semicolon = '; '; 230 | } 231 | if (filterQuality != FilterQuality.none) { 232 | result.write('${semicolon}filterQuality: $filterQuality'); 233 | semicolon = '; '; 234 | } 235 | if (shader != null) { 236 | result.write('${semicolon}shader: $shader'); 237 | semicolon = '; '; 238 | } 239 | if (invertColors) result.write('${semicolon}invert: $invertColors'); 240 | result.write(')'); 241 | return result.toString(); 242 | } 243 | 244 | Paint toPaint() { 245 | Paint paint = Paint(); 246 | if (this.isAntiAlias != true) paint.isAntiAlias = this.isAntiAlias; 247 | if (this.color != const Color(_kColorDefault)) paint.color = this.color!; 248 | if (this.blendMode != BlendMode.srcOver) paint.blendMode = this.blendMode; 249 | if (this.style != PaintingStyle.fill) paint.style = this.style; 250 | if (this.strokeWidth != 0.0) paint.strokeWidth = this.strokeWidth; 251 | if (this.strokeCap != StrokeCap.butt) paint.strokeCap = this.strokeCap; 252 | if (this.strokeJoin != StrokeJoin.miter) paint.strokeJoin = this.strokeJoin; 253 | if (this.strokeMiterLimit != 4.0) 254 | paint.strokeMiterLimit = this.strokeMiterLimit; 255 | if (this.maskFilter != null) paint.maskFilter = this.maskFilter; 256 | if (this.filterQuality != FilterQuality.none) 257 | paint.filterQuality = this.filterQuality; 258 | if (this.shader != null) paint.shader = this.shader; 259 | if (this.colorFilter != null) paint.colorFilter = this.colorFilter; 260 | if (this.invertColors != false) paint.invertColors = this.invertColors; 261 | return paint; 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import './MainPage.dart'; 4 | 5 | void main() => runApp(new ExampleApplication()); 6 | 7 | class ExampleApplication extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return MaterialApp(home: MainPage()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_bluetooth_serial_example 2 | version: 0.4.0 3 | description: Demonstrates how to use the `flutter_bluetooth_serial` plugin. 4 | authors: 5 | - Patryk Ludwikowski 6 | homepage: https://github.com/edufolly/flutter_bluetooth_serial/tree/master/example/ 7 | 8 | environment: 9 | sdk: ">=2.12.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | scoped_model: ^2.0.0-nullsafety.0 16 | cupertino_icons: ^1.0.3 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | 22 | flutter_bluetooth_serial: 23 | path: ../ 24 | integration_test: 25 | sdk: flutter 26 | 27 | flutter: 28 | uses-material-design: true 29 | -------------------------------------------------------------------------------- /example/test_driver/integration_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:integration_test/integration_test_driver.dart'; 2 | 3 | Future main() => integrationDriver(); 4 | -------------------------------------------------------------------------------- /flutter_bluetooth_serial.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /flutter_bluetooth_serial_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaynrix/flutter_bluetooth2/0cdb246ff55220c5cb49b4d6f4e67deb35224573/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/FlutterBluetoothSerialPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface FlutterBluetoothSerialPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/FlutterBluetoothSerialPlugin.m: -------------------------------------------------------------------------------- 1 | #import "FlutterBluetoothSerialPlugin.h" 2 | 3 | @implementation FlutterBluetoothSerialPlugin 4 | + (void)registerWithRegistrar:(NSObject*)registrar { 5 | FlutterMethodChannel* channel = [FlutterMethodChannel 6 | methodChannelWithName:@"flutter_bluetooth_serial" 7 | binaryMessenger:[registrar messenger]]; 8 | FlutterBluetoothSerialPlugin* instance = [[FlutterBluetoothSerialPlugin alloc] init]; 9 | [registrar addMethodCallDelegate:instance channel:channel]; 10 | } 11 | 12 | - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 13 | if ([@"getPlatformVersion" isEqualToString:call.method]) { 14 | result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); 15 | } else { 16 | result(FlutterMethodNotImplemented); 17 | } 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /ios/flutter_bluetooth_serial.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'flutter_bluetooth_serial' 6 | s.version = '0.0.1' 7 | s.summary = 'A basic Flutter Bluetooth Serial' 8 | s.description = <<-DESC 9 | A basic Flutter Bluetooth Serial 10 | DESC 11 | s.homepage = 'http://example.com' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Your Company' => 'email@example.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | 19 | s.ios.deployment_target = '8.0' 20 | end 21 | 22 | -------------------------------------------------------------------------------- /lib/BluetoothBondState.dart: -------------------------------------------------------------------------------- 1 | part of flutter_bluetooth_serial; 2 | 3 | class BluetoothBondState { 4 | final int underlyingValue; 5 | final String stringValue; 6 | 7 | const BluetoothBondState.fromString(String string) 8 | : this.underlyingValue = (string == 'none' 9 | ? 10 10 | : string == 'bonding' 11 | ? 11 12 | : string == 'bonded' 13 | ? 12 14 | : -2 // Unknown, if not found valid 15 | ), 16 | this.stringValue = 17 | ((string == 'none' || string == 'bonding' || string == 'bonded' // 18 | ) 19 | ? string 20 | : 'unknown' // Unknown, if not found valid 21 | ); 22 | 23 | const BluetoothBondState.fromUnderlyingValue(int value) 24 | : this.underlyingValue = ((value >= 10 && value <= 12) 25 | ? value 26 | : 0 // Unknown, if not found valid 27 | ), 28 | this.stringValue = (value == 10 29 | ? 'none' 30 | : value == 11 31 | ? 'bonding' 32 | : value == 12 33 | ? 'bonded' 34 | : 'unknown' // Unknown, if not found valid 35 | ); 36 | 37 | String toString() => 'BluetoothBondState.$stringValue'; 38 | 39 | int toUnderlyingValue() => underlyingValue; 40 | 41 | static const unknown = BluetoothBondState.fromUnderlyingValue(0); 42 | static const none = BluetoothBondState.fromUnderlyingValue(10); 43 | static const bonding = BluetoothBondState.fromUnderlyingValue(11); 44 | static const bonded = BluetoothBondState.fromUnderlyingValue(12); 45 | 46 | operator ==(Object other) { 47 | return other is BluetoothBondState && 48 | other.underlyingValue == this.underlyingValue; 49 | } 50 | 51 | @override 52 | int get hashCode => underlyingValue.hashCode; 53 | 54 | bool get isBonded => this == bonded; 55 | } 56 | -------------------------------------------------------------------------------- /lib/BluetoothConnection.dart: -------------------------------------------------------------------------------- 1 | part of flutter_bluetooth_serial; 2 | 3 | /// Represents ongoing Bluetooth connection to remote device. 4 | class BluetoothConnection { 5 | // Note by PsychoX at 2019-08-19 while working on issue #60: 6 | // Fixed and then tested whole thing multiple times: 7 | // - [X] basic `connect`, use `output`/`input` and `disconnect` by local scenario, 8 | // - [X] fail on connecting to device that isn't listening, 9 | // - [X] working `output` if no `listen`ing to `input`, 10 | // - [X] closing by local if no `input` written, 11 | // - [X] closing by local if no `listen`ing to `input` (this was the #60), 12 | // - [X] closing by remote if no `input` written, 13 | // - [X] closing by remote if no `listen`ing to `input`, 14 | // It works, but library user is notifed either by error on `output.add` or 15 | // by observing `connection.isConnected`. In "normal" conditions user can 16 | // listen to `input` even just for the `onDone` to proper detect closing. 17 | // 18 | 19 | /// This ID identifies real full `BluetoothConenction` object on platform side code. 20 | final int? _id; 21 | 22 | final EventChannel _readChannel; 23 | late StreamSubscription _readStreamSubscription; 24 | late StreamController _readStreamController; 25 | 26 | /// Stream sink used to read from the remote Bluetooth device 27 | /// 28 | /// `.onDone` could be used to detect when remote device closes the connection. 29 | /// 30 | /// You should use some encoding to receive string in your `.listen` callback, for example `ascii.decode(data)` or `utf8.encode(data)`. 31 | Stream? input; 32 | 33 | /// Stream sink used to write to the remote Bluetooth device 34 | /// 35 | /// You should use some encoding to send string, for example `.add(ascii.encode('Hello!'))` or `.add(utf8.encode('Cześć!))`. 36 | late _BluetoothStreamSink output; 37 | 38 | /// Describes is stream connected. 39 | bool get isConnected => output.isConnected; 40 | 41 | BluetoothConnection._consumeConnectionID(int? id) 42 | : this._id = id, 43 | this._readChannel = 44 | EventChannel('${FlutterBluetoothSerial.namespace}/read/$id') { 45 | _readStreamController = StreamController(); 46 | 47 | _readStreamSubscription = 48 | _readChannel.receiveBroadcastStream().cast().listen( 49 | _readStreamController.add, 50 | onError: _readStreamController.addError, 51 | onDone: this.close, 52 | ); 53 | 54 | input = _readStreamController.stream; 55 | output = _BluetoothStreamSink(id); 56 | } 57 | 58 | /// Returns connection to given address. 59 | static Future toAddress(String? address) async { 60 | // Sorry for pseudo-factory, but `factory` keyword disallows `Future`. 61 | return BluetoothConnection._consumeConnectionID(await FlutterBluetoothSerial 62 | ._methodChannel 63 | .invokeMethod('connect', {"address": address})); 64 | } 65 | 66 | /// Should be called to make sure the connection is closed and resources are freed (sockets/channels). 67 | void dispose() { 68 | finish(); 69 | } 70 | 71 | /// Closes connection (rather immediately), in result should also disconnect. 72 | Future close() { 73 | return Future.wait([ 74 | output.close(), 75 | _readStreamSubscription.cancel(), 76 | (!_readStreamController.isClosed) 77 | ? _readStreamController.close() 78 | : Future.value(/* Empty future */) 79 | ], eagerError: true); 80 | } 81 | 82 | /// Closes connection (rather immediately), in result should also disconnect. 83 | @Deprecated('Use `close` instead') 84 | Future cancel() => this.close(); 85 | 86 | /// Closes connection (rather gracefully), in result should also disconnect. 87 | Future finish() async { 88 | await output.allSent; 89 | close(); 90 | } 91 | } 92 | 93 | /// Helper class for sending responses. 94 | class _BluetoothStreamSink extends StreamSink { 95 | final int? _id; 96 | 97 | /// Describes is stream connected. 98 | bool isConnected = true; 99 | 100 | /// Chain of features, the variable represents last of the futures. 101 | Future _chainedFutures = Future.value(/* Empty future :F */); 102 | 103 | late Future _doneFuture; 104 | 105 | /// Exception to be returend from `done` Future, passed from `add` function or related. 106 | dynamic exception; 107 | 108 | _BluetoothStreamSink(this._id) { 109 | // `_doneFuture` must be initialized here because `close` must return the same future. 110 | // If it would be in `done` get body, it would result in creating new futures every call. 111 | _doneFuture = Future(() async { 112 | // @TODO ? is there any better way to do it? xD this below is weird af 113 | while (this.isConnected) { 114 | await Future.delayed(Duration(milliseconds: 111)); 115 | } 116 | if (this.exception != null) { 117 | throw this.exception; 118 | } 119 | }); 120 | } 121 | 122 | /// Adds raw bytes to the output sink. 123 | /// 124 | /// The data is sent almost immediately, but if you want to be sure, 125 | /// there is `this.allSent` that provides future which completes when 126 | /// all added data are sent. 127 | /// 128 | /// You should use some encoding to send string, for example `ascii.encode('Hello!')` or `utf8.encode('Cześć!)`. 129 | /// 130 | /// Might throw `StateError("Not connected!")` if not connected. 131 | @override 132 | void add(Uint8List data) { 133 | if (!isConnected) { 134 | throw StateError("Not connected!"); 135 | } 136 | 137 | _chainedFutures = _chainedFutures.then((_) async { 138 | if (!isConnected) { 139 | throw StateError("Not connected!"); 140 | } 141 | 142 | await FlutterBluetoothSerial._methodChannel 143 | .invokeMethod('write', {'id': _id, 'bytes': data}); 144 | }).catchError((e) { 145 | this.exception = e; 146 | close(); 147 | }); 148 | } 149 | 150 | /// Unsupported - this ouput sink cannot pass errors to platfom code. 151 | @override 152 | void addError(Object error, [StackTrace? stackTrace]) { 153 | throw UnsupportedError( 154 | "BluetoothConnection output (response) sink cannot receive errors!"); 155 | } 156 | 157 | @override 158 | Future addStream(Stream stream) => Future(() async { 159 | // @TODO ??? `addStream`, "alternating simultaneous addition" problem (read below) 160 | // If `onDone` were called some time after last `add` to the stream (what is okay), 161 | // this `addStream` function might wait not for the last "own" addition to this sink, 162 | // but might wait for last addition at the moment of the `onDone`. 163 | // This can happen if user of the library would use another `add` related function 164 | // while `addStream` still in-going. We could do something about it, but this seems 165 | // not to be so necessary since `StreamSink` specifies that `addStream` should be 166 | // blocking for other forms of `add`ition on the sink. 167 | var completer = Completer(); 168 | stream.listen(this.add).onDone(completer.complete); 169 | await completer.future; 170 | await _chainedFutures; // Wait last* `add` of the stream to be fulfilled 171 | }); 172 | 173 | @override 174 | Future close() { 175 | isConnected = false; 176 | return this.done; 177 | } 178 | 179 | @override 180 | Future get done => _doneFuture; 181 | 182 | /// Returns a future which is completed when the sink sent all added data, 183 | /// instead of only if the sink got closed. 184 | /// 185 | /// Might fail with an error in case if some occured while sending the data. 186 | /// Typical error could be `StateError("Not connected!")` which could happen 187 | /// if disconnected in middle of sending (queued) `add`ed data. 188 | /// 189 | /// Otherwise, the returned future will complete when either: 190 | Future get allSent => Future(() async { 191 | // Simple `await` can't get job done here, because the `_chainedFutures` member 192 | // in one access time provides last Future, then `await`ing for it allows the library 193 | // user to add more futures on top of the waited-out Future. 194 | Future lastFuture; 195 | do { 196 | lastFuture = this._chainedFutures; 197 | await lastFuture; 198 | } while (lastFuture != this._chainedFutures); 199 | 200 | if (this.exception != null) { 201 | throw this.exception; 202 | } 203 | 204 | this._chainedFutures = 205 | Future.value(); // Just in case if Dart VM is retarded 206 | }); 207 | } 208 | -------------------------------------------------------------------------------- /lib/BluetoothDevice.dart: -------------------------------------------------------------------------------- 1 | part of flutter_bluetooth_serial; 2 | 3 | /// Represents information about the device. Could be out-of-date. // @TODO . add updating the info via some fn 4 | class BluetoothDevice { 5 | /// Broadcasted friendly name of the device. 6 | final String? name; 7 | 8 | /// MAC address of the device or identificator for platform system (if MAC addresses are prohibited). 9 | final String address; 10 | 11 | /// Type of the device (Bluetooth standard type). 12 | final BluetoothDeviceType type; 13 | 14 | /// Class of the device. 15 | //final BluetoothClass bluetoothClass // @TODO . !BluetoothClass! 16 | 17 | /// Describes is device connected. 18 | final bool isConnected; 19 | 20 | /// Bonding state of the device. 21 | final BluetoothBondState bondState; 22 | 23 | /// Tells whether the device is bonded (ready to secure connect). 24 | @Deprecated('Use `isBonded` instead') 25 | bool get bonded => bondState.isBonded; 26 | 27 | /// Tells whether the device is bonded (ready to secure connect). 28 | bool get isBonded => bondState.isBonded; 29 | 30 | /// Construct `BluetoothDevice` with given values. 31 | const BluetoothDevice({ 32 | this.name, 33 | required this.address, 34 | this.type = BluetoothDeviceType.unknown, 35 | this.isConnected = false, 36 | this.bondState = BluetoothBondState.unknown, 37 | }); 38 | 39 | /// Creates `BluetoothDevice` from map. 40 | /// 41 | /// Internally used to receive the object from platform code. 42 | factory BluetoothDevice.fromMap(Map map) { 43 | return BluetoothDevice( 44 | name: map["name"], 45 | address: map["address"]!, 46 | type: map["type"] != null 47 | ? BluetoothDeviceType.fromUnderlyingValue(map["type"]) 48 | : BluetoothDeviceType.unknown, 49 | isConnected: map["isConnected"] ?? false, 50 | bondState: map["bondState"] != null 51 | ? BluetoothBondState.fromUnderlyingValue(map["bondState"]) 52 | : BluetoothBondState.unknown, 53 | ); 54 | } 55 | 56 | /// Creates map from `BluetoothDevice`. 57 | Map toMap() => { 58 | "name": this.name, 59 | "address": this.address, 60 | "type": this.type.toUnderlyingValue(), 61 | "isConnected": this.isConnected, 62 | "bondState": this.bondState.toUnderlyingValue(), 63 | }; 64 | 65 | /// Compares for equality of this and other `BluetoothDevice`. 66 | /// 67 | /// In fact, only `address` is compared, since this is most important 68 | /// and unchangable information that identifies each device. 69 | operator ==(Object other) { 70 | return other is BluetoothDevice && other.address == this.address; 71 | } 72 | 73 | @override 74 | int get hashCode => address.hashCode; 75 | } 76 | -------------------------------------------------------------------------------- /lib/BluetoothDeviceType.dart: -------------------------------------------------------------------------------- 1 | part of flutter_bluetooth_serial; 2 | 3 | class BluetoothDeviceType { 4 | final int underlyingValue; 5 | final String stringValue; 6 | 7 | const BluetoothDeviceType.fromString(String string) 8 | : this.underlyingValue = (string == 'unknown' 9 | ? 0 10 | : string == 'classic' 11 | ? 1 12 | : string == 'le' 13 | ? 2 14 | : string == 'dual' 15 | ? 3 16 | : -2 // Unknown, if not found valid 17 | ), 18 | this.stringValue = ((string == 'unknown' || 19 | string == 'classic' || 20 | string == 'le' || 21 | string == 'dual' // 22 | ) 23 | ? string 24 | : 'unknown' // Unknown, if not found valid 25 | ); 26 | 27 | const BluetoothDeviceType.fromUnderlyingValue(int value) 28 | : this.underlyingValue = ((value >= 0 && value <= 3) 29 | ? value 30 | : 0 // Unknown, if not found valid 31 | ), 32 | this.stringValue = (value == 0 33 | ? 'unknown' 34 | : value == 1 35 | ? 'classic' 36 | : value == 2 37 | ? 'le' 38 | : value == 3 39 | ? 'dual' 40 | : 'unknown' // Unknown, if not found valid 41 | ); 42 | 43 | String toString() => 'BluetoothDeviceType.$stringValue'; 44 | 45 | int toUnderlyingValue() => underlyingValue; 46 | 47 | static const unknown = BluetoothDeviceType.fromUnderlyingValue(0); 48 | static const classic = BluetoothDeviceType.fromUnderlyingValue(1); 49 | static const le = BluetoothDeviceType.fromUnderlyingValue(2); 50 | static const dual = BluetoothDeviceType.fromUnderlyingValue(3); 51 | 52 | operator ==(Object other) { 53 | return other is BluetoothDeviceType && 54 | other.underlyingValue == this.underlyingValue; 55 | } 56 | 57 | @override 58 | int get hashCode => underlyingValue.hashCode; 59 | } 60 | -------------------------------------------------------------------------------- /lib/BluetoothDiscoveryResult.dart: -------------------------------------------------------------------------------- 1 | part of flutter_bluetooth_serial; 2 | 3 | class BluetoothDiscoveryResult { 4 | final BluetoothDevice device; 5 | final int rssi; 6 | 7 | BluetoothDiscoveryResult({ 8 | required this.device, 9 | this.rssi = 0, 10 | }); 11 | 12 | factory BluetoothDiscoveryResult.fromMap(Map map) { 13 | return BluetoothDiscoveryResult( 14 | device: BluetoothDevice.fromMap(map), 15 | rssi: map['rssi'] ?? 0, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/BluetoothPairingRequest.dart: -------------------------------------------------------------------------------- 1 | part of flutter_bluetooth_serial; 2 | 3 | /// Enum-like class for all types of pairing variants. 4 | class PairingVariant { 5 | final int underlyingValue; 6 | 7 | const PairingVariant._(this.underlyingValue); 8 | 9 | factory PairingVariant.fromUnderlyingValue(int? value) { 10 | switch (value) { 11 | case 0: 12 | return PairingVariant.Pin; 13 | case 1: 14 | return PairingVariant.Passkey; 15 | case 2: 16 | return PairingVariant.PasskeyConfirmation; 17 | case 3: 18 | return PairingVariant.Consent; 19 | case 4: 20 | return PairingVariant.DisplayPasskey; 21 | case 5: 22 | return PairingVariant.DisplayPin; 23 | case 6: 24 | return PairingVariant.OOB; 25 | case 7: 26 | return PairingVariant.Pin16Digits; 27 | default: 28 | return PairingVariant.Error; 29 | } 30 | } 31 | int toUnderlyingValue() => underlyingValue; 32 | 33 | String toString() { 34 | switch (underlyingValue) { 35 | case 0: 36 | return 'PairingVariant.Pin'; 37 | case 1: 38 | return 'PairingVariant.Passkey'; 39 | case 2: 40 | return 'PairingVariant.PasskeyConfirmation'; 41 | case 3: 42 | return 'PairingVariant.Consent'; 43 | case 4: 44 | return 'PairingVariant.DisplayPasskey'; 45 | case 5: 46 | return 'PairingVariant.DisplayPin'; 47 | case 6: 48 | return 'PairingVariant.OOB'; 49 | case 7: 50 | return 'PairingVariant.Pin16Digits'; 51 | default: 52 | return 'PairingVariant.Error'; 53 | } 54 | } 55 | 56 | static const Error = PairingVariant._(-1); 57 | static const Pin = PairingVariant._(0); 58 | static const Passkey = PairingVariant._(1); 59 | static const PasskeyConfirmation = PairingVariant._(2); 60 | static const Consent = PairingVariant._(3); 61 | static const DisplayPasskey = PairingVariant._(4); 62 | static const DisplayPin = PairingVariant._(5); 63 | static const OOB = PairingVariant._(6); 64 | static const Pin16Digits = PairingVariant._(7); 65 | 66 | // operator ==(Object other) { 67 | // return other is PairingVariant && other.underlyingValue == this.underlyingValue; 68 | // } 69 | 70 | // @override 71 | // int get hashCode => underlyingValue.hashCode; 72 | } 73 | 74 | /// Represents information about incoming pairing request 75 | class BluetoothPairingRequest { 76 | /// MAC address of the device or identificator for platform system (if MAC addresses are prohibited). 77 | final String? address; 78 | 79 | /// Variant of the pairing methods. 80 | final PairingVariant? pairingVariant; 81 | 82 | /// Passkey for confirmation. 83 | final int? passkey; 84 | 85 | /// Construct `BluetoothPairingRequest` with given values. 86 | const BluetoothPairingRequest({ 87 | this.address, 88 | this.pairingVariant, 89 | this.passkey, 90 | }); 91 | 92 | /// Creates `BluetoothPairingRequest` from map. 93 | /// Internally used to receive the object from platform code. 94 | factory BluetoothPairingRequest.fromMap(Map map) { 95 | return BluetoothPairingRequest( 96 | address: map['address'], 97 | pairingVariant: PairingVariant.fromUnderlyingValue(map['variant']), 98 | passkey: map['passkey'], 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/BluetoothState.dart: -------------------------------------------------------------------------------- 1 | part of flutter_bluetooth_serial; 2 | 3 | class BluetoothState { 4 | final int underlyingValue; 5 | final String stringValue; 6 | 7 | const BluetoothState.fromString(String string) 8 | : this.underlyingValue = (string == 'STATE_OFF' 9 | ? 10 10 | : string == 'STATE_TURNING_ON' 11 | ? 11 12 | : string == 'STATE_ON' 13 | ? 12 14 | : string == 'STATE_TURNING_OFF' 15 | ? 13 16 | : 17 | 18 | //ring == 'STATE_BLE_OFF' ? 10 : 19 | string == 'STATE_BLE_TURNING_ON' 20 | ? 14 21 | : string == 'STATE_BLE_ON' 22 | ? 15 23 | : string == 'STATE_BLE_TURNING_OFF' 24 | ? 16 25 | : string == 'ERROR' 26 | ? -1 27 | : -2 // Unknown, if not found valid 28 | ), 29 | this.stringValue = ((string == 'STATE_OFF' || 30 | string == 'STATE_TURNING_ON' || 31 | string == 'STATE_ON' || 32 | string == 'STATE_TURNING_OFF' || 33 | 34 | //ring == 'STATE_BLE_OFF' || 35 | string == 'STATE_BLE_TURNING_ON' || 36 | string == 'STATE_BLE_ON' || 37 | string == 'STATE_BLE_TURNING_OFF' || 38 | string == 'ERROR') 39 | ? string 40 | : 'UNKNOWN' // Unknown, if not found valid 41 | ); 42 | 43 | const BluetoothState.fromUnderlyingValue(int value) 44 | : this.underlyingValue = (((value >= 10 && value <= 16) || value == -1) 45 | ? value 46 | : -2 // Unknown, if not found valid 47 | ), 48 | this.stringValue = (value == 10 49 | ? 'STATE_OFF' 50 | : value == 11 51 | ? 'STATE_TURNING_ON' 52 | : value == 12 53 | ? 'STATE_ON' 54 | : value == 13 55 | ? 'STATE_TURNING_OFF' 56 | : 57 | 58 | //lue == 10 ? 'STATE_BLE_OFF' : // Just for symetry in code :F 59 | value == 14 60 | ? 'STATE_BLE_TURNING_ON' 61 | : value == 15 62 | ? 'STATE_BLE_ON' 63 | : value == 16 64 | ? 'STATE_BLE_TURNING_OFF' 65 | : value == -1 66 | ? 'ERROR' 67 | : 'UNKNOWN' // Unknown, if not found valid 68 | ); 69 | 70 | String toString() => 'BluetoothState.$stringValue'; 71 | 72 | int toUnderlyingValue() => underlyingValue; 73 | 74 | static const STATE_OFF = BluetoothState.fromUnderlyingValue(10); 75 | static const STATE_TURNING_ON = BluetoothState.fromUnderlyingValue(11); 76 | static const STATE_ON = BluetoothState.fromUnderlyingValue(12); 77 | static const STATE_TURNING_OFF = BluetoothState.fromUnderlyingValue(13); 78 | 79 | //atic const STATE_BLE_OFF = BluetoothState.STATE_OFF; // Just for symetry in code :F 80 | static const STATE_BLE_TURNING_ON = BluetoothState.fromUnderlyingValue(14); 81 | static const STATE_BLE_ON = BluetoothState.fromUnderlyingValue(15); 82 | static const STATE_BLE_TURNING_OFF = BluetoothState.fromUnderlyingValue(16); 83 | 84 | static const ERROR = BluetoothState.fromUnderlyingValue(-1); 85 | static const UNKNOWN = BluetoothState.fromUnderlyingValue(-2); 86 | 87 | operator ==(Object other) { 88 | return other is BluetoothState && 89 | other.underlyingValue == this.underlyingValue; 90 | } 91 | 92 | @override 93 | int get hashCode => underlyingValue.hashCode; 94 | 95 | bool get isEnabled => this == STATE_ON; 96 | } 97 | -------------------------------------------------------------------------------- /lib/FlutterBluetoothSerial.dart: -------------------------------------------------------------------------------- 1 | part of flutter_bluetooth_serial; 2 | 3 | class FlutterBluetoothSerial { 4 | // Plugin 5 | static const String namespace = 'flutter_bluetooth_serial'; 6 | 7 | static FlutterBluetoothSerial _instance = new FlutterBluetoothSerial._(); 8 | 9 | static FlutterBluetoothSerial get instance => _instance; 10 | 11 | static final MethodChannel _methodChannel = 12 | const MethodChannel('$namespace/methods'); 13 | 14 | FlutterBluetoothSerial._() { 15 | _methodChannel.setMethodCallHandler((MethodCall call) async { 16 | switch (call.method) { 17 | case 'handlePairingRequest': 18 | if (_pairingRequestHandler != null) { 19 | return _pairingRequestHandler!( 20 | BluetoothPairingRequest.fromMap(call.arguments)); 21 | } 22 | break; 23 | 24 | default: 25 | throw 'unknown common code method - not implemented'; 26 | } 27 | }); 28 | } 29 | 30 | /* Status */ 31 | /// Checks is the Bluetooth interface avaliable on host device. 32 | Future get isAvailable async => 33 | await _methodChannel.invokeMethod('isAvailable'); 34 | 35 | /// Describes is the Bluetooth interface enabled on host device. 36 | Future get isEnabled async => 37 | await _methodChannel.invokeMethod('isEnabled'); 38 | 39 | /// Checks is the Bluetooth interface enabled on host device. 40 | @Deprecated('Use `isEnabled` instead') 41 | Future get isOn async => await _methodChannel.invokeMethod('isOn'); 42 | 43 | static final EventChannel _stateChannel = 44 | const EventChannel('$namespace/state'); 45 | 46 | /// Allows monitoring the Bluetooth adapter state changes. 47 | Stream onStateChanged() => _stateChannel 48 | .receiveBroadcastStream() 49 | .map((data) => BluetoothState.fromUnderlyingValue(data)); 50 | 51 | /// State of the Bluetooth adapter. 52 | Future get state async => BluetoothState.fromUnderlyingValue( 53 | await _methodChannel.invokeMethod('getState')); 54 | 55 | /// Returns the hardware address of the local Bluetooth adapter. 56 | /// 57 | /// Does not work for third party applications starting at Android 6.0. 58 | Future get address => _methodChannel.invokeMethod("getAddress"); 59 | 60 | /// Returns the friendly Bluetooth name of the local Bluetooth adapter. 61 | /// 62 | /// This name is visible to remote Bluetooth devices. 63 | /// 64 | /// Does not work for third party applications starting at Android 6.0. 65 | Future get name => _methodChannel.invokeMethod("getName"); 66 | 67 | /// Sets the friendly Bluetooth name of the local Bluetooth adapter. 68 | /// 69 | /// This name is visible to remote Bluetooth devices. 70 | /// 71 | /// Valid Bluetooth names are a maximum of 248 bytes using UTF-8 encoding, 72 | /// although many remote devices can only display the first 40 characters, 73 | /// and some may be limited to just 20. 74 | /// 75 | /// Does not work for third party applications starting at Android 6.0. 76 | Future changeName(String name) => 77 | _methodChannel.invokeMethod("setName", {"name": name}); 78 | 79 | /* Adapter settings and general */ 80 | /// Tries to enable Bluetooth interface (if disabled). 81 | /// Probably results in asking user for confirmation. 82 | Future requestEnable() async => 83 | await _methodChannel.invokeMethod('requestEnable'); 84 | 85 | /// Tries to disable Bluetooth interface (if enabled). 86 | Future requestDisable() async => 87 | await _methodChannel.invokeMethod('requestDisable'); 88 | 89 | /// Opens the Bluetooth platform system settings. 90 | Future openSettings() async => 91 | await _methodChannel.invokeMethod('openSettings'); 92 | 93 | /* Discovering and bonding devices */ 94 | /// Checks bond state for given address (might be from system cache). 95 | Future getBondStateForAddress(String address) async { 96 | return BluetoothBondState.fromUnderlyingValue(await _methodChannel 97 | .invokeMethod('getDeviceBondState', {"address": address})); 98 | } 99 | 100 | /// Starts outgoing bonding (pairing) with device with given address. 101 | /// Returns true if bonded, false if canceled or failed gracefully. 102 | /// 103 | /// `pin` or `passkeyConfirm` could be used to automate the bonding process, 104 | /// using provided pin or confirmation if necessary. Can be used only if no 105 | /// pairing request handler is already registered. 106 | /// 107 | /// Note: `passkeyConfirm` will probably not work, since 3rd party apps cannot 108 | /// get `BLUETOOTH_PRIVILEGED` permission (at least on newest Androids). 109 | Future bondDeviceAtAddress(String address, 110 | {String? pin, bool? passkeyConfirm}) async { 111 | if (pin != null || passkeyConfirm != null) { 112 | if (_pairingRequestHandler != null) { 113 | throw "pairing request handler already registered"; 114 | } 115 | setPairingRequestHandler((BluetoothPairingRequest request) async { 116 | Future.delayed(Duration(seconds: 1), () { 117 | setPairingRequestHandler(null); 118 | }); 119 | if (pin != null) { 120 | switch (request.pairingVariant) { 121 | case PairingVariant.Pin: 122 | return pin; 123 | default: 124 | // Other pairing variant requested, ignoring pin 125 | break; 126 | } 127 | } 128 | if (passkeyConfirm != null) { 129 | switch (request.pairingVariant) { 130 | case PairingVariant.Consent: 131 | case PairingVariant.PasskeyConfirmation: 132 | return passkeyConfirm; 133 | default: 134 | // Other pairing variant requested, ignoring confirming 135 | break; 136 | } 137 | } 138 | // Other pairing variant used, cannot automate 139 | return null; 140 | }); 141 | } 142 | return await _methodChannel 143 | .invokeMethod('bondDevice', {"address": address}); 144 | } 145 | 146 | /// Removes bond with device with specified address. 147 | /// Returns true if unbonded, false if canceled or failed gracefully. 148 | /// 149 | /// Note: May not work at every Android device! 150 | Future removeDeviceBondWithAddress(String address) async => 151 | await _methodChannel 152 | .invokeMethod('removeDeviceBond', {'address': address}); 153 | 154 | // Function used as pairing request handler. 155 | Function? _pairingRequestHandler; 156 | 157 | /// Allows listening and responsing for incoming pairing requests. 158 | /// 159 | /// Various variants of pairing requests might require different returns: 160 | /// * `PairingVariant.Pin` or `PairingVariant.Pin16Digits` 161 | /// (prompt to enter a pin) 162 | /// - return string containing the pin for pairing 163 | /// - return `false` to reject. 164 | /// * `BluetoothDevice.PasskeyConfirmation` 165 | /// (user needs to confirm displayed passkey, no rewriting necessary) 166 | /// - return `true` to accept, `false` to reject. 167 | /// - there is `passkey` parameter available. 168 | /// * `PairingVariant.Consent` 169 | /// (just prompt with device name to accept without any code or passkey) 170 | /// - return `true` to accept, `false` to reject. 171 | /// 172 | /// If returned null, the request will be passed for manual pairing 173 | /// using default Android Bluetooth settings pairing dialog. 174 | /// 175 | /// Note: Accepting request variant of `PasskeyConfirmation` and `Consent` 176 | /// will probably fail, because it require Android `setPairingConfirmation` 177 | /// which requires `BLUETOOTH_PRIVILEGED` permission that 3rd party apps 178 | /// cannot acquire (at least on newest Androids) due to security reasons. 179 | /// 180 | /// Note: It is necessary to return from handler within 10 seconds, since 181 | /// Android BroadcastReceiver can wait safely only up to that duration. 182 | void setPairingRequestHandler( 183 | Future handler(BluetoothPairingRequest request)?) { 184 | if (handler == null) { 185 | _pairingRequestHandler = null; 186 | _methodChannel.invokeMethod('pairingRequestHandlingDisable'); 187 | return; 188 | } 189 | if (_pairingRequestHandler == null) { 190 | _methodChannel.invokeMethod('pairingRequestHandlingEnable'); 191 | } 192 | _pairingRequestHandler = handler; 193 | } 194 | 195 | /// Returns list of bonded devices. 196 | Future> getBondedDevices() async { 197 | final List list = await (_methodChannel.invokeMethod('getBondedDevices')); 198 | return list.map((map) => BluetoothDevice.fromMap(map)).toList(); 199 | } 200 | 201 | static final EventChannel _discoveryChannel = 202 | const EventChannel('$namespace/discovery'); 203 | 204 | /// Describes is the dicovery process of Bluetooth devices running. 205 | Future get isDiscovering async => 206 | await _methodChannel.invokeMethod('isDiscovering'); 207 | 208 | /// Starts discovery and provides stream of `BluetoothDiscoveryResult`s. 209 | Stream startDiscovery() async* { 210 | late StreamSubscription subscription; 211 | StreamController controller; 212 | 213 | controller = new StreamController( 214 | onCancel: () { 215 | // `cancelDiscovery` happens automaticly by platform code when closing event sink 216 | subscription.cancel(); 217 | }, 218 | ); 219 | 220 | await _methodChannel.invokeMethod('startDiscovery'); 221 | 222 | subscription = _discoveryChannel.receiveBroadcastStream().listen( 223 | controller.add, 224 | onError: controller.addError, 225 | onDone: controller.close, 226 | ); 227 | 228 | yield* controller.stream 229 | .map((map) => BluetoothDiscoveryResult.fromMap(map)); 230 | } 231 | 232 | /// Cancels the discovery 233 | Future cancelDiscovery() async => 234 | await _methodChannel.invokeMethod('cancelDiscovery'); 235 | 236 | /// Describes is the local device in discoverable mode. 237 | Future get isDiscoverable => 238 | _methodChannel.invokeMethod("isDiscoverable"); 239 | 240 | /// Asks for discoverable mode (probably always prompt for user interaction in fact). 241 | /// Returns number of seconds acquired or zero if canceled or failed gracefully. 242 | /// 243 | /// Duration might be capped to 120, 300 or 3600 seconds on some devices. 244 | Future requestDiscoverable(int durationInSeconds) async => 245 | await _methodChannel 246 | .invokeMethod("requestDiscoverable", {"duration": durationInSeconds}); 247 | 248 | /* Connecting and connection */ 249 | // Default connection methods 250 | BluetoothConnection? _defaultConnection; 251 | 252 | @Deprecated('Use `BluetoothConnection.isEnabled` instead') 253 | Future get isConnected async => Future.value( 254 | _defaultConnection == null ? false : _defaultConnection!.isConnected); 255 | 256 | @Deprecated('Use `BluetoothConnection.toAddress(device.address)` instead') 257 | Future connect(BluetoothDevice device) => 258 | connectToAddress(device.address); 259 | 260 | @Deprecated('Use `BluetoothConnection.toAddress(address)` instead') 261 | Future connectToAddress(String? address) => Future(() async { 262 | _defaultConnection = await BluetoothConnection.toAddress(address); 263 | }); 264 | 265 | @Deprecated( 266 | 'Use `BluetoothConnection.finish` or `BluetoothConnection.close` instead') 267 | Future disconnect() => _defaultConnection!.finish(); 268 | 269 | @Deprecated('Use `BluetoothConnection.input` instead') 270 | Stream? onRead() => _defaultConnection!.input; 271 | 272 | @Deprecated( 273 | 'Use `BluetoothConnection.output` with some decoding (such as `ascii.decode` for strings) instead') 274 | Future write(String message) { 275 | _defaultConnection!.output.add(utf8.encode(message) as Uint8List); 276 | return _defaultConnection!.output.allSent; 277 | } 278 | 279 | @Deprecated('Use `BluetoothConnection.output` instead') 280 | Future writeBytes(Uint8List message) { 281 | _defaultConnection!.output.add(message); 282 | return _defaultConnection!.output.allSent; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /lib/flutter_bluetooth_serial.dart: -------------------------------------------------------------------------------- 1 | library flutter_bluetooth_serial; 2 | 3 | import 'dart:async'; 4 | import 'dart:typed_data'; 5 | import 'dart:convert'; 6 | 7 | import 'package:flutter/services.dart'; 8 | 9 | part './BluetoothState.dart'; 10 | part './BluetoothBondState.dart'; 11 | part './BluetoothDeviceType.dart'; 12 | part './BluetoothDevice.dart'; 13 | part './BluetoothPairingRequest.dart'; 14 | part './BluetoothDiscoveryResult.dart'; 15 | part './BluetoothConnection.dart'; 16 | part './FlutterBluetoothSerial.dart'; 17 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_bluetooth_serial 2 | version: 0.4.0 3 | description: A basic Flutter Bluetooth Serial library 4 | authors: 5 | - Eduardo Folly 6 | - Martin Mauch 7 | - Patryk Ludwikowski 8 | homepage: https://github.com/edufolly/flutter_bluetooth_serial 9 | repository: https://github.com/edufolly/flutter_bluetooth_serial 10 | issue_tracker: https://github.com/edufolly/flutter_bluetooth_serial/issues 11 | 12 | environment: 13 | sdk: '>=2.12.0 <3.0.0' 14 | flutter: ">=1.17.0" 15 | 16 | dependencies: 17 | flutter: 18 | sdk: flutter 19 | 20 | flutter: 21 | plugin: 22 | platforms: 23 | android: 24 | package: io.github.edufolly.flutterbluetoothserial 25 | pluginClass: FlutterBluetoothSerialPlugin 26 | --------------------------------------------------------------------------------