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);
+ });
+}