diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 4e62f489..6795bb69 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -38,7 +38,7 @@ android { defaultConfig { applicationId "com.PolyCortex.Polydodo" - minSdkVersion 19 + minSdkVersion 24 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 98c8e964..22862eb9 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -44,8 +44,5 @@ android:name="flutterEmbedding" android:value="2" /> - - - diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist index f2872cf4..beffb28c 100644 --- a/mobile/ios/Flutter/AppFrameworkInfo.plist +++ b/mobile/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 10.0 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index bbd0e334..a81b9d8b 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -272,7 +272,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -354,7 +354,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -403,7 +403,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/mobile/lib/src/application/device/device_selector_cubit.dart b/mobile/lib/src/application/device/device_selector_cubit.dart index 02a657c2..90412b90 100644 --- a/mobile/lib/src/application/device/device_selector_cubit.dart +++ b/mobile/lib/src/application/device/device_selector_cubit.dart @@ -30,13 +30,16 @@ class DeviceSelectorCubit extends Cubit { Future connect(AcquisitionDevice device) async { emit(DeviceConnectionInProgress()); - try { - await _deviceRepository.connect(device); - _acquisitionDeviceStream.cancel(); - emit(DeviceConnectionSuccess()); - } catch (e) { + _deviceRepository.connect(device, connectionCallback); + } + + void connectionCallback(bool connected, Exception e) { + if (e != null) { emit(DeviceConnectionFailure(e)); resetSearch(); + } else if (connected) { + _acquisitionDeviceStream.cancel(); + emit(DeviceConnectionSuccess()); } } diff --git a/mobile/lib/src/domain/acquisition_device/i_acquisition_device_repository.dart b/mobile/lib/src/domain/acquisition_device/i_acquisition_device_repository.dart index 04067b8e..4c6148a7 100644 --- a/mobile/lib/src/domain/acquisition_device/i_acquisition_device_repository.dart +++ b/mobile/lib/src/domain/acquisition_device/i_acquisition_device_repository.dart @@ -3,7 +3,7 @@ import 'acquisition_device.dart'; abstract class IAcquisitionDeviceRepository { void initializeRepository(); - Future connect(AcquisitionDevice device); + void connect(AcquisitionDevice device, Function(bool, Exception) callback); void disconnect(); Future>> startDataStream(); diff --git a/mobile/lib/src/domain/eeg_data/eeg_data.dart b/mobile/lib/src/domain/eeg_data/eeg_data.dart index 6ba58bbf..b90928c7 100644 --- a/mobile/lib/src/domain/eeg_data/eeg_data.dart +++ b/mobile/lib/src/domain/eeg_data/eeg_data.dart @@ -2,8 +2,7 @@ import 'package:polydodo/src/domain/entity.dart'; class EEGData extends Entity { - List _values; - int sampleCounter = 0; + final List _values; EEGData(id, this._values) : assert(_values != null), diff --git a/mobile/lib/src/infrastructure/bluetooth_repository.dart b/mobile/lib/src/infrastructure/bluetooth_repository.dart index 371a2670..57e3f812 100644 --- a/mobile/lib/src/infrastructure/bluetooth_repository.dart +++ b/mobile/lib/src/infrastructure/bluetooth_repository.dart @@ -1,55 +1,41 @@ import 'dart:async'; -import 'package:flutter/services.dart'; -import 'package:flutter_blue/flutter_blue.dart'; +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/domain/unique_id.dart'; class BluetoothRepository implements IAcquisitionDeviceRepository { - static const String BLE_SERVICE = "fe84"; - static const String BLE_RECEIVE = "2d30c082"; - static const String BLE_SEND = "2d30c083"; + 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'; - BluetoothDevice _selectedDevice; - BluetoothCharacteristic _sendCharacteristic; - BluetoothCharacteristic _receiveCharacteristic; + AcquisitionDevice _selectedDevice; + QualifiedCharacteristic _sendCharacteristic; + QualifiedCharacteristic _receiveCharacteristic; - FlutterBlue flutterBlue; - StreamSubscription> _bluetoothScanSubscription; + FlutterReactiveBle flutterReactiveBle; + StreamSubscription _connectedDeviceStream; + StreamSubscription _bluetoothScanSubscription; List _acquisitionDevicePersistency = []; - List _bluetoothDevices = []; final streamController = StreamController>(); BluetoothRepository(); void initializeRepository() { if (_bluetoothScanSubscription == null) { - flutterBlue = FlutterBlue.instance; - - flutterBlue.connectedDevices - .asStream() - .asBroadcastStream() - .listen((List devices) { - for (BluetoothDevice device in devices) { - addDevice(device); - } - }); - _bluetoothScanSubscription = - flutterBlue.scanResults.listen((List results) { - for (ScanResult result in results) { - addDevice(result.device); - } - }); + flutterReactiveBle = FlutterReactiveBle(); + + _bluetoothScanSubscription = flutterReactiveBle.scanForDevices( + withServices: []).listen((device) => addDevice(device)); } else { _bluetoothScanSubscription.resume(); } - flutterBlue.startScan(); } - void addDevice(BluetoothDevice bluetoothDevice) { + void addDevice(DiscoveredDevice bluetoothDevice) { AcquisitionDevice device = AcquisitionDevice( UniqueId.from(bluetoothDevice.id.toString()), bluetoothDevice.name); @@ -57,73 +43,70 @@ class BluetoothRepository implements IAcquisitionDeviceRepository { if (idx == -1) { _acquisitionDevicePersistency.add(device); - _bluetoothDevices.add(bluetoothDevice); } else { _acquisitionDevicePersistency[idx] = device; - _bluetoothDevices[idx] = bluetoothDevice; } streamController.add(_acquisitionDevicePersistency); } - Future connect(AcquisitionDevice device) async { - _selectedDevice = - _bluetoothDevices[_acquisitionDevicePersistency.indexOf(device)]; - + void connect( + AcquisitionDevice device, Function(bool, Exception) callback) async { + _selectedDevice = device; _acquisitionDevicePersistency.clear(); - _bluetoothDevices.clear(); _bluetoothScanSubscription.pause(); - flutterBlue.stopScan(); - - try { - await _selectedDevice.connect().timeout(Duration(seconds: 6), - onTimeout: () => - {disconnect(), throw Exception("Connection Timed out")}); - - await findRelevantCharacteristics(); - } catch (e) { - if (e is PlatformException) { - if (e.code != "already_connected") throw Exception(e.details); - } else - throw e; - } + + _connectedDeviceStream = flutterReactiveBle + .connectToDevice( + id: _selectedDevice.id.toString(), + connectionTimeout: Duration(seconds: 10)) + .listen((event) { + if (event.connectionState == DeviceConnectionState.connected) { + setupCharacteristics(); + callback(true, null); + } else if (event.connectionState == DeviceConnectionState.disconnected) { + disconnect(); + callback(false, Exception("Failed to connect to device")); + } + }); } - Future disconnect() async { + void disconnect() async { if (_selectedDevice != null) { - await _selectedDevice.disconnect(); _selectedDevice = null; + _connectedDeviceStream.cancel(); } } - Future findRelevantCharacteristics() async { - var characteristics = (await _selectedDevice.discoverServices()) - .firstWhere((service) => service.uuid.toString().contains(BLE_SERVICE)) - .characteristics; - for (BluetoothCharacteristic characteristic in characteristics) { - if (characteristic.uuid.toString().contains(BLE_RECEIVE)) { - _receiveCharacteristic = characteristic; - } else if (characteristic.uuid.toString().contains(BLE_SEND)) { - _sendCharacteristic = characteristic; - } - } + void setupCharacteristics() async { + _sendCharacteristic = QualifiedCharacteristic( + characteristicId: Uuid.parse(BLE_SEND), + serviceId: Uuid.parse(BLE_SERVICE), + deviceId: _selectedDevice.id.toString()); - if (_receiveCharacteristic == null) - throw Exception('Device is missing receive Characteristic'); - if (_sendCharacteristic == null) - throw Exception('Device is missing send Characteristic'); + _receiveCharacteristic = QualifiedCharacteristic( + characteristicId: Uuid.parse(BLE_RECEIVE), + serviceId: Uuid.parse(BLE_SERVICE), + deviceId: _selectedDevice.id.toString()); } Future>> startDataStream() async { - await _receiveCharacteristic.setNotifyValue(true); + await flutterReactiveBle.requestConnectionPriority( + deviceId: _selectedDevice.id.toString(), + priority: ConnectionPriority.highPerformance); + + flutterReactiveBle.writeCharacteristicWithoutResponse(_sendCharacteristic, + value: startStreamChar.codeUnits); - await _sendCharacteristic.write(startStreamChar.codeUnits); - return _receiveCharacteristic.value; + return flutterReactiveBle.subscribeToCharacteristic(_receiveCharacteristic); } Future stopDataStream() async { - await _receiveCharacteristic.setNotifyValue(false); - await _sendCharacteristic.write(stopStreamChar.codeUnits); + await flutterReactiveBle.requestConnectionPriority( + deviceId: _selectedDevice.id.toString(), + priority: ConnectionPriority.balanced); + flutterReactiveBle.writeCharacteristicWithoutResponse(_sendCharacteristic, + value: stopStreamChar.codeUnits); } @override diff --git a/mobile/lib/src/infrastructure/eeg_data_repository.dart b/mobile/lib/src/infrastructure/eeg_data_repository.dart index 7e29ca01..c24e2a26 100644 --- a/mobile/lib/src/infrastructure/eeg_data_repository.dart +++ b/mobile/lib/src/infrastructure/eeg_data_repository.dart @@ -8,20 +8,17 @@ import 'package:polydodo/src/domain/unique_id.dart'; import 'constants.dart'; class EEGDataRepository implements IEEGDataRepository { - bool initializeStream = false; - EEGData recordingData; - List lastSampleData = [0, 0, 0, 0, 0]; - int packetLoss; - int totalPackets; - int nextPacketId; + bool _streamInitialized = false; + EEGData _recordingData; + List _lastSampleData = [0, 0, 0, 0, 0]; + int _sampleCounter; void createRecordingFromStream(Stream> stream) { - recordingData = - new EEGData(UniqueId.from(DateTime.now().toString()), new List()); - packetLoss = 0; - totalPackets = 0; - if (!initializeStream) { - initializeStream = true; + _recordingData = + EEGData(UniqueId.from(DateTime.now().toString()), List()); + _sampleCounter = 0; + if (!_streamInitialized) { + _streamInitialized = true; stream.listen((value) { addData(value); }); @@ -30,13 +27,14 @@ class EEGDataRepository implements IEEGDataRepository { Future stopRecordingFromStream() async { // todo: move save future to another file + final directory = await getExternalStorageDirectory(); final pathOfTheFileToWrite = - directory.path + '/' + recordingData.fileName + ".txt"; + directory.path + '/' + _recordingData.fileName + ".txt"; File file = File(pathOfTheFileToWrite); - List fileContent = new List(); + var fileContent = []; fileContent.addAll(OPEN_BCI_HEADER); - fileContent.addAll(recordingData.values); + fileContent.addAll(_recordingData.values); String csv = const ListToCsvConverter().convert(fileContent); await file.writeAsString(csv); } @@ -46,42 +44,31 @@ class EEGDataRepository implements IEEGDataRepository { void exportData() {} Future addData(List event) async { - //print("Lost packets: " + packetLoss.toString()); - //print("Total packets: " + totalPackets.toString()); - //print("Lost percentage: " + - // (packetLoss.toDouble() / totalPackets.toDouble()).toString()); if (event.length != 20) { print("Invalid Event"); return; } - totalPackets++; int packetID = event[0]; // todo: handle packet id 0 (raw data) and possibly impedence for signal validation if (packetID == 0) { - nextPacketId = 101; - List data = parseRaw(event); data = convertToMicrovolts(data, false); - recordingData.values.add(data.sublist(0, 15)); + _recordingData.values.add(data.sublist(0, 15)); } else if (packetID >= 101 && packetID <= 200) { - // print(packetID); - // print(nextPacketId); - packetLoss += packetID - nextPacketId; - nextPacketId = packetID + 1; List data = parse19Bit(event); data = convertToMicrovolts(data, true); - recordingData.values.add(data.sublist(0, 15)); - recordingData.values.add(data.sublist(15, 30)); + _recordingData.values.add(data.sublist(0, 15)); + _recordingData.values.add(data.sublist(15, 30)); } } List parseRaw(event) { List data = getListForCSV(); - data[0] = recordingData.sampleCounter++; + 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]; @@ -99,7 +86,7 @@ class EEGDataRepository implements IEEGDataRepository { List data = getListForCSV(); - data[0] = recordingData.sampleCounter; + 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) | @@ -107,7 +94,7 @@ class EEGDataRepository implements IEEGDataRepository { (event[7] << 1) | (event[8] >> 7); data[4] = ((event[8] & 127) << 12) | (event[9] << 4) | (event[10] >> 4); - data[15] = recordingData.sampleCounter++; + data[15] = _sampleCounter++; data[16] = ((event[10] & 15) << 15) | (event[11] << 7) | (event[12] >> 1); data[17] = ((event[12] & 1) << 18) | (event[13] << 10) | @@ -120,7 +107,7 @@ class EEGDataRepository implements IEEGDataRepository { } List getListForCSV() { - List data = new List(30); + List data = List(30); for (int i = 5; i < 15; ++i) { data[i] = 0; @@ -147,9 +134,9 @@ class EEGDataRepository implements IEEGDataRepository { data[i + offset].toDouble() * (1200000 / (8388607.0 * 1.5 * 51.0)); // Convert delta - if (isDelta) data[i + offset] = lastSampleData[i] - data[i + offset]; + if (isDelta) data[i + offset] = _lastSampleData[i] - data[i + offset]; - lastSampleData[i] = data[i + offset]; + _lastSampleData[i] = data[i + offset]; } } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 920435e6..5c26b06c 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -160,18 +160,25 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.6" - flutter_blue: + flutter_reactive_ble: dependency: "direct main" description: - name: flutter_blue + name: flutter_reactive_ble url: "https://pub.dartlang.org" source: hosted - version: "0.7.2" + version: "2.5.2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + functional_data: + dependency: transitive + description: + name: functional_data + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" get_it: dependency: "direct main" description: @@ -263,6 +270,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.4+1" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + plain_optional: + dependency: transitive + description: + name: plain_optional + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" platform: dependency: transitive description: @@ -298,13 +319,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.3.2+2" - rxdart: - dependency: transitive - description: - name: rxdart - url: "https://pub.dartlang.org" - source: hosted - version: "0.24.1" share: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index d082d4d9..10bc9657 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -28,20 +28,20 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. archive: ^2.0.13 battery: ^1.0.5 + binary: ^2.0.0 bloc: ^6.0.3 + csv: ^4.0.3 cupertino_icons: ^0.1.3 equatable: ^1.2.5 flutter_bloc: ^6.0.5 - flutter_blue: ^0.7.2 + flutter_reactive_ble : ^2.5.2 get_it: ^4.0.4 hive: ^1.4.4 meta: ^1.1.8 + path_provider: ^1.6.16 share: ^0.6.5 uuid: ^2.2.2 usb_serial: ^0.2.4 - binary: ^2.0.0 - csv: ^4.0.3 - path_provider: ^1.6.16 dev_dependencies: flutter_test: