diff --git a/README.md b/README.md index 97b6130..f61067d 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ C2Bluetooth is a flutter package designed to provide an easy API for accessing data from Concept2 PM5 Indoor fitness machines via bluetooth. This library implements the [Concept2 Performance Monitor Bluetooth Smart Communications Interface Definition](https://www.concept2.com/files/pdf/us/monitors/PM5_BluetoothSmartInterfaceDefinition.pdf) Specification ([newer versions](https://www.c2forum.com/viewtopic.php?f=15&t=193697#p527068) are also available). It also relies heavily on the [CSAFE specification](https://web.archive.org/web/20060718175014/http://www.fitlinxx.com/csafe/specification.htm) from FitLinxx. ## Demo +This package comes with a demo app in the `example/` directory. -![A demo showing the distance completed after a workout](docs/images/demo/demo1-small.jpg) +See the [`example/README.md`](example/README.md) for more detailed information about the demo app and how to use it. -This is a relatively old screenshot of the included example app using an older version of the library to display the completed distance from a short 20-second test workout. Many improvements to expose more datapoints have been made since this screenshot was taken. -## Key Features +## Key Library Features Currently this library supports a few basic features such as: - retrieving workout summary information from the erg after a workout @@ -44,7 +44,6 @@ Similar to how the underlying bluetooth library works, pretty much everything be ```dart ErgBleManager bleManager = ErgBleManager(); -bleManager.init(); //ready to go! ``` ### Scanning for devices Next, you need to start scanning for available devices. This uses a Stream to return an instance of the `Ergometer` class for each erg found. Each of these instances represents an erg and should be stored for later reuse as these act as the base upon which everything else (retrieving data, sending workouts .etc) is based. @@ -54,27 +53,43 @@ Next, you need to start scanning for available devices. This uses a Stream to re ```dart Ergometer myErg; -bleManager.startErgScan().listen((erg) { +StreamSubscription ergScanStream = bleManager.startErgScan().listen((erg) { //your code for detecting an erg here. - myErg = erg + + //you can store the erg instance somewhere + myErg = erg; + + //or connect to it (see later examples) + + //or stop scanning + ergScanStream.cancel(); + + return erg; }); ``` This block of code is where you can do things like: - determine what erg(s) you want to work with (this can be based on name, user choice, or basicaly anything) - store the erg instance somewhere more permanent, like the `myErg` variable to allow you to be able to access it after you stop scanning. - - call `bleManager.stopErgScan()` if you know you are done scanning early. As an example, one way to immediately connect to the first erg found is to unconditionally call `stopErgScan` within this function so the scan stops after the first erg is received. Don't forget to close the stream too! + - cancel the stream if you are done scanning. ### Connecting and disconnecting -Once you have the `Ergometer` instance for the erg you want to connect to, you can call `connectAndDiscover()` on it to connect. +Once you have the `Ergometer` instance for the erg you want to connect to, you can call `connectAndDiscover()` on it to connect. This will provide you with a stream indicating the connection state of the erg. ```dart -await myErg.connectAndDiscover(); +StreamSubscription ergConnectionStream = myErg.connectAndDiscover().listen((event) { + if(event == ErgometerConnectionState.connected) { + //do stuff here once the erg is connected + } else if (event == ErgometerConnectionState.disconnected) { + //handle disconnection here + } + }); +} ``` -When you are done, make sure to disconnect from your erg: +When you are done, disconnect from your erg by cancelling the stream: ```dart -await myErg.disconnectOrCancel(); +ergConnectionStream.cancel(); ``` ### Getting data from the erg diff --git a/docs/API.md b/docs/API.md index cc54f1e..adad656 100644 --- a/docs/API.md +++ b/docs/API.md @@ -4,10 +4,14 @@ This document is the starting point for learning more about the c2bluetooth API - The broadest overview comes from using the API as documented in the README - sections like [Overall API Design](#overall-api-design) explain some of the core concepts or goals that we wanted to achieve with the API. -- For people looking to get into the internals of c2bluetooth, the [Core API Concepts](#core-api-concepts) section below is a good mid-level overview of the various groups or categories of classes that are used in the API and what their purpose is. +- For people looking to get into the internals of c2bluetooth, the [Core API Concepts](#core-api-concepts) section below is a good mid-level overview of the various groups or categories of classes that are used in the API and what their purpose is. - For summaries of how c2bluetooth works internally and all the things it "takes care of" for end users, see the [internals](internals.md) document - Obviously the most detailed explaination of how the code works comes from reading the code and inline comments themselves. It is helpful to understand the general goals first +## Terms used + +"implementor" generally refers to users of this library. This is intended to be an audience of primarily other flutter developers looking to use this library in their apps. + ## Overall API design @@ -15,20 +19,19 @@ This document is the starting point for learning more about the c2bluetooth API ### Inspiration In order for this library to be a good fit within the community and provide a good user experience for developers, the goal is to design the interface for this library after other existing libraries interfacing with Concept2 rowing machines. The libraries looked at were [Py3Row](https://github.com/droogmic/Py3Row) (Python, BSD-2), [BoutFitness/Concept2-SDK](https://github.com/BoutFitness/Concept2-SDK) (Swift, MIT), [ErgometerJS](https://github.com/tijmenvangulik/ErgometerJS) (Javascript, Apache 2). -There are likely more libraries like these available, but these were just the first few that were looked at based on a GitHub search. +There are likely more libraries like these available, but these were just the first few that were looked at based on a GitHub search at the time of writing. ### Object Oriented -These three examples all seem to use some kind of Class-based approach where a particular instance of an object represents a particular rowing machine and contains functions to make interaction with the machine easier, like getting data. +These three examples all seem to use some kind of object-oriented approach where a particular instance of an object represents a particular rowing machine and contains functions to make interaction with the machine easier, like getting data. -Designing the library in an object oriented way seemed to make the most sense given what other projects in the space seem to ave done. This should also should keep things relatively straightforward to program and maintain. +Designing the library in an object oriented way seemed to make the most sense given what other projects in the space seem to have done. This should also should keep things relatively straightforward to program and maintain. ### Subscription-based data access -Both BoutFitness/Concept2-SDK and ErgometerJS also seemed to have a way to asynchronously "subscribe" to get a callback when particular values change so the screen can be updated. Since the FlutterBleLib bluetooth library also exposes [Flutter streams](https://apgapg.medium.com/using-streams-in-flutter-62fed41662e4) for reading values in this way, it seems like a good choice to follow this model when exposing data about a PM5. +Both BoutFitness/Concept2-SDK and ErgometerJS also seemed to have a way to asynchronously "subscribe" to get a callback when particular values change so values being displayed on the screen in the implementors flutter app can be updated. Since many Flutter bluetooth libraries also expose notification data from bluetooth devices as [Flutter streams](https://apgapg.medium.com/using-streams-in-flutter-62fed41662e4), this seems like a good, clean way to expose data about a PM5. #### Single Values For getting single values from an erg, such as serial number, software and hardware version info, and other things that likely wont change that often, Streams may be unnecessary and it might be easier to have a simple synchronous interface for grabbing a value either from the erg or from somewhere in the memory allocated to the particular Erg object being used. -Whether or not this is actually a good solution is still TBD @@ -43,14 +46,14 @@ These concepts are roughly divided up into "external" (i.e. those that are part ### External Concepts #### Data Objects -Data objects, like the WorkoutSummary class, are essentially wrappers around data provided by the PM and allow the data to be accessed as an object by an application. +Data objects, such as the WorkoutSummary class, are essentially wrappers around data exposed by the PM (Performance Monitor)'s bluetooth interface. This makes it easier for applications to access this data by providing a more object-oriented interface. -Data objects are primarily one-way communication from a PM to your application. +Data objects are primarily a form of one-way communication from a PM to your application. -Data objects are located in the `data` directory and represent a large chunk of the public API for c2bluetooth +Data objects are located in the `data` directory and represent the parts of c2bluetooth's public API that are most likely to be useful to an application. #### Model Objects -This is a gairly general group of classes that represent various indoor rowing conceptsas objects for ease of use by applications looking to interact with ergs. Some examples of classses in this category are the `Ergometer` and `Workout` classes. Unlike Data Objects, they are intended to be able to enable bidirectional data flow. For example, an `Ergometer` object may have properties for getting data (like Data Objects) but also may contain methods like `sendWorkout()` that allow you to provide a `Workout` object to set up on the erg. `Workout` objects could also be returned by other methods as a way to represent a workout if needed. +This is a gairly general group of classes that represent various indoor rowing concepts (in the form of objects). Some examples of classses in this category are the `Ergometer` and `Workout` classes. Unlike Data Objects, they are intended to be able to enable bidirectional data flow. For example, an `Ergometer` object may have properties for getting data (such as Data Objects) but also may contain methods like `sendWorkout()` that allow you to provide a `Workout` object to set up on the erg. `Workout` objects could also be returned by other methods as a way to represent a workout. -Model objects are located in the `models` directory and represent a large chunk of the public API for c2bluetooth \ No newline at end of file +Model objects are located in the `models` directory. \ No newline at end of file diff --git a/docs/DesignDecisions.md b/docs/DesignDecisions.md index c9d198f..0b52632 100644 --- a/docs/DesignDecisions.md +++ b/docs/DesignDecisions.md @@ -1,7 +1,10 @@ # Design Decisions ## Bluetooth Library -This library ultimately depends on some bluetooth library to function. Originally the plan was to use [flutter_blue](https://github.com/pauldemarco/flutter_blue) because thats the first [tutorial](https://lupyuen.github.io/pinetime-rust-mynewt/articles/flutter#bluetooth-le-services) I came across. However, after seeing how many open issues and PR's they still have, the decline evident in their contributor graph, [comments online](https://www.reddit.com/r/FlutterDev/comments/hm63uk/why_bluetooth_in_flutter_is_so_problematic/), and [an analysis on cauldron.io](https://cauldron.io/project/5134), I've decided to use [FlutterBleLib](https://github.com/dotintent/FlutterBleLib) instead since, even though it seems similarly unmaintained, it has less open issues and seems to have reached a later stage of maturity based on its version number being in the 2.X range, rather than the 0.X range. +This library ultimately depends on some bluetooth library to function. Originally the plan was to use [flutter_blue](https://github.com/pauldemarco/flutter_blue) because thats the first [tutorial](https://lupyuen.github.io/pinetime-rust-mynewt/articles/flutter#bluetooth-le-services) I came across at the time development was started on c2bluetooth. However, after seeing how many open issues and PR's they still have, the decline evident in their contributor graph, [comments online](https://www.reddit.com/r/FlutterDev/comments/hm63uk/why_bluetooth_in_flutter_is_so_problematic/), and [an analysis on cauldron.io](https://cauldron.io/project/5134), [FlutterBleLib](https://github.com/dotintent/FlutterBleLib) was briefly used instead, before the project ultimately switched to using [flutter_reactive_ble](https://github.com/PhilipsHue/flutter_reactive_ble) mainteined by Philips Hue because it seems to be the most likely to continue to be maintained into the future. + + +During the transition from FlutterBleLib to flutter_reactive_ble creating an interface to represent any bluetooth library was considered because it would give implementors the ability to use a bluetooth library that may already exist in their app. This would halp maintainers reduce app dependencies, app size, and conflicting libraries, but was ultimately never implemented because it would make the process of debugging implementor-reported issues reported against the library more difficult. ## CSAFE API Usage diff --git a/docs/images/demo/completed.png b/docs/images/demo/completed.png new file mode 100644 index 0000000..deacd08 Binary files /dev/null and b/docs/images/demo/completed.png differ diff --git a/docs/images/demo/demo1-small.jpg b/docs/images/demo/demo1-small.jpg deleted file mode 100644 index 8c23263..0000000 Binary files a/docs/images/demo/demo1-small.jpg and /dev/null differ diff --git a/docs/images/demo/pre-scan.png b/docs/images/demo/pre-scan.png new file mode 100644 index 0000000..bb7f2a9 Binary files /dev/null and b/docs/images/demo/pre-scan.png differ diff --git a/docs/internals.md b/docs/internals.md index 72cb15e..399e8b4 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -3,10 +3,6 @@ This document is meant to be similar to the [API](API.md) document, but specific Only people interested in contributing to c2bluetooth should need to understand things at this level. -## Internal API Design - -TODO - ## Internal API Concepts #### Commands The command classes are based on the similarly named classes in the csafe-fitness library. There is a command superclass that is responsible for implementing general-purpose command structures from the relevant CSAFE/Concept2 specifications. These general command classes can then be subclassed to make clearly-named human readable shortcuts that pre-fill details like the identifier and command type while also performing validation of the command data. diff --git a/example/README.md b/example/README.md index 80716b9..be615e6 100644 --- a/example/README.md +++ b/example/README.md @@ -2,16 +2,23 @@ This is a sample app created from a fresh flutter project it is useful as a playground for experimenting with/testing the c2bluetooth library as it is being built as well as providing an example for how this could be used in an app -This app simply connects to the first erg that it sees. +Currently this app just connects to the first erg that it sees. An update is planned to make this a little more user-friendly for testing in environments with many ergs. + + ## Sample App capabilities ### Get workout summary information -1. build and run app. -2. long press app on android(samsung) home screen. click the info button, go to permissions and enable location permissions -3. confirm bluetooth is on -4. turn on PM and go to the screen where you would connect something like the ergdata app (usually this is a connect button on the main menu) -5. open/run the app. it should do some discovery and show you a stroke rate: 0 message -6. hit back on the erg and set up a piece. Recommended to set a 20 sec (minimum allowed) single time piece. -7. start the piece and take some strokes. after the piece is over you should see some data for the piece you completed appear on screen. feel free to modify the app to show other data points. +1. build and install the example app for your platform. +2. confirm bluetooth is on. +3. Accept any permission prompts you are given +4. turn on PM and go to the screen where you would connect something like the ergdata app (on newer firmware there will be a connect button on the main menu) +5. open/run the app. you should see a screen with a "Start scan" button. ![A demo screenshot showing the start scan button](../docs/images/demo/pre-scan.png) +6. Press this "start scan" button when you are ready to start scanning for ergs. You will see a few messages on the screen while it scans. Wait until the app says "setting up streams". +7. Use the back button on the erg to go back and set up a piece. A 20 sec (minimum allowed) single time piece is the shortest thing you can do that still works (just row pieces must be longer than 1 minute in order to be visible to the app and be saved in the PM's memory). +8. start the piece. after the piece is over you should see some data for the piece you completed appear on screen. ![A demo screenshot showing the results of a piece](../docs/images/demo/completed.png) +9. you are now ready to start making changes to the sample app to play around with the API and explore the other data points that are made available. + diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 5a1c3ea..8b1b141 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -35,7 +35,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.fresh_example" - minSdkVersion 18 + minSdkVersion 21 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 99c6fb1..88b2d57 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,19 @@ + + + + + + + + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle index 9b6ed06..1881064 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.21' repositories { google() jcenter() diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..2f24574 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,39 @@ +PODS: + - Flutter (1.0.0) + - permission_handler_apple (9.0.4): + - Flutter + - Protobuf (3.21.9) + - reactive_ble_mobile (0.0.1): + - Flutter + - Protobuf (~> 3.5) + - SwiftProtobuf (~> 1.0) + - SwiftProtobuf (1.20.3) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - reactive_ble_mobile (from `.symlinks/plugins/reactive_ble_mobile/ios`) + +SPEC REPOS: + trunk: + - Protobuf + - SwiftProtobuf + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + reactive_ble_mobile: + :path: ".symlinks/plugins/reactive_ble_mobile/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce + Protobuf: 02524ec14183fe08fb259741659e79683788158b + reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c + SwiftProtobuf: b02b5075dcf60c9f5f403000b3b0c202a11b6ae1 + +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 + +COCOAPODS: 1.11.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4877c65..3514c05 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 71E0CED985FD1762BCF30DDE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 534834C7320293612BB51C2B /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -29,9 +30,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 05A57BCE264DB95F63255FBC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 534834C7320293612BB51C2B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -42,6 +45,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B8D80978C7D3224A89929ED7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + B8D95075A9661C60A4AD023F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 71E0CED985FD1762BCF30DDE /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1E4F1236A3F1929EB4383FE3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 534834C7320293612BB51C2B /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +86,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + F0E4278F0E8B08CD7FD47A28 /* Pods */, + 1E4F1236A3F1929EB4383FE3 /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +114,16 @@ path = Runner; sourceTree = ""; }; + F0E4278F0E8B08CD7FD47A28 /* Pods */ = { + isa = PBXGroup; + children = ( + B8D95075A9661C60A4AD023F /* Pods-Runner.debug.xcconfig */, + B8D80978C7D3224A89929ED7 /* Pods-Runner.release.xcconfig */, + 05A57BCE264DB95F63255FBC /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +131,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + EBEC1DE42650B27D75C6CB9F /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + AECB0AC27820DCCC1649CDF1 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -127,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1340; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -197,6 +225,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + AECB0AC27820DCCC1649CDF1 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EBEC1DE42650B27D75C6CB9F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -254,6 +321,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -272,7 +340,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -288,10 +356,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UJPCPV3425; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.example.freshExample; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.adriancedwards.c2bluetooth.freshExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -322,6 +394,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -346,7 +419,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -377,6 +450,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -395,10 +469,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -414,8 +489,11 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.example.freshExample; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -433,8 +511,11 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.example.freshExample; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140c..a0f6453 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index f12a5c4..672d7e3 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -41,5 +41,19 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + NSBonjourServices + + _dartobservatory._tcp + + NSBluetoothAlwaysUsageDescription + The app uses bluetooth to connect and pull data from Concept2 Ergometer PM5 monitors + NSBluetoothPeripheralUsageDescription + The app uses bluetooth to connect and pull data from Concept2 Ergometer PM5 monitor + UIBackgroundModes + + bluetooth-central + diff --git a/example/lib/main.dart b/example/lib/main.dart index 97d8f46..94de161 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'dart:io'; import 'package:c2bluetooth/c2bluetooth.dart'; import 'package:c2bluetooth/models/workout.dart'; import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; void main() { runApp(MyApp()); @@ -49,30 +51,52 @@ class _SimpleErgViewState extends State { @override void initState() { super.initState(); - bleManager.init(); //ready to go! - - startScan(); + //startScan(); } - startScan() { - setState(() { - displayText = "Start Scanning"; - }); + startScan() async { + await Future.wait([ + Permission.location.request(), + Permission.locationWhenInUse.request() + ]).then((results) { + PermissionStatus locationPermission = results[0]; + PermissionStatus finePermission = results[1]; + if (Platform.isAndroid) { + if (locationPermission == PermissionStatus.granted && + finePermission == PermissionStatus.granted) { + return true; + } + } else if (Platform.isIOS) { + return true; + } + return false; + }).then((result) { + if (result) { + setState(() { + displayText = "Start Scanning"; + }); - scanSub = bleManager.startErgScan().listen((erg) { - //Scan one peripheral and stop scanning - print("Scanned Peripheral ${erg.name}"); + scanSub = bleManager.startErgScan().listen((erg) { + //Scan one peripheral and stop scanning + print("Scanned Peripheral ${erg.name}"); - stopScan(); - targetDevice = erg; - connectToDevice(); + stopScan(); + targetDevice = erg; + connectToDevice(); + }); + } else { + print( + 'Your device is experiencing a permission issue. Make sure you allow location services.'); + setState(() { + displayText = "Permission Issue Stopped Scanning"; + }); + } }); } stopScan() { scanSub?.cancel(); scanSub = null; - bleManager.stopErgScan(); } connectToDevice() async { @@ -82,41 +106,24 @@ class _SimpleErgViewState extends State { displayText = "Device Connecting"; }); - await targetDevice!.connectAndDiscover(); - - // if (!connected) { - // targetDevice! - // .observeConnectionState( - // emitCurrentValue: true, completeOnDisconnect: true) - // .listen((connectionState) { - // print( - // "Peripheral ${targetDevice!.name} connection state is $connectionState"); - // }); - // try { - // await targetDevice!.connect(); - // } catch (BleError) { - // print("a"); - // } - // print('CONNECTING'); - // } else { - // print('DEVICE Already CONNECTED'); - // } - // setState(() { - // displayText = "Device Connected"; - // }); - // discoverServices(); - subscribeToStreams(); + targetDevice!.connectAndDiscover().listen((event) { + if (event == ErgometerConnectionState.connected) { + subscribeToStreams(); + } + }); } setup2kH() async { if (targetDevice == null) return; + // ignore: deprecated_member_use targetDevice?.configure2kWorkout(); } setup10kH() async { if (targetDevice == null) return; + // ignore: deprecated_member_use targetDevice?.configure10kWorkout(); } @@ -132,17 +139,6 @@ class _SimpleErgViewState extends State { targetDevice?.configureWorkout(Workout.single(WorkoutGoal.meters(10000))); } - disconnectFromDevice() async { - if (targetDevice == null) return; - - // targetDevice!.disconnect(); - await targetDevice?.disconnectOrCancel(); - - setState(() { - displayText = "Device Disconnected"; - }); - } - subscribeToStreams() async { if (targetDevice == null) return; @@ -178,6 +174,15 @@ class _SimpleErgViewState extends State { title: Text("hello"), ), body: Column(children: [ + Visibility( + visible: scanSub == null && targetDevice == null, + child: ElevatedButton( + onPressed: () { + startScan(); + }, + child: Text("Start Scan"), + ), + ), Center( child: Text( displayText, @@ -217,7 +222,7 @@ class _SimpleErgViewState extends State { @override void dispose() { - disconnectFromDevice(); + //disconnectFromDevice(); bleManager .destroy(); //remember to release native resources when you're done! super.dispose(); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bc45844..c669235 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: c2bluetooth: path: ../ + permission_handler: ^10.2.0 dev_dependencies: flutter_test: diff --git a/lib/models/ergblemanager.dart b/lib/models/ergblemanager.dart index ccc0a1b..6fc3ca9 100644 --- a/lib/models/ergblemanager.dart +++ b/lib/models/ergblemanager.dart @@ -1,32 +1,22 @@ import 'package:c2bluetooth/constants.dart' as Identifiers; -import 'package:flutter_ble_lib_ios_15/flutter_ble_lib.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'ergometer.dart'; class ErgBleManager { - BleManager _manager = BleManager(); - - /// perform set up to get the Bluetooth client ready to scan for devices - void init() { - _manager.createClient(); - } + final _manager = FlutterReactiveBle(); /// Begin scanning for Ergs. /// /// This begins a scan for bluetooth devices with a filter applied so that only Concept2 Performance Monitors show up. - /// Bluetooth must be on and adequate permissions must be granted to the app for this to work. + /// Bluetooth must be on and adequate permissions must be granted for this to work. Stream startErgScan() { - return _manager.startPeripheralScan(uuids: [ - Identifiers.C2_ROWING_BASE_UUID - ]).map((scanResult) => Ergometer(scanResult.peripheral)); - } - - /// Stops scanning for ergs - Future stopErgScan() { - return _manager.stopPeripheralScan(); + return _manager.scanForDevices(withServices: [ + Uuid.parse(Identifiers.C2_ROWING_BASE_UUID) + ]).map((scanResult) => Ergometer(scanResult)); } /// Clean up/destroy/deallocate resources so that they are availalble again Future destroy() { - return _manager.destroyClient(); + return _manager.deinitialize(); } } diff --git a/lib/models/ergometer.dart b/lib/models/ergometer.dart index 8beef74..5b0e4fe 100644 --- a/lib/models/ergometer.dart +++ b/lib/models/ergometer.dart @@ -1,55 +1,70 @@ import 'dart:typed_data'; import 'package:c2bluetooth/c2bluetooth.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import '../internal/commands.dart'; import '../internal/datatypes.dart'; import 'package:csafe_fitness/csafe_fitness.dart'; import '../helpers.dart'; import 'workout.dart'; import 'package:c2bluetooth/constants.dart' as Identifiers; -import 'package:flutter_ble_lib_ios_15/flutter_ble_lib.dart'; import 'package:rxdart/rxdart.dart'; enum ErgometerConnectionState { connecting, connected, disconnected } class Ergometer { - Peripheral _peripheral; + final _flutterReactiveBle = FlutterReactiveBle(); + DiscoveredDevice _peripheral; Csafe? _csafeClient; /// Get the name of this erg. i.e. "PM5" + serial number - /// - /// Returns "Unknown" if the erg does not report a name - String get name => _peripheral.name ?? "Unknown"; + String get name => _peripheral.name; - /// Create an Ergometer from a FlutterBleLib peripheral object + /// Create an [Ergometer] from a discovered bluetooth device object /// - /// This is mainly intended for internal use + /// This is intended only for internal use by [ErgBleManager.startErgScan]. + /// Consider this method a private API that is subject to unannounced breaking + /// changes. There are likely much better methods to use for whatever you are trying to do. Ergometer(this._peripheral); /// Connect to this erg and discover the services and characteristics that it offers - Future connectAndDiscover() async { - await _peripheral.connect(); - await _peripheral.discoverAllServicesAndCharacteristics(); - + /// this returns a stream of [ErgometerConnectionState] events to enable monitoring the erg's connection state and disconnecting. + Stream connectAndDiscover() { + //having this first might cause problems _csafeClient = Csafe(_readCsafe, _writeCsafe); - } - /// Disconnect from this erg or cancel the connection - Future disconnectOrCancel() async { - return _peripheral.disconnectOrCancelConnection(); + //this may cause problems if the device goes out of range between scenning and trying to connect. maybe use connectToAdvertisingDevice instead to mitigate this and prevent a hang on android + + //if no services are specified in the `servicesWithCharacteristicsToDiscover` parameter, then full service discovery will be performed + return _flutterReactiveBle.connectToDevice(id: _peripheral.id).asyncMap((connectionStateUpdate) { + switch (connectionStateUpdate.connectionState) { + case DeviceConnectionState.connecting: + return ErgometerConnectionState.connecting; + case DeviceConnectionState.connected: + return ErgometerConnectionState.connected; + case DeviceConnectionState.disconnecting: + return ErgometerConnectionState.disconnected; + case DeviceConnectionState.disconnected: + return ErgometerConnectionState.disconnected; + default: + return ErgometerConnectionState.disconnected; + } + }); + } - /// Returns a stream of [WorkoutSummary] objects upon completion of any programmed piece or a "just row" piece that is longer than 1 minute. + /// Returns a stream of [WorkoutSummary] objects upon completion of any workout that would normally be saved to the Erg's memory. This includes any pre-programmed piece and any "just row" pieces longer than 1 minute. + @Deprecated("This API is being deprecated in an upcoming version") Stream monitorForWorkoutSummary() { - Stream ws1 = _peripheral - .monitorCharacteristic(Identifiers.C2_ROWING_PRIMARY_SERVICE_UUID, - Identifiers.C2_ROWING_END_OF_WORKOUT_SUMMARY_CHARACTERISTIC_UUID) - .asyncMap((datapoint) => datapoint.read()); + + var workoutSummaryCharacteristic1 = QualifiedCharacteristic(serviceId: Uuid.parse(Identifiers.C2_ROWING_PRIMARY_SERVICE_UUID), characteristicId: Uuid.parse(Identifiers.C2_ROWING_END_OF_WORKOUT_SUMMARY_CHARACTERISTIC_UUID), deviceId: _peripheral.id); + + var workoutSummaryCharacteristic2 = QualifiedCharacteristic(serviceId: Uuid.parse(Identifiers.C2_ROWING_PRIMARY_SERVICE_UUID), characteristicId: Uuid.parse(Identifiers.C2_ROWING_END_OF_WORKOUT_SUMMARY_CHARACTERISTIC2_UUID), deviceId: _peripheral.id); - Stream ws2 = _peripheral - .monitorCharacteristic(Identifiers.C2_ROWING_PRIMARY_SERVICE_UUID, - Identifiers.C2_ROWING_END_OF_WORKOUT_SUMMARY_CHARACTERISTIC2_UUID) - .asyncMap((datapoint) => datapoint.read()); + Stream ws1 = _flutterReactiveBle.subscribeToCharacteristic(workoutSummaryCharacteristic1).asyncMap((datapoint) => Uint8List.fromList(datapoint)); + + + Stream ws2 = _flutterReactiveBle.subscribeToCharacteristic(workoutSummaryCharacteristic2).asyncMap((datapoint) => Uint8List.fromList(datapoint)); return Rx.zip2(ws1, ws2, (Uint8List ws1Result, Uint8List ws2Result) { List combinedList = ws1Result.toList(); @@ -58,48 +73,32 @@ class Ergometer { }); } - /// Expose a stream of events to enable monitoring the erg's connection state - /// This acts as a wrapper around the state provided by the internal bluetooth library to aid with swapping it out later. - Stream monitorConnectionState() { - return _peripheral.observeConnectionState().asyncMap((connectionState) { - switch (connectionState) { - case PeripheralConnectionState.connecting: - return ErgometerConnectionState.connecting; - case PeripheralConnectionState.connected: - return ErgometerConnectionState.connected; - case PeripheralConnectionState.disconnecting: - return ErgometerConnectionState.disconnected; - case PeripheralConnectionState.disconnected: - return ErgometerConnectionState.disconnected; - default: - return ErgometerConnectionState.disconnected; - } - }); - } - - /// A read function for the PM over bluetooth. + /// An internal read function for accessing the PM's CSAFE API over bluetooth. /// - /// Intended for passing to the csafe_fitness library to allow it to read data from the erg + /// Intended for passing to the csafe_fitness library to allow it to read response data from the erg Stream _readCsafe() { - return _peripheral - .monitorCharacteristic(Identifiers.C2_ROWING_CONTROL_SERVICE_UUID, - Identifiers.C2_ROWING_PM_TRANSMIT_CHARACTERISTIC_UUID) - .asyncMap((datapoint) { - print("reading data: ${datapoint.value}"); - return datapoint.value; + var csafeRxCharacteristic = QualifiedCharacteristic(serviceId: Uuid.parse(Identifiers.C2_ROWING_CONTROL_SERVICE_UUID), characteristicId: Uuid.parse(Identifiers.C2_ROWING_PM_TRANSMIT_CHARACTERISTIC_UUID), deviceId: _peripheral.id); + + return _flutterReactiveBle.subscribeToCharacteristic(csafeRxCharacteristic).asyncMap((datapoint) => Uint8List.fromList(datapoint)).asyncMap((datapoint) { + print("reading data: $datapoint"); + return datapoint; }); } - /// A write function for the PM over bluetooth. + /// An internal write function for accessing the PM's CSAFE API over bluetooth. /// - /// Intended for passing to the csafe_fitness library to allow it to write data to the erg - Future _writeCsafe(Uint8List value) { - return _peripheral.writeCharacteristic( - Identifiers.C2_ROWING_CONTROL_SERVICE_UUID, - Identifiers.C2_ROWING_PM_RECEIVE_CHARACTERISTIC_UUID, - value, - true); - //.asyncMap((datapoint) => datapoint.read()); + /// Intended for passing to the csafe_fitness library to allow it to write commands to the erg + void _writeCsafe(Uint8List value) { + var csafeTxCharacteristic = QualifiedCharacteristic(serviceId: Uuid.parse(Identifiers.C2_ROWING_CONTROL_SERVICE_UUID), characteristicId: Uuid.parse(Identifiers.C2_ROWING_PM_RECEIVE_CHARACTERISTIC_UUID), deviceId: _peripheral.id); + + // return _peripheral.writeCharacteristic( + // Identifiers.C2_ROWING_CONTROL_SERVICE_UUID, + // Identifiers.C2_ROWING_PM_RECEIVE_CHARACTERISTIC_UUID, + // value, + // true); + // //.asyncMap((datapoint) => datapoint.read()); + + _flutterReactiveBle.writeCharacteristicWithResponse(csafeTxCharacteristic, value: value); } @Deprecated("This is a temporary function for development/experimentation and will be gone very soon") @@ -141,7 +140,7 @@ class Ergometer { /// Program a workout into the PM with particular parameters /// - ///Currently only the more basic of workout types are supported, such as basic single intervals, single distance, and single time pieces + /// Currently only the more basic of workout types are supported, such as basic single intervals, single distance, and single time pieces void configureWorkout(Workout workout, [bool startImmediately = true]) async { //Workout workout diff --git a/pubspec.yaml b/pubspec.yaml index 5326386..dab0f9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,9 +12,9 @@ dependencies: flutter: sdk: flutter - flutter_ble_lib_ios_15: ^2.5.2 csafe_fitness: ^0.1.0 rxdart: ^0.27.3 + flutter_reactive_ble: ^5.0.2 dev_dependencies: flutter_test: diff --git a/test/helpers_test.dart b/test/helpers_test.dart index 8474a59..adbdf45 100644 --- a/test/helpers_test.dart +++ b/test/helpers_test.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:c2bluetooth/helpers.dart'; import 'package:c2bluetooth/models/workout.dart'; import 'package:flutter_test/flutter_test.dart';