Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2496966 Pull Request, Breathing Assistant #83

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions open_earable/lib/apps_tab/apps_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:open_earable_flutter/open_earable_flutter.dart';
import '../shared/global_theme.dart';
import 'package:open_earable/apps_tab/jump_rope_counter/jump_rope_counter.dart';
import 'powernapper/home_screen.dart';
import 'breathing_assistant/view/main_page/main_page.dart';

class AppInfo {
final String logoPath;
Expand Down Expand Up @@ -189,6 +190,25 @@ class AppsTab extends StatelessWidget {
);
},
),
AppInfo(
logoPath: "lib/apps_tab/breathing_assistant/assets/logo.png",
//iconData: Icons.face_6,
title: "Breathing Assistant",
description: "Calm your mind with guided breathing",
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Material(
child: Theme(
data: materialTheme,
child: MainPage(openEarable),
),
),
),
);
},
),
// ... similarly for other apps
];
}
Expand Down
37 changes: 37 additions & 0 deletions open_earable/lib/apps_tab/breathing_assistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Breathing Assistant App

## Overview
The **Breathing Assistant App** is a Flutter-based application designed for the OpenEarable to help users reduce anxiety, improve relaxation, and promote better sleep through the scientifically-backed 4-7-8 breathing technique. It provides real-time posture feedback based on sensor data, ensuring an effective and comfortable breathing exercise experience.

---

## Features
### 1. **Breathing Exercises**
- Supports the **4-7-8 breathing technique** with three phases:
- **Inhale**: Breathe in for 4 seconds.
- **Hold**: Hold your breath for 7 seconds.
- **Exhale**: Exhale for 8 seconds.
- Visual cues guide the breathing process.

### 2. **Posture Feedback**
- Real-time feedback based on roll and pitch angles from the IMU sensor.
- Tailored messages ensure correct posture:
- Sitting: Encourages a neutral upright position.
- Lying: Guides the user to lay down comfortably.

### 3. **Customization**
- Adjustable **session durations** (4, 6, 10, or 12 minutes).
- **Night Mode** for low-light environments.

### 4. **Instructions**
- A "How to Use" section with detailed guidance.
- Clear explanations of feedback colors (e.g., Green for correct posture, Red/Yellow for adjustments).

---

## Technologies Used
- **Flutter** for cross-platform mobile development.
- **OpenEarable Sensors** for real-time posture tracking using IMU (roll/pitch).
- **Gradle** for project build and dependency management.

---
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import 'dart:async';
import 'dart:math';
import 'package:open_earable_flutter/open_earable_flutter.dart';

