Skip to content
Merged
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
40 changes: 26 additions & 14 deletions lib/src/models/devices/open_earable_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ import 'wearable.dart';
import '../../fota/firmware_slot_manager_impl.dart';

const String _deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468";
const String _deviceFirmwareVersionCharacteristicUuid =
const String _deviceHardwareVersionCharacteristicUuid =
"45622512-6468-465a-b141-0b9b0f96b468";
const String _deviceFirmwareVersionCharacteristicUuid =
"45622513-6468-465a-b141-0b9b0f96b468";

class OpenEarableFactory extends WearableFactory {
final _v1Regex = RegExp(r'^1\.\d+\.\d+$');
Expand All @@ -58,13 +60,16 @@ class OpenEarableFactory extends WearableFactory {
logger.d("'$device' has no service matching '$_deviceInfoServiceUuid'");
return false;
}
String firmwareVersion = await _getFirmwareVersion(device);
logger.d("Firmware Version: '$firmwareVersion'");
final hardwareVersion = await _readVersion(
device,
_deviceHardwareVersionCharacteristicUuid,
);
logger.d("Hardware Version: '$hardwareVersion'");

logger.t("matches V2: ${_v2Regex.hasMatch(firmwareVersion)}");
logger.t("matches V2: ${_v2Regex.hasMatch(hardwareVersion)}");

return _v1Regex.hasMatch(firmwareVersion) ||
_v2Regex.hasMatch(firmwareVersion);
return _v1Regex.hasMatch(hardwareVersion) ||
_v2Regex.hasMatch(hardwareVersion);
}

@override
Expand All @@ -80,18 +85,23 @@ class OpenEarableFactory extends WearableFactory {
"disconnectNotifier needs to be set before using the factory",
);
}
String firmwareVersion = await _getFirmwareVersion(device);
final hardwareVersion = await _readVersion(
device,
_deviceHardwareVersionCharacteristicUuid,
);

if (_v1Regex.hasMatch(firmwareVersion)) {
if (_v1Regex.hasMatch(hardwareVersion)) {
return OpenEarableV1(
name: device.name,
disconnectNotifier: disconnectNotifier!,
bleManager: bleManager!,
discoveredDevice: device,
);
} else if (_v2Regex.hasMatch(firmwareVersion)) {
(List<Sensor>, List<SensorConfiguration>) sensorInfo =
await _initSensors(device, firmwareVersion);
} else if (_v2Regex.hasMatch(hardwareVersion)) {
(List<Sensor>, List<SensorConfiguration>) sensorInfo = await _initSensors(
device,
await _readVersion(device, _deviceFirmwareVersionCharacteristicUuid),
);
final wearable = OpenEarableV2(
name: device.name,
disconnectNotifier: disconnectNotifier!,
Expand Down Expand Up @@ -172,13 +182,15 @@ class OpenEarableFactory extends WearableFactory {
}
}

Future<String> _getFirmwareVersion(DiscoveredDevice device) async {
Future<String> _readVersion(
DiscoveredDevice device,
String characteristicId,
) async {
List<int> softwareGenerationBytes = await bleManager!.read(
deviceId: device.id,
serviceId: _deviceInfoServiceUuid,
characteristicId: _deviceFirmwareVersionCharacteristicUuid,
characteristicId: characteristicId,
);
logger.d("Raw Firmware Version: $softwareGenerationBytes");
int firstZeroIndex = softwareGenerationBytes.indexOf(0);
if (firstZeroIndex != -1) {
softwareGenerationBytes =
Expand Down
135 changes: 135 additions & 0 deletions test/factory_transport_version_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:open_earable_flutter/open_earable_flutter.dart';
import 'package:open_earable_flutter/src/constants.dart';
import 'package:open_earable_flutter/src/models/devices/open_earable_factory.dart';
import 'package:universal_ble/universal_ble.dart';

const hardwareUuid = '45622512-6468-465a-b141-0b9b0f96b468';
const firmwareUuid = '45622513-6468-465a-b141-0b9b0f96b468';

class VersionGatt extends Fake implements BleGattManager {
String firmware = '2.2.9';
final reads = <String>[];
final schemes = StreamController<List<int>>.broadcast();
final data = StreamController<List<int>>.broadcast();
Completer<void> subscribed = Completer<void>();

@override
bool isConnected(String deviceId) => true;

@override
Future<bool> hasService({
required String deviceId,
required String serviceId,
}) async =>
false;

@override
Future<bool> hasCharacteristic({
required String deviceId,
required String serviceId,
required String characteristicId,
}) async =>
false;

@override
Future<List<int>> read({
required String deviceId,
required String serviceId,
required String characteristicId,
}) async {
reads.add(characteristicId);
if (characteristicId == hardwareUuid) return '2.0.1\x00'.codeUnits;
if (characteristicId == firmwareUuid) return '$firmware\x00'.codeUnits;
if (characteristicId == sensorListCharacteristicUuid) return [1, 4];
throw StateError('Unexpected read: $characteristicId');
}

@override
Future<Stream<List<int>>> subscribe({
required String deviceId,
required String serviceId,
required String characteristicId,
}) async {
if (characteristicId == sensorSchemeCharacteristicUuid) {
return schemes.stream;
}
expect(characteristicId, sensorDataCharacteristicUuid);
subscribed.complete();
return data.stream;
}

@override
Future<void> write({
required String deviceId,
required String serviceId,
required String characteristicId,
required List<int> byteData,
bool withoutResponse = false,
}) async {
expect(characteristicId, requestSensorSchemeCharacteristicUuid);
List<int> text(String s) => [s.length, ...s.codeUnits];
schemes.add([
4, ...text('PPG'), 4,
for (final axis in ['RED', 'IR', 'GREEN', 'AMBIENT']) ...[
5,
...text('PPG'),
...text(axis),
...text('ADC'),
],
1, // Streaming, with no optional frequency table.
]);
}
}

void main() {
test('real factory selects transport from firmware and refreshes after FOTA',
() async {
final ble = VersionGatt();
addTearDown(ble.schemes.close);
addTearDown(ble.data.close);
final factory = OpenEarableFactory()
..bleManager = ble
..disconnectNotifier = WearableDisconnectNotifier();
final device = DiscoveredDevice(
id: 'ear',
name: 'Ear',
manufacturerData: Uint8List(0),
rssi: -40,
serviceUuids: [],
);
for (final version in ['2.2.9', '2.3.0', '2.3.0-dev.91+gabc', '2.2.9']) {
ble.firmware = version;
ble.reads.clear();
ble.subscribed = Completer<void>();
expect(
await factory.matches(device, [
BleService(
OpenEarableV2.deviceInfoServiceUuid,
[],
),
]),
isTrue,
);
final wearable = await factory.createFromDevice(device);
expect(ble.reads, contains(firmwareUuid));
final sensor = wearable.requireCapability<SensorManager>().sensors.single;
expect(sensor.axisUnits, ['ADC', 'ADC', 'ADC', 'ADC']);
final value = sensor.sensorStream.first;
await ble.subscribed.future;
await Future<void>.delayed(Duration.zero);
final compact = version.startsWith('2.3.');
final sample = compact
? [1, 0, 16, 0, 192, 0, 0, 8, 0, 0]
: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0];
ble.data.add([4, sample.length, 123, 0, 0, 0, 0, 0, 0, 0, ...sample]);
final parsed =
await value.timeout(const Duration(seconds: 2)) as SensorDoubleValue;
expect(parsed.values, [1.0, 2.0, 3.0, 4.0]);
expect(parsed.timestamp, 123);
}
});
}
Loading