diff --git a/example/lib/widgets/rgb_led_control_widget.dart b/example/lib/widgets/rgb_led_control_widget.dart index 20b614b..944a7fc 100644 --- a/example/lib/widgets/rgb_led_control_widget.dart +++ b/example/lib/widgets/rgb_led_control_widget.dart @@ -65,9 +65,9 @@ class _RgbLedControlWidgetState extends State { ElevatedButton( onPressed: () { widget.rgbLed.writeLedColor( - r: _currentColor.red, - g: _currentColor.green, - b: _currentColor.blue, + r: (255 * _currentColor.r).round(), + g: (255 * _currentColor.g).round(), + b: (255 * _currentColor.b).round(), ); }, child: const Text('Set'), diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index cc02cb0..891e03e 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -6,6 +6,7 @@ import 'package:universal_ble/universal_ble.dart'; import 'src/managers/ble_manager.dart'; import 'src/managers/notifier.dart'; +import 'src/models/devices/cosinuss_one.dart'; import 'src/models/devices/discovered_device.dart'; import 'src/models/devices/wearable.dart'; @@ -25,8 +26,6 @@ export 'src/models/capabilities/jingle_player.dart'; export 'src/models/capabilities/audio_player_controls.dart'; export 'src/models/capabilities/storage_path_audio_player.dart'; -part 'src/constants.dart'; - class WearableManager { static final WearableManager _instance = WearableManager._internal(); @@ -59,6 +58,15 @@ class WearableManager { disconnectNotifier.notifyListeners, ); if (connectionResult.$1) { + if (device.name == "earconnect") { + return CosinussOne( + name: device.name, + disconnectNotifier: disconnectNotifier, + bleManager: _bleManager, + discoveredDevice: device, + ); + } + return OpenEarableV1( name: device.name, disconnectNotifier: disconnectNotifier, diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 99ba65e..f422d88 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,4 +1,5 @@ -part of open_earable_flutter; +import 'models/devices/cosinuss_one.dart'; +import 'models/devices/open_earable_v1.dart'; const String sensorServiceUuid = "34c2e3bb-34aa-11eb-adc1-0242ac120002"; const String sensorConfigurationCharacteristicUuid = @@ -34,24 +35,15 @@ const String ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; const String ledSetStateCharacteristic = "81040e7a-4819-11ee-be56-0242ac120002"; // All UUIDs in a list for filters -List allUuids = [ - sensorServiceUuid, - sensorConfigurationCharacteristicUuid, - sensorDataCharacteristicUuid, - deviceInfoServiceUuid, - deviceIdentifierCharacteristicUuid, - deviceFirmwareVersionCharacteristicUuid, - deviceHardwareVersionCharacteristicUuid, - parseInfoServiceUuid, - schemeCharacteristicUuid, - sensorNamesCharacteristicUuid, - audioPlayerServiceUuid, - audioSourceCharacteristic, - audioStateCharacteristic, - batteryServiceUuid, - batteryLevelCharacteristicUuid, - buttonServiceUuid, - buttonStateCharacteristicUuid, - ledServiceUuid, - ledSetStateCharacteristic, +List allServiceUuids = [ + OpenEarableV1.ledServiceUuid, + OpenEarableV1.deviceInfoServiceUuid, + OpenEarableV1.audioPlayerServiceUuid, + OpenEarableV1.sensorServiceUuid, + OpenEarableV1.parseInfoServiceUuid, + OpenEarableV1.buttonServiceUuid, + OpenEarableV1.batteryServiceUuid, + CosinussOne.ppgAndAccServiceUuid, + CosinussOne.temperatureServiceUuid, + CosinussOne.heartRateServiceUuid, ]; diff --git a/lib/src/managers/ble_manager.dart b/lib/src/managers/ble_manager.dart index 890ca08..1c563d2 100644 --- a/lib/src/managers/ble_manager.dart +++ b/lib/src/managers/ble_manager.dart @@ -6,6 +6,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:universal_ble/universal_ble.dart'; import '../../open_earable_flutter.dart'; +import '../constants.dart'; /// A class that establishes and manages Bluetooth Low Energy (BLE) /// communication with OpenEarable devices. @@ -131,7 +132,7 @@ class BleManager { await UniversalBle.startScan( scanFilter: ScanFilter( // Needs to be passed for web, can be empty for the rest - withServices: kIsWeb ? allUuids : [], + withServices: kIsWeb ? allServiceUuids : [], ), ); } diff --git a/lib/src/managers/open_earable_sensor_manager.dart b/lib/src/managers/open_earable_sensor_manager.dart index ddafe6b..df9da4a 100644 --- a/lib/src/managers/open_earable_sensor_manager.dart +++ b/lib/src/managers/open_earable_sensor_manager.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; -import '../../open_earable_flutter.dart'; +import '../constants.dart'; import '../utils/mahony_ahrs.dart'; import 'ble_manager.dart'; @@ -31,6 +31,7 @@ class OpenEarableSensorManager { Exception("Can't write sensor config. Earable not connected"); } await _bleManager.write( + deviceId: deviceId, serviceId: sensorServiceUuid, characteristicId: sensorConfigurationCharacteristicUuid, byteData: sensorConfig.byteList, diff --git a/lib/src/models/devices/cosinuss_one.dart b/lib/src/models/devices/cosinuss_one.dart new file mode 100644 index 0000000..4294dc9 --- /dev/null +++ b/lib/src/models/devices/cosinuss_one.dart @@ -0,0 +1,328 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import '../capabilities/sensor.dart'; +import '../capabilities/sensor_manager.dart'; +import '../../managers/ble_manager.dart'; +import 'discovered_device.dart'; +import 'wearable.dart'; + +// For activating PPG and ACC +final List _sensorBluetoothCharacteristics = [ + 0x32, + 0x31, + 0x39, + 0x32, + 0x37, + 0x34, + 0x31, + 0x30, + 0x35, + 0x39, + 0x35, + 0x35, + 0x30, + 0x32, + 0x34, + 0x35, +]; + +class CosinussOne extends Wearable implements SensorManager { + static const ppgAndAccServiceUuid = "0000a000-1212-efde-1523-785feabcd123"; + static const temperatureServiceUuid = "00001809-0000-1000-8000-00805f9b34fb"; + static const heartRateServiceUuid = "0000180d-0000-1000-8000-00805f9b34fb"; + + final List _sensors; + final BleManager _bleManager; + final DiscoveredDevice _discoveredDevice; + + CosinussOne({ + required super.name, + required super.disconnectNotifier, + required BleManager bleManager, + required DiscoveredDevice discoveredDevice, + }) : _sensors = [], + _bleManager = bleManager, + _discoveredDevice = discoveredDevice { + _initSensors(); + } + + void _initSensors() { + _sensors.add( + _CosinussOneSensor( + discoveredDevice: _discoveredDevice, + bleManager: _bleManager, + sensorName: 'ACC', + chartTitle: 'Accelerometer', + shortChartTitle: 'Acc.', + axisNames: ['X', 'Y', 'Z'], + axisUnits: ["(unknown unit)", "(unknown unit)", "(unknown unit)"], + ), + ); + _sensors.add( + _CosinussOneSensor( + discoveredDevice: _discoveredDevice, + bleManager: _bleManager, + sensorName: 'PPG', + chartTitle: 'PPG', + shortChartTitle: 'PPG', + axisNames: ['Raw Red', 'Raw Green', 'Ambient'], + axisUnits: ["(unknown unit)", "(unknown unit)", "(unknown unit)"], + ), + ); + _sensors.add( + _CosinussOneSensor( + discoveredDevice: _discoveredDevice, + bleManager: _bleManager, + sensorName: 'TEMP', + chartTitle: 'Body Temperature', + shortChartTitle: 'Temp.', + axisNames: ['Temperature'], + axisUnits: ["°C"], + ), + ); + _sensors.add( + _CosinussOneSensor( + discoveredDevice: _discoveredDevice, + bleManager: _bleManager, + sensorName: 'HR', + chartTitle: 'Heart Rate', + shortChartTitle: 'HR', + axisNames: ['Heart Rate'], + axisUnits: ["BPM"], + ), + ); + } + + @override + String get deviceId => _discoveredDevice.id; + + @override + Future disconnect() { + return _bleManager.disconnect(_discoveredDevice.id); + } + + @override + List get sensors => List.unmodifiable(_sensors); +} + +// Based on https://github.com/teco-kit/cosinuss-flutter +class _CosinussOneSensor extends Sensor { + final List _axisNames; + final List _axisUnits; + final BleManager _bleManager; + final DiscoveredDevice _discoveredDevice; + + StreamSubscription? _dataSubscription; + + _CosinussOneSensor({ + required String sensorName, + required String chartTitle, + required String shortChartTitle, + required List axisNames, + required List axisUnits, + required BleManager bleManager, + required DiscoveredDevice discoveredDevice, + }) : _axisNames = axisNames, + _axisUnits = axisUnits, + _bleManager = bleManager, + _discoveredDevice = discoveredDevice, + super( + sensorName: sensorName, + chartTitle: chartTitle, + shortChartTitle: shortChartTitle, + ); + + @override + List get axisNames => _axisNames; + + @override + List get axisUnits => _axisUnits; + + int _twosComplimentOfNegativeMantissa(int mantissa) { + if ((4194304 & mantissa) != 0) { + return (((mantissa ^ -1) & 16777215) + 1) * -1; + } + + return mantissa; + } + + Stream _createAccStream() { + StreamController streamController = StreamController(); + + int startTime = DateTime.now().millisecondsSinceEpoch; + + _bleManager.write( + deviceId: _discoveredDevice.id, + serviceId: CosinussOne.ppgAndAccServiceUuid, + characteristicId: "0000a001-1212-efde-1523-785feabcd123", + byteData: _sensorBluetoothCharacteristics, + ); + + _dataSubscription?.cancel(); + _dataSubscription = _bleManager + .subscribe( + deviceId: _discoveredDevice.id, + serviceId: CosinussOne.ppgAndAccServiceUuid, + characteristicId: "0000a001-1212-efde-1523-785feabcd123", + ) + .listen((data) { + Int8List bytes = Int8List.fromList(data); + + // description based on placing the earable into your right ear canal + int accX = bytes[14]; + int accY = bytes[16]; + int accZ = bytes[18]; + + streamController.add( + SensorValue( + values: [accX.toDouble(), accY.toDouble(), accZ.toDouble()], + timestamp: DateTime.now().millisecondsSinceEpoch - startTime, + ), + ); + }); + + return streamController.stream; + } + + Stream _createPpgStream() { + StreamController streamController = StreamController(); + + int startTime = DateTime.now().millisecondsSinceEpoch; + + _bleManager.write( + deviceId: _discoveredDevice.id, + serviceId: CosinussOne.ppgAndAccServiceUuid, + characteristicId: "0000a001-1212-efde-1523-785feabcd123", + byteData: _sensorBluetoothCharacteristics, + ); + + _dataSubscription?.cancel(); + _dataSubscription = _bleManager + .subscribe( + deviceId: _discoveredDevice.id, + serviceId: CosinussOne.ppgAndAccServiceUuid, + characteristicId: "0000a001-1212-efde-1523-785feabcd123", + ) + .listen((data) { + Uint8List bytes = Uint8List.fromList(data); + + // corresponds to the raw reading of the PPG sensor from which the heart rate is computed + // + // example plot https://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/73/Screen-Shot-2019_2D00_01_2D00_24-at-19.30.24.png + // (image just for illustration purpose, obtained from a different sensor! Sensor value range differs.) + + var ppgRed = bytes[0] | + bytes[1] << 8 | + bytes[2] << 16 | + bytes[3] << 32; // raw green color value of PPG sensor + var ppgGreen = bytes[4] | + bytes[5] << 8 | + bytes[6] << 16 | + bytes[7] << 32; // raw red color value of PPG sensor + + var ppgGreenAmbient = bytes[8] | + bytes[9] << 8 | + bytes[10] << 16 | + bytes[11] << + 32; // ambient light sensor (e.g., if sensor is not placed correctly) + + streamController.add( + SensorValue( + values: [ + ppgRed.toDouble(), + ppgGreen.toDouble(), + ppgGreenAmbient.toDouble(), + ], + timestamp: DateTime.now().millisecondsSinceEpoch - startTime, + ), + ); + }); + + return streamController.stream; + } + + Stream _createTempStream() { + StreamController streamController = StreamController(); + + int startTime = DateTime.now().millisecondsSinceEpoch; + + _dataSubscription?.cancel(); + _dataSubscription = _bleManager + .subscribe( + deviceId: _discoveredDevice.id, + serviceId: CosinussOne.temperatureServiceUuid, + characteristicId: "00002a1c-0000-1000-8000-00805f9b34fb", + ) + .listen((data) { + var flag = data[0]; + + // based on GATT standard + double temperature = _twosComplimentOfNegativeMantissa( + ((data[3] << 16) | (data[2] << 8) | data[1]) & 16777215, + ) / + 100.0; + if ((flag & 1) != 0) { + temperature = ((98.6 * temperature) - 32.0) * + (5.0 / 9.0); // convert Fahrenheit to Celsius + } + + streamController.add( + SensorValue( + values: [temperature], + timestamp: DateTime.now().millisecondsSinceEpoch - startTime, + ), + ); + }); + + return streamController.stream; + } + + Stream _createHeartRateStream() { + StreamController streamController = StreamController(); + + int startTime = DateTime.now().millisecondsSinceEpoch; + + _dataSubscription?.cancel(); + _dataSubscription = _bleManager + .subscribe( + deviceId: _discoveredDevice.id, + serviceId: CosinussOne.heartRateServiceUuid, + characteristicId: "00002a37-0000-1000-8000-00805f9b34fb", + ) + .listen((data) { + Uint8List bytes = Uint8List.fromList(data); + + // based on GATT standard + int bpm = bytes[1]; + if (!((bytes[0] & 0x01) == 0)) { + bpm = (((bpm >> 8) & 0xFF) | ((bpm << 8) & 0xFF00)); + } + + streamController.add( + SensorValue( + values: [bpm.toDouble()], + timestamp: DateTime.now().millisecondsSinceEpoch - startTime, + ), + ); + }); + + return streamController.stream; + } + + @override + Stream get sensorStream { + switch (sensorName) { + case "ACC": + return _createAccStream(); + case "PPG": + return _createPpgStream(); + case "TEMP": + return _createTempStream(); + case "HR": + return _createHeartRateStream(); + default: + throw UnimplementedError(); + } + } +} diff --git a/lib/src/models/devices/open_earable_v1.dart b/lib/src/models/devices/open_earable_v1.dart index 724ca01..50b95ff 100644 --- a/lib/src/models/devices/open_earable_v1.dart +++ b/lib/src/models/devices/open_earable_v1.dart @@ -20,11 +20,9 @@ import '../capabilities/storage_path_audio_player.dart'; import 'discovered_device.dart'; import 'wearable.dart'; -const String _ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; const String _ledSetStateCharacteristic = "81040e7a-4819-11ee-be56-0242ac120002"; -const String _deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; const String _deviceIdentifierCharacteristicUuid = "45622511-6468-465a-b141-0b9b0f96b468"; const String _deviceFirmwareVersionCharacteristicUuid = @@ -32,7 +30,6 @@ const String _deviceFirmwareVersionCharacteristicUuid = const String _deviceHardwareVersionCharacteristicUuid = "45622513-6468-465a-b141-0b9b0f96b468"; -const String _audioPlayerServiceUuid = "5669146e-476d-11ee-be56-0242ac120002"; const String _audioSourceCharacteristic = "566916a8-476d-11ee-be56-0242ac120002"; const String _audioStateCharacteristic = "566916a9-476d-11ee-be56-0242ac120002"; @@ -49,6 +46,16 @@ class OpenEarableV1 extends Wearable JinglePlayer, AudioPlayerControls, StoragePathAudioPlayer { + static const String ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; + static const String deviceInfoServiceUuid = + "45622510-6468-465a-b141-0b9b0f96b468"; + static const String audioPlayerServiceUuid = + "5669146e-476d-11ee-be56-0242ac120002"; + static const String sensorServiceUuid = "34c2e3bb-34aa-11eb-adc1-0242ac120002"; + static const String parseInfoServiceUuid = "caa25cb7-7e1b-44f2-adc9-e8c06c9ced43"; + static const String buttonServiceUuid = "29c10bdc-4773-11ee-be56-0242ac120002"; + static const String batteryServiceUuid = "180F"; + final List _sensors; final List _sensorConfigurations; final BleManager _bleManager; @@ -180,7 +187,7 @@ class OpenEarableV1 extends Wearable data.setUint8(2, b); await _bleManager.write( deviceId: _discoveredDevice.id, - serviceId: _ledServiceUuid, + serviceId: ledServiceUuid, characteristicId: _ledSetStateCharacteristic, byteData: data.buffer.asUint8List(), ); @@ -193,7 +200,7 @@ class OpenEarableV1 extends Wearable Future readDeviceIdentifier() async { List deviceIdentifierBytes = await _bleManager.read( deviceId: _discoveredDevice.id, - serviceId: _deviceInfoServiceUuid, + serviceId: deviceInfoServiceUuid, characteristicId: _deviceIdentifierCharacteristicUuid, ); return String.fromCharCodes(deviceIdentifierBytes); @@ -206,7 +213,7 @@ class OpenEarableV1 extends Wearable Future readDeviceFirmwareVersion() async { List deviceGenerationBytes = await _bleManager.read( deviceId: _discoveredDevice.id, - serviceId: _deviceInfoServiceUuid, + serviceId: deviceInfoServiceUuid, characteristicId: _deviceFirmwareVersionCharacteristicUuid, ); return String.fromCharCodes(deviceGenerationBytes); @@ -219,7 +226,7 @@ class OpenEarableV1 extends Wearable Future readDeviceHardwareVersion() async { List hardwareGenerationBytes = await _bleManager.read( deviceId: _discoveredDevice.id, - serviceId: _deviceInfoServiceUuid, + serviceId: deviceInfoServiceUuid, characteristicId: _deviceHardwareVersionCharacteristicUuid, ); return String.fromCharCodes(hardwareGenerationBytes); @@ -269,7 +276,7 @@ class OpenEarableV1 extends Wearable data.setAll(6, loudnessBytes.buffer.asUint8List()); await _bleManager.write( - serviceId: _audioPlayerServiceUuid, + serviceId: audioPlayerServiceUuid, characteristicId: _audioSourceCharacteristic, byteData: data, ); @@ -294,7 +301,7 @@ class OpenEarableV1 extends Wearable data[0] = type; data[1] = jingleMap[jingle.key]!; await _bleManager.write( - serviceId: _audioPlayerServiceUuid, + serviceId: audioPlayerServiceUuid, characteristicId: _audioSourceCharacteristic, byteData: data, ); @@ -308,7 +315,7 @@ class OpenEarableV1 extends Wearable Uint8List data = Uint8List(1); data[0] = 1; await _bleManager.write( - serviceId: _audioPlayerServiceUuid, + serviceId: audioPlayerServiceUuid, characteristicId: _audioStateCharacteristic, byteData: data, ); @@ -319,18 +326,18 @@ class OpenEarableV1 extends Wearable Uint8List data = Uint8List(1); data[0] = 2; await _bleManager.write( - serviceId: _audioPlayerServiceUuid, + serviceId: audioPlayerServiceUuid, characteristicId: _audioStateCharacteristic, byteData: data, ); } @override - Future stopAudio()async { + Future stopAudio() async { Uint8List data = Uint8List(1); data[0] = 3; await _bleManager.write( - serviceId: _audioPlayerServiceUuid, + serviceId: audioPlayerServiceUuid, characteristicId: _audioStateCharacteristic, byteData: data, ); @@ -347,7 +354,7 @@ class OpenEarableV1 extends Wearable data.setRange(2, 2 + nameBytes.length, nameBytes); await _bleManager.write( - serviceId: _audioPlayerServiceUuid, + serviceId: audioPlayerServiceUuid, characteristicId: _audioSourceCharacteristic, byteData: data, );