From 4c49ca062789d0b3b99e05f8fd490499182ec292 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:09:55 +0100 Subject: [PATCH 1/5] lib/src/models/capabilites/audio_response_manager.dart: added new capability --- lib/src/models/capabilities/audio_response_manager.dart | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 lib/src/models/capabilities/audio_response_manager.dart diff --git a/lib/src/models/capabilities/audio_response_manager.dart b/lib/src/models/capabilities/audio_response_manager.dart new file mode 100644 index 00000000..7cdfa399 --- /dev/null +++ b/lib/src/models/capabilities/audio_response_manager.dart @@ -0,0 +1,4 @@ +/// An interface for managing audio response measurements. +abstract class AudioResponseManager { + Future> measureAudioResponse(Map parameters); +} From d6d34a1a2645781dcf509c57d5cc48d658350ff7 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:20:07 +0100 Subject: [PATCH 2/5] lib/open_earable_flutter.dart: export AudioResponseManager --- lib/open_earable_flutter.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index c3262467..0fb30aff 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -70,6 +70,7 @@ export 'src/models/wearable_factory.dart'; export 'src/models/capabilities/system_device.dart'; export 'src/managers/ble_gatt_manager.dart'; export 'src/models/capabilities/time_synchronizable.dart'; +export 'src/models/capabilities/audio_response_manager.dart'; export 'src/fota/fota.dart'; From 664278e41e049256d618a0cc5e9155c2a8e6dd78 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:20:41 +0100 Subject: [PATCH 3/5] lib/src/models/devices/open_earable_v2.dart: implement AudioResponseManager --- lib/src/models/devices/open_earable_v2.dart | 133 +++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index e2b617d6..0d306290 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -41,6 +41,10 @@ const String _timeSyncTimeMappingCharacteristicUuid = const String _timeSyncRttCharacteristicUuid = "2e04cbf9-939d-4be5-823e-271838b75259"; +const String _audioResponseServiceUuid = "12345678-1234-5678-9abc-def123456789"; +const String _audioResponseControlCharacteristicUuid = "12345679-1234-5678-9abc-def123456789"; +const String _audioResponseDataCharacteristicUuid = "1234567a-1234-5678-9abc-def123456789"; + final VersionConstraint _versionConstraint = VersionConstraint.parse(">=2.1.0 <2.3.0"); @@ -77,7 +81,8 @@ class OpenEarableV2 extends BluetoothWearable EdgeRecorderManager, ButtonManager, StereoDevice, - SystemDevice { + SystemDevice, + AudioResponseManager { static const String deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; static const String ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; @@ -552,6 +557,132 @@ class OpenEarableV2 extends BluetoothWearable _pairedDevice?.unpair(); _pairedDevice = null; } + + // MARK: AudioResponseManager + + void _triggerAudioResponseMeasurement() { + bleManager.write( + deviceId: deviceId, + serviceId: _audioResponseServiceUuid, + characteristicId: _audioResponseControlCharacteristicUuid, + byteData: [0xFF], // Command to start audio response measurement + ); + } + + Future> _parseAudioResponseData(Uint8List data) async { + if (data.isEmpty) { + throw StateError('Audio response data is empty'); + } + + // New v1 payload size: + // 1 (version) + 1 (quality) + 1 (mean_magnitude) + 1 (num_peaks) + // + 9*2 (frequencies) + 9*2 (magnitudes) = 40 bytes + const int expectedLenV1 = 40; + + if (data.length < expectedLenV1) { + throw StateError( + 'Audio response data too short: ${data.length} bytes (expected $expectedLenV1)', + ); + } + + final int version = data[0]; + if (version != 1) { + throw StateError('Unsupported audio response data version: $version'); + } + + if (data.length != expectedLenV1) { + throw StateError( + 'Unexpected audio response data length for version 1: ${data.length} bytes (expected $expectedLenV1)', + ); + } + + final int quality = data[1]; + final int meanMagnitude = data[2]; + final int numPeaks = data[3]; + + // Frequencies: 9 * uint16_t (12.4 fixed point) starting at offset 4 + // NOTE: Endianness: this uses big-endian to match your previous implementation. + // If firmware sends little-endian, swap the byte order. + const int freqBase = 4; + final List frequenciesRaw = List.filled(9, 0); + final List frequenciesHz = List.filled(9, 0); + for (int i = 0; i < 9; i++) { + final int off = freqBase + i * 2; + final int raw = (data[off + 1] << 8) | data[off]; + frequenciesRaw[i] = raw; + frequenciesHz[i] = raw / 16.0; // 12.4 fixed point -> Hz + } + + // Magnitudes: 9 * uint16_t starting at offset 4 + 18 = 22 + const int magBase = freqBase + 9 * 2; // 22 + final List magnitudes = List.filled(9, 0); + for (int i = 0; i < 9; i++) { + final int off = magBase + i * 2; + final int mag = (data[off + 1] << 8) | data[off]; + magnitudes[i] = mag; + } + + final List> points = List.generate(9, (i) { + return { + 'frequency_hz': frequenciesHz[i], + 'frequency_raw_q12_4': frequenciesRaw[i], + 'magnitude': magnitudes[i], + }; + }); + + return { + 'version': version, + 'quality': quality, + 'mean_magnitude': meanMagnitude, + 'num_peaks': numPeaks, + 'frequencies_hz': frequenciesHz, + 'frequencies_raw_q12_4': frequenciesRaw, + 'magnitudes': magnitudes, + 'points': points, + }; + } + + @override + Future> measureAudioResponse(Map parameters) async { + _triggerAudioResponseMeasurement(); + + // Wait for the result via notification + final completer = Completer>(); + + late final StreamSubscription> audioRespSub; + audioRespSub = bleManager + .subscribe( + deviceId: deviceId, + serviceId: _audioResponseServiceUuid, + characteristicId: _audioResponseDataCharacteristicUuid, + ) + .listen( + (data) async { + logger.d("Received audio response data: $data"); + try { + final parsed = await _parseAudioResponseData(Uint8List.fromList(data)); + if (!completer.isCompleted) { + completer.complete(parsed); + } + } catch (e, stack) { + logger.e("Error parsing audio response data: $e, $stack"); + if (!completer.isCompleted) { + completer.completeError(e, stack); + } + } finally { + await audioRespSub.cancel(); + } + }, + onError: (error, stack) async { + logger.e("Error during audio response subscription: $error, $stack"); + if (!completer.isCompleted) { + completer.completeError(error, stack); + } + }, + ); + + return completer.future; + } } // MARK: OpenEarableV2Mic From dd4812afe9cb14b3196e80be3da77437a0698222 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:37:28 +0100 Subject: [PATCH 4/5] lib/src/models/devices/open_earable_v2.dart: changed mean magnitude format to 5.3 for audio response --- lib/src/models/devices/open_earable_v2.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index 0d306290..bcfa0b3c 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -597,7 +597,8 @@ class OpenEarableV2 extends BluetoothWearable } final int quality = data[1]; - final int meanMagnitude = data[2]; + final int meanMagnitudeRaw = data[2]; + final double meanMagnitude = meanMagnitudeRaw / 8.0; final int numPeaks = data[3]; // Frequencies: 9 * uint16_t (12.4 fixed point) starting at offset 4 @@ -634,6 +635,7 @@ class OpenEarableV2 extends BluetoothWearable 'version': version, 'quality': quality, 'mean_magnitude': meanMagnitude, + 'mean_magnitude_raw': meanMagnitudeRaw, 'num_peaks': numPeaks, 'frequencies_hz': frequenciesHz, 'frequencies_raw_q12_4': frequenciesRaw, From 025ea53d3d7eb469b1c3b3854bf6c51cd52e58c3 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:04:16 +0100 Subject: [PATCH 5/5] lib/src/models/devices: extracted audio resopnse manager to be an optional capability for openearable --- .../models/devices/open_earable_factory.dart | 9 +++++++ lib/src/models/devices/open_earable_v2.dart | 26 ++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index 11dadebd..fa8ec6cc 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -10,6 +10,7 @@ import '../../../open_earable_flutter.dart' show logger; import '../../managers/v2_sensor_handler.dart'; import '../../utils/sensor_value_parser/v2_sensor_value_parser.dart'; import '../capabilities/audio_mode_manager.dart'; +import '../capabilities/audio_response_manager.dart'; import '../capabilities/fota_capability.dart'; import '../capabilities/fota_slot_info_capability.dart'; import '../capabilities/sensor.dart'; @@ -99,6 +100,14 @@ class OpenEarableFactory extends WearableFactory { ), ); } + if (await bleManager!.hasService(deviceId: device.id, serviceId: audioResponseServiceUuid)) { + wearable.registerCapability( + OpenEarableV2AudioResponseManager( + bleManager: bleManager!, + deviceId: device.id, + ), + ); + } if (await bleManager!.hasService( deviceId: device.id, serviceId: mcuMgrSmpServiceUuid, diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index bcfa0b3c..0d79ff8a 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -41,7 +41,7 @@ const String _timeSyncTimeMappingCharacteristicUuid = const String _timeSyncRttCharacteristicUuid = "2e04cbf9-939d-4be5-823e-271838b75259"; -const String _audioResponseServiceUuid = "12345678-1234-5678-9abc-def123456789"; +const String audioResponseServiceUuid = "12345678-1234-5678-9abc-def123456789"; const String _audioResponseControlCharacteristicUuid = "12345679-1234-5678-9abc-def123456789"; const String _audioResponseDataCharacteristicUuid = "1234567a-1234-5678-9abc-def123456789"; @@ -81,8 +81,7 @@ class OpenEarableV2 extends BluetoothWearable EdgeRecorderManager, ButtonManager, StereoDevice, - SystemDevice, - AudioResponseManager { + SystemDevice { static const String deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; static const String ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; @@ -558,12 +557,23 @@ class OpenEarableV2 extends BluetoothWearable _pairedDevice = null; } - // MARK: AudioResponseManager +} + +// MARK: AudioResponseManager + +class OpenEarableV2AudioResponseManager implements AudioResponseManager { + final BleGattManager bleManager; + final String deviceId; + + OpenEarableV2AudioResponseManager({ + required this.bleManager, + required this.deviceId, + }); void _triggerAudioResponseMeasurement() { bleManager.write( deviceId: deviceId, - serviceId: _audioResponseServiceUuid, + serviceId: audioResponseServiceUuid, characteristicId: _audioResponseControlCharacteristicUuid, byteData: [0xFF], // Command to start audio response measurement ); @@ -645,7 +655,9 @@ class OpenEarableV2 extends BluetoothWearable } @override - Future> measureAudioResponse(Map parameters) async { + Future> measureAudioResponse( + Map parameters, + ) async { _triggerAudioResponseMeasurement(); // Wait for the result via notification @@ -655,7 +667,7 @@ class OpenEarableV2 extends BluetoothWearable audioRespSub = bleManager .subscribe( deviceId: deviceId, - serviceId: _audioResponseServiceUuid, + serviceId: audioResponseServiceUuid, characteristicId: _audioResponseDataCharacteristicUuid, ) .listen(