diff --git a/README.md b/README.md
index 46c260c..a614ab2 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,23 @@
[OpenEarable](https://open-earable.teco.edu) is a new, open-source, Arduino-based platform for ear-based sensing applications. It provides a versatile prototyping platform with support for various sensors and actuators, making it suitable for earable research and development.
-
[Download iOS beta app!](https://testflight.apple.com/join/Kht3e1Cb)
+
+
+ ⬇️ Download iOS beta app
+
+
+
+
+
+ ⬇️ Download Android beta app
+
+
-
-
[Get OpenEarable device now!](https://forms.gle/R3LMcqtyKwVH7PZB9)
+
+
+ 🦻 Get OpenEarable device now
+
+
## Table of Contents
- [Introduction](#Introduction)
diff --git a/open_earable/assets/digital-7-mono.ttf b/open_earable/assets/digital-7-mono.ttf
new file mode 100644
index 0000000..74209e6
Binary files /dev/null and b/open_earable/assets/digital-7-mono.ttf differ
diff --git a/open_earable/ios/Podfile.lock b/open_earable/ios/Podfile.lock
index 67f9ad9..fd2fbe4 100644
--- a/open_earable/ios/Podfile.lock
+++ b/open_earable/ios/Podfile.lock
@@ -1,4 +1,6 @@
PODS:
+ - app_settings (5.1.1):
+ - Flutter
- Flutter (1.0.0)
- flutter_gl (0.0.3):
- Flutter
@@ -21,6 +23,7 @@ PODS:
- three3d_egl (0.1.3)
DEPENDENCIES:
+ - app_settings (from `.symlinks/plugins/app_settings/ios`)
- Flutter (from `Flutter`)
- flutter_gl (from `.symlinks/plugins/flutter_gl/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
@@ -36,6 +39,8 @@ SPEC REPOS:
- three3d_egl
EXTERNAL SOURCES:
+ app_settings:
+ :path: ".symlinks/plugins/app_settings/ios"
Flutter:
:path: Flutter
flutter_gl:
@@ -52,6 +57,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/reactive_ble_mobile/ios"
SPEC CHECKSUMS:
+ app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_gl: 5a5603f35db897697f064027864a32b15d0c421d
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
@@ -65,4 +71,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189
-COCOAPODS: 1.14.3
+COCOAPODS: 1.12.1
diff --git a/open_earable/lib/apps/posture_tracker/model/bad_posture_reminder.dart b/open_earable/lib/apps/posture_tracker/model/bad_posture_reminder.dart
index 4d3fe70..d494d04 100644
--- a/open_earable/lib/apps/posture_tracker/model/bad_posture_reminder.dart
+++ b/open_earable/lib/apps/posture_tracker/model/bad_posture_reminder.dart
@@ -37,10 +37,10 @@ class PostureTimestamps {
class BadPostureReminder {
BadPostureSettings _settings = BadPostureSettings(
- rollAngleThreshold: 7,
- pitchAngleThreshold: 15,
+ rollAngleThreshold: 20,
+ pitchAngleThreshold: 20,
timeThreshold: 10,
- resetTimeThreshold: 2
+ resetTimeThreshold: 1
);
final OpenEarable _openEarable;
final AttitudeTracker _attitudeTracker;
@@ -50,6 +50,8 @@ class BadPostureReminder {
BadPostureReminder(this._openEarable, this._attitudeTracker);
+ bool? _lastPostureWasBad = null;
+
void start() {
_timestamps.lastReset = DateTime.now();
_timestamps.lastBadPosture = null;
@@ -64,6 +66,10 @@ class BadPostureReminder {
DateTime now = DateTime.now();
if (_isBadPosture(attitude)) {
+ if (!(_lastPostureWasBad ?? false)) {
+ _openEarable.rgbLed.writeLedColor(r: 255, g: 0, b: 0);
+ }
+
// If this is the first time the program enters the bad state, store the current time
if (_timestamps.lastBadPosture == null) {
_timestamps.lastBadPosture = now;
@@ -81,6 +87,10 @@ class BadPostureReminder {
// Reset the last good state time
_timestamps.lastGoodPosture = null;
} else {
+ if (_lastPostureWasBad ?? false) {
+ _openEarable.rgbLed.writeLedColor(r: 0, g: 255, b: 0);
+ }
+
// If this is the first time the program enters the good state, store the current time
if (_timestamps.lastGoodPosture == null) {
_timestamps.lastGoodPosture = now;
@@ -90,13 +100,23 @@ class BadPostureReminder {
// Calculate the duration in seconds
int duration = now.difference(_timestamps.lastGoodPosture!).inSeconds;
// If the duration exceeds the minimum required, reset the last bad state time
- if (duration > _settings.resetTimeThreshold) {
+ if (duration >= _settings.resetTimeThreshold) {
+ print("duration: $duration, reset time threshold: ${_settings.resetTimeThreshold}");
+ print("resetting last bad posture time");
_timestamps.lastBadPosture = null;
}
}
}
+ _lastPostureWasBad = _isBadPosture(attitude);
});
}
+
+ void stop() {
+ _timestamps.lastBadPosture = null;
+ _timestamps.lastGoodPosture = null;
+ _openEarable.rgbLed.writeLedColor(r: 0, g: 0, b: 0);
+ _attitudeTracker.stop();
+ }
void setSettings(BadPostureSettings settings) {
_settings = settings;
diff --git a/open_earable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart b/open_earable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart
index 99ce712..0e151e4 100644
--- a/open_earable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart
+++ b/open_earable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart';
+import 'package:open_earable/apps/posture_tracker/model/ewma.dart';
import 'package:open_earable_flutter/src/open_earable_flutter.dart';
class EarableAttitudeTracker extends AttitudeTracker {
@@ -12,6 +13,10 @@ class EarableAttitudeTracker extends AttitudeTracker {
@override
bool get isAvailable => _openEarable.bleManager.connected;
+ EWMA _rollEWMA = EWMA(0.5);
+ EWMA _pitchEWMA = EWMA(0.5);
+ EWMA _yawEWMA = EWMA(0.5);
+
EarableAttitudeTracker(this._openEarable) {
_openEarable.bleManager.connectionStateStream.listen((connected) {
didChangeAvailability(this);
@@ -21,7 +26,6 @@ class EarableAttitudeTracker extends AttitudeTracker {
});
}
-
@override
void start() {
if (_subscription?.isPaused ?? false) {
@@ -32,9 +36,9 @@ class EarableAttitudeTracker extends AttitudeTracker {
_openEarable.sensorManager.writeSensorConfig(_buildSensorConfig());
_subscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((event) {
updateAttitude(
- roll: event["EULER"]["ROLL"],
- pitch: event["EULER"]["PITCH"],
- yaw: event["EULER"]["YAW"]
+ roll: _rollEWMA.update(event["EULER"]["ROLL"]),
+ pitch: _pitchEWMA.update(event["EULER"]["PITCH"]),
+ yaw: _yawEWMA.update(event["EULER"]["YAW"])
);
});
}
@@ -46,6 +50,7 @@ class EarableAttitudeTracker extends AttitudeTracker {
@override
void cancle() {
+ stop();
_subscription?.cancel();
super.cancle();
}
diff --git a/open_earable/lib/apps/posture_tracker/model/ewma.dart b/open_earable/lib/apps/posture_tracker/model/ewma.dart
new file mode 100644
index 0000000..da2d62f
--- /dev/null
+++ b/open_earable/lib/apps/posture_tracker/model/ewma.dart
@@ -0,0 +1,11 @@
+class EWMA {
+ double _alpha;
+ double _oldValue = 0;
+
+ EWMA(this._alpha);
+
+ double update(double newValue) {
+ _oldValue = _alpha * newValue + (1 - _alpha) * _oldValue;
+ return _oldValue;
+ }
+}
\ No newline at end of file
diff --git a/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart b/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart
index 24051f1..1bab1cd 100644
--- a/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart
+++ b/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart
@@ -17,44 +17,45 @@ class PostureRollView extends StatelessWidget {
final String neckAssetPath;
final AlignmentGeometry headAlignment;
- const PostureRollView({Key? key,
- required this.roll,
- this.angleThreshold = 0,
- required this.headAssetPath,
- required this.neckAssetPath,
- this.headAlignment = Alignment.center}) : super(key: key);
+ const PostureRollView(
+ {Key? key,
+ required this.roll,
+ this.angleThreshold = 0,
+ required this.headAssetPath,
+ required this.neckAssetPath,
+ this.headAlignment = Alignment.center})
+ : super(key: key);
@override
Widget build(BuildContext context) {
return Column(children: [
- Text(
- "${(this.roll * 180 / 3.14).abs().toStringAsFixed(0)}°",
- style: TextStyle(
- // use proper color matching the background
- color: Theme.of(context).colorScheme.onBackground,
- fontSize: 30,
- fontWeight: FontWeight.bold
- )
- ),
+ Text("${(this.roll * 180 / 3.14).abs().toStringAsFixed(0)}°",
+ style: TextStyle(
+ // use proper color matching the background
+ color: Theme.of(context).colorScheme.onBackground,
+ fontSize: 30,
+ fontWeight: FontWeight.bold)),
CustomPaint(
- painter: ArcPainter(angle: this.roll, angleThreshold: this.angleThreshold),
- child: Padding(
- padding: EdgeInsets.all(10),
- child: ClipOval(
- child: Container(
- color: roll.abs() > _MAX_ROLL ? Colors.red.withOpacity(0.5) : Colors.transparent,
- child: Stack(children: [
- Image.asset(this.neckAssetPath),
- Transform.rotate(
- angle: this.roll.isFinite ? roll.abs() < _MAX_ROLL ? this.roll : roll.sign * _MAX_ROLL : 0,
- alignment: this.headAlignment,
- child: Image.asset(this.headAssetPath)
- ),
- ])
- )
- )
- )
- ),
+ painter:
+ ArcPainter(angle: this.roll, angleThreshold: this.angleThreshold),
+ child: Padding(
+ padding: EdgeInsets.all(10),
+ child: ClipOval(
+ child: Container(
+ color: roll.abs() > _MAX_ROLL
+ ? Colors.red.withOpacity(0.5)
+ : Colors.transparent,
+ child: Stack(children: [
+ Image.asset(this.neckAssetPath),
+ Transform.rotate(
+ angle: this.roll.isFinite
+ ? roll.abs() < _MAX_ROLL
+ ? this.roll
+ : roll.sign * _MAX_ROLL
+ : 0,
+ alignment: this.headAlignment,
+ child: Image.asset(this.headAssetPath)),
+ ]))))),
]);
}
-}
\ No newline at end of file
+}
diff --git a/open_earable/lib/apps/posture_tracker/view/posture_tracker_view.dart b/open_earable/lib/apps/posture_tracker/view/posture_tracker_view.dart
index 67b465e..d66b0bc 100644
--- a/open_earable/lib/apps/posture_tracker/view/posture_tracker_view.dart
+++ b/open_earable/lib/apps/posture_tracker/view/posture_tracker_view.dart
@@ -10,7 +10,6 @@ import 'package:provider/provider.dart';
import 'package:open_earable_flutter/src/open_earable_flutter.dart';
-
class PostureTrackerView extends StatefulWidget {
final AttitudeTracker _tracker;
final OpenEarable _openEarable;
@@ -22,52 +21,44 @@ class PostureTrackerView extends StatefulWidget {
}
class _PostureTrackerViewState extends State {
- late final PostureTrackerViewModel _viewModel;
-
- @override
- void initState() {
- super.initState();
- this._viewModel = PostureTrackerViewModel(widget._tracker, BadPostureReminder(widget._openEarable, widget._tracker));
- }
-
@override
Widget build(BuildContext context) {
- return ChangeNotifierProvider.value(
- value: _viewModel,
+ return ChangeNotifierProvider(
+ create: (context) => PostureTrackerViewModel(widget._tracker,
+ BadPostureReminder(widget._openEarable, widget._tracker)),
builder: (context, child) => Consumer(
- builder: (context, postureTrackerViewModel, child) => Scaffold(
- appBar: AppBar(
- title: const Text("Posture Tracker"),
- actions: [
- IconButton(
- onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => SettingsView(this._viewModel))),
- icon: Icon(Icons.settings)
- ),
- ],
- ),
- body: Center(
- child: this._buildContentView(postureTrackerViewModel),
- ),
- )
- )
- );
+ builder: (context, postureTrackerViewModel, child) => Scaffold(
+ appBar: AppBar(
+ title: const Text("Posture Tracker"),
+ actions: [
+ IconButton(
+ onPressed: () => Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (context) =>
+ SettingsView(postureTrackerViewModel))),
+ icon: Icon(Icons.settings)),
+ ],
+ ),
+ body: Center(
+ child: this._buildContentView(postureTrackerViewModel),
+ ),
+ backgroundColor: Theme.of(context).colorScheme.background,
+ )));
}
Widget _buildContentView(PostureTrackerViewModel postureTrackerViewModel) {
var headViews = this._createHeadViews(postureTrackerViewModel);
- return Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- ...headViews.map((e) => FractionallySizedBox(
- widthFactor: .7,
- child: e,
- )),
- this._buildTrackingButton(postureTrackerViewModel),
- ]
- );
+ return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
+ ...headViews.map((e) => FractionallySizedBox(
+ widthFactor: .7,
+ child: e,
+ )),
+ this._buildTrackingButton(postureTrackerViewModel),
+ ]);
}
- Widget _buildHeadView(String headAssetPath, String neckAssetPath, AlignmentGeometry headAlignment, double roll, double angleThreshold) {
+ Widget _buildHeadView(String headAssetPath, String neckAssetPath,
+ AlignmentGeometry headAlignment, double roll, double angleThreshold) {
return Padding(
padding: const EdgeInsets.all(5),
child: PostureRollView(
@@ -83,33 +74,41 @@ class _PostureTrackerViewState extends State {
List _createHeadViews(postureTrackerViewModel) {
return [
this._buildHeadView(
- "assets/posture_tracker/Head_Front.png",
- "assets/posture_tracker/Neck_Front.png",
- Alignment.center.add(Alignment(0, 0.3)),
- postureTrackerViewModel.attitude.roll,
- postureTrackerViewModel.badPostureSettings.rollAngleThreshold.toDouble()
- ),
+ "assets/posture_tracker/Head_Front.png",
+ "assets/posture_tracker/Neck_Front.png",
+ Alignment.center.add(Alignment(0, 0.3)),
+ postureTrackerViewModel.attitude.roll,
+ postureTrackerViewModel.badPostureSettings.rollAngleThreshold
+ .toDouble()),
this._buildHeadView(
- "assets/posture_tracker/Head_Side.png",
- "assets/posture_tracker/Neck_Side.png",
- Alignment.center.add(Alignment(0, 0.3)),
- -postureTrackerViewModel.attitude.pitch,
- postureTrackerViewModel.badPostureSettings.pitchAngleThreshold.toDouble()
- ),
+ "assets/posture_tracker/Head_Side.png",
+ "assets/posture_tracker/Neck_Side.png",
+ Alignment.center.add(Alignment(0, 0.3)),
+ -postureTrackerViewModel.attitude.pitch,
+ postureTrackerViewModel.badPostureSettings.pitchAngleThreshold
+ .toDouble()),
];
}
-
+
Widget _buildTrackingButton(PostureTrackerViewModel postureTrackerViewModel) {
return Column(children: [
ElevatedButton(
onPressed: postureTrackerViewModel.isAvailable
- ? () { postureTrackerViewModel.isTracking ? this._viewModel.stopTracking() : this._viewModel.startTracking(); }
- : null,
+ ? () {
+ postureTrackerViewModel.isTracking
+ ? postureTrackerViewModel.stopTracking()
+ : postureTrackerViewModel.startTracking();
+ }
+ : null,
style: ElevatedButton.styleFrom(
- backgroundColor: !postureTrackerViewModel.isTracking ? Color(0xff77F2A1) : Color(0xfff27777),
- foregroundColor: Colors.black,
- ),
- child: postureTrackerViewModel.isTracking ? const Text("Stop Tracking") : const Text("Start Tracking"),
+ backgroundColor: !postureTrackerViewModel.isTracking
+ ? Color(0xff77F2A1)
+ : Color(0xfff27777),
+ foregroundColor: Colors.black,
+ ),
+ child: postureTrackerViewModel.isTracking
+ ? const Text("Stop Tracking")
+ : const Text("Start Tracking"),
),
Visibility(
visible: !postureTrackerViewModel.isAvailable,
diff --git a/open_earable/lib/apps/posture_tracker/view/settings_view.dart b/open_earable/lib/apps/posture_tracker/view/settings_view.dart
index 9dd1167..9076399 100644
--- a/open_earable/lib/apps/posture_tracker/view/settings_view.dart
+++ b/open_earable/lib/apps/posture_tracker/view/settings_view.dart
@@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
class SettingsView extends StatefulWidget {
final PostureTrackerViewModel _viewModel;
-
+
SettingsView(this._viewModel);
@override
@@ -24,24 +24,27 @@ class _SettingsViewState extends State {
void initState() {
super.initState();
_viewModel = widget._viewModel;
- _rollAngleThresholdController = TextEditingController(text: _viewModel.badPostureSettings.rollAngleThreshold.toString());
- _pitchAngleThresholdController = TextEditingController(text: _viewModel.badPostureSettings.pitchAngleThreshold.toString());
- _badPostureTimeThresholdController = TextEditingController(text: _viewModel.badPostureSettings.timeThreshold.toString());
- _goodPostureTimeThresholdController = TextEditingController(text: _viewModel.badPostureSettings.resetTimeThreshold.toString());
+ _rollAngleThresholdController = TextEditingController(
+ text: _viewModel.badPostureSettings.rollAngleThreshold.toString());
+ _pitchAngleThresholdController = TextEditingController(
+ text: _viewModel.badPostureSettings.pitchAngleThreshold.toString());
+ _badPostureTimeThresholdController = TextEditingController(
+ text: _viewModel.badPostureSettings.timeThreshold.toString());
+ _goodPostureTimeThresholdController = TextEditingController(
+ text: _viewModel.badPostureSettings.resetTimeThreshold.toString());
}
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(
- title: const Text("Posture Tracker Settings")
- ),
+ appBar: AppBar(title: const Text("Posture Tracker Settings")),
body: ChangeNotifierProvider.value(
- value: _viewModel,
- builder: (context, child) => Consumer(
- builder: (context, postureTrackerViewModel, child) => _buildSettingsView(),
- )
- ),
+ value: _viewModel,
+ builder: (context, child) => Consumer(
+ builder: (context, postureTrackerViewModel, child) =>
+ _buildSettingsView(),
+ )),
+ backgroundColor: Theme.of(context).colorScheme.background,
);
}
@@ -52,13 +55,16 @@ class _SettingsViewState extends State {
color: Theme.of(context).colorScheme.primary,
child: ListTile(
title: Text("Status"),
- trailing: Text(_viewModel.isTracking ? "Tracking" : _viewModel.isAvailable ? "Available" : "Unavailable"),
+ trailing: Text(_viewModel.isTracking
+ ? "Tracking"
+ : _viewModel.isAvailable
+ ? "Available"
+ : "Unavailable"),
),
),
Card(
- color: Theme.of(context).colorScheme.primary,
- child: Column(
- children: [
+ color: Theme.of(context).colorScheme.primary,
+ child: Column(children: [
// add a switch to control the `isActive` property of the `BadPostureSettings`
SwitchListTile(
title: Text("Bad Posture Reminder"),
@@ -70,8 +76,7 @@ class _SettingsViewState extends State {
},
),
Visibility(
- child: Column(
- children: [
+ child: Column(children: [
ListTile(
title: Text("Roll Angle Threshold (in degrees)"),
trailing: SizedBox(
@@ -82,17 +87,18 @@ class _SettingsViewState extends State {
textAlign: TextAlign.end,
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
- contentPadding: EdgeInsets.all(10),
- floatingLabelBehavior:
- FloatingLabelBehavior.never,
- border: OutlineInputBorder(),
- labelText: 'Roll',
- filled: true,
- labelStyle: TextStyle(color: Colors.black),
- fillColor: Colors.white
- ),
+ contentPadding: EdgeInsets.all(10),
+ floatingLabelBehavior:
+ FloatingLabelBehavior.never,
+ border: OutlineInputBorder(),
+ labelText: 'Roll',
+ filled: true,
+ labelStyle: TextStyle(color: Colors.black),
+ fillColor: Colors.white),
keyboardType: TextInputType.number,
- onChanged: (_) { _updatePostureSettings(); },
+ onChanged: (_) {
+ _updatePostureSettings();
+ },
),
),
),
@@ -106,17 +112,18 @@ class _SettingsViewState extends State {
textAlign: TextAlign.end,
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
- contentPadding: EdgeInsets.all(10),
- floatingLabelBehavior:
- FloatingLabelBehavior.never,
- border: OutlineInputBorder(),
- labelText: 'Pitch',
- filled: true,
- labelStyle: TextStyle(color: Colors.black),
- fillColor: Colors.white
- ),
+ contentPadding: EdgeInsets.all(10),
+ floatingLabelBehavior:
+ FloatingLabelBehavior.never,
+ border: OutlineInputBorder(),
+ labelText: 'Pitch',
+ filled: true,
+ labelStyle: TextStyle(color: Colors.black),
+ fillColor: Colors.white),
keyboardType: TextInputType.number,
- onChanged: (_) { _updatePostureSettings(); },
+ onChanged: (_) {
+ _updatePostureSettings();
+ },
),
),
),
@@ -130,17 +137,18 @@ class _SettingsViewState extends State {
textAlign: TextAlign.end,
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
- contentPadding: EdgeInsets.all(10),
- floatingLabelBehavior:
- FloatingLabelBehavior.never,
- border: OutlineInputBorder(),
- labelText: 'Seconds',
- filled: true,
- labelStyle: TextStyle(color: Colors.black),
- fillColor: Colors.white
- ),
+ contentPadding: EdgeInsets.all(10),
+ floatingLabelBehavior:
+ FloatingLabelBehavior.never,
+ border: OutlineInputBorder(),
+ labelText: 'Seconds',
+ filled: true,
+ labelStyle: TextStyle(color: Colors.black),
+ fillColor: Colors.white),
keyboardType: TextInputType.number,
- onChanged: (_) { _updatePostureSettings(); },
+ onChanged: (_) {
+ _updatePostureSettings();
+ },
),
),
),
@@ -154,57 +162,47 @@ class _SettingsViewState extends State {
textAlign: TextAlign.end,
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
- contentPadding: EdgeInsets.all(10),
- floatingLabelBehavior:
- FloatingLabelBehavior.never,
- border: OutlineInputBorder(),
- labelText: 'Seconds',
- filled: true,
- labelStyle: TextStyle(color: Colors.black),
- fillColor: Colors.white
- ),
+ contentPadding: EdgeInsets.all(10),
+ floatingLabelBehavior:
+ FloatingLabelBehavior.never,
+ border: OutlineInputBorder(),
+ labelText: 'Seconds',
+ filled: true,
+ labelStyle: TextStyle(color: Colors.black),
+ fillColor: Colors.white),
keyboardType: TextInputType.number,
- onChanged: (_) { _updatePostureSettings(); },
+ onChanged: (_) {
+ _updatePostureSettings();
+ },
),
),
),
- ]
- ),
- visible: _viewModel.badPostureSettings.isActive
- ),
- ]
- )
-
- ),
+ ]),
+ visible: _viewModel.badPostureSettings.isActive),
+ ])),
Padding(
padding: EdgeInsets.all(8.0),
- child: Row(
- children: [
- Expanded(
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- backgroundColor:
- _viewModel.isTracking
+ child: Row(children: [
+ Expanded(
+ child: ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _viewModel.isTracking
? Colors.green[300]
: Colors.blue[300],
- foregroundColor: Colors.black,
- ),
- onPressed:
- _viewModel.isTracking
+ foregroundColor: Colors.black,
+ ),
+ onPressed: _viewModel.isTracking
? () {
- _viewModel.calibrate();
- Navigator.of(context).pop();
- }
+ _viewModel.calibrate();
+ Navigator.of(context).pop();
+ }
: () => _viewModel.startTracking(),
- child: Text(
- _viewModel.isTracking
+ child: Text(_viewModel.isTracking
? "Calibrate as Main Posture"
- : "Start Calibration"
- ),
- ),
- )
- ]
- ),
+ : "Start Calibration"),
+ ),
+ )
+ ]),
),
],
);
@@ -213,15 +211,16 @@ class _SettingsViewState extends State {
void _updatePostureSettings() {
BadPostureSettings settings = _viewModel.badPostureSettings;
settings.rollAngleThreshold = int.parse(_rollAngleThresholdController.text);
- settings.pitchAngleThreshold = int.parse(_pitchAngleThresholdController.text);
+ settings.pitchAngleThreshold =
+ int.parse(_pitchAngleThresholdController.text);
settings.timeThreshold = int.parse(_badPostureTimeThresholdController.text);
- settings.resetTimeThreshold = int.parse(_goodPostureTimeThresholdController.text);
+ settings.resetTimeThreshold =
+ int.parse(_goodPostureTimeThresholdController.text);
_viewModel.setBadPostureSettings(settings);
}
-
@override
void dispose() {
super.dispose();
}
-}
\ No newline at end of file
+}
diff --git a/open_earable/lib/apps/posture_tracker/view_model/posture_tracker_view_model.dart b/open_earable/lib/apps/posture_tracker/view_model/posture_tracker_view_model.dart
index 1263e8a..0f07bc2 100644
--- a/open_earable/lib/apps/posture_tracker/view_model/posture_tracker_view_model.dart
+++ b/open_earable/lib/apps/posture_tracker/view_model/posture_tracker_view_model.dart
@@ -38,6 +38,7 @@ class PostureTrackerViewModel extends ChangeNotifier {
void stopTracking() {
_attitudeTracker.stop();
+ _badPostureReminder.stop();
notifyListeners();
}
@@ -51,6 +52,7 @@ class PostureTrackerViewModel extends ChangeNotifier {
@override
void dispose() {
+ stopTracking();
_attitudeTracker.cancle();
super.dispose();
}
diff --git a/open_earable/lib/apps/recorder.dart b/open_earable/lib/apps/recorder.dart
index cabd44c..8fdfbf6 100644
--- a/open_earable/lib/apps/recorder.dart
+++ b/open_earable/lib/apps/recorder.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:open_earable/widgets/earable_not_connected_warning.dart';
import 'dart:async';
import 'dart:io';
import 'package:open_earable_flutter/src/open_earable_flutter.dart';
@@ -23,9 +24,12 @@ class _RecorderState extends State {
late List _csvHeader;
late List _labels;
late String _selectedLabel;
+ Timer? _timer;
+ Duration _duration = Duration();
@override
void initState() {
super.initState();
+
_labels = [
"No Label",
"Label 1",
@@ -63,9 +67,32 @@ class _RecorderState extends State {
listFilesInDocumentsDirectory();
}
+ void _startTimer() {
+ _timer = Timer.periodic(Duration(seconds: 1), (timer) {
+ setState(() {
+ _duration = _duration + Duration(seconds: 1);
+ });
+ });
+ }
+
+ void _stopTimer() {
+ if (_timer != null) {
+ _timer!.cancel();
+ _duration = Duration();
+ }
+ }
+
+ String _formatDuration(Duration duration) {
+ String twoDigits(int n) => n.toString().padLeft(2, '0');
+ String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
+ String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
+ return "$twoDigitMinutes:$twoDigitSeconds";
+ }
+
@override
void dispose() {
super.dispose();
+ _timer?.cancel();
_imuSubscription?.cancel();
_barometerSubscription?.cancel();
}
@@ -178,9 +205,11 @@ class _RecorderState extends State {
_recording = false;
});
_csvWriter?.cancelTimer();
+ _stopTimer();
} else {
_csvWriter = CsvWriter(listFilesInDocumentsDirectory);
_csvWriter?.addData(_csvHeader);
+ _startTimer();
setState(() {
_recording = true;
});
@@ -203,116 +232,142 @@ class _RecorderState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
- backgroundColor: Theme.of(context).colorScheme.background,
- appBar: AppBar(
- title: Text('Recorder'),
- ),
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Row(
- children: [
- Padding(
- padding: EdgeInsets.all(16),
- child: ElevatedButton(
- onPressed: startStopRecording,
- style: ElevatedButton.styleFrom(
- backgroundColor: _recording
- ? Color(0xfff27777)
- : Theme.of(context).colorScheme.secondary,
- foregroundColor: Colors.black,
- ),
- child: Text(
- _recording ? "Stop Recording" : "Start Recording",
- style: TextStyle(fontSize: 20),
- ),
+ backgroundColor: Theme.of(context).colorScheme.background,
+ appBar: AppBar(
+ title: Text('Recorder'),
+ ),
+ body: _openEarable.bleManager.connected
+ ? _recorderWidget()
+ : EarableNotConnectedWarning());
+ }
+
+ Widget _recorderWidget() {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
+ child: Text(
+ _formatDuration(_duration),
+ style: TextStyle(
+ fontFamily: 'Digital', // This is a common monospaced font
+ fontSize: 80,
+ fontWeight: FontWeight.normal,
+ ),
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: EdgeInsets.all(16),
+ child: ElevatedButton(
+ onPressed: startStopRecording,
+ style: ElevatedButton.styleFrom(
+ minimumSize: Size(200, 36),
+ backgroundColor: _recording
+ ? Color(0xfff27777)
+ : Theme.of(context).colorScheme.secondary,
+ foregroundColor: Colors.black,
+ ),
+ child: Text(
+ _recording ? "Stop Recording" : "Start Recording",
+ style: TextStyle(fontSize: 20),
),
),
- DropdownButton(
- value: _selectedLabel,
- icon: const Icon(Icons.arrow_drop_down),
- onChanged: (String? newValue) {
- setState(() {
- _selectedLabel = newValue!;
- });
- },
- items: _labels.map>((String value) {
- return DropdownMenuItem(
- value: value,
- child: Text(value),
- );
- }).toList(),
- ),
- ],
- ),
- Text("Recordings", style: TextStyle(fontSize: 20.0)),
- Divider(
- thickness: 2,
- ),
- Expanded(
- child: _recordings.isEmpty
- ? 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(
- "No recordings found",
- style: TextStyle(
- fontSize: 20,
- fontWeight: FontWeight.bold,
- ),
- textAlign: TextAlign.center,
+ ),
+ DropdownButton(
+ value: _selectedLabel,
+ icon: const Icon(Icons.arrow_drop_down),
+ onChanged: (String? newValue) {
+ setState(() {
+ _selectedLabel = newValue!;
+ });
+ },
+ items: _labels.map>((String value) {
+ return DropdownMenuItem(
+ value: value,
+ child: Text(value),
+ );
+ }).toList(),
+ ),
+ ],
+ ),
+ Text("Recordings", style: TextStyle(fontSize: 20.0)),
+ Divider(
+ thickness: 2,
+ ),
+ Expanded(
+ child: _recordings.isEmpty
+ ? 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(
+ "No recordings found",
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
),
+ textAlign: TextAlign.center,
),
- ],
- ),
+ ),
+ ],
),
- ],
- )
- : ListView.builder(
- itemCount: _recordings.length,
- itemBuilder: (context, index) {
- return ListTile(
- title: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Expanded(
- child: FittedBox(
- fit: BoxFit.scaleDown,
- child: Text(
- _recordings[index].path.split("/").last,
- maxLines: 1,
- ),
+ ),
+ ],
+ )
+ : ListView.builder(
+ itemCount: _recordings.length,
+ itemBuilder: (context, index) {
+ return ListTile(
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(
+ _recordings[index].path.split("/").last,
+ maxLines: 1,
),
),
- IconButton(
- icon: Icon(Icons.delete),
- onPressed: () {
- deleteFile(_recordings[index]);
- },
- ),
- ],
- ),
- onTap: () {
- OpenFile.open(_recordings[index].path);
- },
- );
- },
- ),
- )
- ],
- ),
+ ),
+ IconButton(
+ icon: Icon(Icons.delete,
+ color: (_recording && index == 0)
+ ? Color.fromARGB(50, 255, 255, 255)
+ : Colors.white),
+ onPressed: () {
+ (_recording && index == 0)
+ ? null
+ : deleteFile(_recordings[index]);
+ },
+ splashColor: (_recording && index == 0)
+ ? Colors.transparent
+ : Theme.of(context).splashColor,
+ ),
+ ],
+ ),
+ onTap: () {
+ OpenFile.open(_recordings[index].path);
+ },
+ );
+ },
+ ),
+ )
+ ],
),
);
}
diff --git a/open_earable/lib/controls_tab/models/open_earable_settings.dart b/open_earable/lib/controls_tab/models/open_earable_settings.dart
index 72ba357..0e5a4a2 100644
--- a/open_earable/lib/controls_tab/models/open_earable_settings.dart
+++ b/open_earable/lib/controls_tab/models/open_earable_settings.dart
@@ -64,7 +64,7 @@ class OpenEarableSettings {
selectedBarometerOption = "0";
selectedMicrophoneOption = "0";
- selectedAudioPlayerRadio = 0;
+ selectedAudioPlayerRadio = 3; // no radio is selected
selectedJingle = jingleMap[1]!;
selectedWaveForm = waveFormMap[1]!;
selectedFilename = "filename.wav";
diff --git a/open_earable/lib/controls_tab/views/audio_player.dart b/open_earable/lib/controls_tab/views/audio_player.dart
index b18c04b..f5224b4 100644
--- a/open_earable/lib/controls_tab/views/audio_player.dart
+++ b/open_earable/lib/controls_tab/views/audio_player.dart
@@ -184,7 +184,7 @@ class _AudioPlayerCardState extends State {
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Card(
//Audio Player Card
- color: Color(0xff161618),
+ color: Theme.of(context).colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@@ -209,18 +209,30 @@ class _AudioPlayerCardState extends State {
);
}
+ Widget _getAudioPlayerRadio(int index) {
+ return Radio(
+ value: index,
+ groupValue: OpenEarableSettings().selectedAudioPlayerRadio,
+ onChanged: !_openEarable.bleManager.connected
+ ? null
+ : (int? value) {
+ setState(() {
+ OpenEarableSettings().selectedAudioPlayerRadio = value ?? 0;
+ });
+ },
+ fillColor: MaterialStateProperty.resolveWith((states) {
+ if (states.contains(MaterialState.selected)) {
+ return Theme.of(context).colorScheme.secondary;
+ }
+ return Colors.grey;
+ }),
+ );
+ }
+
Widget _getFileNameRow() {
return Row(
children: [
- Radio(
- value: 0,
- groupValue: OpenEarableSettings().selectedAudioPlayerRadio,
- onChanged: (int? value) {
- setState(() {
- OpenEarableSettings().selectedAudioPlayerRadio = value ?? 0;
- });
- },
- ),
+ _getAudioPlayerRadio(0),
Expanded(
child: SizedBox(
height: 37.0,
@@ -255,15 +267,7 @@ class _AudioPlayerCardState extends State {
Widget _getJingleRow() {
return Row(
children: [
- Radio(
- value: 1,
- groupValue: OpenEarableSettings().selectedAudioPlayerRadio,
- onChanged: (int? value) {
- setState(() {
- OpenEarableSettings().selectedAudioPlayerRadio = value ?? 0;
- });
- },
- ),
+ _getAudioPlayerRadio(1),
Expanded(
child: SizedBox(
height: 37.0,
@@ -300,18 +304,10 @@ class _AudioPlayerCardState extends State {
Widget _getFrequencyRow() {
return Row(
children: [
- Radio(
- value: 2,
- groupValue: OpenEarableSettings().selectedAudioPlayerRadio,
- onChanged: (int? value) {
- setState(() {
- OpenEarableSettings().selectedAudioPlayerRadio = value ?? 0;
- });
- },
- ),
+ _getAudioPlayerRadio(2),
SizedBox(
height: 37.0,
- width: 80,
+ width: 75,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 0),
child: TextField(
@@ -411,9 +407,13 @@ class _AudioPlayerCardState extends State {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(
- _waveFormTextController.text,
- style: TextStyle(fontSize: 16.0),
+ Expanded(
+ child: Text(
+ _waveFormTextController.text,
+ style: TextStyle(fontSize: 16.0),
+ overflow: TextOverflow.ellipsis,
+ softWrap: false,
+ ),
),
Icon(Icons.arrow_drop_down),
],
diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart
index 47a572f..7da1668 100644
--- a/open_earable/lib/controls_tab/views/connect.dart
+++ b/open_earable/lib/controls_tab/views/connect.dart
@@ -13,7 +13,7 @@ class ConnectCard extends StatelessWidget {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Card(
- color: Color(0xff161618),
+ color: Theme.of(context).colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
diff --git a/open_earable/lib/controls_tab/views/led_color.dart b/open_earable/lib/controls_tab/views/led_color.dart
index 42ec8ff..253d450 100644
--- a/open_earable/lib/controls_tab/views/led_color.dart
+++ b/open_earable/lib/controls_tab/views/led_color.dart
@@ -150,7 +150,7 @@ class _LEDColorCardState extends State {
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Card(
//LED Color Picker Card
- color: Color(0xff161618),
+ color: Theme.of(context).colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
diff --git a/open_earable/lib/controls_tab/views/sensor_configuration.dart b/open_earable/lib/controls_tab/views/sensor_configuration.dart
index c52c2f5..eee6266 100644
--- a/open_earable/lib/controls_tab/views/sensor_configuration.dart
+++ b/open_earable/lib/controls_tab/views/sensor_configuration.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:open_earable_flutter/src/open_earable_flutter.dart';
import '../models/open_earable_settings.dart';
@@ -97,7 +98,7 @@ class _SensorConfigurationCardState extends State {
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Card(
//Audio Player Card
- color: Color(0xff161618),
+ color: Theme.of(context).colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@@ -132,9 +133,7 @@ class _SensorConfigurationCardState extends State {
(bool? newValue) {
if (newValue != null) {
setState(() {
- print("SELECTED $newValue");
OpenEarableSettings().barometerSettingSelected = newValue;
- print(OpenEarableSettings().barometerSettingSelected);
});
}
}, (String newValue) {
diff --git a/open_earable/lib/main.dart b/open_earable/lib/main.dart
index 6aca296..55f4f11 100644
--- a/open_earable/lib/main.dart
+++ b/open_earable/lib/main.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:open_earable/open_earable_icon_icons.dart';
@@ -8,6 +10,8 @@ import 'apps_tab.dart';
import 'package:open_earable_flutter/src/open_earable_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:open_earable/controls_tab/models/open_earable_settings.dart';
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:app_settings/app_settings.dart';
void main() => runApp(MyApp());
@@ -20,13 +24,15 @@ class MyApp extends StatelessWidget {
useMaterial3: false,
colorScheme: ColorScheme(
brightness: Brightness.dark,
- primary: Color.fromARGB(255, 22, 22, 24),
+ primary: Color.fromARGB(
+ 255, 54, 53, 59), //Color.fromARGB(255, 22, 22, 24)
onPrimary: Colors.white,
secondary: Color.fromARGB(255, 119, 242, 161),
onSecondary: Colors.white,
error: Colors.red,
onError: Colors.black,
- background: Color.fromARGB(255, 54, 53, 59),
+ background: Color.fromARGB(
+ 255, 22, 22, 24), //Color.fromARGB(255, 54, 53, 59)
onBackground: Colors.white,
surface: Color.fromARGB(255, 22, 22, 24),
onSurface: Colors.white),
@@ -44,12 +50,14 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State {
int _selectedIndex = 0;
late OpenEarable _openEarable;
-
+ final flutterReactiveBle = FlutterReactiveBle();
+ late bool alertOpen;
late List _widgetOptions;
-
+ StreamSubscription? blePermissionSubscription;
@override
void initState() {
super.initState();
+ alertOpen = false;
_checkBLEPermission();
_openEarable = OpenEarable();
_widgetOptions = [
@@ -59,6 +67,12 @@ class _MyHomePageState extends State {
];
}
+ @override
+ dispose() {
+ super.dispose();
+ blePermissionSubscription?.cancel();
+ }
+
Future _checkBLEPermission() async {
PermissionStatus status = await Permission.bluetoothConnect.request();
PermissionStatus status2 = await Permission.location.request();
@@ -66,6 +80,52 @@ class _MyHomePageState extends State {
if (status.isGranted) {
print("BLE is working");
}
+ blePermissionSubscription =
+ flutterReactiveBle.statusStream.listen((status) {
+ if (status != BleStatus.ready &&
+ status != BleStatus.unknown &&
+ alertOpen == false) {
+ alertOpen = true;
+ _showBluetoothAlert(context);
+ }
+ });
+ }
+
+ void _showBluetoothAlert(BuildContext context) {
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text("Bluetooth disabled"),
+ content: Text(
+ "Please make sure your device's bluetooth and location services are turned on and this app has been granted permission to use them in the app's settings.\nThis alert can only be closed if these requirements are fulfilled."),
+ actions: [
+ TextButton(
+ child: Text(
+ 'Open App Settings',
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.onBackground),
+ ),
+ onPressed: () {
+ AppSettings.openAppSettings();
+ },
+ ),
+ TextButton(
+ child: Text('OK',
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.onBackground)),
+ onPressed: () {
+ if (flutterReactiveBle.status == BleStatus.ready) {
+ alertOpen = false;
+ Navigator.of(context).pop();
+ }
+ },
+ ),
+ ],
+ );
+ },
+ );
}
void _onItemTapped(int index) {
@@ -106,7 +166,7 @@ class _MyHomePageState extends State {
),
body: _widgetOptions.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
- backgroundColor: Theme.of(context).colorScheme.primary,
+ backgroundColor: Theme.of(context).colorScheme.background,
items: const [
BottomNavigationBarItem(
icon: Padding(
diff --git a/open_earable/lib/sensor_data_tab/sensor_chart.dart b/open_earable/lib/sensor_data_tab/sensor_chart.dart
index 3043b07..9327df4 100644
--- a/open_earable/lib/sensor_data_tab/sensor_chart.dart
+++ b/open_earable/lib/sensor_data_tab/sensor_chart.dart
@@ -1,5 +1,6 @@
import 'dart:async';
+import 'package:flutter/scheduler.dart';
import 'package:open_earable_flutter/src/open_earable_flutter.dart';
import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
@@ -20,7 +21,7 @@ class EarableDataChart extends StatefulWidget {
class _EarableDataChartState extends State {
final OpenEarable _openEarable;
final String _title;
- late List _data;
+ late List _data;
StreamSubscription? _dataSubscription;
_EarableDataChartState(this._openEarable, this._title);
late int _minX = 0;
@@ -34,22 +35,18 @@ class _EarableDataChartState extends State {
late String _sensorName;
int _numDatapoints = 200;
_setupListeners() {
- if (_title == "Pressure Data") {
+ if (_title == "Pressure" || _title == "Temperature") {
_dataSubscription =
_openEarable.sensorManager.subscribeToSensorData(1).listen((data) {
- Map units = {};
- var baroUnits = data["BARO"]["units"];
- baroUnits["Pressure"] =
- "k${baroUnits["Pressure"]}"; //Use kPA instead of Pa for chart
- units.addAll(baroUnits);
- units.addAll(data["TEMP"]["units"]);
+ //units.addAll(data["TEMP"]["units"]);
int timestamp = data["timestamp"];
- BarometerValue barometerValue = BarometerValue(
+ SensorData sensorData = SensorData(
+ name: _sensorName,
timestamp: timestamp,
- pressure: data["BARO"]["Pressure"],
- temperature: data["TEMP"]["Temperature"],
- units: units);
- _updateData(barometerValue);
+ values: [data[_sensorName][_title]],
+ //temperature: data["TEMP"]["Temperature"],
+ units: data[_sensorName]["units"]);
+ _updateData(sensorData);
});
} else {
kalmanX = SimpleKalman(
@@ -67,31 +64,14 @@ class _EarableDataChartState extends State {
_dataSubscription =
_openEarable.sensorManager.subscribeToSensorData(0).listen((data) {
int timestamp = data["timestamp"];
- /*
- XYZValue accelerometerValue = XYZValue(
- timestamp: timestamp,
- x: data["ACC"]["X"],
- y: data["ACC"]["Y"],
- z: data["ACC"]["Z"],
- units: data["ACC"]["units"]);
- XYZValue gyroscopeValue = XYZValue(
- timestamp: timestamp,
- x: data["GYRO"]["X"],
- y: data["GYRO"]["Y"],
- z: data["GYRO"]["Z"],
- units: data["GYRO"]["units"]);
- XYZValue magnetometerValue = XYZValue(
- timestamp: timestamp,
- x: data["MAG"]["X"],
- y: data["MAG"]["Y"],
- z: data["MAG"]["Z"],
- units: data["MAG"]["units"]);
- */
- XYZValue xyzValue = XYZValue(
+ SensorData xyzValue = SensorData(
+ name: _title,
timestamp: timestamp,
- z: kalmanZ.filtered(data[_sensorName]["Z"]),
- x: kalmanX.filtered(data[_sensorName]["X"]),
- y: kalmanY.filtered(data[_sensorName]["Y"]),
+ values: [
+ kalmanX.filtered(data[_sensorName]["X"]),
+ kalmanY.filtered(data[_sensorName]["Y"]),
+ kalmanZ.filtered(data[_sensorName]["Z"]),
+ ],
units: data[_sensorName]["units"]);
_updateData(xyzValue);
@@ -99,34 +79,40 @@ class _EarableDataChartState extends State {
}
}
- _updateData(DataValue value) {
+ _updateData(SensorData value) {
setState(() {
_data.add(value);
_checkLength(_data);
- DataValue? maxXYZValue = maxBy(_data, (DataValue b) => b.getMax());
- DataValue? minXYZValue = minBy(_data, (DataValue b) => b.getMin());
+ SensorData? maxXYZValue = maxBy(_data, (SensorData b) => b.getMax());
+ SensorData? minXYZValue = minBy(_data, (SensorData b) => b.getMin());
if (maxXYZValue == null || minXYZValue == null) {
return;
}
double maxAbsValue =
max(maxXYZValue.getMax().abs(), minXYZValue.getMin().abs());
- _maxY = maxAbsValue;
+ _maxY = (_title == "Pressure" || _title == "Temperature")
+ ? max(0, maxXYZValue.getMax())
+ : maxAbsValue;
- _minY = -maxAbsValue;
+ _minY = (_title == "Pressure" || _title == "Temperature")
+ ? min(0, minXYZValue.getMin())
+ : -maxAbsValue;
_maxX = value.timestamp;
_minX = _data[0].timestamp;
});
}
_getColor(String title) {
- if (title == "Accelerometer Data") {
+ if (title == "Accelerometer") {
return ['#FF6347', '#3CB371', '#1E90FF'];
- } else if (title == "Gyroscope Data") {
+ } else if (title == "Gyroscope") {
return ['#FFD700', '#FF4500', '#D8BFD8'];
- } else if (title == "Magnetometer Data") {
+ } else if (title == "Magnetometer") {
return ['#F08080', '#98FB98', '#ADD8E6'];
- } else if (title == "Pressure Data") {
- return ['#32CD32', '#FFA07A'];
+ } else if (title == "Pressure") {
+ return ['#32CD32'];
+ } else if (title == "Temperature") {
+ return ['#FFA07A'];
}
}
@@ -135,20 +121,25 @@ class _EarableDataChartState extends State {
super.initState();
_data = [];
switch (_title) {
- case 'Pressure Data':
+ case 'Pressure':
_sensorName = 'BARO';
- case 'Accelerometer Data':
+ case 'Temperature':
+ _sensorName = 'TEMP';
+ case 'Accelerometer':
_sensorName = 'ACC';
- case 'Gyroscope Data':
+ case 'Gyroscope':
_sensorName = 'GYRO';
- case 'Magnetometer Data':
+ case 'Magnetometer':
_sensorName = 'MAG';
}
colors = _getColor(_title);
- if (_title == 'Pressure Data') {
+ if (_title == 'Temperature') {
+ _minY = 0;
+ _maxY = 30;
+ } else if (_title == 'Pressure') {
_minY = 0;
- _maxY = 130;
- } else if (_title == "Magnetometer Data") {
+ _maxY = 130000;
+ } else if (_title == "Magnetometer") {
_minY = -200;
_maxY = 200;
} else {
@@ -172,46 +163,37 @@ class _EarableDataChartState extends State {
@override
Widget build(BuildContext context) {
- if (_title == 'Pressure Data') {
+ if (_title == 'Pressure' || _title == 'Temperature') {
seriesList = [
- charts.Series(
- id: 'Pressure${_data.isNotEmpty ? " (${_data[0].units['Pressure']})" : ""}',
+ charts.Series(
+ id: '$_title${_data.isNotEmpty ? " (${_data[0].units[_title]})" : ""}',
colorFn: (_, __) => charts.Color.fromHex(code: colors[0]),
- domainFn: (DataValue data, _) => data.timestamp,
- measureFn: (DataValue data, _) =>
- (data as BarometerValue).pressure / 1000,
- data: _data,
- ),
- charts.Series(
- id: 'Temperature${_data.isNotEmpty ? " (${_data[0].units['Temperature']})" : ""}',
- colorFn: (_, __) => charts.Color.fromHex(code: colors[1]),
- domainFn: (DataValue data, _) => data.timestamp,
- measureFn: (DataValue data, _) =>
- (data as BarometerValue).temperature,
+ domainFn: (SensorData data, _) => data.timestamp,
+ measureFn: (SensorData data, _) => data.values[0],
data: _data,
),
];
} else {
seriesList = [
- charts.Series(
+ 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,
+ domainFn: (SensorData data, _) => data.timestamp,
+ measureFn: (SensorData data, _) => data.values[0],
data: _data,
),
- charts.Series(
+ 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,
+ domainFn: (SensorData data, _) => data.timestamp,
+ measureFn: (SensorData data, _) => data.values[1],
data: _data,
),
- charts.Series(
+ 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,
+ domainFn: (SensorData data, _) => data.timestamp,
+ measureFn: (SensorData data, _) => data.values[2],
data: _data,
),
];
@@ -243,6 +225,8 @@ class _EarableDataChartState extends State {
)
],
primaryMeasureAxis: charts.NumericAxisSpec(
+ tickProviderSpec:
+ charts.BasicNumericTickProviderSpec(desiredTickCount: 7),
renderSpec: charts.GridlineRendererSpec(
labelStyle: charts.TextStyleSpec(
fontSize: 14,
@@ -266,66 +250,30 @@ class _EarableDataChartState extends State {
}
}
-abstract class DataValue {
+class SensorData {
+ final String name;
final int timestamp;
+ final List values;
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);
+ SensorData(
+ {required this.name,
+ required this.timestamp,
+ required this.values,
+ required this.units});
- @override
double getMax() {
- return max(x, max(y, z));
- }
-
- @override
- double getMin() {
- return min(x, min(y, z));
+ return values.reduce(
+ (currentMax, element) => element > currentMax ? element : currentMax);
}
- @override
- String toString() {
- return "timestamp: $timestamp\nx: $x, y: $y, z: $z";
- }
-}
-
-class BarometerValue extends DataValue {
- final double pressure;
- final double temperature;
-
- BarometerValue(
- {required timestamp,
- required this.pressure,
- required this.temperature,
- required units})
- : super(timestamp: timestamp, units: units);
-
- @override
double getMin() {
- return min(pressure / 1000, temperature);
- }
-
- @override
- double getMax() {
- return max(pressure / 1000, temperature);
+ return values.reduce(
+ (currentMin, element) => element < currentMin ? element : currentMin);
}
@override
String toString() {
- return "timestamp: $timestamp\npressure: $pressure, temperature:$temperature";
+ return "sensor name: $name\ntimestamp: $timestamp\nvalues: ${values.join(", ")}";
}
}
diff --git a/open_earable/lib/sensor_data_tab/sensor_data_tab.dart b/open_earable/lib/sensor_data_tab/sensor_data_tab.dart
index 861b3b4..b89b53e 100644
--- a/open_earable/lib/sensor_data_tab/sensor_data_tab.dart
+++ b/open_earable/lib/sensor_data_tab/sensor_data_tab.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:open_earable/sensor_data_tab/earable_3d_model.dart';
+import 'package:open_earable/widgets/earable_not_connected_warning.dart';
import 'package:open_earable_flutter/src/open_earable_flutter.dart';
import 'package:open_earable/sensor_data_tab/sensor_chart.dart';
@@ -20,17 +21,17 @@ class _SensorDataTabState extends State
StreamSubscription? _batteryLevelSubscription;
StreamSubscription? _buttonStateSubscription;
- List accelerometerData = [];
- List gyroscopeData = [];
- List magnetometerData = [];
- List barometerData = [];
+ List accelerometerData = [];
+ List gyroscopeData = [];
+ List magnetometerData = [];
+ List barometerData = [];
_SensorDataTabState(this._openEarable);
@override
void initState() {
super.initState();
- _tabController = TabController(vsync: this, length: 5);
+ _tabController = TabController(vsync: this, length: 6);
if (_openEarable.bleManager.connected) {
_setupListeners();
}
@@ -58,43 +59,12 @@ class _SensorDataTabState extends State
@override
Widget build(BuildContext context) {
if (!_openEarable.bleManager.connected) {
- return _notConnectedWidget();
+ return EarableNotConnectedWarning();
} 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,
- ),
- ),
- ],
- ),
- ),
- ],
- );
- }
-
Widget _buildSensorDataTabs() {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
@@ -108,8 +78,9 @@ class _SensorDataTabState extends State
tabs: [
Tab(text: 'Accel.'),
Tab(text: 'Gyro.'),
- Tab(text: 'Magnet.'),
- Tab(text: 'Pressure'),
+ Tab(text: 'Mag.'),
+ Tab(text: 'Baro.'),
+ Tab(text: 'Temp.'),
Tab(text: '3D'),
],
),
@@ -117,10 +88,11 @@ class _SensorDataTabState extends State
body: TabBarView(
controller: _tabController,
children: [
- EarableDataChart(_openEarable, 'Accelerometer Data'),
- EarableDataChart(_openEarable, 'Gyroscope Data'),
- EarableDataChart(_openEarable, 'Magnetometer Data'),
- EarableDataChart(_openEarable, 'Pressure Data'),
+ EarableDataChart(_openEarable, 'Accelerometer'),
+ EarableDataChart(_openEarable, 'Gyroscope'),
+ EarableDataChart(_openEarable, 'Magnetometer'),
+ EarableDataChart(_openEarable, 'Pressure'),
+ EarableDataChart(_openEarable, 'Temperature'),
Earable3DModel(_openEarable),
],
),
diff --git a/open_earable/lib/widgets/earable_not_connected_warning.dart b/open_earable/lib/widgets/earable_not_connected_warning.dart
new file mode 100644
index 0000000..2ac7d9a
--- /dev/null
+++ b/open_earable/lib/widgets/earable_not_connected_warning.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+
+class EarableNotConnectedWarning extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ 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/pubspec.lock b/open_earable/pubspec.lock
index bac6f8b..1e6d936 100644
--- a/open_earable/pubspec.lock
+++ b/open_earable/pubspec.lock
@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ app_settings:
+ dependency: "direct main"
+ description:
+ name: app_settings
+ sha256: "09bc7fe0313a507087bec1a3baf555f0576e816a760cbb31813a88890a09d9e5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.1"
archive:
dependency: transitive
description:
@@ -232,7 +240,7 @@ packages:
source: hosted
version: "2.2.16"
flutter_reactive_ble:
- dependency: transitive
+ dependency: "direct main"
description:
name: flutter_reactive_ble
sha256: "7a0d245412dc8e1b72ce2adc423808583b42ce824b1be74001ff22c8bb5ada48"
@@ -350,10 +358,10 @@ packages:
description:
path: "."
ref: HEAD
- resolved-ref: eaf50163a03f24822f087a822c767c284f5b3637
+ resolved-ref: "6841c0b704dffbe559961ea80dccf73abf0c293a"
url: "https://github.com/OpenEarable/open_earable_flutter.git"
source: git
- version: "0.0.1"
+ version: "0.0.2"
open_file:
dependency: "direct main"
description:
@@ -427,45 +435,53 @@ packages:
source: hosted
version: "2.2.1"
permission_handler:
- dependency: transitive
+ dependency: "direct main"
description:
name: permission_handler
- sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
+ sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78"
url: "https://pub.dev"
source: hosted
- version: "10.4.5"
+ version: "11.1.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
- sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
+ sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6"
url: "https://pub.dev"
source: hosted
- version: "10.3.6"
+ version: "12.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
- sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
+ sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.2.0"
+ permission_handler_html:
+ dependency: transitive
+ description:
+ name: permission_handler_html
+ sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df"
url: "https://pub.dev"
source: hosted
- version: "9.1.4"
+ version: "0.1.0+2"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
- sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
+ sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1
url: "https://pub.dev"
source: hosted
- version: "3.12.0"
+ version: "4.0.2"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
- sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
+ sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004"
url: "https://pub.dev"
source: hosted
- version: "0.1.3"
+ version: "0.2.0"
petitparser:
dependency: transitive
description:
@@ -691,4 +707,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.2.0-194.0.dev <4.0.0"
- flutter: ">=3.7.0"
+ flutter: ">=3.16.0"
diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml
index 80b0e37..f709a98 100644
--- a/open_earable/pubspec.yaml
+++ b/open_earable/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 1.0.1+1
+version: 1.0.2+1
environment:
sdk: '>=3.0.6 <4.0.0'
@@ -33,7 +33,9 @@ dependencies:
open_earable_flutter:
git:
url: https://github.com/OpenEarable/open_earable_flutter.git
-
+ permission_handler: ^11.1.0
+ flutter_reactive_ble: ^5.2.0
+ app_settings: ^5.1.1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@@ -128,7 +130,9 @@ flutter:
- family: OpenEarableIcon
fonts:
- asset: assets/OpenEarableIcon.ttf
-
+ - family: Digital
+ fonts:
+ - asset: assets/digital-7-mono.ttf
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware