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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ To get started with the OpenEarable Flutter package, follow these steps:
### 7. Access sensor data
In order to access sensor data, you need to check if the device is a `SensorManager`. Then you can access the sensor data streams by accessing the `sensors` property:
```dart
if (wearable is SensorManager) {
wearable.sensors.forEach((sensor) {
final sensorManager = wearable.getCapability<SensorManager>();
if (sensorManager != null) {
sensorManager.sensors.forEach((sensor) {
sensor.sensorStream.listen((data) {
// Handle sensor data
});
Expand All @@ -110,5 +111,8 @@ To get started with the OpenEarable Flutter package, follow these steps:

For most devices, the sensors have to be configured before they start sending data. You can learn more about configuring sensors in the chapter [Configuring Sensors](doc/SENSOR_CONFIG.md).

> [!WARNING]
> Checking for capabilities using `is <Capability>` is deprecated. Please use `hasCapability<T>()` instead. You can learn more about capabilities in the [Capabilities](doc/CAPABILITIES.md) documentation.

## Add custom Wearable Support
Learn more about how to add support for your own wearable devices in the [Adding Custom Wearable Support](doc/ADD_CUSTOM_WEARABLE.md) documentation.
82 changes: 54 additions & 28 deletions doc/CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@

Wearable functionality in the Open Earable Flutter package is modular and extensible through the use of **capabilities**. Capabilities are abstract interfaces that define specific features (like sensor access, battery info, button interaction, etc.). Each `Wearable` can implement any combination of these capabilities depending on its hardware and firmware support.

This guide outlines the most common capabilities and how to use them.
This guide outlines the most common capabilities and how to use them with the new capability lookup helpers.

Use `hasCapability<T>()` to check support and `getCapability<T>()` or `requireCapability<T>()` to fetch an instance:

```dart
if (wearable.hasCapability<SensorManager>()) {
final sensorManager = wearable.requireCapability<SensorManager>();
final sensors = sensorManager.sensors;
}
```

The difference between `getCapability<T>()` and `requireCapability<T>()` is that the latter throws an exception if the capability is not supported, while the former returns `null`.

> [!WARNING]
> The old way of checking capabilities using `is <Capability>` is deprecated. Please use `hasCapability<T>()` instead.

---

Expand All @@ -15,8 +29,9 @@ Some of the most commonly used capabilities include:
Enables access to available sensors on the wearable.

```dart
if (wearable is SensorManager) {
List<Sensor> sensors = wearable.sensors;
final sensorManager = wearable.getCapability<SensorManager>();
if (sensorManager != null) {
List<Sensor> sensors = sensorManager.sensors;
}
```

Expand All @@ -27,8 +42,9 @@ if (wearable is SensorManager) {
Allows configuration of the wearable’s sensors, including setting sampling rates or modes.

```dart
if (wearable is SensorConfigurationManager) {
List<SensorConfiguration> configurations = wearable.sensorConfigurations;
final configurationManager = wearable.getCapability<SensorConfigurationManager>();
if (configurationManager != null) {
List<SensorConfiguration> configurations = configurationManager.sensorConfigurations;
}
```

Expand All @@ -41,8 +57,9 @@ if (wearable is SensorConfigurationManager) {
Provides access to battery energy data.

```dart
if (wearable is BatteryEnergyStatusService) {
BatteryEnergyStatus status = await wearable.readEnergyStatus();
final energyStatusService = wearable.getCapability<BatteryEnergyStatusService>();
if (energyStatusService != null) {
BatteryEnergyStatus status = await energyStatusService.readEnergyStatus();
}
```

Expand All @@ -51,8 +68,9 @@ if (wearable is BatteryEnergyStatusService) {
Reads battery health and performance metrics.

```dart
if (wearable is BatteryHealthStatusService) {
BatteryHealthStatus healthStatus = await wearable.readHealthStatus();
final healthStatusService = wearable.getCapability<BatteryHealthStatusService>();
if (healthStatusService != null) {
BatteryHealthStatus healthStatus = await healthStatusService.readHealthStatus();
}
```

Expand All @@ -61,8 +79,9 @@ if (wearable is BatteryHealthStatusService) {
Gives the current battery level as a percentage or unit.

```dart
if (wearable is BatteryLevelStatusService) {
BatteryPowerStatus levelStatus = await wearable.readPowerStatus();
final levelStatusService = wearable.getCapability<BatteryLevelStatusService>();
if (levelStatusService != null) {
BatteryPowerStatus levelStatus = await levelStatusService.readPowerStatus();
}
```

Expand All @@ -73,8 +92,9 @@ if (wearable is BatteryLevelStatusService) {
Enables listening to hardware button events on the wearable.

```dart
if (wearable is ButtonManager) {
wearable.buttonEvents.listen((buttonEvent) {
final buttonManager = wearable.getCapability<ButtonManager>();
if (buttonManager != null) {
buttonManager.buttonEvents.listen((buttonEvent) {
// Handle button events
});
}
Expand All @@ -87,8 +107,9 @@ if (wearable is ButtonManager) {
Controls on-device recording. You can specify filename prefixes or manage session behaviors.

```dart
if (wearable is EdgeRecorderManager) {
wearable.setFilePrefix("my_recording");
final edgeRecorder = wearable.getCapability<EdgeRecorderManager>();
if (edgeRecorder != null) {
edgeRecorder.setFilePrefix("my_recording");
}
```

Expand All @@ -99,9 +120,10 @@ if (wearable is EdgeRecorderManager) {
Lets you select the active microphone (if the device has multiple).

```dart
if (wearable is MicrophoneManager) {
List<Microphone> microphones = wearable.availableMicrophones;
wearable.setMicrophone(microphones.first);
final microphoneManager = wearable.getCapability<MicrophoneManager>();
if (microphoneManager != null) {
List<Microphone> microphones = microphoneManager.availableMicrophones;
microphoneManager.setMicrophone(microphones.first);
}
```

Expand All @@ -112,9 +134,10 @@ if (wearable is MicrophoneManager) {
Allows switching between different audio modes (e.g., mono, stereo, streaming).

```dart
if (wearable is AudioModeManager) {
List<AudioMode> audioModes = wearable.availableAudioModes;
wearable.setAudioMode(audioModes.first);
final audioModeManager = wearable.getCapability<AudioModeManager>();
if (audioModeManager != null) {
List<AudioMode> audioModes = audioModeManager.availableAudioModes;
audioModeManager.setAudioMode(audioModes.first);
}
```

Expand All @@ -127,8 +150,9 @@ if (wearable is AudioModeManager) {
Reads the current firmware version of the device.

```dart
if (wearable is DeviceFirmwareVersion) {
String firmwareVersion = await wearable.readDeviceFirmwareVersion();
final firmwareVersionService = wearable.getCapability<DeviceFirmwareVersion>();
if (firmwareVersionService != null) {
String firmwareVersion = await firmwareVersionService.readDeviceFirmwareVersion();
}
```

Expand All @@ -137,8 +161,9 @@ if (wearable is DeviceFirmwareVersion) {
Reads the hardware version of the device.

```dart
if (wearable is DeviceHardwareVersion) {
String hardwareVersion = await wearable.readDeviceHardwareVersion();
final hardwareVersionService = wearable.getCapability<DeviceHardwareVersion>();
if (hardwareVersionService != null) {
String hardwareVersion = await hardwareVersionService.readDeviceHardwareVersion();
}
```

Expand All @@ -147,13 +172,14 @@ if (wearable is DeviceHardwareVersion) {
Retrieves the device’s unique ID.

```dart
if (wearable is DeviceIdentifier) {
String deviceId = await wearable.readDeviceIdentifier();
final deviceIdentifierService = wearable.getCapability<DeviceIdentifier>();
if (deviceIdentifierService != null) {
String deviceId = await deviceIdentifierService.readDeviceIdentifier();
}
```

---

## Summary

Capabilities are the building blocks of wearable functionality. You can dynamically check for and use any supported capability through simple type checks (`if (wearable is SomeCapability)`). This enables modular development and ensures your app only uses features supported by the connected device.
Capabilities are the building blocks of wearable functionality. Use `hasCapability<T>()` to check support and `getCapability<T>()` to access a capability instance. This enables modular development and ensures your app only uses features supported by the connected device.
5 changes: 3 additions & 2 deletions doc/SENSOR_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ To configure sensors, you first need to access the `SensorConfiguration` you wan
If you have a `Wearable` that implements `SensorConfigurationManager`, you can access the configurations like this:

```dart
if (wearable is SensorConfigurationManager) {
List<SensorConfiguration> configurations = wearable.sensorConfigurations;
final sensorConfigurationManager = wearable.getCapability<SensorConfigurationManager>();
if (sensorConfigurationManager != null) {
List<SensorConfiguration> configurations = sensorConfigurationManager.sensorConfigurations;
}
```

Expand Down
12 changes: 12 additions & 0 deletions lib/src/managers/ble_gatt_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ abstract class BleGattManager {
/// Check if a device is connected.
bool isConnected(String deviceId);

/// Checks if a specific service is available on the connected device.
Future<bool> hasService({
required String deviceId,
required String serviceId,
});

Future<bool> hasCharacteristic({
required String deviceId,
required String serviceId,
required String characteristicId,
});

/// Writes byte data to a specific characteristic of a device.
Future<void> write({
required String deviceId,
Expand Down
44 changes: 44 additions & 0 deletions lib/src/managers/ble_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,50 @@ class BleManager extends BleGattManager {
return completer.future;
}

/// Checks if the connected device has a specific service.
@override
Future<bool> hasService({
required String deviceId,
required String serviceId,
}) async {


if (!isConnected(deviceId)) {
throw Exception("Device is not connected");
}

List<BleService> services = await UniversalBle.discoverServices(deviceId);
for (final service in services) {
if (service.uuid.toLowerCase() == serviceId.toLowerCase()) {
return true;
}
}
return false;
}

/// Checks if the connected device has a specific characteristic.
@override
Future<bool> hasCharacteristic({
required String deviceId,
required String serviceId,
required String characteristicId,
}) async {
if (!isConnected(deviceId)) {
throw Exception("Device is not connected");
}
List<BleService> services = await UniversalBle.discoverServices(deviceId);
for (final service in services) {
if (service.uuid.toLowerCase() == serviceId.toLowerCase()) {
for (final characteristic in service.characteristics) {
if (characteristic.uuid.toLowerCase() == characteristicId.toLowerCase()) {
return true;
}
}
}
}
return false;
}

/// Writes byte data to a specific characteristic of the connected Earable device.
@override
Future<void> write({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'dart:async';
import 'dart:math';

import '../../../../open_earable_flutter.dart' show logger;
import '../../capabilities/battery_energy_status.dart';
import '../bluetooth_wearable.dart';

const String _batteryEnergyStatusCharacteristicUuid = "2BF0";
const String _batteryServiceUuid = "180F";

mixin BatteryEnergyStatusGattReader on BluetoothWearable implements BatteryEnergyStatusService {
@override
Future<BatteryEnergyStatus> readEnergyStatus() async {
List<int> energyStatusList = await bleManager.read(
deviceId: discoveredDevice.id,
serviceId: _batteryServiceUuid,
characteristicId: _batteryEnergyStatusCharacteristicUuid,
);

logger.t("Battery energy status bytes: $energyStatusList");

if (energyStatusList.length != 7) {
throw StateError(
'Battery energy status characteristic expected 7 values, but got ${energyStatusList.length}',
);
}

int rawVoltage = (energyStatusList[2] << 8) | energyStatusList[1];
double voltage = _convertSFloat(rawVoltage);

int rawAvailableCapacity = (energyStatusList[4] << 8) | energyStatusList[3];
double availableCapacity = _convertSFloat(rawAvailableCapacity);

int rawChargeRate = (energyStatusList[6] << 8) | energyStatusList[5];
double chargeRate = _convertSFloat(rawChargeRate);

BatteryEnergyStatus batteryEnergyStatus = BatteryEnergyStatus(
voltage: voltage,
availableCapacity: availableCapacity,
chargeRate: chargeRate,
);

logger.d('Battery energy status: $batteryEnergyStatus');

return batteryEnergyStatus;
}

double _convertSFloat(int rawBits) {
int exponent = ((rawBits & 0xF000) >> 12) - 16;
int mantissa = rawBits & 0x0FFF;

if (mantissa >= 0x800) {
mantissa = -((0x1000) - mantissa);
}
logger.t("Exponent: $exponent, Mantissa: $mantissa");
double result = mantissa.toDouble() * pow(10.0, exponent.toDouble());
return result;
}

@override
Stream<BatteryEnergyStatus> get energyStatusStream {
StreamController<BatteryEnergyStatus> controller =
StreamController<BatteryEnergyStatus>();
Timer? energyPollingTimer;

controller.onCancel = () {
energyPollingTimer?.cancel();
};

controller.onListen = () {
energyPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
readEnergyStatus().then((energyStatus) {
controller.add(energyStatus);
}).catchError((e) {
logger.e('Error reading energy status: $e');
});
});

readEnergyStatus().then((energyStatus) {
controller.add(energyStatus);
}).catchError((e) {
logger.e('Error reading energy status: $e');
});
};

return controller.stream;
}
}
Loading