Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Unreleased

* added audio response capability support for OpenEarable V2 devices.
* added audio response upload progress reporting.
* BREAKING CHANGE: `BleGattManager.subscribe` now returns `Future<Stream<List<int>>>`, so callers must `await` subscription setup before listening to BLE notifications.
* BREAKING CHANGE: `SensorHandler.subscribeToSensorData` now returns `Future<Stream<Map<String, dynamic>>>`, so callers must `await` sensor notification readiness before listening to sensor data.
* fixed BLE notification setup races by ensuring subscription futures complete only after the underlying GATT notification subscription is enabled.
Expand Down
10 changes: 9 additions & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,15 @@ packages:
path: ".."
relative: true
source: path
version: "2.3.9"
version: "2.3.10"
open_earable_protocols:
dependency: transitive
description:
name: open_earable_protocols
sha256: c11cae4914827c1d7617d44647a5c49304f0c2a2c02c907c595682cfade6ddb2
url: "https://pub.dev"
source: hosted
version: "0.0.2"
package_config:
dependency: transitive
description:
Expand Down
4 changes: 4 additions & 0 deletions lib/open_earable_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import 'src/models/devices/discovered_device.dart';
import 'src/models/devices/open_ring_factory.dart';
import 'src/models/devices/wearable.dart';

export 'package:open_earable_protocols/open_earable_protocols.dart'
show AudioResponseConfig, AudioResponseResult;

export 'src/models/devices/discovered_device.dart';
export 'src/models/devices/wearable.dart';
export 'src/models/devices/cosinuss_one.dart';
Expand Down Expand Up @@ -71,6 +74,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';

Expand Down
4 changes: 4 additions & 0 deletions lib/src/managers/ble_gatt_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ abstract class BleGattManager {
});

/// Writes byte data to a specific characteristic of a device.
///
/// Set [withoutResponse] when the characteristic only supports writes without
/// response or when an acknowledged write is not required.
Future<void> write({
required String deviceId,
required String serviceId,
required String characteristicId,
required List<int> byteData,
bool withoutResponse = false,
});

/// Subscribes to a specific characteristic of the connected device.
Expand Down
2 changes: 2 additions & 0 deletions lib/src/managers/ble_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ class BleManager extends BleGattManager {
required String serviceId,
required String characteristicId,
required List<int> byteData,
bool withoutResponse = false,
}) async {
if (!isConnected(deviceId)) {
throw Exception("Write failed because no Earable is connected");
Expand All @@ -337,6 +338,7 @@ class BleManager extends BleGattManager {
serviceId,
characteristicId,
Uint8List.fromList(byteData),
withoutResponse: withoutResponse,
);
}

Expand Down
85 changes: 85 additions & 0 deletions lib/src/models/capabilities/audio_response_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'package:open_earable_protocols/open_earable_protocols.dart';

/// Receives progress updates for an audio buffer upload.
typedef AudioResponseUploadProgressCallback = void Function(
AudioResponseUploadProgress progress,
);

/// Current phase of an audio buffer upload.
enum AudioResponseUploadPhase {
/// The transfer is being initialized on the device.
starting,

/// Audio samples are being transferred and acknowledged by the device.
uploading,

/// All samples are acknowledged and the transfer is being committed.
committing,

/// The device successfully committed the transfer.
completed,
}

/// Immutable progress reported while uploading an audio buffer.
class AudioResponseUploadProgress {
/// Creates an audio buffer upload progress update.
const AudioResponseUploadProgress({
required this.phase,
required this.acknowledgedSamples,
required this.totalSamples,
});

/// Current transfer phase.
final AudioResponseUploadPhase phase;

/// Number of samples acknowledged by the device.
final int acknowledgedSamples;

/// Total number of samples in the transfer.
final int totalSamples;

/// Fraction of samples acknowledged by the device, between zero and one.
double get fraction =>
totalSamples == 0 ? 0 : acknowledgedSamples / totalSamples;
}

/// An interface for managing audio response measurements.
abstract class AudioResponseManager {
/// Uploads and commits signed 16-bit PCM [samples] for later measurements.
///
/// [maximumSamplesPerChunk] must fit the effective GATT write payload.
/// [onProgress] receives synchronous updates based on sample offsets
/// acknowledged by the device. Exceptions thrown by the callback terminate
/// the upload.
Future<void> uploadAudioBuffer({
required int transferId,
required List<int> samples,
required int samplingRate,
int maximumSamplesPerChunk = 118,
AudioResponseUploadProgressCallback? onProgress,
});

/// Starts a measurement and returns its protocol result.
///
/// The [config] must reference a buffer previously committed by
/// [uploadAudioBuffer].
Future<AudioResponseResult> measureAudioResponse(AudioResponseConfig config);
}

/// Error reported by the device while transferring an audio response buffer.
class AudioResponseTransferException implements Exception {
/// Creates an audio response transfer error from a protocol status value.
const AudioResponseTransferException({
required this.status,
required this.message,
});

/// Numeric status reported by the audio response protocol.
final int status;

/// Human-readable description of [status].
final String message;

@override
String toString() => 'AudioResponseTransferException($status): $message';
}
14 changes: 14 additions & 0 deletions lib/src/models/devices/open_earable_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import 'package:open_earable_flutter/src/managers/sensor_handler.dart';
import 'package:open_earable_flutter/src/models/wearable_factory.dart';
import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/sensor_scheme_reader.dart';
import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/v2_sensor_scheme_reader.dart';
import 'package:open_earable_protocols/open_earable_protocols.dart';
import 'package:universal_ble/universal_ble.dart';

import '../../../open_earable_flutter.dart' show logger;
import '../../constants.dart';
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/power_saving_mode_manager.dart';
Expand All @@ -25,6 +27,7 @@ import '../capabilities/time_synchronizable.dart';
import 'discovered_device.dart';
import 'open_earable_v1.dart';
import 'open_earable_v2.dart';
import 'open_earable_v2_audio_response_manager.dart';
import 'wearable.dart';
import '../../fota/firmware_slot_manager_impl.dart';

Expand Down Expand Up @@ -116,6 +119,17 @@ class OpenEarableFactory extends WearableFactory {
),
);
}
if (await bleManager!.hasService(
deviceId: device.id,
serviceId: AudioResponseBleUuids.serviceUuid,
)) {
wearable.registerCapability<AudioResponseManager>(
OpenEarableV2AudioResponseManager(
bleManager: bleManager!,
deviceId: device.id,
),
);
}
if (await bleManager!.hasService(
deviceId: device.id,
serviceId: mcuMgrSmpServiceUuid,
Expand Down
Loading
Loading