import 'dart:async'; import 'dart:convert'; import 'package:rxdart/rxdart.dart'; import 'package:rxdart/subjects.dart'; import 'package:flutter_blue/flutter_blue.dart'; import 'package:touch_demonstrator/model/touchData.dart'; import 'package:touch_demonstrator/model/buttonData.dart'; import 'package:flutter_test/flutter_test.dart'; class BluetoothBloc { FlutterBlue _flutterBlue = FlutterBlue.instance; /// Scanning StreamSubscription _scanSubscription; Map scanResults = Map(); /// State StreamSubscription _stateSubscription; final _state$ = BehaviorSubject(); final _isScanning$ = PublishSubject(); final _isConnected$ = BehaviorSubject(); final _deviceFound$ = PublishSubject>(); // final _touchMessage$ = BehaviorSubject(seedValue: 'init'); final _singleTouchMessage$ = BehaviorSubject(); final _touchDataHistory$ = BehaviorSubject>(); final _touchVisualise$ = BehaviorSubject>(); final _buttonEnabledStatus$ = BehaviorSubject>(); final _batteryValue$ = BehaviorSubject(); final _buttonVibration$ = PublishSubject(); final _slider$ = PublishSubject(); String touchMessage = ""; /// Device BluetoothDevice device; /// Device specifications StreamSubscription deviceConnection; StreamSubscription deviceStateSubscription; Map valueChangedSubscriptions = {}; BluetoothDeviceState deviceState = BluetoothDeviceState.disconnected; // Stream Controllers final _scanController = StreamController(); final _stopController = StreamController(); final _connectController = StreamController(); final _disconnectController = StreamController(); //SINKS (into bloc) Sink get scan => _scanController.sink; Sink get stop => _stopController.sink; Sink get connect => _connectController.sink; Sink get disconnect => _disconnectController.sink; //STREAMS (output bloc) Stream get bluetoothState$ => _state$.stream; Stream get batteryValue$ => _batteryValue$.stream; Stream get singleTouchMessage => _singleTouchMessage$.stream; Stream> get getTouchesToVisualise$ => _touchVisualise$.stream; //-> Stream> get buttonsState$ => _buttonEnabledStatus$.stream; Stream get vibrationButton$ => _buttonVibration$.stream; Stream get slider$ => _slider$.stream; Stream> get getHistory$ => _touchDataHistory$.stream.debounce(Duration(milliseconds: 600)); //-> Stream get isScanning$ => _isScanning$.stream; Stream get isConnected$ => _isConnected$.stream; Stream> get devicesFound$ => _deviceFound$.stream.throttle(Duration(seconds: 2)); static const xMax = 1200; String inputString = ""; final history = List(); static List _fingerTouches = List.generate(5, (index) => TouchData(5, index, 1200, 1200)); static List _buttonPressed = [ buttonsState.released, buttonsState.released, buttonsState.released ]; static List _buttonsState = [false, false, false]; // Listen to BluetoothBloc() { print('init: Bluetooth BLOC'); _flutterBlue.setLogLevel(LogLevel.warning); // Listen to BLoC inputs _flutterBlue.state.then((s) { _state$.add(s); }); _stateSubscription = _flutterBlue.onStateChanged().listen((s) { _state$.add(s); if (s == BluetoothState.off) { _resetSensor(); } }); _scanController.stream.listen((void _) { _startScan(); }); _stopController.stream.listen((void _) => _stopScan); _connectController.stream .listen((ScanResult result) => _connect(result.device)); _disconnectController.stream.listen((void _) { _disconnect(); }); _isConnected$.add(false); } void _startScan() { print('Bluetooth Bloc: start Bluetooth Scan'); scanResults.clear(); _isScanning$.add(true); _scanSubscription = _flutterBlue .scan( timeout: const Duration(seconds: 1), ) .listen((scanResult) { // print('Local Name: ${scanResult.advertisementData.localName}'); scanResults[scanResult.device.id] = scanResult; // print(scanResult.device.id); }, onDone: _stopScan); } void _stopScan() { /// Stops any ongoing Bluetooth Search print('BLOC: _stopScan()'); _scanSubscription?.cancel(); _scanSubscription = null; _findDevice("Touchpad Demonstrator"); } void _disconnect() { /// Disconnects all Bluetooth connections // Remove all value changed listeners print('Bluetooth Bloc: disconnect'); valueChangedSubscriptions.forEach((uuid, sub) => sub.cancel()); valueChangedSubscriptions.clear(); deviceStateSubscription?.cancel(); deviceStateSubscription = null; deviceConnection?.cancel(); deviceConnection = null; device = null; _resetSensor(); } void _resetSensor() { /// Resets all visual components of the app to initial state _buttonsState = [false, false, false]; _slider$.add(101); _buttonEnabledStatus$.add(_buttonsState); _isConnected$.add(false); _fingerTouches = [ TouchData(5, 0, 1200, 1200), TouchData(5, 1, 1200, 1200), TouchData(5, 2, 1200, 1200), TouchData(5, 3, 1200, 1200), TouchData(5, 4, 1200, 1200), ]; _touchVisualise$.add(_fingerTouches); history.clear(); _touchDataHistory$.add(history); _batteryValue$.add(null); } _findDevice(String deviceNameSearched) { /// If one device found -> connect to it. If more are found -> Stream the results var devicesFound = 0; BluetoothDevice deviceFound; // const String touchDemonstratorName = "Touchpad Demonstrator"; print("_findDevices()"); scanResults.forEach((k, v) { if (v.advertisementData.localName.contains(deviceNameSearched)) { devicesFound++; deviceFound = v.device; } }); print('$devicesFound devices Found'); if (devicesFound == 1) { // Only one device found -> connect directly. _connect(deviceFound); // _deviceFound$.add(scanResults); } else if (devicesFound > 1) { // More than one device found. _deviceFound$.add(scanResults); } else if (devicesFound == 0) { // No device found. scanResults.clear(); _deviceFound$.add(scanResults); } Future.delayed(const Duration(milliseconds: 700), () { _isScanning$.add(false); }); } void _connect(BluetoothDevice d) async { /// Connects device to BluetoothDevice D and subscribes to it. print('_connect'); device = d; deviceConnection = _flutterBlue .connect(device, timeout: const Duration(seconds: 2)) .listen(null, onDone: _disconnect); device.state.then((s) { deviceState = s; print('device state: $deviceState'); //change it immediately }); // Subscribe to connection changes deviceStateSubscription = device.onStateChanged().listen((s) { print('statesubscription change: $s'); deviceState = s; if (s == BluetoothDeviceState.connected) { ///discover services device.discoverServices().then((services) { // services = s; _checkServiceAndCharacteristic(services); }); } else if (s == BluetoothDeviceState.disconnected) { _disconnect(); } if (device != null) { _isConnected$.add(true); } }); } _checkServiceAndCharacteristic(List services) { print("_lookForService()"); services.forEach((s) { print(s.uuid.toString()); if (s.uuid.toString().toUpperCase().substring(4, 8) == "0001") { // Bluetooth UART characteristic found! print( "Service found: ${s.uuid.toString().toUpperCase().substring(4, 8)} -> check Characteristic"); s.characteristics.forEach((c) { if (c.uuid.toString().toUpperCase().substring(4, 8) == "0003") { /* print( "Characteristic found: ${f.uuid.toString().toUpperCase().substring(4, 8)}");*/ _subscribeTouchDataNotification(c); } }); } else if(s.uuid.toString().toUpperCase().substring(4,8) == "180F"){ print('battery service found'); s.characteristics.forEach((c){ if(c.uuid.toString().toUpperCase().substring(4,8) == "2A19"){ _subscribeBatteryNotfications(c); } }); } }); } _subscribeBatteryNotfications(BluetoothCharacteristic c) async { print('_subscribe battery: ${c.isNotifying}'); if (c.isNotifying) { // Unsubscribe from subscription await device.setNotifyValue(c, false); valueChangedSubscriptions[c.uuid]?.cancel(); valueChangedSubscriptions.remove(c.uuid); } else { print('subscribe battery'); var _batteryValue = await device.readCharacteristic(c); assert(_batteryValue[0] >= 0 && _batteryValue[0] <= 100); print('battery right now: ${_batteryValue[0]} %'); _batteryValue$.add(_batteryValue[0]); // ignore: cancel_subscriptions final subBattery = device.onValueChanged(c).listen((d) { print("battery: $d"); }); valueChangedSubscriptions[c.uuid] = subBattery; } } _subscribeTouchDataNotification(BluetoothCharacteristic c) async { if (c.isNotifying) { // Unsubscribe from subscription await device.setNotifyValue(c, false); valueChangedSubscriptions[c.uuid]?.cancel(); valueChangedSubscriptions.remove(c.uuid); } else { // ignore: cancel_subscriptions final sub = device.onValueChanged(c).listen((d) { List dFiltered = new List.from(d); // Copy List dFiltered.removeWhere((item) => item < 48 && item != 40 && item != 41 && item != 24 || item > 57 && item != 124); var dDecoded = utf8.decode(dFiltered); _combineStringToMeasurement(dDecoded); }); valueChangedSubscriptions[c.uuid] = sub; } } void _combineStringToMeasurement(String dDecoded) { // Combines one data set. for (var x = 0; x < dDecoded.length; x++) { if (touchMessage.length < 15) { if (touchMessage.length == 0) { if (dDecoded[x] == '(') { touchMessage += dDecoded[x]; } } else { touchMessage += dDecoded[x]; } } if (touchMessage.length == 15) { _extractTouchpointsFromString(touchMessage); touchMessage = ""; } else if (touchMessage.length > 15) { touchMessage = ""; // clear string } } } void _extractTouchpointsFromString(String inputString) { // Gets data from data string and saves it TouchData t = TouchData(0, 0, 0, 0); final RegExp regExp = new RegExp( r"\(\d\|\d\|\d{4}\|\d{4}\)", caseSensitive: false, multiLine: false, ); var matches = regExp.allMatches(inputString); for (var m in matches) { t.event = int.tryParse(m.group(0)[1]); t.fingerNumber = int.tryParse(m.group(0)[3]); t.x = int.tryParse(m.group(0).substring(5, 9)); t.y = int.tryParse(m.group(0).substring(10, 14)); if (t.x < 1100 && t.y < 1100 && (t.fingerNumber <= 4) && (t.event == 1 || t.event == 4 || t.event == 5)) { _checkButtons(t); if (t.x >= 950) { _checkSlider(t); } _fillSensorVisualisationStreams(t); } else { // print("Touch not Added!!!!!!!!!!!! $i ${t.e} ${t.f} ${t.x} ${t.y}"); } } } void _fillSensorVisualisationStreams(TouchData t) { _singleTouchMessage$.add(t); _fingerTouches[t.fingerNumber].touchEvent(t.event, t.x, t.y); _touchVisualise$.add(_fingerTouches); //History: history.add(t); _touchDataHistory$.add(history); } static bool enableButtons = true; Future _checkButtons(TouchData t) async { // 4 == press down; 1 == move; 5 == up final int button0Upper = 130, button0Lower = 320; final int button1Upper = 320, button1Lower = 510; final int button2Upper = 510, button2Lower = 730; const Duration durationButtonDebounce = Duration(milliseconds: 50); const Duration durationButtonPressMinimum = Duration(milliseconds: 100); if (t.x < 100 && t.y >= button0Upper && t.y <= button2Lower && enableButtons) { if (t.event == 5) { // a button is released! if (t.y >= button0Upper && t.y <= button0Lower && _buttonPressed[0] == buttonsState.pressed) { _buttonsState[0] = !_buttonsState[0]; _buttonsState[1] = _buttonsState[2] = false; _buttonPressed[0] = buttonsState.released; // wait for a new button press now print('bBloc: button released -> ButtonStatus: $_buttonsState'); } else if (t.y >= button1Upper && t.y <= button1Lower && _buttonPressed[1] == buttonsState.pressed) { _buttonsState[1] = !_buttonsState[1]; _buttonsState[0] = _buttonsState[2] = false; _buttonPressed[1] = buttonsState.released; // wait for a new button press now print('bBloc: button released -> ButtonStatus: $_buttonsState'); } else if (t.y >= button2Upper && t.y <= button2Lower && _buttonPressed[2] == buttonsState.pressed) { _buttonsState[2] = !_buttonsState[2]; _buttonsState[0] = _buttonsState[1] = false; _buttonPressed[2] = buttonsState.released; // wait for a new button press now print('bBloc: button released -> ButtonStatus: $_buttonsState'); } _buttonEnabledStatus$.add(_buttonsState); // update visuals _buttonVibration$.add(buttonsState.released); enableButtons = false; Future.delayed(durationButtonDebounce, () { enableButtons = true; }); } else if (t.event == 4) { // finger pressed on a button -> send event out var whichButtonPressed; if (t.y >= button0Upper && t.y <= button0Lower && _buttonPressed[0] == buttonsState.released) { whichButtonPressed = 0; } else if (t.y >= button1Upper && t.y <= button1Lower && _buttonPressed[1] == buttonsState.released) { whichButtonPressed = 1; } else if (t.y >= button2Upper && t.y <= button2Lower && _buttonPressed[2] == buttonsState.released) { whichButtonPressed = 2; } if (whichButtonPressed != null && whichButtonPressed >= 0 && whichButtonPressed <= 2) { print('bBloc: Button pressed!'); _buttonVibration$.add(buttonsState.pressed); Future.delayed(durationButtonPressMinimum, () { _buttonPressed[whichButtonPressed] = buttonsState.pressed; }); } } } } Future _checkSlider(TouchData t) async { const double topSlider = 75.0; const double bottomSlider = 940.0; int sliderPercent; const double range = bottomSlider - topSlider; double sliderPosition; double sliderValue; if ((t.event == 4 || t.event == 1)) { if (t.y > topSlider) { sliderPosition = t.y - topSlider; } else { sliderPosition = 0; } sliderValue = sliderPosition / range; // 0...1 sliderPercent = 100 - (sliderValue * 100).round(); // % if (sliderPercent > 100) sliderPercent = 100; if (sliderPercent < 0) sliderPercent = 0; _slider$.add(sliderPercent); } } void dispose() { print('cleanup'); _disconnect(); _scanController.close(); _stopController.close(); _connectController.close(); _disconnectController.close(); _touchDataHistory$.close(); _touchVisualise$.close(); _buttonEnabledStatus$.close(); _buttonVibration$.close(); _slider$.close(); _batteryValue$.close(); _isScanning$.close(); _isConnected$.close(); _singleTouchMessage$.close(); _stateSubscription?.cancel(); _stateSubscription = null; _scanSubscription?.cancel(); _scanSubscription = null; deviceConnection?.cancel(); deviceConnection = null; } int checkButtonPressed(TouchData t) { return 1; } int parseData(String s) { // _parseData(s); return 1; } }