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
13 changes: 13 additions & 0 deletions lib/src/managers/ble_gatt_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ abstract class BleGattManager {
required String characteristicId,
});

/// Ensures notifications are enabled before request/response flows depend on them.
Future<void> prepareSubscription({
required String deviceId,
required String serviceId,
required String characteristicId,
}) async {
subscribe(
deviceId: deviceId,
serviceId: serviceId,
characteristicId: characteristicId,
);
}
Comment on lines +33 to +44

/// Reads data from a specific characteristic of the connected device.
Future<List<int>> read({
required String deviceId,
Expand Down
73 changes: 59 additions & 14 deletions lib/src/managers/ble_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class BleManager extends BleGattManager {
int get mtu => _mtu;

final Map<String, StreamController<List<int>>> _streamControllers = {};
final Map<String, Future<void>> _subscriptionSetups = {};

/// A stream of discovered devices during scanning.
StreamController<DiscoveredDevice>? _scanStreamController;
Expand All @@ -24,6 +25,35 @@ class BleManager extends BleGattManager {
String _getCharacteristicKey(String deviceId, String characteristicId) =>
"$deviceId||$characteristicId";

StreamController<List<int>> _ensureStreamController(String streamIdentifier) {
return _streamControllers.putIfAbsent(
streamIdentifier,
StreamController<List<int>>.broadcast,
);
}

Future<void> _setupSubscription({
required String deviceId,
required String serviceId,
required String characteristicId,
}) {
final streamIdentifier = _getCharacteristicKey(deviceId, characteristicId);
_ensureStreamController(streamIdentifier);

return _subscriptionSetups.putIfAbsent(streamIdentifier, () async {
try {
await UniversalBle.subscribeNotifications(
deviceId,
serviceId,
characteristicId,
);
} catch (error) {
_subscriptionSetups.remove(streamIdentifier);
rethrow;
}
});
}

final Map<String, Completer> _connectionCompleters = {};
final Map<String, VoidCallback> _connectCallbacks = {};
final Map<String, VoidCallback> _disconnectCallbacks = {};
Expand All @@ -48,6 +78,7 @@ class BleManager extends BleGattManager {

for (final key in keys) {
logger.d("Closing stream for $key due to device disconnection");
_subscriptionSetups.remove(key);
_streamControllers.remove(key)?.close();
}
}
Expand Down Expand Up @@ -324,33 +355,47 @@ class BleManager extends BleGattManager {
deviceId,
characteristicId,
);
StreamController<List<int>>? streamController =
_streamControllers[streamIdentifier];
streamController ??= StreamController<List<int>>.broadcast();
if (!_streamControllers.containsKey(streamIdentifier)) {
UniversalBle.subscribeNotifications(
deviceId,
serviceId,
characteristicId,
final streamController = _ensureStreamController(streamIdentifier);
if (!_subscriptionSetups.containsKey(streamIdentifier)) {
unawaited(
_setupSubscription(
deviceId: deviceId,
serviceId: serviceId,
characteristicId: characteristicId,
).catchError((Object error, StackTrace stackTrace) {
logger.e(
"Failed to subscribe to $streamIdentifier: $error\n$stackTrace",
);
}),
);
_streamControllers[streamIdentifier] = streamController;
}

streamController.onCancel = () {
if (_streamControllers.containsKey(streamIdentifier)) {
_subscriptionSetups.remove(streamIdentifier);
_streamControllers.remove(streamIdentifier)?.close();
UniversalBle.unsubscribe(
deviceId,
serviceId,
characteristicId,
unawaited(
UniversalBle.unsubscribe(deviceId, serviceId, characteristicId),
);
_streamControllers.remove(streamIdentifier);
}
};

return streamController.stream;
}

@override
Future<void> prepareSubscription({
required String deviceId,
required String serviceId,
required String characteristicId,
}) {
return _setupSubscription(
deviceId: deviceId,
serviceId: serviceId,
characteristicId: characteristicId,
);
}

/// Reads data from a specific characteristic of the connected Earable device.
@override
Future<List<int>> read({
Expand Down
123 changes: 94 additions & 29 deletions lib/src/utils/sensor_scheme_parser/v2_sensor_scheme_reader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,14 @@ class V2SensorSchemeReader extends SensorSchemeReader {
return _sensorSchemes[sensorId]!;
}

// Listen to the notification of the characteristic
await _bleManager.prepareSubscription(
deviceId: _deviceId,
serviceId: parseInfoServiceUuid,
characteristicId: sensorSchemeCharacteristicUuid,
);

// Listen only after notifications are enabled, otherwise some Android
// stacks can drop the first response packet.
final Stream<List<int>> stream = _bleManager.subscribe(
deviceId: _deviceId,
serviceId: parseInfoServiceUuid,
Comment on lines 70 to 72
Expand All @@ -83,22 +90,26 @@ class V2SensorSchemeReader extends SensorSchemeReader {
"Received notification for sensor scheme of sensor $sensorId: $value",
);

final scheme = _parseSensorScheme(value);
if (scheme.sensorId == 0 && sensorId != 0) {
logger.w(
"Sensor scheme response for sensor $sensorId omitted the sensor id. Using the requested id.",
);
scheme.sensorId = sensorId;
} else if (scheme.sensorId != sensorId) {
logger.w(
"Sensor scheme response for sensor $sensorId reported sensor id ${scheme.sensorId}. Using the returned scheme.",
);
}

_sensorSchemes[scheme.sensorId] = scheme;
return scheme;
return _storeParsedScheme(
requestedSensorId: sensorId,
rawScheme: value,
source: 'notification',
);
} on TimeoutException catch (e) {
throw TimeoutException("Timeout while waiting for sensor scheme: $e");
logger.w(
"Notification timeout while waiting for sensor scheme $sensorId: $e. "
"Falling back to direct characteristic read.",
);
final polledValue = await _bleManager.read(
deviceId: _deviceId,
serviceId: parseInfoServiceUuid,
characteristicId: sensorSchemeCharacteristicUuid,
);
return _storeParsedScheme(
requestedSensorId: sensorId,
rawScheme: polledValue,
source: 'direct read',
);
}
}

Expand All @@ -107,11 +118,23 @@ class V2SensorSchemeReader extends SensorSchemeReader {
if (_sensorIds.isEmpty || forceRead) {
await _readSensorIds();
}
if (forceRead) {
_sensorSchemes.clear();
}

for (int sensorId in _sensorIds) {
if (!_sensorSchemes.containsKey(sensorId) || forceRead) {
final int maxPasses = _sensorIds.length;
for (int pass = 0; pass < maxPasses; pass++) {
final missingSensorIds = _sensorIds
.where((sensorId) => !_sensorSchemes.containsKey(sensorId))
.toList();
if (missingSensorIds.isEmpty) {
break;
}

final int schemesBeforePass = _sensorSchemes.length;
for (final sensorId in missingSensorIds) {
try {
SensorScheme scheme = await getSchemeForSensor(sensorId);
final scheme = await getSchemeForSensor(sensorId);
_sensorSchemes[scheme.sensorId] = scheme;
} catch (e) {
Comment on lines 136 to 139
logger.e(
Expand All @@ -123,10 +146,16 @@ class V2SensorSchemeReader extends SensorSchemeReader {
"This may be a BLE notification timeout or subscription issue.",
);
}
// Continue with next sensor instead of failing entirely
continue;
}
}

if (_sensorSchemes.length == schemesBeforePass) {
logger.w(
"Sensor scheme read made no progress on pass ${pass + 1}. "
"Stopping with ${_sensorSchemes.length}/${_sensorIds.length} scheme(s).",
);
break;
}
}

logger.d(
Expand All @@ -142,23 +171,31 @@ class V2SensorSchemeReader extends SensorSchemeReader {

int nameLength = byteStream[currentIndex++];

List<int> nameBytes =
byteStream.sublist(currentIndex, currentIndex + nameLength);
List<int> nameBytes = byteStream.sublist(
currentIndex,
currentIndex + nameLength,
);
String sensorName = utf8.decode(nameBytes);
currentIndex += nameLength;

int componentCount = byteStream[currentIndex++];

SensorScheme sensorScheme =
SensorScheme(sensorId, sensorName, componentCount, null);
SensorScheme sensorScheme = SensorScheme(
sensorId,
sensorName,
componentCount,
null,
);

for (int j = 0; j < componentCount; j++) {
int componentType = byteStream[currentIndex++];

int groupNameLength = byteStream[currentIndex++];

List<int> groupNameBytes =
byteStream.sublist(currentIndex, currentIndex + groupNameLength);
List<int> groupNameBytes = byteStream.sublist(
currentIndex,
currentIndex + groupNameLength,
);
String groupName = utf8.decode(groupNameBytes);
currentIndex += groupNameLength;

Expand All @@ -173,8 +210,10 @@ class V2SensorSchemeReader extends SensorSchemeReader {

int unitNameLength = byteStream[currentIndex++];

List<int> unitNameBytes =
byteStream.sublist(currentIndex, currentIndex + unitNameLength);
List<int> unitNameBytes = byteStream.sublist(
currentIndex,
currentIndex + unitNameLength,
);
String unitName = utf8.decode(unitNameBytes);
currentIndex += unitNameLength;

Expand Down Expand Up @@ -223,4 +262,30 @@ class V2SensorSchemeReader extends SensorSchemeReader {

return sensorScheme;
}

SensorScheme _storeParsedScheme({
required int requestedSensorId,
required List<int> rawScheme,
required String source,
}) {
logger.d(
"Received $source for sensor scheme of sensor $requestedSensorId: $rawScheme",
);

final scheme = _parseSensorScheme(rawScheme);
final bool canAssumeMissingId = source == 'notification';
if (canAssumeMissingId && scheme.sensorId == 0 && requestedSensorId != 0) {
logger.w(
"Sensor scheme response for sensor $requestedSensorId omitted the sensor id. Using the requested id.",
);
scheme.sensorId = requestedSensorId;
} else if (scheme.sensorId != requestedSensorId) {
logger.w(
"Sensor scheme response for sensor $requestedSensorId reported sensor id ${scheme.sensorId}. Using the returned scheme.",
);
}

_sensorSchemes[scheme.sensorId] = scheme;
return scheme;
}
}
Loading
Loading