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 @@
## 2.3.9

* fixed permissions on web, where web was not able to access the devices BLE services

## 2.3.8

* updated dependencies to latest versions
Expand Down
14 changes: 13 additions & 1 deletion doc/ADD_CUSTOM_WEARABLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,20 @@ class MyCustomWearable extends Wearable {

## 2. Implement a Custom Wearable Factory

Create a factory that determines when your custom wearable should be used. This factory is responsible for recognizing a device and constructing the corresponding wearable object. If you need to perform ble gatt operations, you can use the `BleGattManager` in `bleManager` of the `WearableFactory` class. The `BleGattManager` provides methods for interacting with BLE devices, such as reading and writing characteristics. It is provided by the `WearableManager` and should not be set manually.
Create a factory that determines when your custom wearable should be used. This factory is responsible for recognizing a device and constructing the corresponding wearable object. If you need to perform BLE GATT operations, you can use the `BleGattManager` in `bleManager` of the `WearableFactory` class. The `BleGattManager` provides methods for interacting with BLE devices, such as reading and writing characteristics. It is provided by the `WearableManager` and should not be set manually.

If your wearable uses BLE services, declare them in `usedServiceUuids`. Web Bluetooth requires all services to be requested before a device is selected, so the `WearableManager` collects this list from all registered factories when scanning starts.

```dart
class MyCustomWearableFactory extends WearableFactory {
static const String _customServiceUuid =
"00000000-0000-1000-8000-000000000000";

@override
Set<String> get usedServiceUuids => const {
_customServiceUuid,
};

@override
Future<bool> matches(DiscoveredDevice device, List<BleService> services) async {
// Define logic to check if the device matches your custom wearable
Expand All @@ -45,6 +55,8 @@ class MyCustomWearableFactory extends WearableFactory {
}
```

Include every service your factory may need while matching, creating, or using the wearable. This includes services used by capabilities that are registered during `createFromDevice`, because those capabilities are still created after Web Bluetooth has already requested access.

---

## 3. Register the Custom Factory
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ packages:
path: ".."
relative: true
source: path
version: "2.3.7"
version: "2.3.9"
package_config:
dependency: transitive
description:
Expand Down
3 changes: 3 additions & 0 deletions lib/open_earable_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ class WearableManager {
}) {
return _bleManager.startScan(
checkAndRequestPermissions: checkAndRequestPermissions,
webOptionalServiceUuids: {
for (final factory in _wearableFactories) ...factory.usedServiceUuids,
},
);
}

