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
1 change: 1 addition & 0 deletions lib/open_earable_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export 'src/models/capabilities/battery_health_status.dart';
export 'src/models/capabilities/battery_energy_status.dart';
export 'src/models/capabilities/rgb_led.dart';
export 'src/models/capabilities/status_led.dart';
export 'src/models/capabilities/led_state_reader.dart';
export 'src/models/capabilities/sensor.dart';
export 'src/models/capabilities/sensor_specializations/heart_rate_sensor.dart';
export 'src/models/capabilities/sensor_specializations/heart_rate_variability_sensor.dart';
Expand Down
20 changes: 20 additions & 0 deletions lib/src/models/capabilities/led_state_reader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// Optional readback of the LED override selected on a device.
abstract class LedStateReader {
Future<LedState> readLedState();
}

class LedState {
final bool showStatus;
final int red;
final int green;
final int blue;

const LedState({
required this.showStatus,
required this.red,
required this.green,
required this.blue,
});

bool get isBlack => red == 0 && green == 0 && blue == 0;
}
24 changes: 23 additions & 1 deletion lib/src/models/devices/open_earable_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/sensor_schem
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 'package:pub_semver/pub_semver.dart' as semver;

import '../../../open_earable_flutter.dart' show logger;
import '../../constants.dart';
Expand All @@ -17,6 +18,7 @@ import '../capabilities/audio_response_manager.dart';
import '../capabilities/fota_capability.dart';
import '../capabilities/fota_slot_info_capability.dart';
import '../capabilities/microphone_gain_manager.dart';
import '../capabilities/led_state_reader.dart';
import '../capabilities/power_saving_mode_manager.dart';
import '../capabilities/sensor.dart';
import '../capabilities/sensor_configuration.dart';
Expand All @@ -30,6 +32,7 @@ import 'open_earable_v1.dart';
import 'open_earable_v2.dart';
import 'open_earable_v2_audio_response_manager.dart';
import 'open_earable_v2_microphone_gain_manager.dart';
import 'open_earable_v2_led_state_reader.dart';
import 'wearable.dart';
import '../../fota/firmware_slot_manager_impl.dart';

Expand Down Expand Up @@ -98,9 +101,11 @@ class OpenEarableFactory extends WearableFactory {
discoveredDevice: device,
);
} else if (_v2Regex.hasMatch(hardwareVersion)) {
final firmwareVersion =
await _readVersion(device, _deviceFirmwareVersionCharacteristicUuid);
(List<Sensor>, List<SensorConfiguration>) sensorInfo = await _initSensors(
device,
await _readVersion(device, _deviceFirmwareVersionCharacteristicUuid),
firmwareVersion,
);
final wearable = OpenEarableV2(
name: device.name,
Expand All @@ -120,6 +125,14 @@ class OpenEarableFactory extends WearableFactory {
},
isConnectedViaSystem: options.contains(const ConnectedViaSystem()),
);
if (_supportsLedReadback(firmwareVersion)) {
wearable.registerCapability<LedStateReader>(
OpenEarableV2LedStateReader(
bleManager: bleManager!,
deviceId: device.id,
),
);
}
if (await bleManager!.hasService(
deviceId: device.id,
serviceId: timeSynchronizationServiceUuid,
Expand Down Expand Up @@ -199,6 +212,15 @@ class OpenEarableFactory extends WearableFactory {
return String.fromCharCodes(softwareGenerationBytes);
}

bool _supportsLedReadback(String firmwareVersion) {
try {
final version = semver.Version.parse(firmwareVersion.trim());
return version.major == 2 && version.minor >= 3;
} on FormatException {
return false;
}
}

Future<bool> _hasPowerSavingService(DiscoveredDevice device) async {
return await bleManager!.hasCharacteristic(
deviceId: device.id,
Expand Down
40 changes: 40 additions & 0 deletions lib/src/models/devices/open_earable_v2_led_state_reader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import '../../managers/ble_gatt_manager.dart';
import '../capabilities/led_state_reader.dart';

/// LED readback added in OpenEarable firmware 2.3.0.
class OpenEarableV2LedStateReader implements LedStateReader {
static const serviceUuid = '81040a2e-4819-11ee-be56-0242ac120002';
static const colorUuid = '81040e7a-4819-11ee-be56-0242ac120002';
static const modeUuid = '81040e7b-4819-11ee-be56-0242ac120002';

final BleGattManager bleManager;
final String deviceId;

const OpenEarableV2LedStateReader({
required this.bleManager,
required this.deviceId,
});

@override
Future<LedState> readLedState() async {
final mode = await bleManager.read(
deviceId: deviceId,
serviceId: serviceUuid,
characteristicId: modeUuid,
);
final color = await bleManager.read(
deviceId: deviceId,
serviceId: serviceUuid,
characteristicId: colorUuid,
);
if (mode.length != 1 || mode[0] > 1 || color.length != 3) {
throw const FormatException('Invalid LED state');
}
return LedState(
showStatus: mode[0] == 0,
red: color[0],
green: color[1],
blue: color[2],
);
}
}
4 changes: 4 additions & 0 deletions test/factory_transport_version_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ void main() {
);
final wearable = await factory.createFromDevice(device);
expect(ble.reads, contains(firmwareUuid));
expect(
wearable.hasCapability<LedStateReader>(),
version.startsWith('2.3.'),
);
final sensor = wearable.requireCapability<SensorManager>().sensors.single;
expect(sensor.axisUnits, ['ADC', 'ADC', 'ADC', 'ADC']);
final value = sensor.sensorStream.first;
Expand Down
56 changes: 56 additions & 0 deletions test/led_state_reader_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:open_earable_flutter/open_earable_flutter.dart';
import 'package:open_earable_flutter/src/models/devices/open_earable_v2_led_state_reader.dart';

class LedGatt extends Fake implements BleGattManager {
List<int> mode = [1];
List<int> color = [12, 34, 56];

@override
Future<List<int>> read({
required String deviceId,
required String serviceId,
required String characteristicId,
}) async {
expect(deviceId, 'ear');
expect(serviceId, OpenEarableV2LedStateReader.serviceUuid);
if (characteristicId == OpenEarableV2LedStateReader.modeUuid) return mode;
expect(characteristicId, OpenEarableV2LedStateReader.colorUuid);
return color;
}
}

void main() {
test('reads manual color, disabled output and automatic status', () async {
final ble = LedGatt();
final reader =
OpenEarableV2LedStateReader(bleManager: ble, deviceId: 'ear');
var state = await reader.readLedState();
expect(state.showStatus, isFalse);
expect([state.red, state.green, state.blue], [12, 34, 56]);
expect(state.isBlack, isFalse);
ble.color = [0, 0, 0];
state = await reader.readLedState();
expect(state.isBlack, isTrue);
ble.mode = [0];
expect((await reader.readLedState()).showStatus, isTrue);
});

test('rejects malformed state instead of displaying a guessed mode',
() async {
final ble = LedGatt();
final reader =
OpenEarableV2LedStateReader(bleManager: ble, deviceId: 'ear');
for (final mode in <List<int>>[
[],
[0, 1],
[2],
]) {
ble.mode = mode;
await expectLater(reader.readLedState(), throwsFormatException);
}
ble.mode = [1];
ble.color = [1, 2];
await expectLater(reader.readLedState(), throwsFormatException);
});
}
Loading