Skip to content

Commit 91d2884

Browse files
Feature/audio response (#143)
* feat(audio-response): implement protocol * feat(audio-response): report upload progress * docs(changelog): document audio response support * fix(audio-response): make repeated operations reliable (#144) * fix(ble): preserve replacement subscriptions * fix(audio-response): serialize repeated operations --------- Co-authored-by: Tobias Roeddiger <roeddiger@teco.edu>
1 parent 99ce1e5 commit 91d2884

11 files changed

Lines changed: 1261 additions & 2 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Unreleased
22

3+
* added audio response capability support for OpenEarable V2 devices.
4+
* added audio response upload progress reporting.
35
* BREAKING CHANGE: `BleGattManager.subscribe` now returns `Future<Stream<List<int>>>`, so callers must `await` subscription setup before listening to BLE notifications.
46
* BREAKING CHANGE: `SensorHandler.subscribeToSensorData` now returns `Future<Stream<Map<String, dynamic>>>`, so callers must `await` sensor notification readiness before listening to sensor data.
57
* fixed BLE notification setup races by ensuring subscription futures complete only after the underlying GATT notification subscription is enabled.

example/pubspec.lock

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,15 @@ packages:
414414
path: ".."
415415
relative: true
416416
source: path
417-
version: "2.3.9"
417+
version: "2.3.10"
418+
open_earable_protocols:
419+
dependency: transitive
420+
description:
421+
name: open_earable_protocols
422+
sha256: c11cae4914827c1d7617d44647a5c49304f0c2a2c02c907c595682cfade6ddb2
423+
url: "https://pub.dev"
424+
source: hosted
425+
version: "0.0.2"
418426
package_config:
419427
dependency: transitive
420428
description:

lib/open_earable_flutter.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import 'src/models/devices/discovered_device.dart';
2424
import 'src/models/devices/open_ring_factory.dart';
2525
import 'src/models/devices/wearable.dart';
2626

27+
export 'package:open_earable_protocols/open_earable_protocols.dart'
28+
show AudioResponseConfig, AudioResponseResult;
29+
2730
export 'src/models/devices/discovered_device.dart';
2831
export 'src/models/devices/wearable.dart';
2932
export 'src/models/devices/cosinuss_one.dart';
@@ -72,6 +75,7 @@ export 'src/models/wearable_factory.dart';
7275
export 'src/models/capabilities/system_device.dart';
7376
export 'src/managers/ble_gatt_manager.dart';
7477
export 'src/models/capabilities/time_synchronizable.dart';
78+
export 'src/models/capabilities/audio_response_manager.dart';
7579

7680
export 'src/fota/fota.dart';
7781

lib/src/managers/ble_gatt_manager.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ abstract class BleGattManager {
1616
});
1717

1818
/// Writes byte data to a specific characteristic of a device.
19+
///
20+
/// Set [withoutResponse] when the characteristic only supports writes without
21+
/// response or when an acknowledged write is not required.
1922
Future<void> write({
2023
required String deviceId,
2124
required String serviceId,
2225
required String characteristicId,
2326
required List<int> byteData,
27+
bool withoutResponse = false,
2428
});
2529

2630
/// Subscribes to a specific characteristic of the connected device.

lib/src/managers/ble_manager.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ class BleManager extends BleGattManager {
328328
required String serviceId,
329329
required String characteristicId,
330330
required List<int> byteData,
331+
bool withoutResponse = false,
331332
}) async {
332333
if (!isConnected(deviceId)) {
333334
throw Exception("Write failed because no Earable is connected");
@@ -337,6 +338,7 @@ class BleManager extends BleGattManager {
337338
serviceId,
338339
characteristicId,
339340
Uint8List.fromList(byteData),
341+
withoutResponse: withoutResponse,
340342
);
341343
}
342344

@@ -394,7 +396,14 @@ class BleManager extends BleGattManager {
394396
}
395397

396398
streamController.onCancel = () async {
397-
if (_streamControllers.containsKey(streamIdentifier)) {
399+
// A canceled controller may finish closing after a replacement
400+
// subscription has already installed a new controller for the same
401+
// characteristic. Only the controller that still owns the map entry may
402+
// tear down the native notification subscription.
403+
if (identical(
404+
_streamControllers[streamIdentifier],
405+
streamController,
406+
)) {
398407
final canceledController = _streamControllers.remove(streamIdentifier);
399408
_subscriptionSetups.remove(streamIdentifier);
400409
if (canceledController != null && !canceledController.isClosed) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:open_earable_protocols/open_earable_protocols.dart';
2+
3+
/// Receives progress updates for an audio buffer upload.
4+
typedef AudioResponseUploadProgressCallback = void Function(
5+
AudioResponseUploadProgress progress,
6+
);
7+
8+
/// Current phase of an audio buffer upload.
9+
enum AudioResponseUploadPhase {
10+
/// The transfer is being initialized on the device.
11+
starting,
12+
13+
/// Audio samples are being transferred and acknowledged by the device.
14+
uploading,
15+
16+
/// All samples are acknowledged and the transfer is being committed.
17+
committing,
18+
19+
/// The device successfully committed the transfer.
20+
completed,
21+
}
22+
23+
/// Immutable progress reported while uploading an audio buffer.
24+
class AudioResponseUploadProgress {
25+
/// Creates an audio buffer upload progress update.
26+
const AudioResponseUploadProgress({
27+
required this.phase,
28+
required this.acknowledgedSamples,
29+
required this.totalSamples,
30+
});
31+
32+
/// Current transfer phase.
33+
final AudioResponseUploadPhase phase;
34+
35+
/// Number of samples acknowledged by the device.
36+
final int acknowledgedSamples;
37+
38+
/// Total number of samples in the transfer.
39+
final int totalSamples;
40+
41+
/// Fraction of samples acknowledged by the device, between zero and one.
42+
double get fraction =>
43+
totalSamples == 0 ? 0 : acknowledgedSamples / totalSamples;
44+
}
45+
46+
/// An interface for managing audio response measurements.
47+
abstract class AudioResponseManager {
48+
/// Uploads and commits signed 16-bit PCM [samples] for later measurements.
49+
///
50+
/// [maximumSamplesPerChunk] must fit the effective GATT write payload.
51+
/// [onProgress] receives synchronous updates based on sample offsets
52+
/// acknowledged by the device. Exceptions thrown by the callback terminate
53+
/// the upload.
54+
Future<void> uploadAudioBuffer({
55+
required int transferId,
56+
required List<int> samples,
57+
required int samplingRate,
58+
int maximumSamplesPerChunk = 118,
59+
AudioResponseUploadProgressCallback? onProgress,
60+
});
61+
62+
/// Starts a measurement and returns its protocol result.
63+
///
64+
/// The [config] must reference a buffer previously committed by
65+
/// [uploadAudioBuffer].
66+
Future<AudioResponseResult> measureAudioResponse(AudioResponseConfig config);
67+
}
68+
69+
/// Error reported by the device while transferring an audio response buffer.
70+
class AudioResponseTransferException implements Exception {
71+
/// Creates an audio response transfer error from a protocol status value.
72+
const AudioResponseTransferException({
73+
required this.status,
74+
required this.message,
75+
});
76+
77+
/// Numeric status reported by the audio response protocol.
78+
final int status;
79+
80+
/// Human-readable description of [status].
81+
final String message;
82+
83+
@override
84+
String toString() => 'AudioResponseTransferException($status): $message';
85+
}

lib/src/models/devices/open_earable_factory.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import 'package:open_earable_flutter/src/managers/sensor_handler.dart';
55
import 'package:open_earable_flutter/src/models/wearable_factory.dart';
66
import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/sensor_scheme_reader.dart';
77
import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/v2_sensor_scheme_reader.dart';
8+
import 'package:open_earable_protocols/open_earable_protocols.dart';
89
import 'package:universal_ble/universal_ble.dart';
910

1011
import '../../../open_earable_flutter.dart' show logger;
1112
import '../../constants.dart';
1213
import '../../managers/v2_sensor_handler.dart';
1314
import '../../utils/sensor_value_parser/v2_sensor_value_parser.dart';
1415
import '../capabilities/audio_mode_manager.dart';
16+
import '../capabilities/audio_response_manager.dart';
1517
import '../capabilities/fota_capability.dart';
1618
import '../capabilities/fota_slot_info_capability.dart';
1719
import '../capabilities/power_saving_mode_manager.dart';
@@ -25,6 +27,7 @@ import '../capabilities/time_synchronizable.dart';
2527
import 'discovered_device.dart';
2628
import 'open_earable_v1.dart';
2729
import 'open_earable_v2.dart';
30+
import 'open_earable_v2_audio_response_manager.dart';
2831
import 'wearable.dart';
2932
import '../../fota/firmware_slot_manager_impl.dart';
3033

@@ -116,6 +119,17 @@ class OpenEarableFactory extends WearableFactory {
116119
),
117120
);
118121
}
122+
if (await bleManager!.hasService(
123+
deviceId: device.id,
124+
serviceId: AudioResponseBleUuids.serviceUuid,
125+
)) {
126+
wearable.registerCapability<AudioResponseManager>(
127+
OpenEarableV2AudioResponseManager(
128+
bleManager: bleManager!,
129+
deviceId: device.id,
130+
),
131+
);
132+
}
119133
if (await bleManager!.hasService(
120134
deviceId: device.id,
121135
serviceId: mcuMgrSmpServiceUuid,

0 commit comments

Comments
 (0)