/// A tracker for monitoring and providing feedback on position during breathing sessions.
///
/// The `BreathingSensorTracker`:
/// - Configures sensors for position tracking.
/// - Monitors sensor data for position feedback.
/// - Provides feedback streams to inform the user of their position status.
class BreathingSensorTracker {
final OpenEarable openEarable;

late StreamController<String> postureFeedbackStreamController =
StreamController.broadcast();

StreamSubscription? _sensorSubscription;

Stream<String> get postureFeedbackStream =>
postureFeedbackStreamController.stream;

final EWMA rollEWMA = EWMA(0.5);
final EWMA pitchEWMA = EWMA(0.5);

final PostureStateTracker postureStateTracker = PostureStateTracker();

double rollThreshold = 30.0;
double pitchThreshold = 55.0;

/// Determines whether pitch feedback is relevant (e.g., for sitting mode).
bool isPitchRelevant = true;

/// Constructor for the `BreathingSensorTracker`.
///
/// - [openEarable]: An instance of the OpenEarable framework.
BreathingSensorTracker(this.openEarable);

/// Sets position thresholds and relevancy based on the specified mode.
///
/// - [mode]: The mode of the session (e.g., 'sitting', 'lying').
void setMode(String mode) {
if (mode == 'sitting') {
rollThreshold = 30.0;
pitchThreshold = 55.0;
isPitchRelevant = true;
} else if (mode == 'lying') {
rollThreshold = 30.0;
pitchThreshold = 55.0;
isPitchRelevant = false; // Potential pillow makes pitch irrelevant
}
}

/// Configures the sensors for position tracking.
void configureSensors() {
openEarable.sensorManager.writeSensorConfig(
OpenEarableSensorConfig(sensorId: 0, samplingRate: 30, latency: 0),
);
}

/// Starts tracking position by subscribing to sensor data and providing feedback.
void startTracking() {
if (postureFeedbackStreamController.isClosed) {
postureFeedbackStreamController = StreamController.broadcast();
}

// Configure sensors before starting tracking.
configureSensors();

_sensorSubscription?.cancel();
_sensorSubscription =
openEarable.sensorManager.subscribeToSensorData(0).listen((sensorData) {
if (sensorData['EULER'] == null) {
postureFeedbackStreamController.add('Error: No position data available.');
return;
}

final roll = rollEWMA.update(sensorData['EULER']?['ROLL'] ?? 0.0);
final pitch = pitchEWMA.update(sensorData['EULER']?['PITCH'] ?? 0.0);

final feedback = _getPostureFeedback(roll, pitch);
if (feedback != null) {
postureFeedbackStreamController.add(feedback);
} else {
postureFeedbackStreamController.add('Correct Position');
}
});
}

/// Stops tracking position and optionally closes the feedback stream.
///
/// - [closeStream]: If true, closes the position feedback stream.
void stopTracking({bool closeStream = false}) {
_sensorSubscription?.cancel();
if (closeStream) {
postureFeedbackStreamController.close();
}
}

/// Converts radians to degrees.
double radToDeg(double rad) => rad * (180 / pi);

/// Evaluates position and provides feedback based on roll and pitch thresholds.
///
/// - [roll]: The roll value in radians.
/// - [pitch]: The pitch value in radians.
/// - Returns: A string message with position feedback, or null if position is correct.
String? _getPostureFeedback(double roll, double pitch) {
if (radToDeg(roll).abs() > rollThreshold) {
if (!isPitchRelevant) {
return roll > 0
? "You're tilting to the right. Lay down on your back."
: "You're tilting to the left. Lay down on your back.";
} else {
return roll > 0
? "You're tilting to the right. Keep your shoulders level."
: "You're tilting to the left. Keep your shoulders level.";
}
}
if (isPitchRelevant && radToDeg(pitch).abs() > pitchThreshold) {
return pitch > 0
? "You're leaning forward. Straighten your back."
: "You're leaning backward. Sit upright with a neutral back posture.";
}
return null; // Position is correct.
}
}

/// A tracker for managing position states and their transitions.
class PostureStateTracker {
DateTime? _lastBadPostureTime;
DateTime? _lastGoodPostureTime;

final int badPostureDurationThreshold = 3;
final int resetThreshold = 2;

/// Evaluates whether the user is in bad posture based on timing thresholds.
///
/// - [isBadPosture]: Whether the current posture is bad.
/// - Returns: True if the user holds bad posture for the defined threshold.
bool evaluatePosture(bool isBadPosture) {
final now = DateTime.now();

if (isBadPosture) {
if (_lastBadPostureTime == null) {
_lastBadPostureTime = now;
}

final durationInBadPosture =
now.difference(_lastBadPostureTime!).inSeconds;
if (durationInBadPosture >= badPostureDurationThreshold) {
_lastGoodPostureTime = null;
return true;
}
} else {
if (_lastGoodPostureTime == null) {
_lastGoodPostureTime = now;
}

final durationInGoodPosture =
now.difference(_lastGoodPostureTime!).inSeconds;
if (durationInGoodPosture >= resetThreshold) {
_lastBadPostureTime = null;
}
}

return false;
}
}

