From 859890b5bd22dca9774bf253ea88aa3c08012dce Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:49:11 +0100 Subject: [PATCH 01/12] added rough implementation of OpenEarable v2 --- example/.metadata | 30 +- example/android/.gitignore | 2 +- example/android/app/build.gradle | 53 +- .../android/app/src/main/AndroidManifest.xml | 12 + .../com/example/example/MainActivity.kt | 3 +- example/android/build.gradle | 17 +- example/android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 19 +- example/pubspec.lock | 8 + example/test/widget_test.dart | 30 ++ lib/open_earable_flutter.dart | 54 +- lib/src/managers/audio_player.dart | 154 ++++++ lib/src/managers/rgb_led.dart | 42 ++ lib/src/managers/sensor_manager.dart | 303 +++++++++++ lib/src/models/devices/open_earable_v2.dart | 496 ++++++++++++++++++ lib/src/models/discovered_device.dart | 23 + pubspec.yaml | 1 + 18 files changed, 1165 insertions(+), 86 deletions(-) create mode 100644 example/test/widget_test.dart create mode 100644 lib/src/managers/audio_player.dart create mode 100644 lib/src/managers/rgb_led.dart create mode 100644 lib/src/managers/sensor_manager.dart create mode 100644 lib/src/models/devices/open_earable_v2.dart create mode 100644 lib/src/models/discovered_device.dart diff --git a/example/.metadata b/example/.metadata index de53984..2d1be89 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "e1e47221e86272429674bec4f1bd36acc4fc7b77" + revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" channel: "stable" project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: android - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: ios - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: linux - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: macos - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: web - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - platform: windows - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 # User provided section diff --git a/example/android/.gitignore b/example/android/.gitignore index 6f56801..55afd91 100644 --- a/example/android/.gitignore +++ b/example/android/.gitignore @@ -7,7 +7,7 @@ gradle-wrapper.jar GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 118ee1d..b5511a9 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,67 +1,44 @@ plugins { id "com.android.application" id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" } -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - android { - namespace "com.example.example" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion + namespace = "com.example.example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + jvmTarget = JavaVersion.VERSION_1_8 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.example" + applicationId = "com.example.example" // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } } flutter { - source '../..' + source = "../.." } - -dependencies {} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index a66e8e3..aa355bc 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" + android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" @@ -30,6 +31,17 @@ android:name="flutterEmbedding" android:value="2" /> + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt index e793a00..70f8f08 100644 --- a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -2,5 +2,4 @@ package com.example.example import io.flutter.embedding.android.FlutterActivity -class MainActivity: FlutterActivity() { -} +class MainActivity: FlutterActivity() diff --git a/example/android/build.gradle b/example/android/build.gradle index f7eb7f6..d2ffbff 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -18,12 +5,12 @@ allprojects { } } -rootProject.buildDir = '../build' +rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { - project.evaluationDependsOn(':app') + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 94adc3a..2597170 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..7bb2df6 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 55c4ca8..b9e43bd 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -5,16 +5,21 @@ pluginManagement { def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() + }() - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + repositories { + google() + mavenCentral() + gradlePluginPortal() } } -include ":app" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.1.0" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ":app" diff --git a/example/pubspec.lock b/example/pubspec.lock index a6bbe60..adcce7c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -176,6 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + logger: + dependency: transitive + description: + name: logger + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.dev" + source: hosted + version: "2.5.0" logging: dependency: transitive description: diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..092d222 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index cc02cb0..3d79979 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -2,6 +2,8 @@ library open_earable_flutter; import 'dart:async'; +import 'package:logger/logger.dart'; +import 'package:open_earable_flutter/src/models/devices/open_earable_v2.dart'; import 'package:universal_ble/universal_ble.dart'; import 'src/managers/ble_manager.dart'; @@ -27,6 +29,12 @@ export 'src/models/capabilities/storage_path_audio_player.dart'; part 'src/constants.dart'; +Logger _logger = Logger(); + +const String _deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; +const String _deviceFirmwareVersionCharacteristicUuid = + "45622512-6468-465a-b141-0b9b0f96b468"; + class WearableManager { static final WearableManager _instance = WearableManager._internal(); @@ -59,12 +67,46 @@ class WearableManager { disconnectNotifier.notifyListeners, ); if (connectionResult.$1) { - return OpenEarableV1( - name: device.name, - disconnectNotifier: disconnectNotifier, - bleManager: _bleManager, - discoveredDevice: device, - ); + _logger.d("found following BLEServices: ${connectionResult.$2}"); + + if (connectionResult.$2.any((service) => service.uuid == _deviceInfoServiceUuid)) { + List softwareGenerationBytes = await _bleManager.read( + deviceId: device.id, + serviceId: _deviceInfoServiceUuid, + characteristicId: _deviceFirmwareVersionCharacteristicUuid, + ); + String softwareVersion = String.fromCharCodes(softwareGenerationBytes); + _logger.i("Softare version: $softwareVersion"); + + final versionRegex = RegExp(r'^\d+\.\d+\.\d+$'); + if (!versionRegex.hasMatch(softwareVersion)) { + throw Exception('Invalid software version format'); + } + + final version1Regex = RegExp(r'^1\.\d+\.\d+$'); + if (version1Regex.hasMatch(softwareVersion)) { + return OpenEarableV1( + name: device.name, + disconnectNotifier: disconnectNotifier, + bleManager: _bleManager, + discoveredDevice: device, + ); + } + + final v2Regex = RegExp(r'^\d+\.\d+.\d+$'); + if (v2Regex.hasMatch(softwareVersion)) { + return OpenEarableV2( + name: device.name, + disconnectNotifier: disconnectNotifier, + bleManager: _bleManager, + discoveredDevice: device, + ); + } + + throw Exception('Unsupported Firmware Version'); + } else { + throw Exception('Unsupported Device'); + } } else { throw Exception('Failed to connect to device'); } diff --git a/lib/src/managers/audio_player.dart b/lib/src/managers/audio_player.dart new file mode 100644 index 0000000..6846539 --- /dev/null +++ b/lib/src/managers/audio_player.dart @@ -0,0 +1,154 @@ +part of open_earable_flutter; + +/// A class that manages the playback of audio on the OpenEarable device from +/// an audio file on the SD card. +/// +/// This class provides functionality for controlling and interacting with an +/// audio player via Bluetooth Low Energy (BLE) communication. It allows you +/// to send commands to the audio player. +class AudioPlayer { + /// The BleManager instance used for Bluetooth communication. + final BleManager _bleManager; + + /// Creates an [AudioPlayer] instance with the provided [bleManager]. + /// + /// The [bleManager] is required for handling BLE communication. + AudioPlayer({required BleManager bleManager}) : _bleManager = bleManager; + + /// Plays a WAV file with the specified [fileName]. + /// + /// Example usage: + /// ```dart + /// setWavState('mySound.wav'); + /// ``` + void wavFile(String fileName) { + int type = 1; // 1 indicates it's a WAV file + Uint8List data = prepareData(type, fileName); + _bleManager.write( + serviceId: audioPlayerServiceUuid, + characteristicId: audioSourceCharacteristic, + byteData: data, + ); + } + + /// Plays a sound with a specified frequency and waveform. + /// + /// This method is used to generate and play sounds with a specific [waveType], [frequency] and [loudness] + /// Possible waveforms are: + /// + /// - 0: Sine. + /// - 1: Triangle. + /// - 2: Square. + /// - 3: Sawtooth. + /// + /// loudness must be between 0.0 - 1.0 + /// + /// Example usage: + /// ```dart + /// setFrequencyState(1, 440.0, 1.0); + /// ``` + void frequency(int waveType, double frequency, double loudness) { + int type = 2; // 2 indicates it's a frequency + var data = Uint8List(10); + data[0] = type; + data[1] = waveType; + + var freqBytes = Float32List.fromList([frequency]); + var loudnessBytes = Float32List.fromList([loudness]); + data.setAll(2, freqBytes.buffer.asUint8List()); + data.setAll(6, loudnessBytes.buffer.asUint8List()); + + _bleManager.write( + serviceId: audioPlayerServiceUuid, + characteristicId: audioSourceCharacteristic, + byteData: data, + ); + } + + /// Plays a jingle or short musical sound with [jingleId]. + /// + /// following jingles are supported: + /// + /// 0: 'IDLE' + /// 1: 'NOTIFICATION' + /// 2: 'SUCCESS' + /// 3: 'ERROR' + /// 4: 'ALARM' + /// 5: 'PING' + /// 6: 'OPEN' + /// 7: 'CLOSE' + /// 8: 'CLICK' + /// + void jingle(int jingleId) { + int type = 3; // 3 indicates it's a jingle + Uint8List data = Uint8List(2); + data[0] = type; + data[1] = jingleId; + _bleManager.write( + serviceId: audioPlayerServiceUuid, + characteristicId: audioSourceCharacteristic, + byteData: data, + ); + } + + Uint8List prepareData(int type, String name) { + List nameBytes = utf8.encode(name); + Uint8List data = Uint8List(2 + nameBytes.length); + data[0] = type; + data[1] = nameBytes.length; + data.setRange(2, 2 + nameBytes.length, nameBytes); + return data; + } + + /// Writes the audio state to the OpenEarable. + /// + /// The [state] parameter represents the playback state and should be one of + /// the following values from the [AudioPlayerState] enum: + /// + /// - [WavPlayerState.stop]: Stops audio playback. + /// - [WavPlayerState.start]: Starts audio playback. + /// - [WavPlayerState.pause]: Pauses audio playback. + /// - [WavPlayerState.unpause]: unpauses audio playback. + /// + void setState(AudioPlayerState state) async { + Uint8List data = Uint8List(1); + data[0] = getAudioPlayerStateValue(state); + await _bleManager.write( + serviceId: audioPlayerServiceUuid, + characteristicId: audioStateCharacteristic, + byteData: data, + ); + } + + /// Sets the audio player to the idle state. + /// + /// The audio player transitions to the idle state, + /// indicating that it is not currently playing any sound. + void setIdle() { + //_writeAudioPlayerState(SoundType.idle, AudioPlayerState.idle, ""); //TODO + } +} + +int getAudioPlayerStateValue(AudioPlayerState state) { + switch(state) { + case AudioPlayerState.idle: return 0; + case AudioPlayerState.start: return 1; + case AudioPlayerState.pause: return 2; + case AudioPlayerState.stop: return 3; + } +} + +/// An enumeration representing the possible states of the audio player. +enum AudioPlayerState { + /// Idle state. + idle, + + /// Play the audio file. + start, + + /// Pause the audio file. + pause, + + /// Stop the audio file. + stop, +} diff --git a/lib/src/managers/rgb_led.dart b/lib/src/managers/rgb_led.dart new file mode 100644 index 0000000..7c65e7f --- /dev/null +++ b/lib/src/managers/rgb_led.dart @@ -0,0 +1,42 @@ +part of open_earable_flutter; + +/// The `RgbLed` class provides methods to control the RGB LED on an OpenEarable device. +/// +/// You can use this class to set the LED state to control its color and behavior. +class RgbLed { + final BleManager _bleManager; + + /// Creates an instance of the `RgbLed` class with the provided `BleManager` instance. + /// + /// The `BleManager` is used for communication with the OpenEarable device. + RgbLed({required BleManager bleManager}) : _bleManager = bleManager; + + /// Writes the state of the RGB LED on the OpenEarable device. + /// + /// Parameters: + /// - `r`: The red color component value (0-255) for the LED. + /// - `g`: The green color component value (0-255) for the LED. + /// - `b`: The blue color component value (0-255) for the LED. + /// + /// Use this method to easily set the color of the in-built RGB LED on the OpenEarable device. + Future writeLedColor( + {required int r, required int g, required int b}) async { + if (!_bleManager.connected) { + Exception("Can't write sensor config. Earable not connected"); + } + if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) { + throw ArgumentError('The color values must be in range 0-255'); + } + ByteData data = ByteData(3); + data.setUint8(0, r); + data.setUint8(1, g); + data.setUint8(2, b); + await _bleManager.write( + serviceId: ledServiceUuid, + characteristicId: ledSetStateCharacteristic, + byteData: data.buffer.asUint8List()); + } +} + +/// Enum representing the LED state for an RGB LED. +enum LedState { off, green, blue, red, cyan, yellow, magenta, white } diff --git a/lib/src/managers/sensor_manager.dart b/lib/src/managers/sensor_manager.dart new file mode 100644 index 0000000..483bea4 --- /dev/null +++ b/lib/src/managers/sensor_manager.dart @@ -0,0 +1,303 @@ +part of open_earable_flutter; + +/// Manages sensor-related functionality for the OpenEarable device. +class SensorManager { + final imuID = 0; + final BleManager _bleManager; + final MahonyAHRS _mahonyAHRS = MahonyAHRS(); + List? _sensorSchemes; + + /// Creates a [SensorManager] instance with the specified [bleManager]. + SensorManager({required BleManager bleManager}) : _bleManager = bleManager; + + /// Writes the sensor configuration to the OpenEarable device. + /// + /// The [sensorConfig] parameter contains the sensor id, sampling rate + /// and latency of the sensor. + Future writeSensorConfig(OpenEarableSensorConfig sensorConfig) async { + if (!_bleManager.connected) { + Exception("Can't write sensor config. Earable not connected"); + } + await _bleManager.write( + serviceId: sensorServiceUuid, + characteristicId: sensorConfigurationCharacteristicUuid, + byteData: sensorConfig.byteList); + if (_sensorSchemes == null) { + await _readSensorScheme(); + } + } + + /// Subscribes to sensor data for a specific sensor. + /// + /// The [sensorId] parameter specifies the ID of the sensor to subscribe to. + /// - 0: IMU data + /// - 1: Barometer data + /// Returns a [Stream] of sensor data as a [Map] of sensor values. + Stream> subscribeToSensorData(int sensorId) { + if (!_bleManager.connected) { + Exception("Can't subscribe to sensor data. Earable not connected"); + } + StreamController> streamController = + StreamController(); + int lastTimestamp = 0; + _bleManager + .subscribe( + serviceId: sensorServiceUuid, + characteristicId: sensorDataCharacteristicUuid) + .listen((data) async { + if (data.isNotEmpty && data[0] == sensorId) { + Map parsedData = await _parseData(data); + if (sensorId == imuID) { + int timestamp = parsedData["timestamp"]; + double ax = parsedData["ACC"]["X"]; + double ay = parsedData["ACC"]["Y"]; + double az = parsedData["ACC"]["Z"]; + + double gx = parsedData["GYRO"]["X"]; + double gy = parsedData["GYRO"]["Y"]; + double gz = parsedData["GYRO"]["Z"]; + + double dt = (timestamp - lastTimestamp) / 1000.0; + _mahonyAHRS.update(ax, ay, az, gx, gy, gz, + dt); // x, y, z was changed in firmware to -x, z, y + lastTimestamp = timestamp; + List q = _mahonyAHRS.quaternion; + double yaw = -atan2(2 * (q[0] * q[3] + q[1] * q[2]), + 1 - 2 * (q[2] * q[2] + q[3] * q[3])); + // Pitch (around Y-axis) + double pitch = -asin(2 * (q[0] * q[2] - q[3] * q[1])); + // Roll (around X-axis) + double roll = -atan2(2 * (q[0] * q[1] + q[2] * q[3]), + 1 - 2 * (q[1] * q[1] + q[2] * q[2])); + parsedData["EULER"] = {}; + parsedData["EULER"]["YAW"] = yaw; + parsedData["EULER"]["PITCH"] = pitch; + parsedData["EULER"]["ROLL"] = roll; + parsedData["EULER"] + ["units"] = {"YAW": "rad", "PITCH": "rad", "ROLL": "rad"}; + } + streamController.add(parsedData); + } + }, onError: (error) {}); + + return streamController.stream; + } + + // Parses raw sensor data bytes into a [Map] of sensor values. + Future> _parseData(data) async { + ByteData byteData = ByteData.sublistView(Uint8List.fromList(data)); + var byteIndex = 0; + final sensorId = byteData.getUint8(byteIndex); + byteIndex += 2; // skip one byte because of size byte that is not used + final timestamp = byteData.getUint32(byteIndex, Endian.little); + byteIndex += 4; + Map parsedData = {}; + if (_sensorSchemes == null) { + await _readSensorScheme(); + } + SensorScheme foundScheme = _sensorSchemes!.firstWhere( + (scheme) => scheme.sensorId == sensorId, + ); + parsedData["sensorId"] = sensorId; + parsedData["timestamp"] = timestamp; + parsedData["sensorName"] = foundScheme.sensorName; + for (Component component in foundScheme.components) { + if (parsedData[component.groupName] == null) { + parsedData[component.groupName] = {}; + } + if (parsedData[component.groupName]["units"] == null) { + parsedData[component.groupName]["units"] = {}; + } + final dynamic parsedValue; + switch (ParseType.values[component.type]) { + case ParseType.int8: + parsedValue = byteData.getInt8(byteIndex); + byteIndex += 1; + break; + case ParseType.uint8: + parsedValue = byteData.getUint8(byteIndex); + byteIndex += 1; + break; + case ParseType.int16: + parsedValue = byteData.getInt16(byteIndex, Endian.little); + byteIndex += 2; + break; + case ParseType.uint16: + parsedValue = byteData.getUint16(byteIndex, Endian.little); + byteIndex += 2; + break; + case ParseType.int32: + parsedValue = byteData.getInt32(byteIndex, Endian.little); + byteIndex += 4; + break; + case ParseType.uint32: + parsedValue = byteData.getUint32(byteIndex, Endian.little); + byteIndex += 4; + break; + case ParseType.float: + parsedValue = byteData.getFloat32(byteIndex, Endian.little); + byteIndex += 4; + break; + case ParseType.double: + parsedValue = byteData.getFloat64(byteIndex, Endian.little); + byteIndex += 8; + break; + } + parsedData[component.groupName][component.componentName] = parsedValue; + parsedData[component.groupName]["units"][component.componentName] = + component.unitName; + } + return parsedData; + } + + /// Returns a [Stream] of battery level updates. + /// Battery level is provided as percent values (0-100). + Stream getBatteryLevelStream() { + return _bleManager.subscribe( + serviceId: batteryServiceUuid, + characteristicId: batteryLevelCharacteristicUuid.toString()); + } + + /// Returns a [Stream] of button state updates. + /// - 0: Idle + /// - 1: Pressed + /// - 2: Held + Stream getButtonStateStream() { + return _bleManager.subscribe( + serviceId: buttonServiceUuid, + characteristicId: buttonStateCharacteristicUuid.toString()); + } + + /// Reads the sensor scheme that is needed to parse the raw sensor + /// data bytes + Future _readSensorScheme() async { + List byteStream = await _bleManager.read( + serviceId: parseInfoServiceUuid, + characteristicId: schemeCharacteristicUuid); + + int currentIndex = 0; + + int numSensors = byteStream[currentIndex++]; + List sensorSchemes = []; + for (int i = 0; i < numSensors; i++) { + int sensorId = byteStream[currentIndex++]; + + int nameLength = byteStream[currentIndex++]; + + List nameBytes = + byteStream.sublist(currentIndex, currentIndex + nameLength); + String sensorName = String.fromCharCodes(nameBytes); + currentIndex += nameLength; + + int componentCount = byteStream[currentIndex++]; + + SensorScheme sensorScheme = + SensorScheme(sensorId, sensorName, componentCount); + + for (int j = 0; j < componentCount; j++) { + int componentType = byteStream[currentIndex++]; + + int groupNameLength = byteStream[currentIndex++]; + + List groupNameBytes = + byteStream.sublist(currentIndex, currentIndex + groupNameLength); + String groupName = String.fromCharCodes(groupNameBytes); + currentIndex += groupNameLength; + + int componentNameLength = byteStream[currentIndex++]; + + List componentNameBytes = byteStream.sublist( + currentIndex, currentIndex + componentNameLength); + String componentName = String.fromCharCodes(componentNameBytes); + currentIndex += componentNameLength; + + int unitNameLength = byteStream[currentIndex++]; + + List unitNameBytes = + byteStream.sublist(currentIndex, currentIndex + unitNameLength); + String unitName = String.fromCharCodes(unitNameBytes); + currentIndex += unitNameLength; + + Component component = + Component(componentType, groupName, componentName, unitName); + sensorScheme.components.add(component); + } + + sensorSchemes.add(sensorScheme); + } + _sensorSchemes = sensorSchemes; + } +} + +/// Represents a sensor component with its type, group name, component name, and unit name. +class Component { + int type; + String groupName; + String componentName; + String unitName; + + /// Creates a [Component] instance with the specified parameters. + Component(this.type, this.groupName, this.componentName, this.unitName); + + @override + String toString() { + return 'Component(type: $type, groupName: $groupName, componentName: $componentName, unitName: $unitName)'; + } +} + +/// Represents a sensor scheme that contains the components for a sensor. +class SensorScheme { + int sensorId; + String sensorName; + int componentCount; + List components = []; + + SensorScheme(this.sensorId, this.sensorName, this.componentCount); + + @override + String toString() { + return 'Sensorscheme(sensorId: $sensorId, sensorName: $sensorName, components: ${components.map((component) => component.toString()).toList()})'; + } +} + +/// Represents the configuration for an OpenEarable sensor, including sensor ID, sampling rate, and latency. +class OpenEarableSensorConfig { + int sensorId; // 8-bit unsigned integer + double samplingRate; // 4-byte float + int latency; // 32-bit unsigned integer + + /// Creates an [OpenEarableSensorConfig] instance with the specified properties. + OpenEarableSensorConfig({ + required this.sensorId, + required this.samplingRate, + required this.latency, + }); + + /// Returns a byte list representing the sensor configuration for writing to the device. + List get byteList { + ByteData data = ByteData(9); + data.setUint8(0, sensorId); + data.setFloat32(1, samplingRate, Endian.little); + data.setUint32(5, latency, Endian.little); + return data.buffer.asUint8List(); + } + + @override + String toString() { + return 'OpenEarableSensorConfig(sensorId: $sensorId, sampleRate: $samplingRate, latency: $latency)'; + } +} + +enum ParseType { + int8, + uint8, + + int16, + uint16, + + int32, + uint32, + + float, + double +} diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart new file mode 100644 index 0000000..b4ff17b --- /dev/null +++ b/lib/src/models/devices/open_earable_v2.dart @@ -0,0 +1,496 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:logger/logger.dart'; + +import '../../managers/open_earable_sensor_manager.dart'; +import '../../utils/simple_kalman.dart'; +import '../capabilities/device_firmware_version.dart'; +import '../capabilities/device_hardware_version.dart'; +import '../capabilities/device_identifier.dart'; +import '../capabilities/rgb_led.dart'; +import '../capabilities/sensor.dart'; +import '../capabilities/sensor_configuration.dart'; +import '../capabilities/sensor_configuration_manager.dart'; +import '../capabilities/sensor_manager.dart'; +import '../../managers/ble_manager.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 = + "45622512-6468-465a-b141-0b9b0f96b468"; +const String _deviceHardwareVersionCharacteristicUuid = + "45622513-6468-465a-b141-0b9b0f96b468"; + +const String _deviceParseInfoServiceUuid = + "caa25cb7-7e1b-44f2-adc9-e8c06c9ced43"; +const String _deviceParseInfoCharacteristicUuid = + "caa25cb8-7e1b-44f2-adc9-e8c06c9ced43"; + +Logger _logger = Logger(); + +Map _parseSchemeCharacteristic(List data) { + Map parsedData = {}; + + int sensorCount = data.removeAt(0); + for (int i = 0; i < sensorCount; i++) { + Map sensorMap = _parseSensorScheme(data); + parsedData.addAll(sensorMap); + } + + return parsedData; +} + +Map _parseSensorScheme(List data) { + int sensorID = data.removeAt(0); + String sensorName = _parseString(data); + int componentsCount = data.removeAt(0); + + Map componentsMap = {}; + for (int i = 0; i < componentsCount; i++) { + Map comp = _parseComponentScheme(data); + for (var group in comp.keys) { + if (!componentsMap.containsKey(group)) { + componentsMap[group] = {}; + } + (componentsMap[group] as Map).addAll(comp[group] as Map); + } + } + + Map parsedSensorScheme = { + sensorName : { + 'SensorID' : sensorID, + }, + }; + parsedSensorScheme.addAll(componentsMap); + + return parsedSensorScheme; +} + +Map _parseComponentScheme(List data) { + int type = data.removeAt(0); + String groupName = _parseString(data); + String componentName = _parseString(data); + String unitName = _parseString(data); + + Map parsedComponentScheme = { + groupName : { + componentName : { + 'type' : type, + 'unit' : unitName, + }, + }, + }; + + return parsedComponentScheme; +} + +String _parseString(List data) { + int stringLength = data.removeAt(0); + List stringBytes = data.sublist(0, stringLength); + data.removeRange(0, stringLength); + return String.fromCharCodes(stringBytes); +} + +class OpenEarableV2 extends Wearable + implements + SensorManager, + SensorConfigurationManager, + RgbLed, + DeviceIdentifier, + DeviceFirmwareVersion, + DeviceHardwareVersion { + final List _sensors; + final List _sensorConfigurations; + final BleManager _bleManager; + final DiscoveredDevice _discoveredDevice; + + OpenEarableV2({ + required super.name, + required super.disconnectNotifier, + required BleManager bleManager, + required DiscoveredDevice discoveredDevice, + }) : _sensors = [], + _sensorConfigurations = [], + _bleManager = bleManager, + _discoveredDevice = discoveredDevice { + _initSensors(); + } + + void _initSensors() async { + List sensorParseSchemeData = await _bleManager.read(deviceId: _discoveredDevice.id, serviceId: _deviceParseInfoServiceUuid, characteristicId: _deviceParseInfoCharacteristicUuid); + _logger.d("Read raw parse info: $sensorParseSchemeData"); + Map parseInfo = _parseSchemeCharacteristic(sensorParseSchemeData); + _logger.i("Found the following info about parsing: $parseInfo"); + + OpenEarableSensorManager sensorManager = OpenEarableSensorManager( + bleManager: _bleManager, + deviceId: _discoveredDevice.id, + ); + + _sensors.add( + _OpenEarableSensor( + sensorManager: sensorManager, + sensorName: 'ACC', + chartTitle: 'Accelerometer', + shortChartTitle: 'Acc.', + axisNames: ['X', 'Y', 'Z'], + axisUnits: ["m/s\u00B2", "m/s\u00B2", "m/s\u00B2"], + ), + ); + _sensors.add( + _OpenEarableSensor( + sensorManager: sensorManager, + sensorName: 'GYRO', + chartTitle: 'Gyroscope', + shortChartTitle: 'Gyro.', + axisNames: ['X', 'Y', 'Z'], + axisUnits: ["°/s", "°/s", "°/s"], + ), + ); + _sensors.add( + _OpenEarableSensor( + sensorManager: sensorManager, + sensorName: 'MAG', + chartTitle: 'Magnetometer', + shortChartTitle: 'Magn.', + axisNames: ['X', 'Y', 'Z'], + axisUnits: ["µT", "µT", "µT"], + ), + ); + _sensors.add( + _OpenEarableSensor( + sensorManager: sensorManager, + sensorName: 'BARO', + chartTitle: 'Pressure', + shortChartTitle: 'Press.', + axisNames: ['Pressure'], + axisUnits: ["Pa"], + ), + ); + _sensors.add( + _OpenEarableSensor( + sensorManager: sensorManager, + sensorName: 'TEMP', + chartTitle: 'Temperature (Ambient)', + shortChartTitle: 'Temp. (A.)', + axisNames: ['Temperature'], + axisUnits: ["°C"], + ), + ); + + _sensorConfigurations.add( + _ImuSensorConfiguration( + sensorManager: sensorManager, + ), + ); + _sensorConfigurations.add( + _BarometerSensorConfiguration( + sensorManager: sensorManager, + ), + ); + _sensorConfigurations.add( + _MicrophoneSensorConfiguration( + sensorManager: sensorManager, + ), + ); + } + + @override + String get deviceId => _discoveredDevice.id; + + @override + Future writeLedColor({ + required int r, + required int g, + required int b, + }) async { + // if (!_bleManager.connected) { + // Exception("Can't write sensor config. Earable not connected"); + // } + if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) { + throw ArgumentError('The color values must be in range 0-255'); + } + ByteData data = ByteData(3); + data.setUint8(0, r); + data.setUint8(1, g); + data.setUint8(2, b); + await _bleManager.write( + deviceId: _discoveredDevice.id, + serviceId: _ledServiceUuid, + characteristicId: _ledSetStateCharacteristic, + byteData: data.buffer.asUint8List(), + ); + } + + /// Reads the device identifier from the connected OpenEarable device. + /// + /// Returns a `Future` that completes with the device identifier as a `String`. + @override + Future readDeviceIdentifier() async { + List deviceIdentifierBytes = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _deviceInfoServiceUuid, + characteristicId: _deviceIdentifierCharacteristicUuid, + ); + return String.fromCharCodes(deviceIdentifierBytes); + } + + /// Reads the device firmware version from the connected OpenEarable device. + /// + /// Returns a `Future` that completes with the device firmware version as a `String`. + @override + Future readDeviceFirmwareVersion() async { + List deviceGenerationBytes = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _deviceInfoServiceUuid, + characteristicId: _deviceFirmwareVersionCharacteristicUuid, + ); + return String.fromCharCodes(deviceGenerationBytes); + } + + /// Reads the device hardware version from the connected OpenEarable device. + /// + /// Returns a `Future` that completes with the device firmware version as a `String`. + @override + Future readDeviceHardwareVersion() async { + List hardwareGenerationBytes = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _deviceInfoServiceUuid, + characteristicId: _deviceHardwareVersionCharacteristicUuid, + ); + return String.fromCharCodes(hardwareGenerationBytes); + } + + @override + Future disconnect() { + return _bleManager.disconnect(_discoveredDevice.id); + } + + @override + List get sensorConfigurations => + List.unmodifiable(_sensorConfigurations); + + @override + List get sensors => List.unmodifiable(_sensors); +} + +class _OpenEarableSensor extends Sensor { + final List _axisNames; + final List _axisUnits; + final OpenEarableSensorManager _sensorManager; + + StreamSubscription? _dataSubscription; + + _OpenEarableSensor({ + required String sensorName, + required String chartTitle, + required String shortChartTitle, + required List axisNames, + required List axisUnits, + required OpenEarableSensorManager sensorManager, + }) : _axisNames = axisNames, + _axisUnits = axisUnits, + _sensorManager = sensorManager, + super( + sensorName: sensorName, + chartTitle: chartTitle, + shortChartTitle: shortChartTitle, + ); + + @override + List get axisNames => _axisNames; + + @override + List get axisUnits => _axisUnits; + + Stream _getAccGyroMagStream() { + StreamController streamController = StreamController(); + + final errorMeasure = {"ACC": 5.0, "GYRO": 10.0, "MAG": 25.0}; + + SimpleKalman kalmanX = SimpleKalman( + errorMeasure: errorMeasure[sensorName]!, + errorEstimate: errorMeasure[sensorName]!, + q: 0.9, + ); + SimpleKalman kalmanY = SimpleKalman( + errorMeasure: errorMeasure[sensorName]!, + errorEstimate: errorMeasure[sensorName]!, + q: 0.9, + ); + SimpleKalman kalmanZ = SimpleKalman( + errorMeasure: errorMeasure[sensorName]!, + errorEstimate: errorMeasure[sensorName]!, + q: 0.9, + ); + _dataSubscription?.cancel(); + _dataSubscription = _sensorManager.subscribeToSensorData(0).listen((data) { + int timestamp = data["timestamp"]; + + SensorValue sensorValue = SensorValue( + values: [ + kalmanX.filtered(data[sensorName]["X"]), + kalmanY.filtered(data[sensorName]["Y"]), + kalmanZ.filtered(data[sensorName]["Z"]), + ], + timestamp: timestamp, + ); + + streamController.add(sensorValue); + }); + + return streamController.stream; + } + + Stream _createSingleDataSubscription(String componentName) { + StreamController streamController = StreamController(); + + _dataSubscription?.cancel(); + _dataSubscription = _sensorManager.subscribeToSensorData(1).listen((data) { + int timestamp = data["timestamp"]; + + SensorValue sensorValue = SensorValue( + values: [data[sensorName][componentName]], + timestamp: timestamp, + ); + + streamController.add(sensorValue); + }); + + return streamController.stream; + } + + @override + Stream get sensorStream { + switch (sensorName) { + case "ACC": + case "GYRO": + case "MAG": + return _getAccGyroMagStream(); + case "BARO": + return _createSingleDataSubscription("Pressure"); + case "TEMP": + return _createSingleDataSubscription("Temperature"); + default: + throw UnimplementedError(); + } + } +} + +class _ImuSensorConfiguration extends SensorConfiguration { + final OpenEarableSensorManager _sensorManager; + + _ImuSensorConfiguration({ + required OpenEarableSensorManager sensorManager, + }) : _sensorManager = sensorManager, + super( + name: 'IMU', + unit: 'Hz', + values: const [ + SensorConfigurationValue(key: '0'), + SensorConfigurationValue(key: '10'), + SensorConfigurationValue(key: '20'), + SensorConfigurationValue(key: '30'), + ], + ); + + @override + void setConfiguration(SensorConfigurationValue configuration) { + if (!super.values.contains(configuration)) { + throw UnimplementedError(); + } + + double imuSamplingRate = double.parse(configuration.key); + OpenEarableSensorConfig imuConfig = OpenEarableSensorConfig( + sensorId: 0, + samplingRate: imuSamplingRate, + latency: 0, + ); + + _sensorManager.writeSensorConfig(imuConfig); + } +} + +class _BarometerSensorConfiguration extends SensorConfiguration { + final OpenEarableSensorManager _sensorManager; + + _BarometerSensorConfiguration({ + required OpenEarableSensorManager sensorManager, + }) : _sensorManager = sensorManager, + super( + name: 'Barometer', + unit: 'Hz', + values: const [ + SensorConfigurationValue(key: '0'), + SensorConfigurationValue(key: '10'), + SensorConfigurationValue(key: '20'), + SensorConfigurationValue(key: '30'), + ], + ); + + @override + void setConfiguration(SensorConfigurationValue configuration) { + if (!super.values.contains(configuration)) { + throw UnimplementedError(); + } + + double? barometerSamplingRate = double.parse(configuration.key); + OpenEarableSensorConfig barometerConfig = OpenEarableSensorConfig( + sensorId: 1, + samplingRate: barometerSamplingRate, + latency: 0, + ); + + _sensorManager.writeSensorConfig(barometerConfig); + } +} + +class _MicrophoneSensorConfiguration extends SensorConfiguration { + final OpenEarableSensorManager _sensorManager; + + _MicrophoneSensorConfiguration({ + required OpenEarableSensorManager sensorManager, + }) : _sensorManager = sensorManager, + super( + name: 'Microphone', + unit: 'Hz', + values: const [ + SensorConfigurationValue(key: "0"), + SensorConfigurationValue(key: "16000"), + SensorConfigurationValue(key: "20000"), + SensorConfigurationValue(key: "25000"), + SensorConfigurationValue(key: "31250"), + SensorConfigurationValue(key: "33333"), + SensorConfigurationValue(key: "40000"), + SensorConfigurationValue(key: "41667"), + SensorConfigurationValue(key: "50000"), + SensorConfigurationValue(key: "62500"), + ], + ); + + @override + void setConfiguration(SensorConfigurationValue configuration) { + if (!super.values.contains(configuration)) { + throw UnimplementedError(); + } + + double? microphoneSamplingRate = double.parse(configuration.key); + OpenEarableSensorConfig microphoneConfig = OpenEarableSensorConfig( + sensorId: 2, + samplingRate: microphoneSamplingRate, + latency: 0, + ); + + _sensorManager.writeSensorConfig(microphoneConfig); + } +} diff --git a/lib/src/models/discovered_device.dart b/lib/src/models/discovered_device.dart new file mode 100644 index 0000000..187327a --- /dev/null +++ b/lib/src/models/discovered_device.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; + +class DiscoveredDevice { + /// The unique identifier of the device. + final String id; + final String name; + + /// Advertised services + final List serviceUuids; + + /// Manufacturer specific data. The first 2 bytes are the Company Identifier Codes. + final Uint8List manufacturerData; + + final int rssi; + + const DiscoveredDevice({ + required this.id, + required this.name, + required this.manufacturerData, + required this.rssi, + required this.serviceUuids, + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index ad256fc..51c1423 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: permission_handler: ^11.3.1 universal_ble: ^0.12.0 web: 0.5.1 # Needed for web compatibility with permission_handler + logger: ^2.5.0 dev_dependencies: flutter_test: From aac918931152784c5895a28edddf2ee15eac1799 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:39:39 +0100 Subject: [PATCH 02/12] implemented sensor parsing for OpenEarable v2 --- example/lib/main.dart | 4 +- lib/open_earable_flutter.dart | 7 +- lib/src/managers/ble_manager.dart | 15 ++ lib/src/models/devices/open_earable_v2.dart | 145 ++++++++++---------- 4 files changed, 99 insertions(+), 72 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9b01543..778fb7c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -39,8 +39,8 @@ class MyAppState extends State { sensorViews = SensorView.createSensorViews(_connectedDevice!); sensorConfigurationViews = SensorConfigurationView.createSensorConfigurationViews( - _connectedDevice!, - ); + _connectedDevice!, + ); } return MaterialApp( diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index 3d79979..0d3b8ce 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -75,8 +75,13 @@ class WearableManager { serviceId: _deviceInfoServiceUuid, characteristicId: _deviceFirmwareVersionCharacteristicUuid, ); + _logger.d("Raw Firmware Version: $softwareGenerationBytes"); + int firstZeroIndex = softwareGenerationBytes.indexOf(0); + if (firstZeroIndex != -1) { + softwareGenerationBytes = softwareGenerationBytes.sublist(0, firstZeroIndex); + } String softwareVersion = String.fromCharCodes(softwareGenerationBytes); - _logger.i("Softare version: $softwareVersion"); + _logger.i("Softare version: '$softwareVersion'"); final versionRegex = RegExp(r'^\d+\.\d+\.\d+$'); if (!versionRegex.hasMatch(softwareVersion)) { diff --git a/lib/src/managers/ble_manager.dart b/lib/src/managers/ble_manager.dart index 890ca08..bacf1e7 100644 --- a/lib/src/managers/ble_manager.dart +++ b/lib/src/managers/ble_manager.dart @@ -128,6 +128,21 @@ class BleManager { ); }; + UniversalBle.getSystemDevices().then((devices) { + for (var bleDevice in devices) { + _scanStreamController?.add( + DiscoveredDevice( + id: bleDevice.deviceId, + name: bleDevice.name ?? "", + manufacturerData: + bleDevice.manufacturerData ?? Uint8List.fromList([]), + rssi: bleDevice.rssi ?? -1, + serviceUuids: bleDevice.services, + ), + ); + } + }); + await UniversalBle.startScan( scanFilter: ScanFilter( // Needs to be passed for web, can be empty for the rest diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index b4ff17b..e49b7fd 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -60,18 +60,19 @@ Map _parseSensorScheme(List data) { Map comp = _parseComponentScheme(data); for (var group in comp.keys) { if (!componentsMap.containsKey(group)) { - componentsMap[group] = {}; + componentsMap[group] = {}; } - (componentsMap[group] as Map).addAll(comp[group] as Map); + Map groupMap = comp[group] as Map; + (componentsMap[group] as Map).addAll(groupMap); } } Map parsedSensorScheme = { sensorName : { 'SensorID' : sensorID, + 'Components' : componentsMap, }, }; - parsedSensorScheme.addAll(componentsMap); return parsedSensorScheme; } @@ -127,7 +128,11 @@ class OpenEarableV2 extends Wearable } void _initSensors() async { - List sensorParseSchemeData = await _bleManager.read(deviceId: _discoveredDevice.id, serviceId: _deviceParseInfoServiceUuid, characteristicId: _deviceParseInfoCharacteristicUuid); + List sensorParseSchemeData = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _deviceParseInfoServiceUuid, + characteristicId: _deviceParseInfoCharacteristicUuid, + ); _logger.d("Read raw parse info: $sensorParseSchemeData"); Map parseInfo = _parseSchemeCharacteristic(sensorParseSchemeData); _logger.i("Found the following info about parsing: $parseInfo"); @@ -137,72 +142,44 @@ class OpenEarableV2 extends Wearable deviceId: _discoveredDevice.id, ); - _sensors.add( - _OpenEarableSensor( - sensorManager: sensorManager, - sensorName: 'ACC', - chartTitle: 'Accelerometer', - shortChartTitle: 'Acc.', - axisNames: ['X', 'Y', 'Z'], - axisUnits: ["m/s\u00B2", "m/s\u00B2", "m/s\u00B2"], - ), - ); - _sensors.add( - _OpenEarableSensor( - sensorManager: sensorManager, - sensorName: 'GYRO', - chartTitle: 'Gyroscope', - shortChartTitle: 'Gyro.', - axisNames: ['X', 'Y', 'Z'], - axisUnits: ["°/s", "°/s", "°/s"], - ), - ); - _sensors.add( - _OpenEarableSensor( - sensorManager: sensorManager, - sensorName: 'MAG', - chartTitle: 'Magnetometer', - shortChartTitle: 'Magn.', - axisNames: ['X', 'Y', 'Z'], - axisUnits: ["µT", "µT", "µT"], - ), - ); - _sensors.add( - _OpenEarableSensor( - sensorManager: sensorManager, - sensorName: 'BARO', - chartTitle: 'Pressure', - shortChartTitle: 'Press.', - axisNames: ['Pressure'], - axisUnits: ["Pa"], - ), - ); - _sensors.add( - _OpenEarableSensor( - sensorManager: sensorManager, - sensorName: 'TEMP', - chartTitle: 'Temperature (Ambient)', - shortChartTitle: 'Temp. (A.)', - axisNames: ['Temperature'], - axisUnits: ["°C"], - ), - ); + for (String sensorName in parseInfo.keys) { + Map sensorDetail = parseInfo[sensorName] as Map; + _logger.t("sensor detail: $sensorDetail"); - _sensorConfigurations.add( - _ImuSensorConfiguration( - sensorManager: sensorManager, - ), - ); - _sensorConfigurations.add( - _BarometerSensorConfiguration( - sensorManager: sensorManager, - ), - ); - _sensorConfigurations.add( - _MicrophoneSensorConfiguration( - sensorManager: sensorManager, - ), - ); + Map componentsMap = sensorDetail['Components'] as Map; + _logger.t("components: $componentsMap"); + + for (String groupName in componentsMap.keys) { + _sensorConfigurations.add( + _OpenEarableSensorConfiguration( + sensorId: sensorDetail['SensorID'] as int, + name: sensorName, + sensorManager: sensorManager, + ), + ); + + Map groupDetail = componentsMap[groupName] as Map; + _logger.t("group detail: $groupDetail"); + List<(String, String)> axisDetails = groupDetail.entries.map((axis) { + Map v = axis.value as Map; + return (axis.key, v['unit'] as String); + }).toList(); + + _sensors.add( + _OpenEarableSensor( + sensorName: sensorName, + chartTitle: groupName, + shortChartTitle: groupName, + axisNames: axisDetails.map((e) => e.$1).toList(), + axisUnits: axisDetails.map((e) => e.$2).toList(), + sensorManager: sensorManager, + ), + ); + } + } + + _logger.d("Created sensors: $_sensors"); + _logger.d("Created sensor configurations: $_sensorConfigurations"); } @override @@ -494,3 +471,33 @@ class _MicrophoneSensorConfiguration extends SensorConfiguration { _sensorManager.writeSensorConfig(microphoneConfig); } } + +class _OpenEarableSensorConfiguration extends SensorConfiguration { + final OpenEarableSensorManager _sensorManager; + final int _sensorId; + + _OpenEarableSensorConfiguration({required int sensorId, required String name, required OpenEarableSensorManager sensorManager}): + _sensorManager = sensorManager, + _sensorId = sensorId, + super( + name: name, + unit: "Hz", + values: [], //TODO: fill with values + ); + + @override + void setConfiguration(SensorConfigurationValue configuration) { + if (!super.values.contains(configuration)) { + throw UnimplementedError(); + } + + double? microphoneSamplingRate = double.parse(configuration.key); + OpenEarableSensorConfig microphoneConfig = OpenEarableSensorConfig( + sensorId: _sensorId, + samplingRate: microphoneSamplingRate, + latency: 0, + ); + + _sensorManager.writeSensorConfig(microphoneConfig); + } +} From 49ebd78f7ba92eed8e0dea84aa9d1f85f2819491 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:36:34 +0100 Subject: [PATCH 03/12] added WearableFactory --- lib/open_earable_flutter.dart | 61 ++++----------- .../models/devices/open_earable_factory.dart | 76 +++++++++++++++++++ lib/src/models/wearable_factory.dart | 12 +++ 3 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 lib/src/models/devices/open_earable_factory.dart create mode 100644 lib/src/models/wearable_factory.dart diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index 0d3b8ce..fc647d2 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -3,7 +3,9 @@ library open_earable_flutter; import 'dart:async'; import 'package:logger/logger.dart'; +import 'package:open_earable_flutter/src/models/devices/open_earable_factory.dart'; import 'package:open_earable_flutter/src/models/devices/open_earable_v2.dart'; +import 'package:open_earable_flutter/src/models/wearable_factory.dart'; import 'package:universal_ble/universal_ble.dart'; import 'src/managers/ble_manager.dart'; @@ -31,15 +33,13 @@ part 'src/constants.dart'; Logger _logger = Logger(); -const String _deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; -const String _deviceFirmwareVersionCharacteristicUuid = - "45622512-6468-465a-b141-0b9b0f96b468"; - class WearableManager { static final WearableManager _instance = WearableManager._internal(); late final BleManager _bleManager; + final List _wearableFactories = [OpenEarableFactory()]; + factory WearableManager() { return _instance; } @@ -67,51 +67,18 @@ class WearableManager { disconnectNotifier.notifyListeners, ); if (connectionResult.$1) { - _logger.d("found following BLEServices: ${connectionResult.$2}"); - - if (connectionResult.$2.any((service) => service.uuid == _deviceInfoServiceUuid)) { - List softwareGenerationBytes = await _bleManager.read( - deviceId: device.id, - serviceId: _deviceInfoServiceUuid, - characteristicId: _deviceFirmwareVersionCharacteristicUuid, - ); - _logger.d("Raw Firmware Version: $softwareGenerationBytes"); - int firstZeroIndex = softwareGenerationBytes.indexOf(0); - if (firstZeroIndex != -1) { - softwareGenerationBytes = softwareGenerationBytes.sublist(0, firstZeroIndex); - } - String softwareVersion = String.fromCharCodes(softwareGenerationBytes); - _logger.i("Softare version: '$softwareVersion'"); - - final versionRegex = RegExp(r'^\d+\.\d+\.\d+$'); - if (!versionRegex.hasMatch(softwareVersion)) { - throw Exception('Invalid software version format'); + for (WearableFactory wearableFactory in _wearableFactories) { + wearableFactory.bleManager = _bleManager; + wearableFactory.disconnectNotifier = disconnectNotifier; + _logger.t("checking factory: $wearableFactory"); + if (await wearableFactory.matches(device, connectionResult.$2)) { + Wearable wearable = await wearableFactory.createFromDevice(device); + return wearable; + } else { + _logger.d("'$wearableFactory' does not support '$device'"); } - - final version1Regex = RegExp(r'^1\.\d+\.\d+$'); - if (version1Regex.hasMatch(softwareVersion)) { - return OpenEarableV1( - name: device.name, - disconnectNotifier: disconnectNotifier, - bleManager: _bleManager, - discoveredDevice: device, - ); - } - - final v2Regex = RegExp(r'^\d+\.\d+.\d+$'); - if (v2Regex.hasMatch(softwareVersion)) { - return OpenEarableV2( - name: device.name, - disconnectNotifier: disconnectNotifier, - bleManager: _bleManager, - discoveredDevice: device, - ); - } - - throw Exception('Unsupported Firmware Version'); - } else { - throw Exception('Unsupported Device'); } + throw Exception('Device is currently not supported'); } else { throw Exception('Failed to connect to device'); } diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart new file mode 100644 index 0000000..3168574 --- /dev/null +++ b/lib/src/models/devices/open_earable_factory.dart @@ -0,0 +1,76 @@ +import 'package:logger/logger.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:open_earable_flutter/src/managers/ble_manager.dart'; +import 'package:open_earable_flutter/src/models/devices/open_earable_v1.dart'; +import 'package:open_earable_flutter/src/models/devices/open_earable_v2.dart'; +import 'package:open_earable_flutter/src/models/wearable_factory.dart'; +import 'package:universal_ble/universal_ble.dart'; + +const String _deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; +const String _deviceFirmwareVersionCharacteristicUuid = + "45622512-6468-465a-b141-0b9b0f96b468"; + +Logger _logger = Logger(); + +class OpenEarableFactory extends WearableFactory { + final _v1Regex = RegExp(r'^1\.\d+\.\d+$'); + final _v2Regex = RegExp(r'^2\.\d+\.\d+$'); + + @override + Future matches(DiscoveredDevice device, List services) async { + if (!services.any((service) => service.uuid == _deviceInfoServiceUuid)) { + _logger.d("'$device' has no service matching '$_deviceInfoServiceUuid'"); + return false; + } + String firmwareVersion = await _getFirmwareVersion(device); + _logger.d("Firmware Version: '$firmwareVersion'"); + + _logger.t("matches V2: ${_v2Regex.hasMatch(firmwareVersion)}"); + + return _v1Regex.hasMatch(firmwareVersion) || _v2Regex.hasMatch(firmwareVersion); + } + + @override + Future createFromDevice(DiscoveredDevice device) async { + if (bleManager == null) { + throw Exception("bleManager needs to be set before using the factory"); + } + if (disconnectNotifier == null) { + throw Exception("disconnectNotifier needs to be set before using the factory"); + } + String firmwareVersion = await _getFirmwareVersion(device); + + + if (_v1Regex.hasMatch(firmwareVersion)) { + return OpenEarableV1( + name: device.name, + disconnectNotifier: disconnectNotifier!, + bleManager: bleManager!, + discoveredDevice: device, + ); + } else if (_v2Regex.hasMatch(firmwareVersion)) { + return OpenEarableV2( + name: device.name, + disconnectNotifier: disconnectNotifier!, + bleManager: bleManager!, + discoveredDevice: device, + ); + } else { + throw Exception('OpenEarable version is not supported'); + } + } + + Future _getFirmwareVersion(DiscoveredDevice device) async { + List softwareGenerationBytes = await bleManager!.read( + deviceId: device.id, + serviceId: _deviceInfoServiceUuid, + characteristicId: _deviceFirmwareVersionCharacteristicUuid, + ); + _logger.d("Raw Firmware Version: $softwareGenerationBytes"); + int firstZeroIndex = softwareGenerationBytes.indexOf(0); + if (firstZeroIndex != -1) { + softwareGenerationBytes = softwareGenerationBytes.sublist(0, firstZeroIndex); + } + return String.fromCharCodes(softwareGenerationBytes); + } +} diff --git a/lib/src/models/wearable_factory.dart b/lib/src/models/wearable_factory.dart new file mode 100644 index 0000000..d412bb3 --- /dev/null +++ b/lib/src/models/wearable_factory.dart @@ -0,0 +1,12 @@ +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:open_earable_flutter/src/managers/ble_manager.dart'; +import 'package:open_earable_flutter/src/managers/notifier.dart'; +import 'package:universal_ble/universal_ble.dart'; + +abstract class WearableFactory { + BleManager? bleManager; + Notifier? disconnectNotifier; + + Future matches(DiscoveredDevice device, List services); + Future createFromDevice(DiscoveredDevice device); +} \ No newline at end of file From 8295c714f29bcb9d1b5cb779d5afa2b2e8013096 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:40:49 +0100 Subject: [PATCH 04/12] init Sensors in Factory --- lib/open_earable_flutter.dart | 2 - .../models/devices/open_earable_factory.dart | 291 +++++++++++++++++- lib/src/models/devices/open_earable_v2.dart | 269 +--------------- 3 files changed, 295 insertions(+), 267 deletions(-) diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index fc647d2..f34ec01 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'package:logger/logger.dart'; import 'package:open_earable_flutter/src/models/devices/open_earable_factory.dart'; -import 'package:open_earable_flutter/src/models/devices/open_earable_v2.dart'; import 'package:open_earable_flutter/src/models/wearable_factory.dart'; import 'package:universal_ble/universal_ble.dart'; @@ -13,7 +12,6 @@ import 'src/managers/notifier.dart'; import 'src/models/devices/discovered_device.dart'; import 'src/models/devices/wearable.dart'; -import 'src/models/devices/open_earable_v1.dart'; export 'src/models/devices/discovered_device.dart'; export 'src/models/devices/wearable.dart'; export 'src/models/capabilities/device_firmware_version.dart'; diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index 3168574..1f61715 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -1,15 +1,23 @@ +import 'dart:async'; + import 'package:logger/logger.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart'; -import 'package:open_earable_flutter/src/managers/ble_manager.dart'; +import 'package:open_earable_flutter/src/managers/open_earable_sensor_manager.dart'; import 'package:open_earable_flutter/src/models/devices/open_earable_v1.dart'; import 'package:open_earable_flutter/src/models/devices/open_earable_v2.dart'; import 'package:open_earable_flutter/src/models/wearable_factory.dart'; +import 'package:open_earable_flutter/src/utils/simple_kalman.dart'; import 'package:universal_ble/universal_ble.dart'; const String _deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; const String _deviceFirmwareVersionCharacteristicUuid = "45622512-6468-465a-b141-0b9b0f96b468"; +const String _deviceParseInfoServiceUuid = + "caa25cb7-7e1b-44f2-adc9-e8c06c9ced43"; +const String _deviceParseInfoCharacteristicUuid = + "caa25cb8-7e1b-44f2-adc9-e8c06c9ced43"; + Logger _logger = Logger(); class OpenEarableFactory extends WearableFactory { @@ -49,9 +57,12 @@ class OpenEarableFactory extends WearableFactory { discoveredDevice: device, ); } else if (_v2Regex.hasMatch(firmwareVersion)) { + (List, List) sensorInfo = await _initSensors(device); return OpenEarableV2( name: device.name, disconnectNotifier: disconnectNotifier!, + sensors: sensorInfo.$1, + sensorConfigurations: sensorInfo.$2, bleManager: bleManager!, discoveredDevice: device, ); @@ -73,4 +84,282 @@ class OpenEarableFactory extends WearableFactory { } return String.fromCharCodes(softwareGenerationBytes); } + + Future<(List, List)> _initSensors(DiscoveredDevice device) async { + List sensors = []; + List sensorConfigurations = []; + + List sensorParseSchemeData = await bleManager!.read( + deviceId: device.id, + serviceId: _deviceParseInfoServiceUuid, + characteristicId: _deviceParseInfoCharacteristicUuid, + ); + _logger.d("Read raw parse info: $sensorParseSchemeData"); + Map parseInfo = _parseSchemeCharacteristic(sensorParseSchemeData); + _logger.i("Found the following info about parsing: $parseInfo"); + + OpenEarableSensorManager sensorManager = OpenEarableSensorManager( + bleManager: bleManager!, + deviceId: device.id, + ); + + for (String sensorName in parseInfo.keys) { + Map sensorDetail = parseInfo[sensorName] as Map; + _logger.t("sensor detail: $sensorDetail"); + + Map componentsMap = sensorDetail['Components'] as Map; + _logger.t("components: $componentsMap"); + + sensorConfigurations.add( + _OpenEarableSensorConfiguration( + sensorId: sensorDetail['SensorID'] as int, + name: sensorName, + sensorManager: sensorManager, + ), + ); + + for (String groupName in componentsMap.keys) { + + Map groupDetail = componentsMap[groupName] as Map; + _logger.t("group detail: $groupDetail"); + List<(String, String)> axisDetails = groupDetail.entries.map((axis) { + Map v = axis.value as Map; + return (axis.key, v['unit'] as String); + }).toList(); + + sensors.add( + _OpenEarableSensor( + sensorId: sensorDetail['SensorID'] as int, + sensorName: groupName, + chartTitle: groupName, + shortChartTitle: groupName, + axisNames: axisDetails.map((e) => e.$1).toList(), + axisUnits: axisDetails.map((e) => e.$2).toList(), + sensorManager: sensorManager, + ), + ); + } + } + + _logger.d("Created sensors: $sensors"); + _logger.d("Created sensor configurations: $sensorConfigurations"); + + return (sensors, sensorConfigurations); + } +} + + +Map _parseSchemeCharacteristic(List data) { + Map parsedData = {}; + + int sensorCount = data.removeAt(0); + for (int i = 0; i < sensorCount; i++) { + Map sensorMap = _parseSensorScheme(data); + parsedData.addAll(sensorMap); + } + + return parsedData; +} + +Map _parseSensorScheme(List data) { + int sensorID = data.removeAt(0); + String sensorName = _parseString(data); + int componentsCount = data.removeAt(0); + + Map componentsMap = {}; + for (int i = 0; i < componentsCount; i++) { + Map comp = _parseComponentScheme(data); + for (var group in comp.keys) { + if (!componentsMap.containsKey(group)) { + componentsMap[group] = {}; + } + Map groupMap = comp[group] as Map; + (componentsMap[group] as Map).addAll(groupMap); + } + } + + Map parsedSensorScheme = { + sensorName : { + 'SensorID' : sensorID, + 'Components' : componentsMap, + }, + }; + + return parsedSensorScheme; +} + +Map _parseComponentScheme(List data) { + int type = data.removeAt(0); + String groupName = _parseString(data); + String componentName = _parseString(data); + String unitName = _parseString(data); + + Map parsedComponentScheme = { + groupName : { + componentName : { + 'type' : type, + 'unit' : unitName, + }, + }, + }; + + return parsedComponentScheme; +} + +String _parseString(List data) { + int stringLength = data.removeAt(0); + List stringBytes = data.sublist(0, stringLength); + data.removeRange(0, stringLength); + return String.fromCharCodes(stringBytes); +} + +class _OpenEarableSensor extends Sensor { + final int _sensorId; + final List _axisNames; + final List _axisUnits; + final OpenEarableSensorManager _sensorManager; + + StreamSubscription? _dataSubscription; + + _OpenEarableSensor({ + required int sensorId, + required String sensorName, + required String chartTitle, + required String shortChartTitle, + required List axisNames, + required List axisUnits, + required OpenEarableSensorManager sensorManager, + }) : _sensorId = sensorId, + _axisNames = axisNames, + _axisUnits = axisUnits, + _sensorManager = sensorManager, + super( + sensorName: sensorName, + chartTitle: chartTitle, + shortChartTitle: shortChartTitle, + ); + + @override + List get axisNames => _axisNames; + + @override + List get axisUnits => _axisUnits; + + Stream _getAccGyroMagStream() { + StreamController streamController = StreamController(); + + final errorMeasure = {"ACC": 5.0, "GYRO": 10.0, "MAG": 25.0}; + + SimpleKalman kalmanX = SimpleKalman( + errorMeasure: errorMeasure[sensorName]!, + errorEstimate: errorMeasure[sensorName]!, + q: 0.9, + ); + SimpleKalman kalmanY = SimpleKalman( + errorMeasure: errorMeasure[sensorName]!, + errorEstimate: errorMeasure[sensorName]!, + q: 0.9, + ); + SimpleKalman kalmanZ = SimpleKalman( + errorMeasure: errorMeasure[sensorName]!, + errorEstimate: errorMeasure[sensorName]!, + q: 0.9, + ); + _dataSubscription?.cancel(); + _dataSubscription = _sensorManager.subscribeToSensorData(0).listen((data) { + int timestamp = data["timestamp"]; + + SensorValue sensorValue = SensorValue( + values: [ + kalmanX.filtered(data[sensorName]["X"]), + kalmanY.filtered(data[sensorName]["Y"]), + kalmanZ.filtered(data[sensorName]["Z"]), + ], + timestamp: timestamp, + ); + + streamController.add(sensorValue); + }); + + return streamController.stream; + } + + Stream _createSingleDataSubscription(String componentName) { + StreamController streamController = StreamController(); + + _dataSubscription?.cancel(); + _dataSubscription = _sensorManager.subscribeToSensorData(_sensorId).listen((data) { + int timestamp = data["timestamp"]; + _logger.t("SensorData: $data"); + + _logger.t("componentData of $componentName: ${data[componentName]}"); + + List values = []; + for (var entry in (data[componentName] as Map).entries) { + if (entry.key == 'units') { + continue; + } + + values.add(entry.value); + } + + SensorValue sensorValue = SensorValue( + values: values, + timestamp: timestamp, + ); + + streamController.add(sensorValue); + }); + + return streamController.stream; + } + + @override + Stream get sensorStream { + switch (sensorName) { + // case "ACC": + // case "GYRO": + // case "MAG": + // return _getAccGyroMagStream(); + // case "BARO": + // return _createSingleDataSubscription("Pressure"); + // case "TEMP": + // return _createSingleDataSubscription("Temperature"); + default: + return _createSingleDataSubscription(sensorName); + } + } +} + +class _OpenEarableSensorConfiguration extends SensorConfiguration { + final OpenEarableSensorManager _sensorManager; + final int _sensorId; + + _OpenEarableSensorConfiguration({required int sensorId, required String name, required OpenEarableSensorManager sensorManager}): + _sensorManager = sensorManager, + _sensorId = sensorId, + super( + name: name, + unit: "Hz", + values: [ + const SensorConfigurationValue(key: "0"), + const SensorConfigurationValue(key: "10"), + ], //TODO: fill with values + ); + + @override + void setConfiguration(SensorConfigurationValue configuration) { + if (!super.values.contains(configuration)) { + throw UnimplementedError(); + } + + double? microphoneSamplingRate = double.parse(configuration.key); + OpenEarableSensorConfig microphoneConfig = OpenEarableSensorConfig( + sensorId: _sensorId, + samplingRate: microphoneSamplingRate, + latency: 0, + ); + + _sensorManager.writeSensorConfig(microphoneConfig); + } } diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index e49b7fd..9a8b526 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi'; import 'dart:typed_data'; import 'package:logger/logger.dart'; @@ -31,77 +29,8 @@ const String _deviceFirmwareVersionCharacteristicUuid = const String _deviceHardwareVersionCharacteristicUuid = "45622513-6468-465a-b141-0b9b0f96b468"; -const String _deviceParseInfoServiceUuid = - "caa25cb7-7e1b-44f2-adc9-e8c06c9ced43"; -const String _deviceParseInfoCharacteristicUuid = - "caa25cb8-7e1b-44f2-adc9-e8c06c9ced43"; - Logger _logger = Logger(); -Map _parseSchemeCharacteristic(List data) { - Map parsedData = {}; - - int sensorCount = data.removeAt(0); - for (int i = 0; i < sensorCount; i++) { - Map sensorMap = _parseSensorScheme(data); - parsedData.addAll(sensorMap); - } - - return parsedData; -} - -Map _parseSensorScheme(List data) { - int sensorID = data.removeAt(0); - String sensorName = _parseString(data); - int componentsCount = data.removeAt(0); - - Map componentsMap = {}; - for (int i = 0; i < componentsCount; i++) { - Map comp = _parseComponentScheme(data); - for (var group in comp.keys) { - if (!componentsMap.containsKey(group)) { - componentsMap[group] = {}; - } - Map groupMap = comp[group] as Map; - (componentsMap[group] as Map).addAll(groupMap); - } - } - - Map parsedSensorScheme = { - sensorName : { - 'SensorID' : sensorID, - 'Components' : componentsMap, - }, - }; - - return parsedSensorScheme; -} - -Map _parseComponentScheme(List data) { - int type = data.removeAt(0); - String groupName = _parseString(data); - String componentName = _parseString(data); - String unitName = _parseString(data); - - Map parsedComponentScheme = { - groupName : { - componentName : { - 'type' : type, - 'unit' : unitName, - }, - }, - }; - - return parsedComponentScheme; -} - -String _parseString(List data) { - int stringLength = data.removeAt(0); - List stringBytes = data.sublist(0, stringLength); - data.removeRange(0, stringLength); - return String.fromCharCodes(stringBytes); -} - class OpenEarableV2 extends Wearable implements SensorManager, @@ -118,69 +47,14 @@ class OpenEarableV2 extends Wearable OpenEarableV2({ required super.name, required super.disconnectNotifier, + required List sensors, + required List sensorConfigurations, required BleManager bleManager, required DiscoveredDevice discoveredDevice, - }) : _sensors = [], - _sensorConfigurations = [], + }) : _sensors = sensors, + _sensorConfigurations = sensorConfigurations, _bleManager = bleManager, - _discoveredDevice = discoveredDevice { - _initSensors(); - } - - void _initSensors() async { - List sensorParseSchemeData = await _bleManager.read( - deviceId: _discoveredDevice.id, - serviceId: _deviceParseInfoServiceUuid, - characteristicId: _deviceParseInfoCharacteristicUuid, - ); - _logger.d("Read raw parse info: $sensorParseSchemeData"); - Map parseInfo = _parseSchemeCharacteristic(sensorParseSchemeData); - _logger.i("Found the following info about parsing: $parseInfo"); - - OpenEarableSensorManager sensorManager = OpenEarableSensorManager( - bleManager: _bleManager, - deviceId: _discoveredDevice.id, - ); - - for (String sensorName in parseInfo.keys) { - Map sensorDetail = parseInfo[sensorName] as Map; - _logger.t("sensor detail: $sensorDetail"); - - Map componentsMap = sensorDetail['Components'] as Map; - _logger.t("components: $componentsMap"); - - for (String groupName in componentsMap.keys) { - _sensorConfigurations.add( - _OpenEarableSensorConfiguration( - sensorId: sensorDetail['SensorID'] as int, - name: sensorName, - sensorManager: sensorManager, - ), - ); - - Map groupDetail = componentsMap[groupName] as Map; - _logger.t("group detail: $groupDetail"); - List<(String, String)> axisDetails = groupDetail.entries.map((axis) { - Map v = axis.value as Map; - return (axis.key, v['unit'] as String); - }).toList(); - - _sensors.add( - _OpenEarableSensor( - sensorName: sensorName, - chartTitle: groupName, - shortChartTitle: groupName, - axisNames: axisDetails.map((e) => e.$1).toList(), - axisUnits: axisDetails.map((e) => e.$2).toList(), - sensorManager: sensorManager, - ), - ); - } - } - - _logger.d("Created sensors: $_sensors"); - _logger.d("Created sensor configurations: $_sensorConfigurations"); - } + _discoveredDevice = discoveredDevice; @override String get deviceId => _discoveredDevice.id; @@ -261,109 +135,6 @@ class OpenEarableV2 extends Wearable List get sensors => List.unmodifiable(_sensors); } -class _OpenEarableSensor extends Sensor { - final List _axisNames; - final List _axisUnits; - final OpenEarableSensorManager _sensorManager; - - StreamSubscription? _dataSubscription; - - _OpenEarableSensor({ - required String sensorName, - required String chartTitle, - required String shortChartTitle, - required List axisNames, - required List axisUnits, - required OpenEarableSensorManager sensorManager, - }) : _axisNames = axisNames, - _axisUnits = axisUnits, - _sensorManager = sensorManager, - super( - sensorName: sensorName, - chartTitle: chartTitle, - shortChartTitle: shortChartTitle, - ); - - @override - List get axisNames => _axisNames; - - @override - List get axisUnits => _axisUnits; - - Stream _getAccGyroMagStream() { - StreamController streamController = StreamController(); - - final errorMeasure = {"ACC": 5.0, "GYRO": 10.0, "MAG": 25.0}; - - SimpleKalman kalmanX = SimpleKalman( - errorMeasure: errorMeasure[sensorName]!, - errorEstimate: errorMeasure[sensorName]!, - q: 0.9, - ); - SimpleKalman kalmanY = SimpleKalman( - errorMeasure: errorMeasure[sensorName]!, - errorEstimate: errorMeasure[sensorName]!, - q: 0.9, - ); - SimpleKalman kalmanZ = SimpleKalman( - errorMeasure: errorMeasure[sensorName]!, - errorEstimate: errorMeasure[sensorName]!, - q: 0.9, - ); - _dataSubscription?.cancel(); - _dataSubscription = _sensorManager.subscribeToSensorData(0).listen((data) { - int timestamp = data["timestamp"]; - - SensorValue sensorValue = SensorValue( - values: [ - kalmanX.filtered(data[sensorName]["X"]), - kalmanY.filtered(data[sensorName]["Y"]), - kalmanZ.filtered(data[sensorName]["Z"]), - ], - timestamp: timestamp, - ); - - streamController.add(sensorValue); - }); - - return streamController.stream; - } - - Stream _createSingleDataSubscription(String componentName) { - StreamController streamController = StreamController(); - - _dataSubscription?.cancel(); - _dataSubscription = _sensorManager.subscribeToSensorData(1).listen((data) { - int timestamp = data["timestamp"]; - - SensorValue sensorValue = SensorValue( - values: [data[sensorName][componentName]], - timestamp: timestamp, - ); - - streamController.add(sensorValue); - }); - - return streamController.stream; - } - - @override - Stream get sensorStream { - switch (sensorName) { - case "ACC": - case "GYRO": - case "MAG": - return _getAccGyroMagStream(); - case "BARO": - return _createSingleDataSubscription("Pressure"); - case "TEMP": - return _createSingleDataSubscription("Temperature"); - default: - throw UnimplementedError(); - } - } -} - class _ImuSensorConfiguration extends SensorConfiguration { final OpenEarableSensorManager _sensorManager; @@ -471,33 +242,3 @@ class _MicrophoneSensorConfiguration extends SensorConfiguration { _sensorManager.writeSensorConfig(microphoneConfig); } } - -class _OpenEarableSensorConfiguration extends SensorConfiguration { - final OpenEarableSensorManager _sensorManager; - final int _sensorId; - - _OpenEarableSensorConfiguration({required int sensorId, required String name, required OpenEarableSensorManager sensorManager}): - _sensorManager = sensorManager, - _sensorId = sensorId, - super( - name: name, - unit: "Hz", - values: [], //TODO: fill with values - ); - - @override - void setConfiguration(SensorConfigurationValue configuration) { - if (!super.values.contains(configuration)) { - throw UnimplementedError(); - } - - double? microphoneSamplingRate = double.parse(configuration.key); - OpenEarableSensorConfig microphoneConfig = OpenEarableSensorConfig( - sensorId: _sensorId, - samplingRate: microphoneSamplingRate, - latency: 0, - ); - - _sensorManager.writeSensorConfig(microphoneConfig); - } -} From 08975d97b405b475b9c33301574c2d716e3e49d7 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:46:08 +0100 Subject: [PATCH 05/12] cast every sensor value to a double --- lib/src/models/devices/open_earable_factory.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index 1f61715..6cec224 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -300,7 +300,7 @@ class _OpenEarableSensor extends Sensor { continue; } - values.add(entry.value); + values.add(entry.value.toDouble()); } SensorValue sensorValue = SensorValue( From 4a24c91c686684512c5ff227ef9f55b10bf5a1aa Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:54:35 +0100 Subject: [PATCH 06/12] added statusLED to activate and deactivate color overwriting --- example/lib/main.dart | 5 +++- .../lib/widgets/rgb_led_control_widget.dart | 9 +++++-- lib/open_earable_flutter.dart | 1 + lib/src/models/capabilities/status_led.dart | 3 +++ lib/src/models/devices/open_earable_v2.dart | 24 ++++++++++++++----- 5 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 lib/src/models/capabilities/status_led.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 778fb7c..3fb526f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -163,7 +163,10 @@ class MyAppState extends State { GroupedBox( title: "RGB LED", child: - RgbLedControlWidget(rgbLed: _connectedDevice as RgbLed), + RgbLedControlWidget( + rgbLed: _connectedDevice as RgbLed, + statusLed: _connectedDevice as StatusLed?, + ), ), if (_connectedDevice is FrequencyPlayer) GroupedBox( diff --git a/example/lib/widgets/rgb_led_control_widget.dart b/example/lib/widgets/rgb_led_control_widget.dart index 20b614b..0a83996 100644 --- a/example/lib/widgets/rgb_led_control_widget.dart +++ b/example/lib/widgets/rgb_led_control_widget.dart @@ -4,8 +4,9 @@ import 'package:open_earable_flutter/open_earable_flutter.dart'; class RgbLedControlWidget extends StatefulWidget { final RgbLed rgbLed; + final StatusLed? statusLed; - const RgbLedControlWidget({Key? key, required this.rgbLed}) : super(key: key); + const RgbLedControlWidget({Key? key, required this.rgbLed, this.statusLed}) : super(key: key); @override State createState() => _RgbLedControlWidgetState(); @@ -69,13 +70,17 @@ class _RgbLedControlWidgetState extends State { g: _currentColor.green, b: _currentColor.blue, ); + widget.statusLed?.showStatus(false); }, child: const Text('Set'), ), const SizedBox(width: 20), ElevatedButton( onPressed: () { - widget.rgbLed.writeLedColor(r: 0, g: 0, b: 0); + widget.statusLed?.showStatus(true); + if (widget.statusLed == null) { + widget.rgbLed.writeLedColor(r: 0, g: 0, b: 0); + } }, child: const Text('Off'), ), diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index f34ec01..e9662a8 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -18,6 +18,7 @@ export 'src/models/capabilities/device_firmware_version.dart'; export 'src/models/capabilities/device_hardware_version.dart'; export 'src/models/capabilities/device_identifier.dart'; export 'src/models/capabilities/rgb_led.dart'; +export 'src/models/capabilities/status_led.dart'; export 'src/models/capabilities/sensor.dart'; export 'src/models/capabilities/sensor_configuration.dart'; export 'src/models/capabilities/sensor_manager.dart'; diff --git a/lib/src/models/capabilities/status_led.dart b/lib/src/models/capabilities/status_led.dart new file mode 100644 index 0000000..601645c --- /dev/null +++ b/lib/src/models/capabilities/status_led.dart @@ -0,0 +1,3 @@ +abstract class StatusLed { + Future showStatus(bool status); +} diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index 9a8b526..c401f03 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:logger/logger.dart'; +import 'package:open_earable_flutter/src/models/capabilities/status_led.dart'; import '../../managers/open_earable_sensor_manager.dart'; -import '../../utils/simple_kalman.dart'; import '../capabilities/device_firmware_version.dart'; import '../capabilities/device_hardware_version.dart'; import '../capabilities/device_identifier.dart'; @@ -18,8 +18,10 @@ import 'discovered_device.dart'; import 'wearable.dart'; const String _ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; -const String _ledSetStateCharacteristic = +const String _ledSetColorCharacteristic = "81040e7a-4819-11ee-be56-0242ac120002"; +const String _ledSetStateCharacteristic = + "81040e7b-4819-11ee-be56-0242ac120002"; const String _deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; const String _deviceIdentifierCharacteristicUuid = @@ -36,6 +38,7 @@ class OpenEarableV2 extends Wearable SensorManager, SensorConfigurationManager, RgbLed, + StatusLed, DeviceIdentifier, DeviceFirmwareVersion, DeviceHardwareVersion { @@ -65,9 +68,6 @@ class OpenEarableV2 extends Wearable required int g, required int b, }) async { - // if (!_bleManager.connected) { - // Exception("Can't write sensor config. Earable not connected"); - // } if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) { throw ArgumentError('The color values must be in range 0-255'); } @@ -78,11 +78,23 @@ class OpenEarableV2 extends Wearable await _bleManager.write( deviceId: _discoveredDevice.id, serviceId: _ledServiceUuid, - characteristicId: _ledSetStateCharacteristic, + characteristicId: _ledSetColorCharacteristic, byteData: data.buffer.asUint8List(), ); } + @override + Future showStatus(bool status) async { + ByteData statusData = ByteData(1); + statusData.setUint8(0, status ? 0 : 1); + await _bleManager.write( + deviceId: _discoveredDevice.id, + serviceId: _ledServiceUuid, + characteristicId: _ledSetStateCharacteristic, + byteData: statusData.buffer.asUint8List(), + ); + } + /// Reads the device identifier from the connected OpenEarable device. /// /// Returns a `Future` that completes with the device identifier as a `String`. From 792a9e318829f25684a8f183b88eea0e3ad19719 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:23:43 +0100 Subject: [PATCH 07/12] implemented extended battery service for openearable v2 --- example/lib/main.dart | 3 + example/lib/widgets/battery_info_widget.dart | 116 ++++++++++ lib/open_earable_flutter.dart | 1 + .../models/capabilities/battery_service.dart | 123 +++++++++++ lib/src/models/devices/open_earable_v2.dart | 203 ++++++++++++++++++ 5 files changed, 446 insertions(+) create mode 100644 example/lib/widgets/battery_info_widget.dart create mode 100644 lib/src/models/capabilities/battery_service.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 3fb526f..a056e90 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:example/widgets/battery_info_widget.dart'; import 'package:flutter/material.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart'; @@ -159,6 +160,8 @@ class MyAppState extends State { ], ), ), + if (_connectedDevice is ExtendedBatteryService) + BatteryInfoWidget(connectedDevice: _connectedDevice as ExtendedBatteryService), if (_connectedDevice is RgbLed) GroupedBox( title: "RGB LED", diff --git a/example/lib/widgets/battery_info_widget.dart b/example/lib/widgets/battery_info_widget.dart new file mode 100644 index 0000000..7f02a76 --- /dev/null +++ b/example/lib/widgets/battery_info_widget.dart @@ -0,0 +1,116 @@ +import 'package:example/widgets/grouped_box.dart'; +import 'package:flutter/material.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; + +class BatteryInfoWidget extends StatelessWidget { + final ExtendedBatteryService connectedDevice; + + const BatteryInfoWidget({super.key, required this.connectedDevice}); + + @override + Widget build(BuildContext context) { + return GroupedBox( + title: "Battery Info", + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamBuilder( + stream: connectedDevice.batteryPercentageStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text( + "Battery Percentage:\t${snapshot.data}%", + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + StreamBuilder( + stream: connectedDevice.powerStatusStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Battery connected: ${snapshot.data!.batteryPresent ? "✅" : "❌"}", + ), + Text( + "Wired Power Connected: ${snapshot.data!.wiredExternalPowerSourceConnected}", + ), + Text( + "Wireless Power Connected: ${snapshot.data!.wirelessExternalPowerSourceConnected}", + ), + Text( + "Charge State: ${snapshot.data!.chargeState}", + ), + Text( + "Charge Level: ${snapshot.data!.chargeLevel}", + ), + Text( + "Charging Type: ${snapshot.data!.chargingType}", + ), + Text( + "Charging Fault Reason: ${snapshot.data!.chargingFaultReason}", + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + const Divider(), + StreamBuilder( + stream: connectedDevice.healthStatusStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Health Summary: ${snapshot.data!.healthSummary}%", + ), + Text( + "Cycle Count: ${snapshot.data!.cycleCount}", + ), + Text( + "Current Temperature: ${snapshot.data!.currentTemperature}°C", + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + const Divider(), + StreamBuilder( + stream: connectedDevice.energyStatusStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Voltage: ${snapshot.data!.voltage}V", + ), + Text( + "Available Capacity: ${snapshot.data!.availableCapacity}mAh", + ), + Text( + "Charge Rate: ${snapshot.data!.chargeRate}mAh", + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index e9662a8..ed336b7 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -17,6 +17,7 @@ export 'src/models/devices/wearable.dart'; export 'src/models/capabilities/device_firmware_version.dart'; export 'src/models/capabilities/device_hardware_version.dart'; export 'src/models/capabilities/device_identifier.dart'; +export 'src/models/capabilities/battery_service.dart'; export 'src/models/capabilities/rgb_led.dart'; export 'src/models/capabilities/status_led.dart'; export 'src/models/capabilities/sensor.dart'; diff --git a/lib/src/models/capabilities/battery_service.dart b/lib/src/models/capabilities/battery_service.dart new file mode 100644 index 0000000..21e53d3 --- /dev/null +++ b/lib/src/models/capabilities/battery_service.dart @@ -0,0 +1,123 @@ +abstract class BatteryService { + /// Reads the battery percentage of the device. + /// The value is between 0 and 100. + Future readBatteryPercentage(); + + Stream get batteryPercentageStream; +} + +enum ExternalPowerSourceConnected { + no, + yes, + unknown, +} + +enum ChargeState { + unknown, + charging, + dischargingActive, + dischargingInactive, +} + +enum BatteryChargeLevel { + unknown, + good, + low, + critical, +} + +enum BatteryChargingType { + unknown, + constantCurrent, + constantVoltage, + trickle, + float, +} + +enum ChargingFaultReason { + battery, + externalPowerSource, + other, +} + +abstract class ExtendedBatteryService extends BatteryService { + Future readEnergyStatus(); + + Future readHealthStatus(); + + Future readPowerStatus(); + + Stream get powerStatusStream; + Stream get energyStatusStream; + Stream get healthStatusStream; +} + +class BatteryEnergyStatus { + final double voltage; + final double availableCapacity; + final double chargeRate; + + const BatteryEnergyStatus({ + required this.voltage, + required this.availableCapacity, + required this.chargeRate, + }); + + @override + String toString() { + return 'BatteryEnergyStatus(voltage: $voltage, ' + 'availableCapacity: $availableCapacity, ' + 'chargeRate: $chargeRate)'; + } +} + +class BatteryHealthStatus { + /// The percentage of the battery health. + final int healthSummary; + final int cycleCount; + final int currentTemperature; + + const BatteryHealthStatus({ + required this.healthSummary, + required this.cycleCount, + required this.currentTemperature, + }); + + @override + String toString() { + return 'BatteryHealthStatus(healthSummary: $healthSummary, ' + 'cycleCount: $cycleCount, ' + 'currentTemperature: $currentTemperature)'; + } +} + +class BatteryPowerStatus { + final bool batteryPresent; + final ExternalPowerSourceConnected wiredExternalPowerSourceConnected; + final ExternalPowerSourceConnected wirelessExternalPowerSourceConnected; + final ChargeState chargeState; + final BatteryChargeLevel chargeLevel; + final BatteryChargingType chargingType; + final List chargingFaultReason; + + const BatteryPowerStatus({ + required this.batteryPresent, + required this.wiredExternalPowerSourceConnected, + required this.wirelessExternalPowerSourceConnected, + required this.chargeState, + required this.chargeLevel, + required this.chargingType, + required this.chargingFaultReason, + }); + + @override + String toString() { + return 'BatteryPowerStatus(batteryPresent: $batteryPresent, ' + 'wiredExternalPowerSourceConnected: $wiredExternalPowerSourceConnected, ' + 'wirelessExternalPowerSourceConnected: $wirelessExternalPowerSourceConnected, ' + 'chargeState: $chargeState, ' + 'chargeLevel: $chargeLevel, ' + 'chargingType: $chargingType, ' + 'chargingFaultReason: $chargingFaultReason)'; + } +} diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index c401f03..1611c52 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -1,7 +1,10 @@ import 'dart:async'; +import 'dart:ffi'; +import 'dart:math'; import 'dart:typed_data'; import 'package:logger/logger.dart'; +import 'package:open_earable_flutter/src/models/capabilities/battery_service.dart'; import 'package:open_earable_flutter/src/models/capabilities/status_led.dart'; import '../../managers/open_earable_sensor_manager.dart'; @@ -17,6 +20,12 @@ import '../../managers/ble_manager.dart'; import 'discovered_device.dart'; import 'wearable.dart'; +const String _batteryServiceUuid = "180F"; +const String _batteryLevelCharacteristicUuid = "2A19"; +const String _batteryLevelStatusCharacteristicUuid = "2BED"; +const String _batteryHealthStatusCharacteristicUuid = "2BEA"; +const String _batteryEnergyStatusCharacteristicUuid = "2BF0"; + const String _ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; const String _ledSetColorCharacteristic = "81040e7a-4819-11ee-be56-0242ac120002"; @@ -39,6 +48,7 @@ class OpenEarableV2 extends Wearable SensorConfigurationManager, RgbLed, StatusLed, + ExtendedBatteryService, DeviceIdentifier, DeviceFirmwareVersion, DeviceHardwareVersion { @@ -145,6 +155,199 @@ class OpenEarableV2 extends Wearable @override List get sensors => List.unmodifiable(_sensors); + + @override + Future readBatteryPercentage() async { + List batteryLevelList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _batteryServiceUuid, + characteristicId: _batteryLevelCharacteristicUuid, + ); + + _logger.t("Battery level bytes: $batteryLevelList"); + + if (batteryLevelList.length != 1) { + throw StateError('Battery level characteristic expected 1 value, but got ${batteryLevelList.length}'); + } + + return batteryLevelList[0]; + } + + @override + Future readEnergyStatus() async { + List energyStatusList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _batteryServiceUuid, + characteristicId: _batteryEnergyStatusCharacteristicUuid, + ); + + _logger.t("Battery energy status bytes: $energyStatusList"); + + if (energyStatusList.length != 7) { + throw StateError('Battery energy status characteristic expected 7 values, but got ${energyStatusList.length}'); + } + + int rawVoltage = (energyStatusList[2] << 8) | energyStatusList[1]; + double voltage = _convertSFloat(rawVoltage); + + int rawAvailableCapacity = (energyStatusList[4] << 8) | energyStatusList[3]; + double availableCapacity = _convertSFloat(rawAvailableCapacity); + + int rawChargeRate = (energyStatusList[6] << 8) | energyStatusList[5]; + double chargeRate = _convertSFloat(rawChargeRate); + + BatteryEnergyStatus batteryEnergyStatus = BatteryEnergyStatus( + voltage: voltage, + availableCapacity: availableCapacity, + chargeRate: chargeRate, + ); + + _logger.d('Battery energy status: $batteryEnergyStatus'); + + return batteryEnergyStatus; + } + + double _convertSFloat(int rawBits) { + int exponent = ((rawBits & 0xF000) >> 12) - 16; + int mantissa = rawBits & 0x0FFF; + + if (mantissa >= 0x800) { + mantissa = -((0x1000) - mantissa); + } + _logger.t("Exponent: $exponent, Mantissa: $mantissa"); + double result = mantissa.toDouble() * pow(10.0, exponent.toDouble()); + return result; + } + + @override + Future readHealthStatus() async { + List healthStatusList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _batteryServiceUuid, + characteristicId: _batteryHealthStatusCharacteristicUuid, + ); + + _logger.t("Battery health status bytes: $healthStatusList"); + + if (healthStatusList.length != 5) { + throw StateError('Battery health status characteristic expected 5 values, but got ${healthStatusList.length}'); + } + + int healthSummary = healthStatusList[1]; + int cycleCount = (healthStatusList[2] << 8) | healthStatusList[3]; + int currentTemperature = healthStatusList[4]; + + BatteryHealthStatus batteryHealthStatus = BatteryHealthStatus( + healthSummary: healthSummary, + cycleCount: cycleCount, + currentTemperature: currentTemperature, + ); + + _logger.d('Battery health status: $batteryHealthStatus'); + + return batteryHealthStatus; + } + + @override + Future readPowerStatus() async { + List powerStateList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _batteryServiceUuid, + characteristicId: _batteryLevelStatusCharacteristicUuid, + ); + + int powerState = (powerStateList[1] << 8) | powerStateList[2]; + _logger.d("Battery power status bits: ${powerState.toRadixString(2)}"); + + bool batteryPresent = powerState >> 15 & 0x1 != 0; + + int wiredExternalPowerSourceConnectedRaw = (powerState >> 13) & 0x3; + ExternalPowerSourceConnected wiredExternalPowerSourceConnected + = ExternalPowerSourceConnected.values[wiredExternalPowerSourceConnectedRaw]; + + int wirelessExternalPowerSourceConnectedRaw = (powerState >> 11) & 0x3; + ExternalPowerSourceConnected wirelessExternalPowerSourceConnected + = ExternalPowerSourceConnected.values[wirelessExternalPowerSourceConnectedRaw]; + + int chargeStateRaw = (powerState >> 9) & 0x3; + ChargeState chargeState = ChargeState.values[chargeStateRaw]; + + int chargeLevelRaw = (powerState >> 7) & 0x3; + BatteryChargeLevel chargeLevel = BatteryChargeLevel.values[chargeLevelRaw]; + + int chargingTypeRaw = (powerState >> 5) & 0x7; + BatteryChargingType chargingType = BatteryChargingType.values[chargingTypeRaw]; + + int chargingFaultReasonRaw = (powerState >> 2) & 0x5; + List chargingFaultReason = []; + if ((chargingFaultReasonRaw & 0x1) != 0) { + chargingFaultReason.add(ChargingFaultReason.other); + } + if ((chargingFaultReasonRaw & 0x2) != 0) { + chargingFaultReason.add(ChargingFaultReason.externalPowerSource); + } + if ((chargingFaultReasonRaw & 0x4) != 0) { + chargingFaultReason.add(ChargingFaultReason.battery); + } + + BatteryPowerStatus batteryPowerStatus = BatteryPowerStatus( + batteryPresent: batteryPresent, + wiredExternalPowerSourceConnected: wiredExternalPowerSourceConnected, + wirelessExternalPowerSourceConnected: wirelessExternalPowerSourceConnected, + chargeState: chargeState, + chargeLevel: chargeLevel, + chargingType: chargingType, + chargingFaultReason: chargingFaultReason, + ); + + _logger.d('Battery power status: $batteryPowerStatus'); + + return batteryPowerStatus; + } + + @override + Stream get batteryPercentageStream async* { + while (true) { + yield await readBatteryPercentage(); + await Future.delayed(const Duration(seconds: 5)); + } + } + + @override + Stream get powerStatusStream async* { + while (true) { + try { + yield await readPowerStatus(); + } catch (e) { + _logger.e('Error reading power status: $e'); + } + await Future.delayed(const Duration(seconds: 5)); + } + } + + @override + Stream get energyStatusStream async* { + while (true) { + try { + yield await readEnergyStatus(); + } catch (e) { + _logger.e('Error reading energy status: $e'); + } + await Future.delayed(const Duration(seconds: 5)); + } + } + + @override + Stream get healthStatusStream async* { + while (true) { + try { + yield await readHealthStatus(); + } catch (e) { + _logger.e('Error reading health status: $e'); + } + await Future.delayed(const Duration(seconds: 5)); + } + } } class _ImuSensorConfiguration extends SensorConfiguration { From 4036cf596f64e1f7f4662205b51ecca99a19e9ce Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:23:43 +0100 Subject: [PATCH 08/12] implemented extended battery service for openearable v2 --- example/lib/main.dart | 3 + example/lib/widgets/battery_info_widget.dart | 116 ++++++++++ lib/open_earable_flutter.dart | 1 + .../models/capabilities/battery_service.dart | 123 +++++++++++ lib/src/models/devices/open_earable_v2.dart | 203 ++++++++++++++++++ 5 files changed, 446 insertions(+) create mode 100644 example/lib/widgets/battery_info_widget.dart create mode 100644 lib/src/models/capabilities/battery_service.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 3fb526f..a056e90 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:example/widgets/battery_info_widget.dart'; import 'package:flutter/material.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart'; @@ -159,6 +160,8 @@ class MyAppState extends State { ], ), ), + if (_connectedDevice is ExtendedBatteryService) + BatteryInfoWidget(connectedDevice: _connectedDevice as ExtendedBatteryService), if (_connectedDevice is RgbLed) GroupedBox( title: "RGB LED", diff --git a/example/lib/widgets/battery_info_widget.dart b/example/lib/widgets/battery_info_widget.dart new file mode 100644 index 0000000..b640555 --- /dev/null +++ b/example/lib/widgets/battery_info_widget.dart @@ -0,0 +1,116 @@ +import 'package:example/widgets/grouped_box.dart'; +import 'package:flutter/material.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; + +class BatteryInfoWidget extends StatelessWidget { + final ExtendedBatteryService connectedDevice; + + const BatteryInfoWidget({super.key, required this.connectedDevice}); + + @override + Widget build(BuildContext context) { + return GroupedBox( + title: "Battery Info", + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamBuilder( + stream: connectedDevice.batteryPercentageStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text( + "Battery Percentage:\t${snapshot.data}%", + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + StreamBuilder( + stream: connectedDevice.powerStatusStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Battery connected: ${snapshot.data!.batteryPresent ? "✅" : "❌"}", + ), + Text( + "Wired Power Connected: ${snapshot.data!.wiredExternalPowerSourceConnected}", + ), + Text( + "Wireless Power Connected: ${snapshot.data!.wirelessExternalPowerSourceConnected}", + ), + Text( + "Charge State: ${snapshot.data!.chargeState}", + ), + Text( + "Charge Level: ${snapshot.data!.chargeLevel}", + ), + Text( + "Charging Type: ${snapshot.data!.chargingType}", + ), + Text( + "Charging Fault Reason: ${snapshot.data!.chargingFaultReason}", + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + const Divider(), + StreamBuilder( + stream: connectedDevice.healthStatusStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Health Summary: ${snapshot.data!.healthSummary}%", + ), + Text( + "Cycle Count: ${snapshot.data!.cycleCount}", + ), + Text( + "Current Temperature: ${snapshot.data!.currentTemperature}°C", + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + const Divider(), + StreamBuilder( + stream: connectedDevice.energyStatusStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Voltage: ${snapshot.data!.voltage}V", + ), + Text( + "Available Capacity: ${snapshot.data!.availableCapacity}Wh", + ), + Text( + "Charge Rate: ${snapshot.data!.chargeRate}W", + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index e9662a8..ed336b7 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -17,6 +17,7 @@ export 'src/models/devices/wearable.dart'; export 'src/models/capabilities/device_firmware_version.dart'; export 'src/models/capabilities/device_hardware_version.dart'; export 'src/models/capabilities/device_identifier.dart'; +export 'src/models/capabilities/battery_service.dart'; export 'src/models/capabilities/rgb_led.dart'; export 'src/models/capabilities/status_led.dart'; export 'src/models/capabilities/sensor.dart'; diff --git a/lib/src/models/capabilities/battery_service.dart b/lib/src/models/capabilities/battery_service.dart new file mode 100644 index 0000000..21e53d3 --- /dev/null +++ b/lib/src/models/capabilities/battery_service.dart @@ -0,0 +1,123 @@ +abstract class BatteryService { + /// Reads the battery percentage of the device. + /// The value is between 0 and 100. + Future readBatteryPercentage(); + + Stream get batteryPercentageStream; +} + +enum ExternalPowerSourceConnected { + no, + yes, + unknown, +} + +enum ChargeState { + unknown, + charging, + dischargingActive, + dischargingInactive, +} + +enum BatteryChargeLevel { + unknown, + good, + low, + critical, +} + +enum BatteryChargingType { + unknown, + constantCurrent, + constantVoltage, + trickle, + float, +} + +enum ChargingFaultReason { + battery, + externalPowerSource, + other, +} + +abstract class ExtendedBatteryService extends BatteryService { + Future readEnergyStatus(); + + Future readHealthStatus(); + + Future readPowerStatus(); + + Stream get powerStatusStream; + Stream get energyStatusStream; + Stream get healthStatusStream; +} + +class BatteryEnergyStatus { + final double voltage; + final double availableCapacity; + final double chargeRate; + + const BatteryEnergyStatus({ + required this.voltage, + required this.availableCapacity, + required this.chargeRate, + }); + + @override + String toString() { + return 'BatteryEnergyStatus(voltage: $voltage, ' + 'availableCapacity: $availableCapacity, ' + 'chargeRate: $chargeRate)'; + } +} + +class BatteryHealthStatus { + /// The percentage of the battery health. + final int healthSummary; + final int cycleCount; + final int currentTemperature; + + const BatteryHealthStatus({ + required this.healthSummary, + required this.cycleCount, + required this.currentTemperature, + }); + + @override + String toString() { + return 'BatteryHealthStatus(healthSummary: $healthSummary, ' + 'cycleCount: $cycleCount, ' + 'currentTemperature: $currentTemperature)'; + } +} + +class BatteryPowerStatus { + final bool batteryPresent; + final ExternalPowerSourceConnected wiredExternalPowerSourceConnected; + final ExternalPowerSourceConnected wirelessExternalPowerSourceConnected; + final ChargeState chargeState; + final BatteryChargeLevel chargeLevel; + final BatteryChargingType chargingType; + final List chargingFaultReason; + + const BatteryPowerStatus({ + required this.batteryPresent, + required this.wiredExternalPowerSourceConnected, + required this.wirelessExternalPowerSourceConnected, + required this.chargeState, + required this.chargeLevel, + required this.chargingType, + required this.chargingFaultReason, + }); + + @override + String toString() { + return 'BatteryPowerStatus(batteryPresent: $batteryPresent, ' + 'wiredExternalPowerSourceConnected: $wiredExternalPowerSourceConnected, ' + 'wirelessExternalPowerSourceConnected: $wirelessExternalPowerSourceConnected, ' + 'chargeState: $chargeState, ' + 'chargeLevel: $chargeLevel, ' + 'chargingType: $chargingType, ' + 'chargingFaultReason: $chargingFaultReason)'; + } +} diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index c401f03..1611c52 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -1,7 +1,10 @@ import 'dart:async'; +import 'dart:ffi'; +import 'dart:math'; import 'dart:typed_data'; import 'package:logger/logger.dart'; +import 'package:open_earable_flutter/src/models/capabilities/battery_service.dart'; import 'package:open_earable_flutter/src/models/capabilities/status_led.dart'; import '../../managers/open_earable_sensor_manager.dart'; @@ -17,6 +20,12 @@ import '../../managers/ble_manager.dart'; import 'discovered_device.dart'; import 'wearable.dart'; +const String _batteryServiceUuid = "180F"; +const String _batteryLevelCharacteristicUuid = "2A19"; +const String _batteryLevelStatusCharacteristicUuid = "2BED"; +const String _batteryHealthStatusCharacteristicUuid = "2BEA"; +const String _batteryEnergyStatusCharacteristicUuid = "2BF0"; + const String _ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; const String _ledSetColorCharacteristic = "81040e7a-4819-11ee-be56-0242ac120002"; @@ -39,6 +48,7 @@ class OpenEarableV2 extends Wearable SensorConfigurationManager, RgbLed, StatusLed, + ExtendedBatteryService, DeviceIdentifier, DeviceFirmwareVersion, DeviceHardwareVersion { @@ -145,6 +155,199 @@ class OpenEarableV2 extends Wearable @override List get sensors => List.unmodifiable(_sensors); + + @override + Future readBatteryPercentage() async { + List batteryLevelList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _batteryServiceUuid, + characteristicId: _batteryLevelCharacteristicUuid, + ); + + _logger.t("Battery level bytes: $batteryLevelList"); + + if (batteryLevelList.length != 1) { + throw StateError('Battery level characteristic expected 1 value, but got ${batteryLevelList.length}'); + } + + return batteryLevelList[0]; + } + + @override + Future readEnergyStatus() async { + List energyStatusList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _batteryServiceUuid, + characteristicId: _batteryEnergyStatusCharacteristicUuid, + ); + + _logger.t("Battery energy status bytes: $energyStatusList"); + + if (energyStatusList.length != 7) { + throw StateError('Battery energy status characteristic expected 7 values, but got ${energyStatusList.length}'); + } + + int rawVoltage = (energyStatusList[2] << 8) | energyStatusList[1]; + double voltage = _convertSFloat(rawVoltage); + + int rawAvailableCapacity = (energyStatusList[4] << 8) | energyStatusList[3]; + double availableCapacity = _convertSFloat(rawAvailableCapacity); + + int rawChargeRate = (energyStatusList[6] << 8) | energyStatusList[5]; + double chargeRate = _convertSFloat(rawChargeRate); + + BatteryEnergyStatus batteryEnergyStatus = BatteryEnergyStatus( + voltage: voltage, + availableCapacity: availableCapacity, + chargeRate: chargeRate, + ); + + _logger.d('Battery energy status: $batteryEnergyStatus'); + + return batteryEnergyStatus; + } + + double _convertSFloat(int rawBits) { + int exponent = ((rawBits & 0xF000) >> 12) - 16; + int mantissa = rawBits & 0x0FFF; + + if (mantissa >= 0x800) { + mantissa = -((0x1000) - mantissa); + } + _logger.t("Exponent: $exponent, Mantissa: $mantissa"); + double result = mantissa.toDouble() * pow(10.0, exponent.toDouble()); + return result; + } + + @override + Future readHealthStatus() async { + List healthStatusList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _batteryServiceUuid, + characteristicId: _batteryHealthStatusCharacteristicUuid, + ); + + _logger.t("Battery health status bytes: $healthStatusList"); + + if (healthStatusList.length != 5) { + throw StateError('Battery health status characteristic expected 5 values, but got ${healthStatusList.length}'); + } + + int healthSummary = healthStatusList[1]; + int cycleCount = (healthStatusList[2] << 8) | healthStatusList[3]; + int currentTemperature = healthStatusList[4]; + + BatteryHealthStatus batteryHealthStatus = BatteryHealthStatus( + healthSummary: healthSummary, + cycleCount: cycleCount, + currentTemperature: currentTemperature, + ); + + _logger.d('Battery health status: $batteryHealthStatus'); + + return batteryHealthStatus; + } + + @override + Future readPowerStatus() async { + List powerStateList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: _batteryServiceUuid, + characteristicId: _batteryLevelStatusCharacteristicUuid, + ); + + int powerState = (powerStateList[1] << 8) | powerStateList[2]; + _logger.d("Battery power status bits: ${powerState.toRadixString(2)}"); + + bool batteryPresent = powerState >> 15 & 0x1 != 0; + + int wiredExternalPowerSourceConnectedRaw = (powerState >> 13) & 0x3; + ExternalPowerSourceConnected wiredExternalPowerSourceConnected + = ExternalPowerSourceConnected.values[wiredExternalPowerSourceConnectedRaw]; + + int wirelessExternalPowerSourceConnectedRaw = (powerState >> 11) & 0x3; + ExternalPowerSourceConnected wirelessExternalPowerSourceConnected + = ExternalPowerSourceConnected.values[wirelessExternalPowerSourceConnectedRaw]; + + int chargeStateRaw = (powerState >> 9) & 0x3; + ChargeState chargeState = ChargeState.values[chargeStateRaw]; + + int chargeLevelRaw = (powerState >> 7) & 0x3; + BatteryChargeLevel chargeLevel = BatteryChargeLevel.values[chargeLevelRaw]; + + int chargingTypeRaw = (powerState >> 5) & 0x7; + BatteryChargingType chargingType = BatteryChargingType.values[chargingTypeRaw]; + + int chargingFaultReasonRaw = (powerState >> 2) & 0x5; + List chargingFaultReason = []; + if ((chargingFaultReasonRaw & 0x1) != 0) { + chargingFaultReason.add(ChargingFaultReason.other); + } + if ((chargingFaultReasonRaw & 0x2) != 0) { + chargingFaultReason.add(ChargingFaultReason.externalPowerSource); + } + if ((chargingFaultReasonRaw & 0x4) != 0) { + chargingFaultReason.add(ChargingFaultReason.battery); + } + + BatteryPowerStatus batteryPowerStatus = BatteryPowerStatus( + batteryPresent: batteryPresent, + wiredExternalPowerSourceConnected: wiredExternalPowerSourceConnected, + wirelessExternalPowerSourceConnected: wirelessExternalPowerSourceConnected, + chargeState: chargeState, + chargeLevel: chargeLevel, + chargingType: chargingType, + chargingFaultReason: chargingFaultReason, + ); + + _logger.d('Battery power status: $batteryPowerStatus'); + + return batteryPowerStatus; + } + + @override + Stream get batteryPercentageStream async* { + while (true) { + yield await readBatteryPercentage(); + await Future.delayed(const Duration(seconds: 5)); + } + } + + @override + Stream get powerStatusStream async* { + while (true) { + try { + yield await readPowerStatus(); + } catch (e) { + _logger.e('Error reading power status: $e'); + } + await Future.delayed(const Duration(seconds: 5)); + } + } + + @override + Stream get energyStatusStream async* { + while (true) { + try { + yield await readEnergyStatus(); + } catch (e) { + _logger.e('Error reading energy status: $e'); + } + await Future.delayed(const Duration(seconds: 5)); + } + } + + @override + Stream get healthStatusStream async* { + while (true) { + try { + yield await readHealthStatus(); + } catch (e) { + _logger.e('Error reading health status: $e'); + } + await Future.delayed(const Duration(seconds: 5)); + } + } } class _ImuSensorConfiguration extends SensorConfiguration { From ebcfbb55a6b19bbdd81757ad9da99ae1ff727893 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:52:43 +0100 Subject: [PATCH 09/12] removed unused imports --- lib/src/models/devices/open_earable_v2.dart | 109 -------------------- lib/src/models/wearable_factory.dart | 2 +- 2 files changed, 1 insertion(+), 110 deletions(-) diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index 1611c52..47c1e70 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ffi'; import 'dart:math'; import 'dart:typed_data'; @@ -7,7 +6,6 @@ import 'package:logger/logger.dart'; import 'package:open_earable_flutter/src/models/capabilities/battery_service.dart'; import 'package:open_earable_flutter/src/models/capabilities/status_led.dart'; -import '../../managers/open_earable_sensor_manager.dart'; import '../capabilities/device_firmware_version.dart'; import '../capabilities/device_hardware_version.dart'; import '../capabilities/device_identifier.dart'; @@ -350,110 +348,3 @@ class OpenEarableV2 extends Wearable } } -class _ImuSensorConfiguration extends SensorConfiguration { - final OpenEarableSensorManager _sensorManager; - - _ImuSensorConfiguration({ - required OpenEarableSensorManager sensorManager, - }) : _sensorManager = sensorManager, - super( - name: 'IMU', - unit: 'Hz', - values: const [ - SensorConfigurationValue(key: '0'), - SensorConfigurationValue(key: '10'), - SensorConfigurationValue(key: '20'), - SensorConfigurationValue(key: '30'), - ], - ); - - @override - void setConfiguration(SensorConfigurationValue configuration) { - if (!super.values.contains(configuration)) { - throw UnimplementedError(); - } - - double imuSamplingRate = double.parse(configuration.key); - OpenEarableSensorConfig imuConfig = OpenEarableSensorConfig( - sensorId: 0, - samplingRate: imuSamplingRate, - latency: 0, - ); - - _sensorManager.writeSensorConfig(imuConfig); - } -} - -class _BarometerSensorConfiguration extends SensorConfiguration { - final OpenEarableSensorManager _sensorManager; - - _BarometerSensorConfiguration({ - required OpenEarableSensorManager sensorManager, - }) : _sensorManager = sensorManager, - super( - name: 'Barometer', - unit: 'Hz', - values: const [ - SensorConfigurationValue(key: '0'), - SensorConfigurationValue(key: '10'), - SensorConfigurationValue(key: '20'), - SensorConfigurationValue(key: '30'), - ], - ); - - @override - void setConfiguration(SensorConfigurationValue configuration) { - if (!super.values.contains(configuration)) { - throw UnimplementedError(); - } - - double? barometerSamplingRate = double.parse(configuration.key); - OpenEarableSensorConfig barometerConfig = OpenEarableSensorConfig( - sensorId: 1, - samplingRate: barometerSamplingRate, - latency: 0, - ); - - _sensorManager.writeSensorConfig(barometerConfig); - } -} - -class _MicrophoneSensorConfiguration extends SensorConfiguration { - final OpenEarableSensorManager _sensorManager; - - _MicrophoneSensorConfiguration({ - required OpenEarableSensorManager sensorManager, - }) : _sensorManager = sensorManager, - super( - name: 'Microphone', - unit: 'Hz', - values: const [ - SensorConfigurationValue(key: "0"), - SensorConfigurationValue(key: "16000"), - SensorConfigurationValue(key: "20000"), - SensorConfigurationValue(key: "25000"), - SensorConfigurationValue(key: "31250"), - SensorConfigurationValue(key: "33333"), - SensorConfigurationValue(key: "40000"), - SensorConfigurationValue(key: "41667"), - SensorConfigurationValue(key: "50000"), - SensorConfigurationValue(key: "62500"), - ], - ); - - @override - void setConfiguration(SensorConfigurationValue configuration) { - if (!super.values.contains(configuration)) { - throw UnimplementedError(); - } - - double? microphoneSamplingRate = double.parse(configuration.key); - OpenEarableSensorConfig microphoneConfig = OpenEarableSensorConfig( - sensorId: 2, - samplingRate: microphoneSamplingRate, - latency: 0, - ); - - _sensorManager.writeSensorConfig(microphoneConfig); - } -} diff --git a/lib/src/models/wearable_factory.dart b/lib/src/models/wearable_factory.dart index d412bb3..1c3f8da 100644 --- a/lib/src/models/wearable_factory.dart +++ b/lib/src/models/wearable_factory.dart @@ -9,4 +9,4 @@ abstract class WearableFactory { Future matches(DiscoveredDevice device, List services); Future createFromDevice(DiscoveredDevice device); -} \ No newline at end of file +} From e8539b03a34e7aa87604f0fda6d5f2a936249264 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:54:23 +0100 Subject: [PATCH 10/12] simple chart implementation --- example/lib/main.dart | 6 ++ .../lib/widgets/all_sensor_charts_widget.dart | 37 +++++++ example/lib/widgets/sensor_chart.dart | 99 +++++++++++++++++++ example/pubspec.lock | 40 ++++++++ example/pubspec.yaml | 1 + 5 files changed, 183 insertions(+) create mode 100644 example/lib/widgets/all_sensor_charts_widget.dart create mode 100644 example/lib/widgets/sensor_chart.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index a056e90..fb93ac1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:example/widgets/all_sensor_charts_widget.dart'; import 'package:example/widgets/battery_info_widget.dart'; import 'package:flutter/material.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart'; @@ -222,6 +223,11 @@ class MyAppState extends State { .toList(), ), ), + if (_connectedDevice is SensorManager) + GroupedBox( + title: "Sensor Charts", + child: AllSensorChartsWidget(sensorManager: _connectedDevice as SensorManager) + ), ] .map((e) => Padding( padding: const EdgeInsets.only( diff --git a/example/lib/widgets/all_sensor_charts_widget.dart b/example/lib/widgets/all_sensor_charts_widget.dart new file mode 100644 index 0000000..e497716 --- /dev/null +++ b/example/lib/widgets/all_sensor_charts_widget.dart @@ -0,0 +1,37 @@ +import 'package:example/widgets/sensor_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; + +class AllSensorChartsWidget extends StatelessWidget { + final SensorManager sensorManager; + + const AllSensorChartsWidget({Key? key, required this.sensorManager}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 500, + child: DefaultTabController( + length: sensorManager.sensors.length, + child: Column( + children: [ + TabBar( + isScrollable: true, + tabs: sensorManager.sensors.map((sensor) { + return Tab(text: sensor.sensorName); + }).toList(), + ), + Expanded( + child: TabBarView( + children: sensorManager.sensors.map((sensor) { + return SensorChart(sensor: sensor); + }).toList(), + ), + ), + ], + ), + ) + ); + } +} diff --git a/example/lib/widgets/sensor_chart.dart b/example/lib/widgets/sensor_chart.dart new file mode 100644 index 0000000..32be2dd --- /dev/null +++ b/example/lib/widgets/sensor_chart.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:community_charts_flutter/community_charts_flutter.dart' as charts; + +class SensorChart extends StatefulWidget { + final Sensor sensor; + + const SensorChart({Key? key, required this.sensor}) : super(key: key); + + @override + State createState() => _SensorChartState(); +} + +class _SensorChartState extends State { + List> _chartData = []; + final List _dataPoints = []; + + @override + void initState() { + super.initState(); + _listenToSensorStream(); + } + + void _listenToSensorStream() { + widget.sensor.sensorStream.listen((sensorValue) { + setState(() { + // Add new data points + for (int i = 0; i < widget.sensor.axisCount; i++) { + _dataPoints.add(ChartData(sensorValue.timestamp, sensorValue.values[i], widget.sensor.axisNames[i])); + } + + // Remove data older than 5 seconds + int cutoffTime = sensorValue.timestamp - 5000; + _dataPoints.removeWhere((data) => data.time < cutoffTime); + + // Update chart data + _chartData = [ + for (int i = 0; i < widget.sensor.axisCount; i++) + charts.Series( + id: widget.sensor.axisNames[i], + colorFn: (_, __) => charts.MaterialPalette.blue.makeShades(widget.sensor.axisCount)[i], + domainFn: (ChartData point, _) => point.time, // X-axis (timestamp) + measureFn: (ChartData point, _) => point.value, // Y-axis (sensor value) + data: _dataPoints.where((point) => point.axisName == widget.sensor.axisNames[i]).toList(), + ), + ]; + }); + }); + } + + @override + Widget build(BuildContext context) { + // Determine min/max range for X and Y axes + final xValues = _dataPoints.map((e) => e.time).toList(); + final yValues = _dataPoints.map((e) => e.value).toList(); + + final int? xMin = xValues.isNotEmpty ? xValues.reduce((a, b) => a < b ? a : b) : null; + final int? xMax = xValues.isNotEmpty ? xValues.reduce((a, b) => a > b ? a : b) : null; + + final double? yMin = yValues.isNotEmpty ? yValues.reduce((a, b) => a < b ? a : b) : null; + final double? yMax = yValues.isNotEmpty ? yValues.reduce((a, b) => a > b ? a : b) : null; + + return Column( + children: [ + Expanded( + child: charts.LineChart( + _chartData, + animate: true, + domainAxis: charts.NumericAxisSpec( + viewport: xMin != null && xMax != null + ? charts.NumericExtents(xMin.toDouble(), xMax.toDouble()) + : null, + tickProviderSpec: const charts.BasicNumericTickProviderSpec(desiredTickCount: 5), + ), + primaryMeasureAxis: charts.NumericAxisSpec( + viewport: yMin != null && yMax != null + ? charts.NumericExtents(yMin, yMax) + : null, + tickProviderSpec: const charts.BasicNumericTickProviderSpec(desiredTickCount: 5), + ), + behaviors: [ + charts.SeriesLegend(), + charts.ChartTitle('Time (ms)', behaviorPosition: charts.BehaviorPosition.bottom), + charts.ChartTitle('Value', behaviorPosition: charts.BehaviorPosition.start), + ], + ), + ), + ], + ); + } +} + +class ChartData { + final int time; // Timestamp in milliseconds + final double value; // Sensor value + final String axisName; // Name of the axis + + ChartData(this.time, this.value, this.axisName); +} \ No newline at end of file diff --git a/example/pubspec.lock b/example/pubspec.lock index adcce7c..69482d5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -57,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + community_charts_common: + dependency: transitive + description: + name: community_charts_common + sha256: d997ade57f15490346de46efbe23805d378a672aafbf5e47e19517964b671009 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + community_charts_flutter: + dependency: "direct main" + description: + name: community_charts_flutter + sha256: "4614846b99782ab79b613687704865e5468ecada3f0ad1afe1cdc3ff5b727f72" + url: "https://pub.dev" + source: hosted + version: "1.0.4" convert: dependency: transitive description: @@ -136,6 +152,30 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: "99f282cb0e02edcbbf8c6b3bbc7c90b65635156c412e58f3975a7e55284ce685" + url: "https://pub.dev" + source: hosted + version: "0.20.0" js: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9703840..39e59a7 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 flutter_colorpicker: ^1.1.0 + community_charts_flutter: ^1.0.4 dev_dependencies: flutter_test: From 2016bfde1dcbc1ffce611b66ae7f5b1795ff97e5 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:13:59 +0100 Subject: [PATCH 11/12] added ability to enable and disable axes in sensor --- example/lib/widgets/sensor_chart.dart | 73 ++++++++++++++++++++------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/example/lib/widgets/sensor_chart.dart b/example/lib/widgets/sensor_chart.dart index 32be2dd..03295d3 100644 --- a/example/lib/widgets/sensor_chart.dart +++ b/example/lib/widgets/sensor_chart.dart @@ -15,9 +15,13 @@ class _SensorChartState extends State { List> _chartData = []; final List _dataPoints = []; + // Track which axes are enabled + late Map _axisEnabled; + @override void initState() { super.initState(); + _axisEnabled = {for (var name in widget.sensor.axisNames) name: true}; _listenToSensorStream(); } @@ -33,26 +37,42 @@ class _SensorChartState extends State { int cutoffTime = sensorValue.timestamp - 5000; _dataPoints.removeWhere((data) => data.time < cutoffTime); - // Update chart data - _chartData = [ - for (int i = 0; i < widget.sensor.axisCount; i++) - charts.Series( - id: widget.sensor.axisNames[i], - colorFn: (_, __) => charts.MaterialPalette.blue.makeShades(widget.sensor.axisCount)[i], - domainFn: (ChartData point, _) => point.time, // X-axis (timestamp) - measureFn: (ChartData point, _) => point.value, // Y-axis (sensor value) - data: _dataPoints.where((point) => point.axisName == widget.sensor.axisNames[i]).toList(), - ), - ]; + _updateChartData(); }); }); } + void _updateChartData() { + // Update chart data based on enabled axes + _chartData = [ + for (int i = 0; i < widget.sensor.axisCount; i++) + if (_axisEnabled[widget.sensor.axisNames[i]] ?? false) + charts.Series( + id: widget.sensor.axisNames[i], + colorFn: (_, __) => charts.MaterialPalette.blue.makeShades(widget.sensor.axisCount)[i], + domainFn: (ChartData point, _) => point.time, + measureFn: (ChartData point, _) => point.value, + data: _dataPoints.where((point) => point.axisName == widget.sensor.axisNames[i]).toList(), + ), + ]; + } + + void _toggleAxis(String axisName, bool value) { + setState(() { + _axisEnabled[axisName] = value; + _updateChartData(); + }); + } + @override Widget build(BuildContext context) { - // Determine min/max range for X and Y axes - final xValues = _dataPoints.map((e) => e.time).toList(); - final yValues = _dataPoints.map((e) => e.value).toList(); + // Filter only enabled axes data for scaling + final filteredPoints = _dataPoints + .where((point) => _axisEnabled[point.axisName] ?? false) + .toList(); + + final xValues = filteredPoints.map((e) => e.time).toList(); + final yValues = filteredPoints.map((e) => e.value).toList(); final int? xMin = xValues.isNotEmpty ? xValues.reduce((a, b) => a < b ? a : b) : null; final int? xMax = xValues.isNotEmpty ? xValues.reduce((a, b) => a > b ? a : b) : null; @@ -62,26 +82,43 @@ class _SensorChartState extends State { return Column( children: [ + // Checkbox controls for each axis + Wrap( + spacing: 8.0, + children: widget.sensor.axisNames.map((axisName) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: _axisEnabled[axisName], + onChanged: (value) => _toggleAxis(axisName, value ?? false), + ), + Text(axisName), + ], + ); + }).toList(), + ), + // Chart display Expanded( child: charts.LineChart( _chartData, - animate: true, + animate: false, domainAxis: charts.NumericAxisSpec( viewport: xMin != null && xMax != null ? charts.NumericExtents(xMin.toDouble(), xMax.toDouble()) : null, - tickProviderSpec: const charts.BasicNumericTickProviderSpec(desiredTickCount: 5), + tickProviderSpec: const charts.BasicNumericTickProviderSpec(zeroBound: false, desiredMinTickCount: 3), ), primaryMeasureAxis: charts.NumericAxisSpec( viewport: yMin != null && yMax != null ? charts.NumericExtents(yMin, yMax) : null, - tickProviderSpec: const charts.BasicNumericTickProviderSpec(desiredTickCount: 5), + tickProviderSpec: const charts.BasicNumericTickProviderSpec(zeroBound: false, desiredMinTickCount: 3), ), behaviors: [ charts.SeriesLegend(), charts.ChartTitle('Time (ms)', behaviorPosition: charts.BehaviorPosition.bottom), - charts.ChartTitle('Value', behaviorPosition: charts.BehaviorPosition.start), + charts.ChartTitle(widget.sensor.axisUnits.first, behaviorPosition: charts.BehaviorPosition.start), ], ), ), From 87769b1b59b5dacc848ae85f3439a3871f9442d4 Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:40:49 +0100 Subject: [PATCH 12/12] made logger public to enable overriding of the loglevel and filters --- lib/open_earable_flutter.dart | 6 +-- .../models/devices/open_earable_factory.dart | 27 +++++++------ lib/src/models/devices/open_earable_v2.dart | 39 ++++++------------- 3 files changed, 28 insertions(+), 44 deletions(-) diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index ed336b7..9e4b340 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -31,7 +31,7 @@ export 'src/models/capabilities/storage_path_audio_player.dart'; part 'src/constants.dart'; -Logger _logger = Logger(); +Logger logger = Logger(); class WearableManager { static final WearableManager _instance = WearableManager._internal(); @@ -70,12 +70,12 @@ class WearableManager { for (WearableFactory wearableFactory in _wearableFactories) { wearableFactory.bleManager = _bleManager; wearableFactory.disconnectNotifier = disconnectNotifier; - _logger.t("checking factory: $wearableFactory"); + logger.t("checking factory: $wearableFactory"); if (await wearableFactory.matches(device, connectionResult.$2)) { Wearable wearable = await wearableFactory.createFromDevice(device); return wearable; } else { - _logger.d("'$wearableFactory' does not support '$device'"); + logger.d("'$wearableFactory' does not support '$device'"); } } throw Exception('Device is currently not supported'); diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index 6cec224..29b85e9 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -18,7 +18,6 @@ const String _deviceParseInfoServiceUuid = const String _deviceParseInfoCharacteristicUuid = "caa25cb8-7e1b-44f2-adc9-e8c06c9ced43"; -Logger _logger = Logger(); class OpenEarableFactory extends WearableFactory { final _v1Regex = RegExp(r'^1\.\d+\.\d+$'); @@ -27,13 +26,13 @@ class OpenEarableFactory extends WearableFactory { @override Future matches(DiscoveredDevice device, List services) async { if (!services.any((service) => service.uuid == _deviceInfoServiceUuid)) { - _logger.d("'$device' has no service matching '$_deviceInfoServiceUuid'"); + logger.d("'$device' has no service matching '$_deviceInfoServiceUuid'"); return false; } String firmwareVersion = await _getFirmwareVersion(device); - _logger.d("Firmware Version: '$firmwareVersion'"); + logger.d("Firmware Version: '$firmwareVersion'"); - _logger.t("matches V2: ${_v2Regex.hasMatch(firmwareVersion)}"); + logger.t("matches V2: ${_v2Regex.hasMatch(firmwareVersion)}"); return _v1Regex.hasMatch(firmwareVersion) || _v2Regex.hasMatch(firmwareVersion); } @@ -77,7 +76,7 @@ class OpenEarableFactory extends WearableFactory { serviceId: _deviceInfoServiceUuid, characteristicId: _deviceFirmwareVersionCharacteristicUuid, ); - _logger.d("Raw Firmware Version: $softwareGenerationBytes"); + logger.d("Raw Firmware Version: $softwareGenerationBytes"); int firstZeroIndex = softwareGenerationBytes.indexOf(0); if (firstZeroIndex != -1) { softwareGenerationBytes = softwareGenerationBytes.sublist(0, firstZeroIndex); @@ -94,9 +93,9 @@ class OpenEarableFactory extends WearableFactory { serviceId: _deviceParseInfoServiceUuid, characteristicId: _deviceParseInfoCharacteristicUuid, ); - _logger.d("Read raw parse info: $sensorParseSchemeData"); + logger.d("Read raw parse info: $sensorParseSchemeData"); Map parseInfo = _parseSchemeCharacteristic(sensorParseSchemeData); - _logger.i("Found the following info about parsing: $parseInfo"); + logger.i("Found the following info about parsing: $parseInfo"); OpenEarableSensorManager sensorManager = OpenEarableSensorManager( bleManager: bleManager!, @@ -105,10 +104,10 @@ class OpenEarableFactory extends WearableFactory { for (String sensorName in parseInfo.keys) { Map sensorDetail = parseInfo[sensorName] as Map; - _logger.t("sensor detail: $sensorDetail"); + logger.t("sensor detail: $sensorDetail"); Map componentsMap = sensorDetail['Components'] as Map; - _logger.t("components: $componentsMap"); + logger.t("components: $componentsMap"); sensorConfigurations.add( _OpenEarableSensorConfiguration( @@ -121,7 +120,7 @@ class OpenEarableFactory extends WearableFactory { for (String groupName in componentsMap.keys) { Map groupDetail = componentsMap[groupName] as Map; - _logger.t("group detail: $groupDetail"); + logger.t("group detail: $groupDetail"); List<(String, String)> axisDetails = groupDetail.entries.map((axis) { Map v = axis.value as Map; return (axis.key, v['unit'] as String); @@ -141,8 +140,8 @@ class OpenEarableFactory extends WearableFactory { } } - _logger.d("Created sensors: $sensors"); - _logger.d("Created sensor configurations: $sensorConfigurations"); + logger.d("Created sensors: $sensors"); + logger.d("Created sensor configurations: $sensorConfigurations"); return (sensors, sensorConfigurations); } @@ -290,9 +289,9 @@ class _OpenEarableSensor extends Sensor { _dataSubscription?.cancel(); _dataSubscription = _sensorManager.subscribeToSensorData(_sensorId).listen((data) { int timestamp = data["timestamp"]; - _logger.t("SensorData: $data"); + logger.t("SensorData: $data"); - _logger.t("componentData of $componentName: ${data[componentName]}"); + logger.t("componentData of $componentName: ${data[componentName]}"); List values = []; for (var entry in (data[componentName] as Map).entries) { diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index 47c1e70..d24b9c1 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -2,21 +2,8 @@ import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; -import 'package:logger/logger.dart'; -import 'package:open_earable_flutter/src/models/capabilities/battery_service.dart'; -import 'package:open_earable_flutter/src/models/capabilities/status_led.dart'; - -import '../capabilities/device_firmware_version.dart'; -import '../capabilities/device_hardware_version.dart'; -import '../capabilities/device_identifier.dart'; -import '../capabilities/rgb_led.dart'; -import '../capabilities/sensor.dart'; -import '../capabilities/sensor_configuration.dart'; -import '../capabilities/sensor_configuration_manager.dart'; -import '../capabilities/sensor_manager.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; import '../../managers/ble_manager.dart'; -import 'discovered_device.dart'; -import 'wearable.dart'; const String _batteryServiceUuid = "180F"; const String _batteryLevelCharacteristicUuid = "2A19"; @@ -38,8 +25,6 @@ const String _deviceFirmwareVersionCharacteristicUuid = const String _deviceHardwareVersionCharacteristicUuid = "45622513-6468-465a-b141-0b9b0f96b468"; -Logger _logger = Logger(); - class OpenEarableV2 extends Wearable implements SensorManager, @@ -162,7 +147,7 @@ class OpenEarableV2 extends Wearable characteristicId: _batteryLevelCharacteristicUuid, ); - _logger.t("Battery level bytes: $batteryLevelList"); + logger.t("Battery level bytes: $batteryLevelList"); if (batteryLevelList.length != 1) { throw StateError('Battery level characteristic expected 1 value, but got ${batteryLevelList.length}'); @@ -179,7 +164,7 @@ class OpenEarableV2 extends Wearable characteristicId: _batteryEnergyStatusCharacteristicUuid, ); - _logger.t("Battery energy status bytes: $energyStatusList"); + logger.t("Battery energy status bytes: $energyStatusList"); if (energyStatusList.length != 7) { throw StateError('Battery energy status characteristic expected 7 values, but got ${energyStatusList.length}'); @@ -200,7 +185,7 @@ class OpenEarableV2 extends Wearable chargeRate: chargeRate, ); - _logger.d('Battery energy status: $batteryEnergyStatus'); + logger.d('Battery energy status: $batteryEnergyStatus'); return batteryEnergyStatus; } @@ -212,7 +197,7 @@ class OpenEarableV2 extends Wearable if (mantissa >= 0x800) { mantissa = -((0x1000) - mantissa); } - _logger.t("Exponent: $exponent, Mantissa: $mantissa"); + logger.t("Exponent: $exponent, Mantissa: $mantissa"); double result = mantissa.toDouble() * pow(10.0, exponent.toDouble()); return result; } @@ -225,7 +210,7 @@ class OpenEarableV2 extends Wearable characteristicId: _batteryHealthStatusCharacteristicUuid, ); - _logger.t("Battery health status bytes: $healthStatusList"); + logger.t("Battery health status bytes: $healthStatusList"); if (healthStatusList.length != 5) { throw StateError('Battery health status characteristic expected 5 values, but got ${healthStatusList.length}'); @@ -241,7 +226,7 @@ class OpenEarableV2 extends Wearable currentTemperature: currentTemperature, ); - _logger.d('Battery health status: $batteryHealthStatus'); + logger.d('Battery health status: $batteryHealthStatus'); return batteryHealthStatus; } @@ -255,7 +240,7 @@ class OpenEarableV2 extends Wearable ); int powerState = (powerStateList[1] << 8) | powerStateList[2]; - _logger.d("Battery power status bits: ${powerState.toRadixString(2)}"); + logger.d("Battery power status bits: ${powerState.toRadixString(2)}"); bool batteryPresent = powerState >> 15 & 0x1 != 0; @@ -298,7 +283,7 @@ class OpenEarableV2 extends Wearable chargingFaultReason: chargingFaultReason, ); - _logger.d('Battery power status: $batteryPowerStatus'); + logger.d('Battery power status: $batteryPowerStatus'); return batteryPowerStatus; } @@ -317,7 +302,7 @@ class OpenEarableV2 extends Wearable try { yield await readPowerStatus(); } catch (e) { - _logger.e('Error reading power status: $e'); + logger.e('Error reading power status: $e'); } await Future.delayed(const Duration(seconds: 5)); } @@ -329,7 +314,7 @@ class OpenEarableV2 extends Wearable try { yield await readEnergyStatus(); } catch (e) { - _logger.e('Error reading energy status: $e'); + logger.e('Error reading energy status: $e'); } await Future.delayed(const Duration(seconds: 5)); } @@ -341,7 +326,7 @@ class OpenEarableV2 extends Wearable try { yield await readHealthStatus(); } catch (e) { - _logger.e('Error reading health status: $e'); + logger.e('Error reading health status: $e'); } await Future.delayed(const Duration(seconds: 5)); }