diff --git a/mobile/lib/src/application/eeg_data/data_cubit.dart b/mobile/lib/src/application/eeg_data/data_cubit.dart index 825ed24c..4a7e7d53 100644 --- a/mobile/lib/src/application/eeg_data/data_cubit.dart +++ b/mobile/lib/src/application/eeg_data/data_cubit.dart @@ -1,10 +1,10 @@ -import 'package:polydodo/src/domain/acquisition_device/i_acquisition_device_repository.dart'; -import 'package:polydodo/src/domain/eeg_data/i_eeg_data_repository.dart'; - import 'package:bloc/bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -part './data_states.dart'; +import 'package:polydodo/src/domain/acquisition_device/i_acquisition_device_repository.dart'; +import 'package:polydodo/src/domain/eeg_data/i_eeg_data_repository.dart'; +import 'package:polydodo/src/domain/eeg_data/signal_result.dart'; +import 'package:polydodo/src/application/eeg_data/data_states.dart'; class DataCubit extends Cubit { final IAcquisitionDeviceRepository _deviceRepository; @@ -25,4 +25,28 @@ class DataCubit extends Cubit { _deviceRepository.stopDataStream(); _eegDataRepository.stopRecordingFromStream(); } + + Future startSignalValidation() async { + _eegDataRepository.initialize(); + _eegDataRepository.testSignal( + await _deviceRepository.startDataStream(), signalCallback); + + emit(DataStateTestSignalInProgress( + SignalResult.untested, SignalResult.untested)); + } + + void signalCallback( + SignalResult fpzCzChannelResult, SignalResult pzOzChannelResult, + [Exception e]) { + if (e != null) { + emit(DataStateTestSignalFailure(e)); + } else if (fpzCzChannelResult == SignalResult.good && + pzOzChannelResult == SignalResult.good) { + _eegDataRepository.stopRecordingFromStream(); + emit(DataStateTestSignalSuccess(fpzCzChannelResult, pzOzChannelResult)); + } else { + emit( + DataStateTestSignalInProgress(fpzCzChannelResult, pzOzChannelResult)); + } + } } diff --git a/mobile/lib/src/application/eeg_data/data_states.dart b/mobile/lib/src/application/eeg_data/data_states.dart index 239ab2e6..be10fc83 100644 --- a/mobile/lib/src/application/eeg_data/data_states.dart +++ b/mobile/lib/src/application/eeg_data/data_states.dart @@ -1,7 +1,27 @@ -part of './data_cubit.dart'; +import 'package:polydodo/src/domain/eeg_data/signal_result.dart'; abstract class DataState {} class DataStateInitial extends DataState {} class DataStateRecording extends DataState {} + +class DataStateTestSignalInProgress extends DataState { + final SignalResult channelOneResult; + final SignalResult channelTwoResult; + + DataStateTestSignalInProgress(this.channelOneResult, this.channelTwoResult); +} + +class DataStateTestSignalSuccess extends DataState { + final SignalResult fpzCzChannelResult; + final SignalResult pzOzChannelResult; + + DataStateTestSignalSuccess(this.fpzCzChannelResult, this.pzOzChannelResult); +} + +class DataStateTestSignalFailure extends DataState { + final Exception cause; + + DataStateTestSignalFailure(this.cause); +} diff --git a/mobile/lib/src/domain/eeg_data/i_eeg_data_repository.dart b/mobile/lib/src/domain/eeg_data/i_eeg_data_repository.dart index 12c3781c..3c4dfa3d 100644 --- a/mobile/lib/src/domain/eeg_data/i_eeg_data_repository.dart +++ b/mobile/lib/src/domain/eeg_data/i_eeg_data_repository.dart @@ -1,8 +1,12 @@ +import 'package:polydodo/src/domain/eeg_data/signal_result.dart'; + abstract class IEEGDataRepository { void initialize(); void createRecordingFromStream(Stream> stream); void stopRecordingFromStream(); + void testSignal(Stream> stream, + Function(SignalResult, SignalResult, [Exception]) callback); // todo: implement export and import void importData(); void exportData(); diff --git a/mobile/lib/src/domain/eeg_data/signal_result.dart b/mobile/lib/src/domain/eeg_data/signal_result.dart new file mode 100644 index 00000000..e7415eb3 --- /dev/null +++ b/mobile/lib/src/domain/eeg_data/signal_result.dart @@ -0,0 +1 @@ +enum SignalResult { railed, near_railed, good, untested, invalid } diff --git a/mobile/lib/src/infrastructure/connection_repositories/eeg_data_repository.dart b/mobile/lib/src/infrastructure/connection_repositories/eeg_data_repository.dart index 44e1f54f..156cd279 100644 --- a/mobile/lib/src/infrastructure/connection_repositories/eeg_data_repository.dart +++ b/mobile/lib/src/infrastructure/connection_repositories/eeg_data_repository.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:csv/csv.dart'; import 'package:path_provider/path_provider.dart'; import 'package:polydodo/src/domain/eeg_data/eeg_data.dart'; import 'package:polydodo/src/domain/eeg_data/i_eeg_data_repository.dart'; +import 'package:polydodo/src/domain/eeg_data/signal_result.dart'; import 'package:polydodo/src/domain/unique_id.dart'; import 'package:polydodo/src/infrastructure/eeg_data_transformers/baseOpenBCITransformer.dart'; import 'package:polydodo/src/infrastructure/eeg_data_transformers/cytonTransformer.dart'; @@ -16,19 +18,19 @@ import 'package:pedantic/pedantic.dart'; import 'package:intl/intl.dart'; class EEGDataRepository implements IEEGDataRepository { - EEGData _recordingData; - BaseOpenBCITransformer, List> currentStreamTransformer; - final GanglionTransformer, List> _ganglionTransformer = GanglionTransformer, List>.broadcast(); - final CytonTransformer, List> _cytonTransformer = CytonTransformer.broadcast(); + BaseOpenBCITransformer, List> currentStreamTransformer; BaseOpenBCITransformer, List> _currentTransformer; StreamSubscription _currentTransformerStream; - StreamingSharedPreferences _preferences; + EEGData _recordingData; + double _fpzCzChannelMax = 0; + double _pzOzChannelMax = 0; + int _dataCount = 0; @override void initialize() async { @@ -58,6 +60,8 @@ class EEGDataRepository implements IEEGDataRepository { // todo: move save future to another file unawaited(_currentTransformerStream.cancel()); + if (_recordingData == null) return; + final directory = await getExternalStorageDirectory(); final pathOfTheFileToWrite = directory.path + '/' + _recordingData.fileName + '.txt'; @@ -70,6 +74,48 @@ class EEGDataRepository implements IEEGDataRepository { await file.writeAsString(csv); } + @override + void testSignal(Stream> stream, + Function(SignalResult, SignalResult, [Exception]) callback) { + _dataCount = 0; + _currentTransformer.reset(); + _currentTransformerStream = stream + .asBroadcastStream() + .transform(_currentTransformer) + .listen((data) => _checkSignalData(data, callback)); + } + + void _checkSignalData( + List data, Function(SignalResult, SignalResult, [Exception]) callback) { + _dataCount++; + + _fpzCzChannelMax = max(_fpzCzChannelMax, data[1].abs()); + _pzOzChannelMax = max(_pzOzChannelMax, data[2].abs()); + + if (_dataCount == 1000) { + var signalOneResult = _getResult(_fpzCzChannelMax); + var signalTwoResult = _getResult(_pzOzChannelMax); + + callback(signalOneResult, signalTwoResult); + + _dataCount = 0; + _fpzCzChannelMax = 0; + _pzOzChannelMax = 0; + } + } + + SignalResult _getResult(double maxValue) { + var result = SignalResult.good; + + if (maxValue > MAX_SIGNAL_VALUE * THRESHOLD_RAILED_WARN) { + result = (maxValue > MAX_SIGNAL_VALUE * THRESHOLD_RAILED) + ? SignalResult.railed + : SignalResult.near_railed; + } + + return result; + } + // todo: implement export and import @override void importData() {} diff --git a/mobile/lib/src/infrastructure/constants.dart b/mobile/lib/src/infrastructure/constants.dart index 98848681..58ebdd21 100644 --- a/mobile/lib/src/infrastructure/constants.dart +++ b/mobile/lib/src/infrastructure/constants.dart @@ -1,3 +1,8 @@ +const THRESHOLD_RAILED = 0.9; +const THRESHOLD_RAILED_WARN = 0.75; + +const MAX_SIGNAL_VALUE = 185000; + const START_STREAM_CHAR = 'b'; const STOP_STREAM_CHAR = 's'; diff --git a/mobile/lib/src/presentation/navigation/routes/router.dart b/mobile/lib/src/presentation/navigation/routes/router.dart index 134c94ef..a4b0f1a3 100644 --- a/mobile/lib/src/presentation/navigation/routes/router.dart +++ b/mobile/lib/src/presentation/navigation/routes/router.dart @@ -2,9 +2,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route_annotations.dart'; import 'package:polydodo/src/presentation/pages/bluetooth_page/bluetoothSelector_page.dart'; import 'package:polydodo/src/presentation/pages/dashboard/dashboard_page.dart'; +import 'package:polydodo/src/presentation/pages/record_sleep/record_sleep_guide_page.dart'; +import 'package:polydodo/src/presentation/pages/record_sleep/record_sleep_recording_page.dart'; +import 'package:polydodo/src/presentation/pages/record_sleep/record_sleep_validate_page.dart'; + import 'package:polydodo/src/presentation/pages/sleep_history_page/sleep_history_page.dart'; import 'package:polydodo/src/presentation/pages/night_stats_page/night_stats_page.dart'; -import 'package:polydodo/src/presentation/pages/record_sleep/record_sleep_guide_page.dart'; @MaterialAutoRouter( generateNavigationHelperExtension: true, @@ -19,6 +22,9 @@ import 'package:polydodo/src/presentation/pages/record_sleep/record_sleep_guide_ CustomRoute( page: RecordSleepValidatePage, transitionsBuilder: TransitionsBuilders.fadeIn), + CustomRoute( + page: RecordSleepRecordingPage, + transitionsBuilder: TransitionsBuilders.fadeIn), CustomRoute( page: BluetoothSelectorPage, transitionsBuilder: TransitionsBuilders.fadeIn), diff --git a/mobile/lib/src/presentation/pages/record_sleep/record_sleep_guide_page.dart b/mobile/lib/src/presentation/pages/record_sleep/record_sleep_guide_page.dart index bb12f70d..9514770e 100644 --- a/mobile/lib/src/presentation/pages/record_sleep/record_sleep_guide_page.dart +++ b/mobile/lib/src/presentation/pages/record_sleep/record_sleep_guide_page.dart @@ -1,14 +1,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:polydodo/src/application/eeg_data/data_cubit.dart'; +import 'package:polydodo/src/application/blocs.dart'; import 'package:polydodo/src/presentation/navigation/navdrawer_tabs.dart'; import 'package:polydodo/src/presentation/navigation/navdrawer_widget.dart'; import 'package:polydodo/src/presentation/navigation/routes/router.gr.dart'; -part 'record_sleep_recording_page.dart'; -part 'record_sleep_validate_page.dart'; - class RecordSleepGuidePage extends StatelessWidget { @override Widget build(BuildContext context) { @@ -23,6 +20,8 @@ class RecordSleepGuidePage extends StatelessWidget { ), floatingActionButton: FloatingActionButton.extended( onPressed: () { + // todo: Place start validation at last page of guide or skip guide button + BlocProvider.of(context).startSignalValidation(); ExtendedNavigator.of(context).replace(Routes.recordSleepValidatePage); }, icon: Icon(Icons.radio_button_checked), diff --git a/mobile/lib/src/presentation/pages/record_sleep/record_sleep_recording_page.dart b/mobile/lib/src/presentation/pages/record_sleep/record_sleep_recording_page.dart index 5490f9d3..68306b51 100644 --- a/mobile/lib/src/presentation/pages/record_sleep/record_sleep_recording_page.dart +++ b/mobile/lib/src/presentation/pages/record_sleep/record_sleep_recording_page.dart @@ -1 +1,48 @@ -part of 'record_sleep_guide_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:polydodo/src/application/eeg_data/data_cubit.dart'; +import 'package:polydodo/src/application/eeg_data/data_states.dart'; +import 'package:polydodo/src/presentation/navigation/navdrawer_tabs.dart'; +import 'package:polydodo/src/presentation/navigation/navdrawer_widget.dart'; + +class RecordSleepRecordingPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Record Sleep')), + drawer: NavDrawer(activeTab: NavdrawerTab.RecordSleep), + body: BlocConsumer( + listener: (context, state) { + print(state.runtimeType); + }, + builder: (context, state) { + if (state is DataStateTestSignalSuccess) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RaisedButton( + child: Text('Start'), + onPressed: () => + BlocProvider.of(context).startStreaming(), + ) + ]), + ); + } else { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RaisedButton( + child: Text('Stop'), + onPressed: () => + BlocProvider.of(context).stopStreaming(), + ), + ]), + ); + } + }, + ), + ); + } +} diff --git a/mobile/lib/src/presentation/pages/record_sleep/record_sleep_validate_page.dart b/mobile/lib/src/presentation/pages/record_sleep/record_sleep_validate_page.dart index b4ff4ac0..f0a49bc5 100644 --- a/mobile/lib/src/presentation/pages/record_sleep/record_sleep_validate_page.dart +++ b/mobile/lib/src/presentation/pages/record_sleep/record_sleep_validate_page.dart @@ -1,43 +1,83 @@ -part of 'record_sleep_guide_page.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:polydodo/src/application/eeg_data/data_cubit.dart'; +import 'package:polydodo/src/application/eeg_data/data_states.dart'; +import 'package:polydodo/src/presentation/navigation/navdrawer_tabs.dart'; +import 'package:polydodo/src/presentation/navigation/navdrawer_widget.dart'; +import 'package:polydodo/src/presentation/navigation/routes/router.gr.dart'; +import 'package:polydodo/src/presentation/pages/record_sleep/signal_section.dart'; class RecordSleepValidatePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Record Sleep')), + appBar: AppBar( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + centerTitle: true, + iconTheme: IconThemeData(color: Colors.black), + ), drawer: NavDrawer(activeTab: NavdrawerTab.RecordSleep), body: BlocConsumer( listener: (context, state) { print(state.runtimeType); }, builder: (context, state) { - if (state is DataStateInitial) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - RaisedButton( - child: Text('Start'), - onPressed: () => - BlocProvider.of(context).startStreaming(), - ), - ]), - ); - } else { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - RaisedButton( - child: Text('Stop'), - onPressed: () => - BlocProvider.of(context).stopStreaming(), - ), - ]), - ); - } + return Center( + child: Column(children: [ + _buildValidationCircle(context, state), + buildSignalSection(state), + if (state is DataStateTestSignalSuccess) + _buildNextButton(context), + ]), + ); }, ), ); } } + +Widget _buildValidationCircle(var context, var state) { + return Center( + child: Stack(alignment: Alignment.center, children: [ + SizedBox(width: 200, height: 200, child: _buildProgressIndicator(state)), + _buildProgressIndicatorContent(state), + ])); +} + +Widget _buildProgressIndicator(var state) { + return (state is DataStateTestSignalSuccess) + ? CircularProgressIndicator( + strokeWidth: 10, + valueColor: AlwaysStoppedAnimation(Colors.green), + value: 100, + ) + : CircularProgressIndicator( + strokeWidth: 10, + ); +} + +Widget _buildProgressIndicatorContent(var state) { + return (state is DataStateTestSignalSuccess) + ? Icon( + Icons.check, + color: Colors.green, + size: 50, + ) + : Text( + 'Validating ...', + style: TextStyle(fontWeight: FontWeight.bold), + ); +} + +Widget _buildNextButton(var context) { + return Expanded( + child: Align( + alignment: Alignment.bottomRight, + child: FlatButton( + child: Text('Next', + style: TextStyle(color: Colors.blue, fontSize: 15.0)), + onPressed: () => ExtendedNavigator.of(context) + .replace(Routes.recordSleepRecordingPage)))); +} diff --git a/mobile/lib/src/presentation/pages/record_sleep/signal_section.dart b/mobile/lib/src/presentation/pages/record_sleep/signal_section.dart new file mode 100644 index 00000000..d4989026 --- /dev/null +++ b/mobile/lib/src/presentation/pages/record_sleep/signal_section.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:polydodo/src/application/eeg_data/data_states.dart'; +import 'package:polydodo/src/domain/eeg_data/signal_result.dart'; + +Container buildSignalSection(var state) { + var fpzCzChannelResult = SignalResult.invalid; + var pzOzChannelResult = SignalResult.invalid; + + if (state is DataStateTestSignalInProgress || + state is DataStateTestSignalSuccess) { + fpzCzChannelResult = state.fpzCzChannelResult; + pzOzChannelResult = state.pzOzChannelResult; + } + + return Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildSignalInformation('Fpz-Cz', fpzCzChannelResult), + _buildSignalInformation('Pz-Oz', pzOzChannelResult) + ], + ), + ); +} + +Container _buildSignalInformation(var signalName, var signalResult) { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text(signalName), _getIcon(signalResult)], + ), + _getErrorText(signalResult) + ], + ), + ); +} + +Widget _getIcon(var signalResult) { + switch (signalResult) { + case SignalResult.near_railed: + return Icon(Icons.warning, color: Colors.yellow[800]); + case SignalResult.railed: + return Icon(Icons.error, color: Colors.red); + case SignalResult.untested: + return Icon(Icons.hourglass_empty, color: Colors.blue); + default: + return Container(); + } +} + +Text _getErrorText(var signalResult) { + switch (signalResult) { + case SignalResult.near_railed: + return Text('Electrode is nearly railed'); + case SignalResult.railed: + return Text('Electrode is railed'); + case SignalResult.good: + return Text('Valid Signal'); + default: + return Text(''); + } +}