From 420d63c46af77ffc938199add6bc890cdf6148e2 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 12 May 2026 11:17:43 +0200 Subject: [PATCH 01/23] feat(fota): add eraseFirmwareSlot method --- .../Flutter/GeneratedPluginRegistrant.swift | 2 ++ lib/src/fota/firmware_slot_manager_impl.dart | 18 ++++++++++++++---- .../fota_slot_info_capability.dart | 12 ++++++++++++ pubspec.yaml | 5 ++++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 317a6682..301491e5 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 mcumgr_flutter import universal_ble func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FlutterArchivePlugin.register(with: registry.registrar(forPlugin: "FlutterArchivePlugin")) + McumgrFlutterPlugin.register(with: registry.registrar(forPlugin: "McumgrFlutterPlugin")) UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) } diff --git a/lib/src/fota/firmware_slot_manager_impl.dart b/lib/src/fota/firmware_slot_manager_impl.dart index c162b253..941f1397 100644 --- a/lib/src/fota/firmware_slot_manager_impl.dart +++ b/lib/src/fota/firmware_slot_manager_impl.dart @@ -68,15 +68,25 @@ class McuMgrFotaSlotInfoManager implements FotaSlotInfoCapability { @override Future> readFirmwareSlots() async { - final updateManager = await _updateManagerFactory.getUpdateManager(_deviceId); + final updateManager = + await _updateManagerFactory.getUpdateManager(_deviceId); try { final slots = await updateManager.readImageList(); if (slots == null) { return const []; } - return slots - .map(FirmwareSlotInfo.fromImageSlot) - .toList(growable: false); + return slots.map(FirmwareSlotInfo.fromImageSlot).toList(growable: false); + } finally { + await updateManager.kill(); + } + } + + @override + Future eraseFirmwareSlot({int? channel}) async { + final updateManager = + await _updateManagerFactory.getUpdateManager(_deviceId); + try { + await updateManager.erase(channel); } finally { await updateManager.kill(); } diff --git a/lib/src/models/capabilities/fota_slot_info_capability.dart b/lib/src/models/capabilities/fota_slot_info_capability.dart index 0203c2ef..9c3a15ae 100644 --- a/lib/src/models/capabilities/fota_slot_info_capability.dart +++ b/lib/src/models/capabilities/fota_slot_info_capability.dart @@ -9,6 +9,18 @@ import 'package:mcumgr_flutter/mcumgr_flutter.dart'; abstract class FotaSlotInfoCapability { /// Reads the firmware images or slots currently reported by the wearable. Future> readFirmwareSlots(); + + /// Erases an inactive firmware image slot on the wearable. + /// + /// When [channel] is omitted, the underlying firmware backend erases its + /// default secondary image slot. When [channel] is provided, the erase request + /// targets that raw mcumgr image slot channel. + /// + /// Devices reject erase requests for slots that contain a confirmed image, an + /// image pending test on the next reboot, or an active split-image slot. Use + /// [readFirmwareSlots] first when the UI needs to decide whether erasing is + /// available. + Future eraseFirmwareSlot({int? channel}); } /// Snapshot of one firmware image slot reported by the wearable. diff --git a/pubspec.yaml b/pubspec.yaml index 99d92a30..7fb1c2fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,10 @@ dependencies: flutter_bloc: ^9.1.1 http: ^1.1.2 json_annotation: ^4.8.1 - mcumgr_flutter: ^0.6.1 + mcumgr_flutter: + git: + url: https://github.com/DennisMoschina/Flutter-nRF-Connect-Device-Manager.git + ref: master path_provider: ^2.1.1 provider: ^6.1.1 rxdart: ^0.28.0 From d48557911d425b2d94cee4c908a76bca614380b8 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 12 May 2026 11:18:22 +0200 Subject: [PATCH 02/23] doc(fota): added documentation for image erase --- doc/CAPABILITIES.md | 1 + doc/FOTA.md | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/doc/CAPABILITIES.md b/doc/CAPABILITIES.md index e2b68300..17be381c 100644 --- a/doc/CAPABILITIES.md +++ b/doc/CAPABILITIES.md @@ -200,6 +200,7 @@ Provides firmware slot or image-table state for FOTA backends that expose it. final slotInfo = wearable.getCapability(); if (slotInfo != null) { final slots = await slotInfo.readFirmwareSlots(); + await slotInfo.eraseFirmwareSlot(); } ``` diff --git a/doc/FOTA.md b/doc/FOTA.md index f033b50e..4bc92e5f 100644 --- a/doc/FOTA.md +++ b/doc/FOTA.md @@ -259,6 +259,25 @@ Each `FirmwareSlotInfo` contains: This is useful when you want to show the current primary and secondary images before or after an update. +## Erase An Inactive Firmware Slot + +Slot-aware wearables can erase an inactive firmware image slot through +`FotaSlotInfoCapability`: + +```dart +final slotInfo = wearable.getCapability(); +if (slotInfo != null) { + await slotInfo.eraseFirmwareSlot(); + await slotInfo.eraseFirmwareSlot(channel: 1); +} +``` + +When `channel` is omitted, the firmware backend erases its default secondary +image slot. When `channel` is provided, the erase request targets that raw +mcumgr image slot channel. Devices reject erase requests for slots that contain +a confirmed image, an image pending test on the next reboot, or an active +split-image slot. + ## What Happens Internally You do not need to call the lower-level handler classes directly, but it helps to know what `UpdateBloc` is doing: @@ -367,7 +386,7 @@ Recommended UX: - `UnifiedFirmwareRepository` caches results for 15 minutes unless you request a refresh - The current upload path uses `mcumgr_flutter` under the hood - `FotaSlotInfoCapability` is optional and only available on wearables whose firmware backend exposes slot-style state -- `mcumgr_flutter 0.6.1` does not expose an API to erase an individual image slot, so this library does not currently offer slot erase either +- The current local `mcumgr_flutter` integration exposes slot erase through `FotaSlotInfoCapability.eraseFirmwareSlot` ## Related Source Files From 063dc7c21229bfd92b45a965b889000a54965ddc Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 12 May 2026 11:18:48 +0200 Subject: [PATCH 03/23] chore(example): changed dependency of mcumgr_flutter to be compatible with library --- example/pubspec.lock | 33 +++++++++++++++++++++++++-------- example/pubspec.yaml | 5 ++++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index a88ca564..957ea6bf 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" args: dependency: transitive description: @@ -315,11 +323,12 @@ packages: mcumgr_flutter: dependency: "direct main" description: - name: mcumgr_flutter - sha256: fbf2f621dea23dd5dc70494e700c5d4706010841b9e68739ba563cbd88c7e8ba - url: "https://pub.dev" - source: hosted - version: "0.6.1" + path: "." + ref: master + resolved-ref: "7bec874051397e0e0dcbce4a1a660e14be5a3eb3" + url: "https://github.com/DennisMoschina/Flutter-nRF-Connect-Device-Manager.git" + source: git + version: "0.8.1" meta: dependency: "direct main" description: @@ -350,7 +359,7 @@ packages: path: ".." relative: true source: path - version: "2.3.5" + version: "2.3.6" path: dependency: transitive description: @@ -487,14 +496,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" protobuf: dependency: transitive description: name: protobuf - sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e + sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "6.0.0" provider: dependency: "direct main" description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2f208ccd..5e4c81fa 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -48,7 +48,10 @@ dependencies: flutter_svg: ^2.0.17 provider: ^6.1.5 file_picker: ^10.3.7 - mcumgr_flutter: ^0.6.1 + mcumgr_flutter: + git: + url: https://github.com/DennisMoschina/Flutter-nRF-Connect-Device-Manager.git + ref: master flutter_bloc: ^9.1.1 dev_dependencies: From d20f63ba17c87498f55a613650dcfbe52f6be4f5 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 18 May 2026 09:35:11 +0200 Subject: [PATCH 04/23] chore(dependencies): update mcumgr_flutter to version 0.9.0 --- example/linux/flutter/generated_plugins.cmake | 1 + example/pubspec.lock | 187 +++++++++++++----- example/pubspec.yaml | 5 +- .../windows/flutter/generated_plugins.cmake | 1 + pubspec.yaml | 5 +- 5 files changed, 137 insertions(+), 62 deletions(-) diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 2e1de87a..be1ee3e5 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/example/pubspec.lock b/example/pubspec.lock index 957ea6bf..8a77d9fb 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" bloc: dependency: "direct main" description: name: bloc - sha256: a2cebb899f91d36eeeaa55c7b20b5915db5a9df1b8fd4a3c9c825e22e474537d + sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413 url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.2.1" bluez: dependency: transitive description: @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -85,10 +93,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" url: "https://pub.dev" source: hosted - version: "0.3.5+1" + version: "0.3.5+2" crypto: dependency: transitive description: @@ -101,26 +109,26 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" dbus: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" equatable: dependency: transitive description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: @@ -133,18 +141,26 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" file_picker: dependency: "direct main" description: name: file_picker - sha256: "7872545770c277236fd32b022767576c562ba28366204ff1a5628853cf8f2200" + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" url: "https://pub.dev" source: hosted - version: "10.3.7" + version: "10.3.10" fixnum: dependency: transitive description: @@ -162,10 +178,10 @@ packages: dependency: transitive description: name: flutter_archive - sha256: "5ca235f304c12bf468979235f400f79846d204169d715939e39197106f5fc970" + sha256: e433389fde0bdfc20af40784fdb6d91753794b40cf708a43f95244fcd5d6c298 url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.0.4" flutter_bloc: dependency: "direct main" description: @@ -202,18 +218,18 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" url: "https://pub.dev" source: hosted - version: "2.0.32" + version: "2.0.34" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -232,6 +248,22 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.dev" + source: hosted + version: "1.0.3" http: dependency: transitive description: @@ -248,14 +280,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" json_annotation: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.12.0" leak_tracker: dependency: transitive description: @@ -284,18 +332,18 @@ packages: dependency: transitive description: name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" logger: dependency: transitive description: name: logger - sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.7.0" logging: dependency: transitive description: @@ -323,12 +371,11 @@ packages: mcumgr_flutter: dependency: "direct main" description: - path: "." - ref: master - resolved-ref: "7bec874051397e0e0dcbce4a1a660e14be5a3eb3" - url: "https://github.com/DennisMoschina/Flutter-nRF-Connect-Device-Manager.git" - source: git - version: "0.8.1" + name: mcumgr_flutter + sha256: "8165dd611941cabfa0b3c1c3fb7f57fda26a86a2336c896ea263fd0f0c04c605" + url: "https://pub.dev" + source: hosted + version: "0.9.0" meta: dependency: "direct main" description: @@ -337,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" nested: dependency: transitive description: @@ -349,10 +404,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "1f81ed9e41909d44162d7ec8663b2c647c202317cc0b56d3d56f6a13146a0b64" + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.3.0" open_earable_flutter: dependency: "direct main" description: @@ -360,6 +415,14 @@ packages: relative: true source: path version: "2.3.6" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -388,18 +451,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "95c68a74d3cab950fd0ed8073d9fab15c1c06eb1f3eec68676e87aabc9ecee5a" + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" url: "https://pub.dev" source: hosted - version: "2.2.21" + version: "2.3.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "6192e477f34018ef1ea790c56fffc7302e3bc3efede9e798b934c252c8c105ba" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -476,10 +539,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -528,6 +591,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" rxdart: dependency: transitive description: @@ -545,10 +616,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" stack_trace: dependency: transitive description: @@ -617,18 +688,18 @@ packages: dependency: transitive description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + sha256: "4d35a36400983c3457c289d4d553b5308f506ea84f7e51c7a564651b5525209a" url: "https://pub.dev" source: hosted - version: "1.1.19" + version: "1.2.1" vector_graphics_codec: dependency: transitive description: @@ -641,10 +712,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + sha256: "98e7e94de127b46a86ef46197fff84ff99f3d3b80a708390d717ad731efef598" url: "https://pub.dev" source: hosted - version: "1.1.19" + version: "1.2.2" vector_math: dependency: transitive description: @@ -657,10 +728,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.2.0" web: dependency: transitive description: @@ -693,6 +764,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5e4c81fa..e188ae40 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -48,10 +48,7 @@ dependencies: flutter_svg: ^2.0.17 provider: ^6.1.5 file_picker: ^10.3.7 - mcumgr_flutter: - git: - url: https://github.com/DennisMoschina/Flutter-nRF-Connect-Device-Manager.git - ref: master + mcumgr_flutter: ^0.9.0 flutter_bloc: ^9.1.1 dev_dependencies: diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 3dcad9f2..7e0c5455 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.yaml b/pubspec.yaml index 7fb1c2fc..443223ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,10 +30,7 @@ dependencies: flutter_bloc: ^9.1.1 http: ^1.1.2 json_annotation: ^4.8.1 - mcumgr_flutter: - git: - url: https://github.com/DennisMoschina/Flutter-nRF-Connect-Device-Manager.git - ref: master + mcumgr_flutter: ^0.9.0 path_provider: ^2.1.1 provider: ^6.1.1 rxdart: ^0.28.0 From 282432158c6dcb74d87443272344f34b7b9d0abe Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 18 May 2026 11:00:48 +0200 Subject: [PATCH 05/23] chore(changelog): updated changelog for 2.3.7 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90adf51b..b46e6620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.7 + +* added erase firmware image slot function for FOTA slot info capability + ## 2.3.6 * added function to get all connected system devices From 4ef7f62bb4525bfab73418adcad115bae0a36214 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 18 May 2026 11:29:11 +0200 Subject: [PATCH 06/23] chore(version): bump version to 2.3.7 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 443223ff..f33e62c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: open_earable_flutter description: This package provides functionality for interacting with OpenEarable devices. Control LED colors, control audio, and access raw sensor data. -version: 2.3.6 +version: 2.3.7 repository: https://github.com/OpenEarable/open_earable_flutter/tree/main platforms: From e904261019391adde4acdba398ce75a33b6d2982 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 19 May 2026 15:41:20 +0200 Subject: [PATCH 07/23] chore(deps): update flutter dependencies --- .../fota/firmware_select/firmware_list.dart | 2 +- example/pubspec.lock | 32 +++++++++---------- example/pubspec.yaml | 6 ++-- lib/src/managers/ble_manager.dart | 1 + pubspec.yaml | 6 ++-- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/example/lib/widgets/fota/firmware_select/firmware_list.dart b/example/lib/widgets/fota/firmware_select/firmware_list.dart index 1c8c8225..fce06c19 100644 --- a/example/lib/widgets/fota/firmware_select/firmware_list.dart +++ b/example/lib/widgets/fota/firmware_select/firmware_list.dart @@ -20,7 +20,7 @@ class FirmwareList extends StatelessWidget { floatingActionButton: FloatingActionButton( onPressed: () async { // Navigator.pop(context, 'Firmware'); - FilePickerResult? result = await FilePicker.platform.pickFiles( + FilePickerResult? result = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: ['zip', 'bin'], ); diff --git a/example/pubspec.lock b/example/pubspec.lock index 8a77d9fb..93e06f85 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 url: "https://pub.dev" source: hosted - version: "10.3.10" + version: "11.0.2" fixnum: dependency: transitive description: @@ -210,10 +210,10 @@ packages: dependency: "direct main" description: name: flutter_platform_widgets - sha256: "22a86564cb6cc0b93637c813ca91b0b1f61c2681a31e0f9d77590c1fa9f12020" + sha256: aa110ef638076831d060047911a62810d02b4695db58e7682b716c4c4eee65bc url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -372,10 +372,10 @@ packages: dependency: "direct main" description: name: mcumgr_flutter - sha256: "8165dd611941cabfa0b3c1c3fb7f57fda26a86a2336c896ea263fd0f0c04c605" + sha256: ad2647f188cb8ecbc5c5da1fc136e47103e1956706fdc343a5774ace797be160 url: "https://pub.dev" source: hosted - version: "0.9.0" + version: "0.9.1" meta: dependency: "direct main" description: @@ -414,7 +414,7 @@ packages: path: ".." relative: true source: path - version: "2.3.6" + version: "2.3.7" package_config: dependency: transitive description: @@ -571,10 +571,10 @@ packages: dependency: transitive description: name: protobuf - sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" + sha256: "2fcc8a202ca7ec17dab7c97d6b6d91cf03aa07fe6f65f8afbb6dfa52cc5bd902" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "5.1.0" provider: dependency: "direct main" description: @@ -680,10 +680,10 @@ packages: dependency: transitive description: name: universal_ble - sha256: "6a5c6c1fb295015934a5aef3dc751ae7e00721535275f8478bfe74db77b238c5" + sha256: "8e5f4e2827375900b805fe3eb6cb8a305ade2303289d9a5cba0d09a3a37fea29" url: "https://pub.dev" source: hosted - version: "0.21.1" + version: "2.0.1" uuid: dependency: transitive description: @@ -696,10 +696,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "4d35a36400983c3457c289d4d553b5308f506ea84f7e51c7a564651b5525209a" + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" vector_graphics_codec: dependency: transitive description: @@ -712,10 +712,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "98e7e94de127b46a86ef46197fff84ff99f3d3b80a708390d717ad731efef598" + sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" vector_math: dependency: transitive description: @@ -773,5 +773,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.3 <4.0.0" + dart: ">=3.11.4 <4.0.0" flutter: ">=3.38.4" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e188ae40..e0788693 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -42,13 +42,13 @@ 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 + flutter_platform_widgets: ^10.0.1 meta: ^1.16.0 bloc: ^9.1.0 flutter_svg: ^2.0.17 provider: ^6.1.5 - file_picker: ^10.3.7 - mcumgr_flutter: ^0.9.0 + file_picker: ^11.0.2 + mcumgr_flutter: ^0.9.1 flutter_bloc: ^9.1.1 dev_dependencies: diff --git a/lib/src/managers/ble_manager.dart b/lib/src/managers/ble_manager.dart index de50c86b..a8a73f16 100644 --- a/lib/src/managers/ble_manager.dart +++ b/lib/src/managers/ble_manager.dart @@ -77,6 +77,7 @@ class BleManager extends BleGattManager { String deviceId, String characteristicId, Uint8List value, + int? timestamp, ) { String streamIdentifier = _getCharacteristicKey(deviceId, characteristicId); diff --git a/pubspec.yaml b/pubspec.yaml index f33e62c7..9c32a858 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,16 +21,16 @@ dependencies: typed_data: ^1.3.2 convert: ^3.1.1 permission_handler: ^12.0.1 - universal_ble: ^0.21.1 + universal_ble: ^2.0.1 logger: ^2.5.0 # for fota: equatable: ^2.0.5 - file_picker: ^10.3.7 + file_picker: ^11.0.2 flutter_archive: ^6.0.0 flutter_bloc: ^9.1.1 http: ^1.1.2 json_annotation: ^4.8.1 - mcumgr_flutter: ^0.9.0 + mcumgr_flutter: ^0.9.1 path_provider: ^2.1.1 provider: ^6.1.1 rxdart: ^0.28.0 From df58a9bbd9fed6f15801bae0877c8d5de8079763 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 19 May 2026 16:47:26 +0200 Subject: [PATCH 08/23] chore: add .pubignore to exclude build artifacts and Python cache files --- .pubignore | 3 +++ lib/src/fota/utils/device.dart | 0 2 files changed, 3 insertions(+) create mode 100644 .pubignore delete mode 100644 lib/src/fota/utils/device.dart diff --git a/.pubignore b/.pubignore new file mode 100644 index 00000000..033a1a10 --- /dev/null +++ b/.pubignore @@ -0,0 +1,3 @@ +build/ +**/__pycache__/ +*.pyc diff --git a/lib/src/fota/utils/device.dart b/lib/src/fota/utils/device.dart deleted file mode 100644 index e69de29b..00000000 From 52e69dd42d42d0980a65fcd9c164f483f6233f69 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 20 May 2026 15:19:16 +0200 Subject: [PATCH 09/23] chore(flutter_version): bump flutter version to 3.44.0 --- .flutter_version | 2 +- example/pubspec.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.flutter_version b/.flutter_version index cac6068c..faf0dcbb 100644 --- a/.flutter_version +++ b/.flutter_version @@ -1 +1 @@ -3.32.0 +3.44.0 diff --git a/example/pubspec.lock b/example/pubspec.lock index 93e06f85..30301d6c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -380,10 +380,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" native_toolchain_c: dependency: transitive description: @@ -656,10 +656,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" tuple: dependency: transitive description: From be3df2f33cf35e1ad82dcf83d64dd570c5b2922a Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 20 May 2026 21:56:20 +0200 Subject: [PATCH 10/23] chore(version): bump version to 2.3.8 and update changelog for new dependencies --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b46e6620..00d95288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.8 + +* updated dependencies to latest versions + ## 2.3.7 * added erase firmware image slot function for FOTA slot info capability diff --git a/pubspec.yaml b/pubspec.yaml index 9c32a858..eaeb84f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: open_earable_flutter description: This package provides functionality for interacting with OpenEarable devices. Control LED colors, control audio, and access raw sensor data. -version: 2.3.7 +version: 2.3.8 repository: https://github.com/OpenEarable/open_earable_flutter/tree/main platforms: From 95d9aa28dedf19d8fcf369fc32f9b803dcb93222 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Thu, 21 May 2026 10:41:32 +0300 Subject: [PATCH 11/23] feat(web): add browser support --- lib/src/managers/ble_manager.dart | 38 +++++++++- .../models/devices/open_earable_factory.dart | 37 +++++++--- lib/src/models/devices/open_earable_v2.dart | 8 +++ .../v2_sensor_scheme_reader.dart | 72 +++++++++++++++---- .../v2_sensor_value_parser.dart | 8 ++- 5 files changed, 136 insertions(+), 27 deletions(-) diff --git a/lib/src/managers/ble_manager.dart b/lib/src/managers/ble_manager.dart index a8a73f16..68b4fab0 100644 --- a/lib/src/managers/ble_manager.dart +++ b/lib/src/managers/ble_manager.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:open_earable_flutter/src/constants.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:universal_ble/universal_ble.dart'; @@ -10,6 +11,29 @@ import '../../open_earable_flutter.dart'; /// A class that establishes and manages Bluetooth Low Energy (BLE) /// communication with OpenEarable devices. class BleManager extends BleGattManager { + // Web Bluetooth requires services to be declared at requestDevice time. + static const Set _webOptionalServiceUuids = { + sensorServiceUuid, + deviceInfoServiceUuid, + parseInfoServiceUuid, + audioPlayerServiceUuid, + batteryServiceUuid, + buttonServiceUuid, + ledServiceUuid, + // OpenEarable V2 + "1410df95-5f68-4ebb-a7c7-5e0fb9ae7557", // audio config service + "2e04cbf7-939d-4be5-823e-271838b75259", // time sync service + "8d53dc1d-1db7-4cd3-868b-8a527460aa84", // mcumgr SMP service + // OpenRing + "bae80001-4f05-4503-8e65-3af1f7329d1f", + // Other supported devices + "0000a000-1212-efde-1523-785feabcd123", // Cosinuss PPG+ACC + "00001809-0000-1000-8000-00805f9b34fb", // temperature + "0000180d-0000-1000-8000-00805f9b34fb", // heart rate + "0000180a-0000-1000-8000-00805f9b34fb", // device information + "ff06", // eSense + }; + static const int _desiredMtu = 60; int _mtu = _desiredMtu; // Largest Byte package sent is 42 bytes for IMU int get mtu => _mtu; @@ -164,7 +188,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; } diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index 11dadebd..bfde824f 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -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'; @@ -52,7 +53,8 @@ class OpenEarableFactory extends WearableFactory { } @override - Future createFromDevice(DiscoveredDevice device, { Set options = const {} }) async { + Future createFromDevice(DiscoveredDevice device, + {Set options = const {}}) async { if (bleManager == null) { throw Exception("bleManager needs to be set before using the factory"); } @@ -91,7 +93,8 @@ 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( OpenEarableV2TimeSyncImp( bleManager: bleManager!, @@ -151,15 +154,22 @@ class OpenEarableFactory extends WearableFactory { List 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 sensorConfigurationValues = []; + List 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) { @@ -211,7 +221,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( @@ -229,16 +241,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 = >{}; 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, diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index e2b617d6..f6fb4633 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:open_earable_flutter/src/constants.dart'; import 'package:open_earable_flutter/src/models/devices/bluetooth_wearable.dart'; import 'package:pub_semver/pub_semver.dart'; @@ -688,6 +689,13 @@ class OpenEarableV2TimeSyncImp implements TimeSynchronizable { @override Future synchronizeTime() async { + if (kIsWeb) { + logger.i( + 'Skipping OpenEarable V2 time synchronization on web because the packet format uses Uint64 serialization, which is unsupported by dart2js.', + ); + return; + } + logger.i("Synchronizing time with OpenEarable V2 device..."); // Will complete when we have enough samples and wrote the final offset. diff --git a/lib/src/utils/sensor_scheme_parser/v2_sensor_scheme_reader.dart b/lib/src/utils/sensor_scheme_parser/v2_sensor_scheme_reader.dart index 78f8b9be..07b03df8 100644 --- a/lib/src/utils/sensor_scheme_parser/v2_sensor_scheme_reader.dart +++ b/lib/src/utils/sensor_scheme_parser/v2_sensor_scheme_reader.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:open_earable_flutter/open_earable_flutter.dart' show logger; import 'package:open_earable_flutter/src/constants.dart'; @@ -29,10 +30,20 @@ class V2SensorSchemeReader extends SensorSchemeReader { } int sensorIdCount = sensorIdBuffer[0]; - List sensorIds = sensorIdBuffer.sublist(1, sensorIdCount + 1); + if (sensorIdBuffer.length < 1 + sensorIdCount) { + logger.w( + "Sensor id buffer shorter than expected (count=$sensorIdCount, len=${sensorIdBuffer.length}).", + ); + } + + List sensorIds = sensorIdBuffer.length >= 1 + sensorIdCount + ? sensorIdBuffer.sublist(1, sensorIdCount + 1) + : sensorIdBuffer.sublist(1); _sensorIds.clear(); _sensorIds.addAll(sensorIds); + + logger.d("Parsed sensor ids: $sensorIds (count: $sensorIdCount)"); } @override @@ -55,10 +66,8 @@ class V2SensorSchemeReader extends SensorSchemeReader { characteristicId: sensorSchemeCharacteristicUuid, ); - final Future> responseFuture = stream - .cast>() - .first - .timeout(const Duration(seconds: 5)); + final Future> responseFuture = + stream.cast>().first.timeout(const Duration(seconds: 5)); // Request sensor value only after the listener/future is set up await _bleManager.write( @@ -70,16 +79,23 @@ class V2SensorSchemeReader extends SensorSchemeReader { try { final value = await responseFuture; - logger.d("Received notification for sensor scheme of sensor $sensorId: $value"); + logger.d( + "Received notification for sensor scheme of sensor $sensorId: $value", + ); final scheme = _parseSensorScheme(value); - if (scheme.sensorId != sensorId) { - throw Exception( - "Sensor id mismatch. Expected: $sensorId, got: ${scheme.sensorId}", + if (scheme.sensorId == 0 && sensorId != 0) { + logger.w( + "Sensor scheme response for sensor $sensorId omitted the sensor id. Using the requested id.", + ); + scheme.sensorId = sensorId; + } else if (scheme.sensorId != sensorId) { + logger.w( + "Sensor scheme response for sensor $sensorId reported sensor id ${scheme.sensorId}. Using the returned scheme.", ); } - _sensorSchemes[sensorId] = scheme; + _sensorSchemes[scheme.sensorId] = scheme; return scheme; } on TimeoutException catch (e) { throw TimeoutException("Timeout while waiting for sensor scheme: $e"); @@ -94,11 +110,29 @@ class V2SensorSchemeReader extends SensorSchemeReader { for (int sensorId in _sensorIds) { if (!_sensorSchemes.containsKey(sensorId) || forceRead) { - SensorScheme scheme = await getSchemeForSensor(sensorId); - _sensorSchemes[sensorId] = scheme; + try { + SensorScheme scheme = await getSchemeForSensor(sensorId); + _sensorSchemes[scheme.sensorId] = scheme; + } catch (e) { + logger.e( + "Failed to read sensor scheme for sensor $sensorId: $e${kIsWeb ? ' (on web platform)' : ''}", + ); + if (kIsWeb) { + logger.d( + "Skipping sensor $sensorId due to read failure on web. " + "This may be a BLE notification timeout or subscription issue.", + ); + } + // Continue with next sensor instead of failing entirely + continue; + } } } + logger.d( + "Successfully read ${_sensorSchemes.length} sensor scheme(s): " + "${_sensorSchemes.keys.join(', ')}", + ); return _sensorSchemes.values.toList(); } @@ -144,8 +178,12 @@ class V2SensorSchemeReader extends SensorSchemeReader { String unitName = utf8.decode(unitNameBytes); currentIndex += unitNameLength; - Component component = - Component(ParseType.fromInt(componentType), groupName, componentName, unitName); + Component component = Component( + ParseType.fromInt(componentType), + groupName, + componentName, + unitName, + ); sensorScheme.components.add(component); } @@ -175,7 +213,11 @@ class V2SensorSchemeReader extends SensorSchemeReader { freqs.add(byteData.getFloat32(0, Endian.little)); } currentIndex += frequencyCount * 4; - frequencies = SensorConfigFrequencies(maxStreamingFreqIndex, defaultFreqIndex, freqs); + frequencies = SensorConfigFrequencies( + maxStreamingFreqIndex, + defaultFreqIndex, + freqs, + ); } sensorScheme.options = SensorConfigOptions(features, frequencies); diff --git a/lib/src/utils/sensor_value_parser/v2_sensor_value_parser.dart b/lib/src/utils/sensor_value_parser/v2_sensor_value_parser.dart index 9b4e7c27..4a9b2d0c 100644 --- a/lib/src/utils/sensor_value_parser/v2_sensor_value_parser.dart +++ b/lib/src/utils/sensor_value_parser/v2_sensor_value_parser.dart @@ -25,7 +25,7 @@ class V2SensorValueParser extends SensorValueParser { ); _requireBytes(data, i, 8, 'timestamp'); - final baseTimestamp = data.getUint64(i, Endian.little); + final baseTimestamp = _readUint64(data, i); i += 8; // Precompute size of one component payload for efficiency. @@ -137,6 +137,12 @@ int _getTimeDiff(ByteData data) { return data.getUint16(data.lengthInBytes - 2, Endian.little); } +int _readUint64(ByteData data, int index) { + final low = data.getUint32(index, Endian.little); + final high = data.getUint32(index + 4, Endian.little); + return high * 0x100000000 + low; +} + _ParsedSample _parseSample({ required ByteData data, required int startIndex, From 5d1ce7fc9d2534b774f868b6345b22ca5e4fccf3 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Thu, 21 May 2026 10:53:07 +0300 Subject: [PATCH 12/23] style: add missing trailing commas --- lib/src/models/devices/open_earable_factory.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index bfde824f..1b394ba4 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -53,8 +53,10 @@ class OpenEarableFactory extends WearableFactory { } @override - Future createFromDevice(DiscoveredDevice device, - {Set options = const {}}) async { + Future createFromDevice( + DiscoveredDevice device, { + Set options = const {}, + }) async { if (bleManager == null) { throw Exception("bleManager needs to be set before using the factory"); } @@ -94,7 +96,9 @@ class OpenEarableFactory extends WearableFactory { isConnectedViaSystem: options.contains(const ConnectedViaSystem()), ); if (await bleManager!.hasService( - deviceId: device.id, serviceId: timeSynchronizationServiceUuid)) { + deviceId: device.id, + serviceId: timeSynchronizationServiceUuid, + )) { wearable.registerCapability( OpenEarableV2TimeSyncImp( bleManager: bleManager!, @@ -156,7 +160,8 @@ class OpenEarableFactory extends WearableFactory { logger.d("Platform: ${kIsWeb ? 'WEB/Chrome' : 'NATIVE'}"); logger.d( - "Sensor schemes provided by device: ${sensorSchemes.map((s) => 'ID:${s.sensorId}(${s.sensorName})').join(', ')}"); + "Sensor schemes provided by device: ${sensorSchemes.map((s) => 'ID:${s.sensorId}(${s.sensorName})').join(', ')}", + ); for (SensorScheme scheme in sensorSchemes) { List sensorConfigurationValues = From 06eadb74058487426a9281d1acdcc0316ab93be8 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Thu, 21 May 2026 12:33:55 +0200 Subject: [PATCH 13/23] feat(wearable-factories): get list of ble services from factories in order to make services available from web --- lib/open_earable_flutter.dart | 3 ++ lib/src/managers/ble_manager.dart | 27 +--------- .../models/devices/cosinuss_one_factory.dart | 36 +++++++++---- lib/src/models/devices/esense_factory.dart | 50 +++++++++++++------ .../models/devices/open_earable_factory.dart | 8 +++ lib/src/models/devices/open_earable_v1.dart | 9 ++++ lib/src/models/devices/open_earable_v2.dart | 40 +++++++++------ lib/src/models/devices/open_ring_factory.dart | 5 ++ lib/src/models/devices/polar_factory.dart | 11 +++- lib/src/models/wearable_factory.dart | 12 ++++- 10 files changed, 134 insertions(+), 67 deletions(-) diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index c3262467..da52dc0b 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -200,6 +200,9 @@ class WearableManager { }) { return _bleManager.startScan( checkAndRequestPermissions: checkAndRequestPermissions, + webOptionalServiceUuids: { + for (final factory in _wearableFactories) ...factory.usedServiceUuids, + }, ); } diff --git a/lib/src/managers/ble_manager.dart b/lib/src/managers/ble_manager.dart index 68b4fab0..2f6b54e4 100644 --- a/lib/src/managers/ble_manager.dart +++ b/lib/src/managers/ble_manager.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:open_earable_flutter/src/constants.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:universal_ble/universal_ble.dart'; @@ -11,29 +10,6 @@ import '../../open_earable_flutter.dart'; /// A class that establishes and manages Bluetooth Low Energy (BLE) /// communication with OpenEarable devices. class BleManager extends BleGattManager { - // Web Bluetooth requires services to be declared at requestDevice time. - static const Set _webOptionalServiceUuids = { - sensorServiceUuid, - deviceInfoServiceUuid, - parseInfoServiceUuid, - audioPlayerServiceUuid, - batteryServiceUuid, - buttonServiceUuid, - ledServiceUuid, - // OpenEarable V2 - "1410df95-5f68-4ebb-a7c7-5e0fb9ae7557", // audio config service - "2e04cbf7-939d-4be5-823e-271838b75259", // time sync service - "8d53dc1d-1db7-4cd3-868b-8a527460aa84", // mcumgr SMP service - // OpenRing - "bae80001-4f05-4503-8e65-3af1f7329d1f", - // Other supported devices - "0000a000-1212-efde-1523-785feabcd123", // Cosinuss PPG+ACC - "00001809-0000-1000-8000-00805f9b34fb", // temperature - "0000180d-0000-1000-8000-00805f9b34fb", // heart rate - "0000180a-0000-1000-8000-00805f9b34fb", // device information - "ff06", // eSense - }; - static const int _desiredMtu = 60; int _mtu = _desiredMtu; // Largest Byte package sent is 42 bytes for IMU int get mtu => _mtu; @@ -150,6 +126,7 @@ class BleManager extends BleGattManager { /// Initiates the BLE device scan to discover nearby Bluetooth devices. Future startScan({ bool checkAndRequestPermissions = true, + Set webOptionalServiceUuids = const {}, }) async { bool? permGranted; @@ -196,7 +173,7 @@ class BleManager extends BleGattManager { ? PlatformConfig( web: WebOptions( optionalServices: - _webOptionalServiceUuids.toList(growable: false), + webOptionalServiceUuids.toList(growable: false), ), ) : null, diff --git a/lib/src/models/devices/cosinuss_one_factory.dart b/lib/src/models/devices/cosinuss_one_factory.dart index 5eab8e2f..68d47397 100644 --- a/lib/src/models/devices/cosinuss_one_factory.dart +++ b/lib/src/models/devices/cosinuss_one_factory.dart @@ -8,28 +8,44 @@ class CosinussOneFactory extends WearableFactory { static const String _name = "earconnect"; @override - Future matches(DiscoveredDevice device, List services) async { + Set get usedServiceUuids => const { + CosinussOne.ppgAndAccServiceUuid, + CosinussOne.temperatureServiceUuid, + CosinussOne.heartRateServiceUuid, + CosinussOne.batteryServiceUuid, + }; + + @override + Future matches( + DiscoveredDevice device, + List services, + ) async { return device.name == _name; } @override - Future createFromDevice(DiscoveredDevice device, { Set options = const {} }) async { + Future createFromDevice( + DiscoveredDevice device, { + Set 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, + ); } } diff --git a/lib/src/models/devices/esense_factory.dart b/lib/src/models/devices/esense_factory.dart index 0e948891..1f798ad2 100644 --- a/lib/src/models/devices/esense_factory.dart +++ b/lib/src/models/devices/esense_factory.dart @@ -15,9 +15,15 @@ import 'wearable.dart'; class EsenseFactory extends WearableFactory { @override - Future createFromDevice(DiscoveredDevice device, - {Set options = const {},}) async { + Set get usedServiceUuids => const { + esenseServiceUuid, + }; + @override + Future createFromDevice( + DiscoveredDevice device, { + Set options = const {}, + }) async { EsenseSensorHandler sensorHandler = EsenseSensorHandler( bleGattManager: bleManager!, discoveredDevice: device, @@ -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, @@ -71,12 +84,15 @@ class EsenseFactory extends WearableFactory { ), ], ); - + return esense; } @override - Future matches(DiscoveredDevice device, List services) async { + Future matches( + DiscoveredDevice device, + List services, + ) async { return RegExp(r'^eSense-\d{4}$').hasMatch(device.name); } } @@ -127,7 +143,9 @@ class EsenseSensor extends Sensor { } 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}", + ); } } diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index 1b394ba4..a15c0c70 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -34,6 +34,14 @@ class OpenEarableFactory extends WearableFactory { final _v1Regex = RegExp(r'^1\.\d+\.\d+$'); final _v2Regex = RegExp(r'^2\.\d+\.\d+$'); + @override + Set get usedServiceUuids => { + ...OpenEarableV1.serviceUuids, + ...OpenEarableV2.serviceUuids, + mcuMgrSmpServiceUuid, + timeSynchronizationServiceUuid, + }; + @override Future matches( DiscoveredDevice device, diff --git a/lib/src/models/devices/open_earable_v1.dart b/lib/src/models/devices/open_earable_v1.dart index 26cf4069..6366c52e 100644 --- a/lib/src/models/devices/open_earable_v1.dart +++ b/lib/src/models/devices/open_earable_v1.dart @@ -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 serviceUuids = { + sensorServiceUuid, + parseInfoServiceUuid, + deviceInfoServiceUuid, + ledServiceUuid, + audioPlayerServiceUuid, + buttonServiceUuid, + batteryServiceUuid, + }; final List _sensors; final List _sensorConfigurations; diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index f6fb4633..9ebb78b2 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -36,7 +36,8 @@ const String _audioModeCharacteristicUuid = const String _buttonServiceUuid = "29c10bdc-4773-11ee-be56-0242ac120002"; const String _buttonCharacteristicUuid = "29c10f38-4773-11ee-be56-0242ac120002"; -const String timeSynchronizationServiceUuid = "2e04cbf7-939d-4be5-823e-271838b75259"; +const String timeSynchronizationServiceUuid = + "2e04cbf7-939d-4be5-823e-271838b75259"; const String _timeSyncTimeMappingCharacteristicUuid = "2e04cbf8-939d-4be5-823e-271838b75259"; const String _timeSyncRttCharacteristicUuid = @@ -59,11 +60,11 @@ final VersionConstraint _versionConstraint = /// as well as health and energy status. class OpenEarableV2 extends BluetoothWearable with - DeviceFirmwareVersionNumberExt, - BatteryLevelStatusGattReader, - BatteryLevelStatusServiceGattReader, - BatteryHealthStatusGattReader, - BatteryEnergyStatusGattReader + DeviceFirmwareVersionNumberExt, + BatteryLevelStatusGattReader, + BatteryLevelStatusServiceGattReader, + BatteryHealthStatusGattReader, + BatteryEnergyStatusGattReader implements SensorManager, SensorConfigurationManager, @@ -83,6 +84,15 @@ class OpenEarableV2 extends BluetoothWearable "45622510-6468-465a-b141-0b9b0f96b468"; static const String ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; static const String batteryServiceUuid = "180F"; + static const Set serviceUuids = { + sensorServiceUuid, + parseInfoServiceUuid, + deviceInfoServiceUuid, + ledServiceUuid, + batteryServiceUuid, + _buttonServiceUuid, + _audioConfigServiceUuid, + }; final List _sensors; final List _sensorConfigurations; @@ -192,7 +202,6 @@ class OpenEarableV2 extends BluetoothWearable StreamSubscription? _sensorConfigSubscription; StreamSubscription? _buttonSubscription; - @override final Set availableMicrophones; @override @@ -708,10 +717,10 @@ class OpenEarableV2TimeSyncImp implements TimeSynchronizable { late final StreamSubscription> rttSub; rttSub = bleManager .subscribe( - deviceId: deviceId, - serviceId: timeSynchronizationServiceUuid, - characteristicId: _timeSyncRttCharacteristicUuid, - ) + deviceId: deviceId, + serviceId: timeSynchronizationServiceUuid, + characteristicId: _timeSyncRttCharacteristicUuid, + ) .listen( (data) async { final t4 = DateTime.now().microsecondsSinceEpoch; @@ -723,8 +732,9 @@ class OpenEarableV2TimeSyncImp implements TimeSynchronizable { logger.d("Received time sync response packet: $pkt"); - final t1 = pkt.timePhoneSend; // phone send timestamp (µs) - final t3 = pkt.timeDeviceSend; // device send timestamp (µs, device clock) + final t1 = pkt.timePhoneSend; // phone send timestamp (µs) + final t3 = + pkt.timeDeviceSend; // device send timestamp (µs, device clock) // Estimate Unix time at the moment the device sent the response. // Use midpoint between T1 and T4 as an estimate of when the device was "in the middle". @@ -762,7 +772,9 @@ class OpenEarableV2TimeSyncImp implements TimeSynchronizable { } }, onError: (error, stack) async { - logger.e("Error during time sync subscription $error, $stack",); + logger.e( + "Error during time sync subscription $error, $stack", + ); if (!completer.isCompleted) { completer.completeError(error, stack); } diff --git a/lib/src/models/devices/open_ring_factory.dart b/lib/src/models/devices/open_ring_factory.dart index 9fd1bb5d..c40f2ff0 100644 --- a/lib/src/models/devices/open_ring_factory.dart +++ b/lib/src/models/devices/open_ring_factory.dart @@ -18,6 +18,11 @@ import 'open_ring.dart'; import 'wearable.dart'; class OpenRingFactory extends WearableFactory { + @override + Set get usedServiceUuids => const { + OpenRingGatt.service, + }; + @override Future createFromDevice( DiscoveredDevice device, { diff --git a/lib/src/models/devices/polar_factory.dart b/lib/src/models/devices/polar_factory.dart index dbbd6392..c0ab8c6f 100644 --- a/lib/src/models/devices/polar_factory.dart +++ b/lib/src/models/devices/polar_factory.dart @@ -15,7 +15,16 @@ class PolarFactory extends WearableFactory { static const String _namePrefix = "Polar"; @override - Future createFromDevice(DiscoveredDevice device, { Set options = const {} }) async { + Set get usedServiceUuids => const { + Polar.disServiceUuid, + Polar.heartRateServiceUuid, + }; + + @override + Future createFromDevice( + DiscoveredDevice device, { + Set options = const {}, + }) async { if (bleManager == null) { throw Exception("bleManager needs to be set before using the factory"); } diff --git a/lib/src/models/wearable_factory.dart b/lib/src/models/wearable_factory.dart index db4e6820..e0fc465f 100644 --- a/lib/src/models/wearable_factory.dart +++ b/lib/src/models/wearable_factory.dart @@ -16,8 +16,18 @@ abstract class WearableFactory { /// It is provided by the [WearableManager] and should not be set directly. WearableDisconnectNotifier? disconnectNotifier; + /// BLE service UUIDs that this factory may need after selecting a device. + /// + /// Web Bluetooth requires services to be requested before a wearable instance + /// can be created, so factories declare the service set up front. + Set get usedServiceUuids => const {}; + /// Checks if the factory can create a wearable from the given device and services. Future matches(DiscoveredDevice device, List services); + /// Creates a wearable from the given device. - Future createFromDevice(DiscoveredDevice device, { Set options = const {} }); + Future createFromDevice( + DiscoveredDevice device, { + Set options = const {}, + }); } From 3aade5e3709da0e6d980b8c0127a04a8183e9716 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Thu, 21 May 2026 12:42:16 +0200 Subject: [PATCH 14/23] chore(version): bumped version to 2.3.9 --- CHANGELOG.md | 4 ++++ example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d95288..d4b67d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/pubspec.lock b/example/pubspec.lock index 30301d6c..eb7392d2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -414,7 +414,7 @@ packages: path: ".." relative: true source: path - version: "2.3.7" + version: "2.3.9" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index eaeb84f2..96bd3d8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: open_earable_flutter description: This package provides functionality for interacting with OpenEarable devices. Control LED colors, control audio, and access raw sensor data. -version: 2.3.8 +version: 2.3.9 repository: https://github.com/OpenEarable/open_earable_flutter/tree/main platforms: From 4fb97cc1caa60d7f06467ee489f58ec9124ecf04 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Thu, 21 May 2026 13:35:04 +0200 Subject: [PATCH 15/23] docs(documentation): enhance custom wearable factory guide with service UUIDs --- doc/ADD_CUSTOM_WEARABLE.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/ADD_CUSTOM_WEARABLE.md b/doc/ADD_CUSTOM_WEARABLE.md index e161d886..30a7eb0b 100644 --- a/doc/ADD_CUSTOM_WEARABLE.md +++ b/doc/ADD_CUSTOM_WEARABLE.md @@ -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 get usedServiceUuids => const { + _customServiceUuid, + }; + @override Future matches(DiscoveredDevice device, List services) async { // Define logic to check if the device matches your custom wearable @@ -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 From 175b582ea61d3f07b33dc10198005800ffe680b5 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 27 May 2026 14:52:49 +0200 Subject: [PATCH 16/23] fix(ble_manager): removed prefix filter in ble scanning --- lib/src/managers/ble_manager.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/managers/ble_manager.dart b/lib/src/managers/ble_manager.dart index 2f6b54e4..ef71e7ec 100644 --- a/lib/src/managers/ble_manager.dart +++ b/lib/src/managers/ble_manager.dart @@ -166,9 +166,6 @@ class BleManager extends BleGattManager { } } await UniversalBle.startScan( - scanFilter: ScanFilter( - withNamePrefix: ["OpenEarable", "OpenRing", "Cosinuss", "eSense"], - ), platformConfig: kIsWeb ? PlatformConfig( web: WebOptions( From 51b770bc9f18f83253c39b4f7142de18de02287b Mon Sep 17 00:00:00 2001 From: Ruben Lohberg Date: Mon, 18 May 2026 20:17:00 +0200 Subject: [PATCH 17/23] feat(power-saving-service): Support power saving service --- doc/CAPABILITIES.md | 16 +++ lib/open_earable_flutter.dart | 1 + lib/src/constants.dart | 6 + .../power_saving_mode_manager.dart | 39 ++++++ .../models/devices/open_earable_factory.dart | 23 ++++ lib/src/models/devices/open_earable_v2.dart | 115 ++++++++++++++++++ 6 files changed, 200 insertions(+) create mode 100644 lib/src/models/capabilities/power_saving_mode_manager.dart diff --git a/doc/CAPABILITIES.md b/doc/CAPABILITIES.md index 17be381c..908cafea 100644 --- a/doc/CAPABILITIES.md +++ b/doc/CAPABILITIES.md @@ -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(); +if (powerSaving != null) { + final modes = await powerSaving.readSupportedPowerSavingModes(); + final currentMode = await powerSaving.readPowerSavingMode(); + await powerSaving.setPowerSavingMode(modes.first); +} +``` + +--- + ### ℹ️ Device Information Capabilities #### DeviceFirmwareVersion diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index da52dc0b..a9cc9266 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -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'; diff --git a/lib/src/constants.dart b/lib/src/constants.dart index e20e21a4..30e4ee74 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -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"; diff --git a/lib/src/models/capabilities/power_saving_mode_manager.dart b/lib/src/models/capabilities/power_saving_mode_manager.dart new file mode 100644 index 00000000..83a51188 --- /dev/null +++ b/lib/src/models/capabilities/power_saving_mode_manager.dart @@ -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> readSupportedPowerSavingModes(); + + /// Reads the currently selected power saving mode. + Future readPowerSavingMode(); + + /// Applies [mode] as the current power saving mode. + Future 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; + } +} diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index a15c0c70..2dd59b4b 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -8,11 +8,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'; @@ -128,6 +130,14 @@ class OpenEarableFactory extends WearableFactory { McuMgrFotaSlotInfoManager(deviceId: device.id), ); } + if (await _hasPowerSavingService(device)) { + wearable.registerCapability( + OpenEarableV2PowerSavingManager( + bleManager: bleManager!, + deviceId: device.id, + ), + ); + } return wearable; } else { throw Exception('OpenEarable version is not supported'); @@ -149,6 +159,19 @@ class OpenEarableFactory extends WearableFactory { return String.fromCharCodes(softwareGenerationBytes); } + Future _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, List)> _initSensors( DiscoveredDevice device, ) async { diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index 9ebb78b2..235c4fd3 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/foundation.dart' show kIsWeb; @@ -592,6 +593,120 @@ class OpenEarableV2PairingRule extends PairingRule { } } +// 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? _supportedModesCache; + + @override + Future> 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 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 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 _decodeSupportedModes(List 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 = []; + + 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 { From 5bb07773d0a053fd36d524d9076bd3fe5a177054 Mon Sep 17 00:00:00 2001 From: Ruben Lohberg Date: Thu, 28 May 2026 09:16:54 +0200 Subject: [PATCH 18/23] chore(version): bump version to 2.3.10 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b67d7d..74629a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.10 + +* added dynamic power saving mode capability for OpenEarable v2 devices + ## 2.3.9 * fixed permissions on web, where web was not able to access the devices BLE services diff --git a/pubspec.yaml b/pubspec.yaml index 96bd3d8e..8993d031 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: open_earable_flutter description: This package provides functionality for interacting with OpenEarable devices. Control LED colors, control audio, and access raw sensor data. -version: 2.3.9 +version: 2.3.10 repository: https://github.com/OpenEarable/open_earable_flutter/tree/main platforms: From 4373f5b02efc70455be57faec469dcecb6a58947 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:09:55 +0100 Subject: [PATCH 19/23] lib/src/models/capabilites/audio_response_manager.dart: added new capability --- lib/src/models/capabilities/audio_response_manager.dart | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 lib/src/models/capabilities/audio_response_manager.dart diff --git a/lib/src/models/capabilities/audio_response_manager.dart b/lib/src/models/capabilities/audio_response_manager.dart new file mode 100644 index 00000000..7cdfa399 --- /dev/null +++ b/lib/src/models/capabilities/audio_response_manager.dart @@ -0,0 +1,4 @@ +/// An interface for managing audio response measurements. +abstract class AudioResponseManager { + Future> measureAudioResponse(Map parameters); +} From e3ace3641ff581be43c323af1e86947c697bd080 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:20:07 +0100 Subject: [PATCH 20/23] lib/open_earable_flutter.dart: export AudioResponseManager --- lib/open_earable_flutter.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index a9cc9266..b1ac3ca2 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -71,6 +71,7 @@ export 'src/models/wearable_factory.dart'; export 'src/models/capabilities/system_device.dart'; export 'src/managers/ble_gatt_manager.dart'; export 'src/models/capabilities/time_synchronizable.dart'; +export 'src/models/capabilities/audio_response_manager.dart'; export 'src/fota/fota.dart'; From 20ff23d1ae81c179a3eb8cea198f98b0084884b8 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:20:41 +0100 Subject: [PATCH 21/23] lib/src/models/devices/open_earable_v2.dart: implement AudioResponseManager --- lib/src/models/devices/open_earable_v2.dart | 133 +++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index 235c4fd3..ab351bb2 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -44,6 +44,10 @@ const String _timeSyncTimeMappingCharacteristicUuid = const String _timeSyncRttCharacteristicUuid = "2e04cbf9-939d-4be5-823e-271838b75259"; +const String _audioResponseServiceUuid = "12345678-1234-5678-9abc-def123456789"; +const String _audioResponseControlCharacteristicUuid = "12345679-1234-5678-9abc-def123456789"; +const String _audioResponseDataCharacteristicUuid = "1234567a-1234-5678-9abc-def123456789"; + final VersionConstraint _versionConstraint = VersionConstraint.parse(">=2.1.0 <2.3.0"); @@ -80,7 +84,8 @@ class OpenEarableV2 extends BluetoothWearable EdgeRecorderManager, ButtonManager, StereoDevice, - SystemDevice { + SystemDevice, + AudioResponseManager { static const String deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; static const String ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; @@ -563,6 +568,132 @@ class OpenEarableV2 extends BluetoothWearable _pairedDevice?.unpair(); _pairedDevice = null; } + + // MARK: AudioResponseManager + + void _triggerAudioResponseMeasurement() { + bleManager.write( + deviceId: deviceId, + serviceId: _audioResponseServiceUuid, + characteristicId: _audioResponseControlCharacteristicUuid, + byteData: [0xFF], // Command to start audio response measurement + ); + } + + Future> _parseAudioResponseData(Uint8List data) async { + if (data.isEmpty) { + throw StateError('Audio response data is empty'); + } + + // New v1 payload size: + // 1 (version) + 1 (quality) + 1 (mean_magnitude) + 1 (num_peaks) + // + 9*2 (frequencies) + 9*2 (magnitudes) = 40 bytes + const int expectedLenV1 = 40; + + if (data.length < expectedLenV1) { + throw StateError( + 'Audio response data too short: ${data.length} bytes (expected $expectedLenV1)', + ); + } + + final int version = data[0]; + if (version != 1) { + throw StateError('Unsupported audio response data version: $version'); + } + + if (data.length != expectedLenV1) { + throw StateError( + 'Unexpected audio response data length for version 1: ${data.length} bytes (expected $expectedLenV1)', + ); + } + + final int quality = data[1]; + final int meanMagnitude = data[2]; + final int numPeaks = data[3]; + + // Frequencies: 9 * uint16_t (12.4 fixed point) starting at offset 4 + // NOTE: Endianness: this uses big-endian to match your previous implementation. + // If firmware sends little-endian, swap the byte order. + const int freqBase = 4; + final List frequenciesRaw = List.filled(9, 0); + final List frequenciesHz = List.filled(9, 0); + for (int i = 0; i < 9; i++) { + final int off = freqBase + i * 2; + final int raw = (data[off + 1] << 8) | data[off]; + frequenciesRaw[i] = raw; + frequenciesHz[i] = raw / 16.0; // 12.4 fixed point -> Hz + } + + // Magnitudes: 9 * uint16_t starting at offset 4 + 18 = 22 + const int magBase = freqBase + 9 * 2; // 22 + final List magnitudes = List.filled(9, 0); + for (int i = 0; i < 9; i++) { + final int off = magBase + i * 2; + final int mag = (data[off + 1] << 8) | data[off]; + magnitudes[i] = mag; + } + + final List> points = List.generate(9, (i) { + return { + 'frequency_hz': frequenciesHz[i], + 'frequency_raw_q12_4': frequenciesRaw[i], + 'magnitude': magnitudes[i], + }; + }); + + return { + 'version': version, + 'quality': quality, + 'mean_magnitude': meanMagnitude, + 'num_peaks': numPeaks, + 'frequencies_hz': frequenciesHz, + 'frequencies_raw_q12_4': frequenciesRaw, + 'magnitudes': magnitudes, + 'points': points, + }; + } + + @override + Future> measureAudioResponse(Map parameters) async { + _triggerAudioResponseMeasurement(); + + // Wait for the result via notification + final completer = Completer>(); + + late final StreamSubscription> audioRespSub; + audioRespSub = bleManager + .subscribe( + deviceId: deviceId, + serviceId: _audioResponseServiceUuid, + characteristicId: _audioResponseDataCharacteristicUuid, + ) + .listen( + (data) async { + logger.d("Received audio response data: $data"); + try { + final parsed = await _parseAudioResponseData(Uint8List.fromList(data)); + if (!completer.isCompleted) { + completer.complete(parsed); + } + } catch (e, stack) { + logger.e("Error parsing audio response data: $e, $stack"); + if (!completer.isCompleted) { + completer.completeError(e, stack); + } + } finally { + await audioRespSub.cancel(); + } + }, + onError: (error, stack) async { + logger.e("Error during audio response subscription: $error, $stack"); + if (!completer.isCompleted) { + completer.completeError(error, stack); + } + }, + ); + + return completer.future; + } } // MARK: OpenEarableV2Mic From c051138e37b8722b1c576cf91152e54eb0b0aaca Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:37:28 +0100 Subject: [PATCH 22/23] lib/src/models/devices/open_earable_v2.dart: changed mean magnitude format to 5.3 for audio response --- lib/src/models/devices/open_earable_v2.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index ab351bb2..861d85ef 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -608,7 +608,8 @@ class OpenEarableV2 extends BluetoothWearable } final int quality = data[1]; - final int meanMagnitude = data[2]; + final int meanMagnitudeRaw = data[2]; + final double meanMagnitude = meanMagnitudeRaw / 8.0; final int numPeaks = data[3]; // Frequencies: 9 * uint16_t (12.4 fixed point) starting at offset 4 @@ -645,6 +646,7 @@ class OpenEarableV2 extends BluetoothWearable 'version': version, 'quality': quality, 'mean_magnitude': meanMagnitude, + 'mean_magnitude_raw': meanMagnitudeRaw, 'num_peaks': numPeaks, 'frequencies_hz': frequenciesHz, 'frequencies_raw_q12_4': frequenciesRaw, From d6108d65dba2be94c8ec4c6464de91e12380e43f Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:04:16 +0100 Subject: [PATCH 23/23] lib/src/models/devices: extracted audio resopnse manager to be an optional capability for openearable --- .../models/devices/open_earable_factory.dart | 9 +++++++ lib/src/models/devices/open_earable_v2.dart | 26 ++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/src/models/devices/open_earable_factory.dart b/lib/src/models/devices/open_earable_factory.dart index 2dd59b4b..28de64a4 100644 --- a/lib/src/models/devices/open_earable_factory.dart +++ b/lib/src/models/devices/open_earable_factory.dart @@ -12,6 +12,7 @@ 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/audio_response_manager.dart'; import '../capabilities/fota_capability.dart'; import '../capabilities/fota_slot_info_capability.dart'; import '../capabilities/power_saving_mode_manager.dart'; @@ -116,6 +117,14 @@ class OpenEarableFactory extends WearableFactory { ), ); } + if (await bleManager!.hasService(deviceId: device.id, serviceId: audioResponseServiceUuid)) { + wearable.registerCapability( + OpenEarableV2AudioResponseManager( + bleManager: bleManager!, + deviceId: device.id, + ), + ); + } if (await bleManager!.hasService( deviceId: device.id, serviceId: mcuMgrSmpServiceUuid, diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index 861d85ef..a127f46e 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -44,7 +44,7 @@ const String _timeSyncTimeMappingCharacteristicUuid = const String _timeSyncRttCharacteristicUuid = "2e04cbf9-939d-4be5-823e-271838b75259"; -const String _audioResponseServiceUuid = "12345678-1234-5678-9abc-def123456789"; +const String audioResponseServiceUuid = "12345678-1234-5678-9abc-def123456789"; const String _audioResponseControlCharacteristicUuid = "12345679-1234-5678-9abc-def123456789"; const String _audioResponseDataCharacteristicUuid = "1234567a-1234-5678-9abc-def123456789"; @@ -84,8 +84,7 @@ class OpenEarableV2 extends BluetoothWearable EdgeRecorderManager, ButtonManager, StereoDevice, - SystemDevice, - AudioResponseManager { + SystemDevice { static const String deviceInfoServiceUuid = "45622510-6468-465a-b141-0b9b0f96b468"; static const String ledServiceUuid = "81040a2e-4819-11ee-be56-0242ac120002"; @@ -569,12 +568,23 @@ class OpenEarableV2 extends BluetoothWearable _pairedDevice = null; } - // MARK: AudioResponseManager +} + +// MARK: AudioResponseManager + +class OpenEarableV2AudioResponseManager implements AudioResponseManager { + final BleGattManager bleManager; + final String deviceId; + + OpenEarableV2AudioResponseManager({ + required this.bleManager, + required this.deviceId, + }); void _triggerAudioResponseMeasurement() { bleManager.write( deviceId: deviceId, - serviceId: _audioResponseServiceUuid, + serviceId: audioResponseServiceUuid, characteristicId: _audioResponseControlCharacteristicUuid, byteData: [0xFF], // Command to start audio response measurement ); @@ -656,7 +666,9 @@ class OpenEarableV2 extends BluetoothWearable } @override - Future> measureAudioResponse(Map parameters) async { + Future> measureAudioResponse( + Map parameters, + ) async { _triggerAudioResponseMeasurement(); // Wait for the result via notification @@ -666,7 +678,7 @@ class OpenEarableV2 extends BluetoothWearable audioRespSub = bleManager .subscribe( deviceId: deviceId, - serviceId: _audioResponseServiceUuid, + serviceId: audioResponseServiceUuid, characteristicId: _audioResponseDataCharacteristicUuid, ) .listen(