diff --git a/mobile/lib/src/application/device/device_selector_cubit.dart b/mobile/lib/src/application/device/device_selector_cubit.dart index 6644c9eb..1a1b9870 100644 --- a/mobile/lib/src/application/device/device_selector_cubit.dart +++ b/mobile/lib/src/application/device/device_selector_cubit.dart @@ -3,32 +3,32 @@ import 'dart:async'; 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:polydodo/src/domain/acquisition_device/device_locator_service.dart'; import 'device_selector_state.dart'; class DeviceSelectorCubit extends Cubit { - final IAcquisitionDeviceRepository _deviceRepository; + final DeviceLocatorService _deviceLocatorService; + final List _scannedDevices = []; - StreamSubscription> _acquisitionDeviceStream; + Stream _deviceLocatorStream; + StreamSubscription _deviceLocatorStreamSubscription; - DeviceSelectorCubit(this._deviceRepository) : super(DeviceInitial()) { + DeviceSelectorCubit(this._deviceLocatorService) : super(DeviceInitial()) { startSearching(); } void startSearching() { - _deviceRepository.initializeRepository(); + _deviceLocatorStream = _deviceLocatorService.scan(); - _acquisitionDeviceStream ??= _deviceRepository - .watch() - .asBroadcastStream() - .listen((devices) => emit(DeviceSearchInProgress(devices)), - onError: (e) => emit(DeviceSearchFailure(e))); + _deviceLocatorStreamSubscription ??= _deviceLocatorStream.listen((device) { + _addDevice(device); + }); } Future connect(AcquisitionDevice device) async { emit(DeviceConnectionInProgress()); - _deviceRepository.connect(device, connectionCallback); + _deviceLocatorService.connect(device, connectionCallback); } void connectionCallback(bool connected, [Exception e]) { @@ -36,13 +36,21 @@ class DeviceSelectorCubit extends Cubit { emit(DeviceConnectionFailure(e)); resetSearch(); } else if (connected) { - _acquisitionDeviceStream.cancel(); + _deviceLocatorStreamSubscription.cancel(); emit(DeviceConnectionSuccess()); } } void resetSearch() { - _deviceRepository.disconnect(); + _deviceLocatorService.disconnect(); startSearching(); } + + void _addDevice(AcquisitionDevice device) { + if (!_scannedDevices.contains(device)) { + _scannedDevices.add(device); + + emit(DeviceSearchInProgress(_scannedDevices)); + } + } } diff --git a/mobile/lib/src/domain/acquisition_device/acquisition_device.dart b/mobile/lib/src/domain/acquisition_device/acquisition_device.dart index 9721ec3b..f2243067 100644 --- a/mobile/lib/src/domain/acquisition_device/acquisition_device.dart +++ b/mobile/lib/src/domain/acquisition_device/acquisition_device.dart @@ -1,10 +1,8 @@ +import 'package:polydodo/src/domain/acquisition_device/device_type.dart'; import 'package:polydodo/src/domain/entity.dart'; class AcquisitionDevice extends Entity { final String name; - - AcquisitionDevice( - id, - this.name, - ) : super(id); + final DeviceType deviceType; + AcquisitionDevice(id, this.name, this.deviceType) : super(id); } diff --git a/mobile/lib/src/domain/acquisition_device/device_locator_service.dart b/mobile/lib/src/domain/acquisition_device/device_locator_service.dart new file mode 100644 index 00000000..5681870c --- /dev/null +++ b/mobile/lib/src/domain/acquisition_device/device_locator_service.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:polydodo/src/domain/acquisition_device/acquisition_device.dart'; +import 'package:polydodo/src/domain/acquisition_device/device_type.dart'; +import 'package:polydodo/src/domain/acquisition_device/i_acquisition_device_repository.dart'; + +class DeviceLocatorService { + final IAcquisitionDeviceRepository _bluetoothRepository; + final IAcquisitionDeviceRepository _serialRepository; + + IAcquisitionDeviceRepository _currentRepository; + + StreamSubscription _serialStreamSubscription; + StreamSubscription _bluetoothStreamSubscription; + StreamController _acquisitionDeviceController; + + DeviceLocatorService(this._bluetoothRepository, this._serialRepository) { + _currentRepository = _serialRepository; + _acquisitionDeviceController = StreamController(); + } + + Stream scan() { + var bluetoothStream = _bluetoothRepository.scan(); + var serialStream = _serialRepository.scan(); + + _serialStreamSubscription ??= bluetoothStream.listen((event) { + _acquisitionDeviceController.add(event); + }); + _bluetoothStreamSubscription ??= serialStream.listen((event) { + _acquisitionDeviceController.add(event); + }); + + return _acquisitionDeviceController.stream; + } + + void connect(AcquisitionDevice device, Function(bool, Exception) callback) { + _bluetoothRepository.pauseScan(); + + _currentRepository = (device.deviceType == DeviceType.bluetooth) + ? _bluetoothRepository + : _serialRepository; + + _currentRepository.connect(device, callback); + } + + void disconnect() { + _currentRepository.disconnect(); + } + + Future>> startDataStream() { + return _currentRepository.startDataStream(); + } + + void stopDataStream() { + _currentRepository.stopDataStream(); + } +} diff --git a/mobile/lib/src/domain/acquisition_device/device_type.dart b/mobile/lib/src/domain/acquisition_device/device_type.dart new file mode 100644 index 00000000..a6a9ecf6 --- /dev/null +++ b/mobile/lib/src/domain/acquisition_device/device_type.dart @@ -0,0 +1 @@ +enum DeviceType { serial, bluetooth } 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 f0b569e8..897b4963 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 @@ -1,13 +1,12 @@ import 'acquisition_device.dart'; abstract class IAcquisitionDeviceRepository { - void initializeRepository(); + Stream scan(); + void pauseScan(); void connect(AcquisitionDevice device, Function(bool, [Exception]) callback); void disconnect(); Future>> startDataStream(); void stopDataStream(); - - Stream> watch(); } diff --git a/mobile/lib/src/infrastructure/connection_repositories/acquisition_device_repository.dart b/mobile/lib/src/infrastructure/connection_repositories/acquisition_device_repository.dart deleted file mode 100644 index d703295f..00000000 --- a/mobile/lib/src/infrastructure/connection_repositories/acquisition_device_repository.dart +++ /dev/null @@ -1,78 +0,0 @@ -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 = BluetoothRepository(); - final SerialRepository _serialRepository = SerialRepository(); - IAcquisitionDeviceRepository _currentRepository; - - StreamSubscription _bluetoothStream; - StreamSubscription _serialStream; - StreamSubscription _currentStream; - - StreamController> _acquisitionDeviceController; - StreamingSharedPreferences _preferences; - - AcquisitionDeviceRepository() { - _currentRepository = _serialRepository; - _acquisitionDeviceController = StreamController(); - } - - @override - 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(); - } - } - - @override - void connect(AcquisitionDevice device, Function(bool, Exception) callback) { - _currentRepository.connect(device, callback); - } - - @override - void disconnect() { - _currentRepository.disconnect(); - } - - @override - Future>> startDataStream() { - return _currentRepository.startDataStream(); - } - - @override - void stopDataStream() { - _currentRepository.stopDataStream(); - } - - @override - Stream> watch() { - return _acquisitionDeviceController.stream; - } -} diff --git a/mobile/lib/src/infrastructure/connection_repositories/bluetooth_repository.dart b/mobile/lib/src/infrastructure/connection_repositories/bluetooth_repository.dart index c362c7c9..9bd6e979 100644 --- a/mobile/lib/src/infrastructure/connection_repositories/bluetooth_repository.dart +++ b/mobile/lib/src/infrastructure/connection_repositories/bluetooth_repository.dart @@ -2,6 +2,7 @@ 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/device_type.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'; @@ -19,43 +20,42 @@ class BluetoothRepository implements IAcquisitionDeviceRepository { FlutterReactiveBle flutterReactiveBle; StreamSubscription _connectedDeviceStream; StreamSubscription _bluetoothScanSubscription; - final List _acquisitionDevicePersistency = []; - final streamController = StreamController>(); + Stream bluetoothStream; @override - void initializeRepository() { + Stream scan() { if (_bluetoothScanSubscription == null) { - flutterReactiveBle = FlutterReactiveBle(); - - _bluetoothScanSubscription = flutterReactiveBle.scanForDevices( - withServices: []).listen((device) => addDevice(device)); + _initScan(); } else { - _bluetoothScanSubscription.resume(); - _acquisitionDevicePersistency.clear(); + resumeScan(); } + + return bluetoothStream; } - void addDevice(DiscoveredDevice bluetoothDevice) { - var device = AcquisitionDevice( - UniqueId.from(bluetoothDevice.id.toString()), bluetoothDevice.name); + void _initScan() { + flutterReactiveBle = FlutterReactiveBle(); - final idx = _acquisitionDevicePersistency.indexOf(device); + bluetoothStream = flutterReactiveBle.scanForDevices(withServices: []).map( + (device) => AcquisitionDevice( + UniqueId.from(device.id), + (device.name.isEmpty) ? 'Unknown' : device.name, + DeviceType.bluetooth)); + } - if (idx == -1) { - _acquisitionDevicePersistency.add(device); - } else { - _acquisitionDevicePersistency[idx] = device; - } + @override + void pauseScan() { + _bluetoothScanSubscription.pause(); + } - streamController.add(_acquisitionDevicePersistency); + void resumeScan() { + _bluetoothScanSubscription.resume(); } @override void connect( AcquisitionDevice device, Function(bool, [Exception]) callback) async { _selectedDevice = device; - _acquisitionDevicePersistency.clear(); - _bluetoothScanSubscription.pause(); _connectedDeviceStream = flutterReactiveBle .connectToDevice( @@ -112,7 +112,4 @@ class BluetoothRepository implements IAcquisitionDeviceRepository { _sendCharacteristic, value: STOP_STREAM_CHAR.codeUnits)); } - - @override - Stream> watch() => streamController.stream; } diff --git a/mobile/lib/src/infrastructure/connection_repositories/serial_repository.dart b/mobile/lib/src/infrastructure/connection_repositories/serial_repository.dart index 0e854546..6418c103 100644 --- a/mobile/lib/src/infrastructure/connection_repositories/serial_repository.dart +++ b/mobile/lib/src/infrastructure/connection_repositories/serial_repository.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:polydodo/src/domain/acquisition_device/device_type.dart'; 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'; @@ -12,34 +13,48 @@ class SerialRepository implements IAcquisitionDeviceRepository { UsbDevice _selectedDevice; UsbPort _serialPort; StreamSubscription _inputStreamSubscription; - final List _acquisitionDevicePersistency = []; - final List _serialDevices = []; - final streamController = StreamController>(); + StreamSubscription _usbEventSubscription; + final Map _serialDevices = {}; + final streamController = StreamController(); @override - void initializeRepository() { - _acquisitionDevicePersistency.clear(); + Stream scan() { _serialDevices.clear(); - UsbSerial.listDevices().then((devices) => addDevices(devices)); + _usbEventSubscription ??= UsbSerial.usbEventStream.listen((event) { + if (event.event == UsbEvent.ACTION_USB_ATTACHED) { + _addDevices([event.device]); + } + }); + + UsbSerial.listDevices().then((devices) => _addDevices(devices)); + + return streamController.stream; } - void addDevices(List serialDevices) { + @override + void pauseScan() {} + + void _addDevices(List serialDevices) { for (var serialDevice in serialDevices) { + if (_serialDevices.containsKey(serialDevice.deviceId.toString())) { + continue; + } + var device = AcquisitionDevice( UniqueId.from(serialDevice.deviceId.toString()), - serialDevice.productName); + serialDevice.productName, + DeviceType.serial); + + streamController.add(device); - _acquisitionDevicePersistency.add(device); - _serialDevices.add(serialDevice); + _serialDevices[serialDevice.deviceId.toString()] = serialDevice; } - streamController.add(_acquisitionDevicePersistency); } @override Future connect( AcquisitionDevice device, Function(bool, Exception) callback) async { - _selectedDevice = - _serialDevices[_acquisitionDevicePersistency.indexOf(device)]; + _selectedDevice = _serialDevices[device.id.toString()]; _serialPort = await _selectedDevice.create(); var openSuccessful = await _serialPort.open(); @@ -100,7 +115,4 @@ class SerialRepository implements IAcquisitionDeviceRepository { Future stopDataStream() async { await _serialPort.write(Uint8List.fromList(STOP_STREAM_CHAR.codeUnits)); } - - @override - Stream> watch() => streamController.stream; } diff --git a/mobile/lib/src/locator.dart b/mobile/lib/src/locator.dart index 7a0f1be4..91c91893 100644 --- a/mobile/lib/src/locator.dart +++ b/mobile/lib/src/locator.dart @@ -4,19 +4,21 @@ import 'package:polydodo/src/application/device/device_selector_cubit.dart'; import 'package:polydodo/src/application/eeg_data/data_cubit.dart'; import 'package:polydodo/src/application/sleep_sequence_history/sleep_sequence_history_cubit.dart'; import 'package:polydodo/src/application/sleep_sequence_stats/sleep_sequence_stats_cubit.dart'; +import 'package:polydodo/src/domain/acquisition_device/device_locator_service.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/sleep_sequence/i_sleep_sequence_repository.dart'; -import 'package:polydodo/src/infrastructure/connection_repositories/acquisition_device_repository.dart'; +import 'package:polydodo/src/infrastructure/connection_repositories/bluetooth_repository.dart'; import 'package:polydodo/src/infrastructure/connection_repositories/eeg_data_repository.dart'; +import 'package:polydodo/src/infrastructure/connection_repositories/serial_repository.dart'; import 'package:polydodo/src/infrastructure/sleep_history/sleep_history_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( - AcquisitionDeviceRepository()); + _serviceLocator.registerSingleton( + DeviceLocatorService(BluetoothRepository(), SerialRepository())); _serviceLocator.registerSingleton(EEGDataRepository()); _serviceLocator .registerSingleton(SleepHistoryRepository()); @@ -26,7 +28,7 @@ void registerServices() { List createBlocProviders() => [ BlocProvider( create: (context) => DeviceSelectorCubit( - _serviceLocator.get(), + _serviceLocator.get(), ), ), BlocProvider( diff --git a/mobile/lib/src/presentation/navigation/navdrawer_tabs.dart b/mobile/lib/src/presentation/navigation/navdrawer_tabs.dart index 9c392844..1e1c0689 100644 --- a/mobile/lib/src/presentation/navigation/navdrawer_tabs.dart +++ b/mobile/lib/src/presentation/navigation/navdrawer_tabs.dart @@ -1,7 +1,7 @@ enum NavdrawerTab { Dashboard, RecordSleep, - BluetoothSelector, + DeviceSelector, History, SleepSequenceStats } diff --git a/mobile/lib/src/presentation/navigation/navdrawer_widget.dart b/mobile/lib/src/presentation/navigation/navdrawer_widget.dart index f1a5a995..7bda7b8e 100644 --- a/mobile/lib/src/presentation/navigation/navdrawer_widget.dart +++ b/mobile/lib/src/presentation/navigation/navdrawer_widget.dart @@ -26,10 +26,11 @@ class NavDrawer extends StatelessWidget { route: Routes.dashboardPage, context: context, ), + // Todo: find the real place for the device selector, up to debate _createDrawerItem( icon: Icons.bluetooth, - text: 'Bluetooth selector', - route: Routes.bluetoothSelectorPage, + text: 'Device selector', + route: Routes.deviceSelectorPage, context: context, ), _createDrawerItem( diff --git a/mobile/lib/src/presentation/navigation/routes/router.dart b/mobile/lib/src/presentation/navigation/routes/router.dart index f375a268..bf295daa 100644 --- a/mobile/lib/src/presentation/navigation/routes/router.dart +++ b/mobile/lib/src/presentation/navigation/routes/router.dart @@ -1,7 +1,7 @@ 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/device_selector/device_selector_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'; @@ -25,7 +25,7 @@ import 'package:polydodo/src/presentation/pages/sleep_sequence_stats_page/sleep_ page: RecordSleepRecordingPage, transitionsBuilder: TransitionsBuilders.fadeIn), CustomRoute( - page: BluetoothSelectorPage, + page: DeviceSelectorPage, transitionsBuilder: TransitionsBuilders.fadeIn), CustomRoute( page: SleepHistoryPage, diff --git a/mobile/lib/src/presentation/pages/device_selector/device_list.dart b/mobile/lib/src/presentation/pages/device_selector/device_list.dart new file mode 100644 index 00000000..0c2c9099 --- /dev/null +++ b/mobile/lib/src/presentation/pages/device_selector/device_list.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:polydodo/src/application/device/device_selector_cubit.dart'; +import 'package:polydodo/src/domain/acquisition_device/device_type.dart'; + +Widget buildDeviceList(var state) { + return ListView.builder( + itemCount: state.devices.length, + itemBuilder: (context, index) { + return Card( + child: ListTile( + onTap: () => BlocProvider.of(context) + .connect(state.devices[index]), + title: Text(state.devices[index].name), + subtitle: Text(state.devices[index].id.toString()), + trailing: _getTrailing(state.devices[index].deviceType), + ), + ); + }); +} + +Widget _getTrailing(DeviceType type) { + switch (type) { + case DeviceType.bluetooth: + return Icon(Icons.bluetooth); + case DeviceType.serial: + return Icon(Icons.usb); + default: + return Icon(Icons.error); + } +} diff --git a/mobile/lib/src/presentation/pages/bluetooth_page/bluetoothSelector_page.dart b/mobile/lib/src/presentation/pages/device_selector/device_selector_page.dart similarity index 60% rename from mobile/lib/src/presentation/pages/bluetooth_page/bluetoothSelector_page.dart rename to mobile/lib/src/presentation/pages/device_selector/device_selector_page.dart index 9a65314b..5303d85c 100644 --- a/mobile/lib/src/presentation/pages/bluetooth_page/bluetoothSelector_page.dart +++ b/mobile/lib/src/presentation/pages/device_selector/device_selector_page.dart @@ -7,23 +7,24 @@ 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'; -class BluetoothSelectorPage extends StatelessWidget { - static const name = 'bluetoothRoute'; +import 'device_list.dart'; - BluetoothSelectorPage({Key key}) : super(key: key); +class DeviceSelectorPage extends StatelessWidget { + static const name = 'DeviceSelectorRoute'; + + DeviceSelectorPage({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Polydodo')), - drawer: NavDrawer(activeTab: NavdrawerTab.BluetoothSelector), + drawer: NavDrawer(activeTab: NavdrawerTab.DeviceSelector), body: BlocConsumer( listener: (context, state) { - print(state.runtimeType); if (state is DeviceSearchFailure) { Scaffold.of(context).showSnackBar(SnackBar( - content: Text( - 'Unable to search for bluetooth devices because ${state.cause}'), + content: + Text('Unable to search for devices because ${state.cause}'), )); } else if (state is DeviceConnectionFailure) { Scaffold.of(context).showSnackBar(SnackBar( @@ -36,18 +37,7 @@ class BluetoothSelectorPage extends StatelessWidget { }, builder: (context, state) { if (state is DeviceSearchInProgress) { - return ListView.builder( - itemCount: state.devices.length, - itemBuilder: (context, index) { - return Card( - child: ListTile( - onTap: () => - BlocProvider.of(context) - .connect(state.devices[index]), - title: Text(state.devices[index].name), - subtitle: Text(state.devices[index].id.toString())), - ); - }); + return buildDeviceList(state); } else { return Container(); }