/// A utility class for calculating Exponential Weighted Moving Averages (EWMA).
class EWMA {
final double _alpha;
double _oldValue = 0;
EWMA(this._alpha);

double update(double newValue) {
_oldValue = _alpha * newValue + (1 - _alpha) * _oldValue;
return _oldValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import 'dart:async';

import 'breathing_sensor_tracker.dart';

/// A model class to manage the state and functionality of a breathing session.
///
/// The `BreathingSessionModel` handles:
/// - Managing breathing phases (Inhale, Hold, Exhale).
/// - Communicating with the `BreathingSensorTracker` to monitor posture feedback.
/// - Providing streams for UI updates (breathing phase, posture feedback).
class BreathingSessionModel {
final StreamController<String> _breathingPhaseController =
StreamController.broadcast();

Stream<String> get breathingPhaseStream => _breathingPhaseController.stream;

final StreamController<String> _postureFeedbackController =
StreamController.broadcast();

Stream<String> get postureFeedbackStream =>
_postureFeedbackController.stream;

/// Timer to manage the duration of each breathing phase.
Timer? _breathingTimer;

/// Tracker to monitor and provide posture feedback during the session.
BreathingSensorTracker? sensorTracker;

/// The index of the current phase in the breathing cycle.
int _phaseIndex = 0;

/// The list of breathing phases and their durations.
final List<Map<String, dynamic>> _phases = [
{'phase': 'Inhale', 'duration': 4},
{'phase': 'Hold', 'duration': 7},
{'phase': 'Exhale', 'duration': 8},
];

List<Map<String, dynamic>> get phases => _phases;

bool _isNightMode = false;

/// The total duration of the breathing session in seconds.
int _sessionDuration = 240; // Default 4 minutes.

int _elapsedTime = 0;

/// The posture mode (e.g., 'sitting' or 'lying').
String _positionMode = 'sitting'; // Default mode is 'sitting'.

bool get isNightMode => _isNightMode;

set isNightMode(bool value) {
_isNightMode = value;
}

int get elapsedTime => _elapsedTime;

int get sessionDuration => _sessionDuration;

set sessionDuration(int duration) {
_sessionDuration = duration;
}

/// Gets the current posture mode (e.g., 'sitting' or 'lying').
String get positionMode => _positionMode;

/// Sets the posture mode and updates the sensor tracker thresholds accordingly.
///
/// - [mode]: The posture mode to set (e.g., 'sitting' or 'lying').
void setMode(String mode) {
_positionMode = mode;
sensorTracker?.setMode(mode);
}

/// Starts the breathing session by resetting state, starting posture tracking,
/// and initiating the breathing cycle
void startSession() {
stopSession(sessionEnded: false);
_phaseIndex = 0;
_elapsedTime = 0;

sensorTracker?.startTracking();

sensorTracker?.postureFeedbackStream.listen((feedback) {
print('Feedback Received in Model: $feedback');
_postureFeedbackController.add(feedback);
});

_nextBreathingPhase();
}

/// Moves to the next breathing phase or stops the session if complete.
void _nextBreathingPhase() {
if (_elapsedTime >= sessionDuration) {
stopSession();
return;
}

final currentPhase = _phases[_phaseIndex];
_breathingPhaseController.add(currentPhase['phase']);

_breathingTimer = Timer(
Duration(seconds: currentPhase['duration']),
() {
_elapsedTime += currentPhase['duration'] as int;
_phaseIndex = (_phaseIndex + 1) % _phases.length;

// Check for session completion.
if (_elapsedTime >= sessionDuration) {
stopSession();
} else {
_nextBreathingPhase();
}
},
);
}

/// Stops the breathing session and sensor tracking.
///
/// - [sessionEnded]: If true, emits a 'Completed' signal to indicate the session ended.
void stopSession({bool sessionEnded = true}) {
_breathingTimer?.cancel();

sensorTracker?.stopTracking();

if (sessionEnded) {
_breathingPhaseController.add('Completed');
}
}
}
Loading