diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 22862eb9..d167ca07 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -33,9 +33,14 @@ android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background" /> + + + + + diff --git a/mobile/lib/src/application/device/device_selector_cubit.dart b/mobile/lib/src/application/device/device_selector_cubit.dart index 90412b90..8db63920 100644 --- a/mobile/lib/src/application/device/device_selector_cubit.dart +++ b/mobile/lib/src/application/device/device_selector_cubit.dart @@ -4,12 +4,15 @@ import 'package:bloc/bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:polydodo/src/domain/acquisition_device/acquisition_device.dart'; import 'package:polydodo/src/domain/acquisition_device/i_acquisition_device_repository.dart'; +import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; import 'device_selector_state.dart'; class DeviceSelectorCubit extends Cubit { final IAcquisitionDeviceRepository _deviceRepository; StreamSubscription> _acquisitionDeviceStream; + // todo: remove this variable, also test that switch works correctly once UI is done + bool usingBluetooth = true; DeviceSelectorCubit(this._deviceRepository) : super(DeviceInitial()) { startSearching(); @@ -47,4 +50,14 @@ class DeviceSelectorCubit extends Cubit { _deviceRepository.disconnect(); startSearching(); } + + // todo: change bluetooth preferences in the preference section of the app + void swapBluetooth() async { + print("swap"); + usingBluetooth = !usingBluetooth; + StreamingSharedPreferences _prefs = + await StreamingSharedPreferences.instance; + + _prefs.setBool('using_bluetooth', usingBluetooth); + } } diff --git a/mobile/lib/src/application/eeg_data/data_cubit.dart b/mobile/lib/src/application/eeg_data/data_cubit.dart index 3f69dbce..d8b21fd4 100644 --- a/mobile/lib/src/application/eeg_data/data_cubit.dart +++ b/mobile/lib/src/application/eeg_data/data_cubit.dart @@ -14,6 +14,7 @@ class DataCubit extends Cubit { Future startStreaming() async { emit(DataStateRecording()); + _eegDataRepository.initialize(); _eegDataRepository .createRecordingFromStream(await _deviceRepository.startDataStream()); } 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 d1e12b38..12c3781c 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,4 +1,5 @@ abstract class IEEGDataRepository { + void initialize(); void createRecordingFromStream(Stream> stream); void stopRecordingFromStream(); diff --git a/mobile/lib/src/infrastructure/connection_repositories/acquisition_device_repository.dart b/mobile/lib/src/infrastructure/connection_repositories/acquisition_device_repository.dart new file mode 100644 index 00000000..44a52fbd --- /dev/null +++ b/mobile/lib/src/infrastructure/connection_repositories/acquisition_device_repository.dart @@ -0,0 +1,72 @@ +import 'dart:async'; + +import 'package:polydodo/src/domain/acquisition_device/acquisition_device.dart'; +import 'package:polydodo/src/domain/acquisition_device/i_acquisition_device_repository.dart'; +import 'package:polydodo/src/infrastructure/connection_repositories/bluetooth_repository.dart'; +import 'package:polydodo/src/infrastructure/connection_repositories/serial_repository.dart'; +import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; + +class AcquisitionDeviceRepository implements IAcquisitionDeviceRepository { + final BluetoothRepository _bluetoothRepository = new BluetoothRepository(); + final SerialRepository _serialRepository = new SerialRepository(); + IAcquisitionDeviceRepository _currentRepository; + + StreamSubscription _bluetoothStream; + StreamSubscription _serialStream; + StreamSubscription _currentStream; + + StreamController> _acquisitionDeviceController; + StreamingSharedPreferences _preferences; + + AcquisitionDeviceRepository() { + _currentRepository = _serialRepository; + _acquisitionDeviceController = new StreamController(); + } + + Future initializeRepository() async { + if (_preferences == null) { + _preferences = await StreamingSharedPreferences.instance; + _preferences + .getBool('using_bluetooth', defaultValue: false) + .listen((usingBluetooth) { + disconnect(); + _currentStream.pause(); + _currentRepository = + usingBluetooth ? _bluetoothRepository : _serialRepository; + _currentRepository.initializeRepository(); + _currentStream = usingBluetooth ? _bluetoothStream : _serialStream; + _currentStream.resume(); + }); + + _serialStream = _serialRepository.watch().listen((event) { + _acquisitionDeviceController.add(event); + }); + _bluetoothStream = _bluetoothRepository.watch().listen((event) { + _acquisitionDeviceController.add(event); + }); + + _currentStream = _bluetoothStream; + _serialStream.pause(); + } + } + + void connect(AcquisitionDevice device, Function(bool, Exception) callback) { + _currentRepository.connect(device, callback); + } + + void disconnect() { + _currentRepository.disconnect(); + } + + Future>> startDataStream() { + return _currentRepository.startDataStream(); + } + + void stopDataStream() { + _currentRepository.stopDataStream(); + } + + Stream> watch() { + return _acquisitionDeviceController.stream; + } +} diff --git a/mobile/lib/src/infrastructure/bluetooth_repository.dart b/mobile/lib/src/infrastructure/connection_repositories/bluetooth_repository.dart similarity index 92% rename from mobile/lib/src/infrastructure/bluetooth_repository.dart rename to mobile/lib/src/infrastructure/connection_repositories/bluetooth_repository.dart index 57e3f812..b9a2500e 100644 --- a/mobile/lib/src/infrastructure/bluetooth_repository.dart +++ b/mobile/lib/src/infrastructure/connection_repositories/bluetooth_repository.dart @@ -3,14 +3,13 @@ import 'dart:async'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:polydodo/src/domain/acquisition_device/acquisition_device.dart'; import 'package:polydodo/src/domain/acquisition_device/i_acquisition_device_repository.dart'; +import 'package:polydodo/src/infrastructure/constants.dart'; import 'package:polydodo/src/domain/unique_id.dart'; class BluetoothRepository implements IAcquisitionDeviceRepository { static const String BLE_SERVICE = "0000fe84-0000-1000-8000-00805f9b34fb"; static const String BLE_RECEIVE = "2d30c082-f39f-4ce6-923f-3484ea480596"; static const String BLE_SEND = "2d30c083-f39f-4ce6-923f-3484ea480596"; - static const startStreamChar = 'b'; - static const stopStreamChar = 's'; AcquisitionDevice _selectedDevice; QualifiedCharacteristic _sendCharacteristic; @@ -22,8 +21,6 @@ class BluetoothRepository implements IAcquisitionDeviceRepository { List _acquisitionDevicePersistency = []; final streamController = StreamController>(); - BluetoothRepository(); - void initializeRepository() { if (_bluetoothScanSubscription == null) { flutterReactiveBle = FlutterReactiveBle(); @@ -32,6 +29,7 @@ class BluetoothRepository implements IAcquisitionDeviceRepository { withServices: []).listen((device) => addDevice(device)); } else { _bluetoothScanSubscription.resume(); + _acquisitionDevicePersistency.clear(); } } @@ -72,10 +70,8 @@ class BluetoothRepository implements IAcquisitionDeviceRepository { } void disconnect() async { - if (_selectedDevice != null) { - _selectedDevice = null; - _connectedDeviceStream.cancel(); - } + _selectedDevice = null; + _connectedDeviceStream?.cancel(); } void setupCharacteristics() async { @@ -96,7 +92,7 @@ class BluetoothRepository implements IAcquisitionDeviceRepository { priority: ConnectionPriority.highPerformance); flutterReactiveBle.writeCharacteristicWithoutResponse(_sendCharacteristic, - value: startStreamChar.codeUnits); + value: START_STREAM_CHAR.codeUnits); return flutterReactiveBle.subscribeToCharacteristic(_receiveCharacteristic); } @@ -106,7 +102,7 @@ class BluetoothRepository implements IAcquisitionDeviceRepository { deviceId: _selectedDevice.id.toString(), priority: ConnectionPriority.balanced); flutterReactiveBle.writeCharacteristicWithoutResponse(_sendCharacteristic, - value: stopStreamChar.codeUnits); + value: STOP_STREAM_CHAR.codeUnits); } @override diff --git a/mobile/lib/src/infrastructure/connection_repositories/eeg_data_repository.dart b/mobile/lib/src/infrastructure/connection_repositories/eeg_data_repository.dart new file mode 100644 index 00000000..784d5f46 --- /dev/null +++ b/mobile/lib/src/infrastructure/connection_repositories/eeg_data_repository.dart @@ -0,0 +1,72 @@ +import 'dart:async'; +import 'dart:io'; +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/unique_id.dart'; +import 'package:polydodo/src/infrastructure/eeg_data_transformers/baseOpenBCITransformer.dart'; +import 'package:polydodo/src/infrastructure/eeg_data_transformers/cytonTransformer.dart'; +import 'package:polydodo/src/infrastructure/constants.dart'; +import 'package:polydodo/src/infrastructure/eeg_data_transformers/ganglionTransformer.dart'; +import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; + +class EEGDataRepository implements IEEGDataRepository { + EEGData _recordingData; + BaseOpenBCITransformer, List> currentStreamTransformer; + + final GanglionTransformer, List> _ganglionTransformer = + new GanglionTransformer, List>.broadcast(); + + final CytonTransformer, List> _cytonTransformer = + new CytonTransformer.broadcast(); + + BaseOpenBCITransformer, List> _currentTransformer; + StreamSubscription _currentTransformerStream; + + StreamingSharedPreferences _preferences; + + void initialize() async { + if (_preferences == null) { + _preferences = await StreamingSharedPreferences.instance; + } + + _currentTransformer = + _preferences.getBool('using_bluetooth', defaultValue: false).getValue() + ? _ganglionTransformer + : _cytonTransformer; + } + + void createRecordingFromStream(Stream> stream) { + _recordingData = + EEGData(UniqueId.from(DateTime.now().toString()), List()); + + _currentTransformer.reset(); + _currentTransformerStream = stream + .asBroadcastStream() + .transform(_currentTransformer) + .listen((data) => _recordingData.values.add(data)); + } + + Future stopRecordingFromStream() async { + // todo: move save future to another file + _currentTransformerStream.cancel(); + + final directory = await getExternalStorageDirectory(); + final pathOfTheFileToWrite = + directory.path + '/' + _recordingData.fileName + ".txt"; + File file = File(pathOfTheFileToWrite); + List fileContent = []; + //todo: dynamically change header when we change transformer + fileContent.addAll(OPEN_BCI_CYTON_HEADER); + fileContent.addAll(_recordingData.values); + String csv = const ListToCsvConverter().convert(fileContent); + await file.writeAsString(csv); + } + + // todo: implement export and import + void importData() {} + void exportData() {} +} diff --git a/mobile/lib/src/infrastructure/connection_repositories/serial_repository.dart b/mobile/lib/src/infrastructure/connection_repositories/serial_repository.dart new file mode 100644 index 00000000..8715e355 --- /dev/null +++ b/mobile/lib/src/infrastructure/connection_repositories/serial_repository.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:polydodo/src/domain/unique_id.dart'; +import 'package:polydodo/src/domain/acquisition_device/acquisition_device.dart'; +import 'package:polydodo/src/domain/acquisition_device/i_acquisition_device_repository.dart'; +import 'package:polydodo/src/infrastructure/constants.dart'; +import 'package:usb_serial/usb_serial.dart'; + +class SerialRepository implements IAcquisitionDeviceRepository { + UsbDevice _selectedDevice; + UsbPort _serialPort; + List _acquisitionDevicePersistency = []; + List _serialDevices = []; + final streamController = StreamController>(); + + void initializeRepository() { + _acquisitionDevicePersistency.clear(); + _serialDevices.clear(); + UsbSerial.listDevices().then((devices) => addDevices(devices)); + } + + void addDevices(List serialDevices) { + for (UsbDevice serialDevice in serialDevices) { + AcquisitionDevice device = AcquisitionDevice( + UniqueId.from(serialDevice.deviceId.toString()), + serialDevice.productName); + + _acquisitionDevicePersistency.add(device); + _serialDevices.add(serialDevice); + } + streamController.add(_acquisitionDevicePersistency); + } + + Future connect( + AcquisitionDevice device, Function(bool, Exception) callback) async { + _selectedDevice = + _serialDevices[_acquisitionDevicePersistency.indexOf(device)]; + _serialPort = await _selectedDevice.create(); + bool openSuccessful = await _serialPort.open(); + + if (!openSuccessful) { + callback(false, Exception("Could not open port")); + } + + _serialPort.setPortParameters( + 115200, UsbPort.DATABITS_8, UsbPort.STOPBITS_1, UsbPort.PARITY_NONE); + + callback(true, null); + } + + Future disconnect() async { + await _serialPort?.close(); + _selectedDevice = null; + _serialPort = null; + } + + Future>> startDataStream() async { + await _serialPort.write(Uint8List.fromList(START_STREAM_CHAR.codeUnits)); + + return _serialPort.inputStream; + } + + Future stopDataStream() async { + await _serialPort.write(Uint8List.fromList(STOP_STREAM_CHAR.codeUnits)); + } + + Stream> watch() => streamController.stream; +} diff --git a/mobile/lib/src/infrastructure/constants.dart b/mobile/lib/src/infrastructure/constants.dart index cc7f7932..dd30eddf 100644 --- a/mobile/lib/src/infrastructure/constants.dart +++ b/mobile/lib/src/infrastructure/constants.dart @@ -1,4 +1,11 @@ -const OPEN_BCI_HEADER = [ +const START_STREAM_CHAR = 'b'; +const STOP_STREAM_CHAR = 's'; + +const GANGLION_NUMBER_CHANNELS = 4; +const GANGLION_PACKET_SIZE = 20; +const GANGLION_NUMBER_COLUMNS = 15; +const GANGLION_EXTRA_COLUMNS = 10; +const OPEN_BCI_GANGLION_HEADER = [ ["%OpenBCI Raw EEG Data"], ["%Number of channels = 4"], ["%Sample Rate = 200 Hz"], @@ -21,3 +28,42 @@ const OPEN_BCI_HEADER = [ " Timestamp (Formatted)" ] ]; + +const CYTON_NUMBER_CHANNELS = 8; +const CYTON_PACKET_SIZE = 33; +const CYTON_NUMBER_COLUMNS = 24; +const CYTON_EXTRA_COLUMNS = 15; +const CYTON_HEADER = 160; +const CYTON_FOOTER_MINIMUM = 192; +const OPEN_BCI_CYTON_HEADER = [ + ["%OpenBCI Raw EEG Data"], + ["%Number of channels = 8"], + ["%Sample Rate = 250 Hz"], + ["%Board = OpenBCI_GUI\$BoardCytonSerial"], + [ + "Sample Index", + " EXG Channel 0", + " EXG Channel 1", + " EXG Channel 2", + " EXG Channel 3", + " EXG Channel 4", + " EXG Channel 5", + " EXG Channel 6", + " EXG Channel 7", + " Accel Channel 0", + " Accel Channel 1", + " Accel Channel 2", + " Other", + " Other", + " Other", + " Other", + " Other", + " Other", + " Other", + " Analog Channel 0", + " Analog Channel 1", + " Analog Channel 2", + " Timestamp", + " Timestamp (Formatted)" + ] +]; diff --git a/mobile/lib/src/infrastructure/eeg_data_repository.dart b/mobile/lib/src/infrastructure/eeg_data_repository.dart deleted file mode 100644 index c24e2a26..00000000 --- a/mobile/lib/src/infrastructure/eeg_data_repository.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:io'; - -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/unique_id.dart'; -import 'constants.dart'; - -class EEGDataRepository implements IEEGDataRepository { - bool _streamInitialized = false; - EEGData _recordingData; - List _lastSampleData = [0, 0, 0, 0, 0]; - int _sampleCounter; - - void createRecordingFromStream(Stream> stream) { - _recordingData = - EEGData(UniqueId.from(DateTime.now().toString()), List()); - _sampleCounter = 0; - if (!_streamInitialized) { - _streamInitialized = true; - stream.listen((value) { - addData(value); - }); - } - } - - Future stopRecordingFromStream() async { - // todo: move save future to another file - - final directory = await getExternalStorageDirectory(); - final pathOfTheFileToWrite = - directory.path + '/' + _recordingData.fileName + ".txt"; - File file = File(pathOfTheFileToWrite); - var fileContent = []; - fileContent.addAll(OPEN_BCI_HEADER); - fileContent.addAll(_recordingData.values); - String csv = const ListToCsvConverter().convert(fileContent); - await file.writeAsString(csv); - } - - // todo: implement export and import - void importData() {} - void exportData() {} - - Future addData(List event) async { - if (event.length != 20) { - print("Invalid Event"); - return; - } - int packetID = event[0]; - - // todo: handle packet id 0 (raw data) and possibly impedence for signal validation - if (packetID == 0) { - List data = parseRaw(event); - data = convertToMicrovolts(data, false); - - _recordingData.values.add(data.sublist(0, 15)); - } else if (packetID >= 101 && packetID <= 200) { - List data = parse19Bit(event); - data = convertToMicrovolts(data, true); - - _recordingData.values.add(data.sublist(0, 15)); - _recordingData.values.add(data.sublist(15, 30)); - } - } - - List parseRaw(event) { - List data = getListForCSV(); - - data[0] = _sampleCounter++; - data[1] = (event[1] << 16) | (event[2] << 8) | event[3]; - data[2] = (event[4] << 16) | (event[5] << 8) | event[6]; - data[3] = (event[7] << 16) | (event[8] << 8) | event[9]; - data[4] = (event[10] << 16) | (event[11] << 8) | event[12]; - - return data; - } - - List parse19Bit(event) { - // Test event, comment scale factor - // event = [ 101, 0, 0, 0, 0, 8, 0, 5, 0, 0, 72, 0, 9, 240, 1, 176, 0, 48, 0, 8]; // Positive Test - // Expected [[0, 2, 10, 4], [262148, 507910, 393222, 8]] - // event = [ 101, 255, 255, 191, 255, 239, 255, 252, 255, 255, 88, 0, 11, 62, 56, 224, 0, 63, 240, 1 ]; // Negative Test - // Expected [[-3, -5, -7, -11], [-262139, -198429, -262137, -4095]] - - List data = getListForCSV(); - - data[0] = _sampleCounter; - data[1] = (event[1] << 11) | (event[2] << 3) | (event[3] >> 5); - data[2] = ((event[3] & 31) << 14) | (event[4] << 6) | (event[5] >> 2); - data[3] = ((event[5] & 3) << 17) | - (event[6] << 9) | - (event[7] << 1) | - (event[8] >> 7); - data[4] = ((event[8] & 127) << 12) | (event[9] << 4) | (event[10] >> 4); - data[15] = _sampleCounter++; - data[16] = ((event[10] & 15) << 15) | (event[11] << 7) | (event[12] >> 1); - data[17] = ((event[12] & 1) << 18) | - (event[13] << 10) | - (event[14] << 2) | - (event[15] >> 6); - data[18] = ((event[15] & 63) << 13) | (event[16] << 5) | (event[17] >> 3); - data[19] = ((event[17] & 7) << 16) | (event[18] << 8) | (event[19]); - - return data; - } - - List getListForCSV() { - List data = List(30); - - for (int i = 5; i < 15; ++i) { - data[i] = 0; - data[i + 15] = 0; - } - return data; - } - - List convertToMicrovolts(List data, bool isDelta) { - for (int i = 1; i < 5; ++i) { - for (int j = 0; j < 2; ++j) { - if (j == 1 && !isDelta) break; - - int offset = 15 * j; - String binary = data[i + offset].toRadixString(2); - - // Handle negatives - if (isDelta && binary[binary.length - 1] == '1') { - data[i + offset] = (~data[i + offset] & 524287 | 1) * -1; - } - - // Convert to microvolts using the scale factor - data[i + offset] = - data[i + offset].toDouble() * (1200000 / (8388607.0 * 1.5 * 51.0)); - - // Convert delta - if (isDelta) data[i + offset] = _lastSampleData[i] - data[i + offset]; - - _lastSampleData[i] = data[i + offset]; - } - } - - return data; - } -} diff --git a/mobile/lib/src/infrastructure/eeg_data_transformers/baseOpenBCITransformer.dart b/mobile/lib/src/infrastructure/eeg_data_transformers/baseOpenBCITransformer.dart new file mode 100644 index 00000000..3e3bd609 --- /dev/null +++ b/mobile/lib/src/infrastructure/eeg_data_transformers/baseOpenBCITransformer.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +abstract class BaseOpenBCITransformer implements StreamTransformer { + StreamController controller; + StreamSubscription _subscription; + bool cancelOnError; + Stream _stream; + + BaseOpenBCITransformer({bool synchronous: false, this.cancelOnError}) { + controller = new StreamController( + onListen: _onListen, + onCancel: _onCancel, + onPause: () { + _subscription.pause(); + }, + onResume: () { + _subscription.resume(); + }, + sync: synchronous); + } + + BaseOpenBCITransformer.broadcast( + {bool synchronous: false, this.cancelOnError}) { + controller = new StreamController.broadcast( + onListen: _onListen, onCancel: _onCancel, sync: synchronous); + } + + void reset(); + + void onData(S data); + + void _onListen() { + reset(); + + _subscription = _stream.listen(onData, + onError: controller.addError, + onDone: controller.close, + cancelOnError: cancelOnError); + } + + void _onCancel() { + _subscription.cancel(); + _subscription = null; + } + + @override + Stream bind(Stream stream) { + this._stream = stream; + return controller.stream; + } + + @override + StreamTransformer cast() { + return StreamTransformer.castFrom(this); + } +} diff --git a/mobile/lib/src/infrastructure/eeg_data_transformers/cytonTransformer.dart b/mobile/lib/src/infrastructure/eeg_data_transformers/cytonTransformer.dart new file mode 100644 index 00000000..e42fc0fb --- /dev/null +++ b/mobile/lib/src/infrastructure/eeg_data_transformers/cytonTransformer.dart @@ -0,0 +1,85 @@ +import 'dart:math'; + +import 'package:polydodo/src/infrastructure/constants.dart'; +import 'baseOpenBCITransformer.dart'; + +class CytonTransformer extends BaseOpenBCITransformer { + List packet = List(); + + CytonTransformer.broadcast({bool synchronous: false, cancelOnError}) + : super.broadcast(synchronous: synchronous, cancelOnError: cancelOnError); + + void reset() { + packet.clear(); + } + + void onData(S data) { + List event = data as List; + + for (var i in event) { + if (packet.length == 0) { + if (i != CYTON_HEADER) { + print("Missing header byte"); + continue; + } + } + packet.add(i); + + if (packet.length == CYTON_PACKET_SIZE) { + if (packet[CYTON_PACKET_SIZE - 1] < CYTON_FOOTER_MINIMUM) { + print("Invalid packet"); + packet.clear(); + continue; + } + + List data = parsePacket(packet); + + packet.clear(); + + data = processData(data, true); + + controller.add(data); + } + } + } + + List parsePacket(List fullPacket) { + List data = getListForCSV(); + + data[0] = fullPacket[1]; + data[1] = (fullPacket[2] << 16) | (fullPacket[3] << 8) | fullPacket[4]; + data[2] = (fullPacket[5] << 16) | (fullPacket[6] << 8) | fullPacket[7]; + data[3] = (fullPacket[8] << 16) | (fullPacket[9] << 8) | fullPacket[10]; + data[4] = (fullPacket[11] << 16) | (fullPacket[12] << 8) | fullPacket[13]; + data[5] = (fullPacket[14] << 16) | (fullPacket[15] << 8) | fullPacket[16]; + data[6] = (fullPacket[17] << 16) | (fullPacket[18] << 8) | fullPacket[19]; + data[7] = (fullPacket[20] << 16) | (fullPacket[21] << 8) | fullPacket[22]; + data[8] = (fullPacket[23] << 16) | (fullPacket[24] << 8) | fullPacket[25]; + + return data; + } + + List getListForCSV() => List.generate(CYTON_NUMBER_COLUMNS, (index) => 0); + + List processData(List data, bool hasNegativeCompression) { + List result = List.from(data); + + for (int i = 1; i < CYTON_NUMBER_CHANNELS + 1; ++i) { + if (hasNegativeCompression) result[i] = handleNegative(result[i]); + + result[i] = convertToMicrovolts(result[i]); + } + + return result; + } + + int handleNegative(int i) { + return ((i & 0x00800000) > 0) + ? (i | 0xFFFFFFFFFF000000) + : (i & 0x0000000000FFFFFF); + } + + double convertToMicrovolts(int i) { + return i.toDouble() * (4500000 / (24 * (pow(2, 23) - 1))); + } +} diff --git a/mobile/lib/src/infrastructure/eeg_data_transformers/ganglionTransformer.dart b/mobile/lib/src/infrastructure/eeg_data_transformers/ganglionTransformer.dart new file mode 100644 index 00000000..8785956d --- /dev/null +++ b/mobile/lib/src/infrastructure/eeg_data_transformers/ganglionTransformer.dart @@ -0,0 +1,113 @@ +import 'package:polydodo/src/infrastructure/constants.dart'; +import 'baseOpenBCITransformer.dart'; + +class GanglionTransformer extends BaseOpenBCITransformer { + List _lastSampleData = [0, 0, 0, 0, 0]; + int _sampleCounter = 0; + + GanglionTransformer.broadcast({bool synchronous: false, cancelOnError}) + : super.broadcast(synchronous: synchronous, cancelOnError: cancelOnError); + + void reset() { + _lastSampleData = [0, 0, 0, 0, 0]; + _sampleCounter = 0; + } + + void onData(S data) { + List event = data as List; + + if (event.length != GANGLION_PACKET_SIZE) return; + + int packetID = event[0]; + + if (packetID == 0) { + List data = parseRaw(event); + data = processData(data, + nbSamples: 1, hasNegativeCompression: false, isDelta: false); + + controller.add(data.sublist(0, 15)); + } else if (packetID >= 101 && packetID <= 200) { + List data = parse19Bit(event); + data = processData(data, + nbSamples: 2, hasNegativeCompression: true, isDelta: true); + + controller.add(data.sublist(0, 15)); + controller.add(data.sublist(15, 30)); + } + } + + List parseRaw(event) { + List data = getListForCSV(); + + data[0] = _sampleCounter++; + data[1] = (event[1] << 16) | (event[2] << 8) | event[3]; + data[2] = (event[4] << 16) | (event[5] << 8) | event[6]; + data[3] = (event[7] << 16) | (event[8] << 8) | event[9]; + data[4] = (event[10] << 16) | (event[11] << 8) | event[12]; + + return data; + } + + List parse19Bit(event) { + List data = getListForCSV(); + + data[0] = _sampleCounter; + data[1] = (event[1] << 11) | (event[2] << 3) | (event[3] >> 5); + data[2] = ((event[3] & 31) << 14) | (event[4] << 6) | (event[5] >> 2); + data[3] = ((event[5] & 3) << 17) | + (event[6] << 9) | + (event[7] << 1) | + (event[8] >> 7); + data[4] = ((event[8] & 127) << 12) | (event[9] << 4) | (event[10] >> 4); + data[15] = _sampleCounter++; + data[16] = ((event[10] & 15) << 15) | (event[11] << 7) | (event[12] >> 1); + data[17] = ((event[12] & 1) << 18) | + (event[13] << 10) | + (event[14] << 2) | + (event[15] >> 6); + data[18] = ((event[15] & 63) << 13) | (event[16] << 5) | (event[17] >> 3); + data[19] = ((event[17] & 7) << 16) | (event[18] << 8) | (event[19]); + + return data; + } + + List getListForCSV() => + List.generate(GANGLION_NUMBER_COLUMNS * 2, (index) => 0); + + List processData(List data, + {int nbSamples, bool hasNegativeCompression, bool isDelta}) { + List result = List.from(data); + + for (int i = 1; i < GANGLION_NUMBER_CHANNELS + 1; ++i) { + for (int j = 0; j < nbSamples; ++j) { + int offset = 15 * j; + + if (hasNegativeCompression) + result[i + offset] = handleNegative(result[i + offset]); + + result[i + offset] = convertToMicrovolts(result[i + offset]); + + if (isDelta) + result[i + offset] = convertDeltaToData(i, result[i + offset]); + + _lastSampleData[i] = result[i + offset]; + } + } + + return result; + } + + int handleNegative(int i) { + String binary = i.toRadixString(2); + + return binary[binary.length - 1] == '1' ? (~i & 524287 | 1) * -1 : i; + } + + double convertToMicrovolts(int i) { + return i.toDouble() * (1200000 / (8388607.0 * 1.5 * 51.0)); + } + + double convertDeltaToData(int lastSampleIndex, double i) { + return _lastSampleData[lastSampleIndex] - i; + } +} diff --git a/mobile/lib/src/infrastructure/serial_repository.dart b/mobile/lib/src/infrastructure/serial_repository.dart deleted file mode 100644 index 8b137891..00000000 --- a/mobile/lib/src/infrastructure/serial_repository.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/mobile/lib/src/locator.dart b/mobile/lib/src/locator.dart index 8d521dae..9b347f04 100644 --- a/mobile/lib/src/locator.dart +++ b/mobile/lib/src/locator.dart @@ -3,17 +3,17 @@ import 'package:get_it/get_it.dart'; import 'package:polydodo/src/application/device/device_selector_cubit.dart'; import 'package:polydodo/src/application/eeg_data/data_cubit.dart'; import 'package:polydodo/src/domain/acquisition_device/i_acquisition_device_repository.dart'; - -import 'domain/eeg_data/i_eeg_data_repository.dart'; -import 'infrastructure/bluetooth_repository.dart'; -import 'infrastructure/eeg_data_repository.dart'; +import 'package:polydodo/src/domain/eeg_data/i_eeg_data_repository.dart'; +import 'package:polydodo/src/infrastructure/connection_repositories/acquisition_device_repository.dart'; +import 'package:polydodo/src/infrastructure/connection_repositories/eeg_data_repository.dart'; /// Private GetIt instance as we want all DI to be performed here in this file final _serviceLocator = GetIt.asNewInstance(); void registerServices() { - _serviceLocator - .registerSingleton(BluetoothRepository()); + // todo: dynamically change repository + _serviceLocator.registerSingleton( + AcquisitionDeviceRepository()); _serviceLocator.registerSingleton(EEGDataRepository()); } diff --git a/mobile/lib/src/presentation/bluetooth_route/bluetoothSelector_route.dart b/mobile/lib/src/presentation/bluetooth_route/bluetoothSelector_route.dart index eaef0c99..97b2eef3 100644 --- a/mobile/lib/src/presentation/bluetooth_route/bluetoothSelector_route.dart +++ b/mobile/lib/src/presentation/bluetooth_route/bluetoothSelector_route.dart @@ -52,6 +52,9 @@ class BluetoothSelectorRoute extends StatelessWidget { return Container(); }, ), + floatingActionButton: FloatingActionButton( + onPressed: BlocProvider.of(context).swapBluetooth, + ), ); } } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5c26b06c..98a50879 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -28,7 +28,7 @@ packages: name: battery url: "https://pub.dartlang.org" source: hosted - version: "1.0.6" + version: "1.0.7" battery_platform_interface: dependency: transitive description: @@ -49,7 +49,7 @@ packages: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "6.0.3" + version: "6.1.0" boolean_selector: dependency: transitive description: @@ -166,12 +166,17 @@ packages: name: flutter_reactive_ble url: "https://pub.dartlang.org" source: hosted - version: "2.5.2" + version: "2.5.3" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" functional_data: dependency: transitive description: @@ -241,7 +246,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.18" + version: "1.6.21" path_provider_linux: dependency: transitive description: @@ -325,7 +330,49 @@ packages: name: share url: "https://pub.dartlang.org" source: hosted - version: "0.6.5+2" + version: "0.6.5+3" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.12+2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+2" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+10" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2+7" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+1" sky_engine: dependency: transitive description: flutter @@ -352,6 +399,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + streaming_shared_preferences: + dependency: "direct main" + description: + name: streaming_shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" string_scanner: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 10bc9657..b76a979a 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: meta: ^1.1.8 path_provider: ^1.6.16 share: ^0.6.5 + streaming_shared_preferences: ^1.0.1 uuid: ^2.2.2 usb_serial: ^0.2.4 diff --git a/mobile/test/constants.dart b/mobile/test/constants.dart new file mode 100644 index 00000000..85e57ec8 --- /dev/null +++ b/mobile/test/constants.dart @@ -0,0 +1,158 @@ +// Ganglion + +const ganglion_compressed_packet = [ + 101, + 0, + 0, + 0, + 0, + 8, + 0, + 5, + 0, + 0, + 72, + 0, + 9, + 240, + 1, + 176, + 0, + 48, + 0, + 8 +]; + +const ganglion_expected_compressed_1 = [0, 2, 10, 4]; +const ganglion_expected_compressed_2 = [262148, 507910, 393222, 8]; + +const ganglion_negative_packet = [ + 101, + 255, + 255, + 191, + 255, + 239, + 255, + 252, + 255, + 255, + 88, + 0, + 11, + 62, + 56, + 224, + 0, + 63, + 240, + 1 +]; + +const ganglion_expected_negative_1 = [-3, -5, -7, -11]; +const ganglion_expected_negative_2 = [-262139, -198429, -262137, -4095]; + +const raw_packet = [ + 0, + 4, + 0, + 4, + 7, + 192, + 6, + 6, + 0, + 6, + 0, + 0, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 +]; + +const expected_raw = [262148, 507910, 393222, 8]; + +// Cyton + +const cyton_packet = [ + 160, + 243, + 250, + 250, + 2, + 250, + 248, + 164, + 251, + 6, + 30, + 250, + 211, + 205, + 156, + 60, + 249, + 156, + 190, + 128, + 154, + 202, + 100, + 176, + 224, + 132, + 0, + 0, + 0, + 0, + 0, + 0, + 192 +]; + +const cyton_expected = [16448002, 16447652, 16451102, 16438221]; + +const cyton_negative_packet = [ + 160, + 243, + 0x7F, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x80, + 0x00, + 0x01, + 0x5D, + 0xCB, + 0xED, + 0xA2, + 0x34, + 0x13, + 156, + 190, + 128, + 154, + 202, + 100, + 176, + 0, + 0, + 0, + 0, + 0, + 0, + 192 +]; + +const cyton_expected_negative = [8388607, -1, -8388607, 6147053]; diff --git a/mobile/test/cytonTransformer_test.dart b/mobile/test/cytonTransformer_test.dart new file mode 100644 index 00000000..9f76cf58 --- /dev/null +++ b/mobile/test/cytonTransformer_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:polydodo/src/infrastructure/eeg_data_transformers/cytonTransformer.dart'; + +import 'constants.dart'; + +void main() { + test('Parse Packet', () { + final transformer = CytonTransformer.broadcast(); + + var result = transformer.parsePacket(cyton_packet); + + expect(result.sublist(1, 5), cyton_expected); + }); + + test('Process Data', () { + final transformer = CytonTransformer.broadcast(); + + var result = transformer.parsePacket(cyton_negative_packet); + result = transformer.processData(result, true); + + var expected = + cyton_expected_negative.map((e) => transformer.convertToMicrovolts(e)); + + expect(result.sublist(1, 5), expected); + }); +} diff --git a/mobile/test/ganglionTransformer_test.dart b/mobile/test/ganglionTransformer_test.dart new file mode 100644 index 00000000..fc26740e --- /dev/null +++ b/mobile/test/ganglionTransformer_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:polydodo/src/infrastructure/eeg_data_transformers/ganglionTransformer.dart'; +import 'constants.dart'; + +void main() { + final transformer = GanglionTransformer.broadcast(); + test('Parse 19 bit packet', () { + var result = transformer.parse19Bit(ganglion_compressed_packet); + + expect(result.sublist(1, 5), ganglion_expected_compressed_1); + expect(result.sublist(16, 20), ganglion_expected_compressed_2); + }); + + test('Parse 19 bit packet - Negative', () { + var expected_1 = ganglion_expected_negative_1 + .map((e) => transformer.convertToMicrovolts(e)); + + var expected_2 = ganglion_expected_negative_2 + .map((e) => transformer.convertToMicrovolts(e)); + + var result = transformer.parse19Bit(ganglion_negative_packet); + + result = transformer.processData(result, + nbSamples: 2, hasNegativeCompression: true, isDelta: false); + + expect(result.sublist(1, 5), expected_1); + expect(result.sublist(16, 20), expected_2); + }); + + test('Parse raw', () { + var result = transformer.parseRaw(raw_packet); + expect(result.sublist(1, 5), expected_raw); + }); +}