Expand Down
15 changes: 14 additions & 1 deletion lib/src/managers/ble_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class BleManager extends BleGattManager {
/// Initiates the BLE device scan to discover nearby Bluetooth devices.
Future<void> startScan({
bool checkAndRequestPermissions = true,
Set<String> webOptionalServiceUuids = const {},
}) async {
bool? permGranted;

Expand Down Expand Up @@ -164,7 +165,19 @@ class BleManager extends BleGattManager {
_scanStreamController?.add(device);
}
}
await UniversalBle.startScan();
await UniversalBle.startScan(
scanFilter: ScanFilter(
withNamePrefix: ["OpenEarable", "OpenRing", "Cosinuss", "eSense"],
),
platformConfig: kIsWeb
? PlatformConfig(
web: WebOptions(
optionalServices:
webOptionalServiceUuids.toList(growable: false),
),
)
: null,
);
}
_firstScan = false;
}
Expand Down
36 changes: 26 additions & 10 deletions lib/src/models/devices/cosinuss_one_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,44 @@ class CosinussOneFactory extends WearableFactory {
static const String _name = "earconnect";

@override
Future<bool> matches(DiscoveredDevice device, List<BleService> services) async {
Set<String> get usedServiceUuids => const {
CosinussOne.ppgAndAccServiceUuid,
CosinussOne.temperatureServiceUuid,
CosinussOne.heartRateServiceUuid,
CosinussOne.batteryServiceUuid,
};

@override
Future<bool> matches(
DiscoveredDevice device,
List<BleService> services,
) async {
return device.name == _name;
}

@override
Future<Wearable> createFromDevice(DiscoveredDevice device, { Set<ConnectionOption> options = const {} }) async {
Future<Wearable> createFromDevice(
DiscoveredDevice device, {
Set<ConnectionOption> options = const {},
}) async {
if (bleManager == null) {
throw Exception("bleManager needs to be set before using the factory");
throw StateError("bleManager needs to be set before using the factory");
}
if (disconnectNotifier == null) {
throw Exception("disconnectNotifier needs to be set before using the factory");
throw StateError(
"disconnectNotifier needs to be set before using the factory",
);
}

if (device.name != _name) {
throw Exception("device is not a cosinuss one");
throw ArgumentError.value(device.name, 'device.name', 'Expected $_name');
}

return CosinussOne(
name: device.name,
disconnectNotifier: disconnectNotifier!,
bleManager: bleManager!,
discoveredDevice: device,
);
name: device.name,
disconnectNotifier: disconnectNotifier!,
bleManager: bleManager!,
discoveredDevice: device,
);
}
}
50 changes: 34 additions & 16 deletions lib/src/models/devices/esense_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ import 'wearable.dart';

class EsenseFactory extends WearableFactory {
@override
Future<Wearable> createFromDevice(DiscoveredDevice device,
{Set<ConnectionOption> options = const {},}) async {
Set<String> get usedServiceUuids => const {
esenseServiceUuid,
};

@override
Future<Wearable> createFromDevice(
DiscoveredDevice device, {
Set<ConnectionOption> options = const {},
}) async {
EsenseSensorHandler sensorHandler = EsenseSensorHandler(
bleGattManager: bleManager!,
discoveredDevice: device,
Expand All @@ -29,18 +35,25 @@ class EsenseFactory extends WearableFactory {
EsenseSensorConfigurationValue(frequencyHz: 50.0),
EsenseSensorConfigurationValue(frequencyHz: 100.0),
EsenseSensorConfigurationValue(frequencyHz: 200.0),
].expand((v) => [v, v.copyWith(options: {StreamSensorConfigOption()})]).toList();

]
.expand(
(v) => [
v,
v.copyWith(options: {StreamSensorConfigOption()}),
],
)
.toList();

final imuConfig = EsenseSensorConfiguration(
name: "6-axis IMU",
values: imuConfigValues,
sensorCommand: 0x53,
sensorHandler: sensorHandler,
availableOptions: {
StreamSensorConfigOption(),
},
offValue: imuConfigValues.firstWhere((v) => v.options.isEmpty),
);
name: "6-axis IMU",
values: imuConfigValues,
sensorCommand: 0x53,
sensorHandler: sensorHandler,
availableOptions: {
StreamSensorConfigOption(),
},
offValue: imuConfigValues.firstWhere((v) => v.options.isEmpty),
);

Esense esense = Esense(
name: device.name,
Expand Down Expand Up @@ -71,12 +84,15 @@ class EsenseFactory extends WearableFactory {
),
],
);

return esense;
}

@override
Future<bool> matches(DiscoveredDevice device, List<BleService> services) async {
Future<bool> matches(
DiscoveredDevice device,
List<BleService> services,
) async {
return RegExp(r'^eSense-\d{4}$').hasMatch(device.name);
}
}
Expand Down Expand Up @@ -127,7 +143,9 @@ class EsenseSensor extends Sensor<SensorDoubleValue> {
} else if (entry.value is double) {
values.add(entry.value as double);
} else {
throw Exception("Unsupported sensor value type: ${entry.value.runtimeType}");
throw UnsupportedError(
"Unsupported sensor value type: ${entry.value.runtimeType}",
);
}
}

