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/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart new file mode 100644 index 0000000..41b1652 --- /dev/null +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -0,0 +1,372 @@ +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'; + +/// 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); +} + +/// 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; + /// 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; + /// 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.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, + 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"]; + _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²"} + ); + + 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."); + } + }); + } + + /// 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 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; + _height = 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); + } + + /// Updates the data of the chart. + _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; + }); + } + + /// Gets the color of the chart lines. + _getColor(String title) { + 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."); + } + } + + @override + void initState() { + super.initState(); + _data = []; + colors = _getColor(_title); + _minY = -25; + _maxY = 25; + _setupListeners(); + } + + @override + void dispose() { + super.dispose(); + _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); + } + } + + @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)), + ), + ), + ], + ); + } +} + +/// A class representing a generic data value. +abstract class DataValue { + /// 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(); + + /// 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 { + /// 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 double x, + required double y, + required double z, + required units}) + : _z = z, _y = y, _x = x, 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 { + /// 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'}); + + @override + double getMin() { + return 0.0; + } + + @override + double getMax() { + return _height; + } + + @override + String toString() { + return "timestamp: ${_time.millisecondsSinceEpoch}\nheight $_height"; + } +} 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 new file mode 100644 index 0000000..86c9d00 --- /dev/null +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -0,0 +1,341 @@ +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: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); +} + +/// State class for JumpHeightTest widget. +class _JumpHeightTestState extends State + with SingleTickerProviderStateMixin { + /// Stores the start time of a jump test. + DateTime? _startOfJump; + /// 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. + List _jumpData = []; + // Flag to indicate if jump measurement is ongoing. + 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. + 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; + /// Manages the [TabBar]. + late TabController _tabController; + + /// 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(); + _tabController = TabController(vsync: this, length: 3); + // 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; + } + } + + /// 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; + } + setState(() { + _jumpDuration = DateTime.now().difference(_startOfJump!); + }); + _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() { + _startOfJump = 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( + 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); + } + + /// 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; + + _updateHeight(currentAcc); + } + + /// 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(() { + if (_deviceIsStationary(0.3)) { + _velocity = 0.0; + _height = 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"); + } + + 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. + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: AppBar( + title: Text('Jump Height Test'), + ), + 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(), + ), + SizedBox(height: 20), // Margin between chart and button + _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, + ), + ), + ), + SizedBox(height: 20), // Margin between button and text + _buildText() + ], + ), + ); + } + + 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") + ], + ); + } + + Widget _buildText() { + return Container( + 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( + 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' : 'Set Baseline & Start Jump'), + ), + ); + } + + /// Builds a sensor configuration for the OpenEarable device. + /// Sets the sensor ID, sampling rate, and latency. + OpenEarableSensorConfig _buildSensorConfig() { + return OpenEarableSensorConfig( + sensorId: 0, + samplingRate: 30, + latency: 0, + ); + } +} diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 17e5a27..85dec37 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/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -45,6 +46,16 @@ class AppsTab extends StatelessWidget { MaterialPageRoute( builder: (context) => Recorder(_openEarable))); }), + AppInfo( + iconData: Icons.height, + title: "Jump Height Test", + description: "Test your maximum jump height.", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => JumpHeightTest(_openEarable))); + }), // ... similarly for other apps ]; }