From 9cb676cfc17ad6ee911a18fc0cf02f1c28bdac7a Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Tue, 12 Dec 2023 21:37:30 +0100 Subject: [PATCH 01/15] Add Jump Height Test tab to apps --- open_earable/ios/Podfile.lock | 8 +------- open_earable/lib/apps_tab.dart | 11 +++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/open_earable/ios/Podfile.lock b/open_earable/ios/Podfile.lock index ea196ab..67f9ad9 100644 --- a/open_earable/ios/Podfile.lock +++ b/open_earable/ios/Podfile.lock @@ -5,8 +5,6 @@ PODS: - three3d_egl (~> 0.1.3) - flutter_native_splash (0.0.1): - Flutter - - location (0.0.1): - - Flutter - open_file (0.0.1): - Flutter - path_provider_foundation (0.0.1): @@ -26,7 +24,6 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_gl (from `.symlinks/plugins/flutter_gl/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - - location (from `.symlinks/plugins/location/ios`) - open_file (from `.symlinks/plugins/open_file/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -45,8 +42,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_gl/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" - location: - :path: ".symlinks/plugins/location/ios" open_file: :path: ".symlinks/plugins/open_file/ios" path_provider_foundation: @@ -60,7 +55,6 @@ SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_gl: 5a5603f35db897697f064027864a32b15d0c421d flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - location: d5cf8598915965547c3f36761ae9cc4f4e87d22e open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 @@ -71,4 +65,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 17e5a27..2787829 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -45,6 +45,17 @@ class AppsTab extends StatelessWidget { MaterialPageRoute( builder: (context) => Recorder(_openEarable))); }), + AppInfo( + iconData: Icons.arrow_upward, + title: "Jump Height Test", + description: "Test your maximum jump height.", + onTap: () { + Navigator.push( + context, + // TODO: Change PageRoute + MaterialPageRoute( + builder: (context) => Recorder(_openEarable))); + }), // ... similarly for other apps ]; } From 9265e593c3fe64673f8d13a4c8e221bc2c006937 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Tue, 12 Dec 2023 22:07:04 +0100 Subject: [PATCH 02/15] Change tab icon for Jump Height Test --- open_earable/lib/apps_tab.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 2787829..39ac81b 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; +import 'package:open_earable/apps/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -46,7 +47,7 @@ class AppsTab extends StatelessWidget { builder: (context) => Recorder(_openEarable))); }), AppInfo( - iconData: Icons.arrow_upward, + iconData: Icons.height, title: "Jump Height Test", description: "Test your maximum jump height.", onTap: () { @@ -54,7 +55,7 @@ class AppsTab extends StatelessWidget { context, // TODO: Change PageRoute MaterialPageRoute( - builder: (context) => Recorder(_openEarable))); + builder: (context) => JumpHeightTest(_openEarable))); }), // ... similarly for other apps ]; From 48f4d6b27eda9642221014c95388f6bdfa6f1365 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Thu, 21 Dec 2023 12:06:08 +0100 Subject: [PATCH 03/15] Implement basic functionality of Jump Height Test --- .../ios/Runner.xcodeproj/project.pbxproj | 18 +-- open_earable/ios/Runner/Info.plist | 119 ++++++++-------- open_earable/ios/Runner/Runner.entitlements | 5 +- .../ios/Runner/RunnerDebug.entitlements | 5 +- open_earable/lib/apps/jump_height_test.dart | 133 ++++++++++++++++++ 5 files changed, 201 insertions(+), 79 deletions(-) create mode 100644 open_earable/lib/apps/jump_height_test.dart diff --git a/open_earable/ios/Runner.xcodeproj/project.pbxproj b/open_earable/ios/Runner.xcodeproj/project.pbxproj index d696e14..dc894e3 100644 --- a/open_earable/ios/Runner.xcodeproj/project.pbxproj +++ b/open_earable/ios/Runner.xcodeproj/project.pbxproj @@ -480,7 +480,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -490,7 +490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; + PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -505,7 +505,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -525,7 +525,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -543,7 +543,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -669,7 +669,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -679,7 +679,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; + PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -696,7 +696,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -706,7 +706,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; + PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/open_earable/ios/Runner/Info.plist b/open_earable/ios/Runner/Info.plist index f5e62dd..18d65a8 100644 --- a/open_earable/ios/Runner/Info.plist +++ b/open_earable/ios/Runner/Info.plist @@ -1,66 +1,61 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - OpenEarable - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - OpenEarable - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - ITSAppUsesNonExemptEncryption - - LSApplicationCategoryType - aps-environment - LSRequiresIPhoneOS - - NSBluetoothAlwaysUsageDescription - This app uses bluetooth to connect to earable devices - NSBluetoothPeripheralUsageDescription - This app uses bluetooth to connect to earable devices - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - remote-notification - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - UIStatusBarHidden - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + OpenEarable + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + OpenEarable + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + + LSApplicationCategoryType + aps-environment + LSRequiresIPhoneOS + + NSBluetoothAlwaysUsageDescription + This app uses bluetooth to connect to earable devices + NSBluetoothPeripheralUsageDescription + This app uses bluetooth to connect to earable devices + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + diff --git a/open_earable/ios/Runner/Runner.entitlements b/open_earable/ios/Runner/Runner.entitlements index 903def2..0c67376 100644 --- a/open_earable/ios/Runner/Runner.entitlements +++ b/open_earable/ios/Runner/Runner.entitlements @@ -1,8 +1,5 @@ - - aps-environment - development - + diff --git a/open_earable/ios/Runner/RunnerDebug.entitlements b/open_earable/ios/Runner/RunnerDebug.entitlements index 903def2..0c67376 100644 --- a/open_earable/ios/Runner/RunnerDebug.entitlements +++ b/open_earable/ios/Runner/RunnerDebug.entitlements @@ -1,8 +1,5 @@ - - aps-environment - development - + diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart new file mode 100644 index 0000000..addb832 --- /dev/null +++ b/open_earable/lib/apps/jump_height_test.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +class JumpHeightTest extends StatefulWidget { + final OpenEarable _openEarable; + JumpHeightTest(this._openEarable); + @override + _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); +} + +class _JumpHeightTestState extends State { + DateTime? _startTime; + double _jumpHeight = 0.0; + bool _isJumping = false; + final OpenEarable _openEarable; + StreamSubscription? _imuSubscription; + _JumpHeightTestState(this._openEarable); + double _maxJumpHeight = 0.0; // Variable to keep track of maximum jump height + + + @override + void initState() { + super.initState(); + if (_openEarable.bleManager.connected) { + _setupListeners(); + } + } + + List _accelerations = []; // Store relative accelerations + double _lambda = 1.4; // Correction factor, adjust as needed + + _setupListeners() { + _imuSubscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + double currentAcc = double.parse(data["ACC"]["Y"].toString()); + + if (_accelerations.isNotEmpty) { + double relativeAcc = currentAcc - _accelerations.last; + _accelerations.add(relativeAcc); + } else { + _accelerations.add(currentAcc); + } + }); + } + + _calculateJumpHeight() { + double height = 0.0; + // TODO: timeSlice = 1 / samplingRate + double timeSlice = 0.04; // Ensure this matches your data sampling rate + + print("Acc length: ${_accelerations.length}"); // Debug log + for (int i = 0; i < _accelerations.length; i++) { + height += 0.5 * _accelerations[i] * timeSlice * timeSlice; + } + height *= _lambda; + + print("Calculated Height: $height"); // Debug log + + if (height > _maxJumpHeight) { + _maxJumpHeight = height; // Update max height if current height is greater + } + + setState(() { + _jumpHeight = height; + }); + } + + + @override + void dispose() { + super.dispose(); + _imuSubscription?.cancel(); + } + + void _startJump() { + _startTime = DateTime.now(); + setState(() { + _isJumping = true; + _maxJumpHeight = 0.0; // Reset max height on starting a new jump + }); + // Set sampling rate to maximum + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + } + + void _stopJump() { + if (_isJumping) { + // Calculate final jump height + _calculateJumpHeight(); + + // Resetting the state for the next jump + _accelerations.clear(); + setState(() { + _isJumping = false; + }); + } + // Here, _maxJumpHeight holds the maximum height reached during the jump + print("Maximum Jump Height: $_maxJumpHeight meters"); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Jump Height Test'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Jump Height: ${_jumpHeight.toStringAsFixed(2)} meters', + style: Theme.of(context).textTheme.headlineMedium, + ), + SizedBox(height: 20), + ElevatedButton( + onPressed: _isJumping ? _stopJump : _startJump, + child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + ), + ], + ), + ), + ); + } + + OpenEarableSensorConfig _buildSensorConfig() { + return OpenEarableSensorConfig( + sensorId: 0, + samplingRate: 30, + latency: 0, + ); + } +} \ No newline at end of file From 1c320200777f99d3b2283350ed07942133794d65 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 22:18:23 +0100 Subject: [PATCH 04/15] Add line chart for jump height data, add comments and make small changes --- open_earable/lib/apps/jump_height_test.dart | 226 ++++++++++++++++---- 1 file changed, 183 insertions(+), 43 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index addb832..84153df 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -1,105 +1,226 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:simple_kalman/simple_kalman.dart'; +import 'dart:math'; +/// An app that lets you test your jump height using an OpenEarable device. class JumpHeightTest extends StatefulWidget { final OpenEarable _openEarable; + + /// Constructs a JumpHeightTest widget with a given OpenEarable device. JumpHeightTest(this._openEarable); + @override _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); } +/// A class representing a jump with a time and height. +class Jump { + final DateTime _time; + final double _height; + + /// Constructs a Jump object with a time and height. + Jump(this._time, this._height); +} + +/// A stateless widget to display jump heights in a bar chart. +class HeightChart extends StatelessWidget { + final List _seriesList; + final bool _animate; + + /// Constructs a HeightChart widget with given series list and animate flag. + HeightChart(this._seriesList, {required bool animate}) : _animate = animate; + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + _seriesList.cast>(), + animate: _animate, + ); + } +} + +/// State class for JumpHeightTest widget. class _JumpHeightTestState extends State { + /// Stores the start time of a jump test. DateTime? _startTime; - double _jumpHeight = 0.0; + /// Current height calculated from sensor data. + double _height = 0.0; + // List to store each jump's data. + List _jumpData = []; + // Flag to indicate if jump measurement is ongoing. bool _isJumping = false; + /// Instance of OpenEarable device. final OpenEarable _openEarable; + /// Subscription to IMU sensor data. StreamSubscription? _imuSubscription; - _JumpHeightTestState(this._openEarable); - double _maxJumpHeight = 0.0; // Variable to keep track of maximum jump height + /// Stores the maximum height achieved in a jump. + double _maxHeight = 0.0; // Variable to keep track of maximum jump height + /// Error measure for Kalman filter. + final _errorMeasureAcc = 5.0; + /// Kalman filters for accelerometer data. + late SimpleKalman _kalmanX, _kalmanY, _kalmanZ; + /// Current velocity calculated from acceleration. + double _velocity = 0.0; + /// Sampling rate time slice (inverse of frequency). + double _timeSlice = 1 / 30.0; + /// Standard gravity in m/s^2. + double _gravity = 9.81; + /// X-axis acceleration. + double _accX = 0.0; + /// Y-axis acceleration. + double _accY = 0.0; + /// Z-axis acceleration. + double _accZ = 0.0; + /// Pitch angle in radians. + double _pitch = 0.0; + /// Constructs a _JumpHeightTestState object with a given OpenEarable device. + _JumpHeightTestState(this._openEarable); @override void initState() { super.initState(); + // Initialize Kalman filters. + _initializeKalmanFilters(); + // Set up listeners for sensor data. if (_openEarable.bleManager.connected) { _setupListeners(); } } - List _accelerations = []; // Store relative accelerations - double _lambda = 1.4; // Correction factor, adjust as needed + /// Initializes Kalman filters for accelerometer data. + void _initializeKalmanFilters() { + _kalmanX = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _kalmanY = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _kalmanZ = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + } + /// Sets up listeners to receive sensor data from the OpenEarable device. _setupListeners() { _imuSubscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { - double currentAcc = double.parse(data["ACC"]["Y"].toString()); - - if (_accelerations.isNotEmpty) { - double relativeAcc = currentAcc - _accelerations.last; - _accelerations.add(relativeAcc); - } else { - _accelerations.add(currentAcc); - } - }); + // Only process sensor data if jump measurement is ongoing. + if (!_isJumping) { + return; + } + _processSensorData(data); + }); } - _calculateJumpHeight() { - double height = 0.0; - // TODO: timeSlice = 1 / samplingRate - double timeSlice = 0.04; // Ensure this matches your data sampling rate + /// Processes incoming sensor data and updates jump height. + void _processSensorData(Map data) { + /// Kalman filtered accelerometer data for X. + _accX = _kalmanX.filtered(data["ACC"]["X"]); + /// Kalman filtered accelerometer data for Y. + _accY = _kalmanY.filtered(data["ACC"]["Y"]); + /// Kalman filtered accelerometer data for Z. + _accZ = _kalmanZ.filtered(data["ACC"]["Z"]); + /// Pitch angle in radians. + _pitch = data["EULER"]["PITCH"]; + // Calculates the current vertical acceleration. + // It adjusts the Z-axis acceleration with the pitch angle to account for the device's orientation. + double currentAcc = _accZ * cos(_pitch) + _accX * sin(_pitch); + // Subtract gravity to get acceleration due to movement. + currentAcc -= _gravity; - print("Acc length: ${_accelerations.length}"); // Debug log - for (int i = 0; i < _accelerations.length; i++) { - height += 0.5 * _accelerations[i] * timeSlice * timeSlice; - } - height *= _lambda; - - print("Calculated Height: $height"); // Debug log + _updateHeight(currentAcc); + } - if (height > _maxJumpHeight) { - _maxJumpHeight = height; // Update max height if current height is greater - } + /// Checks if the device is stationary based on acceleration magnitude. + bool _deviceIsStationary(double threshold) { + double accMagnitude = sqrt(_accX * _accX + _accY * _accY + _accZ * _accZ); + bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); + return isStationary; + } + /// Updates the current height based on the current acceleration. + /// If the device is stationary, the velocity is reset to 0. + /// Otherwise, it integrates the current acceleration to update velocity and height. + _updateHeight(double currentAcc) { setState(() { - _jumpHeight = height; + if (_deviceIsStationary(0.3)) { + _velocity = 0.0; + } else { + // Integrate acceleration to get velocity. + _velocity += currentAcc * _timeSlice; + + // Integrate velocity to get height. + _height += _velocity * _timeSlice; + } + + // Prevent height from going negative. + _height = max(0, _height); + + // Update maximum height if the current height is greater. + if (_height > _maxHeight) { + _maxHeight = _height; + } + + _jumpData.add(Jump(DateTime.now(), _height)); }); + // For debugging. + // print("Stationary: ${deviceIsStationary(0.3)}, Acc: $currentAcc, Vel: $velocity, Height: $height"); } - @override void dispose() { super.dispose(); _imuSubscription?.cancel(); } + /// Starts the jump height measurement process. + /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. void _startJump() { + // Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); _startTime = DateTime.now(); + setState(() { + // Clear data from previous jump. + _jumpData.clear(); _isJumping = true; - _maxJumpHeight = 0.0; // Reset max height on starting a new jump + _height = 0.0; + _velocity = 0.0; + // Reset max height on starting a new jump + _maxHeight = 0.0; }); - // Set sampling rate to maximum - _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); } + /// Stops the jump height measurement process. void _stopJump() { if (_isJumping) { - // Calculate final jump height - _calculateJumpHeight(); - - // Resetting the state for the next jump - _accelerations.clear(); setState(() { _isJumping = false; }); } - // Here, _maxJumpHeight holds the maximum height reached during the jump - print("Maximum Jump Height: $_maxJumpHeight meters"); } + /// Builds the UI for the jump height test. + /// It displays a line chart of jump height over time and the maximum jump height achieved. @override Widget build(BuildContext context) { + List> jumpDataSeries = [ + charts.Series( + id: "Jumps", + data: _jumpData, + // X-axis: time in milliseconds since the start of the jump. + domainFn: (Jump series, _) => series._time.difference(_startTime!).inMilliseconds, + measureFn: (Jump series, _) => series._height, + colorFn: (Jump series, _) => charts.MaterialPalette.cyan.shadeDefault, + ) + ]; return Scaffold( appBar: AppBar( title: Text('Jump Height Test'), @@ -108,11 +229,28 @@ class _JumpHeightTestState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + Expanded( + child: charts.LineChart( + jumpDataSeries, + animate: false, + behaviors: [ + new charts.ChartTitle('Time (ms)', + behaviorPosition: charts.BehaviorPosition.bottom, + titleStyleSpec: charts.TextStyleSpec(color: charts.MaterialPalette.white), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + new charts.ChartTitle('Height (m)', + behaviorPosition: charts.BehaviorPosition.start, + titleStyleSpec: charts.TextStyleSpec(color: charts.MaterialPalette.white), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea) + ], + // Include timeline points in line. + defaultRenderer: charts.LineRendererConfig(includePoints: true), + ), + ), Text( - 'Jump Height: ${_jumpHeight.toStringAsFixed(2)} meters', + 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', style: Theme.of(context).textTheme.headlineMedium, ), - SizedBox(height: 20), ElevatedButton( onPressed: _isJumping ? _stopJump : _startJump, child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), @@ -123,6 +261,8 @@ class _JumpHeightTestState extends State { ); } + /// Builds a sensor configuration for the OpenEarable device. + /// Sets the sensor ID, sampling rate, and latency. OpenEarableSensorConfig _buildSensorConfig() { return OpenEarableSensorConfig( sensorId: 0, @@ -130,4 +270,4 @@ class _JumpHeightTestState extends State { latency: 0, ); } -} \ No newline at end of file +} From 0bbf296be63baca8393d62094b2088d6e33afc78 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 22:44:26 +0100 Subject: [PATCH 05/15] Add error message if no OpenEarable device is connected and change font size of axes labels --- open_earable/lib/apps/jump_height_test.dart | 43 ++++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index 84153df..32f16f3 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -54,6 +54,8 @@ class _JumpHeightTestState extends State { bool _isJumping = false; /// Instance of OpenEarable device. final OpenEarable _openEarable; + /// Flag to indicate if an OpenEarable device is connected. + bool _earableConnected = false; /// Subscription to IMU sensor data. StreamSubscription? _imuSubscription; /// Stores the maximum height achieved in a jump. @@ -88,6 +90,7 @@ class _JumpHeightTestState extends State { // Set up listeners for sensor data. if (_openEarable.bleManager.connected) { _setupListeners(); + _earableConnected = true; } } @@ -234,13 +237,21 @@ class _JumpHeightTestState extends State { jumpDataSeries, animate: false, behaviors: [ + // X-axis label. new charts.ChartTitle('Time (ms)', behaviorPosition: charts.BehaviorPosition.bottom, - titleStyleSpec: charts.TextStyleSpec(color: charts.MaterialPalette.white), + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10 + ), titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + // Y-axis label. new charts.ChartTitle('Height (m)', behaviorPosition: charts.BehaviorPosition.start, - titleStyleSpec: charts.TextStyleSpec(color: charts.MaterialPalette.white), + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10 + ), titleOutsideJustification: charts.OutsideJustification.middleDrawArea) ], // Include timeline points in line. @@ -251,10 +262,30 @@ class _JumpHeightTestState extends State { 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', style: Theme.of(context).textTheme.headlineMedium, ), - ElevatedButton( - onPressed: _isJumping ? _stopJump : _startJump, - child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), - ), + Column(children: [ + ElevatedButton( + onPressed: _earableConnected ? () { _isJumping ? _stopJump() : _startJump(); } : null, + style: ElevatedButton.styleFrom( + backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, + foregroundColor: Colors.black, + ), + child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + ), + Visibility( + // Show error message if no OpenEarable device is connected. + visible: !_earableConnected, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ) + ]) ], ), ), From 6354eb986d07461723999afbc85b77c3e3803fe5 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 22:51:36 +0100 Subject: [PATCH 06/15] Reorganize code structure --- open_earable/lib/apps/jump_height_test.dart | 92 +++++++++++---------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index 32f16f3..52222f6 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -82,6 +82,7 @@ class _JumpHeightTestState extends State { /// Constructs a _JumpHeightTestState object with a given OpenEarable device. _JumpHeightTestState(this._openEarable); + /// Initializes state and sets up listeners for sensor data. @override void initState() { super.initState(); @@ -94,6 +95,52 @@ class _JumpHeightTestState extends State { } } + /// Disposes IMU data subscription when the state object is removed. + @override + void dispose() { + super.dispose(); + _imuSubscription?.cancel(); + } + + /// Sets up listeners to receive sensor data from the OpenEarable device. + _setupListeners() { + _imuSubscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + // Only process sensor data if jump measurement is ongoing. + if (!_isJumping) { + return; + } + _processSensorData(data); + }); + } + + /// Starts the jump height measurement process. + /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. + void _startJump() { + // Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + _startTime = DateTime.now(); + + setState(() { + // Clear data from previous jump. + _jumpData.clear(); + _isJumping = true; + _height = 0.0; + _velocity = 0.0; + // Reset max height on starting a new jump + _maxHeight = 0.0; + }); + } + + /// Stops the jump height measurement process. + void _stopJump() { + if (_isJumping) { + setState(() { + _isJumping = false; + }); + } + } + /// Initializes Kalman filters for accelerometer data. void _initializeKalmanFilters() { _kalmanX = SimpleKalman( @@ -110,18 +157,6 @@ class _JumpHeightTestState extends State { q: 0.9); } - /// Sets up listeners to receive sensor data from the OpenEarable device. - _setupListeners() { - _imuSubscription = - _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { - // Only process sensor data if jump measurement is ongoing. - if (!_isJumping) { - return; - } - _processSensorData(data); - }); - } - /// Processes incoming sensor data and updates jump height. void _processSensorData(Map data) { /// Kalman filtered accelerometer data for X. @@ -177,39 +212,6 @@ class _JumpHeightTestState extends State { // print("Stationary: ${deviceIsStationary(0.3)}, Acc: $currentAcc, Vel: $velocity, Height: $height"); } - @override - void dispose() { - super.dispose(); - _imuSubscription?.cancel(); - } - - /// Starts the jump height measurement process. - /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. - void _startJump() { - // Set sampling rate to maximum. - _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); - _startTime = DateTime.now(); - - setState(() { - // Clear data from previous jump. - _jumpData.clear(); - _isJumping = true; - _height = 0.0; - _velocity = 0.0; - // Reset max height on starting a new jump - _maxHeight = 0.0; - }); - } - - /// Stops the jump height measurement process. - void _stopJump() { - if (_isJumping) { - setState(() { - _isJumping = false; - }); - } - } - /// Builds the UI for the jump height test. /// It displays a line chart of jump height over time and the maximum jump height achieved. @override From 64ae36a3692861516256fefa7e1af50a47e490e2 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 23:08:24 +0100 Subject: [PATCH 07/15] Refactor the build method into more modular parts --- open_earable/lib/apps/jump_height_test.dart | 144 +++++++++++--------- 1 file changed, 81 insertions(+), 63 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index 52222f6..301e578 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -214,8 +214,28 @@ class _JumpHeightTestState extends State { /// Builds the UI for the jump height test. /// It displays a line chart of jump height over time and the maximum jump height achieved. + // This build function is getting a little too big. Consider refactoring. @override Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Jump Height Test'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildChart(), + _buildText(), + _buildButtons(), + ], + ), + ), + ); + } + + /// Builds a line chart to display jump height over time. + Widget _buildChart() { List> jumpDataSeries = [ charts.Series( id: "Jumps", @@ -226,74 +246,72 @@ class _JumpHeightTestState extends State { colorFn: (Jump series, _) => charts.MaterialPalette.cyan.shadeDefault, ) ]; - return Scaffold( - appBar: AppBar( - title: Text('Jump Height Test'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: charts.LineChart( - jumpDataSeries, - animate: false, - behaviors: [ - // X-axis label. - new charts.ChartTitle('Time (ms)', - behaviorPosition: charts.BehaviorPosition.bottom, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10 - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea), - // Y-axis label. - new charts.ChartTitle('Height (m)', - behaviorPosition: charts.BehaviorPosition.start, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10 - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea) - ], - // Include timeline points in line. - defaultRenderer: charts.LineRendererConfig(includePoints: true), - ), - ), - Text( - 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', - style: Theme.of(context).textTheme.headlineMedium, - ), - Column(children: [ - ElevatedButton( - onPressed: _earableConnected ? () { _isJumping ? _stopJump() : _startJump(); } : null, - style: ElevatedButton.styleFrom( - backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, - foregroundColor: Colors.black, - ), - child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + + return Expanded( + child: charts.LineChart( + jumpDataSeries, + animate: false, + behaviors: [ + // X-axis label. + new charts.ChartTitle('Time (ms)', + behaviorPosition: charts.BehaviorPosition.bottom, + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10 ), - Visibility( - // Show error message if no OpenEarable device is connected. - visible: !_earableConnected, - maintainState: true, - maintainAnimation: true, - maintainSize: true, - child: Text( - "No Earable Connected", - style: TextStyle( - color: Colors.red, - fontSize: 12, - ), - ), - ) - ]) - ], - ), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + // Y-axis label. + new charts.ChartTitle('Height (m)', + behaviorPosition: charts.BehaviorPosition.start, + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10 + ), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea) + ], + // Include timeline points in line. + defaultRenderer: charts.LineRendererConfig(includePoints: true), ), ); } + /// Builds a text widget to display the maximum jump height achieved. + Widget _buildText() { + return Text( + 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', + style: + Theme.of(context).textTheme.headlineMedium + ); + } + + /// Builds buttons to start and stop the jump height measurement process. + Widget _buildButtons() { + return Column(children: [ + ElevatedButton( + onPressed: _earableConnected ? () { _isJumping ? _stopJump() : _startJump(); } : null, + style: ElevatedButton.styleFrom( + backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, + foregroundColor: Colors.black, + ), + child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + ), + Visibility( + // Show error message if no OpenEarable device is connected. + visible: !_earableConnected, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ) + ]); + } + /// Builds a sensor configuration for the OpenEarable device. /// Sets the sensor ID, sampling rate, and latency. OpenEarableSensorConfig _buildSensorConfig() { From 2ba4691d17eeae731ab2a81652d6c28410495f16 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 23:19:58 +0100 Subject: [PATCH 08/15] Slightly change widget structure --- open_earable/lib/apps/jump_height_test.dart | 98 +++++++++++---------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index 301e578..5ccfc57 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -228,6 +228,19 @@ class _JumpHeightTestState extends State { _buildChart(), _buildText(), _buildButtons(), + Visibility( + // Show error message if no OpenEarable device is connected. + visible: !_earableConnected, + maintainState: true, + maintainAnimation: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ), ], ), ), @@ -248,70 +261,63 @@ class _JumpHeightTestState extends State { ]; return Expanded( - child: charts.LineChart( - jumpDataSeries, - animate: false, - behaviors: [ - // X-axis label. - new charts.ChartTitle('Time (ms)', - behaviorPosition: charts.BehaviorPosition.bottom, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10 - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea), - // Y-axis label. - new charts.ChartTitle('Height (m)', - behaviorPosition: charts.BehaviorPosition.start, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10 - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea) - ], - // Include timeline points in line. - defaultRenderer: charts.LineRendererConfig(includePoints: true), + child: Container( + child: charts.LineChart( + jumpDataSeries, + animate: false, + behaviors: [ + // X-axis label. + charts.ChartTitle('Time (ms)', + behaviorPosition: charts.BehaviorPosition.bottom, + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10, + ), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + // Y-axis label. + charts.ChartTitle('Height (m)', + behaviorPosition: charts.BehaviorPosition.start, + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10, + ), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + ], + // Include timeline points in line. + defaultRenderer: charts.LineRendererConfig(includePoints: true), + ), ), ); } /// Builds a text widget to display the maximum jump height achieved. Widget _buildText() { - return Text( - 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', - style: - Theme.of(context).textTheme.headlineMedium + return Container( + child: Text( + 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', + style: Theme.of(context).textTheme.headlineSmall, + ), ); } /// Builds buttons to start and stop the jump height measurement process. Widget _buildButtons() { - return Column(children: [ - ElevatedButton( - onPressed: _earableConnected ? () { _isJumping ? _stopJump() : _startJump(); } : null, + return Flexible( + child: ElevatedButton( + onPressed: _earableConnected + ? () { + _isJumping ? _stopJump() : _startJump(); + } + : null, style: ElevatedButton.styleFrom( backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, foregroundColor: Colors.black, ), child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), ), - Visibility( - // Show error message if no OpenEarable device is connected. - visible: !_earableConnected, - maintainState: true, - maintainAnimation: true, - maintainSize: true, - child: Text( - "No Earable Connected", - style: TextStyle( - color: Colors.red, - fontSize: 12, - ), - ), - ) - ]); + ); } - + /// Builds a sensor configuration for the OpenEarable device. /// Sets the sensor ID, sampling rate, and latency. OpenEarableSensorConfig _buildSensorConfig() { From 395f714576ca3030df2673477fb2f393774f7dbe Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sun, 24 Dec 2023 15:37:58 +0100 Subject: [PATCH 09/15] Remove TODO --- open_earable/lib/apps_tab.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 39ac81b..85dec37 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; -import 'package:open_earable/apps/jump_height_test.dart'; +import 'package:open_earable/apps/jump_height_test/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -53,7 +53,6 @@ class AppsTab extends StatelessWidget { onTap: () { Navigator.push( context, - // TODO: Change PageRoute MaterialPageRoute( builder: (context) => JumpHeightTest(_openEarable))); }), From 5956f04bb917cbfde5526f2c2fc036fc67216cbc Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sun, 24 Dec 2023 15:38:22 +0100 Subject: [PATCH 10/15] Add charts for raw acc. and filtered acc. in real-time --- .../jump_height_test/jump_height_chart.dart | 318 ++++++++++++++++++ .../jump_height_data_tab.dart | 99 ++++++ .../jump_height_test.dart | 87 ++++- 3 files changed, 489 insertions(+), 15 deletions(-) create mode 100644 open_earable/lib/apps/jump_height_test/jump_height_chart.dart create mode 100644 open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart rename open_earable/lib/apps/{ => jump_height_test}/jump_height_test.dart (82%) diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart new file mode 100644 index 0000000..6caad19 --- /dev/null +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -0,0 +1,318 @@ +import 'dart:async'; + +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:simple_kalman/simple_kalman.dart'; +import 'package:collection/collection.dart'; +import 'dart:math'; +import 'dart:core'; + +class JumpHeightChart extends StatefulWidget { + final OpenEarable _openEarable; + final String _title; + JumpHeightChart(this._openEarable, this._title); + @override + _JumpHeightChartState createState() => + _JumpHeightChartState(_openEarable, _title); +} + +class _JumpHeightChartState extends State { + final OpenEarable _openEarable; + final String _title; + late List _data; + StreamSubscription? _dataSubscription; + _JumpHeightChartState(this._openEarable, this._title); + late int _minX = 0; + late int _maxX = 0; + late List colors; + List> seriesList = []; + late double _minY; + late double _maxY; + final _errorMeasureAcc = 5.0; + late SimpleKalman _kalmanX, _kalmanY, _kalmanZ; + int _numDatapoints = 200; + + double _velocity = 0.0; + /// Sampling rate time slice (inverse of frequency). + double _timeSlice = 1 / 30.0; + /// Standard gravity in m/s^2. + double _gravity = 9.81; + /// Pitch angle in radians. + double _pitch = 0.0; + double _height = 0.0; + + _setupListeners() { + _kalmanX = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _kalmanY = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _kalmanZ = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _dataSubscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + int timestamp = data["timestamp"]; + XYZValue rawAccelerometerValue = XYZValue( + timestamp: timestamp, + x: data["ACC"]["X"], + y: data["ACC"]["Y"], + z: data["ACC"]["Z"], + units: data["ACC"]["units"] + ); + XYZValue filteredAccelerometerValue = XYZValue( + timestamp: timestamp, + x: _kalmanX.filtered(data["ACC"]["X"]), + y: _kalmanY.filtered(data["ACC"]["Y"]), + z: _kalmanZ.filtered(data["ACC"]["Z"]), + units: data["ACC"]["units"] + ); + + if (_title == "Height Data") { + DataValue height = _calculateHeightData(filteredAccelerometerValue); + _updateData(height); + } + if (_title == "Raw Acceleration Data") { + _updateData(rawAccelerometerValue); + } else if (_title == "Filtered Acceleration Data") { + _updateData(filteredAccelerometerValue); + } + // double pitch = data["EULER"]["PITCH"]; + }); + } + + DataValue _calculateHeightData(XYZValue accValue) { + double currentAcc = accValue.z * cos(_pitch) + accValue.x * sin(_pitch); + // Subtract gravity to get acceleration due to movement. + currentAcc -= _gravity; + double threshold = 0.3; + double accMagnitude = sqrt(accValue.x * accValue.x + accValue.y * accValue.y + accValue.z * accValue.z); + bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); + /// Checks if the device is stationary based on acceleration magnitude. + if (isStationary) { + _velocity = 0.0; + } else { + // Integrate acceleration to get velocity. + _velocity += currentAcc * _timeSlice; + + // Integrate velocity to get height. + _height += _velocity * _timeSlice; + } + + // Prevent height from going negative. + _height = max(0, _height); + return Jump(DateTime.fromMillisecondsSinceEpoch(accValue.timestamp), _height); + } + + _updateData(DataValue value) { + setState(() { + _data.add(value); + _checkLength(_data); + DataValue? maxXYZValue = maxBy(_data, (DataValue b) => b.getMax()); + DataValue? minXYZValue = minBy(_data, (DataValue b) => b.getMin()); + if (maxXYZValue == null || minXYZValue == null) { + return; + } + double maxAbsValue = + max(maxXYZValue.getMax().abs(), minXYZValue.getMin().abs()); + _maxY = maxAbsValue; + + _minY = -maxAbsValue; + _maxX = value.timestamp; + _minX = _data[0].timestamp; + }); + } + + _getColor(String title) { + if (title == "Height Data") { + return ['#FF6347', '#3CB371', '#1E90FF']; + } else if (title == "Raw Acceleration Data") { + return ['#FFD700', '#FF4500', '#D8BFD8']; + } else if (title == "Filtered Acceleration Data") { + return ['#F08080', '#98FB98', '#ADD8E6']; + } + } + + @override + void initState() { + super.initState(); + _data = []; + colors = _getColor(_title); + _minY = -25; + _maxY = 25; + _setupListeners(); + } + + @override + void dispose() { + super.dispose(); + _dataSubscription?.cancel(); + } + + _checkLength(data) { + if (data.length > _numDatapoints) { + data.removeRange(0, data.length - _numDatapoints); + } + } + + @override + Widget build(BuildContext context) { + if (_title == "Height Data") { + seriesList = [ + charts.Series( + id: 'Height (m)', + colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), + domainFn: (DataValue data, _) => data.timestamp, + measureFn: (DataValue data, _) => (data as Jump).height, + data: _data, + ), + ]; + } else { + seriesList = [ + charts.Series( + id: 'X${_data.isNotEmpty ? " (${_data[0].units['X']})" : ""}', + colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), + domainFn: (DataValue data, _) => data.timestamp, + measureFn: (DataValue data, _) => (data as XYZValue).x, + data: _data, + ), + charts.Series( + id: 'Y${_data.isNotEmpty ? " (${_data[0].units['Y']})" : ""}', + colorFn: (_, __) => charts.Color.fromHex(code: colors[1]), + domainFn: (DataValue data, _) => data.timestamp, + measureFn: (DataValue data, _) => (data as XYZValue).y, + data: _data, + ), + charts.Series( + id: 'Z${_data.isNotEmpty ? " (${_data[0].units['Z']})" : ""}', + colorFn: (_, __) => charts.Color.fromHex(code: colors[2]), + domainFn: (DataValue data, _) => data.timestamp, + measureFn: (DataValue data, _) => (data as XYZValue).z, + data: _data, + ), + ]; + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + ), + Expanded( + child: charts.LineChart( + seriesList, + animate: false, + behaviors: [ + charts.SeriesLegend( + position: charts.BehaviorPosition + .bottom, // To position the legend at the end (bottom). You can change this as per requirement. + outsideJustification: charts.OutsideJustification + .middleDrawArea, // To justify the position. + horizontalFirst: false, // To stack items horizontally. + desiredMaxRows: + 1, // Optional if you want to define max rows for the legend. + entryTextStyle: charts.TextStyleSpec( + // Optional styling for the text. + color: charts.Color(r: 255, g: 255, b: 255), + fontSize: 12, + ), + ) + ], + primaryMeasureAxis: charts.NumericAxisSpec( + renderSpec: charts.GridlineRendererSpec( + labelStyle: charts.TextStyleSpec( + fontSize: 14, + color: charts.MaterialPalette.white, // Set the color here + ), + ), + viewport: charts.NumericExtents(_minY, _maxY), + ), + domainAxis: charts.NumericAxisSpec( + renderSpec: charts.GridlineRendererSpec( + labelStyle: charts.TextStyleSpec( + fontSize: 14, + color: charts.MaterialPalette.white, // Set the color here + ), + ), + viewport: charts.NumericExtents(_minX, _maxX)), + ), + ), + ], + ); + } +} + +abstract class DataValue { + final int timestamp; + final Map units; + double getMin(); + double getMax(); + DataValue({required this.timestamp, required this.units}); +} + +class XYZValue extends DataValue { + final double x; + final double y; + final double z; + + XYZValue( + {required timestamp, + required this.x, + required this.y, + required this.z, + required units}) + : super(timestamp: timestamp, units: units); + + @override + double getMax() { + return max(x, max(y, z)); + } + + @override + double getMin() { + return min(x, min(y, z)); + } + + @override + String toString() { + return "timestamp: $timestamp\nx: $x, y: $y, z: $z"; + } +} + +/// A class representing a jump with a time and height. +class Jump extends DataValue { + final DateTime _time; + final double _height; + + /// Constructs a Jump object with a time and height. + Jump(DateTime time, double height) + : _time = time, + _height = height, + super( + timestamp: time.millisecondsSinceEpoch, + units: {'height': 'meters'} // Providing default units + ); + + @override + double getMin() { + // Implement logic for min value + // For example, it might always be 0 for a jump. + return 0.0; + } + + @override + double getMax() { + // Implement logic for max value + // For Jump, it's likely the height. + return _height; + } + + // Optionally, if you need to access time and height outside, consider adding getters. + DateTime get time => _time; + double get height => _height; +} diff --git a/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart b/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart new file mode 100644 index 0000000..8a6badd --- /dev/null +++ b/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:open_earable/sensor_data_tab/earable_3d_model.dart'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:open_earable/sensor_data_tab/sensor_chart.dart'; + +class JumpHeightDataTab extends StatefulWidget { + final OpenEarable _openEarable; + JumpHeightDataTab(this._openEarable); + @override + _SensorDataTabState createState() => _SensorDataTabState(_openEarable); +} + +class _SensorDataTabState extends State + with SingleTickerProviderStateMixin { + //late EarableModel _earableModel; + final OpenEarable _openEarable; + late TabController _tabController; + + StreamSubscription? _batteryLevelSubscription; + StreamSubscription? _buttonStateSubscription; + List accelerometerData = []; + List gyroscopeData = []; + List magnetometerData = []; + List barometerData = []; + + _SensorDataTabState(this._openEarable); + + @override + void initState() { + super.initState(); + _tabController = TabController(vsync: this, length: 5); + if (_openEarable.bleManager.connected) { + _setupListeners(); + } + } + + int lastTimestamp = 0; + _setupListeners() { + _batteryLevelSubscription = + _openEarable.sensorManager.getBatteryLevelStream().listen((data) { + print("Battery level is ${data[0]}"); + }); + _buttonStateSubscription = + _openEarable.sensorManager.getButtonStateStream().listen((data) { + print("Button State is ${data[0]}"); + }); + } + + @override + void dispose() { + super.dispose(); + _buttonStateSubscription?.cancel(); + _batteryLevelSubscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + if (!_openEarable.bleManager.connected) { + return _notConnectedWidget(); + } else { + return _buildSensorDataTabs(); + } + } + + Widget _notConnectedWidget() { + return Stack( + fit: StackFit.expand, + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.warning, + size: 48, + color: Colors.red, + ), + SizedBox(height: 16), + Center( + child: Text( + "Not connected to\nOpenEarable device", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ); + } + + +} diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart similarity index 82% rename from open_earable/lib/apps/jump_height_test.dart rename to open_earable/lib/apps/jump_height_test/jump_height_test.dart index 5ccfc57..27694a9 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:open_earable/apps/jump_height_test/jump_height_chart.dart'; import 'dart:async'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'package:charts_flutter/flutter.dart' as charts; @@ -12,7 +13,6 @@ class JumpHeightTest extends StatefulWidget { /// Constructs a JumpHeightTest widget with a given OpenEarable device. JumpHeightTest(this._openEarable); - @override _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); } @@ -43,7 +43,8 @@ class HeightChart extends StatelessWidget { } /// State class for JumpHeightTest widget. -class _JumpHeightTestState extends State { +class _JumpHeightTestState extends State + with SingleTickerProviderStateMixin { /// Stores the start time of a jump test. DateTime? _startTime; /// Current height calculated from sensor data. @@ -78,6 +79,7 @@ class _JumpHeightTestState extends State { double _accZ = 0.0; /// Pitch angle in radians. double _pitch = 0.0; + late TabController _tabController; /// Constructs a _JumpHeightTestState object with a given OpenEarable device. _JumpHeightTestState(this._openEarable); @@ -86,6 +88,9 @@ class _JumpHeightTestState extends State { @override void initState() { super.initState(); + // Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + _tabController = TabController(vsync: this, length: 3); // Initialize Kalman filters. _initializeKalmanFilters(); // Set up listeners for sensor data. @@ -117,8 +122,6 @@ class _JumpHeightTestState extends State { /// Starts the jump height measurement process. /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. void _startJump() { - // Set sampling rate to maximum. - _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); _startTime = DateTime.now(); setState(() { @@ -218,17 +221,31 @@ class _JumpHeightTestState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, appBar: AppBar( title: Text('Jump Height Test'), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildChart(), - _buildText(), - _buildButtons(), - Visibility( + body: Column( + children: [ + TabBar( + controller: _tabController, + indicatorColor: Colors.white, // Color of the underline indicator + labelColor: Colors.white, // Color of the active tab label + unselectedLabelColor: Colors.grey, // Color of the inactive tab labels + tabs: [ + Tab(text: 'Height'), + Tab(text: 'Raw Acc.'), + Tab(text: 'Filtered Acc.'), + ], + ), + Expanded( + child: (!_openEarable.bleManager.connected) + ? _notConnectedWidget() + : _buildJumpHeightDataTabs(), + ), + _buildText(), + _buildButtons(), + Visibility( // Show error message if no OpenEarable device is connected. visible: !_earableConnected, maintainState: true, @@ -241,12 +258,52 @@ class _JumpHeightTestState extends State { ), ), ), - ], - ), + ], ), ); } + Widget _notConnectedWidget() { + return Stack( + fit: StackFit.expand, + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.warning, + size: 48, + color: Colors.red, + ), + SizedBox(height: 16), + Center( + child: Text( + "Not connected to\nOpenEarable device", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildJumpHeightDataTabs() { + return TabBarView( + controller: _tabController, + children: [ + JumpHeightChart(_openEarable, "Height Data"), + JumpHeightChart(_openEarable, "Raw Acceleration Data"), + JumpHeightChart(_openEarable, "Filtered Acceleration Data") + ], + ); + } /// Builds a line chart to display jump height over time. Widget _buildChart() { List> jumpDataSeries = [ @@ -313,7 +370,7 @@ class _JumpHeightTestState extends State { backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, foregroundColor: Colors.black, ), - child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + child: Text(_isJumping ? 'Stop Jump' : 'Set Baseline & Start Jump'), ), ); } From 8656efecb981899172a006fba5c82e9177293ede Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sun, 24 Dec 2023 17:06:19 +0100 Subject: [PATCH 11/15] Minor code improvements and add jump timer --- .../jump_height_test/jump_height_chart.dart | 63 +++++------ .../jump_height_test/jump_height_test.dart | 105 +++++------------- 2 files changed, 61 insertions(+), 107 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart index 6caad19..483944a 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -58,55 +58,56 @@ class _JumpHeightChartState extends State { _dataSubscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { int timestamp = data["timestamp"]; - XYZValue rawAccelerometerValue = XYZValue( - timestamp: timestamp, - x: data["ACC"]["X"], - y: data["ACC"]["Y"], - z: data["ACC"]["Z"], - units: data["ACC"]["units"] - ); - XYZValue filteredAccelerometerValue = XYZValue( - timestamp: timestamp, - x: _kalmanX.filtered(data["ACC"]["X"]), - y: _kalmanY.filtered(data["ACC"]["Y"]), - z: _kalmanZ.filtered(data["ACC"]["Z"]), - units: data["ACC"]["units"] - ); + _pitch = data["EULER"]["PITCH"]; + + XYZValue rawAccData = XYZValue( + timestamp: timestamp, + x: data["ACC"]["X"], + y: data["ACC"]["Y"], + z: data["ACC"]["Z"], + units: {"X": "m/s²", "Y": "m/s²", "Z": "m/s²"} + ); + XYZValue filteredAccData = XYZValue( + timestamp: timestamp, + x: _kalmanX.filtered(data["ACC"]["X"]), + y: _kalmanY.filtered(data["ACC"]["Y"]), + z: _kalmanZ.filtered(data["ACC"]["Z"]), + units: {"X": "m/s²", "Y": "m/s²", "Z": "m/s²"} + ); if (_title == "Height Data") { - DataValue height = _calculateHeightData(filteredAccelerometerValue); + DataValue height = _calculateHeightData(filteredAccData); _updateData(height); } if (_title == "Raw Acceleration Data") { - _updateData(rawAccelerometerValue); + _updateData(rawAccData); } else if (_title == "Filtered Acceleration Data") { - _updateData(filteredAccelerometerValue); + _updateData(filteredAccData); } - // double pitch = data["EULER"]["PITCH"]; }); } DataValue _calculateHeightData(XYZValue accValue) { - double currentAcc = accValue.z * cos(_pitch) + accValue.x * sin(_pitch); // Subtract gravity to get acceleration due to movement. - currentAcc -= _gravity; + double currentAcc = accValue.z * cos(_pitch) + accValue.x * sin(_pitch) - _gravity;; + double threshold = 0.3; double accMagnitude = sqrt(accValue.x * accValue.x + accValue.y * accValue.y + accValue.z * accValue.z); bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); /// Checks if the device is stationary based on acceleration magnitude. - if (isStationary) { - _velocity = 0.0; - } else { - // Integrate acceleration to get velocity. - _velocity += currentAcc * _timeSlice; + if (isStationary) { + _velocity = 0.0; + } else { + // Integrate acceleration to get velocity. + _velocity += currentAcc * _timeSlice; - // Integrate velocity to get height. - _height += _velocity * _timeSlice; - } + // Integrate velocity to get height. + _height += _velocity * _timeSlice; + } - // Prevent height from going negative. - _height = max(0, _height); - return Jump(DateTime.fromMillisecondsSinceEpoch(accValue.timestamp), _height); + // Prevent height from going negative. + _height = max(0, _height); + return Jump(DateTime.fromMillisecondsSinceEpoch(accValue.timestamp), _height); } _updateData(DataValue value) { diff --git a/open_earable/lib/apps/jump_height_test/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart index 27694a9..7d5e4ba 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -16,37 +16,14 @@ class JumpHeightTest extends StatefulWidget { _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); } -/// A class representing a jump with a time and height. -class Jump { - final DateTime _time; - final double _height; - - /// Constructs a Jump object with a time and height. - Jump(this._time, this._height); -} - -/// A stateless widget to display jump heights in a bar chart. -class HeightChart extends StatelessWidget { - final List _seriesList; - final bool _animate; - - /// Constructs a HeightChart widget with given series list and animate flag. - HeightChart(this._seriesList, {required bool animate}) : _animate = animate; - - @override - Widget build(BuildContext context) { - return new charts.BarChart( - _seriesList.cast>(), - animate: _animate, - ); - } -} - /// State class for JumpHeightTest widget. class _JumpHeightTestState extends State with SingleTickerProviderStateMixin { /// Stores the start time of a jump test. - DateTime? _startTime; + Timer? _timer; + Duration _jumpDuration = Duration.zero; + DateTime? _startOfJump; + DateTime? _endOfJump; /// Current height calculated from sensor data. double _height = 0.0; // List to store each jump's data. @@ -115,6 +92,9 @@ class _JumpHeightTestState extends State if (!_isJumping) { return; } + setState(() { + _jumpDuration = DateTime.now().difference(_startOfJump!); + }); _processSensorData(data); }); } @@ -122,7 +102,7 @@ class _JumpHeightTestState extends State /// Starts the jump height measurement process. /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. void _startJump() { - _startTime = DateTime.now(); + _startOfJump = DateTime.now(); setState(() { // Clear data from previous jump. @@ -137,6 +117,7 @@ class _JumpHeightTestState extends State /// Stops the jump height measurement process. void _stopJump() { + _endOfJump = DateTime.now(); if (_isJumping) { setState(() { _isJumping = false; @@ -215,6 +196,11 @@ class _JumpHeightTestState extends State // print("Stationary: ${deviceIsStationary(0.3)}, Acc: $currentAcc, Vel: $velocity, Height: $height"); } + String _prettyDuration(Duration duration) { + var seconds = duration.inMilliseconds / 1000; + return '${seconds.toStringAsFixed(2)} s'; + } + /// Builds the UI for the jump height test. /// It displays a line chart of jump height over time and the maximum jump height achieved. // This build function is getting a little too big. Consider refactoring. @@ -243,8 +229,10 @@ class _JumpHeightTestState extends State ? _notConnectedWidget() : _buildJumpHeightDataTabs(), ), - _buildText(), + SizedBox(height: 20), // Margin between chart and button _buildButtons(), + SizedBox(height: 20), // Margin between button and text + _buildText(), Visibility( // Show error message if no OpenEarable device is connected. visible: !_earableConnected, @@ -304,59 +292,24 @@ class _JumpHeightTestState extends State ], ); } - /// Builds a line chart to display jump height over time. - Widget _buildChart() { - List> jumpDataSeries = [ - charts.Series( - id: "Jumps", - data: _jumpData, - // X-axis: time in milliseconds since the start of the jump. - domainFn: (Jump series, _) => series._time.difference(_startTime!).inMilliseconds, - measureFn: (Jump series, _) => series._height, - colorFn: (Jump series, _) => charts.MaterialPalette.cyan.shadeDefault, - ) - ]; - - return Expanded( - child: Container( - child: charts.LineChart( - jumpDataSeries, - animate: false, - behaviors: [ - // X-axis label. - charts.ChartTitle('Time (ms)', - behaviorPosition: charts.BehaviorPosition.bottom, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10, - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea), - // Y-axis label. - charts.ChartTitle('Height (m)', - behaviorPosition: charts.BehaviorPosition.start, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10, - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea), - ], - // Include timeline points in line. - defaultRenderer: charts.LineRendererConfig(includePoints: true), - ), - ), - ); - } - - /// Builds a text widget to display the maximum jump height achieved. + Widget _buildText() { return Container( - child: Text( - 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', - style: Theme.of(context).textTheme.headlineSmall, + child: Column( + children: [ + Text( + 'Max height: ${_maxHeight.toStringAsFixed(2)} m', + style: Theme.of(context).textTheme.headlineMedium, + ), + Text('Jump time: ${_prettyDuration(_jumpDuration)}', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], ), ); } + /// Builds buttons to start and stop the jump height measurement process. Widget _buildButtons() { return Flexible( From f59eef7ee27b17ba741788231641364b52e3acc8 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Mon, 25 Dec 2023 23:00:30 +0100 Subject: [PATCH 12/15] Minor code improvements --- .../jump_height_test/jump_height_chart.dart | 168 +++++++++++------- .../jump_height_data_tab.dart | 99 ----------- .../jump_height_test/jump_height_test.dart | 18 +- 3 files changed, 117 insertions(+), 168 deletions(-) delete mode 100644 open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart index 483944a..4aba41b 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -8,40 +8,68 @@ import 'package:collection/collection.dart'; import 'dart:math'; import 'dart:core'; +/// A class representing a Chart for Jump Height. class JumpHeightChart extends StatefulWidget { + /// The OpenEarable object. final OpenEarable _openEarable; + /// The title of the chart. final String _title; + + /// Constructs a JumpHeightChart object with an OpenEarable object and a title. JumpHeightChart(this._openEarable, this._title); + @override - _JumpHeightChartState createState() => - _JumpHeightChartState(_openEarable, _title); + _JumpHeightChartState createState() => _JumpHeightChartState(_openEarable, _title); } +/// A class representing the state of a JumpHeightChart. class _JumpHeightChartState extends State { + /// The OpenEarable object. final OpenEarable _openEarable; + /// The title of the chart. final String _title; + /// The data of the chart. late List _data; + /// The subscription to the data. StreamSubscription? _dataSubscription; - _JumpHeightChartState(this._openEarable, this._title); + /// The minimum x value of the chart. late int _minX = 0; + /// The maximum x value of the chart. late int _maxX = 0; + /// The colors of the chart. late List colors; + /// The series of the chart. List> seriesList = []; + /// The minimum y value of the chart. late double _minY; + /// The maximum y value of the chart. late double _maxY; + /// The error measure of the Kalman filter. final _errorMeasureAcc = 5.0; - late SimpleKalman _kalmanX, _kalmanY, _kalmanZ; + /// The Kalman filter for the x value. + late SimpleKalman _kalmanX; + /// The Kalman filter for the y value. + late SimpleKalman _kalmanY; + /// The Kalman filter for the z value. + late SimpleKalman _kalmanZ; + /// The number of datapoints to display on the chart. int _numDatapoints = 200; + /// The velocity of the device. double _velocity = 0.0; /// Sampling rate time slice (inverse of frequency). - double _timeSlice = 1 / 30.0; + double _timeSlice = 1.0 / 30.0; /// Standard gravity in m/s^2. double _gravity = 9.81; /// Pitch angle in radians. double _pitch = 0.0; + /// The height of the jump. double _height = 0.0; + /// Constructs a _JumpHeightChartState object with an OpenEarable object and a title. + _JumpHeightChartState(this._openEarable, this._title); + + /// Sets up the listeners for the data. _setupListeners() { _kalmanX = SimpleKalman( errorMeasure: _errorMeasureAcc, @@ -75,24 +103,29 @@ class _JumpHeightChartState extends State { units: {"X": "m/s²", "Y": "m/s²", "Z": "m/s²"} ); - if (_title == "Height Data") { - DataValue height = _calculateHeightData(filteredAccData); - _updateData(height); - } - if (_title == "Raw Acceleration Data") { - _updateData(rawAccData); - } else if (_title == "Filtered Acceleration Data") { - _updateData(filteredAccData); + switch (_title) { + case "Height Data": + DataValue height = _calculateHeightData(filteredAccData); + _updateData(height); + break; + case "Raw Acceleration Data": + _updateData(rawAccData); + break; + case "Filtered Acceleration Data": + _updateData(filteredAccData); + break; + default: + throw ArgumentError("Invalid tab title."); } }); } DataValue _calculateHeightData(XYZValue accValue) { // Subtract gravity to get acceleration due to movement. - double currentAcc = accValue.z * cos(_pitch) + accValue.x * sin(_pitch) - _gravity;; + double currentAcc = accValue._z * cos(_pitch) + accValue._x * sin(_pitch) - _gravity;; double threshold = 0.3; - double accMagnitude = sqrt(accValue.x * accValue.x + accValue.y * accValue.y + accValue.z * accValue.z); + double accMagnitude = sqrt(accValue._x * accValue._x + accValue._y * accValue._y + accValue._z * accValue._z); bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); /// Checks if the device is stationary based on acceleration magnitude. if (isStationary) { @@ -104,10 +137,10 @@ class _JumpHeightChartState extends State { // Integrate velocity to get height. _height += _velocity * _timeSlice; } - // Prevent height from going negative. _height = max(0, _height); - return Jump(DateTime.fromMillisecondsSinceEpoch(accValue.timestamp), _height); + + return Jump(DateTime.fromMillisecondsSinceEpoch(accValue._timestamp), _height); } _updateData(DataValue value) { @@ -124,18 +157,24 @@ class _JumpHeightChartState extends State { _maxY = maxAbsValue; _minY = -maxAbsValue; - _maxX = value.timestamp; - _minX = _data[0].timestamp; + _maxX = value._timestamp; + _minX = _data[0]._timestamp; }); } _getColor(String title) { - if (title == "Height Data") { - return ['#FF6347', '#3CB371', '#1E90FF']; - } else if (title == "Raw Acceleration Data") { - return ['#FFD700', '#FF4500', '#D8BFD8']; - } else if (title == "Filtered Acceleration Data") { - return ['#F08080', '#98FB98', '#ADD8E6']; + switch (title) { + case "Height Data": + // Blue, Orange, and Teal - Good for colorblindness + return ['#007bff', '#ff7f0e', '#2ca02c']; + case "Raw Acceleration Data": + // Purple, Magenta, and Cyan - Diverse hue and brightness + return ['#9467bd', '#d62728', '#17becf']; + case "Filtered Acceleration Data": + // Olive, Brown, and Navy - High contrast + return ['#8c564b', '#e377c2', '#1f77b4']; + default: + throw ArgumentError("Invalid tab title."); } } @@ -168,32 +207,32 @@ class _JumpHeightChartState extends State { charts.Series( id: 'Height (m)', colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), - domainFn: (DataValue data, _) => data.timestamp, - measureFn: (DataValue data, _) => (data as Jump).height, + domainFn: (DataValue data, _) => data._timestamp, + measureFn: (DataValue data, _) => (data as Jump)._height, data: _data, ), ]; } else { seriesList = [ charts.Series( - id: 'X${_data.isNotEmpty ? " (${_data[0].units['X']})" : ""}', + id: 'X${_data.isNotEmpty ? " (${_data[0]._units['X']})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), - domainFn: (DataValue data, _) => data.timestamp, - measureFn: (DataValue data, _) => (data as XYZValue).x, + domainFn: (DataValue data, _) => data._timestamp, + measureFn: (DataValue data, _) => (data as XYZValue)._x, data: _data, ), charts.Series( - id: 'Y${_data.isNotEmpty ? " (${_data[0].units['Y']})" : ""}', + id: 'Y${_data.isNotEmpty ? " (${_data[0]._units['Y']})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[1]), - domainFn: (DataValue data, _) => data.timestamp, - measureFn: (DataValue data, _) => (data as XYZValue).y, + domainFn: (DataValue data, _) => data._timestamp, + measureFn: (DataValue data, _) => (data as XYZValue)._y, data: _data, ), charts.Series( - id: 'Z${_data.isNotEmpty ? " (${_data[0].units['Z']})" : ""}', + id: 'Z${_data.isNotEmpty ? " (${_data[0]._units['Z']})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[2]), - domainFn: (DataValue data, _) => data.timestamp, - measureFn: (DataValue data, _) => (data as XYZValue).z, + domainFn: (DataValue data, _) => data._timestamp, + measureFn: (DataValue data, _) => (data as XYZValue)._z, data: _data, ), ]; @@ -248,72 +287,81 @@ class _JumpHeightChartState extends State { } } +/// A class representing a generic data value. abstract class DataValue { - final int timestamp; - final Map units; + /// The timestamp of the data. + final int _timestamp; + /// The units of the data. + final Map _units; + + /// Returns the minimum value of the data. double getMin(); + /// Returns the maximum value of the data. double getMax(); - DataValue({required this.timestamp, required this.units}); + + /// Constructs a DataValue object with a timestamp and units. + DataValue({required int timestamp, required Map units}) : _units = units, _timestamp = timestamp; } +/// A class representing a generic XYZ value. class XYZValue extends DataValue { - final double x; - final double y; - final double z; + /// The x value of the data. + final double _x; + /// The y value of the data. + final double _y; + /// The z value of the data. + final double _z; + /// Constructs a XYZValue object with a timestamp, x, y, z, and units. XYZValue( {required timestamp, - required this.x, - required this.y, - required this.z, + required double x, + required double y, + required double z, required units}) - : super(timestamp: timestamp, units: units); + : _z = z, _y = y, _x = x, super(timestamp: timestamp, units: units); @override double getMax() { - return max(x, max(y, z)); + return max(_x, max(_y, _z)); } @override double getMin() { - return min(x, min(y, z)); + return min(_x, min(_y, _z)); } @override String toString() { - return "timestamp: $timestamp\nx: $x, y: $y, z: $z"; + return "timestamp: $_timestamp\nx: $_x, y: $_y, z: $_z"; } } /// A class representing a jump with a time and height. class Jump extends DataValue { + /// The time of the jump. final DateTime _time; + /// The height of the jump. final double _height; /// Constructs a Jump object with a time and height. Jump(DateTime time, double height) : _time = time, _height = height, - super( - timestamp: time.millisecondsSinceEpoch, - units: {'height': 'meters'} // Providing default units - ); + super(timestamp: time.millisecondsSinceEpoch, units: {'height': 'meters'}); @override double getMin() { - // Implement logic for min value - // For example, it might always be 0 for a jump. return 0.0; } @override double getMax() { - // Implement logic for max value - // For Jump, it's likely the height. return _height; } - // Optionally, if you need to access time and height outside, consider adding getters. - DateTime get time => _time; - double get height => _height; + @override + String toString() { + return "timestamp: ${_time.millisecondsSinceEpoch}\nheight $_height"; + } } diff --git a/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart b/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart deleted file mode 100644 index 8a6badd..0000000 --- a/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:open_earable/sensor_data_tab/earable_3d_model.dart'; -import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -import 'package:open_earable/sensor_data_tab/sensor_chart.dart'; - -class JumpHeightDataTab extends StatefulWidget { - final OpenEarable _openEarable; - JumpHeightDataTab(this._openEarable); - @override - _SensorDataTabState createState() => _SensorDataTabState(_openEarable); -} - -class _SensorDataTabState extends State - with SingleTickerProviderStateMixin { - //late EarableModel _earableModel; - final OpenEarable _openEarable; - late TabController _tabController; - - StreamSubscription? _batteryLevelSubscription; - StreamSubscription? _buttonStateSubscription; - List accelerometerData = []; - List gyroscopeData = []; - List magnetometerData = []; - List barometerData = []; - - _SensorDataTabState(this._openEarable); - - @override - void initState() { - super.initState(); - _tabController = TabController(vsync: this, length: 5); - if (_openEarable.bleManager.connected) { - _setupListeners(); - } - } - - int lastTimestamp = 0; - _setupListeners() { - _batteryLevelSubscription = - _openEarable.sensorManager.getBatteryLevelStream().listen((data) { - print("Battery level is ${data[0]}"); - }); - _buttonStateSubscription = - _openEarable.sensorManager.getButtonStateStream().listen((data) { - print("Button State is ${data[0]}"); - }); - } - - @override - void dispose() { - super.dispose(); - _buttonStateSubscription?.cancel(); - _batteryLevelSubscription?.cancel(); - } - - @override - Widget build(BuildContext context) { - if (!_openEarable.bleManager.connected) { - return _notConnectedWidget(); - } else { - return _buildSensorDataTabs(); - } - } - - Widget _notConnectedWidget() { - return Stack( - fit: StackFit.expand, - children: [ - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.warning, - size: 48, - color: Colors.red, - ), - SizedBox(height: 16), - Center( - child: Text( - "Not connected to\nOpenEarable device", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ], - ); - } - - -} diff --git a/open_earable/lib/apps/jump_height_test/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart index 7d5e4ba..9fb8fce 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -2,17 +2,18 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/jump_height_test/jump_height_chart.dart'; import 'dart:async'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -import 'package:charts_flutter/flutter.dart' as charts; import 'package:simple_kalman/simple_kalman.dart'; import 'dart:math'; /// An app that lets you test your jump height using an OpenEarable device. class JumpHeightTest extends StatefulWidget { + /// Instance of OpenEarable device. final OpenEarable _openEarable; /// Constructs a JumpHeightTest widget with a given OpenEarable device. JumpHeightTest(this._openEarable); + /// Creates a state for JumpHeightTest widget. _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); } @@ -20,10 +21,9 @@ class JumpHeightTest extends StatefulWidget { class _JumpHeightTestState extends State with SingleTickerProviderStateMixin { /// Stores the start time of a jump test. - Timer? _timer; - Duration _jumpDuration = Duration.zero; DateTime? _startOfJump; - DateTime? _endOfJump; + /// Stores the duration of a jump test. + Duration _jumpDuration = Duration.zero; /// Current height calculated from sensor data. double _height = 0.0; // List to store each jump's data. @@ -56,6 +56,7 @@ class _JumpHeightTestState extends State double _accZ = 0.0; /// Pitch angle in radians. double _pitch = 0.0; + /// Manages the [TabBar]. late TabController _tabController; /// Constructs a _JumpHeightTestState object with a given OpenEarable device. @@ -65,13 +66,13 @@ class _JumpHeightTestState extends State @override void initState() { super.initState(); - // Set sampling rate to maximum. - _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); _tabController = TabController(vsync: this, length: 3); - // Initialize Kalman filters. - _initializeKalmanFilters(); // Set up listeners for sensor data. if (_openEarable.bleManager.connected) { + // Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + // Initialize Kalman filters. + _initializeKalmanFilters(); _setupListeners(); _earableConnected = true; } @@ -117,7 +118,6 @@ class _JumpHeightTestState extends State /// Stops the jump height measurement process. void _stopJump() { - _endOfJump = DateTime.now(); if (_isJumping) { setState(() { _isJumping = false; From bd24516817b14ff2adeab9bc2af60c4e8dc05c8b Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Mon, 25 Dec 2023 23:16:36 +0100 Subject: [PATCH 13/15] Add comments --- open_earable/lib/apps/jump_height_test/jump_height_chart.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart index 4aba41b..bdf6b99 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -120,6 +120,7 @@ class _JumpHeightChartState extends State { }); } + /// Calculates the height of the jump. DataValue _calculateHeightData(XYZValue accValue) { // Subtract gravity to get acceleration due to movement. double currentAcc = accValue._z * cos(_pitch) + accValue._x * sin(_pitch) - _gravity;; @@ -143,6 +144,7 @@ class _JumpHeightChartState extends State { return Jump(DateTime.fromMillisecondsSinceEpoch(accValue._timestamp), _height); } + /// Updates the data of the chart. _updateData(DataValue value) { setState(() { _data.add(value); @@ -162,6 +164,7 @@ class _JumpHeightChartState extends State { }); } + /// Gets the color of the chart lines. _getColor(String title) { switch (title) { case "Height Data": @@ -194,6 +197,7 @@ class _JumpHeightChartState extends State { _dataSubscription?.cancel(); } + /// Checks the length of the data an removes the oldest data if it is too long. _checkLength(data) { if (data.length > _numDatapoints) { data.removeRange(0, data.length - _numDatapoints); From 1dec4d2e45628c989ef7683842b4f1826b60fa26 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Tue, 26 Dec 2023 14:05:57 +0100 Subject: [PATCH 14/15] Minor code improvements --- .../lib/apps/jump_height_test/jump_height_chart.dart | 5 +++-- open_earable/lib/apps/jump_height_test/jump_height_test.dart | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart index bdf6b99..41b1652 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -123,14 +123,15 @@ class _JumpHeightChartState extends State { /// Calculates the height of the jump. DataValue _calculateHeightData(XYZValue accValue) { // Subtract gravity to get acceleration due to movement. - double currentAcc = accValue._z * cos(_pitch) + accValue._x * sin(_pitch) - _gravity;; + double currentAcc = accValue._z * cos(_pitch) + accValue._x * sin(_pitch) - _gravity; double threshold = 0.3; double accMagnitude = sqrt(accValue._x * accValue._x + accValue._y * accValue._y + accValue._z * accValue._z); bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); - /// Checks if the device is stationary based on acceleration magnitude. + // Checks if the device is stationary based on acceleration magnitude. if (isStationary) { _velocity = 0.0; + _height = 0.0; } else { // Integrate acceleration to get velocity. _velocity += currentAcc * _timeSlice; diff --git a/open_earable/lib/apps/jump_height_test/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart index 9fb8fce..86c9d00 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -174,6 +174,7 @@ class _JumpHeightTestState extends State setState(() { if (_deviceIsStationary(0.3)) { _velocity = 0.0; + _height = 0.0; } else { // Integrate acceleration to get velocity. _velocity += currentAcc * _timeSlice; @@ -231,8 +232,6 @@ class _JumpHeightTestState extends State ), SizedBox(height: 20), // Margin between chart and button _buildButtons(), - SizedBox(height: 20), // Margin between button and text - _buildText(), Visibility( // Show error message if no OpenEarable device is connected. visible: !_earableConnected, @@ -246,6 +245,8 @@ class _JumpHeightTestState extends State ), ), ), + SizedBox(height: 20), // Margin between button and text + _buildText() ], ), ); From dae03f7bfe48c4cc9713cd4962cd35663f0fa3aa Mon Sep 17 00:00:00 2001 From: o-bagge <47336932+o-bagge@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:08:48 +0100 Subject: [PATCH 15/15] Update project.pbxproj Revert changes --- .../ios/Runner.xcodeproj/project.pbxproj | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/open_earable/ios/Runner.xcodeproj/project.pbxproj b/open_earable/ios/Runner.xcodeproj/project.pbxproj index dc894e3..d696e14 100644 --- a/open_earable/ios/Runner.xcodeproj/project.pbxproj +++ b/open_earable/ios/Runner.xcodeproj/project.pbxproj @@ -480,7 +480,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -490,7 +490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; + PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -505,7 +505,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -525,7 +525,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -543,7 +543,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -669,7 +669,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -679,7 +679,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; + PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -696,7 +696,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -706,7 +706,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; + PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0;