Expand Down
50 changes: 40 additions & 10 deletions lib/src/models/devices/open_earable_factory.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:open_earable_flutter/src/managers/sensor_handler.dart';
import 'package:open_earable_flutter/src/models/wearable_factory.dart';
import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/sensor_scheme_reader.dart';
Expand Down Expand Up @@ -33,6 +34,14 @@ class OpenEarableFactory extends WearableFactory {
final _v1Regex = RegExp(r'^1\.\d+\.\d+$');
final _v2Regex = RegExp(r'^2\.\d+\.\d+$');

@override
Set<String> get usedServiceUuids => {
...OpenEarableV1.serviceUuids,
...OpenEarableV2.serviceUuids,
mcuMgrSmpServiceUuid,
timeSynchronizationServiceUuid,
};

@override
Future<bool> matches(
DiscoveredDevice device,
Expand All @@ -52,7 +61,10 @@ class OpenEarableFactory extends WearableFactory {
}

@override
Future<Wearable> createFromDevice(DiscoveredDevice device, { Set<ConnectionOption> options = const {} }) async {
Future<Wearable> createFromDevice(
DiscoveredDevice device, {
Set<ConnectionOption> options = const {},
}) async {
if (bleManager == null) {
throw Exception("bleManager needs to be set before using the factory");
}
Expand Down Expand Up @@ -91,7 +103,10 @@ class OpenEarableFactory extends WearableFactory {
},
isConnectedViaSystem: options.contains(const ConnectedViaSystem()),
);
if (await bleManager!.hasService(deviceId: device.id, serviceId: timeSynchronizationServiceUuid)) {
if (await bleManager!.hasService(
deviceId: device.id,
serviceId: timeSynchronizationServiceUuid,
)) {
wearable.registerCapability<TimeSynchronizable>(
OpenEarableV2TimeSyncImp(
bleManager: bleManager!,
Expand Down Expand Up @@ -151,15 +166,23 @@ class OpenEarableFactory extends WearableFactory {

List<SensorScheme> sensorSchemes = await schemeParser.readSensorSchemes();

logger.d("Platform: ${kIsWeb ? 'WEB/Chrome' : 'NATIVE'}");
logger.d(
"Sensor schemes provided by device: ${sensorSchemes.map((s) => 'ID:${s.sensorId}(${s.sensorName})').join(', ')}",
);

for (SensorScheme scheme in sensorSchemes) {
List<SensorConfigurationOpenEarableV2Value> sensorConfigurationValues = [];
List<SensorConfigurationOpenEarableV2Value> sensorConfigurationValues =
[];

final features = scheme.options?.features ?? [];
final hasStreaming = features.contains(SensorConfigFeatures.streaming);
final hasRecording = features.contains(SensorConfigFeatures.recording);
final hasFrequencies = features.contains(SensorConfigFeatures.frequencyDefinition);
final hasFrequencies =
features.contains(SensorConfigFeatures.frequencyDefinition);
final frequencies = scheme.options?.frequencies?.frequencies ?? [];
final maxStreamingIndex = scheme.options?.frequencies?.maxStreamingFreqIndex ?? -1;
final maxStreamingIndex =
scheme.options?.frequencies?.maxStreamingFreqIndex ?? -1;

//TODO: handle case where no frequencies are defined
if (hasFrequencies && frequencies.isNotEmpty) {
Expand Down Expand Up @@ -211,7 +234,9 @@ class OpenEarableFactory extends WearableFactory {
.firstOrNull;

if (sensorConfigurationValues.isEmpty) {
logger.w("No configuration values generated for sensor: ${scheme.sensorName}");
logger.w(
"No configuration values generated for sensor: ${scheme.sensorName}",
);
}

final sensorConfiguration = SensorConfigurationOpenEarableV2(
Expand All @@ -229,16 +254,21 @@ class OpenEarableFactory extends WearableFactory {

sensorConfigurations.add(sensorConfiguration);

if (scheme.options?.features.contains(SensorConfigFeatures.streaming) ?? false) {
if (scheme.options?.features.contains(SensorConfigFeatures.streaming) ??
false) {
// Group components by group name
final sensorGroups = <String, List<Component>>{};
for (final component in scheme.components) {
sensorGroups.putIfAbsent(component.groupName, () => []).add(component);
sensorGroups
.putIfAbsent(component.groupName, () => [])
.add(component);
}

for (final groupName in sensorGroups.keys) {
final axisNames = sensorGroups[groupName]!.map((c) => c.componentName).toList();
final axisUnits = sensorGroups[groupName]!.map((c) => c.unitName).toList();
final axisNames =
sensorGroups[groupName]!.map((c) => c.componentName).toList();
final axisUnits =
sensorGroups[groupName]!.map((c) => c.unitName).toList();

final sensor = _OpenEarableSensorV2(
sensorId: scheme.sensorId,
Expand Down
9 changes: 9 additions & 0 deletions lib/src/models/devices/open_earable_v1.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ class OpenEarableV1 extends Wearable
static const String buttonServiceUuid =
"29c10bdc-4773-11ee-be56-0242ac120002";
static const String batteryServiceUuid = "180F";
static const Set<String> serviceUuids = {
sensorServiceUuid,
parseInfoServiceUuid,
deviceInfoServiceUuid,
ledServiceUuid,
audioPlayerServiceUuid,
buttonServiceUuid,
batteryServiceUuid,
};

final List<Sensor> _sensors;
final List<SensorConfiguration> _sensorConfigurations;
Expand Down
Loading
Loading