From 07456abb5ebf5e54255d84342cd3f4c643875dd7 Mon Sep 17 00:00:00 2001 From: TobiasRoeddiger Date: Sun, 4 Oct 2026 23:08:48 +0200 Subject: [PATCH] Select BLE sample format from firmware instead of hardware version --- .../models/devices/open_earable_factory.dart | 40 ++++-- test/factory_transport_version_test.dart | 135 ++++++++++++++++++ 2 files changed, 161 insertions(+), 14 deletions(-) create mode 100644 test/factory_transport_version_test.dart diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index 832d505..bc31ec6 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -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+$'); @@ -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 @@ -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, List) sensorInfo = - await _initSensors(device, firmwareVersion); + } else if (_v2Regex.hasMatch(hardwareVersion)) { + (List, List) sensorInfo = await _initSensors( + device, + await _readVersion(device, _deviceFirmwareVersionCharacteristicUuid), + ); final wearable = OpenEarableV2( name: device.name, disconnectNotifier: disconnectNotifier!, @@ -172,13 +182,15 @@ class OpenEarableFactory extends WearableFactory { } } - Future _getFirmwareVersion(DiscoveredDevice device) async { + Future _readVersion( + DiscoveredDevice device, + String characteristicId, + ) async { List 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 = diff --git a/test/factory_transport_version_test.dart b/test/factory_transport_version_test.dart new file mode 100644 index 0000000..74c8664 --- /dev/null +++ b/test/factory_transport_version_test.dart @@ -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 = []; + final schemes = StreamController>.broadcast(); + final data = StreamController>.broadcast(); + Completer subscribed = Completer(); + + @override + bool isConnected(String deviceId) => true; + + @override + Future hasService({ + required String deviceId, + required String serviceId, + }) async => + false; + + @override + Future hasCharacteristic({ + required String deviceId, + required String serviceId, + required String characteristicId, + }) async => + false; + + @override + Future> 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>> 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 write({ + required String deviceId, + required String serviceId, + required String characteristicId, + required List byteData, + bool withoutResponse = false, + }) async { + expect(characteristicId, requestSensorSchemeCharacteristicUuid); + List 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(); + expect( + await factory.matches(device, [ + BleService( + OpenEarableV2.deviceInfoServiceUuid, + [], + ), + ]), + isTrue, + ); + final wearable = await factory.createFromDevice(device); + expect(ble.reads, contains(firmwareUuid)); + final sensor = wearable.requireCapability().sensors.single; + expect(sensor.axisUnits, ['ADC', 'ADC', 'ADC', 'ADC']); + final value = sensor.sensorStream.first; + await ble.subscribed.future; + await Future.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); + } + }); +}