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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased

* added dynamic power saving mode capability for OpenEarable v2 devices

Comment thread
DennisMoschina marked this conversation as resolved.
## 2.3.7

* added erase firmware image slot function for FOTA slot info capability
Expand Down
16 changes: 16 additions & 0 deletions doc/CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,22 @@ if (audioModeManager != null) {

---

### PowerSavingModeManager

Reads the power saving modes that the firmware currently supports, including
their display names, and applies the selected mode.

```dart
final powerSaving = wearable.getCapability<PowerSavingModeManager>();
if (powerSaving != null) {
final modes = await powerSaving.readSupportedPowerSavingModes();
final currentMode = await powerSaving.readPowerSavingMode();
await powerSaving.setPowerSavingMode(modes.first);
}
```

---

### ℹ️ Device Information Capabilities

#### DeviceFirmwareVersion
Expand Down
1 change: 1 addition & 0 deletions lib/open_earable_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export 'src/models/capabilities/audio_player_controls.dart';
export 'src/models/capabilities/storage_path_audio_player.dart';
export 'src/models/capabilities/audio_mode_manager.dart';
export 'src/models/capabilities/microphone_manager.dart';
export 'src/models/capabilities/power_saving_mode_manager.dart';
export 'src/models/capabilities/stereo_device.dart';
export 'src/models/recorder.dart';
export 'src/models/devices/stereo_pairing/pairing_rule.dart';
Expand Down
6 changes: 6 additions & 0 deletions lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ const String buttonStateCharacteristicUuid =

const String ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002";
const String ledSetStateCharacteristic = "81040e7a-4819-11ee-be56-0242ac120002";

const String powerSavingServiceUuid = "d63fd1f0-5f68-4ebb-a7c7-5e0fb9ae7557";
const String powerSavingModeCharacteristicUuid =
"d63fd1f1-5f68-4ebb-a7c7-5e0fb9ae7557";
const String powerSavingSupportedModesCharacteristicUuid =
"d63fd1f2-5f68-4ebb-a7c7-5e0fb9ae7557";
39 changes: 39 additions & 0 deletions lib/src/models/capabilities/power_saving_mode_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// Manages firmware-defined power saving modes.
///
/// Implementations read the supported mode list from the wearable so apps can
/// show newly added firmware modes without package or app changes.
abstract class PowerSavingModeManager {
/// Reads all power saving modes supported by the wearable.
Future<List<PowerSavingMode>> readSupportedPowerSavingModes();

/// Reads the currently selected power saving mode.
Future<PowerSavingMode> readPowerSavingMode();

/// Applies [mode] as the current power saving mode.
Future<void> setPowerSavingMode(PowerSavingMode mode);
}

/// A selectable firmware-defined power saving mode.
class PowerSavingMode {
/// Stable firmware-defined mode identifier.
final int id;

/// User-facing mode name supplied by the firmware.
final String name;

/// Creates a power saving mode.
const PowerSavingMode({required this.id, required this.name});

@override
bool operator ==(Object other) {
return other is PowerSavingMode && other.id == id;
}

@override
int get hashCode => id.hashCode;

@override
String toString() {
return name;
}
}
23 changes: 23 additions & 0 deletions lib/src/models/devices/open_earable_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/v2_sensor_sc
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/fota_capability.dart';
import '../capabilities/fota_slot_info_capability.dart';
import '../capabilities/power_saving_mode_manager.dart';
import '../capabilities/sensor.dart';
import '../capabilities/sensor_configuration.dart';
import '../capabilities/sensor_configuration_specializations/recordable_sensor_configuration.dart';
Expand Down Expand Up @@ -113,6 +115,14 @@ class OpenEarableFactory extends WearableFactory {
McuMgrFotaSlotInfoManager(deviceId: device.id),
);
}
if (await _hasPowerSavingService(device)) {
wearable.registerCapability<PowerSavingModeManager>(
OpenEarableV2PowerSavingManager(
bleManager: bleManager!,
deviceId: device.id,
),
);
}
return wearable;
} else {
throw Exception('OpenEarable version is not supported');
Expand All @@ -134,6 +144,19 @@ class OpenEarableFactory extends WearableFactory {
return String.fromCharCodes(softwareGenerationBytes);
}

Future<bool> _hasPowerSavingService(DiscoveredDevice device) async {
return await bleManager!.hasCharacteristic(
deviceId: device.id,
serviceId: powerSavingServiceUuid,
characteristicId: powerSavingModeCharacteristicUuid,
) &&
await bleManager!.hasCharacteristic(
deviceId: device.id,
serviceId: powerSavingServiceUuid,
characteristicId: powerSavingSupportedModesCharacteristicUuid,
);
}

Future<(List<Sensor>, List<SensorConfiguration>)> _initSensors(
DiscoveredDevice device,
) async {
Expand Down
115 changes: 115 additions & 0 deletions lib/src/models/devices/open_earable_v2.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:open_earable_flutter/src/constants.dart';
Expand Down Expand Up @@ -582,6 +583,120 @@ class OpenEarableV2PairingRule extends PairingRule<OpenEarableV2> {
}
}

// MARK: PowerSavingModeManager

/// OpenEarable V2 implementation of [PowerSavingModeManager].
class OpenEarableV2PowerSavingManager implements PowerSavingModeManager {
/// Creates a manager that talks to the OpenEarable V2 power saving service.
OpenEarableV2PowerSavingManager({
required this.bleManager,
required this.deviceId,
});

/// GATT manager used for BLE communication.
final BleGattManager bleManager;

/// Connected wearable id.
final String deviceId;

List<PowerSavingMode>? _supportedModesCache;

@override
Future<List<PowerSavingMode>> readSupportedPowerSavingModes() async {
final cachedModes = _supportedModesCache;
if (cachedModes != null) {
return cachedModes;
}

final bytes = await bleManager.read(
deviceId: deviceId,
serviceId: powerSavingServiceUuid,
characteristicId: powerSavingSupportedModesCharacteristicUuid,
);

final modes = _decodeSupportedModes(bytes);
_supportedModesCache = modes;
return modes;
}

@override
Future<PowerSavingMode> readPowerSavingMode() async {
final modeBytes = await bleManager.read(
deviceId: deviceId,
serviceId: powerSavingServiceUuid,
characteristicId: powerSavingModeCharacteristicUuid,
);

if (modeBytes.length != 1) {
throw StateError(
'Power saving mode characteristic expected 1 value, but got ${modeBytes.length}',
);
}

final modeId = modeBytes[0];
final supportedModes = await readSupportedPowerSavingModes();
for (final mode in supportedModes) {
if (mode.id == modeId) {
return mode;
}
}

throw StateError(
'Current power saving mode $modeId is not advertised as supported',
);
}

@override
Future<void> setPowerSavingMode(PowerSavingMode mode) {
if (mode.id < 0 || mode.id > 0xFF) {
throw RangeError.range(mode.id, 0, 0xFF, 'mode.id');
}

return bleManager.write(
deviceId: deviceId,
serviceId: powerSavingServiceUuid,
characteristicId: powerSavingModeCharacteristicUuid,
byteData: [mode.id],
);
}

List<PowerSavingMode> _decodeSupportedModes(List<int> bytes) {
if (bytes.isEmpty) {
throw StateError(
'Supported power saving modes characteristic is too short: ${bytes.length}',
);
}

final modeCount = bytes[0];
var offset = 1;
final modes = <PowerSavingMode>[];

for (var i = 0; i < modeCount; i++) {
if (offset + 2 > bytes.length) {
throw StateError(
'Supported power saving modes ended before mode header $i',
);
}

final modeId = bytes[offset++];
final nameLength = bytes[offset++];

if (offset + nameLength > bytes.length) {
throw StateError(
'Supported power saving mode $modeId has an incomplete name',
);
}

final name = utf8.decode(bytes.sublist(offset, offset + nameLength));
offset += nameLength;

modes.add(PowerSavingMode(id: modeId, name: name));
}

return List.unmodifiable(modes);
}
}

// MARK: OpenEarable Sync Time packet

enum _TimeSyncOperation {
Expand Down
Loading