From 649230dc13096cdf73edfc41382833a47212e5cf Mon Sep 17 00:00:00 2001 From: Josua Hechel Date: Fri, 6 Feb 2026 16:40:07 +0100 Subject: [PATCH 01/13] Add mock wearable and sensor simulation functionality that reads form the sensors of the device the app is running on --- open_wearable/lib/models/mock_wearable.dart | 131 ++++++++++++++++++ .../widgets/devices/connect_devices_page.dart | 17 +++ .../Flutter/GeneratedPluginRegistrant.swift | 2 + open_wearable/macos/Podfile.lock | 4 +- open_wearable/pubspec.lock | 88 ++++-------- open_wearable/pubspec.yaml | 1 + 6 files changed, 181 insertions(+), 62 deletions(-) create mode 100644 open_wearable/lib/models/mock_wearable.dart diff --git a/open_wearable/lib/models/mock_wearable.dart b/open_wearable/lib/models/mock_wearable.dart new file mode 100644 index 00000000..87811a02 --- /dev/null +++ b/open_wearable/lib/models/mock_wearable.dart @@ -0,0 +1,131 @@ +import 'dart:async'; + +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:sensors_plus/sensors_plus.dart'; + +class MockWearable extends Wearable + implements SensorManager, SensorConfigurationManager { + @override + final List sensors = []; + + @override + final List sensorConfigurations = []; + + @override + Stream> + get sensorConfigurationStream => const Stream.empty(); + + MockWearable({required super.disconnectNotifier}) + : super(name: "Mock Device") { + sensors.add(MockGyroSensor()); + sensors.add(MockAccelerometer()); + } + + @override + String get deviceId => "MOCK-001"; + + @override + Future disconnect() async { + // nothing to do + return Future.value(); + } + + @override + String? getWearableIconPath({bool darkmode = false}) { + return null; + } +} + +class MockGyroSensor extends Sensor { + MockGyroSensor() + : super( + sensorName: "Gyroscope", + chartTitle: "Gyroscope", + shortChartTitle: "Gyro", + relatedConfigurations: [], + ); + + @override + List get axisNames => ['X', 'Y', 'Z']; + + @override + List get axisUnits => ['rad/s', 'rad/s', 'rad/s']; + + @override + Stream get sensorStream { + return gyroscopeEventStream().map((event) { + return SensorDoubleValue( + values: [event.x, event.y, event.z], + timestamp: DateTime.now().millisecondsSinceEpoch, + ); + }); + } +} + +class MockAccelerometer extends Sensor { + MockAccelerometer() + : super( + sensorName: "Accelerometer", + chartTitle: "Accelerometer", + shortChartTitle: "Accel", + relatedConfigurations: [ + MockConfigurableSensorConfiguration( + name: "Sensor Rate", + availableOptions: { + StreamSensorConfigOption() + }, + values: [ + MockConfigurableSensorConfigurationValue( + key: "30Hz", + options: { + StreamSensorConfigOption(), + }) + ]) + ], + ); + + @override + List get axisNames => ['X', 'Y', 'Z']; + + @override + List get axisUnits => ['m/s²', 'm/s²', 'm/s²']; + + @override + Stream get sensorStream { + return accelerometerEventStream().map((event) { + return SensorDoubleValue( + values: [event.x, event.y, event.z], + timestamp: DateTime.now().millisecondsSinceEpoch, + ); + }); + } +} + +class MockConfigurableSensorConfiguration + extends ConfigurableSensorConfiguration< + MockConfigurableSensorConfigurationValue> { + MockConfigurableSensorConfiguration({ + required super.name, + required super.values, + super.availableOptions, + }); + + @override + void setConfiguration( + MockConfigurableSensorConfigurationValue configuration) { + // no-op + } +} + +class MockConfigurableSensorConfigurationValue + extends ConfigurableSensorConfigurationValue { + MockConfigurableSensorConfigurationValue({ + required super.key, + super.options, + }); + + @override + MockConfigurableSensorConfigurationValue withoutOptions() { + return MockConfigurableSensorConfigurationValue(key: key); + } +} diff --git a/open_wearable/lib/widgets/devices/connect_devices_page.dart b/open_wearable/lib/widgets/devices/connect_devices_page.dart index 96bc20ef..c8f2b79e 100644 --- a/open_wearable/lib/widgets/devices/connect_devices_page.dart +++ b/open_wearable/lib/widgets/devices/connect_devices_page.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart' hide logger; +import 'package:open_wearable/models/mock_wearable.dart'; +import 'package:open_wearable/view_models/sensor_recorder_provider.dart'; import 'package:open_wearable/view_models/wearables_provider.dart'; import 'package:provider/provider.dart'; @@ -86,6 +88,21 @@ class _ConnectDevicesPageState extends State { onPressed: _startScanning, child: PlatformText('Scan'), ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: PlatformElevatedButton( + onPressed: () { + final mockWearable = MockWearable( + disconnectNotifier: WearableDisconnectNotifier(), + ); + context.read().addWearable(mockWearable); + context + .read() + .addWearable(mockWearable); + }, + child: PlatformText('Simulate Device'), + ), + ), ], ), ), diff --git a/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift b/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift index 7da018f9..a3e457d4 100644 --- a/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import file_picker import file_selector_macos import flutter_archive import open_file_mac +import path_provider_foundation import share_plus import shared_preferences_foundation import universal_ble @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterArchivePlugin.register(with: registry.registrar(forPlugin: "FlutterArchivePlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) diff --git a/open_wearable/macos/Podfile.lock b/open_wearable/macos/Podfile.lock index 1abbe1f3..95737490 100644 --- a/open_wearable/macos/Podfile.lock +++ b/open_wearable/macos/Podfile.lock @@ -7,7 +7,7 @@ PODS: - FlutterMacOS - ZIPFoundation (= 0.9.19) - FlutterMacOS (1.0.0) - - open_file_mac (0.0.1): + - open_file_mac (1.0.3): - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter @@ -67,7 +67,7 @@ SPEC CHECKSUMS: file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 flutter_archive: 07888d9aeb79da005e0ad8b9d347d17cdea07f68 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - open_file_mac: 01874b6d6a2c1485ac9b126d7105b99102dea2cf + open_file_mac: 76f06c8597551249bdb5e8fd8827a98eae0f4585 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb diff --git a/open_wearable/pubspec.lock b/open_wearable/pubspec.lock index 034ce0e3..8fcc6830 100644 --- a/open_wearable/pubspec.lock +++ b/open_wearable/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: @@ -57,14 +57,6 @@ 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: @@ -336,14 +328,6 @@ 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" go_router: dependency: "direct main" description: @@ -352,14 +336,6 @@ packages: url: "https://pub.dev" source: hosted version: "14.8.1" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" http: dependency: "direct main" description: @@ -444,18 +420,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: @@ -480,14 +456,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" nested: dependency: transitive description: @@ -496,14 +464,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e" - url: "https://pub.dev" - source: hosted - version: "9.2.5" open_earable_flutter: dependency: "direct main" description: @@ -612,10 +572,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -744,6 +704,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + sensors_plus: + dependency: "direct main" + description: + name: sensors_plus + sha256: "56e8cd4260d9ed8e00ecd8da5d9fdc8a1b2ec12345a750dfa51ff83fcf12e3fa" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sensors_plus_platform_interface: + dependency: transitive + description: + name: sensors_plus_platform_interface + sha256: "58815d2f5e46c0c41c40fb39375d3f127306f7742efe3b891c0b1c87e2b5cd5d" + url: "https://pub.dev" + source: hosted + version: "2.0.1" share_plus: dependency: "direct main" description: @@ -865,10 +841,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: @@ -1037,14 +1013,6 @@ 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.10.3 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/open_wearable/pubspec.yaml b/open_wearable/pubspec.yaml index 6b9c9fa2..fb498f38 100644 --- a/open_wearable/pubspec.yaml +++ b/open_wearable/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: url_launcher: ^6.3.2 go_router: ^14.6.2 http: ^1.6.0 + sensors_plus: ^7.0.0 dev_dependencies: flutter_test: From 9d16e47ef8c8d5001fbae8c95f1d82b37a1472dd Mon Sep 17 00:00:00 2001 From: Josua Hechel Date: Fri, 6 Feb 2026 16:48:40 +0100 Subject: [PATCH 02/13] Fix formatting issues in mock wearable sensor configuration --- open_wearable/lib/models/mock_wearable.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/open_wearable/lib/models/mock_wearable.dart b/open_wearable/lib/models/mock_wearable.dart index 87811a02..560ab833 100644 --- a/open_wearable/lib/models/mock_wearable.dart +++ b/open_wearable/lib/models/mock_wearable.dart @@ -72,15 +72,15 @@ class MockAccelerometer extends Sensor { MockConfigurableSensorConfiguration( name: "Sensor Rate", availableOptions: { - StreamSensorConfigOption() + StreamSensorConfigOption(), }, values: [ MockConfigurableSensorConfigurationValue( key: "30Hz", options: { StreamSensorConfigOption(), - }) - ]) + },), + ],), ], ); @@ -112,7 +112,7 @@ class MockConfigurableSensorConfiguration @override void setConfiguration( - MockConfigurableSensorConfigurationValue configuration) { + MockConfigurableSensorConfigurationValue configuration,) { // no-op } } From 6108efa91cdb38593fd73e2c109b5f938c6a519c Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:24:11 +0100 Subject: [PATCH 03/13] Renamed MockWearable with ThisDeviceWearable --- .../{mock_wearable.dart => this_device_wearable.dart} | 8 ++++---- .../lib/widgets/devices/connect_devices_page.dart | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) rename open_wearable/lib/models/{mock_wearable.dart => this_device_wearable.dart} (94%) diff --git a/open_wearable/lib/models/mock_wearable.dart b/open_wearable/lib/models/this_device_wearable.dart similarity index 94% rename from open_wearable/lib/models/mock_wearable.dart rename to open_wearable/lib/models/this_device_wearable.dart index 560ab833..98d4b5f5 100644 --- a/open_wearable/lib/models/mock_wearable.dart +++ b/open_wearable/lib/models/this_device_wearable.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:open_earable_flutter/open_earable_flutter.dart'; import 'package:sensors_plus/sensors_plus.dart'; -class MockWearable extends Wearable +class ThisDeviceWearable extends Wearable implements SensorManager, SensorConfigurationManager { @override final List sensors = []; @@ -15,14 +15,14 @@ class MockWearable extends Wearable Stream> get sensorConfigurationStream => const Stream.empty(); - MockWearable({required super.disconnectNotifier}) - : super(name: "Mock Device") { + ThisDeviceWearable({required super.disconnectNotifier}) + : super(name: "This Device") { sensors.add(MockGyroSensor()); sensors.add(MockAccelerometer()); } @override - String get deviceId => "MOCK-001"; + String get deviceId => "THIS-DEVICE-001"; @override Future disconnect() async { diff --git a/open_wearable/lib/widgets/devices/connect_devices_page.dart b/open_wearable/lib/widgets/devices/connect_devices_page.dart index c8f2b79e..18b02e58 100644 --- a/open_wearable/lib/widgets/devices/connect_devices_page.dart +++ b/open_wearable/lib/widgets/devices/connect_devices_page.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart' hide logger; -import 'package:open_wearable/models/mock_wearable.dart'; +import 'package:open_wearable/models/this_device_wearable.dart'; import 'package:open_wearable/view_models/sensor_recorder_provider.dart'; import 'package:open_wearable/view_models/wearables_provider.dart'; import 'package:provider/provider.dart'; @@ -92,7 +92,7 @@ class _ConnectDevicesPageState extends State { padding: const EdgeInsets.only(top: 8.0), child: PlatformElevatedButton( onPressed: () { - final mockWearable = MockWearable( + final mockWearable = ThisDeviceWearable( disconnectNotifier: WearableDisconnectNotifier(), ); context.read().addWearable(mockWearable); From b3da6d6c7b382e2d06ecc41b35cda365c4bc25d9 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:47:44 +0100 Subject: [PATCH 04/13] Integrate device_info_plus for enhanced device profile management and update ThisDeviceWearable to utilize device information --- .../lib/models/this_device_wearable.dart | 267 +++++++++++++++++- .../widgets/devices/connect_devices_page.dart | 5 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 + open_wearable/macos/Podfile.lock | 6 + open_wearable/pubspec.lock | 26 +- open_wearable/pubspec.yaml | 2 + 6 files changed, 293 insertions(+), 15 deletions(-) diff --git a/open_wearable/lib/models/this_device_wearable.dart b/open_wearable/lib/models/this_device_wearable.dart index 98d4b5f5..6a79324d 100644 --- a/open_wearable/lib/models/this_device_wearable.dart +++ b/open_wearable/lib/models/this_device_wearable.dart @@ -1,32 +1,45 @@ import 'dart:async'; -import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart' hide Version, logger; +import 'package:pub_semver/pub_semver.dart'; import 'package:sensors_plus/sensors_plus.dart'; +import 'logger.dart'; + class ThisDeviceWearable extends Wearable - implements SensorManager, SensorConfigurationManager { + implements SensorManager, DeviceFirmwareVersion { @override final List sensors = []; - @override - final List sensorConfigurations = []; + final DeviceProfile deviceProfile; - @override - Stream> - get sensorConfigurationStream => const Stream.empty(); - - ThisDeviceWearable({required super.disconnectNotifier}) - : super(name: "This Device") { + ThisDeviceWearable._({ + required super.disconnectNotifier, + required this.deviceProfile, + }) : super(name: deviceProfile.displayName) { sensors.add(MockGyroSensor()); sensors.add(MockAccelerometer()); } + static Future create({ + required WearableDisconnectNotifier disconnectNotifier, + }) async { + final profile = await DeviceProfile.fetch(); + logger.d('Fetched device profile: $profile'); + return ThisDeviceWearable._( + disconnectNotifier: disconnectNotifier, + deviceProfile: profile, + ); + } + @override - String get deviceId => "THIS-DEVICE-001"; + String get deviceId => deviceProfile.deviceId; @override Future disconnect() async { - // nothing to do + // TODO: Call disconnect listeners return Future.value(); } @@ -34,6 +47,236 @@ class ThisDeviceWearable extends Wearable String? getWearableIconPath({bool darkmode = false}) { return null; } + + @override + Future checkFirmwareSupport() { + return Future.value(FirmwareSupportStatus.supported); + } + + @override + Future readDeviceFirmwareVersion() { + return deviceProfile.osVersion != null + ? Future.value(deviceProfile.osVersion) + : Future.error('OS version not available'); + } + + @override + Future readFirmwareVersionNumber() { + if (deviceProfile.osVersion == null) { + return Future.error('OS version not available'); + } + try { + final version = Version.parse(deviceProfile.osVersion!); + return Future.value(version); + } catch (e) { + return Future.error('Failed to parse OS version: $e'); + } + } + + @override + VersionConstraint get supportedFirmwareRange => VersionConstraint.any; +} + +class DeviceProfile { + final String displayName; + final String deviceId; + final String? model; + final String? manufacturer; + final String? osVersion; + final String? platform; + + const DeviceProfile({ + required this.displayName, + required this.deviceId, + this.model, + this.manufacturer, + this.osVersion, + this.platform, + }); + + static Future fetch() async { + final deviceInfo = DeviceInfoPlugin(); + try { + if (kIsWeb) { + final info = await deviceInfo.webBrowserInfo; + logger.d("Fetched web browser info: $info"); + final browserName = info.browserName.name; + final displayName = _firstNonEmpty( + [info.platform, browserName, info.appName], + 'Web Browser', + ); + final deviceId = _firstNonEmpty( + [info.userAgent, info.appVersion], + 'WEB-DEVICE', + ); + return DeviceProfile( + displayName: displayName, + deviceId: deviceId, + model: browserName, + manufacturer: info.vendor, + osVersion: info.appVersion, + platform: 'web', + ); + } + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + final info = await deviceInfo.androidInfo; + logger.d("Fetched Android device info: $info"); + final displayName = _firstNonEmpty( + [ + _joinNonEmpty([info.brand, info.model]), + info.model, + info.device, + info.product, + ], + 'Android Device', + ); + final deviceId = _firstNonEmpty( + [info.id, info.device, info.product, info.model], + 'ANDROID-DEVICE', + ); + final osVersion = _joinNonEmpty( + [ + 'Android', + info.version.release, + 'SDK ${info.version.sdkInt}', + ], + ); + return DeviceProfile( + displayName: displayName, + deviceId: deviceId, + model: info.model, + manufacturer: info.manufacturer, + osVersion: osVersion, + platform: 'android', + ); + case TargetPlatform.iOS: + final info = await deviceInfo.iosInfo; + logger.d("Fetched iOS device info: $info"); + final displayName = _firstNonEmpty( + [info.name, info.localizedModel, info.model], + 'iOS Device', + ); + final deviceId = _firstNonEmpty( + [info.identifierForVendor, info.name, info.model], + 'IOS-DEVICE', + ); + final osVersion = _joinNonEmpty( + [info.systemName, info.systemVersion], + ); + return DeviceProfile( + displayName: displayName, + deviceId: deviceId, + model: info.model, + manufacturer: 'Apple', + osVersion: osVersion, + platform: 'ios', + ); + case TargetPlatform.macOS: + final info = await deviceInfo.macOsInfo; + logger.d("Fetched macOS device info: $info"); + final displayName = _firstNonEmpty( + [info.computerName, info.model], + 'macOS Device', + ); + final deviceId = _firstNonEmpty( + [info.computerName, info.model], + 'MAC-DEVICE', + ); + final osVersion = _joinNonEmpty( + ['macOS', info.osRelease], + ); + return DeviceProfile( + displayName: displayName, + deviceId: deviceId, + model: info.model, + manufacturer: 'Apple', + osVersion: osVersion, + platform: 'macos', + ); + case TargetPlatform.windows: + final info = await deviceInfo.windowsInfo; + logger.d("Fetched Windows device info: $info"); + final displayName = _firstNonEmpty( + [info.computerName, info.productName], + 'Windows Device', + ); + final deviceId = _firstNonEmpty( + [info.deviceId, info.computerName, info.productName], + 'WINDOWS-DEVICE', + ); + final osVersion = _joinNonEmpty( + ['Windows', info.displayVersion, info.buildNumber.toString()], + ); + return DeviceProfile( + displayName: displayName, + deviceId: deviceId, + model: info.productName, + manufacturer: 'Microsoft', + osVersion: osVersion, + platform: 'windows', + ); + case TargetPlatform.linux: + final info = await deviceInfo.linuxInfo; + logger.d("Fetched Linux device info: $info"); + final displayName = _firstNonEmpty( + [info.prettyName, info.name], + 'Linux Device', + ); + final deviceId = _firstNonEmpty( + [info.machineId, info.prettyName, info.name], + 'LINUX-DEVICE', + ); + final osVersion = _joinNonEmpty( + [info.name, info.version], + ); + return DeviceProfile( + displayName: displayName, + deviceId: deviceId, + model: info.prettyName, + manufacturer: null, + osVersion: osVersion, + platform: 'linux', + ); + case TargetPlatform.fuchsia: + break; + } + } catch (_) { + // Fall back to default profile below. + } + + return const DeviceProfile( + displayName: 'This Device', + deviceId: 'THIS-DEVICE-001', + platform: 'unknown', + ); + } + + @override + String toString() { + return 'DeviceProfile(displayName: $displayName, deviceId: $deviceId, model: $model, manufacturer: $manufacturer, osVersion: $osVersion, platform: $platform)'; + } +} + +String _firstNonEmpty(List candidates, String fallback) { + for (final candidate in candidates) { + if (candidate == null) continue; + final trimmed = candidate.trim(); + if (trimmed.isNotEmpty) return trimmed; + } + return fallback; +} + +String? _joinNonEmpty(List parts) { + final cleaned = []; + for (final part in parts) { + if (part == null) continue; + final trimmed = part.trim(); + if (trimmed.isNotEmpty) cleaned.add(trimmed); + } + if (cleaned.isEmpty) return null; + return cleaned.join(' '); } class MockGyroSensor extends Sensor { diff --git a/open_wearable/lib/widgets/devices/connect_devices_page.dart b/open_wearable/lib/widgets/devices/connect_devices_page.dart index 18b02e58..8537aa68 100644 --- a/open_wearable/lib/widgets/devices/connect_devices_page.dart +++ b/open_wearable/lib/widgets/devices/connect_devices_page.dart @@ -91,10 +91,11 @@ class _ConnectDevicesPageState extends State { Padding( padding: const EdgeInsets.only(top: 8.0), child: PlatformElevatedButton( - onPressed: () { - final mockWearable = ThisDeviceWearable( + onPressed: () async { + final mockWearable = await ThisDeviceWearable.create( disconnectNotifier: WearableDisconnectNotifier(), ); + if (!context.mounted) return; context.read().addWearable(mockWearable); context .read() diff --git a/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift b/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift index a3e457d4..d09d4ff1 100644 --- a/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import device_info_plus import file_picker import file_selector_macos import flutter_archive @@ -16,6 +17,7 @@ import universal_ble import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterArchivePlugin.register(with: registry.registrar(forPlugin: "FlutterArchivePlugin")) diff --git a/open_wearable/macos/Podfile.lock b/open_wearable/macos/Podfile.lock index 95737490..a8f8885a 100644 --- a/open_wearable/macos/Podfile.lock +++ b/open_wearable/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - device_info_plus (0.0.1): + - FlutterMacOS - file_picker (0.0.1): - FlutterMacOS - file_selector_macos (0.0.1): @@ -25,6 +27,7 @@ PODS: - ZIPFoundation (0.9.19) DEPENDENCIES: + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_archive (from `Flutter/ephemeral/.symlinks/plugins/flutter_archive/macos`) @@ -41,6 +44,8 @@ SPEC REPOS: - ZIPFoundation EXTERNAL SOURCES: + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_picker: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos file_selector_macos: @@ -63,6 +68,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 flutter_archive: 07888d9aeb79da005e0ad8b9d347d17cdea07f68 diff --git a/open_wearable/pubspec.lock b/open_wearable/pubspec.lock index 8fcc6830..0a5af415 100644 --- a/open_wearable/pubspec.lock +++ b/open_wearable/pubspec.lock @@ -121,6 +121,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c" + url: "https://pub.dev" + source: hosted + version: "12.3.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" equatable: dependency: transitive description: @@ -689,7 +705,7 @@ packages: source: hosted version: "6.1.5+1" pub_semver: - dependency: transitive + dependency: "direct main" description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" @@ -997,6 +1013,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" xdg_directories: dependency: transitive description: diff --git a/open_wearable/pubspec.yaml b/open_wearable/pubspec.yaml index fb498f38..a2931520 100644 --- a/open_wearable/pubspec.yaml +++ b/open_wearable/pubspec.yaml @@ -56,6 +56,8 @@ dependencies: go_router: ^14.6.2 http: ^1.6.0 sensors_plus: ^7.0.0 + device_info_plus: ^12.3.0 + pub_semver: ^2.2.0 dev_dependencies: flutter_test: From 47e3206f3295aadb15615e1f92a9e12392e84f1b Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:53:41 +0100 Subject: [PATCH 05/13] open_wearable/lib/models/this_device_wearable.dart: Refactor ThisDeviceWearable to initialize sensors asynchronously and improve sensor availability checks --- .../lib/models/this_device_wearable.dart | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/open_wearable/lib/models/this_device_wearable.dart b/open_wearable/lib/models/this_device_wearable.dart index 6a79324d..31ab2af8 100644 --- a/open_wearable/lib/models/this_device_wearable.dart +++ b/open_wearable/lib/models/this_device_wearable.dart @@ -18,20 +18,19 @@ class ThisDeviceWearable extends Wearable ThisDeviceWearable._({ required super.disconnectNotifier, required this.deviceProfile, - }) : super(name: deviceProfile.displayName) { - sensors.add(MockGyroSensor()); - sensors.add(MockAccelerometer()); - } + }) : super(name: deviceProfile.displayName); static Future create({ required WearableDisconnectNotifier disconnectNotifier, }) async { final profile = await DeviceProfile.fetch(); logger.d('Fetched device profile: $profile'); - return ThisDeviceWearable._( + final wearable = ThisDeviceWearable._( disconnectNotifier: disconnectNotifier, deviceProfile: profile, ); + await wearable._initSensors(); + return wearable; } @override @@ -48,6 +47,26 @@ class ThisDeviceWearable extends Wearable return null; } + Future _initSensors() async { + if (await _isSensorAvailable(gyroscopeEventStream())) { + sensors.add(MockGyroSensor()); + } + if (await _isSensorAvailable( + accelerometerEventStream(), + )) { + sensors.add(MockAccelerometer()); + } + } + + static Future _isSensorAvailable(Stream stream) async { + try { + await stream.first.timeout(const Duration(milliseconds: 800)); + return true; + } catch (_) { + return false; + } + } + @override Future checkFirmwareSupport() { return Future.value(FirmwareSupportStatus.supported); From bea2c39e17dee0cd98a8c45bd7946e025e3ec7f6 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:22:21 +0100 Subject: [PATCH 06/13] open_wearable/ios/Runner/Info.plist: added key for motion data --- open_wearable/ios/Runner/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/open_wearable/ios/Runner/Info.plist b/open_wearable/ios/Runner/Info.plist index acaa1598..56e5c1a9 100644 --- a/open_wearable/ios/Runner/Info.plist +++ b/open_wearable/ios/Runner/Info.plist @@ -51,6 +51,8 @@ This app requires Bluetooth access to communicate with wearable devices. NSPhotoLibraryUsageDescription Needed for optional file selection functionality. + NSMotionUsageDescription + This app requires access to device motion in order to provide sensor data. ITSAppUsesNonExemptEncryption UIFileSharingEnabled From a053fff5247153f8567b6c34d13c5e6f7d059899 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:22:55 +0100 Subject: [PATCH 07/13] open_wearable/lib/models/this_device_wearable.dart: added configurable sensors --- .../lib/models/this_device_wearable.dart | 310 +++++++++++++----- 1 file changed, 235 insertions(+), 75 deletions(-) diff --git a/open_wearable/lib/models/this_device_wearable.dart b/open_wearable/lib/models/this_device_wearable.dart index 31ab2af8..49fd5dbd 100644 --- a/open_wearable/lib/models/this_device_wearable.dart +++ b/open_wearable/lib/models/this_device_wearable.dart @@ -9,10 +9,20 @@ import 'package:sensors_plus/sensors_plus.dart'; import 'logger.dart'; class ThisDeviceWearable extends Wearable - implements SensorManager, DeviceFirmwareVersion { + implements SensorManager, SensorConfigurationManager, DeviceFirmwareVersion { @override final List sensors = []; + @override + final List sensorConfigurations = []; + + final StreamController> + _sensorConfigurationController = StreamController.broadcast(); + + @override + Stream> + get sensorConfigurationStream => _sensorConfigurationController.stream; + final DeviceProfile deviceProfile; ThisDeviceWearable._({ @@ -47,14 +57,107 @@ class ThisDeviceWearable extends Wearable return null; } + void _emitSensorConfigurationChange( + SensorConfiguration configuration, + SensorConfigurationValue value, + ) { + _sensorConfigurationController.add({configuration: value}); + } + Future _initSensors() async { if (await _isSensorAvailable(gyroscopeEventStream())) { - sensors.add(MockGyroSensor()); + final gyroConfig = DeviceSensorConfiguration( + name: 'Gyroscope', + onChange: _emitSensorConfigurationChange, + ); + sensorConfigurations.add(gyroConfig); + _emitSensorConfigurationChange(gyroConfig, gyroConfig.currentValue); + sensors.add( + ThisDeviceSensor( + config: gyroConfig, + sensorName: 'Gyroscope', + chartTitle: 'Gyroscope', + shortChartTitle: 'Gyro', + axisNames: ['X', 'Y', 'Z'], + axisUnits: ['rad/s', 'rad/s', 'rad/s'], + valueExtractor: (event) => SensorDoubleValue( + values: [event.x, event.y, event.z], + timestamp: event.timestamp.millisecondsSinceEpoch, + ), + sensorStreamProvider: gyroscopeEventStream, + ), + ); } if (await _isSensorAvailable( accelerometerEventStream(), )) { - sensors.add(MockAccelerometer()); + final accelConfig = DeviceSensorConfiguration( + name: 'Accelerometer', + onChange: _emitSensorConfigurationChange, + ); + sensorConfigurations.add(accelConfig); + _emitSensorConfigurationChange(accelConfig, accelConfig.currentValue); + sensors.add( + ThisDeviceSensor( + config: accelConfig, + sensorName: 'Accelerometer', + chartTitle: 'Accelerometer', + shortChartTitle: 'Accel', + axisNames: ['X', 'Y', 'Z'], + axisUnits: ['m/s²', 'm/s²', 'm/s²'], + valueExtractor: (event) => SensorDoubleValue( + values: [event.x, event.y, event.z], + timestamp: event.timestamp.millisecondsSinceEpoch, + ), + sensorStreamProvider: accelerometerEventStream, + ), + ); + } + if (await _isSensorAvailable(magnetometerEventStream())) { + final magConfig = DeviceSensorConfiguration( + name: 'Magnetometer', + onChange: _emitSensorConfigurationChange, + ); + sensorConfigurations.add(magConfig); + _emitSensorConfigurationChange(magConfig, magConfig.currentValue); + sensors.add( + ThisDeviceSensor( + config: magConfig, + sensorName: 'Magnetometer', + chartTitle: 'Magnetometer', + shortChartTitle: 'Mag', + axisNames: ['X', 'Y', 'Z'], + axisUnits: ['µT', 'µT', 'µT'], + valueExtractor: (event) => SensorDoubleValue( + values: [event.x, event.y, event.z], + timestamp: event.timestamp.millisecondsSinceEpoch, + ), + sensorStreamProvider: magnetometerEventStream, + ), + ); + } + if (await _isSensorAvailable(barometerEventStream())) { + final baroConfig = DeviceSensorConfiguration( + name: 'Barometer', + onChange: _emitSensorConfigurationChange, + ); + sensorConfigurations.add(baroConfig); + _emitSensorConfigurationChange(baroConfig, baroConfig.currentValue); + sensors.add( + ThisDeviceSensor( + config: baroConfig, + sensorName: 'Barometer', + chartTitle: 'Barometer', + shortChartTitle: 'Baro', + axisNames: ['Pressure'], + axisUnits: ['hPa'], + valueExtractor: (event) => SensorDoubleValue( + values: [event.pressure], + timestamp: event.timestamp.millisecondsSinceEpoch, + ), + sensorStreamProvider: barometerEventStream, + ), + ); } } @@ -298,96 +401,153 @@ String? _joinNonEmpty(List parts) { return cleaned.join(' '); } -class MockGyroSensor extends Sensor { - MockGyroSensor() - : super( - sensorName: "Gyroscope", - chartTitle: "Gyroscope", - shortChartTitle: "Gyro", - relatedConfigurations: [], - ); +class ThisDeviceSensor extends Sensor { + final DeviceSensorConfiguration config; + late final StreamController _controller; + StreamSubscription? _subscription; + final Stream Function({required Duration samplingPeriod}) _sensorStreamProvider; + final SensorDoubleValue Function(SensorEvent event) _valueExtractor; + + ThisDeviceSensor({ + required super.sensorName, + required super.chartTitle, + required super.shortChartTitle, + required this.config, + required List axisNames, + required List axisUnits, + required SensorDoubleValue Function(SensorEvent event) valueExtractor, + required Stream Function({required Duration samplingPeriod}) sensorStreamProvider, + }) : _axisNames = axisNames, + _axisUnits = axisUnits, + _valueExtractor = valueExtractor, + _sensorStreamProvider = sensorStreamProvider { + _controller = StreamController.broadcast( + onListen: _updateSubscription, + onCancel: _updateSubscription, + ); + } + + final List _axisNames; @override - List get axisNames => ['X', 'Y', 'Z']; - + List get axisNames => _axisNames; + + final List _axisUnits; @override - List get axisUnits => ['rad/s', 'rad/s', 'rad/s']; - + List get axisUnits => _axisUnits; + @override - Stream get sensorStream { - return gyroscopeEventStream().map((event) { - return SensorDoubleValue( - values: [event.x, event.y, event.z], - timestamp: DateTime.now().millisecondsSinceEpoch, - ); - }); + Stream get sensorStream => _controller.stream; + + void _updateSubscription() { + if (!_controller.hasListener) { + _cancelSubscription(); + return; + } + + final value = config.currentValue; + if (value.isOff) { + _cancelSubscription(); + return; + } + + final samplingPeriod = value.frequencyHz > 0 ? Duration(milliseconds: (1000 / value.frequencyHz).round()) : SensorInterval.normalInterval; + + _cancelSubscription(); + _subscription = + _sensorStreamProvider(samplingPeriod: samplingPeriod).listen( + (event) { + _controller.add( + _valueExtractor(event), + ); + }, + onError: _controller.addError, + ); + } + + void _cancelSubscription() { + _subscription?.cancel(); + _subscription = null; } } -class MockAccelerometer extends Sensor { - MockAccelerometer() - : super( - sensorName: "Accelerometer", - chartTitle: "Accelerometer", - shortChartTitle: "Accel", - relatedConfigurations: [ - MockConfigurableSensorConfiguration( - name: "Sensor Rate", - availableOptions: { - StreamSensorConfigOption(), - }, - values: [ - MockConfigurableSensorConfigurationValue( - key: "30Hz", - options: { - StreamSensorConfigOption(), - },), - ],), - ], +class DeviceSensorConfiguration + extends SensorFrequencyConfiguration { + final void Function( + SensorConfiguration configuration, + SensorConfigurationValue value, + ) onChange; + + DeviceSensorFrequencyValue _currentValue; + + DeviceSensorConfiguration({ + required super.name, + required this.onChange, + }) : _currentValue = DeviceSensorFrequencyValue.normal(), + super( + values: DeviceSensorFrequencyValue.defaults(), + offValue: DeviceSensorFrequencyValue.off(), ); - @override - List get axisNames => ['X', 'Y', 'Z']; + DeviceSensorFrequencyValue get currentValue => _currentValue; - @override - List get axisUnits => ['m/s²', 'm/s²', 'm/s²']; + Stream get changes => + _changesController.stream; + + final StreamController _changesController = + StreamController.broadcast(); @override - Stream get sensorStream { - return accelerometerEventStream().map((event) { - return SensorDoubleValue( - values: [event.x, event.y, event.z], - timestamp: DateTime.now().millisecondsSinceEpoch, - ); - }); + void setConfiguration(DeviceSensorFrequencyValue configuration) { + _currentValue = configuration; + onChange(this, configuration); + _changesController.add(configuration); } } -class MockConfigurableSensorConfiguration - extends ConfigurableSensorConfiguration< - MockConfigurableSensorConfigurationValue> { - MockConfigurableSensorConfiguration({ - required super.name, - required super.values, - super.availableOptions, - }); +class DeviceSensorFrequencyValue extends SensorFrequencyConfigurationValue { + DeviceSensorFrequencyValue({ + required super.frequencyHz, + String? key, + }) : super( + key: key ?? _formatKey(frequencyHz), + ); - @override - void setConfiguration( - MockConfigurableSensorConfigurationValue configuration,) { - // no-op + bool get isOff => frequencyHz <= 0; + + static DeviceSensorFrequencyValue off() { + return DeviceSensorFrequencyValue( + frequencyHz: 0, + key: 'Off', + ); } -} -class MockConfigurableSensorConfigurationValue - extends ConfigurableSensorConfigurationValue { - MockConfigurableSensorConfigurationValue({ - required super.key, - super.options, - }); + static DeviceSensorFrequencyValue normal() { + return DeviceSensorFrequencyValue( + frequencyHz: 5, + ); + } - @override - MockConfigurableSensorConfigurationValue withoutOptions() { - return MockConfigurableSensorConfigurationValue(key: key); + static List defaults() { + return [ + off(), + normal(), + DeviceSensorFrequencyValue( + frequencyHz: 15, + ), + DeviceSensorFrequencyValue( + frequencyHz: 30, + ), + DeviceSensorFrequencyValue( + frequencyHz: 50, + ), + ]; + } + + static String _formatKey(double frequencyHz) { + if (frequencyHz == frequencyHz.roundToDouble()) { + return '${frequencyHz.toInt()} Hz'; + } + return '${frequencyHz.toStringAsFixed(2)} Hz'; } } From 9a8a95208488ff3a2acbff3221873f66601f3273 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:45:21 +0100 Subject: [PATCH 08/13] open_wearable/lib/modles/this_device_wearable.dart: fixed bug where configurations could not be changed --- open_wearable/lib/models/this_device_wearable.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/open_wearable/lib/models/this_device_wearable.dart b/open_wearable/lib/models/this_device_wearable.dart index 49fd5dbd..f7315c7b 100644 --- a/open_wearable/lib/models/this_device_wearable.dart +++ b/open_wearable/lib/models/this_device_wearable.dart @@ -425,9 +425,11 @@ class ThisDeviceSensor extends Sensor { onListen: _updateSubscription, onCancel: _updateSubscription, ); + config.changes.listen((value) { + _updateSubscription(); + }); } - final List _axisNames; @override List get axisNames => _axisNames; From bf29613354affe583da9554e1c093e9f0b45b05f Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:16:44 +0100 Subject: [PATCH 09/13] open_wearalbe/pubspec.yaml: upgrade open_earable_flutter lib to newest version --- open_wearable/pubspec.lock | 16 ++++++++-------- open_wearable/pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/open_wearable/pubspec.lock b/open_wearable/pubspec.lock index 0a5af415..cb7a8ddc 100644 --- a/open_wearable/pubspec.lock +++ b/open_wearable/pubspec.lock @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" device_info_plus: dependency: "direct main" description: @@ -484,10 +484,10 @@ packages: dependency: "direct main" description: name: open_earable_flutter - sha256: "23b784abdb9aa2a67afd6bcf22778cc9e3d124eba5a4d02f49443581fa3f8958" + sha256: "3176341e123cd3c4b870913d3f1d5282d47ee524347dbe414e0ecec18c84b117" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" open_file: dependency: "direct main" description: @@ -817,10 +817,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: @@ -905,10 +905,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: b1aca26728b7cc7a3af971bb6f601554a8ae9df2e0a006de8450ba06a17ad36a url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.4.0" url_launcher_linux: dependency: transitive description: diff --git a/open_wearable/pubspec.yaml b/open_wearable/pubspec.yaml index a2931520..890d8a8c 100644 --- a/open_wearable/pubspec.yaml +++ b/open_wearable/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 open_file: ^3.3.2 - open_earable_flutter: ^2.3.1 + open_earable_flutter: ^2.3.2 flutter_platform_widgets: ^9.0.0 provider: ^6.1.2 logger: ^2.5.0 From 86b8b88b59f529038de81a3814c290c623e9fab7 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:19:58 +0100 Subject: [PATCH 10/13] lib/widgets/devices/connect_devices_page.dart: display this device in the list of discovered devices --- .../widgets/devices/connect_devices_page.dart | 73 ++++++++++++++----- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/open_wearable/lib/widgets/devices/connect_devices_page.dart b/open_wearable/lib/widgets/devices/connect_devices_page.dart index 8537aa68..56955fa1 100644 --- a/open_wearable/lib/widgets/devices/connect_devices_page.dart +++ b/open_wearable/lib/widgets/devices/connect_devices_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; @@ -28,11 +29,13 @@ class _ConnectDevicesPageState extends State { List discoveredDevices = []; Map connectingDevices = {}; + DiscoveredDevice? _thisDeviceEntry; @override void initState() { super.initState(); _startScanning(); + _addThisDeviceToDiscovered(); } @override @@ -54,7 +57,11 @@ class _ConnectDevicesPageState extends State { subtitle: PlatformText(device.id), trailing: _buildTrailingWidget(device.id), onTap: () { - _connectToDevice(device, context); + if (_thisDeviceEntry?.id == device.id) { + _connectToThisDevice(context); + } else { + _connectToDevice(device, context); + } }, ); }).toList(); @@ -88,22 +95,6 @@ class _ConnectDevicesPageState extends State { onPressed: _startScanning, child: PlatformText('Scan'), ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: PlatformElevatedButton( - onPressed: () async { - final mockWearable = await ThisDeviceWearable.create( - disconnectNotifier: WearableDisconnectNotifier(), - ); - if (!context.mounted) return; - context.read().addWearable(mockWearable); - context - .read() - .addWearable(mockWearable); - }, - child: PlatformText('Simulate Device'), - ), - ), ], ), ), @@ -135,6 +126,54 @@ class _ConnectDevicesPageState extends State { }); } + Future _addThisDeviceToDiscovered() async { + if (_thisDeviceEntry != null) return; + final profile = await DeviceProfile.fetch(); + if (!mounted) return; + + final thisDevice = DiscoveredDevice( + id: profile.deviceId, + name: profile.displayName, + manufacturerData: Uint8List(0), + rssi: 0, + serviceUuids: const [], + ); + + setState(() { + _thisDeviceEntry = thisDevice; + if (!discoveredDevices.any((device) => device.id == thisDevice.id)) { + discoveredDevices.insert(0, thisDevice); + } + }); + } + + Future _connectToThisDevice(BuildContext context) async { + final device = _thisDeviceEntry; + if (device == null) return; + + setState(() { + connectingDevices[device.id] = true; + }); + + try { + final wearable = await ThisDeviceWearable.create( + disconnectNotifier: WearableDisconnectNotifier(), + ); + if (!context.mounted) return; + context.read().addWearable(wearable); + context.read().addWearable(wearable); + setState(() { + discoveredDevices.removeWhere((d) => d.id == device.id); + }); + } finally { + if (context.mounted) { + setState(() { + connectingDevices.remove(device.id); + }); + } + } + } + Future _connectToDevice( DiscoveredDevice device, BuildContext context, From 69466e4b308865b4b2f027f8eaf777ee73e9924c Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:25:23 +0100 Subject: [PATCH 11/13] open_wearable/lib/widgets/devices/connect_devices_page.dart: hide already connected devices from the list of discovered devices --- open_wearable/lib/widgets/devices/connect_devices_page.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/open_wearable/lib/widgets/devices/connect_devices_page.dart b/open_wearable/lib/widgets/devices/connect_devices_page.dart index 56955fa1..8518aca0 100644 --- a/open_wearable/lib/widgets/devices/connect_devices_page.dart +++ b/open_wearable/lib/widgets/devices/connect_devices_page.dart @@ -51,7 +51,11 @@ class _ConnectDevicesPageState extends State { trailing: Icon(PlatformIcons(context).checkMark), ); }).toList(); - List discoveredDevicesWidgets = discoveredDevices.map((device) { + final connectedIds = + wearablesProvider.wearables.map((wearable) => wearable.deviceId).toSet(); + List discoveredDevicesWidgets = discoveredDevices + .where((device) => !connectedIds.contains(device.id)) + .map((device) { return PlatformListTile( title: PlatformText(device.name), subtitle: PlatformText(device.id), From 7a08c43248d8c7a35c9bd9d28705c83606e79617 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:34:26 +0100 Subject: [PATCH 12/13] open_wearable/lib/models/this_device_wearable.dart: implemented disconnect --- open_wearable/lib/models/this_device_wearable.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/open_wearable/lib/models/this_device_wearable.dart b/open_wearable/lib/models/this_device_wearable.dart index f7315c7b..5586d881 100644 --- a/open_wearable/lib/models/this_device_wearable.dart +++ b/open_wearable/lib/models/this_device_wearable.dart @@ -25,10 +25,13 @@ class ThisDeviceWearable extends Wearable final DeviceProfile deviceProfile; + final WearableDisconnectNotifier _disconnectNotifier; + ThisDeviceWearable._({ - required super.disconnectNotifier, + required WearableDisconnectNotifier disconnectNotifier, required this.deviceProfile, - }) : super(name: deviceProfile.displayName); + }) : _disconnectNotifier = disconnectNotifier, + super(name: deviceProfile.displayName, disconnectNotifier: disconnectNotifier); static Future create({ required WearableDisconnectNotifier disconnectNotifier, @@ -48,8 +51,7 @@ class ThisDeviceWearable extends Wearable @override Future disconnect() async { - // TODO: Call disconnect listeners - return Future.value(); + _disconnectNotifier.notifyListeners(); } @override From 6a7587506d40da1f3ca3bd9dfd628f993b9b0bee Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:34:10 +0100 Subject: [PATCH 13/13] open_wearable/lib/models/this_device_wearable.dart: fixed issues with flutter analyze --- open_wearable/ios/Podfile.lock | 19 +++++++++++++++++++ .../lib/models/this_device_wearable.dart | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/open_wearable/ios/Podfile.lock b/open_wearable/ios/Podfile.lock index 764cb645..6db677a6 100644 --- a/open_wearable/ios/Podfile.lock +++ b/open_wearable/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - device_info_plus (0.0.1): + - Flutter - DKImagePickerController/Core (4.3.9): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource @@ -48,11 +50,16 @@ PODS: - SwiftProtobuf - open_file_ios (1.0.3): - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - SDWebImage (5.21.5): - SDWebImage/Core (= 5.21.5) - SDWebImage/Core (5.21.5) + - sensors_plus (0.0.1): + - Flutter - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -69,13 +76,16 @@ PODS: - ZIPFoundation (0.9.19) DEPENDENCIES: + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`) - Flutter (from `Flutter`) - flutter_archive (from `.symlinks/plugins/flutter_archive/ios`) - mcumgr_flutter (from `.symlinks/plugins/mcumgr_flutter/ios`) - open_file_ios (from `.symlinks/plugins/open_file_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) @@ -93,6 +103,8 @@ SPEC REPOS: - ZIPFoundation EXTERNAL SOURCES: + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" file_selector_ios: @@ -105,8 +117,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/mcumgr_flutter/ios" open_file_ios: :path: ".symlinks/plugins/open_file_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + sensors_plus: + :path: ".symlinks/plugins/sensors_plus/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -117,6 +133,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be @@ -126,8 +143,10 @@ SPEC CHECKSUMS: iOSMcuManagerLibrary: e9555825af11a61744fe369c12e1e66621061b58 mcumgr_flutter: 969e99cc15e9fe658242669ce1075bf4612aef8a open_file_ios: 46184d802ee7959203f6392abcfa0dd49fdb5be0 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838 + sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb SwiftCBOR: 465775bed0e8bac7bfb8160bcf7b95d7f75971e4 diff --git a/open_wearable/lib/models/this_device_wearable.dart b/open_wearable/lib/models/this_device_wearable.dart index 5586d881..8e0b6d93 100644 --- a/open_wearable/lib/models/this_device_wearable.dart +++ b/open_wearable/lib/models/this_device_wearable.dart @@ -28,10 +28,10 @@ class ThisDeviceWearable extends Wearable final WearableDisconnectNotifier _disconnectNotifier; ThisDeviceWearable._({ - required WearableDisconnectNotifier disconnectNotifier, + required super.disconnectNotifier, required this.deviceProfile, }) : _disconnectNotifier = disconnectNotifier, - super(name: deviceProfile.displayName, disconnectNotifier: disconnectNotifier); + super(name: deviceProfile.displayName); static Future create({ required WearableDisconnectNotifier disconnectNotifier,