From e5250be4444fbaf0599ab4555e090b08e03a64a1 Mon Sep 17 00:00:00 2001 From: maddin Date: Sat, 27 Dec 2025 22:19:37 +0100 Subject: [PATCH 1/6] ExG Implementation, added the iirjdart package for bandpass signalproccessing inside the exg_wearble.dart --- .../Flutter/GeneratedPluginRegistrant.swift | 2 + example/pubspec.lock | 68 +-- lib/open_earable_flutter.dart | 2 + lib/src/managers/exg/exg_factory.dart | 40 ++ lib/src/managers/exg/exg_filter_options.dart | 6 + lib/src/managers/exg/exg_preset.dart | 31 ++ lib/src/managers/exg/exg_wearable.dart | 446 ++++++++++++++++++ pubspec.yaml | 2 + 8 files changed, 567 insertions(+), 30 deletions(-) create mode 100644 lib/src/managers/exg/exg_factory.dart create mode 100644 lib/src/managers/exg/exg_filter_options.dart create mode 100644 lib/src/managers/exg/exg_preset.dart create mode 100644 lib/src/managers/exg/exg_wearable.dart diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 317a668..354253a 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,10 +7,12 @@ import Foundation import file_picker import flutter_archive +import path_provider_foundation import universal_ble func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FlutterArchivePlugin.register(with: registry.registrar(forPlugin: "FlutterArchivePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) } diff --git a/example/pubspec.lock b/example/pubspec.lock index dc169aa..65e0a14 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + complex: + dependency: transitive + description: + name: complex + sha256: dba084899c0a4bd2fcba9a36760409171d7bee7c35a749cc4451348270361325 + url: "https://pub.dev" + source: hosted + version: "0.7.2" convert: dependency: transitive description: @@ -186,10 +194,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" + sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 url: "https://pub.dev" source: hosted - version: "2.0.32" + version: "2.0.31" flutter_svg: dependency: "direct main" description: @@ -232,6 +240,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + iirjdart: + dependency: transitive + description: + name: iirjdart + sha256: "5bd8aa6af6ee67473fbf7fbbfc7b992d0bb95768a16aaa62e70613389e564df8" + url: "https://pub.dev" + source: hosted + version: "0.1.0" json_annotation: dependency: transitive description: @@ -244,26 +260,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -316,10 +332,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" nested: dependency: transitive description: @@ -328,14 +344,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "1f81ed9e41909d44162d7ec8663b2c647c202317cc0b56d3d56f6a13146a0b64" - url: "https://pub.dev" - source: hosted - version: "9.1.0" open_earable_flutter: dependency: "direct main" description: @@ -371,18 +379,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "95c68a74d3cab950fd0ed8073d9fab15c1c06eb1f3eec68676e87aabc9ecee5a" + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" url: "https://pub.dev" source: hosted - version: "2.2.21" + version: "2.2.19" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "6192e477f34018ef1ea790c56fffc7302e3bc3efede9e798b934c252c8c105ba" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -560,10 +568,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.4" tuple: dependency: transitive description: @@ -624,18 +632,18 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.0.0" web: dependency: transitive description: @@ -669,5 +677,5 @@ packages: source: hosted version: "6.6.1" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index 48bb10f..bfffa26 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:logger/logger.dart'; import 'package:meta/meta.dart'; +import 'package:open_earable_flutter/src/managers/exg/exg_factory.dart'; import 'package:open_earable_flutter/src/models/devices/cosinuss_one_factory.dart'; import 'package:open_earable_flutter/src/models/devices/esense_factory.dart'; import 'package:open_earable_flutter/src/models/devices/open_earable_factory.dart'; @@ -106,6 +107,7 @@ class WearableManager { StreamSubscription? _autoconnectScanSubscription; final List _wearableFactories = [ + ExGFactory(), OpenEarableFactory(), CosinussOneFactory(), PolarFactory(), diff --git a/lib/src/managers/exg/exg_factory.dart b/lib/src/managers/exg/exg_factory.dart new file mode 100644 index 0000000..5e0588e --- /dev/null +++ b/lib/src/managers/exg/exg_factory.dart @@ -0,0 +1,40 @@ +import 'package:open_earable_flutter/src/models/devices/cosinuss_one.dart'; +import 'package:open_earable_flutter/src/models/devices/discovered_device.dart'; +import 'package:open_earable_flutter/src/models/devices/wearable.dart'; +import 'package:open_earable_flutter/src/models/wearable_factory.dart'; +import 'package:universal_ble/universal_ble.dart'; +import 'exg_wearable.dart'; + + +class ExGFactory extends WearableFactory { + // todo ExG Devices are still named OpenEarable- fixit or keep it index 0 when creating the _wearableFactories + static final RegExp _nameRegex = RegExp(r'^OpenEarable(?:[-_].*)?$'); + + @override + Future matches(DiscoveredDevice device, List services) async { + final name = (device.name ?? '').trim(); + return _nameRegex.hasMatch(name); + } + + @override + Future createFromDevice(DiscoveredDevice device, { Set options = const {} }) async { + if (bleManager == null) { + throw Exception("bleManager needs to be set before using the factory"); + } + if (disconnectNotifier == null) { + throw Exception("disconnectNotifier needs to be set before using the factory"); + } + + final name = (device.name ?? '').trim(); + if (!_nameRegex.hasMatch(name)) { + throw Exception("device is not an exg device"); + } + + return ExGWearable( + name: device.name, + disconnectNotifier: disconnectNotifier!, + bleManager: bleManager!, + discoveredDevice: device, + ); + } +} diff --git a/lib/src/managers/exg/exg_filter_options.dart b/lib/src/managers/exg/exg_filter_options.dart new file mode 100644 index 0000000..c931aba --- /dev/null +++ b/lib/src/managers/exg/exg_filter_options.dart @@ -0,0 +1,6 @@ +class ExGFilterOptions { + static const List lowerCutoffs = [0.5, 1, 5, 10, 15]; + static const List higherCutoffs = [20, 30, 40, 50, 60]; + static const List samplingFrequencies = [100, 200, 250, 300, 400]; + static const List filterOrders = [1, 2, 3, 4]; +} diff --git a/lib/src/managers/exg/exg_preset.dart b/lib/src/managers/exg/exg_preset.dart new file mode 100644 index 0000000..12897a4 --- /dev/null +++ b/lib/src/managers/exg/exg_preset.dart @@ -0,0 +1,31 @@ +class ExGPreset { + final String name; + final double lowerCutoff; + final double higherCutoff; + final int samplingFrequency; + final int filterOrder; + + ExGPreset({ + required this.name, + required this.lowerCutoff, + required this.higherCutoff, + required this.samplingFrequency, + required this.filterOrder, + }); + + Map toJson() => { + 'name': name, + 'lowerCutoff': lowerCutoff, + 'higherCutoff': higherCutoff, + 'samplingFrequency': samplingFrequency, + 'filterOrder': filterOrder, + }; + + factory ExGPreset.fromJson(Map json) => ExGPreset( + name: json['name'], + lowerCutoff: (json['lowerCutoff'] as num).toDouble(), + higherCutoff: (json['higherCutoff'] as num).toDouble(), + samplingFrequency: json['samplingFrequency'], + filterOrder: json['filterOrder'], + ); +} diff --git a/lib/src/managers/exg/exg_wearable.dart b/lib/src/managers/exg/exg_wearable.dart new file mode 100644 index 0000000..9efd8f6 --- /dev/null +++ b/lib/src/managers/exg/exg_wearable.dart @@ -0,0 +1,446 @@ +import 'dart:async'; +import 'package:iirjdart/butterworth.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:open_earable_flutter/src/managers/open_earable_sensor_manager.dart'; +import 'dart:typed_data'; +import 'exg_filter_options.dart'; +import 'exg_preset.dart'; + +class ExGWearable extends Wearable implements + SensorManager, + SensorConfigurationManager, + BatteryLevelStatus, + EdgeRecorderManager + { + static const batteryServiceUuid = "180f"; + static const _batteryLevelCharacteristicUuid = "02a19"; + + final List _sensorConfigurations; + final List _sensors; + final BleGattManager _bleManager; + final DiscoveredDevice _discoveredDevice; + + final _configCtrl = StreamController< + Map, SensorConfigurationValue> + >.broadcast(); + + final Map, SensorConfigurationValue> + _currentConfigValues = {}; + + ExGWearable({ + required super.name, + required super.disconnectNotifier, + required BleGattManager bleManager, + required DiscoveredDevice discoveredDevice, + }) : _sensors = [], + _sensorConfigurations = [], + _bleManager = bleManager, + _discoveredDevice = discoveredDevice { + _initSensors(); + } + + void _initSensors() { + final sensorManager = OpenEarableSensorHandler( + bleManager: _bleManager, + deviceId: _discoveredDevice.id, + ); + + final exgLowerCutoff = _ExGLowerCutoffSensorConfiguration(wearable: this); + final exgHigherCutoff = _ExGHigherCutoffSensorConfiguration(wearable: this); + final exgFs = _ExGFsSensorConfiguration(wearable: this); + final exgOrder = _ExGOrderSensorConfiguration(wearable: this); + + _sensorConfigurations.addAll([exgLowerCutoff, exgHigherCutoff, exgFs, exgOrder]); + + _sensors.add(_ExGSensor( + bleManager: _bleManager, + discoveredDevice: _discoveredDevice, + sensorManager: sensorManager, + )); + + _seedInitialConfigValues(); // <— important + } + + // optional: call this when you're done with the wearable + Future disposeWearable() async { + await _configCtrl.close(); + } + + void applyPreset(ExGPreset p) { + if (p.lowerCutoff >= p.higherCutoff) { + throw ArgumentError('Lower cutoff must be < higher cutoff'); + } + + for (final cfg in _sensorConfigurations) { + if (cfg is _ExGLowerCutoffSensorConfiguration) { + cfg.setConfiguration(CutoffConfigurationValue(value: p.lowerCutoff.toString())); + } else if (cfg is _ExGHigherCutoffSensorConfiguration) { + cfg.setConfiguration(CutoffConfigurationValue(value: p.higherCutoff.toString())); + } else if (cfg is _ExGFsSensorConfiguration) { + cfg.setConfiguration(CutoffConfigurationValue(value: p.samplingFrequency.toString())); + } else if (cfg is _ExGOrderSensorConfiguration) { + cfg.setConfiguration(CutoffConfigurationValue(value: p.filterOrder.toString())); + } + } + } + + @override + String? getWearableIconPath({bool darkmode = false}) { + // todo add Icon here + return null; + } + + @override + String get deviceId => _discoveredDevice.id; + + @override + Future disconnect() { + return _bleManager.disconnect(_discoveredDevice.id); + } + + + @override + Stream get batteryPercentageStream { + StreamController streamController = StreamController(); + + StreamSubscription subscription = _bleManager + .subscribe( + deviceId: _discoveredDevice.id, + serviceId: batteryServiceUuid, + characteristicId: _batteryLevelCharacteristicUuid, + ) + .listen((data) { + streamController.add(data[0]); + }); + + readBatteryPercentage().then((percentage) { + streamController.add(percentage); + streamController.close(); + }).catchError((error) { + streamController.addError(error); + streamController.close(); + }); + + // Cancel BLE subscription when canceling stream + streamController.onCancel = () { + subscription.cancel(); + }; + + return streamController.stream; + } + + @override + Future readBatteryPercentage() async { + List batteryLevelList = await _bleManager.read( + deviceId: _discoveredDevice.id, + serviceId: batteryServiceUuid, + characteristicId: _batteryLevelCharacteristicUuid, + ); + + logger.t("Battery level bytes: $batteryLevelList"); + + if (batteryLevelList.length != 1) { + throw StateError( + 'Battery level characteristic expected 1 value, but got ${batteryLevelList.length}', + ); + } + + return batteryLevelList[0]; + } + + @override + Stream, SensorConfigurationValue>> + get sensorConfigurationStream => _configCtrl.stream; + + @override + List get sensorConfigurations => + List.unmodifiable(_sensorConfigurations); + + @override + List get sensors => List.unmodifiable(_sensors); + + void _notifyConfigChanged( + SensorConfiguration cfg, + SensorConfigurationValue val, + ) { + _currentConfigValues[cfg] = val; + // emit a snapshot for listeners (UI etc.) + _configCtrl.add(Map.unmodifiable(_currentConfigValues)); + } + + // Call this once after creating the configs to seed initial values + void _seedInitialConfigValues() { + for (final cfg in _sensorConfigurations) { + _currentConfigValues[cfg] = cfg.offValue!; + } + _configCtrl.add(Map.unmodifiable(_currentConfigValues)); + } + + @override + Future get filePrefix async { + return ""; + } + + @override + Future setFilePrefix(String prefix) async { + + } +} + +class _ExGSensor extends Sensor { + static const String exgServiceUuid = "0029d054-23d0-4c58-a199-c6bdc16c4975"; + static const String exgCharacteristicUuid = "20a4a273-c214-4c18-b433-329f30ef7275"; + final BleGattManager _bleManager; + final DiscoveredDevice _discoveredDevice; + + final List _axisNames = ['EOG']; + final List _axisUnits = ['µV']; + final double inampGain = 50.0; + final bool enableFilters = true; + + late double Function(double) _biopotentialFilter; + int filterOrder; + List filterCutoff; + String filterBtype; + double filterFs; + bool filterNotch; + + _ExGSensor({ + required BleGattManager bleManager, + required DiscoveredDevice discoveredDevice, + required OpenEarableSensorHandler sensorManager, + + this.filterOrder = 4, + this.filterCutoff = const [0.5, 50], + this.filterBtype = "bandpass", + this.filterFs = 250, + this.filterNotch = true, + + }) : _bleManager = bleManager, + _discoveredDevice = discoveredDevice, + _biopotentialFilter = _getBiopotentialFilter( + order: filterOrder, + cutoff: filterCutoff, + btype: filterBtype, + fs: filterFs, + notch: filterNotch, + ), + super( + sensorName: 'exg Filters', + chartTitle: 'exg', + shortChartTitle: 'exg', + ) { + _updateFilter(); + } + + void _updateFilter() { + _biopotentialFilter = _getBiopotentialFilter( + order: filterOrder, + cutoff: filterCutoff, + btype: filterBtype, + fs: filterFs, + notch: filterNotch, + ); + + // Print current filter settings for verification + print( + '[BioFilter Update] ' + 'order: $filterOrder, ' + 'cutoff: $filterCutoff, ' + 'btype: $filterBtype, ' + 'fs: $filterFs, ' + 'notch: $filterNotch' + ); + } + + void updateFilterSettings({ + int? filterOrder, + List? filterCutoff, // [low, high] + String? filterBtype, + double? filterFs, + bool? filterNotch, + }) { + if (filterOrder != null) this.filterOrder = filterOrder; + if (filterCutoff != null) this.filterCutoff = filterCutoff; + if (filterBtype != null) this.filterBtype = filterBtype; + if (filterFs != null) this.filterFs = filterFs; + if (filterNotch != null) this.filterNotch = filterNotch; + + _updateFilter(); + } + + @override + List get axisNames => _axisNames; + @override + List get axisUnits => _axisUnits; + + @override + Stream get sensorStream { + final controller = StreamController(); + + final subscription = _bleManager.subscribe( + deviceId: _discoveredDevice.id, + serviceId: exgServiceUuid, + characteristicId: exgCharacteristicUuid, + ).listen((data) { + if (data.length < 20) return; + + final byteData = ByteData.sublistView(Uint8List.fromList(data)); + // 5 values of 4 bytes each (Float32), adjust if needed + for (int i = 0; i < 4; i++) { + final rawValue = byteData.getFloat32(i * 4, Endian.little); + double processedEog; + + if (enableFilters) { + // Call the pre-configured filter + processedEog = (_biopotentialFilter(rawValue) / inampGain) * 1e6; + } else { + final rawUv = (rawValue / inampGain) * 1e6; + processedEog = rawUv; + } + + final values = [processedEog]; + final timestamp = DateTime.now(); + + controller.add( + SensorDoubleValue( + values: values, + timestamp: timestamp.millisecondsSinceEpoch, + ), + ); + } + }); + + controller.onCancel = () { + subscription.cancel(); + }; + return controller.stream; + } + + /// function to create and configure a biopotential filter. + static double Function(double) _getBiopotentialFilter({ + int order = 4, + List cutoff = const [0.5, 50], + String btype = "bandpass", + double fs = 30, + bool notch = true, + }) { + print(cutoff); + + if (btype == "bandpass" && cutoff.length == 2) { + double centerFrequency = (cutoff[0] + cutoff[1]) / 2.0; + double widthFrequency = cutoff[1] - cutoff[0]; + + final biopotentialFilter = Butterworth(); + biopotentialFilter.bandPass(order, fs, centerFrequency, widthFrequency); + + if (notch) { + final notchFilter = Butterworth(); + final double notchWidth = 50.0 / 30.0; + notchFilter.bandStop(2, fs, 50.0, notchWidth); + + return (double x) { + return biopotentialFilter.filter(notchFilter.filter(x)); + }; + } else { + return (double x) { + return biopotentialFilter.filter(x); + }; + } + } else { + throw UnimplementedError("Filter type '$btype' or cutoff configuration is not supported."); + } + } +} + +class CutoffConfigurationValue extends SensorConfigurationValue { + CutoffConfigurationValue({required String value}) + : super(key: value); + + double get cutoff => double.parse(key); +} +class _ExGLowerCutoffSensorConfiguration extends SensorConfiguration { + final ExGWearable wearable; + _ExGLowerCutoffSensorConfiguration({required this.wearable}) + : super( + name: 'Lower Cutoff', + values: ExGFilterOptions.lowerCutoffs + .map((v) => CutoffConfigurationValue(value: v.toString())) + .toList(), + offValue: CutoffConfigurationValue(value: ExGFilterOptions.lowerCutoffs.first.toString()), + ); + + @override + void setConfiguration(SensorConfigurationValue configuration) { + final sensor = wearable.sensors.first as _ExGSensor; + final newLow = double.parse(configuration.key); + sensor.updateFilterSettings( + filterCutoff: [newLow, sensor.filterCutoff[1]], + ); + wearable._notifyConfigChanged(this, configuration); + } +} + +class _ExGHigherCutoffSensorConfiguration extends SensorConfiguration { + final ExGWearable wearable; + _ExGHigherCutoffSensorConfiguration({required this.wearable}) + : super( + name: 'Higher Cutoff', + values: ExGFilterOptions.higherCutoffs + .map((v) => CutoffConfigurationValue(value: v.toString())) + .toList(), + offValue: CutoffConfigurationValue(value: ExGFilterOptions.higherCutoffs.first.toString()), + ); + + @override + void setConfiguration(SensorConfigurationValue configuration) { + final sensor = wearable.sensors.first as _ExGSensor; + final newHigh = double.parse(configuration.key); + sensor.updateFilterSettings( + filterCutoff: [sensor.filterCutoff[0], newHigh], + ); + wearable._notifyConfigChanged(this, configuration); + + } +} + +class _ExGFsSensorConfiguration extends SensorConfiguration { + final ExGWearable wearable; + _ExGFsSensorConfiguration({required this.wearable}) + : super( + name: 'Sampling Frequency (fs)', + values: ExGFilterOptions.samplingFrequencies + .map((v) => CutoffConfigurationValue(value: v.toString())) + .toList(), + offValue: CutoffConfigurationValue(value: ExGFilterOptions.samplingFrequencies.first.toString()), + ); + + @override + void setConfiguration(SensorConfigurationValue configuration) { + final sensor = wearable.sensors.first as _ExGSensor; + sensor.updateFilterSettings(filterFs: double.parse(configuration.key)); + wearable._notifyConfigChanged(this, configuration); + } +} + +class _ExGOrderSensorConfiguration extends SensorConfiguration { + final ExGWearable wearable; + _ExGOrderSensorConfiguration({required this.wearable}) + : super( + name: 'Filter Order', + values: ExGFilterOptions.filterOrders + .map((v) => CutoffConfigurationValue(value: v.toString())) + .toList(), + offValue: CutoffConfigurationValue(value: ExGFilterOptions.filterOrders.first.toString()), + ); + + + @override + void setConfiguration(SensorConfigurationValue configuration) { + final sensor = wearable.sensors.first as _ExGSensor; + sensor.updateFilterSettings(filterOrder: int.parse(configuration.key)); + wearable._notifyConfigChanged(this, configuration); + } +} + + diff --git a/pubspec.yaml b/pubspec.yaml index b57dcb3..5f82a61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: bloc: ^9.1.0 meta: ^1.16.0 pub_semver: ^2.2.0 + iirjdart: ^0.1.0 + dev_dependencies: flutter_test: From fa875b5cfd689d93402e7105caa2dd173466fa0b Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Fri, 2 Jan 2026 16:40:33 +0100 Subject: [PATCH 2/6] update device connection exceptions --- example/lib/main.dart | 31 +++++++++++++++++++++--- example/pubspec.lock | 24 ++++++++++++------ example/pubspec.yaml | 1 + lib/open_earable_flutter.dart | 26 ++++++++++++++++---- lib/src/exceptions/device_exception.dart | 15 ++++++++++++ 5 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 lib/src/exceptions/device_exception.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index fc19a96..d33bf6b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:example/widgets/button_state_widget.dart'; import 'package:example/widgets/fota/firmware_update.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart'; import 'package:example/global_theme.dart'; import 'package:flutter/material.dart'; @@ -152,9 +153,30 @@ class MyAppState extends State { trailing: _buildTrailingWidget(device.id, Theme.of(context).colorScheme.secondary), onTap: () async { - Wearable wearable = await _wearableManager - .connectToDevice(device); - provider.setSelectedPeripheral(wearable); + try { + Wearable wearable = await _wearableManager + .connectToDevice(device); + provider.setSelectedPeripheral(wearable); + } catch (e) { + String message = _wearableManager + .deviceErrorMessage(e, device.name); + if (context.mounted) { + showPlatformDialog( + context: context, + builder: (context) => PlatformAlertDialog( + title: PlatformText('Connection Error'), + content: PlatformText(message), + actions: [ + PlatformDialogAction( + onPressed: () => + Navigator.of(context).pop(), + child: PlatformText('OK'), + ), + ], + ), + ); + } + } }, ), if (index != discoveredDevices.length - 1) @@ -246,7 +268,8 @@ class MyAppState extends State { if (_connectedDevice is ButtonManager) GroupedBox( title: "Button State", - child: ButtonStateWidget(buttonManager: _connectedDevice as ButtonManager), + child: ButtonStateWidget( + buttonManager: _connectedDevice as ButtonManager), ), if (_connectedDevice is FrequencyPlayer) GroupedBox( diff --git a/example/pubspec.lock b/example/pubspec.lock index 65e0a14..e781545 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -190,6 +190,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_platform_widgets: + dependency: "direct main" + description: + name: flutter_platform_widgets + sha256: "22a86564cb6cc0b93637c813ca91b0b1f61c2681a31e0f9d77590c1fa9f12020" + url: "https://pub.dev" + source: hosted + version: "9.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -308,18 +316,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" mcumgr_flutter: dependency: "direct main" description: @@ -568,10 +576,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.8" tuple: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a3e5773..2f208cc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 flutter_colorpicker: ^1.1.0 + flutter_platform_widgets: ^9.0.0 meta: ^1.16.0 bloc: ^9.1.0 flutter_svg: ^2.0.17 diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index bfffa26..bcc91bf 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:logger/logger.dart'; import 'package:meta/meta.dart'; +import 'package:open_earable_flutter/src/exceptions/device_exception.dart'; import 'package:open_earable_flutter/src/managers/exg/exg_factory.dart'; import 'package:open_earable_flutter/src/models/devices/cosinuss_one_factory.dart'; import 'package:open_earable_flutter/src/models/devices/esense_factory.dart'; @@ -195,7 +196,7 @@ class WearableManager { }) async { if (_connectedIds.contains(device.id)) { logger.w('Device ${device.id} is already connected'); - throw Exception('Device is already connected'); + throw AlreadyConnectedException(); } _connectingStreamController.add(device); @@ -228,9 +229,9 @@ class WearableManager { } _connectedIds.remove(device.id); await _bleManager.disconnect(device.id); - throw Exception('Device is currently not supported'); + throw UnsupportedDeviceException(); } else { - throw Exception('Failed to connect to device'); + throw ConnectionFailedException(); } } @@ -254,12 +255,25 @@ class WearableManager { ); connectedWearables.add(wearable); } catch (e) { - logger.e('Failed to connect to system device ${device.id}: $e'); + logger.e( + 'Failed to connect to system device ${device.id}: ${deviceErrorMessage(e, device.name)}', + ); } } return connectedWearables; } + String deviceErrorMessage(dynamic e, String deviceName) { + return switch (e) { + UnsupportedDeviceException _ => 'Device "$deviceName" is not supported.', + AlreadyConnectedException _ => + 'Device "$deviceName" is already connected.', + ConnectionFailedException _ => + 'Failed to connect to device "$deviceName". Please try again.', + _ => e.toString(), + }; + } + void addPairingRule(PairingRule rule) { _pairingManager.addRule(rule); } @@ -294,7 +308,9 @@ class WearableManager { try { await connectToDevice(discoveredDevice); } catch (e) { - logger.e('Error auto connecting device ${discoveredDevice.id}: $e'); + logger.e( + 'Error auto connecting device ${discoveredDevice.id}: ${deviceErrorMessage(e, discoveredDevice.name)}', + ); } } }); diff --git a/lib/src/exceptions/device_exception.dart b/lib/src/exceptions/device_exception.dart new file mode 100644 index 0000000..f409352 --- /dev/null +++ b/lib/src/exceptions/device_exception.dart @@ -0,0 +1,15 @@ +sealed class DeviceException implements Exception { + const DeviceException(); +} + +class UnsupportedDeviceException extends DeviceException { + const UnsupportedDeviceException(); +} + +class AlreadyConnectedException extends DeviceException { + const AlreadyConnectedException(); +} + +class ConnectionFailedException extends DeviceException { + const ConnectionFailedException(); +} From 0272d6501a4371ae6401ca8dbbb3157e3566693d Mon Sep 17 00:00:00 2001 From: o-bagge <47336932+o-bagge@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:55:53 +0100 Subject: [PATCH 3/6] Revert "update device connection exceptions" --- example/lib/main.dart | 31 +++--------------------- example/pubspec.lock | 24 ++++++------------ example/pubspec.yaml | 1 - lib/open_earable_flutter.dart | 26 ++++---------------- lib/src/exceptions/device_exception.dart | 15 ------------ 5 files changed, 17 insertions(+), 80 deletions(-) delete mode 100644 lib/src/exceptions/device_exception.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index d33bf6b..fc19a96 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:example/widgets/button_state_widget.dart'; import 'package:example/widgets/fota/firmware_update.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart'; import 'package:example/global_theme.dart'; import 'package:flutter/material.dart'; @@ -153,30 +152,9 @@ class MyAppState extends State { trailing: _buildTrailingWidget(device.id, Theme.of(context).colorScheme.secondary), onTap: () async { - try { - Wearable wearable = await _wearableManager - .connectToDevice(device); - provider.setSelectedPeripheral(wearable); - } catch (e) { - String message = _wearableManager - .deviceErrorMessage(e, device.name); - if (context.mounted) { - showPlatformDialog( - context: context, - builder: (context) => PlatformAlertDialog( - title: PlatformText('Connection Error'), - content: PlatformText(message), - actions: [ - PlatformDialogAction( - onPressed: () => - Navigator.of(context).pop(), - child: PlatformText('OK'), - ), - ], - ), - ); - } - } + Wearable wearable = await _wearableManager + .connectToDevice(device); + provider.setSelectedPeripheral(wearable); }, ), if (index != discoveredDevices.length - 1) @@ -268,8 +246,7 @@ class MyAppState extends State { if (_connectedDevice is ButtonManager) GroupedBox( title: "Button State", - child: ButtonStateWidget( - buttonManager: _connectedDevice as ButtonManager), + child: ButtonStateWidget(buttonManager: _connectedDevice as ButtonManager), ), if (_connectedDevice is FrequencyPlayer) GroupedBox( diff --git a/example/pubspec.lock b/example/pubspec.lock index e781545..298cc46 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -190,14 +190,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - flutter_platform_widgets: - dependency: "direct main" - description: - name: flutter_platform_widgets - sha256: "22a86564cb6cc0b93637c813ca91b0b1f61c2681a31e0f9d77590c1fa9f12020" - url: "https://pub.dev" - source: hosted - version: "9.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -316,18 +308,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" mcumgr_flutter: dependency: "direct main" description: @@ -576,10 +568,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.7" tuple: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2f208cc..a3e5773 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -42,7 +42,6 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 flutter_colorpicker: ^1.1.0 - flutter_platform_widgets: ^9.0.0 meta: ^1.16.0 bloc: ^9.1.0 flutter_svg: ^2.0.17 diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index bcc91bf..bfffa26 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'package:logger/logger.dart'; import 'package:meta/meta.dart'; -import 'package:open_earable_flutter/src/exceptions/device_exception.dart'; import 'package:open_earable_flutter/src/managers/exg/exg_factory.dart'; import 'package:open_earable_flutter/src/models/devices/cosinuss_one_factory.dart'; import 'package:open_earable_flutter/src/models/devices/esense_factory.dart'; @@ -196,7 +195,7 @@ class WearableManager { }) async { if (_connectedIds.contains(device.id)) { logger.w('Device ${device.id} is already connected'); - throw AlreadyConnectedException(); + throw Exception('Device is already connected'); } _connectingStreamController.add(device); @@ -229,9 +228,9 @@ class WearableManager { } _connectedIds.remove(device.id); await _bleManager.disconnect(device.id); - throw UnsupportedDeviceException(); + throw Exception('Device is currently not supported'); } else { - throw ConnectionFailedException(); + throw Exception('Failed to connect to device'); } } @@ -255,25 +254,12 @@ class WearableManager { ); connectedWearables.add(wearable); } catch (e) { - logger.e( - 'Failed to connect to system device ${device.id}: ${deviceErrorMessage(e, device.name)}', - ); + logger.e('Failed to connect to system device ${device.id}: $e'); } } return connectedWearables; } - String deviceErrorMessage(dynamic e, String deviceName) { - return switch (e) { - UnsupportedDeviceException _ => 'Device "$deviceName" is not supported.', - AlreadyConnectedException _ => - 'Device "$deviceName" is already connected.', - ConnectionFailedException _ => - 'Failed to connect to device "$deviceName". Please try again.', - _ => e.toString(), - }; - } - void addPairingRule(PairingRule rule) { _pairingManager.addRule(rule); } @@ -308,9 +294,7 @@ class WearableManager { try { await connectToDevice(discoveredDevice); } catch (e) { - logger.e( - 'Error auto connecting device ${discoveredDevice.id}: ${deviceErrorMessage(e, discoveredDevice.name)}', - ); + logger.e('Error auto connecting device ${discoveredDevice.id}: $e'); } } }); diff --git a/lib/src/exceptions/device_exception.dart b/lib/src/exceptions/device_exception.dart deleted file mode 100644 index f409352..0000000 --- a/lib/src/exceptions/device_exception.dart +++ /dev/null @@ -1,15 +0,0 @@ -sealed class DeviceException implements Exception { - const DeviceException(); -} - -class UnsupportedDeviceException extends DeviceException { - const UnsupportedDeviceException(); -} - -class AlreadyConnectedException extends DeviceException { - const AlreadyConnectedException(); -} - -class ConnectionFailedException extends DeviceException { - const ConnectionFailedException(); -} From 8eea65ce1003a8f2f5aa93ae51153cb97f7b00b2 Mon Sep 17 00:00:00 2001 From: o-bagge <47336932+o-bagge@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:02:35 +0100 Subject: [PATCH 4/6] Revert "Revert "update device connection exceptions"" --- example/lib/main.dart | 31 +++++++++++++++++++++--- example/pubspec.lock | 24 ++++++++++++------ example/pubspec.yaml | 1 + lib/open_earable_flutter.dart | 26 ++++++++++++++++---- lib/src/exceptions/device_exception.dart | 15 ++++++++++++ 5 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 lib/src/exceptions/device_exception.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index fc19a96..d33bf6b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:example/widgets/button_state_widget.dart'; import 'package:example/widgets/fota/firmware_update.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart'; import 'package:example/global_theme.dart'; import 'package:flutter/material.dart'; @@ -152,9 +153,30 @@ class MyAppState extends State { trailing: _buildTrailingWidget(device.id, Theme.of(context).colorScheme.secondary), onTap: () async { - Wearable wearable = await _wearableManager - .connectToDevice(device); - provider.setSelectedPeripheral(wearable); + try { + Wearable wearable = await _wearableManager + .connectToDevice(device); + provider.setSelectedPeripheral(wearable); + } catch (e) { + String message = _wearableManager + .deviceErrorMessage(e, device.name); + if (context.mounted) { + showPlatformDialog( + context: context, + builder: (context) => PlatformAlertDialog( + title: PlatformText('Connection Error'), + content: PlatformText(message), + actions: [ + PlatformDialogAction( + onPressed: () => + Navigator.of(context).pop(), + child: PlatformText('OK'), + ), + ], + ), + ); + } + } }, ), if (index != discoveredDevices.length - 1) @@ -246,7 +268,8 @@ class MyAppState extends State { if (_connectedDevice is ButtonManager) GroupedBox( title: "Button State", - child: ButtonStateWidget(buttonManager: _connectedDevice as ButtonManager), + child: ButtonStateWidget( + buttonManager: _connectedDevice as ButtonManager), ), if (_connectedDevice is FrequencyPlayer) GroupedBox( diff --git a/example/pubspec.lock b/example/pubspec.lock index 298cc46..e781545 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -190,6 +190,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_platform_widgets: + dependency: "direct main" + description: + name: flutter_platform_widgets + sha256: "22a86564cb6cc0b93637c813ca91b0b1f61c2681a31e0f9d77590c1fa9f12020" + url: "https://pub.dev" + source: hosted + version: "9.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -308,18 +316,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" mcumgr_flutter: dependency: "direct main" description: @@ -568,10 +576,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.8" tuple: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a3e5773..2f208cc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 flutter_colorpicker: ^1.1.0 + flutter_platform_widgets: ^9.0.0 meta: ^1.16.0 bloc: ^9.1.0 flutter_svg: ^2.0.17 diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index bfffa26..bcc91bf 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:logger/logger.dart'; import 'package:meta/meta.dart'; +import 'package:open_earable_flutter/src/exceptions/device_exception.dart'; import 'package:open_earable_flutter/src/managers/exg/exg_factory.dart'; import 'package:open_earable_flutter/src/models/devices/cosinuss_one_factory.dart'; import 'package:open_earable_flutter/src/models/devices/esense_factory.dart'; @@ -195,7 +196,7 @@ class WearableManager { }) async { if (_connectedIds.contains(device.id)) { logger.w('Device ${device.id} is already connected'); - throw Exception('Device is already connected'); + throw AlreadyConnectedException(); } _connectingStreamController.add(device); @@ -228,9 +229,9 @@ class WearableManager { } _connectedIds.remove(device.id); await _bleManager.disconnect(device.id); - throw Exception('Device is currently not supported'); + throw UnsupportedDeviceException(); } else { - throw Exception('Failed to connect to device'); + throw ConnectionFailedException(); } } @@ -254,12 +255,25 @@ class WearableManager { ); connectedWearables.add(wearable); } catch (e) { - logger.e('Failed to connect to system device ${device.id}: $e'); + logger.e( + 'Failed to connect to system device ${device.id}: ${deviceErrorMessage(e, device.name)}', + ); } } return connectedWearables; } + String deviceErrorMessage(dynamic e, String deviceName) { + return switch (e) { + UnsupportedDeviceException _ => 'Device "$deviceName" is not supported.', + AlreadyConnectedException _ => + 'Device "$deviceName" is already connected.', + ConnectionFailedException _ => + 'Failed to connect to device "$deviceName". Please try again.', + _ => e.toString(), + }; + } + void addPairingRule(PairingRule rule) { _pairingManager.addRule(rule); } @@ -294,7 +308,9 @@ class WearableManager { try { await connectToDevice(discoveredDevice); } catch (e) { - logger.e('Error auto connecting device ${discoveredDevice.id}: $e'); + logger.e( + 'Error auto connecting device ${discoveredDevice.id}: ${deviceErrorMessage(e, discoveredDevice.name)}', + ); } } }); diff --git a/lib/src/exceptions/device_exception.dart b/lib/src/exceptions/device_exception.dart new file mode 100644 index 0000000..f409352 --- /dev/null +++ b/lib/src/exceptions/device_exception.dart @@ -0,0 +1,15 @@ +sealed class DeviceException implements Exception { + const DeviceException(); +} + +class UnsupportedDeviceException extends DeviceException { + const UnsupportedDeviceException(); +} + +class AlreadyConnectedException extends DeviceException { + const AlreadyConnectedException(); +} + +class ConnectionFailedException extends DeviceException { + const ConnectionFailedException(); +} From be609b9a90fa471162253d022c3bf4b55ce7bafc Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:15:54 +0100 Subject: [PATCH 5/6] Add build job for Flutter in Firebase workflow Added a build job to the Firebase hosting workflow for Flutter projects, including steps for checking out the code, getting the Flutter version, and analyzing the project. --- .../workflows/firebase-hosting-pull-request.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index 0c716ba..71ceedf 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -8,6 +8,22 @@ permissions: contents: read pull-requests: write jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get flutter version + run: echo "flutter_version=`cat .flutter_version`" >> $GITHUB_ENV + id: flutter_version + - uses: subosito/flutter-action@v2 + with: + channel: "stable" + cache: true + flutter-version: ${{ env.flutter_version }} + - run: flutter analyze + - run: (cd example && flutter build web --dart-define=BUILD_COMMIT=$(git + rev-parse --short HEAD) --dart-define=BUILD_BRANCH=$(git rev-parse + --abbrev-ref HEAD)) build_and_preview: if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} runs-on: ubuntu-latest From 491df21b21580ab9d65b573cdd87c593dc4ff8aa Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:34:49 +0100 Subject: [PATCH 6/6] fixed issues with flutter analyze, unused imports and missing trailing "," --- example/pubspec.lock | 36 +++++++++++++------------- lib/src/managers/exg/exg_factory.dart | 5 ++-- lib/src/managers/exg/exg_wearable.dart | 12 +++------ 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index e781545..36c8429 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -268,26 +268,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -316,18 +316,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" mcumgr_flutter: dependency: "direct main" description: @@ -340,10 +340,10 @@ packages: dependency: "direct main" description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" nested: dependency: transitive description: @@ -576,10 +576,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.7" tuple: dependency: transitive description: @@ -640,10 +640,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: diff --git a/lib/src/managers/exg/exg_factory.dart b/lib/src/managers/exg/exg_factory.dart index 5e0588e..3b93993 100644 --- a/lib/src/managers/exg/exg_factory.dart +++ b/lib/src/managers/exg/exg_factory.dart @@ -1,4 +1,3 @@ -import 'package:open_earable_flutter/src/models/devices/cosinuss_one.dart'; import 'package:open_earable_flutter/src/models/devices/discovered_device.dart'; import 'package:open_earable_flutter/src/models/devices/wearable.dart'; import 'package:open_earable_flutter/src/models/wearable_factory.dart'; @@ -12,7 +11,7 @@ class ExGFactory extends WearableFactory { @override Future matches(DiscoveredDevice device, List services) async { - final name = (device.name ?? '').trim(); + final name = device.name.trim(); return _nameRegex.hasMatch(name); } @@ -25,7 +24,7 @@ class ExGFactory extends WearableFactory { throw Exception("disconnectNotifier needs to be set before using the factory"); } - final name = (device.name ?? '').trim(); + final name = device.name.trim(); if (!_nameRegex.hasMatch(name)) { throw Exception("device is not an exg device"); } diff --git a/lib/src/managers/exg/exg_wearable.dart b/lib/src/managers/exg/exg_wearable.dart index 9efd8f6..552baac 100644 --- a/lib/src/managers/exg/exg_wearable.dart +++ b/lib/src/managers/exg/exg_wearable.dart @@ -56,7 +56,7 @@ class ExGWearable extends Wearable implements bleManager: _bleManager, discoveredDevice: _discoveredDevice, sensorManager: sensorManager, - )); + ),); _seedInitialConfigValues(); // <— important } @@ -311,9 +311,7 @@ class _ExGSensor extends Sensor { } }); - controller.onCancel = () { - subscription.cancel(); - }; + controller.onCancel = subscription.cancel; return controller.stream; } @@ -343,9 +341,7 @@ class _ExGSensor extends Sensor { return biopotentialFilter.filter(notchFilter.filter(x)); }; } else { - return (double x) { - return biopotentialFilter.filter(x); - }; + return biopotentialFilter.filter; } } else { throw UnimplementedError("Filter type '$btype' or cutoff configuration is not supported."); @@ -442,5 +438,3 @@ class _ExGOrderSensorConfiguration extends SensorConfiguration { wearable._notifyConfigChanged(this, configuration); } } - -