diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 852d1d3..e2b8fc2 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -16,17 +16,19 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Note: This workflow uses the latest stable version of the Dart SDK. # You can specify other versions if desired, see documentation here: # https://github.com/dart-lang/setup-dart/blob/main/README.md - # - uses: dart-lang/setup-dart@v1 - - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 + - uses: dart-lang/setup-dart@v1 + with: + sdk: 3.1.3 - uses: subosito/flutter-action@v1 with: channel: 'stable' + flutter-version: 3.13.0 - name: Install dependencies run: flutter pub get @@ -37,7 +39,7 @@ jobs: # Consider passing '--fatal-infos' for slightly stricter analysis. - name: Analyze project source - run: flutter analyze + run: flutter analyze --no-fatal-infos --no-fatal-warnings # Your project will need to have tests in test/ and a dependency on # package:test for this step to succeed. Note that Flutter projects will diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml index e8943a4..91dae00 100644 --- a/.idea/libraries/Dart_SDK.xml +++ b/.idea/libraries/Dart_SDK.xml @@ -1,17 +1,25 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 5b3388c..50c2250 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,36 +1,79 @@ - - - - - - - - - - - - + + - - - - - + + + + + + + + + + + + + + + + + + + - - + + + - + + + + + + + - + + + + + + + + + 1667839562502 + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index a8c4ed0..0ea9197 100644 --- a/README.md +++ b/README.md @@ -109,32 +109,28 @@ myErg.monitorForWorkoutSummary().listen((workoutSummary) { ``` Each workout summary is a flutter object that contains the following fields: -- `Future timestamp` -- `Future workTime` -- `Future workDistance` -- `Future avgSPM` -- `Future endHeartRate` -- `Future avgHeartRate` -- `Future minHeartRate` -- `Future maxHeartRate` -- `Future avgDragFactor` -- `Future recoveryHeartRate` -- `Future workoutType` -- `Future avgPace` -- `Future intervalType` -- `Future intervalSize` -- `Future intervalCount` -- `Future totalCalories` -- `Future watts` -- `Future totalRestDistance` -- `Future intervalRestTime` -- `Future avgCalories` - -Futures are handy here since the erg can send back different data at different times. The primary reason for this is the `recoveryHeartRate` field which the erg sends after the user has been resting for 1 minute. If the workout is cancelled or the erg is turned off before the end of this minute, the data may never arrive. See #10. - -Overall this method of accessing workout summary data is not the most ideal, and is likely to change later if a better solution is found. See #11. - - +- `timestamp` +- `workTime` +- `workDistance` +- `avgSPM` +- `endHeartRate` +- `avgHeartRate` +- `minHeartRate` +- `maxHeartRate` +- `avgDragFactor` +- `recoveryHeartRate` +- `workoutType` +- `avgPace` +- `intervalType` +- `intervalSize` +- `intervalCount` +- `totalCalories` +- `watts` +- `totalRestDistance` +- `intervalRestTime` +- `avgCalories` + +The `recoveryHeartRate` field is one that the erg sends after the rower has been resting for 1 minute. If the workout is cancelled or the erg is turned off before the end of this minute, the data may never arrive. See #10. This is likely to change later if a better solution is found. See #11. diff --git a/c2bluetooth.iml b/c2bluetooth.iml index 6048a33..80f26f4 100644 --- a/c2bluetooth.iml +++ b/c2bluetooth.iml @@ -9,6 +9,15 @@ + + + + + + + + + diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 5a1c3ea..8c92756 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,9 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + // Any value starting with "flutter." get its value from + // the Flutter Gradle plugin. + compileSdk flutter.compileSdkVersion sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -35,8 +37,10 @@ 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 - targetSdkVersion 30 + // You can update the following value to match your application needs. + minSdk flutter.minSdkVersion + targetSdk flutter.targetSdkVersion + // You can set these values in the property declaration or use a variable 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..73a4d4d 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,11 @@ + + + + + + @@ -9,7 +15,8 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:exported="true"> - diff --git a/example/android/build.gradle b/example/android/build.gradle index 9b6ed06..fc94ae1 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.5.20' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { - delete rootProject.buildDir +tasks.register("clean", Delete) { + delete rootProject.layout.buildDirectory } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..cfe88f6 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9367d48..9625e10 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 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/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4877c65..fb03af2 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 = 50; 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,17 @@ path = Runner; sourceTree = ""; }; + F0E4278F0E8B08CD7FD47A28 /* Pods */ = { + isa = PBXGroup; + children = ( + B8D95075A9661C60A4AD023F /* Pods-Runner.debug.xcconfig */, + B8D80978C7D3224A89929ED7 /* Pods-Runner.release.xcconfig */, + 05A57BCE264DB95F63255FBC /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,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 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -197,6 +226,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 */ @@ -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 = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -290,7 +358,10 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.example.freshExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -346,7 +417,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 = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -395,7 +466,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 = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -414,7 +485,10 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.example.freshExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -433,7 +507,10 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.example.freshExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140c..3db53b6 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..7c3eefe 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -41,5 +43,17 @@ UIViewControllerBasedStatusBarAppearance + NSBonjourServices + + _dartobservatory._tcp + + NSBluetoothAlwaysUsageDescription + The app uses bluetooth to find, connect and transfer data between different devices + NSBluetoothPeripheralUsageDescription + The app uses bluetooth to find, connect and transfer data between different devices + UIBackgroundModes + + bluetooth-central + diff --git a/example/lib/main.dart b/example/lib/main.dart index 97d8f46..ca643be 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -49,12 +49,12 @@ class _SimpleErgViewState extends State { @override void initState() { super.initState(); - bleManager.init(); //ready to go! - - startScan(); + unawaited( + bleManager.init().then((_) => startScan()), + ); } - startScan() { + Future startScan() async { setState(() { displayText = "Start Scanning"; }); @@ -151,22 +151,10 @@ class _SimpleErgViewState extends State { }); targetDevice!.monitorForWorkoutSummary().listen((summary) { - print(summary); - //TODO: update this for futures - summary.workDistance.then((dist) { - setState(() { - displayText = "distance: $dist"; - }); - }); - summary.timestamp.then((time) { - setState(() { - displayText2 = "datetime: $time"; - }); - }); - summary.avgSPM.then((spm) { - setState(() { - displayText3 = "sr: $spm"; - }); + setState(() { + displayText = "distance: ${summary.workDistance}"; + displayText2 = "datetime: ${summary.timestamp}"; + displayText3 = "sr: ${summary.avgSPM}"; }); }); } diff --git a/lib/data/workoutsummary.dart b/lib/data/workoutsummary.dart index 3a9c0d9..0ecb5f4 100644 --- a/lib/data/workoutsummary.dart +++ b/lib/data/workoutsummary.dart @@ -7,110 +7,64 @@ import 'package:csafe_fitness/csafe_fitness.dart'; import '../helpers.dart'; import 'package:c2bluetooth/enums.dart'; +///Represents a data packet from Concept2 that is stamped with a date. +class TimestampedData { + DateTime timestamp; + + TimestampedData.fromBytes(Uint8List bytes) + : timestamp = Concept2DateExtension.fromBytes(bytes.sublist(0, 4)); +} + +///Represents a data packet from Concept2 that is stamped with a duration. + +class DurationstampedData { + Duration elapsedTime; + + DurationstampedData.fromBytes(Uint8List data) + : elapsedTime = Concept2DurationExtension.fromBytes(data.sublist(0, 3)); +} + /// Represents a summary of a completed workout /// /// This takes care of processesing the raw byte data from workout summary characteristics into easily accessible fields. This class also takes care of things like byte endianness, combining multiple high and low bytes .etc, allowing applications to access things in terms of flutter native types. -class WorkoutSummary { - Completer _timestamp = new Completer(); - Completer _workTime = new Completer(); - Completer _workDistance = new Completer(); - Completer _avgSPM = new Completer(); - Completer _endHeartRate = new Completer(); - Completer _avgHeartRate = new Completer(); - Completer _minHeartRate = new Completer(); - Completer _maxHeartRate = new Completer(); - Completer _avgDragFactor = new Completer(); - Completer _recoveryHeartRate = new Completer(); - Completer _workoutType = new Completer(); - Completer _avgPace = new Completer(); - Completer _intervalType = new Completer(); - Completer _intervalSize = new Completer(); - Completer _intervalCount = new Completer(); - Completer _totalCalories = new Completer(); - Completer _watts = new Completer(); - Completer _totalRestDistance = new Completer(); - Completer _intervalRestTime = new Completer(); - Completer _avgCalories = new Completer(); - - // external getters for clients to get futures for the data they want - Future get timestamp => _timestamp.future; - Future get workTime => _workTime.future; - Future get workDistance => _workDistance.future; - Future get avgSPM => _avgSPM.future; - Future get endHeartRate => _endHeartRate.future; - Future get avgHeartRate => _avgHeartRate.future; - Future get minHeartRate => _minHeartRate.future; - Future get maxHeartRate => _maxHeartRate.future; - Future get avgDragFactor => _avgDragFactor.future; - //recoveryHeartRate is sent as an amended packet later. zero is not valid - Future get recoveryHeartRate => _recoveryHeartRate.future; - Future get workoutType => _workoutType.future; - Future get avgPace => _avgPace.future; - Future get intervalType => _intervalType.future; - Future get intervalSize => _intervalSize.future; - Future get intervalCount => _intervalCount.future; - Future get totalCalories => _totalCalories.future; - Future get watts => _watts.future; - Future get totalRestDistance => _totalRestDistance.future; - Future get intervalRestTime => _intervalRestTime.future; - Future get avgCalories => _avgCalories.future; - - WorkoutSummary.fromBytes(Uint8List data) { - _setBasicBytes(data.sublist(0, 20)); - if (data.length > 20) { - _setExtendedBytes(data.sublist(20)); - } - } +class WorkoutSummary extends TimestampedData { + double workTime; + double workDistance; + int avgSPM; + int endHeartRate; + int avgHeartRate; + int minHeartRate; + int maxHeartRate; + int avgDragFactor; + late int recoveryHeartRate; + WorkoutType workoutType; + double avgPace; /// Construct a WorkoutSummary from the bytes returned from the erg - void _setBasicBytes(Uint8List data) { - _timestamp.completeIfNotAlready(Concept2DateExtension.fromBytes(data.sublist(0, 4))); - _workTime.completeIfNotAlready( - CsafeIntExtension.fromBytes(data.sublist(4, 7), endian: Endian.little) / - 100); //divide by 100 to convert to seconds - _workDistance.completeIfNotAlready(CsafeIntExtension.fromBytes(data.sublist(7, 10), - endian: Endian.little) / - 10); //divide by 10 to convert to meters - _avgSPM.completeIfNotAlready(data.elementAt(10)); - _endHeartRate.completeIfNotAlready(data.elementAt(11)); - _avgHeartRate.completeIfNotAlready(data.elementAt(12)); - _minHeartRate.completeIfNotAlready(data.elementAt(13)); - _maxHeartRate.completeIfNotAlready(data.elementAt(14)); - _avgDragFactor.completeIfNotAlready(data.elementAt(15)); + WorkoutSummary.fromBytes(Uint8List data) + : workTime = CsafeIntExtension.fromBytes(data.sublist(4, 7), + endian: Endian.little) / + 100, //divide by 100 to convert to seconds + workDistance = CsafeIntExtension.fromBytes(data.sublist(7, 10), + endian: Endian.little) / + 10, //divide by 10 to convert to meters + avgSPM = data.elementAt(10), + endHeartRate = data.elementAt(11), + avgHeartRate = data.elementAt(12), + minHeartRate = data.elementAt(13), + maxHeartRate = data.elementAt(14), + avgDragFactor = data.elementAt(15), + workoutType = WorkoutTypeExtension.fromInt(data.elementAt(17)), + avgPace = CsafeIntExtension.fromBytes(data.sublist(18, 20), + endian: Endian.little) / + 10, + super.fromBytes(data) { //recovery heart rate here int recHRVal = data.elementAt(16); // 0 is not a valid value here according to the spec if (recHRVal > 0) { - _recoveryHeartRate.completeIfNotAlready(recHRVal); + recoveryHeartRate = recHRVal; } - _workoutType.completeIfNotAlready(WorkoutTypeExtension.fromInt(data.elementAt(17))); - _avgPace.completeIfNotAlready(CsafeIntExtension.fromBytes(data.sublist(18, 20), - endian: Endian.little) / - 10); //{ - } - - void _setExtendedBytes(Uint8List data) { - // if (data.length > 20) { - // var timestamp2 = Concept2DateExtension.fromBytes(data.sublist(20, 24)); - // if (timestamp != timestamp2) { - // throw ArgumentError( - // "Bytes passed to WorkoutSummary from multiple characteristics must have the same timestamp"); - // } - _intervalType.completeIfNotAlready(IntervalTypeExtension.fromInt(data.elementAt(4))); - _intervalSize.completeIfNotAlready( - CsafeIntExtension.fromBytes(data.sublist(5, 7), endian: Endian.little)); - _intervalCount.completeIfNotAlready(data.elementAt(7)); - _totalCalories.completeIfNotAlready(CsafeIntExtension.fromBytes(data.sublist(8, 10), - endian: Endian.little)); - _watts.completeIfNotAlready(CsafeIntExtension.fromBytes(data.sublist(10, 12), - endian: Endian.little)); - _totalRestDistance.completeIfNotAlready(CsafeIntExtension.fromBytes( - data.sublist(12, 15), - endian: Endian.little)); - _intervalRestTime.completeIfNotAlready(CsafeIntExtension.fromBytes(data.sublist(15, 17), - endian: Endian.little)); - _avgCalories.completeIfNotAlready(CsafeIntExtension.fromBytes(data.sublist(17, 19), - endian: Endian.little)); } @override @@ -120,3 +74,39 @@ class WorkoutSummary { "distance: $workDistance, " "avgSPM: $avgSPM)"; } + +class WorkoutSummary2 extends TimestampedData { + IntervalType intervalType; + int intervalSize; + int intervalCount; + int totalCalories; + int watts; + int totalRestDistance; + int intervalRestTime; + int avgCalories; + + WorkoutSummary2.fromBytes(Uint8List data) + : + // if (data.length > 20) { + // var timestamp2 = Concept2DateExtension.fromBytes(data.sublist(20, 24)); + // if (timestamp != timestamp2) { + // throw ArgumentError( + // "Bytes passed to WorkoutSummary from multiple characteristics must have the same timestamp"); + // } + + intervalType = IntervalTypeExtension.fromInt(data.elementAt(4)), + intervalSize = CsafeIntExtension.fromBytes(data.sublist(5, 7), + endian: Endian.little), + intervalCount = data.elementAt(7), + totalCalories = CsafeIntExtension.fromBytes(data.sublist(8, 10), + endian: Endian.little), + watts = CsafeIntExtension.fromBytes(data.sublist(10, 12), + endian: Endian.little), + totalRestDistance = CsafeIntExtension.fromBytes(data.sublist(12, 15), + endian: Endian.little), + intervalRestTime = CsafeIntExtension.fromBytes(data.sublist(15, 17), + endian: Endian.little), + avgCalories = CsafeIntExtension.fromBytes(data.sublist(17, 19), + endian: Endian.little), + super.fromBytes(data); +} diff --git a/lib/helpers.dart b/lib/helpers.dart index 1ed7d9c..86109f9 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -91,17 +91,3 @@ String wattsToSplit(double watts) { var split = durationToSplit(Duration(milliseconds: millis)); return split; } - -extension IdempotentCompleter on Completer { - void completeIfNotAlready([FutureOr? value]) { - if (!this.isCompleted) { - this.complete(value); - } - } - - void completeErrorIfNotAlready(Object error, [StackTrace? stackTrace]) { - if (!this.isCompleted) { - this.completeError(error, stackTrace); - } - } -} diff --git a/lib/models/ergblemanager.dart b/lib/models/ergblemanager.dart index ccc0a1b..919e131 100644 --- a/lib/models/ergblemanager.dart +++ b/lib/models/ergblemanager.dart @@ -6,8 +6,8 @@ class ErgBleManager { BleManager _manager = BleManager(); /// perform set up to get the Bluetooth client ready to scan for devices - void init() { - _manager.createClient(); + Future init() async { + await _manager.createClient(restoreStateIdentifier: "example-restore-state-identifier"); } /// Begin scanning for Ergs. diff --git a/pubspec.yaml b/pubspec.yaml index 5326386..f945a96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.1.6 repository: https://github.com/CrewLAB/c2bluetooth environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.15.0 <3.0.0" flutter: ">=1.17.0" dependencies: diff --git a/test/workoutsummary_test.dart b/test/workoutsummary_test.dart index 9832c59..6fdf89e 100644 --- a/test/workoutsummary_test.dart +++ b/test/workoutsummary_test.dart @@ -50,7 +50,7 @@ void main() { 0 ]; - List bothSets = basicBytes + extendedBytes; + // List bothSets = basicBytes + extendedBytes; test('can extract basic values from a workout summary byte list', () { final summary = WorkoutSummary.fromBytes(Uint8List.fromList(basicBytes)); @@ -65,14 +65,12 @@ void main() { expect(summary.avgDragFactor, 120); expect(summary.workoutType, WorkoutType.JUSTROW_SPLITS); expect(summary.avgPace, 10); - expect(summary.watts, null); }); - test( - 'can extract basic and extended values from a workout summary byte list', - () { - final summary = WorkoutSummary.fromBytes(Uint8List.fromList(bothSets)); - // expect(summary.timestamp, DateTime(2000, 0, 0, 0, 0)); + test('can extract extended values from a workout summary byte list', () { + final summary = + WorkoutSummary2.fromBytes(Uint8List.fromList(extendedBytes)); + expect(summary.timestamp, DateTime(2000, 0, 0, 0, 0)); expect(summary.intervalType, IntervalType.TIME); expect(summary.intervalSize, 255); expect(summary.intervalCount, 2); @@ -83,34 +81,34 @@ void main() { expect(summary.avgCalories, 100); }); - test('fails if it receives two different datetime values', () { - List modifiedDateBytes = [ - 42, - 0, - 0, - 0, - 0, - 255, - 0, - 2, - 34, - 0, - 196, - 0, - 72, - 0, - 0, - 55, - 0, - 100, - 0 - ]; + // test('fails if it receives two different datetime values', () { + // List modifiedDateBytes = [ + // 42, + // 0, + // 0, + // 0, + // 0, + // 255, + // 0, + // 2, + // 34, + // 0, + // 196, + // 0, + // 72, + // 0, + // 0, + // 55, + // 0, + // 100, + // 0 + // ]; - Uint8List differentSets = - Uint8List.fromList(basicBytes + modifiedDateBytes); + // Uint8List differentSets = + // Uint8List.fromList(basicBytes + modifiedDateBytes); - expect( - () => WorkoutSummary.fromBytes(differentSets), throwsArgumentError); - }); + // expect( + // () => WorkoutSummary.fromBytes(differentSets), throwsArgumentError); + // }); }); }