From 8224b91c5dd8baae416a035845a30b2643c085e5 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Tue, 24 Jun 2025 13:28:57 +0200 Subject: [PATCH 01/30] added feature for background monitoring beacons --- apps/carp_mobile_sensing_app/pubspec.yaml | 8 +- carp_mobile_sensing/lib/services/log.dart | 1 + .../lib/connectivity.dart | 3 + .../lib/connectivity_data.dart | 80 +++++++++----- .../lib/connectivity_package.dart | 36 +++---- .../lib/connectivity_probes.dart | 101 +++++++++++++----- .../carp_connectivity_package/pubspec.yaml | 4 +- .../carp_esense_package/example/ios/Podfile | 44 ++++++++ 8 files changed, 205 insertions(+), 72 deletions(-) create mode 100644 packages/carp_esense_package/example/ios/Podfile diff --git a/apps/carp_mobile_sensing_app/pubspec.yaml b/apps/carp_mobile_sensing_app/pubspec.yaml index 2d2407ee6..a9cce1d48 100644 --- a/apps/carp_mobile_sensing_app/pubspec.yaml +++ b/apps/carp_mobile_sensing_app/pubspec.yaml @@ -89,10 +89,10 @@ dependency_overrides: git: https://github.com/bardram/device_calendar # due to this issue https://github.com/petri-lipponen-movesense/mdsflutter/issues/36 - mdsflutter: - git: - url: https://github.com/Panosfunk/mdsflutter.git - ref: master + # mdsflutter: + # git: + # url: https://github.com/Panosfunk/mdsflutter.git + # ref: master # due to this issue https://github.com/cph-cachet/carp_studies_app/issues/427 polar: 7.5.1 diff --git a/carp_mobile_sensing/lib/services/log.dart b/carp_mobile_sensing/lib/services/log.dart index ef0f0512c..285a55ea2 100644 --- a/carp_mobile_sensing/lib/services/log.dart +++ b/carp_mobile_sensing/lib/services/log.dart @@ -1,3 +1,4 @@ + /* * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the * Technical University of Denmark (DTU). diff --git a/packages/carp_connectivity_package/lib/connectivity.dart b/packages/carp_connectivity_package/lib/connectivity.dart index 87460d824..8ab420102 100644 --- a/packages/carp_connectivity_package/lib/connectivity.dart +++ b/packages/carp_connectivity_package/lib/connectivity.dart @@ -6,7 +6,10 @@ library; import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; +import 'dart:math'; +import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:connectivity_plus/connectivity_plus.dart' as connectivity; diff --git a/packages/carp_connectivity_package/lib/connectivity_data.dart b/packages/carp_connectivity_package/lib/connectivity_data.dart index c36bf80d9..161450473 100644 --- a/packages/carp_connectivity_package/lib/connectivity_data.dart +++ b/packages/carp_connectivity_package/lib/connectivity_data.dart @@ -41,23 +41,17 @@ class Connectivity extends Data { Connectivity() : super(); - Connectivity.fromConnectivityResult( - List result) - : super() { - connectivityStatus = result - .map((connectivity.ConnectivityResult e) => _parseConnectivityStatus(e)) - .toList(); + Connectivity.fromConnectivityResult(List result) : super() { + connectivityStatus = result.map((connectivity.ConnectivityResult e) => _parseConnectivityStatus(e)).toList(); } @override Function get fromJsonFunction => _$ConnectivityFromJson; - factory Connectivity.fromJson(Map json) => - FromJsonFactory().fromJson(json); + factory Connectivity.fromJson(Map json) => FromJsonFactory().fromJson(json); @override Map toJson() => _$ConnectivityToJson(this); - static ConnectivityStatus _parseConnectivityStatus( - connectivity.ConnectivityResult result) { + static ConnectivityStatus _parseConnectivityStatus(connectivity.ConnectivityResult result) { switch (result) { case connectivity.ConnectivityResult.bluetooth: return ConnectivityStatus.bluetooth; @@ -77,8 +71,7 @@ class Connectivity extends Data { } @override - String toString() => - '${super.toString()}, connectivityStatus: $connectivityStatus'; + String toString() => '${super.toString()}, connectivityStatus: $connectivityStatus'; } /// A [Data] holding information of nearby Bluetooth devices. @@ -98,15 +91,14 @@ class Bluetooth extends Data { /// The list of [BluetoothDevice] found in a scan. List get scanResult => _scanResult.values.toList(); - set scanResult(List devices) => _scanResult.addEntries( - devices.map((device) => MapEntry(device.bluetoothDeviceId, device))); + set scanResult(List devices) => + _scanResult.addEntries(devices.map((device) => MapEntry(device.bluetoothDeviceId, device))); Bluetooth({DateTime? startScan, this.endScan}) : super() { this.startScan = startScan ?? DateTime.now(); } - void addBluetoothDevice(BluetoothDevice device) => - _scanResult[device.bluetoothDeviceId] = device; + void addBluetoothDevice(BluetoothDevice device) => _scanResult[device.bluetoothDeviceId] = device; void addBluetoothDevicesFromScanResults(List results) { for (var scanResult in results) { @@ -114,10 +106,16 @@ class Bluetooth extends Data { } } + void addBluetoothDevicesFromRangingResults( + Beacon result, + String beaconName, + ) { + addBluetoothDevice(BluetoothDevice.fromRangingResult(result, beaconName)); + } + @override Function get fromJsonFunction => _$BluetoothFromJson; - factory Bluetooth.fromJson(Map json) => - FromJsonFactory().fromJson(json); + factory Bluetooth.fromJson(Map json) => FromJsonFactory().fromJson(json); @override Map toJson() => _$BluetoothToJson(this); @@ -164,8 +162,16 @@ class BluetoothDevice { rssi: result.rssi, ); - factory BluetoothDevice.fromJson(Map json) => - _$BluetoothDeviceFromJson(json); + factory BluetoothDevice.fromRangingResult(Beacon result, String beaconName) => BluetoothDevice( + bluetoothDeviceId: beaconName, + bluetoothDeviceName: beaconName, + connectable: false, + txPowerLevel: result.txPower, + advertisementName: beaconName, + rssi: result.rssi, + ); + + factory BluetoothDevice.fromJson(Map json) => _$BluetoothDeviceFromJson(json); Map toJson() => _$BluetoothDeviceToJson(this); @override @@ -202,12 +208,38 @@ class Wifi extends Data { @override Function get fromJsonFunction => _$WifiFromJson; - factory Wifi.fromJson(Map json) => - FromJsonFactory().fromJson(json); + factory Wifi.fromJson(Map json) => FromJsonFactory().fromJson(json); @override Map toJson() => _$WifiToJson(this); @override - String toString() => - '${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip'; + String toString() => '${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip'; +} + +/// Beacon Region +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class BeaconRegion { + String identifier; + String uuid; + int? major; + int? minor; + + BeaconRegion({ + required this.identifier, + required this.uuid, + this.major, + this.minor, + }); + + Region toRegion() { + return Region( + identifier: identifier, + proximityUUID: uuid, + major: major, + minor: minor, + ); + } + + @override + String toString() => '${super.toString()}, Identifier: $identifier, UUID: $uuid, Major: $major, Minor: $minor'; } diff --git a/packages/carp_connectivity_package/lib/connectivity_package.dart b/packages/carp_connectivity_package/lib/connectivity_package.dart index 6ec45b2b9..f3977ce1f 100644 --- a/packages/carp_connectivity_package/lib/connectivity_package.dart +++ b/packages/carp_connectivity_package/lib/connectivity_package.dart @@ -21,8 +21,7 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { static const String WIFI = "${NameSpace.CARP}.wifi"; @override - DataTypeSamplingSchemeMap get samplingSchemes => - DataTypeSamplingSchemeMap.from([ + DataTypeSamplingSchemeMap get samplingSchemes => DataTypeSamplingSchemeMap.from([ DataTypeSamplingScheme( CamsDataTypeMetaData( type: CONNECTIVITY, @@ -80,12 +79,8 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { ]); // registering default privacy functions - DataTransformerSchemaRegistry() - .lookup(PrivacySchema.DEFAULT)! - .add(BLUETOOTH, bluetoothNameAnonymizer); - DataTransformerSchemaRegistry() - .lookup(PrivacySchema.DEFAULT)! - .add(WIFI, wifiNameAnonymizer); + DataTransformerSchemaRegistry().lookup(PrivacySchema.DEFAULT)!.add(BLUETOOTH, bluetoothNameAnonymizer); + DataTransformerSchemaRegistry().lookup(PrivacySchema.DEFAULT)!.add(WIFI, wifiNameAnonymizer); } } @@ -100,29 +95,34 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { /// Filtering on remoteIds allows Android to scan for devices in the background /// without needing to be in the foreground. This is not possible on iOS. @JsonSerializable(includeIfNull: false, explicitToJson: true) -class BluetoothScanPeriodicSamplingConfiguration - extends PeriodicSamplingConfiguration { +class BluetoothScanPeriodicSamplingConfiguration extends PeriodicSamplingConfiguration { /// List of Bluetooth service UUIDs to filter the scan results. List withServices; /// List of remote device IDs to filter the scan results. List withRemoteIds; + /// Use Package `flutter_beacon` to enable beacon monitoring while the app is in background. + bool useBeaconMonitoring; + + /// List of beacon regions to monitor and/or range using the `flutter_beacon` package. + /// + /// When [useBeaconMonitoring] is true, the app will monitor these regions, potentially in the background if platform permissions and conditions allow. + List beaconRegions; + BluetoothScanPeriodicSamplingConfiguration({ required super.interval, required super.duration, this.withServices = const [], this.withRemoteIds = const [], + this.beaconRegions = const [], + this.useBeaconMonitoring = false, }); @override - Map toJson() => - _$BluetoothScanPeriodicSamplingConfigurationToJson(this); + Map toJson() => _$BluetoothScanPeriodicSamplingConfigurationToJson(this); @override - Function get fromJsonFunction => - _$BluetoothScanPeriodicSamplingConfigurationFromJson; - factory BluetoothScanPeriodicSamplingConfiguration.fromJson( - Map json) => - FromJsonFactory() - .fromJson(json); + Function get fromJsonFunction => _$BluetoothScanPeriodicSamplingConfigurationFromJson; + factory BluetoothScanPeriodicSamplingConfiguration.fromJson(Map json) => + FromJsonFactory().fromJson(json); } diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 0649113ba..43b43bf0e 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -13,18 +13,16 @@ class ConnectivityProbe extends StreamProbe { @override Future onStart() async { // collect the current connectivity status on sampling start - var connectivityStatus = - await connectivity.Connectivity().checkConnectivity(); - addMeasurement(Measurement.fromData( - Connectivity.fromConnectivityResult(connectivityStatus))); + var connectivityStatus = await connectivity.Connectivity().checkConnectivity(); + addMeasurement(Measurement.fromData(Connectivity.fromConnectivityResult(connectivityStatus))); return super.onStart(); } @override - Stream get stream => - connectivity.Connectivity().onConnectivityChanged.map((event) => - Measurement.fromData(Connectivity.fromConnectivityResult(event))); + Stream get stream => connectivity.Connectivity() + .onConnectivityChanged + .map((event) => Measurement.fromData(Connectivity.fromConnectivityResult(event))); } // This probe requests access to location permissions (both on Android and iOS). @@ -69,37 +67,44 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { Stream get bufferingStream => FlutterBluePlus.scanResults; @override - Future getMeasurement() async => - _data != null ? Measurement.fromData(_data!) : null; + Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; // if a BT-specific sampling configuration is used, we need to // extract the services and remoteIds from it so FlutterBluePlus can // perform filtered scanning - List get services => (samplingConfiguration - is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) - .withServices - .map((e) => Guid(e)) - .toList() + List get services => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withServices.map((e) => Guid(e)).toList() : []; - List get remoteIds => (samplingConfiguration - is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) - .withRemoteIds + List get remoteIds => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withRemoteIds : []; + bool get useBeaconMonitoring => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).useBeaconMonitoring + : false; + + List get beaconRegions => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconRegions + : []; + + StreamSubscription? _streamMonitoring; + StreamSubscription? _streamRanging; + @override void onSamplingStart() { _data = Bluetooth(); try { - FlutterBluePlus.startScan( - withServices: services, - withRemoteIds: remoteIds, - timeout: samplingConfiguration?.duration ?? - const Duration(milliseconds: DEFAULT_TIMEOUT)); + if (useBeaconMonitoring) { + _startMonitoring(); + } else { + FlutterBluePlus.startScan( + withServices: services, + withRemoteIds: remoteIds, + timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT)); + } } catch (error) { FlutterBluePlus.stopScan(); _data = Error(message: 'Error scanning for bluetooth - $error'); @@ -108,7 +113,12 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { @override void onSamplingEnd() { - FlutterBluePlus.stopScan(); + if (useBeaconMonitoring) { + _stopMonitoring(); + } else { + FlutterBluePlus.stopScan(); + } + if (_data is Bluetooth) (_data as Bluetooth).endScan = DateTime.now(); } @@ -118,4 +128,45 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { (_data as Bluetooth).addBluetoothDevicesFromScanResults(event); } } + + Future _startMonitoring() async { + await flutterBeacon.initializeScanning; + List regions = + beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); + + try { + _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { + if (result.monitoringState == MonitoringState.inside) { + info('🚪 Entered region: ${result.region.identifier}'); + _startRanging(result.region); + } else if (result.monitoringState == MonitoringState.outside) { + info('🚪 Exited region: ${result.region.identifier}'); + _stopMonitoring(); + } + }); + } catch (e) { + info('Error starting monitoring: $e'); + } + } + + void _startRanging(Region region) { + _streamRanging = flutterBeacon.ranging([region]).listen((RangingResult result) { + final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= 2.0); + + for (var beacon in closeBeacons) { + info('✅ beacon in range: ${beacon.proximityUUID}, ${beacon.accuracy} m'); + (_data as Bluetooth).addBluetoothDevicesFromRangingResults( + beacon, + result.region.identifier, + ); + } + }); + } + + void _stopMonitoring() { + _streamRanging?.cancel(); + _streamRanging = null; + _streamMonitoring?.cancel(); + _streamMonitoring = null; + } } diff --git a/packages/carp_connectivity_package/pubspec.yaml b/packages/carp_connectivity_package/pubspec.yaml index 4e90cb066..fbd305424 100644 --- a/packages/carp_connectivity_package/pubspec.yaml +++ b/packages/carp_connectivity_package/pubspec.yaml @@ -21,7 +21,9 @@ dependencies: network_info_plus: ^6.0.0 # wifi ssid name flutter_blue_plus: ^1.35.0 # bluetooth scan crypto: ^3.0.0 # hashing sensitive data - permission_handler: '>=11.0.0 <13.0.0' + permission_handler: '>=11.0.0 <13.0.0' + dchs_flutter_beacon: ^0.6.4 + # Overriding carp libraries to use the local copy # Remove this before release of package diff --git a/packages/carp_esense_package/example/ios/Podfile b/packages/carp_esense_package/example/ios/Podfile new file mode 100644 index 000000000..d97f17e22 --- /dev/null +++ b/packages/carp_esense_package/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end From 2768a7d8804ed86d873a2103ab0c57447b833fae Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 25 Jun 2025 10:01:16 +0200 Subject: [PATCH 02/30] some refactoring --- .../lib/connectivity_probes.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 43b43bf0e..ed4f5c95a 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -101,9 +101,10 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { _startMonitoring(); } else { FlutterBluePlus.startScan( - withServices: services, - withRemoteIds: remoteIds, - timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT)); + withServices: services, + withRemoteIds: remoteIds, + timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT), + ); } } catch (error) { FlutterBluePlus.stopScan(); @@ -135,13 +136,13 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); try { - _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { + _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) async { if (result.monitoringState == MonitoringState.inside) { info('🚪 Entered region: ${result.region.identifier}'); _startRanging(result.region); } else if (result.monitoringState == MonitoringState.outside) { info('🚪 Exited region: ${result.region.identifier}'); - _stopMonitoring(); + await _stopMonitoring(); } }); } catch (e) { @@ -163,10 +164,10 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { }); } - void _stopMonitoring() { - _streamRanging?.cancel(); + Future _stopMonitoring() async { + await _streamRanging?.cancel(); _streamRanging = null; - _streamMonitoring?.cancel(); + await _streamMonitoring?.cancel(); _streamMonitoring = null; } } From f8e84940de39358da8244800f9829462f7fef7b3 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 25 Jun 2025 10:52:21 +0200 Subject: [PATCH 03/30] cleanup --- .../lib/connectivity_probes.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index ed4f5c95a..9acc516f7 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -136,13 +136,13 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); try { - _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) async { + _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { if (result.monitoringState == MonitoringState.inside) { info('🚪 Entered region: ${result.region.identifier}'); _startRanging(result.region); } else if (result.monitoringState == MonitoringState.outside) { info('🚪 Exited region: ${result.region.identifier}'); - await _stopMonitoring(); + _stopMonitoring(); } }); } catch (e) { @@ -164,10 +164,10 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { }); } - Future _stopMonitoring() async { - await _streamRanging?.cancel(); + void _stopMonitoring() { + _streamRanging?.cancel(); _streamRanging = null; - await _streamMonitoring?.cancel(); + _streamMonitoring?.cancel(); _streamMonitoring = null; } } From d250aa7ca3ae618e13f4536e09200d4baa00a44a Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 25 Jun 2025 13:23:28 +0200 Subject: [PATCH 04/30] added log --- packages/carp_connectivity_package/lib/connectivity_probes.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 9acc516f7..0c9eecfd1 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -98,6 +98,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { try { if (useBeaconMonitoring) { + info('Using beacon monitoring.'); _startMonitoring(); } else { FlutterBluePlus.startScan( From ac5dd0c8eded0fb19ac79c9f72ea348abef8428e Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 25 Jun 2025 14:12:24 +0200 Subject: [PATCH 05/30] added some more logging --- .../lib/connectivity_probes.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 0c9eecfd1..046a5103d 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -132,7 +132,14 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { } Future _startMonitoring() async { - await flutterBeacon.initializeScanning; + info('start monitoring & initializing scanning.'); + try { + await flutterBeacon.initializeScanning; + } catch (e) { + warning('error happened while initializing scanner $e'); + } + info('initialized scanner'); + List regions = beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); From cee474b3a8fe814bd2708345a4d0a25ae4372ac2 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 25 Jun 2025 14:18:11 +0200 Subject: [PATCH 06/30] added logging --- packages/carp_connectivity_package/lib/connectivity_probes.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 046a5103d..ed66ab677 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -116,6 +116,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { @override void onSamplingEnd() { if (useBeaconMonitoring) { + info('stopping monitoring'); _stopMonitoring(); } else { FlutterBluePlus.stopScan(); From c0d8fddf13a94bd025b6fcee233cfbd1957580b8 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 25 Jun 2025 14:23:59 +0200 Subject: [PATCH 07/30] trying some things --- .../carp_connectivity_package/lib/connectivity_probes.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index ed66ab677..83e606a0e 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -116,8 +116,8 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { @override void onSamplingEnd() { if (useBeaconMonitoring) { - info('stopping monitoring'); - _stopMonitoring(); + info('stopping monitoring kinda'); + //_stopMonitoring(); } else { FlutterBluePlus.stopScan(); } From 72e34e5f124a1cec410f55e8c115674692a72a8f Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 25 Jun 2025 14:33:42 +0200 Subject: [PATCH 08/30] uncommenting stop monitoring --- packages/carp_connectivity_package/lib/connectivity_probes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 83e606a0e..a9b995d0c 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -117,7 +117,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { void onSamplingEnd() { if (useBeaconMonitoring) { info('stopping monitoring kinda'); - //_stopMonitoring(); + _stopMonitoring(); } else { FlutterBluePlus.stopScan(); } From 33d40a928512e9fa844787f364c506cca632fd62 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 25 Jun 2025 15:28:19 +0200 Subject: [PATCH 09/30] edited log --- packages/carp_connectivity_package/lib/connectivity_probes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index a9b995d0c..ed66ab677 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -116,7 +116,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { @override void onSamplingEnd() { if (useBeaconMonitoring) { - info('stopping monitoring kinda'); + info('stopping monitoring'); _stopMonitoring(); } else { FlutterBluePlus.stopScan(); From ebfdfd474fee3c7ef69768cc704536f97f69c5ed Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Thu, 26 Jun 2025 11:08:58 +0200 Subject: [PATCH 10/30] updated log --- packages/carp_connectivity_package/lib/connectivity_probes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index ed66ab677..4d9b6c19d 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -150,7 +150,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { info('🚪 Entered region: ${result.region.identifier}'); _startRanging(result.region); } else if (result.monitoringState == MonitoringState.outside) { - info('🚪 Exited region: ${result.region.identifier}'); + info('Not in region: ${result.region.identifier}'); _stopMonitoring(); } }); From 9b5bc373b702bf719750ef9f776a2093004564e3 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Thu, 26 Jun 2025 11:10:23 +0200 Subject: [PATCH 11/30] added some comments --- .../lib/connectivity_data.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_data.dart b/packages/carp_connectivity_package/lib/connectivity_data.dart index 161450473..272a614b2 100644 --- a/packages/carp_connectivity_package/lib/connectivity_data.dart +++ b/packages/carp_connectivity_package/lib/connectivity_data.dart @@ -216,12 +216,23 @@ class Wifi extends Data { String toString() => '${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip'; } -/// Beacon Region +/// Beacon Region to use when monitoring for beacons. @JsonSerializable(includeIfNull: false, explicitToJson: true) class BeaconRegion { + /// A unique identifier for the beacon region. + /// Used to distinguish between different regions being monitored. String identifier; + + /// The proximity UUID of the beacon. + /// This is a 128-bit value used to identify a group of related beacons. String uuid; + + /// The major value of the beacon region (optional). + /// Used to further distinguish a subset of beacons within the same UUID. int? major; + + /// The minor value of the beacon region (optional). + /// Provides a finer granularity within a group of beacons identified by the same UUID and major value. int? minor; BeaconRegion({ From 68a767ff41ada39512ddc4f030e50f800d98a526 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Thu, 26 Jun 2025 11:13:38 +0200 Subject: [PATCH 12/30] removed commented code in pubspec --- apps/carp_mobile_sensing_app/pubspec.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/carp_mobile_sensing_app/pubspec.yaml b/apps/carp_mobile_sensing_app/pubspec.yaml index a9cce1d48..dc7c83267 100644 --- a/apps/carp_mobile_sensing_app/pubspec.yaml +++ b/apps/carp_mobile_sensing_app/pubspec.yaml @@ -89,10 +89,10 @@ dependency_overrides: git: https://github.com/bardram/device_calendar # due to this issue https://github.com/petri-lipponen-movesense/mdsflutter/issues/36 - # mdsflutter: - # git: - # url: https://github.com/Panosfunk/mdsflutter.git - # ref: master + mdsflutter: + git: + url: https://github.com/Panosfunk/mdsflutter.git + ref: master # due to this issue https://github.com/cph-cachet/carp_studies_app/issues/427 polar: 7.5.1 From d953302ce0ccb2c70d65419d8ca0f28cd7752678 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Thu, 26 Jun 2025 11:14:02 +0200 Subject: [PATCH 13/30] small fix --- apps/carp_mobile_sensing_app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/carp_mobile_sensing_app/pubspec.yaml b/apps/carp_mobile_sensing_app/pubspec.yaml index dc7c83267..2d2407ee6 100644 --- a/apps/carp_mobile_sensing_app/pubspec.yaml +++ b/apps/carp_mobile_sensing_app/pubspec.yaml @@ -91,7 +91,7 @@ dependency_overrides: # due to this issue https://github.com/petri-lipponen-movesense/mdsflutter/issues/36 mdsflutter: git: - url: https://github.com/Panosfunk/mdsflutter.git + url: https://github.com/Panosfunk/mdsflutter.git ref: master # due to this issue https://github.com/cph-cachet/carp_studies_app/issues/427 From 0105083b10c0f425699a0076c1c984e00baf4015 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Thu, 26 Jun 2025 11:14:50 +0200 Subject: [PATCH 14/30] small cleanup --- carp_mobile_sensing/lib/services/log.dart | 1 - packages/carp_connectivity_package/lib/connectivity.dart | 2 -- 2 files changed, 3 deletions(-) diff --git a/carp_mobile_sensing/lib/services/log.dart b/carp_mobile_sensing/lib/services/log.dart index 285a55ea2..ef0f0512c 100644 --- a/carp_mobile_sensing/lib/services/log.dart +++ b/carp_mobile_sensing/lib/services/log.dart @@ -1,4 +1,3 @@ - /* * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the * Technical University of Denmark (DTU). diff --git a/packages/carp_connectivity_package/lib/connectivity.dart b/packages/carp_connectivity_package/lib/connectivity.dart index 8ab420102..4095965b8 100644 --- a/packages/carp_connectivity_package/lib/connectivity.dart +++ b/packages/carp_connectivity_package/lib/connectivity.dart @@ -6,8 +6,6 @@ library; import 'dart:async'; import 'dart:convert'; -import 'dart:developer'; -import 'dart:math'; import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; From 42cd9921b5947707ac492d8041ecb532d77be354 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Fri, 27 Jun 2025 08:23:58 +0200 Subject: [PATCH 15/30] added scan distance --- .../carp_connectivity_package/lib/connectivity_package.dart | 5 +++++ .../carp_connectivity_package/lib/connectivity_probes.dart | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_package.dart b/packages/carp_connectivity_package/lib/connectivity_package.dart index f3977ce1f..df71576df 100644 --- a/packages/carp_connectivity_package/lib/connectivity_package.dart +++ b/packages/carp_connectivity_package/lib/connectivity_package.dart @@ -110,6 +110,10 @@ class BluetoothScanPeriodicSamplingConfiguration extends PeriodicSamplingConfigu /// When [useBeaconMonitoring] is true, the app will monitor these regions, potentially in the background if platform permissions and conditions allow. List beaconRegions; + /// When a device is within this distance from the beacon, a predefined event is triggered. + /// Defaults to 2 meters. + int beaconDistance; + BluetoothScanPeriodicSamplingConfiguration({ required super.interval, required super.duration, @@ -117,6 +121,7 @@ class BluetoothScanPeriodicSamplingConfiguration extends PeriodicSamplingConfigu this.withRemoteIds = const [], this.beaconRegions = const [], this.useBeaconMonitoring = false, + this.beaconDistance = 2, }); @override diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 4d9b6c19d..6d774efe4 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -89,6 +89,10 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconRegions : []; + int get beaconDistance => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconDistance + : 2; + StreamSubscription? _streamMonitoring; StreamSubscription? _streamRanging; @@ -161,7 +165,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { void _startRanging(Region region) { _streamRanging = flutterBeacon.ranging([region]).listen((RangingResult result) { - final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= 2.0); + final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); for (var beacon in closeBeacons) { info('✅ beacon in range: ${beacon.proximityUUID}, ${beacon.accuracy} m'); From e89dd3916e4087c1799c924a2a0b5e1b937cb833 Mon Sep 17 00:00:00 2001 From: bardram Date: Thu, 17 Jul 2025 11:57:12 +0200 Subject: [PATCH 16/30] Update connectivity.g.dart --- .../lib/connectivity.g.dart | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/carp_connectivity_package/lib/connectivity.g.dart b/packages/carp_connectivity_package/lib/connectivity.g.dart index 8d9c25524..7a65467cb 100644 --- a/packages/carp_connectivity_package/lib/connectivity.g.dart +++ b/packages/carp_connectivity_package/lib/connectivity.g.dart @@ -84,6 +84,21 @@ Map _$WifiToJson(Wifi instance) => { if (instance.ip case final value?) 'ip': value, }; +BeaconRegion _$BeaconRegionFromJson(Map json) => BeaconRegion( + identifier: json['identifier'] as String, + uuid: json['uuid'] as String, + major: (json['major'] as num?)?.toInt(), + minor: (json['minor'] as num?)?.toInt(), + ); + +Map _$BeaconRegionToJson(BeaconRegion instance) => + { + 'identifier': instance.identifier, + 'uuid': instance.uuid, + if (instance.major case final value?) 'major': value, + if (instance.minor case final value?) 'minor': value, + }; + BluetoothScanPeriodicSamplingConfiguration _$BluetoothScanPeriodicSamplingConfigurationFromJson( Map json) => @@ -98,6 +113,14 @@ BluetoothScanPeriodicSamplingConfiguration ?.map((e) => e as String) .toList() ?? const [], + beaconRegions: (json['beaconRegions'] as List?) + ?.map((e) => e == null + ? null + : BeaconRegion.fromJson(e as Map)) + .toList() ?? + const [], + useBeaconMonitoring: json['useBeaconMonitoring'] as bool? ?? false, + beaconDistance: (json['beaconDistance'] as num?)?.toInt() ?? 2, ) ..$type = json['__type'] as String? ..lastTime = json['lastTime'] == null @@ -114,4 +137,7 @@ Map _$BluetoothScanPeriodicSamplingConfigurationToJson( 'duration': instance.duration.inMicroseconds, 'withServices': instance.withServices, 'withRemoteIds': instance.withRemoteIds, + 'useBeaconMonitoring': instance.useBeaconMonitoring, + 'beaconRegions': instance.beaconRegions.map((e) => e?.toJson()).toList(), + 'beaconDistance': instance.beaconDistance, }; From d6f18be75f18524274451473ced144d1bdf61412 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 23 Jul 2025 11:07:59 +0200 Subject: [PATCH 17/30] Updated beacon to be its own sampling method --- .../lib/connectivity.g.dart | 97 ++++++++++++++++ .../lib/connectivity_data.dart | 95 +++++++++++++++- .../lib/connectivity_package.dart | 45 ++++++++ .../lib/connectivity_probes.dart | 105 ++++++++++++++++++ 4 files changed, 341 insertions(+), 1 deletion(-) diff --git a/packages/carp_connectivity_package/lib/connectivity.g.dart b/packages/carp_connectivity_package/lib/connectivity.g.dart index 8d9c25524..c591bc581 100644 --- a/packages/carp_connectivity_package/lib/connectivity.g.dart +++ b/packages/carp_connectivity_package/lib/connectivity.g.dart @@ -71,6 +71,23 @@ Map _$BluetoothDeviceToJson(BluetoothDevice instance) => 'rssi': instance.rssi, }; +BeaconDevice _$BeaconDeviceFromJson(Map json) => BeaconDevice( + rssi: (json['rssi'] as num).toInt(), + uuid: json['uuid'] as String?, + major: (json['major'] as num?)?.toInt(), + minor: (json['minor'] as num?)?.toInt(), + accuracy: (json['accuracy'] as num?)?.toDouble(), + ); + +Map _$BeaconDeviceToJson(BeaconDevice instance) => + { + 'rssi': instance.rssi, + if (instance.uuid case final value?) 'uuid': value, + if (instance.major case final value?) 'major': value, + if (instance.minor case final value?) 'minor': value, + if (instance.accuracy case final value?) 'accuracy': value, + }; + Wifi _$WifiFromJson(Map json) => Wifi( ssid: json['ssid'] as String?, bssid: json['bssid'] as String?, @@ -84,6 +101,44 @@ Map _$WifiToJson(Wifi instance) => { if (instance.ip case final value?) 'ip': value, }; +BeaconData _$BeaconDataFromJson(Map json) => BeaconData( + startScan: json['startScan'] == null + ? null + : DateTime.parse(json['startScan'] as String), + endScan: json['endScan'] == null + ? null + : DateTime.parse(json['endScan'] as String), + ) + ..$type = json['__type'] as String? + ..scanResult = (json['scanResult'] as List) + .map((e) => BeaconDevice.fromJson(e as Map)) + .toList(); + +Map _$BeaconDataToJson(BeaconData instance) => + { + if (instance.$type case final value?) '__type': value, + 'startScan': instance.startScan.toIso8601String(), + if (instance.endScan?.toIso8601String() case final value?) + 'endScan': value, + 'scanResult': instance.scanResult.map((e) => e.toJson()).toList(), + }; + +BeaconRegion _$BeaconRegionFromJson(Map json) => BeaconRegion( + identifier: json['identifier'] as String, + uuid: json['uuid'] as String, + major: (json['major'] as num?)?.toInt(), + minor: (json['minor'] as num?)?.toInt(), + )..$type = json['__type'] as String?; + +Map _$BeaconRegionToJson(BeaconRegion instance) => + { + if (instance.$type case final value?) '__type': value, + 'identifier': instance.identifier, + 'uuid': instance.uuid, + if (instance.major case final value?) 'major': value, + if (instance.minor case final value?) 'minor': value, + }; + BluetoothScanPeriodicSamplingConfiguration _$BluetoothScanPeriodicSamplingConfigurationFromJson( Map json) => @@ -98,6 +153,14 @@ BluetoothScanPeriodicSamplingConfiguration ?.map((e) => e as String) .toList() ?? const [], + beaconRegions: (json['beaconRegions'] as List?) + ?.map((e) => e == null + ? null + : BeaconRegion.fromJson(e as Map)) + .toList() ?? + const [], + useBeaconMonitoring: json['useBeaconMonitoring'] as bool? ?? false, + beaconDistance: (json['beaconDistance'] as num?)?.toInt() ?? 2, ) ..$type = json['__type'] as String? ..lastTime = json['lastTime'] == null @@ -114,4 +177,38 @@ Map _$BluetoothScanPeriodicSamplingConfigurationToJson( 'duration': instance.duration.inMicroseconds, 'withServices': instance.withServices, 'withRemoteIds': instance.withRemoteIds, + 'useBeaconMonitoring': instance.useBeaconMonitoring, + 'beaconRegions': instance.beaconRegions.map((e) => e?.toJson()).toList(), + 'beaconDistance': instance.beaconDistance, + }; + +BeaconRangingPeriodicSamplingConfiguration + _$BeaconRangingPeriodicSamplingConfigurationFromJson( + Map json) => + BeaconRangingPeriodicSamplingConfiguration( + interval: Duration(microseconds: (json['interval'] as num).toInt()), + duration: Duration(microseconds: (json['duration'] as num).toInt()), + beaconRegions: (json['beaconRegions'] as List?) + ?.map((e) => e == null + ? null + : BeaconRegion.fromJson(e as Map)) + .toList() ?? + const [], + beaconDistance: (json['beaconDistance'] as num?)?.toInt() ?? 2, + ) + ..$type = json['__type'] as String? + ..lastTime = json['lastTime'] == null + ? null + : DateTime.parse(json['lastTime'] as String); + +Map _$BeaconRangingPeriodicSamplingConfigurationToJson( + BeaconRangingPeriodicSamplingConfiguration instance) => + { + if (instance.$type case final value?) '__type': value, + if (instance.lastTime?.toIso8601String() case final value?) + 'lastTime': value, + 'interval': instance.interval.inMicroseconds, + 'duration': instance.duration.inMicroseconds, + 'beaconRegions': instance.beaconRegions.map((e) => e?.toJson()).toList(), + 'beaconDistance': instance.beaconDistance, }; diff --git a/packages/carp_connectivity_package/lib/connectivity_data.dart b/packages/carp_connectivity_package/lib/connectivity_data.dart index 272a614b2..5b2f2f383 100644 --- a/packages/carp_connectivity_package/lib/connectivity_data.dart +++ b/packages/carp_connectivity_package/lib/connectivity_data.dart @@ -183,6 +183,48 @@ class BluetoothDevice { ', rssi: $rssi'; } +/// Beacon device data. +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class BeaconDevice { + /// The RSSI signal strength to the device. + int rssi; + + /// Beacon UUID (for iBeacon). + String? uuid; + + /// Major value (for iBeacon). + int? major; + + /// Minor value (for iBeacon). + int? minor; + + double? accuracy; + + BeaconDevice({ + required this.rssi, + this.uuid, + this.major, + this.minor, + this.accuracy, + }) : super(); + + factory BeaconDevice.fromRangingResult(BeaconDevice result) => BeaconDevice( + uuid: result.uuid, + major: result.major, + minor: result.minor, + accuracy: result.accuracy, + rssi: result.rssi, + ); + + factory BeaconDevice.fromJson(Map json) => _$BeaconDeviceFromJson(json); + Map toJson() => _$BeaconDeviceToJson(this); + + @override + String toString() => '$runtimeType - ' + ', uuid: $uuid, major: $major, minor: $minor, accuracy: $accuracy' + ', rssi: $rssi'; +} + /// A [Data] holding wifi connectivity status in terms of connected SSID /// and BSSID. /// @@ -216,9 +258,55 @@ class Wifi extends Data { String toString() => '${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip'; } +/// A [Data] holding information of nearby Bluetooth devices. +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class BeaconData extends Data { + static const dataType = ConnectivitySamplingPackage.BLUETOOTH; + + /// Timestamp of scan start. + late DateTime startScan; + + /// Timestamp of scan end, if available. + DateTime? endScan; + + /// A map of [BeaconDevice] indexed by their [bluetoothDeviceId] to make + /// sure that the same device only appears once. + final Map _scanResult = {}; + + /// The list of [BeaconDevice] found in a scan. + List get scanResult => _scanResult.values.toList(); + set scanResult(List devices) => _scanResult.addEntries( + devices.map( + (device) => MapEntry( + device.uuid ?? '', + device, + ), + ), + ); + + BeaconData({DateTime? startScan, this.endScan}) : super() { + this.startScan = startScan ?? DateTime.now(); + } + + void addBluetoothDevice(BeaconDevice device) => _scanResult[device.uuid ?? ''] = device; + + void addBluetoothDevicesFromRangingResults(BeaconDevice result) { + addBluetoothDevice(BeaconDevice.fromRangingResult(result)); + } + + @override + Function get fromJsonFunction => _$BeaconDataFromJson; + factory BeaconData.fromJson(Map json) => FromJsonFactory().fromJson(json); + @override + Map toJson() => _$BeaconDataToJson(this); + + @override + String toString() => '${super.toString()}, scanResult: $scanResult'; +} + /// Beacon Region to use when monitoring for beacons. @JsonSerializable(includeIfNull: false, explicitToJson: true) -class BeaconRegion { +class BeaconRegion extends Data { /// A unique identifier for the beacon region. /// Used to distinguish between different regions being monitored. String identifier; @@ -251,6 +339,11 @@ class BeaconRegion { ); } + factory BeaconRegion.fromJson(Map json) => FromJsonFactory().fromJson(json); + + @override + Map toJson() => _$BeaconRegionToJson(this); + @override String toString() => '${super.toString()}, Identifier: $identifier, UUID: $uuid, Major: $major, Minor: $minor'; } diff --git a/packages/carp_connectivity_package/lib/connectivity_package.dart b/packages/carp_connectivity_package/lib/connectivity_package.dart index df71576df..e2ab994df 100644 --- a/packages/carp_connectivity_package/lib/connectivity_package.dart +++ b/packages/carp_connectivity_package/lib/connectivity_package.dart @@ -20,6 +20,13 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { /// * Use a [IntervalSamplingConfiguration] for configuration. static const String WIFI = "${NameSpace.CARP}.wifi"; + /// Measure type for Beacon ranging to detect and estimate proximity to + /// Bluetooth beacons (e.g., iBeacon, Eddystone). + /// * Typically returns beacon identifiers (UUID, major, minor) and + /// estimated distance or RSSI. + /// * Use a [PeriodicSamplingConfiguration] for configuration. + static const String BEACON = "${NameSpace.CARP}.beacon"; + @override DataTypeSamplingSchemeMap get samplingSchemes => DataTypeSamplingSchemeMap.from([ DataTypeSamplingScheme( @@ -49,6 +56,18 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { IntervalSamplingConfiguration( interval: const Duration(minutes: 10), )), + DataTypeSamplingScheme( + CamsDataTypeMetaData( + type: BEACON, + displayName: "Ranging beacons in proximity", + timeType: DataTimeType.POINT, + permissions: [Permission.bluetoothScan, Permission.locationAlways], + ), + PeriodicSamplingConfiguration( + interval: const Duration(minutes: 10), + duration: const Duration(seconds: 10), + ), + ), ]); @override @@ -60,6 +79,8 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { return BluetoothProbe(); case WIFI: return WifiProbe(); + case BEACON: + return BeaconProbe(); default: return null; } @@ -131,3 +152,27 @@ class BluetoothScanPeriodicSamplingConfiguration extends PeriodicSamplingConfigu factory BluetoothScanPeriodicSamplingConfiguration.fromJson(Map json) => FromJsonFactory().fromJson(json); } + +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class BeaconRangingPeriodicSamplingConfiguration extends PeriodicSamplingConfiguration { + /// List of beacon regions to monitor and/or range using the `flutter_beacon` package. + List beaconRegions; + + /// When a device is within this distance from the beacon, a predefined event is triggered. + /// Defaults to 2 meters. + int beaconDistance; + + BeaconRangingPeriodicSamplingConfiguration({ + required super.interval, + required super.duration, + this.beaconRegions = const [], + this.beaconDistance = 2, + }); + + @override + Map toJson() => _$BeaconRangingPeriodicSamplingConfigurationToJson(this); + @override + Function get fromJsonFunction => _$BeaconRangingPeriodicSamplingConfigurationFromJson; + factory BeaconRangingPeriodicSamplingConfiguration.fromJson(Map json) => + FromJsonFactory().fromJson(json); +} diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 6d774efe4..99465dc98 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -184,3 +184,108 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { _streamMonitoring = null; } } + +class BeaconProbe extends BufferingPeriodicStreamProbe { + /// Default timeout for bluetooth scan - 4 secs + static const DEFAULT_TIMEOUT = 4 * 1000; + Data? _data; + + @override + Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; + + List get beaconRegions => (samplingConfiguration is BeaconRangingPeriodicSamplingConfiguration) + ? (samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration).beaconRegions + : []; + + int get beaconDistance => (samplingConfiguration is BeaconRangingPeriodicSamplingConfiguration) + ? (samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration).beaconDistance + : 2; + + StreamSubscription? _streamMonitoring; + StreamSubscription? _streamRanging; + + @override + void onSamplingStart() { + _data = BeaconData(); + try { + _startMonitoring(); + } catch (error) { + _data = Error(message: 'Error scanning for bluetooth - $error'); + } + } + + @override + void onSamplingEnd() { + info('stopping monitoring'); + _stopMonitoring(); + + if (_data is BeaconData) (_data as BeaconData).endScan = DateTime.now(); + } + + @override + void onSamplingData(event) { + if (event is List) { + for (var device in event) { + (_data as BeaconData).addBluetoothDevicesFromRangingResults(device); + } + } + } + + Future _startMonitoring() async { + info('start monitoring & initializing scanning.'); + try { + await flutterBeacon.initializeScanning; + } catch (e) { + warning('error happened while initializing scanner $e'); + } + info('initialized scanner'); + + List regions = + beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); + + try { + _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { + if (result.monitoringState == MonitoringState.inside) { + info('🚪 Entered region: ${result.region.identifier}'); + _startRanging(result.region); + } else if (result.monitoringState == MonitoringState.outside) { + info('Not in region: ${result.region.identifier}'); + _stopMonitoring(); + } + }); + } catch (e) { + info('Error starting monitoring: $e'); + } + } + + void _startRanging(Region region) { + _streamRanging = flutterBeacon.ranging([region]).listen( + (RangingResult result) { + final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); + + for (var beacon in closeBeacons) { + info('✅ beacon in range: ${beacon.proximityUUID}, ${beacon.accuracy} m'); + (_data as BeaconData).addBluetoothDevicesFromRangingResults( + BeaconDevice( + rssi: beacon.rssi, + major: beacon.major, + minor: beacon.minor, + accuracy: beacon.accuracy, + ), + ); + } + }, + ); + } + + void _stopMonitoring() { + _streamRanging?.cancel(); + _streamRanging = null; + _streamMonitoring?.cancel(); + _streamMonitoring = null; + } + + @override + // TODO: implement bufferingStream + Stream get bufferingStream => throw UnimplementedError(); +} From bc7dcffa902c5a4ae9cfc9df65dabbd16faf12bc Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 23 Jul 2025 14:05:48 +0200 Subject: [PATCH 18/30] updated some code, implemented a buffer --- .../lib/connectivity_package.dart | 14 -- .../lib/connectivity_probes.dart | 176 ++++++------------ 2 files changed, 54 insertions(+), 136 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_package.dart b/packages/carp_connectivity_package/lib/connectivity_package.dart index e2ab994df..8357987ba 100644 --- a/packages/carp_connectivity_package/lib/connectivity_package.dart +++ b/packages/carp_connectivity_package/lib/connectivity_package.dart @@ -123,26 +123,12 @@ class BluetoothScanPeriodicSamplingConfiguration extends PeriodicSamplingConfigu /// List of remote device IDs to filter the scan results. List withRemoteIds; - /// Use Package `flutter_beacon` to enable beacon monitoring while the app is in background. - bool useBeaconMonitoring; - - /// List of beacon regions to monitor and/or range using the `flutter_beacon` package. - /// - /// When [useBeaconMonitoring] is true, the app will monitor these regions, potentially in the background if platform permissions and conditions allow. - List beaconRegions; - - /// When a device is within this distance from the beacon, a predefined event is triggered. - /// Defaults to 2 meters. - int beaconDistance; BluetoothScanPeriodicSamplingConfiguration({ required super.interval, required super.duration, this.withServices = const [], this.withRemoteIds = const [], - this.beaconRegions = const [], - this.useBeaconMonitoring = false, - this.beaconDistance = 2, }); @override diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 99465dc98..570cf5205 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -81,36 +81,16 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withRemoteIds : []; - bool get useBeaconMonitoring => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).useBeaconMonitoring - : false; - - List get beaconRegions => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconRegions - : []; - - int get beaconDistance => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconDistance - : 2; - - StreamSubscription? _streamMonitoring; - StreamSubscription? _streamRanging; - @override void onSamplingStart() { _data = Bluetooth(); try { - if (useBeaconMonitoring) { - info('Using beacon monitoring.'); - _startMonitoring(); - } else { - FlutterBluePlus.startScan( - withServices: services, - withRemoteIds: remoteIds, - timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT), - ); - } + FlutterBluePlus.startScan( + withServices: services, + withRemoteIds: remoteIds, + timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT), + ); } catch (error) { FlutterBluePlus.stopScan(); _data = Error(message: 'Error scanning for bluetooth - $error'); @@ -119,12 +99,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { @override void onSamplingEnd() { - if (useBeaconMonitoring) { - info('stopping monitoring'); - _stopMonitoring(); - } else { - FlutterBluePlus.stopScan(); - } + FlutterBluePlus.stopScan(); if (_data is Bluetooth) (_data as Bluetooth).endScan = DateTime.now(); } @@ -135,61 +110,21 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { (_data as Bluetooth).addBluetoothDevicesFromScanResults(event); } } - - Future _startMonitoring() async { - info('start monitoring & initializing scanning.'); - try { - await flutterBeacon.initializeScanning; - } catch (e) { - warning('error happened while initializing scanner $e'); - } - info('initialized scanner'); - - List regions = - beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); - - try { - _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { - if (result.monitoringState == MonitoringState.inside) { - info('🚪 Entered region: ${result.region.identifier}'); - _startRanging(result.region); - } else if (result.monitoringState == MonitoringState.outside) { - info('Not in region: ${result.region.identifier}'); - _stopMonitoring(); - } - }); - } catch (e) { - info('Error starting monitoring: $e'); - } - } - - void _startRanging(Region region) { - _streamRanging = flutterBeacon.ranging([region]).listen((RangingResult result) { - final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); - - for (var beacon in closeBeacons) { - info('✅ beacon in range: ${beacon.proximityUUID}, ${beacon.accuracy} m'); - (_data as Bluetooth).addBluetoothDevicesFromRangingResults( - beacon, - result.region.identifier, - ); - } - }); - } - - void _stopMonitoring() { - _streamRanging?.cancel(); - _streamRanging = null; - _streamMonitoring?.cancel(); - _streamMonitoring = null; - } } class BeaconProbe extends BufferingPeriodicStreamProbe { - /// Default timeout for bluetooth scan - 4 secs + /// Default timeout for Bluetooth scan - 4 secs static const DEFAULT_TIMEOUT = 4 * 1000; Data? _data; + final StreamController> _rangingController = StreamController>.broadcast(); + + @override + Stream> get bufferingStream => _rangingController.stream; + + StreamSubscription? _streamMonitoring; + StreamSubscription? _streamRanging; + @override Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; @@ -201,25 +136,28 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { ? (samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration).beaconDistance : 2; - StreamSubscription? _streamMonitoring; - StreamSubscription? _streamRanging; - @override - void onSamplingStart() { + void onSamplingStart() async { + info('Sampling started'); _data = BeaconData(); + try { + await flutterBeacon.initializeScanning; _startMonitoring(); } catch (error) { - _data = Error(message: 'Error scanning for bluetooth - $error'); + warning('Error initializing beacon scanning: $error'); + _data = Error(message: 'Error scanning for Bluetooth - $error'); } } @override void onSamplingEnd() { - info('stopping monitoring'); + info('Sampling ended, stopping monitoring and ranging'); _stopMonitoring(); - if (_data is BeaconData) (_data as BeaconData).endScan = DateTime.now(); + if (_data is BeaconData) { + (_data as BeaconData).endScan = DateTime.now(); + } } @override @@ -231,17 +169,9 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { } } - Future _startMonitoring() async { - info('start monitoring & initializing scanning.'); - try { - await flutterBeacon.initializeScanning; - } catch (e) { - warning('error happened while initializing scanner $e'); - } - info('initialized scanner'); - - List regions = - beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); + void _startMonitoring() { + info('Starting region monitoring'); + List regions = beaconRegions.isEmpty ? [] : beaconRegions.map((r) => r!.toRegion()).toList(); try { _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { @@ -250,42 +180,44 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { _startRanging(result.region); } else if (result.monitoringState == MonitoringState.outside) { info('Not in region: ${result.region.identifier}'); - _stopMonitoring(); + _stopRanging(); } }); } catch (e) { - info('Error starting monitoring: $e'); + warning('Error starting monitoring: $e'); } } void _startRanging(Region region) { - _streamRanging = flutterBeacon.ranging([region]).listen( - (RangingResult result) { - final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); - - for (var beacon in closeBeacons) { - info('✅ beacon in range: ${beacon.proximityUUID}, ${beacon.accuracy} m'); - (_data as BeaconData).addBluetoothDevicesFromRangingResults( - BeaconDevice( - rssi: beacon.rssi, - major: beacon.major, - minor: beacon.minor, - accuracy: beacon.accuracy, - ), - ); - } - }, - ); + info('Starting ranging in region: ${region.identifier}'); + _stopRanging(); + _streamRanging = flutterBeacon.ranging([region]).listen((RangingResult result) { + final closeBeacons = result.beacons + .where((b) => b.accuracy <= beaconDistance) + .map((b) => BeaconDevice( + rssi: b.rssi, + major: b.major, + minor: b.minor, + accuracy: b.accuracy, + )) + .toList(); + + _rangingController.add(closeBeacons); + + for (var device in closeBeacons) { + (_data as BeaconData).addBluetoothDevicesFromRangingResults(device); + } + }); } void _stopMonitoring() { - _streamRanging?.cancel(); - _streamRanging = null; + _stopRanging(); _streamMonitoring?.cancel(); _streamMonitoring = null; } - @override - // TODO: implement bufferingStream - Stream get bufferingStream => throw UnimplementedError(); + void _stopRanging() { + _streamRanging?.cancel(); + _streamRanging = null; + } } From 40699733cd84e0fbe45e967f84102b8d5f0070c8 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 23 Jul 2025 14:09:00 +0200 Subject: [PATCH 19/30] ran build runner to fix issue in g file --- .../carp_connectivity_package/lib/connectivity.g.dart | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity.g.dart b/packages/carp_connectivity_package/lib/connectivity.g.dart index c591bc581..60958f8c1 100644 --- a/packages/carp_connectivity_package/lib/connectivity.g.dart +++ b/packages/carp_connectivity_package/lib/connectivity.g.dart @@ -153,14 +153,6 @@ BluetoothScanPeriodicSamplingConfiguration ?.map((e) => e as String) .toList() ?? const [], - beaconRegions: (json['beaconRegions'] as List?) - ?.map((e) => e == null - ? null - : BeaconRegion.fromJson(e as Map)) - .toList() ?? - const [], - useBeaconMonitoring: json['useBeaconMonitoring'] as bool? ?? false, - beaconDistance: (json['beaconDistance'] as num?)?.toInt() ?? 2, ) ..$type = json['__type'] as String? ..lastTime = json['lastTime'] == null @@ -177,9 +169,6 @@ Map _$BluetoothScanPeriodicSamplingConfigurationToJson( 'duration': instance.duration.inMicroseconds, 'withServices': instance.withServices, 'withRemoteIds': instance.withRemoteIds, - 'useBeaconMonitoring': instance.useBeaconMonitoring, - 'beaconRegions': instance.beaconRegions.map((e) => e?.toJson()).toList(), - 'beaconDistance': instance.beaconDistance, }; BeaconRangingPeriodicSamplingConfiguration From 28831dfe0aa514a3d182c8f0bc4237ed4d22dbac Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 23 Jul 2025 14:17:42 +0200 Subject: [PATCH 20/30] updated the buffer method, as it was not working like supposed to --- .../lib/connectivity_probes.dart | 102 ++++++++++-------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 570cf5205..7e0cc4daf 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -113,18 +113,10 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { } class BeaconProbe extends BufferingPeriodicStreamProbe { - /// Default timeout for Bluetooth scan - 4 secs + /// Default timeout for bluetooth scan - 4 secs static const DEFAULT_TIMEOUT = 4 * 1000; Data? _data; - final StreamController> _rangingController = StreamController>.broadcast(); - - @override - Stream> get bufferingStream => _rangingController.stream; - - StreamSubscription? _streamMonitoring; - StreamSubscription? _streamRanging; - @override Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; @@ -136,28 +128,25 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { ? (samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration).beaconDistance : 2; + StreamSubscription? _streamMonitoring; + StreamSubscription? _streamRanging; + @override - void onSamplingStart() async { - info('Sampling started'); + void onSamplingStart() { _data = BeaconData(); - try { - await flutterBeacon.initializeScanning; _startMonitoring(); } catch (error) { - warning('Error initializing beacon scanning: $error'); - _data = Error(message: 'Error scanning for Bluetooth - $error'); + _data = Error(message: 'Error scanning for bluetooth - $error'); } } @override void onSamplingEnd() { - info('Sampling ended, stopping monitoring and ranging'); + info('stopping monitoring'); _stopMonitoring(); - if (_data is BeaconData) { - (_data as BeaconData).endScan = DateTime.now(); - } + if (_data is BeaconData) (_data as BeaconData).endScan = DateTime.now(); } @override @@ -169,9 +158,17 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { } } - void _startMonitoring() { - info('Starting region monitoring'); - List regions = beaconRegions.isEmpty ? [] : beaconRegions.map((r) => r!.toRegion()).toList(); + Future _startMonitoring() async { + info('start monitoring & initializing scanning.'); + try { + await flutterBeacon.initializeScanning; + } catch (e) { + warning('error happened while initializing scanner $e'); + } + info('initialized scanner'); + + List regions = + beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); try { _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { @@ -180,19 +177,51 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { _startRanging(result.region); } else if (result.monitoringState == MonitoringState.outside) { info('Not in region: ${result.region.identifier}'); - _stopRanging(); + _stopMonitoring(); } }); } catch (e) { - warning('Error starting monitoring: $e'); + info('Error starting monitoring: $e'); } } void _startRanging(Region region) { - info('Starting ranging in region: ${region.identifier}'); - _stopRanging(); - _streamRanging = flutterBeacon.ranging([region]).listen((RangingResult result) { - final closeBeacons = result.beacons + _streamRanging = flutterBeacon.ranging([region]).listen( + (RangingResult result) { + final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); + + for (var beacon in closeBeacons) { + info('✅ beacon in range: ${beacon.proximityUUID}, ${beacon.accuracy} m'); + (_data as BeaconData).addBluetoothDevicesFromRangingResults( + BeaconDevice( + rssi: beacon.rssi, + major: beacon.major, + minor: beacon.minor, + accuracy: beacon.accuracy, + ), + ); + } + }, + ); + } + + void _stopMonitoring() { + _streamRanging?.cancel(); + _streamRanging = null; + _streamMonitoring?.cancel(); + _streamMonitoring = null; + } + + @override + Stream> get bufferingStream { + List regions = beaconRegions.isEmpty ? [] : beaconRegions.map((r) => r!.toRegion()).toList(); + + if (regions.isEmpty) { + return Stream.empty(); + } + + return flutterBeacon.ranging(regions).map((result) { + return result.beacons .where((b) => b.accuracy <= beaconDistance) .map((b) => BeaconDevice( rssi: b.rssi, @@ -201,23 +230,6 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { accuracy: b.accuracy, )) .toList(); - - _rangingController.add(closeBeacons); - - for (var device in closeBeacons) { - (_data as BeaconData).addBluetoothDevicesFromRangingResults(device); - } }); } - - void _stopMonitoring() { - _stopRanging(); - _streamMonitoring?.cancel(); - _streamMonitoring = null; - } - - void _stopRanging() { - _streamRanging?.cancel(); - _streamRanging = null; - } } From 6ac63b7e5184398f8b8feac0966e192d14a0bb80 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 23 Jul 2025 14:23:37 +0200 Subject: [PATCH 21/30] trying out stream Probe --- .../lib/connectivity_probes.dart | 73 +++++++------------ 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 7e0cc4daf..e9ed83493 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -112,14 +112,11 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { } } -class BeaconProbe extends BufferingPeriodicStreamProbe { +class BeaconProbe extends StreamProbe { /// Default timeout for bluetooth scan - 4 secs static const DEFAULT_TIMEOUT = 4 * 1000; Data? _data; - @override - Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; - List get beaconRegions => (samplingConfiguration is BeaconRangingPeriodicSamplingConfiguration) ? (samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration).beaconRegions : []; @@ -131,33 +128,6 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { StreamSubscription? _streamMonitoring; StreamSubscription? _streamRanging; - @override - void onSamplingStart() { - _data = BeaconData(); - try { - _startMonitoring(); - } catch (error) { - _data = Error(message: 'Error scanning for bluetooth - $error'); - } - } - - @override - void onSamplingEnd() { - info('stopping monitoring'); - _stopMonitoring(); - - if (_data is BeaconData) (_data as BeaconData).endScan = DateTime.now(); - } - - @override - void onSamplingData(event) { - if (event is List) { - for (var device in event) { - (_data as BeaconData).addBluetoothDevicesFromRangingResults(device); - } - } - } - Future _startMonitoring() async { info('start monitoring & initializing scanning.'); try { @@ -213,23 +183,30 @@ class BeaconProbe extends BufferingPeriodicStreamProbe { } @override - Stream> get bufferingStream { - List regions = beaconRegions.isEmpty ? [] : beaconRegions.map((r) => r!.toRegion()).toList(); - - if (regions.isEmpty) { - return Stream.empty(); - } + Future onStart() async { + _startMonitoring(); - return flutterBeacon.ranging(regions).map((result) { - return result.beacons - .where((b) => b.accuracy <= beaconDistance) - .map((b) => BeaconDevice( - rssi: b.rssi, - major: b.major, - minor: b.minor, - accuracy: b.accuracy, - )) - .toList(); - }); + return super.onStart(); } + + @override + Stream get stream => flutterBeacon.ranging(beaconRegions.map((e) => e!.toRegion()).toList()).map( + (RangingResult result) { + final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); + if (closeBeacons.isNotEmpty) { + return Measurement.fromData(BeaconData() + ..startScan = DateTime.now() + ..scanResult = closeBeacons + .map((beacon) => BeaconDevice( + rssi: beacon.rssi, + uuid: beacon.proximityUUID, + major: beacon.major, + minor: beacon.minor, + accuracy: beacon.accuracy, + )) + .toList()); + } + return Measurement.fromData(BeaconData()); + }, + ); } From 049c43bedf56435a17123665198c18237e7ef090 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Wed, 23 Jul 2025 15:02:11 +0200 Subject: [PATCH 22/30] init data --- packages/carp_connectivity_package/lib/connectivity_probes.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index e9ed83493..43a9a5f5c 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -184,6 +184,8 @@ class BeaconProbe extends StreamProbe { @override Future onStart() async { + _data = BeaconData(); + _startMonitoring(); return super.onStart(); From 647767b1da6a71218b13d6a3f6ce98e0f6c4403a Mon Sep 17 00:00:00 2001 From: bardram Date: Fri, 25 Jul 2025 09:48:08 +0200 Subject: [PATCH 23/30] Update connectivity.g.dart --- .../lib/connectivity.g.dart | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity.g.dart b/packages/carp_connectivity_package/lib/connectivity.g.dart index 89739dcdf..60958f8c1 100644 --- a/packages/carp_connectivity_package/lib/connectivity.g.dart +++ b/packages/carp_connectivity_package/lib/connectivity.g.dart @@ -101,15 +101,38 @@ Map _$WifiToJson(Wifi instance) => { if (instance.ip case final value?) 'ip': value, }; +BeaconData _$BeaconDataFromJson(Map json) => BeaconData( + startScan: json['startScan'] == null + ? null + : DateTime.parse(json['startScan'] as String), + endScan: json['endScan'] == null + ? null + : DateTime.parse(json['endScan'] as String), + ) + ..$type = json['__type'] as String? + ..scanResult = (json['scanResult'] as List) + .map((e) => BeaconDevice.fromJson(e as Map)) + .toList(); + +Map _$BeaconDataToJson(BeaconData instance) => + { + if (instance.$type case final value?) '__type': value, + 'startScan': instance.startScan.toIso8601String(), + if (instance.endScan?.toIso8601String() case final value?) + 'endScan': value, + 'scanResult': instance.scanResult.map((e) => e.toJson()).toList(), + }; + BeaconRegion _$BeaconRegionFromJson(Map json) => BeaconRegion( identifier: json['identifier'] as String, uuid: json['uuid'] as String, major: (json['major'] as num?)?.toInt(), minor: (json['minor'] as num?)?.toInt(), - ); + )..$type = json['__type'] as String?; Map _$BeaconRegionToJson(BeaconRegion instance) => { + if (instance.$type case final value?) '__type': value, 'identifier': instance.identifier, 'uuid': instance.uuid, if (instance.major case final value?) 'major': value, @@ -130,14 +153,6 @@ BluetoothScanPeriodicSamplingConfiguration ?.map((e) => e as String) .toList() ?? const [], - beaconRegions: (json['beaconRegions'] as List?) - ?.map((e) => e == null - ? null - : BeaconRegion.fromJson(e as Map)) - .toList() ?? - const [], - useBeaconMonitoring: json['useBeaconMonitoring'] as bool? ?? false, - beaconDistance: (json['beaconDistance'] as num?)?.toInt() ?? 2, ) ..$type = json['__type'] as String? ..lastTime = json['lastTime'] == null @@ -154,9 +169,6 @@ Map _$BluetoothScanPeriodicSamplingConfigurationToJson( 'duration': instance.duration.inMicroseconds, 'withServices': instance.withServices, 'withRemoteIds': instance.withRemoteIds, - 'useBeaconMonitoring': instance.useBeaconMonitoring, - 'beaconRegions': instance.beaconRegions.map((e) => e?.toJson()).toList(), - 'beaconDistance': instance.beaconDistance, }; BeaconRangingPeriodicSamplingConfiguration From d50d5fc873c58f40b5d0d56402be04d3d26b15f4 Mon Sep 17 00:00:00 2001 From: bardram Date: Fri, 25 Jul 2025 12:32:58 +0200 Subject: [PATCH 24/30] major refactor of data model and probe for iBeacon scanning --- .../carp_connectivity_package/CHANGELOG.md | 5 + packages/carp_connectivity_package/README.md | 47 ++++- .../example/lib/example.dart | 46 +++++ .../lib/connectivity.g.dart | 84 ++++---- .../lib/connectivity_data.dart | 182 ++++++++---------- .../lib/connectivity_package.dart | 121 +++++++++--- .../lib/connectivity_probes.dart | 167 +++++++--------- .../carp_connectivity_package/pubspec.yaml | 2 +- 8 files changed, 372 insertions(+), 282 deletions(-) diff --git a/packages/carp_connectivity_package/CHANGELOG.md b/packages/carp_connectivity_package/CHANGELOG.md index 110fea769..c7758f698 100644 --- a/packages/carp_connectivity_package/CHANGELOG.md +++ b/packages/carp_connectivity_package/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.9.0 + +* added support for scanning iBeacons using the [dchs_flutter_beacon](https://pub.dev/packages/dchs_flutter_beacon) plugin +* use the `BEACON` measure type for collecting `BeaconData` within a set of specific regions as specified in a `BeaconRangingPeriodicSamplingConfiguration` + ## 1.8.1+1 * added `BluetoothScanPeriodicSamplingConfiguration` diff --git a/packages/carp_connectivity_package/README.md b/packages/carp_connectivity_package/README.md index ea3264a69..30f64b8f1 100644 --- a/packages/carp_connectivity_package/README.md +++ b/packages/carp_connectivity_package/README.md @@ -12,6 +12,7 @@ This packages supports sampling of the following [`Measure`](https://github.com/ * `dk.cachet.carp.wifi` * `dk.cachet.carp.connectivity` * `dk.cachet.carp.bluetooth` +* `dk.cachet.carp.beacon` See the [wiki](https://github.com/cph-cachet/carp.sensing-flutter/wiki) for further documentation, particularly on available [measure types](https://github.com/cph-cachet/carp.sensing-flutter/wiki/A.-Measure-Types). See the [CARP Mobile Sensing App](https://github.com/cph-cachet/carp.sensing-flutter/tree/master/apps/carp_mobile_sensing_app) for an example of how to build a mobile sensing app in Flutter. @@ -55,7 +56,7 @@ Add the following to your app's `AndroidManifest.xml` file located in `android/a - + ```` @@ -70,8 +71,10 @@ and here for the [iOS](https://developer.apple.com/documentation/systemconfigura To enable bluetooth tracking, add these permissions in the `Info.plist` file located in `ios/Runner`: ````xml + + NSBluetoothAlwaysUsageDescription -Bluetooth needed +Reason why app needs bluetooth UIBackgroundModes bluetooth-central @@ -83,6 +86,21 @@ To enable bluetooth tracking, add these permissions in the `Info.plist` file loc > Note that on iOS, it is [impossible to do a general Bluetooth scan when the screen is off or the app is in background](https://developer.apple.com/forums/thread/652592). This will simply result in an empty scan. Hence, bluetooth devices are only collected when the app is in the foreground. +To collect iBeacon measurements, please follow the setup described in the [dchs_flutter_beacon](https://pub.dev/packages/dchs_flutter_beacon) plugin. Especially, for iOS you need permissions to access location information in the `Info.plist` file: + +````xml + +NSLocationWhenInUseUsageDescription +Reason why app needs location + + +NSLocationAlwaysAndWhenInUseUsageDescription +Reason why app needs location + +NSLocationAlwaysUsageDescription +Reason why app needs location +```` + ## Using it To use this package, import it into your app together with the @@ -144,3 +162,28 @@ protocol.addTaskControl( ]), phone); ``` + +If you want to collect iBeacon measurements, you can use a [BeaconRangingPeriodicSamplingConfiguration] to configure the scan. The following example will scan for iBeacons in the specified regions which are closer than 2 meters. The regions are specified by their identifier and UUID. See the [dchs_flutter_beacon](https://pub.dev/packages/dchs_flutter_beacon) plugin for more information on how to set up iBeacon regions. + +```dart + protocol.addTaskControl( + ImmediateTrigger(), + BackgroundTask(measures: [ + Measure( + type: ConnectivitySamplingPackage.BEACON, + samplingConfiguration: BeaconRangingPeriodicSamplingConfiguration( + beaconDistance: 2, + beaconRegions: [ + BeaconRegion( + identifier: 'region1', + uuid: '12345678-1234-1234-1234-123456789012', + ), + BeaconRegion( + identifier: 'region2', + uuid: '12345678-1234-1234-1234-123456789012', + ), + ], + )) + ]), + phone); +``` diff --git a/packages/carp_connectivity_package/example/lib/example.dart b/packages/carp_connectivity_package/example/lib/example.dart index 827979575..12e87af51 100644 --- a/packages/carp_connectivity_package/example/lib/example.dart +++ b/packages/carp_connectivity_package/example/lib/example.dart @@ -31,4 +31,50 @@ void main() async { Measure(type: ConnectivitySamplingPackage.WIFI), ]), phone); + + // If you want to scan for nearby bluetooth devices, you can use a + // [BluetoothScanPeriodicSamplingConfiguration] to configure the scan. + // This will scan for bluetooth devices every 10 minutes for 10 seconds. + // You can also filter by remoteIds and services. + protocol.addTaskControl( + ImmediateTrigger(), + BackgroundTask(measures: [ + Measure( + type: ConnectivitySamplingPackage.BLUETOOTH, + samplingConfiguration: BluetoothScanPeriodicSamplingConfiguration( + interval: const Duration(minutes: 10), + duration: const Duration(seconds: 10), + withRemoteIds: ['123', '456'], + withServices: ['service1', 'service2'], + )) + ]), + phone); + + // If you want to collect iBeacon measurements, you can use a + // [BeaconRangingPeriodicSamplingConfiguration] to configure the scan. + // This will scan for iBeacons in the specified regions which are closer than + // 2 meters. The regions are specified by their identifier and UUID. + // + // See the dchs_flutter_beacon plugin for more information on how to set up + // iBeacon regions. + protocol.addTaskControl( + ImmediateTrigger(), + BackgroundTask(measures: [ + Measure( + type: ConnectivitySamplingPackage.BEACON, + samplingConfiguration: BeaconRangingPeriodicSamplingConfiguration( + beaconDistance: 2, // 2 meters + beaconRegions: [ + BeaconRegion( + identifier: 'region1', + uuid: '12345678-1234-1234-1234-123456789012', + ), + BeaconRegion( + identifier: 'region2', + uuid: '12345678-1234-1234-1234-123456789012', + ), + ], + )) + ]), + phone); } diff --git a/packages/carp_connectivity_package/lib/connectivity.g.dart b/packages/carp_connectivity_package/lib/connectivity.g.dart index 60958f8c1..e9cdd6e45 100644 --- a/packages/carp_connectivity_package/lib/connectivity.g.dart +++ b/packages/carp_connectivity_package/lib/connectivity.g.dart @@ -71,23 +71,6 @@ Map _$BluetoothDeviceToJson(BluetoothDevice instance) => 'rssi': instance.rssi, }; -BeaconDevice _$BeaconDeviceFromJson(Map json) => BeaconDevice( - rssi: (json['rssi'] as num).toInt(), - uuid: json['uuid'] as String?, - major: (json['major'] as num?)?.toInt(), - minor: (json['minor'] as num?)?.toInt(), - accuracy: (json['accuracy'] as num?)?.toDouble(), - ); - -Map _$BeaconDeviceToJson(BeaconDevice instance) => - { - 'rssi': instance.rssi, - if (instance.uuid case final value?) 'uuid': value, - if (instance.major case final value?) 'major': value, - if (instance.minor case final value?) 'minor': value, - if (instance.accuracy case final value?) 'accuracy': value, - }; - Wifi _$WifiFromJson(Map json) => Wifi( ssid: json['ssid'] as String?, bssid: json['bssid'] as String?, @@ -102,12 +85,7 @@ Map _$WifiToJson(Wifi instance) => { }; BeaconData _$BeaconDataFromJson(Map json) => BeaconData( - startScan: json['startScan'] == null - ? null - : DateTime.parse(json['startScan'] as String), - endScan: json['endScan'] == null - ? null - : DateTime.parse(json['endScan'] as String), + region: json['region'] as String, ) ..$type = json['__type'] as String? ..scanResult = (json['scanResult'] as List) @@ -117,28 +95,37 @@ BeaconData _$BeaconDataFromJson(Map json) => BeaconData( Map _$BeaconDataToJson(BeaconData instance) => { if (instance.$type case final value?) '__type': value, - 'startScan': instance.startScan.toIso8601String(), - if (instance.endScan?.toIso8601String() case final value?) - 'endScan': value, + 'region': instance.region, 'scanResult': instance.scanResult.map((e) => e.toJson()).toList(), }; -BeaconRegion _$BeaconRegionFromJson(Map json) => BeaconRegion( - identifier: json['identifier'] as String, +BeaconDevice _$BeaconDeviceFromJson(Map json) => BeaconDevice( + rssi: (json['rssi'] as num).toInt(), uuid: json['uuid'] as String, major: (json['major'] as num?)?.toInt(), minor: (json['minor'] as num?)?.toInt(), - )..$type = json['__type'] as String?; + accuracy: (json['accuracy'] as num?)?.toDouble(), + proximity: $enumDecodeNullable(_$ProximityEnumMap, json['proximity']), + ); -Map _$BeaconRegionToJson(BeaconRegion instance) => +Map _$BeaconDeviceToJson(BeaconDevice instance) => { - if (instance.$type case final value?) '__type': value, - 'identifier': instance.identifier, 'uuid': instance.uuid, + 'rssi': instance.rssi, if (instance.major case final value?) 'major': value, if (instance.minor case final value?) 'minor': value, + if (instance.accuracy case final value?) 'accuracy': value, + if (_$ProximityEnumMap[instance.proximity] case final value?) + 'proximity': value, }; +const _$ProximityEnumMap = { + Proximity.unknown: 'unknown', + Proximity.immediate: 'immediate', + Proximity.near: 'near', + Proximity.far: 'far', +}; + BluetoothScanPeriodicSamplingConfiguration _$BluetoothScanPeriodicSamplingConfigurationFromJson( Map json) => @@ -175,29 +162,32 @@ BeaconRangingPeriodicSamplingConfiguration _$BeaconRangingPeriodicSamplingConfigurationFromJson( Map json) => BeaconRangingPeriodicSamplingConfiguration( - interval: Duration(microseconds: (json['interval'] as num).toInt()), - duration: Duration(microseconds: (json['duration'] as num).toInt()), beaconRegions: (json['beaconRegions'] as List?) - ?.map((e) => e == null - ? null - : BeaconRegion.fromJson(e as Map)) + ?.map((e) => BeaconRegion.fromJson(e as Map)) .toList() ?? const [], beaconDistance: (json['beaconDistance'] as num?)?.toInt() ?? 2, - ) - ..$type = json['__type'] as String? - ..lastTime = json['lastTime'] == null - ? null - : DateTime.parse(json['lastTime'] as String); + )..$type = json['__type'] as String?; Map _$BeaconRangingPeriodicSamplingConfigurationToJson( BeaconRangingPeriodicSamplingConfiguration instance) => { if (instance.$type case final value?) '__type': value, - if (instance.lastTime?.toIso8601String() case final value?) - 'lastTime': value, - 'interval': instance.interval.inMicroseconds, - 'duration': instance.duration.inMicroseconds, - 'beaconRegions': instance.beaconRegions.map((e) => e?.toJson()).toList(), + 'beaconRegions': instance.beaconRegions.map((e) => e.toJson()).toList(), 'beaconDistance': instance.beaconDistance, }; + +BeaconRegion _$BeaconRegionFromJson(Map json) => BeaconRegion( + identifier: json['identifier'] as String, + uuid: json['uuid'] as String, + major: (json['major'] as num?)?.toInt(), + minor: (json['minor'] as num?)?.toInt(), + ); + +Map _$BeaconRegionToJson(BeaconRegion instance) => + { + 'identifier': instance.identifier, + 'uuid': instance.uuid, + if (instance.major case final value?) 'major': value, + if (instance.minor case final value?) 'minor': value, + }; diff --git a/packages/carp_connectivity_package/lib/connectivity_data.dart b/packages/carp_connectivity_package/lib/connectivity_data.dart index 5b2f2f383..244ebfb4a 100644 --- a/packages/carp_connectivity_package/lib/connectivity_data.dart +++ b/packages/carp_connectivity_package/lib/connectivity_data.dart @@ -41,17 +41,23 @@ class Connectivity extends Data { Connectivity() : super(); - Connectivity.fromConnectivityResult(List result) : super() { - connectivityStatus = result.map((connectivity.ConnectivityResult e) => _parseConnectivityStatus(e)).toList(); + Connectivity.fromConnectivityResult( + List result) + : super() { + connectivityStatus = result + .map((connectivity.ConnectivityResult e) => _parseConnectivityStatus(e)) + .toList(); } @override Function get fromJsonFunction => _$ConnectivityFromJson; - factory Connectivity.fromJson(Map json) => FromJsonFactory().fromJson(json); + factory Connectivity.fromJson(Map json) => + FromJsonFactory().fromJson(json); @override Map toJson() => _$ConnectivityToJson(this); - static ConnectivityStatus _parseConnectivityStatus(connectivity.ConnectivityResult result) { + static ConnectivityStatus _parseConnectivityStatus( + connectivity.ConnectivityResult result) { switch (result) { case connectivity.ConnectivityResult.bluetooth: return ConnectivityStatus.bluetooth; @@ -71,7 +77,8 @@ class Connectivity extends Data { } @override - String toString() => '${super.toString()}, connectivityStatus: $connectivityStatus'; + String toString() => + '${super.toString()}, connectivityStatus: $connectivityStatus'; } /// A [Data] holding information of nearby Bluetooth devices. @@ -91,14 +98,15 @@ class Bluetooth extends Data { /// The list of [BluetoothDevice] found in a scan. List get scanResult => _scanResult.values.toList(); - set scanResult(List devices) => - _scanResult.addEntries(devices.map((device) => MapEntry(device.bluetoothDeviceId, device))); + set scanResult(List devices) => _scanResult.addEntries( + devices.map((device) => MapEntry(device.bluetoothDeviceId, device))); Bluetooth({DateTime? startScan, this.endScan}) : super() { this.startScan = startScan ?? DateTime.now(); } - void addBluetoothDevice(BluetoothDevice device) => _scanResult[device.bluetoothDeviceId] = device; + void addBluetoothDevice(BluetoothDevice device) => + _scanResult[device.bluetoothDeviceId] = device; void addBluetoothDevicesFromScanResults(List results) { for (var scanResult in results) { @@ -115,7 +123,8 @@ class Bluetooth extends Data { @override Function get fromJsonFunction => _$BluetoothFromJson; - factory Bluetooth.fromJson(Map json) => FromJsonFactory().fromJson(json); + factory Bluetooth.fromJson(Map json) => + FromJsonFactory().fromJson(json); @override Map toJson() => _$BluetoothToJson(this); @@ -162,7 +171,8 @@ class BluetoothDevice { rssi: result.rssi, ); - factory BluetoothDevice.fromRangingResult(Beacon result, String beaconName) => BluetoothDevice( + factory BluetoothDevice.fromRangingResult(Beacon result, String beaconName) => + BluetoothDevice( bluetoothDeviceId: beaconName, bluetoothDeviceName: beaconName, connectable: false, @@ -171,7 +181,8 @@ class BluetoothDevice { rssi: result.rssi, ); - factory BluetoothDevice.fromJson(Map json) => _$BluetoothDeviceFromJson(json); + factory BluetoothDevice.fromJson(Map json) => + _$BluetoothDeviceFromJson(json); Map toJson() => _$BluetoothDeviceToJson(this); @override @@ -183,48 +194,6 @@ class BluetoothDevice { ', rssi: $rssi'; } -/// Beacon device data. -@JsonSerializable(includeIfNull: false, explicitToJson: true) -class BeaconDevice { - /// The RSSI signal strength to the device. - int rssi; - - /// Beacon UUID (for iBeacon). - String? uuid; - - /// Major value (for iBeacon). - int? major; - - /// Minor value (for iBeacon). - int? minor; - - double? accuracy; - - BeaconDevice({ - required this.rssi, - this.uuid, - this.major, - this.minor, - this.accuracy, - }) : super(); - - factory BeaconDevice.fromRangingResult(BeaconDevice result) => BeaconDevice( - uuid: result.uuid, - major: result.major, - minor: result.minor, - accuracy: result.accuracy, - rssi: result.rssi, - ); - - factory BeaconDevice.fromJson(Map json) => _$BeaconDeviceFromJson(json); - Map toJson() => _$BeaconDeviceToJson(this); - - @override - String toString() => '$runtimeType - ' - ', uuid: $uuid, major: $major, minor: $minor, accuracy: $accuracy' - ', rssi: $rssi'; -} - /// A [Data] holding wifi connectivity status in terms of connected SSID /// and BSSID. /// @@ -250,53 +219,49 @@ class Wifi extends Data { @override Function get fromJsonFunction => _$WifiFromJson; - factory Wifi.fromJson(Map json) => FromJsonFactory().fromJson(json); + factory Wifi.fromJson(Map json) => + FromJsonFactory().fromJson(json); @override Map toJson() => _$WifiToJson(this); @override - String toString() => '${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip'; + String toString() => + '${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip'; } -/// A [Data] holding information of nearby Bluetooth devices. +/// A [Data] holding information of nearby Beacon devices. @JsonSerializable(includeIfNull: false, explicitToJson: true) class BeaconData extends Data { - static const dataType = ConnectivitySamplingPackage.BLUETOOTH; + static const dataType = ConnectivitySamplingPackage.BEACON; - /// Timestamp of scan start. - late DateTime startScan; + /// The unique identifier of the region that this beacon belongs to. + String region; - /// Timestamp of scan end, if available. - DateTime? endScan; - - /// A map of [BeaconDevice] indexed by their [bluetoothDeviceId] to make + /// A map of [BeaconDevice] indexed by their UUID to make /// sure that the same device only appears once. final Map _scanResult = {}; /// The list of [BeaconDevice] found in a scan. List get scanResult => _scanResult.values.toList(); - set scanResult(List devices) => _scanResult.addEntries( - devices.map( - (device) => MapEntry( - device.uuid ?? '', - device, - ), - ), - ); + set scanResult(List devices) => _scanResult + .addEntries(devices.map((device) => MapEntry(device.uuid, device))); - BeaconData({DateTime? startScan, this.endScan}) : super() { - this.startScan = startScan ?? DateTime.now(); - } + BeaconData({required this.region}) : super(); - void addBluetoothDevice(BeaconDevice device) => _scanResult[device.uuid ?? ''] = device; + void addBeaconDevice(BeaconDevice device) => + _scanResult[device.uuid] = device; - void addBluetoothDevicesFromRangingResults(BeaconDevice result) { - addBluetoothDevice(BeaconDevice.fromRangingResult(result)); + void addBeaconDevicesFromRangingResults(RangingResult result) { + region = result.region.identifier; + for (var beacon in result.beacons) { + addBeaconDevice(BeaconDevice.fromRegionAndBeacon(beacon)); + } } @override Function get fromJsonFunction => _$BeaconDataFromJson; - factory BeaconData.fromJson(Map json) => FromJsonFactory().fromJson(json); + factory BeaconData.fromJson(Map json) => + FromJsonFactory().fromJson(json); @override Map toJson() => _$BeaconDataToJson(this); @@ -304,46 +269,51 @@ class BeaconData extends Data { String toString() => '${super.toString()}, scanResult: $scanResult'; } -/// Beacon Region to use when monitoring for beacons. +/// Beacon device data. @JsonSerializable(includeIfNull: false, explicitToJson: true) -class BeaconRegion extends Data { - /// A unique identifier for the beacon region. - /// Used to distinguish between different regions being monitored. - String identifier; - - /// The proximity UUID of the beacon. - /// This is a 128-bit value used to identify a group of related beacons. +class BeaconDevice { + /// The proximity UUID of beacon. String uuid; - /// The major value of the beacon region (optional). - /// Used to further distinguish a subset of beacons within the same UUID. + /// The RSSI signal strength to the device. + int rssi; + + /// Major value (for iBeacon). int? major; - /// The minor value of the beacon region (optional). - /// Provides a finer granularity within a group of beacons identified by the same UUID and major value. + /// Minor value (for iBeacon). int? minor; - BeaconRegion({ - required this.identifier, + /// The accuracy of distance of beacon in meter. + double? accuracy; + + /// The proximity of beacon. + final Proximity? proximity; + + BeaconDevice({ + required this.rssi, required this.uuid, this.major, this.minor, - }); - - Region toRegion() { - return Region( - identifier: identifier, - proximityUUID: uuid, - major: major, - minor: minor, - ); - } + this.accuracy, + this.proximity, + }) : super(); - factory BeaconRegion.fromJson(Map json) => FromJsonFactory().fromJson(json); + factory BeaconDevice.fromRegionAndBeacon(Beacon beacon) => BeaconDevice( + rssi: beacon.rssi, + uuid: beacon.proximityUUID, + major: beacon.major, + minor: beacon.minor, + accuracy: beacon.accuracy, + proximity: beacon.proximity, + ); - @override - Map toJson() => _$BeaconRegionToJson(this); + factory BeaconDevice.fromJson(Map json) => + _$BeaconDeviceFromJson(json); + Map toJson() => _$BeaconDeviceToJson(this); @override - String toString() => '${super.toString()}, Identifier: $identifier, UUID: $uuid, Major: $major, Minor: $minor'; + String toString() => '$runtimeType - ' + ', uuid: $uuid, major: $major, minor: $minor, accuracy: $accuracy' + ', rssi: $rssi'; } diff --git a/packages/carp_connectivity_package/lib/connectivity_package.dart b/packages/carp_connectivity_package/lib/connectivity_package.dart index 8357987ba..7ccff0cc9 100644 --- a/packages/carp_connectivity_package/lib/connectivity_package.dart +++ b/packages/carp_connectivity_package/lib/connectivity_package.dart @@ -11,7 +11,7 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { /// Measure type for collection of nearby Bluetooth devices on a regular basis. /// * Event-based (Periodic) measure - default every 10 minutes for 10 seconds. /// * Uses the [Smartphone] master device for data collection. - /// * Use a [PeriodicSamplingConfiguration] for configuration. + /// * Use a [BluetoothScanPeriodicSamplingConfiguration] for configuration. static const String BLUETOOTH = "${NameSpace.CARP}.bluetooth"; /// Measure type for collection of wifi information (SSID, BSSID, IP). @@ -22,13 +22,16 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { /// Measure type for Beacon ranging to detect and estimate proximity to /// Bluetooth beacons (e.g., iBeacon, Eddystone). - /// * Typically returns beacon identifiers (UUID, major, minor) and - /// estimated distance or RSSI. - /// * Use a [PeriodicSamplingConfiguration] for configuration. + /// Typically collects beacon identifiers (UUID, major, minor) and + /// estimated distance or RSSI. + /// * Event-based (Interval) measure - default every 10 minutes. + /// * Uses the [Smartphone] master device for data collection. + /// * Use a [BeaconRangingPeriodicSamplingConfiguration] for configuration. static const String BEACON = "${NameSpace.CARP}.beacon"; @override - DataTypeSamplingSchemeMap get samplingSchemes => DataTypeSamplingSchemeMap.from([ + DataTypeSamplingSchemeMap get samplingSchemes => + DataTypeSamplingSchemeMap.from([ DataTypeSamplingScheme( CamsDataTypeMetaData( type: CONNECTIVITY, @@ -59,14 +62,11 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { DataTypeSamplingScheme( CamsDataTypeMetaData( type: BEACON, - displayName: "Ranging beacons in proximity", + displayName: "Ranging iBeacons", timeType: DataTimeType.POINT, permissions: [Permission.bluetoothScan, Permission.locationAlways], ), - PeriodicSamplingConfiguration( - interval: const Duration(minutes: 10), - duration: const Duration(seconds: 10), - ), + BeaconRangingPeriodicSamplingConfiguration(), ), ]); @@ -93,6 +93,7 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { Connectivity(), Bluetooth(), Wifi(), + BeaconData(region: ''), BluetoothScanPeriodicSamplingConfiguration( interval: const Duration(minutes: 10), duration: const Duration(seconds: 10), @@ -100,8 +101,12 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { ]); // registering default privacy functions - DataTransformerSchemaRegistry().lookup(PrivacySchema.DEFAULT)!.add(BLUETOOTH, bluetoothNameAnonymizer); - DataTransformerSchemaRegistry().lookup(PrivacySchema.DEFAULT)!.add(WIFI, wifiNameAnonymizer); + DataTransformerSchemaRegistry() + .lookup(PrivacySchema.DEFAULT)! + .add(BLUETOOTH, bluetoothNameAnonymizer); + DataTransformerSchemaRegistry() + .lookup(PrivacySchema.DEFAULT)! + .add(WIFI, wifiNameAnonymizer); } } @@ -116,14 +121,14 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { /// Filtering on remoteIds allows Android to scan for devices in the background /// without needing to be in the foreground. This is not possible on iOS. @JsonSerializable(includeIfNull: false, explicitToJson: true) -class BluetoothScanPeriodicSamplingConfiguration extends PeriodicSamplingConfiguration { +class BluetoothScanPeriodicSamplingConfiguration + extends PeriodicSamplingConfiguration { /// List of Bluetooth service UUIDs to filter the scan results. List withServices; /// List of remote device IDs to filter the scan results. List withRemoteIds; - BluetoothScanPeriodicSamplingConfiguration({ required super.interval, required super.duration, @@ -132,33 +137,89 @@ class BluetoothScanPeriodicSamplingConfiguration extends PeriodicSamplingConfigu }); @override - Map toJson() => _$BluetoothScanPeriodicSamplingConfigurationToJson(this); + Map toJson() => + _$BluetoothScanPeriodicSamplingConfigurationToJson(this); @override - Function get fromJsonFunction => _$BluetoothScanPeriodicSamplingConfigurationFromJson; - factory BluetoothScanPeriodicSamplingConfiguration.fromJson(Map json) => - FromJsonFactory().fromJson(json); + Function get fromJsonFunction => + _$BluetoothScanPeriodicSamplingConfigurationFromJson; + factory BluetoothScanPeriodicSamplingConfiguration.fromJson( + Map json) => + FromJsonFactory() + .fromJson(json); } +/// A sampling configuration specifying how to scan for iBeacon devices. +/// +/// The regions of the beacons to monitor are specified in the [beaconRegions] +/// list. The [beaconDistance] is used to determine the proximity to the beacon, +/// with a default value of 2 meters. @JsonSerializable(includeIfNull: false, explicitToJson: true) -class BeaconRangingPeriodicSamplingConfiguration extends PeriodicSamplingConfiguration { - /// List of beacon regions to monitor and/or range using the `flutter_beacon` package. - List beaconRegions; +class BeaconRangingPeriodicSamplingConfiguration extends SamplingConfiguration { + /// List of beacon regions to monitor and range. + List beaconRegions; - /// When a device is within this distance from the beacon, a predefined event is triggered. - /// Defaults to 2 meters. + /// The distance in meters to consider a beacon as "in range". int beaconDistance; BeaconRangingPeriodicSamplingConfiguration({ - required super.interval, - required super.duration, this.beaconRegions = const [], this.beaconDistance = 2, - }); + }) : super(); @override - Map toJson() => _$BeaconRangingPeriodicSamplingConfigurationToJson(this); + Map toJson() => + _$BeaconRangingPeriodicSamplingConfigurationToJson(this); + @override + Function get fromJsonFunction => + _$BeaconRangingPeriodicSamplingConfigurationFromJson; + factory BeaconRangingPeriodicSamplingConfiguration.fromJson( + Map json) => + FromJsonFactory() + .fromJson(json); +} + +/// Beacon region to use when scanning for beacons. +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class BeaconRegion { + /// A unique identifier for the beacon region. + /// Used to distinguish between different regions being monitored. + String identifier; + + /// The proximity UUID of the beacon. + /// This is a 128-bit value used to identify a group of related beacons. + String uuid; + + /// The major value of the beacon region (optional). + /// Used to further distinguish a subset of beacons within the same UUID. + int? major; + + /// The minor value of the beacon region (optional). + /// Provides a finer granularity within a group of beacons identified by + /// the same UUID and major value. + int? minor; + + BeaconRegion({ + required this.identifier, + required this.uuid, + this.major, + this.minor, + }); + + Region toRegion() { + return Region( + identifier: identifier, + proximityUUID: uuid, + major: major, + minor: minor, + ); + } + + factory BeaconRegion.fromJson(Map json) => + _$BeaconRegionFromJson(json); + + Map toJson() => _$BeaconRegionToJson(this); + @override - Function get fromJsonFunction => _$BeaconRangingPeriodicSamplingConfigurationFromJson; - factory BeaconRangingPeriodicSamplingConfiguration.fromJson(Map json) => - FromJsonFactory().fromJson(json); + String toString() => + '${super.toString()}, Identifier: $identifier, UUID: $uuid, Major: $major, Minor: $minor'; } diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 43a9a5f5c..a4c28630f 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -13,16 +13,18 @@ class ConnectivityProbe extends StreamProbe { @override Future onStart() async { // collect the current connectivity status on sampling start - var connectivityStatus = await connectivity.Connectivity().checkConnectivity(); - addMeasurement(Measurement.fromData(Connectivity.fromConnectivityResult(connectivityStatus))); + var connectivityStatus = + await connectivity.Connectivity().checkConnectivity(); + addMeasurement(Measurement.fromData( + Connectivity.fromConnectivityResult(connectivityStatus))); return super.onStart(); } @override - Stream get stream => connectivity.Connectivity() - .onConnectivityChanged - .map((event) => Measurement.fromData(Connectivity.fromConnectivityResult(event))); + Stream get stream => + connectivity.Connectivity().onConnectivityChanged.map((event) => + Measurement.fromData(Connectivity.fromConnectivityResult(event))); } // This probe requests access to location permissions (both on Android and iOS). @@ -67,18 +69,24 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { Stream get bufferingStream => FlutterBluePlus.scanResults; @override - Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; + Future getMeasurement() async => + _data != null ? Measurement.fromData(_data!) : null; // if a BT-specific sampling configuration is used, we need to // extract the services and remoteIds from it so FlutterBluePlus can // perform filtered scanning - - List get services => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withServices.map((e) => Guid(e)).toList() + List get services => (samplingConfiguration + is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) + .withServices + .map((e) => Guid(e)) + .toList() : []; - List get remoteIds => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withRemoteIds + List get remoteIds => (samplingConfiguration + is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) + .withRemoteIds : []; @override @@ -89,7 +97,8 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { FlutterBluePlus.startScan( withServices: services, withRemoteIds: remoteIds, - timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT), + timeout: samplingConfiguration?.duration ?? + const Duration(milliseconds: DEFAULT_TIMEOUT), ); } catch (error) { FlutterBluePlus.stopScan(); @@ -112,103 +121,69 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { } } +/// A Probe that constantly scans for nearby and visible iBeacon devices and collects a +/// [BeaconData] measurement that lists each [BeaconDevice] found during the scan. +/// +/// Uses a [BeaconRangingPeriodicSamplingConfiguration] for configuration the +/// [beaconRegions] to include and the [beaconDistance]. class BeaconProbe extends StreamProbe { - /// Default timeout for bluetooth scan - 4 secs - static const DEFAULT_TIMEOUT = 4 * 1000; - Data? _data; - - List get beaconRegions => (samplingConfiguration is BeaconRangingPeriodicSamplingConfiguration) - ? (samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration).beaconRegions - : []; + @override + BeaconRangingPeriodicSamplingConfiguration? get samplingConfiguration => + super.samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration; - int get beaconDistance => (samplingConfiguration is BeaconRangingPeriodicSamplingConfiguration) - ? (samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration).beaconDistance - : 2; + List get beaconRegions => + samplingConfiguration?.beaconRegions + .map((region) => region.toRegion()) + .toList() ?? + []; - StreamSubscription? _streamMonitoring; - StreamSubscription? _streamRanging; + int get beaconDistance => samplingConfiguration?.beaconDistance ?? 2; - Future _startMonitoring() async { - info('start monitoring & initializing scanning.'); - try { - await flutterBeacon.initializeScanning; - } catch (e) { - warning('error happened while initializing scanner $e'); + @override + bool onInitialize() { + super.onInitialize(); + if (beaconRegions.isEmpty) { + warning( + '$runtimeType - No beacon regions specified for monitoring. Will not start monitoring.'); + return false; } - info('initialized scanner'); - - List regions = - beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); try { - _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { - if (result.monitoringState == MonitoringState.inside) { - info('🚪 Entered region: ${result.region.identifier}'); - _startRanging(result.region); - } else if (result.monitoringState == MonitoringState.outside) { - info('Not in region: ${result.region.identifier}'); - _stopMonitoring(); - } + info('$runtimeType - Initializing iBeacon scanning.'); + flutterBeacon.initializeScanning.then((_) { + info('$runtimeType - Initialized.'); + return true; + }, onError: (Object error) { + warning('$runtimeType - Error while initializing scanner - $error'); + return false; }); - } catch (e) { - info('Error starting monitoring: $e'); + } catch (error) { + warning('$runtimeType - Error while initializing scanner - $error'); + return false; } - } - - void _startRanging(Region region) { - _streamRanging = flutterBeacon.ranging([region]).listen( - (RangingResult result) { - final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); - - for (var beacon in closeBeacons) { - info('✅ beacon in range: ${beacon.proximityUUID}, ${beacon.accuracy} m'); - (_data as BeaconData).addBluetoothDevicesFromRangingResults( - BeaconDevice( - rssi: beacon.rssi, - major: beacon.major, - minor: beacon.minor, - accuracy: beacon.accuracy, - ), - ); - } - }, - ); - } - - void _stopMonitoring() { - _streamRanging?.cancel(); - _streamRanging = null; - _streamMonitoring?.cancel(); - _streamMonitoring = null; - } - - @override - Future onStart() async { - _data = BeaconData(); - - _startMonitoring(); - - return super.onStart(); + return true; } @override - Stream get stream => flutterBeacon.ranging(beaconRegions.map((e) => e!.toRegion()).toList()).map( + Stream get stream => flutterBeacon.ranging(beaconRegions).map( (RangingResult result) { - final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); - if (closeBeacons.isNotEmpty) { - return Measurement.fromData(BeaconData() - ..startScan = DateTime.now() - ..scanResult = closeBeacons - .map((beacon) => BeaconDevice( - rssi: beacon.rssi, - uuid: beacon.proximityUUID, - major: beacon.major, - minor: beacon.minor, - accuracy: beacon.accuracy, - )) - .toList()); - } - return Measurement.fromData(BeaconData()); + final closeBeacons = result.beacons + .where((beacon) => beacon.accuracy <= beaconDistance); + + // If no beacons are close, still return an measurement with an empty + // list of iBeacons for this region. + return Measurement.fromData( + BeaconData(region: result.region.identifier) + ..scanResult = closeBeacons + .map((beacon) => BeaconDevice( + uuid: beacon.proximityUUID, + rssi: beacon.rssi, + major: beacon.major, + minor: beacon.minor, + accuracy: beacon.accuracy, + proximity: beacon.proximity, + )) + .toList()); }, ); } diff --git a/packages/carp_connectivity_package/pubspec.yaml b/packages/carp_connectivity_package/pubspec.yaml index fbd305424..797eb29b5 100644 --- a/packages/carp_connectivity_package/pubspec.yaml +++ b/packages/carp_connectivity_package/pubspec.yaml @@ -1,6 +1,6 @@ name: carp_connectivity_package description: CARP connectivity sampling package. Samples connectivity status, bluetooth devices, and wifi access points. -version: 1.8.1+1 +version: 1.9.0 homepage: https://github.com/cph-cachet/carp.sensing-flutter/tree/master/packages/carp_connectivity_package environment: From d871deabfe9780ba9e3b557a309e64aacf973fc3 Mon Sep 17 00:00:00 2001 From: bardram Date: Fri, 25 Jul 2025 12:36:41 +0200 Subject: [PATCH 25/30] Update of unit test to include beacon measure in test protocol --- .../example/lib/example.dart | 2 +- .../lib/connectivity_package.dart | 2 + .../test/carp_connectivity_package_test.dart | 21 ++++++++++ .../test/json/study_protocol.json | 42 ++++++++++++++++++- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/carp_connectivity_package/example/lib/example.dart b/packages/carp_connectivity_package/example/lib/example.dart index 12e87af51..3b157a02f 100644 --- a/packages/carp_connectivity_package/example/lib/example.dart +++ b/packages/carp_connectivity_package/example/lib/example.dart @@ -63,7 +63,7 @@ void main() async { Measure( type: ConnectivitySamplingPackage.BEACON, samplingConfiguration: BeaconRangingPeriodicSamplingConfiguration( - beaconDistance: 2, // 2 meters + beaconDistance: 2, beaconRegions: [ BeaconRegion( identifier: 'region1', diff --git a/packages/carp_connectivity_package/lib/connectivity_package.dart b/packages/carp_connectivity_package/lib/connectivity_package.dart index 7ccff0cc9..5792b5f74 100644 --- a/packages/carp_connectivity_package/lib/connectivity_package.dart +++ b/packages/carp_connectivity_package/lib/connectivity_package.dart @@ -98,6 +98,8 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { interval: const Duration(minutes: 10), duration: const Duration(seconds: 10), ), + BeaconRangingPeriodicSamplingConfiguration( + beaconDistance: 2, beaconRegions: []) ]); // registering default privacy functions diff --git a/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart b/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart index 7805a0f36..61011888e 100644 --- a/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart +++ b/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart @@ -68,6 +68,27 @@ void main() { )) ]), phone); + + protocol.addTaskControl( + ImmediateTrigger(), + BackgroundTask(measures: [ + Measure( + type: ConnectivitySamplingPackage.BEACON, + samplingConfiguration: BeaconRangingPeriodicSamplingConfiguration( + beaconDistance: 2, + beaconRegions: [ + BeaconRegion( + identifier: 'region1', + uuid: '12345678-1234-1234-1234-123456789012', + ), + BeaconRegion( + identifier: 'region2', + uuid: '12345678-1234-1234-1234-123456789012', + ), + ], + )) + ]), + phone); }); test('CAMSStudyProtocol -> JSON', () async { diff --git a/packages/carp_connectivity_package/test/json/study_protocol.json b/packages/carp_connectivity_package/test/json/study_protocol.json index 2b321b4f4..b52883f38 100644 --- a/packages/carp_connectivity_package/test/json/study_protocol.json +++ b/packages/carp_connectivity_package/test/json/study_protocol.json @@ -1,6 +1,6 @@ { - "id": "62c13ff1-cffd-4b18-bc04-58d7b882dd77", - "createdOn": "2025-06-03T06:38:28.601596Z", + "id": "aec1d280-eb33-4694-a83c-8e356cca3292", + "createdOn": "2025-07-25T10:34:06.769594Z", "version": 0, "ownerId": "alex@uni.dk", "name": "Connectivity package test", @@ -81,6 +81,10 @@ { "__type": "dk.cachet.carp.common.application.tasks.Measure.DataStream", "type": "dk.cachet.carp.wifi" + }, + { + "__type": "dk.cachet.carp.common.application.tasks.Measure.DataStream", + "type": "dk.cachet.carp.beacon" } ] }, @@ -121,6 +125,30 @@ } } ] + }, + { + "__type": "dk.cachet.carp.common.application.tasks.BackgroundTask", + "name": "Task #10", + "measures": [ + { + "__type": "dk.cachet.carp.common.application.tasks.Measure.DataStream", + "type": "dk.cachet.carp.beacon", + "overrideSamplingConfiguration": { + "__type": "dk.cachet.carp.common.application.sampling.BeaconRangingPeriodicSamplingConfiguration", + "beaconRegions": [ + { + "identifier": "region1", + "uuid": "12345678-1234-1234-1234-123456789012" + }, + { + "identifier": "region2", + "uuid": "12345678-1234-1234-1234-123456789012" + } + ], + "beaconDistance": 2 + } + } + ] } ], "triggers": { @@ -135,6 +163,10 @@ "2": { "__type": "dk.cachet.carp.common.application.triggers.ImmediateTrigger", "sourceDeviceRoleName": "Primary Phone" + }, + "3": { + "__type": "dk.cachet.carp.common.application.triggers.ImmediateTrigger", + "sourceDeviceRoleName": "Primary Phone" } }, "taskControls": [ @@ -155,6 +187,12 @@ "taskName": "Task #9", "destinationDeviceRoleName": "Primary Phone", "control": "Start" + }, + { + "triggerId": 3, + "taskName": "Task #10", + "destinationDeviceRoleName": "Primary Phone", + "control": "Start" } ], "expectedParticipantData": [] From 9b2f56187cc4c316dd0b633e787a81156054a970 Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Fri, 1 Aug 2025 10:49:33 +0200 Subject: [PATCH 26/30] updated the stream probe --- .../lib/connectivity_probes.dart | 72 +++++++++---------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index a4c28630f..871a9976d 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -13,18 +13,16 @@ class ConnectivityProbe extends StreamProbe { @override Future onStart() async { // collect the current connectivity status on sampling start - var connectivityStatus = - await connectivity.Connectivity().checkConnectivity(); - addMeasurement(Measurement.fromData( - Connectivity.fromConnectivityResult(connectivityStatus))); + var connectivityStatus = await connectivity.Connectivity().checkConnectivity(); + addMeasurement(Measurement.fromData(Connectivity.fromConnectivityResult(connectivityStatus))); return super.onStart(); } @override - Stream get stream => - connectivity.Connectivity().onConnectivityChanged.map((event) => - Measurement.fromData(Connectivity.fromConnectivityResult(event))); + Stream get stream => connectivity.Connectivity() + .onConnectivityChanged + .map((event) => Measurement.fromData(Connectivity.fromConnectivityResult(event))); } // This probe requests access to location permissions (both on Android and iOS). @@ -69,24 +67,17 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { Stream get bufferingStream => FlutterBluePlus.scanResults; @override - Future getMeasurement() async => - _data != null ? Measurement.fromData(_data!) : null; + Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; // if a BT-specific sampling configuration is used, we need to // extract the services and remoteIds from it so FlutterBluePlus can // perform filtered scanning - List get services => (samplingConfiguration - is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) - .withServices - .map((e) => Guid(e)) - .toList() + List get services => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withServices.map((e) => Guid(e)).toList() : []; - List get remoteIds => (samplingConfiguration - is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) - .withRemoteIds + List get remoteIds => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withRemoteIds : []; @override @@ -97,8 +88,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { FlutterBluePlus.startScan( withServices: services, withRemoteIds: remoteIds, - timeout: samplingConfiguration?.duration ?? - const Duration(milliseconds: DEFAULT_TIMEOUT), + timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT), ); } catch (error) { FlutterBluePlus.stopScan(); @@ -132,10 +122,7 @@ class BeaconProbe extends StreamProbe { super.samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration; List get beaconRegions => - samplingConfiguration?.beaconRegions - .map((region) => region.toRegion()) - .toList() ?? - []; + samplingConfiguration?.beaconRegions.map((region) => region.toRegion()).toList() ?? []; int get beaconDistance => samplingConfiguration?.beaconDistance ?? 2; @@ -143,8 +130,7 @@ class BeaconProbe extends StreamProbe { bool onInitialize() { super.onInitialize(); if (beaconRegions.isEmpty) { - warning( - '$runtimeType - No beacon regions specified for monitoring. Will not start monitoring.'); + warning('$runtimeType - No beacon regions specified for monitoring. Will not start monitoring.'); return false; } @@ -165,15 +151,17 @@ class BeaconProbe extends StreamProbe { } @override - Stream get stream => flutterBeacon.ranging(beaconRegions).map( - (RangingResult result) { - final closeBeacons = result.beacons - .where((beacon) => beacon.accuracy <= beaconDistance); - - // If no beacons are close, still return an measurement with an empty - // list of iBeacons for this region. - return Measurement.fromData( - BeaconData(region: result.region.identifier) + Stream get stream async* { + await for (final monitoringResult in flutterBeacon.monitoring(beaconRegions)) { + if (monitoringResult.monitoringState == MonitoringState.inside) { + info('$runtimeType - Entered region: ${monitoringResult.region.identifier}'); + + yield* flutterBeacon.ranging(beaconRegions).map( + (rangingResult) { + final closeBeacons = rangingResult.beacons.where((beacon) => beacon.accuracy <= beaconDistance); + + return Measurement.fromData( + BeaconData(region: rangingResult.region.identifier) ..scanResult = closeBeacons .map((beacon) => BeaconDevice( uuid: beacon.proximityUUID, @@ -183,7 +171,13 @@ class BeaconProbe extends StreamProbe { accuracy: beacon.accuracy, proximity: beacon.proximity, )) - .toList()); - }, - ); + .toList(), + ); + }, + ); + } else if (monitoringResult.monitoringState == MonitoringState.outside) { + info('$runtimeType - Exited region: ${monitoringResult.region.identifier}'); + } + } + } } From d2fe959015ce7a088c227c3e6a795d7cd702873d Mon Sep 17 00:00:00 2001 From: bardram Date: Thu, 7 Aug 2025 11:59:52 +0200 Subject: [PATCH 27/30] Added a BeaconData.fromRegionAndBeacons constructor to pretty syntax --- .../lib/connectivity_data.dart | 35 +++++++-- .../lib/connectivity_probes.dart | 74 +++++++++++-------- 2 files changed, 69 insertions(+), 40 deletions(-) diff --git a/packages/carp_connectivity_package/lib/connectivity_data.dart b/packages/carp_connectivity_package/lib/connectivity_data.dart index 244ebfb4a..d53bbd12a 100644 --- a/packages/carp_connectivity_package/lib/connectivity_data.dart +++ b/packages/carp_connectivity_package/lib/connectivity_data.dart @@ -246,8 +246,26 @@ class BeaconData extends Data { set scanResult(List devices) => _scanResult .addEntries(devices.map((device) => MapEntry(device.uuid, device))); + /// Creates a [BeaconData] instance with the specified region. BeaconData({required this.region}) : super(); + /// Creates a [BeaconData] instance from a region and a list of beacons. + BeaconData.fromRegionAndBeacons({ + required this.region, + required List beacons, + }) : super() { + scanResult = beacons + .map((beacon) => BeaconDevice( + uuid: beacon.proximityUUID, + rssi: beacon.rssi, + major: beacon.major, + minor: beacon.minor, + accuracy: beacon.accuracy, + proximity: beacon.proximity, + )) + .toList(); + } + void addBeaconDevice(BeaconDevice device) => _scanResult[device.uuid] = device; @@ -299,14 +317,15 @@ class BeaconDevice { this.proximity, }) : super(); - factory BeaconDevice.fromRegionAndBeacon(Beacon beacon) => BeaconDevice( - rssi: beacon.rssi, - uuid: beacon.proximityUUID, - major: beacon.major, - minor: beacon.minor, - accuracy: beacon.accuracy, - proximity: beacon.proximity, - ); + BeaconDevice.fromRegionAndBeacon(Beacon beacon) + : this( + rssi: beacon.rssi, + uuid: beacon.proximityUUID, + major: beacon.major, + minor: beacon.minor, + accuracy: beacon.accuracy, + proximity: beacon.proximity, + ); factory BeaconDevice.fromJson(Map json) => _$BeaconDeviceFromJson(json); diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 871a9976d..e4b09c6c9 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -13,16 +13,18 @@ class ConnectivityProbe extends StreamProbe { @override Future onStart() async { // collect the current connectivity status on sampling start - var connectivityStatus = await connectivity.Connectivity().checkConnectivity(); - addMeasurement(Measurement.fromData(Connectivity.fromConnectivityResult(connectivityStatus))); + var connectivityStatus = + await connectivity.Connectivity().checkConnectivity(); + addMeasurement(Measurement.fromData( + Connectivity.fromConnectivityResult(connectivityStatus))); return super.onStart(); } @override - Stream get stream => connectivity.Connectivity() - .onConnectivityChanged - .map((event) => Measurement.fromData(Connectivity.fromConnectivityResult(event))); + Stream get stream => + connectivity.Connectivity().onConnectivityChanged.map((event) => + Measurement.fromData(Connectivity.fromConnectivityResult(event))); } // This probe requests access to location permissions (both on Android and iOS). @@ -67,17 +69,24 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { Stream get bufferingStream => FlutterBluePlus.scanResults; @override - Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; + Future getMeasurement() async => + _data != null ? Measurement.fromData(_data!) : null; // if a BT-specific sampling configuration is used, we need to // extract the services and remoteIds from it so FlutterBluePlus can // perform filtered scanning - List get services => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withServices.map((e) => Guid(e)).toList() + List get services => (samplingConfiguration + is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) + .withServices + .map((e) => Guid(e)) + .toList() : []; - List get remoteIds => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withRemoteIds + List get remoteIds => (samplingConfiguration + is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) + .withRemoteIds : []; @override @@ -88,7 +97,8 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { FlutterBluePlus.startScan( withServices: services, withRemoteIds: remoteIds, - timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT), + timeout: samplingConfiguration?.duration ?? + const Duration(milliseconds: DEFAULT_TIMEOUT), ); } catch (error) { FlutterBluePlus.stopScan(); @@ -122,7 +132,10 @@ class BeaconProbe extends StreamProbe { super.samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration; List get beaconRegions => - samplingConfiguration?.beaconRegions.map((region) => region.toRegion()).toList() ?? []; + samplingConfiguration?.beaconRegions + .map((region) => region.toRegion()) + .toList() ?? + []; int get beaconDistance => samplingConfiguration?.beaconDistance ?? 2; @@ -130,12 +143,13 @@ class BeaconProbe extends StreamProbe { bool onInitialize() { super.onInitialize(); if (beaconRegions.isEmpty) { - warning('$runtimeType - No beacon regions specified for monitoring. Will not start monitoring.'); + warning( + '$runtimeType - No beacon regions specified for monitoring. Will not start monitoring.'); return false; } try { - info('$runtimeType - Initializing iBeacon scanning.'); + info('$runtimeType - Initializing iBeacon scanning...'); flutterBeacon.initializeScanning.then((_) { info('$runtimeType - Initialized.'); return true; @@ -152,31 +166,27 @@ class BeaconProbe extends StreamProbe { @override Stream get stream async* { - await for (final monitoringResult in flutterBeacon.monitoring(beaconRegions)) { + await for (final monitoringResult + in flutterBeacon.monitoring(beaconRegions)) { if (monitoringResult.monitoringState == MonitoringState.inside) { - info('$runtimeType - Entered region: ${monitoringResult.region.identifier}'); + debug( + '$runtimeType - Entered region: ${monitoringResult.region.identifier}'); yield* flutterBeacon.ranging(beaconRegions).map( (rangingResult) { - final closeBeacons = rangingResult.beacons.where((beacon) => beacon.accuracy <= beaconDistance); - - return Measurement.fromData( - BeaconData(region: rangingResult.region.identifier) - ..scanResult = closeBeacons - .map((beacon) => BeaconDevice( - uuid: beacon.proximityUUID, - rssi: beacon.rssi, - major: beacon.major, - minor: beacon.minor, - accuracy: beacon.accuracy, - proximity: beacon.proximity, - )) - .toList(), - ); + final closeBeacons = rangingResult.beacons + .where((beacon) => beacon.accuracy <= beaconDistance) + .toList(); + + return Measurement.fromData(BeaconData.fromRegionAndBeacons( + region: rangingResult.region.identifier, + beacons: closeBeacons, + )); }, ); } else if (monitoringResult.monitoringState == MonitoringState.outside) { - info('$runtimeType - Exited region: ${monitoringResult.region.identifier}'); + debug( + '$runtimeType - Exited region: ${monitoringResult.region.identifier}'); } } } From 37df2e85b9657c650a9f2e030ad2cf2ac38aa27c Mon Sep 17 00:00:00 2001 From: bardram Date: Thu, 7 Aug 2025 12:18:16 +0200 Subject: [PATCH 28/30] update of example and docs --- packages/carp_connectivity_package/README.md | 19 +++++++++++++------ .../example/lib/example.dart | 8 ++++---- .../carp_connectivity_package/pubspec.yaml | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/carp_connectivity_package/README.md b/packages/carp_connectivity_package/README.md index 30f64b8f1..b5f9fac7d 100644 --- a/packages/carp_connectivity_package/README.md +++ b/packages/carp_connectivity_package/README.md @@ -84,7 +84,8 @@ To enable bluetooth tracking, add these permissions in the `Info.plist` file loc ```` -> Note that on iOS, it is [impossible to do a general Bluetooth scan when the screen is off or the app is in background](https://developer.apple.com/forums/thread/652592). This will simply result in an empty scan. Hence, bluetooth devices are only collected when the app is in the foreground. +> [!NOTE] +> On iOS, it is [impossible to do a general Bluetooth scan when the screen is off or the app is in background](https://developer.apple.com/forums/thread/652592). This will simply result in an empty scan. Hence, bluetooth devices are only collected when the app is in the foreground. To collect iBeacon measurements, please follow the setup described in the [dchs_flutter_beacon](https://pub.dev/packages/dchs_flutter_beacon) plugin. Especially, for iOS you need permissions to access location information in the `Info.plist` file: @@ -134,13 +135,13 @@ Smartphone phone = Smartphone(); protocol.addPrimaryDevice(phone); // Add an automatic task that immediately starts collecting connectivity, -// nearby bluetooth devices, and wifi information. +// wifi information, and nearby bluetooth devices. protocol.addTaskControl( ImmediateTrigger(), BackgroundTask(measures: [ Measure(type: ConnectivitySamplingPackage.CONNECTIVITY), - Measure(type: ConnectivitySamplingPackage.BLUETOOTH), Measure(type: ConnectivitySamplingPackage.WIFI), + Measure(type: ConnectivitySamplingPackage.BLUETOOTH), ]), phone); ``` @@ -154,8 +155,8 @@ protocol.addTaskControl( Measure( type: ConnectivitySamplingPackage.BLUETOOTH, samplingConfiguration: BluetoothScanPeriodicSamplingConfiguration( - interval: const Duration(minutes: 10), - duration: const Duration(seconds: 10), + interval: const Duration(minutes: 20), + duration: const Duration(seconds: 15), withRemoteIds: ['123', '456'], withServices: ['service1', 'service2'], )) @@ -163,7 +164,13 @@ protocol.addTaskControl( phone); ``` -If you want to collect iBeacon measurements, you can use a [BeaconRangingPeriodicSamplingConfiguration] to configure the scan. The following example will scan for iBeacons in the specified regions which are closer than 2 meters. The regions are specified by their identifier and UUID. See the [dchs_flutter_beacon](https://pub.dev/packages/dchs_flutter_beacon) plugin for more information on how to set up iBeacon regions. +The default configuration scans every 10 minutes for 10 seconds, and does not specify any remote IDs or services. + +If you want to collect iBeacon measurements, you need to configure the scanning by setting a [`BeaconRangingPeriodicSamplingConfiguration`](https://pub.dev/documentation/carp_connectivity_package/latest/connectivity/BeaconRangingPeriodicSamplingConfiguration-class.html). +The following example will scan for iBeacons in the specified regions which are closer than 2 meters. The regions are specified by their identifier and UUID. See the [dchs_flutter_beacon](https://pub.dev/packages/dchs_flutter_beacon) plugin for more information on how to set up iBeacon regions. + +> [!NOTE] +> There is no default sampling configuration for iBeacons. You need to specify at least one region to scan for. ```dart protocol.addTaskControl( diff --git a/packages/carp_connectivity_package/example/lib/example.dart b/packages/carp_connectivity_package/example/lib/example.dart index 3b157a02f..133541c4b 100644 --- a/packages/carp_connectivity_package/example/lib/example.dart +++ b/packages/carp_connectivity_package/example/lib/example.dart @@ -22,13 +22,13 @@ void main() async { protocol.addPrimaryDevice(phone); // Add an automatic task that immediately starts collecting connectivity, - // nearby bluetooth devices, and wifi information. + // wifi information, and nearby bluetooth devices. protocol.addTaskControl( ImmediateTrigger(), BackgroundTask(measures: [ Measure(type: ConnectivitySamplingPackage.CONNECTIVITY), - Measure(type: ConnectivitySamplingPackage.BLUETOOTH), Measure(type: ConnectivitySamplingPackage.WIFI), + Measure(type: ConnectivitySamplingPackage.BLUETOOTH), ]), phone); @@ -42,8 +42,8 @@ void main() async { Measure( type: ConnectivitySamplingPackage.BLUETOOTH, samplingConfiguration: BluetoothScanPeriodicSamplingConfiguration( - interval: const Duration(minutes: 10), - duration: const Duration(seconds: 10), + interval: const Duration(minutes: 20), + duration: const Duration(seconds: 15), withRemoteIds: ['123', '456'], withServices: ['service1', 'service2'], )) diff --git a/packages/carp_connectivity_package/pubspec.yaml b/packages/carp_connectivity_package/pubspec.yaml index 797eb29b5..f3577bad7 100644 --- a/packages/carp_connectivity_package/pubspec.yaml +++ b/packages/carp_connectivity_package/pubspec.yaml @@ -20,9 +20,9 @@ dependencies: connectivity_plus: ^6.0.0 # connectivity events network_info_plus: ^6.0.0 # wifi ssid name flutter_blue_plus: ^1.35.0 # bluetooth scan + dchs_flutter_beacon: ^0.6.4 # collecting nearby iBeacon devices crypto: ^3.0.0 # hashing sensitive data permission_handler: '>=11.0.0 <13.0.0' - dchs_flutter_beacon: ^0.6.4 # Overriding carp libraries to use the local copy From be62b49b03d2e02ebe07ae5da2b015a306ba754a Mon Sep 17 00:00:00 2001 From: Yarno Van de Weyer Date: Thu, 14 Aug 2025 10:19:09 +0200 Subject: [PATCH 29/30] added smll test --- .../test/carp_connectivity_package_test.dart | 152 ++++++++++-------- 1 file changed, 82 insertions(+), 70 deletions(-) diff --git a/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart b/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart index 61011888e..36fc34df5 100644 --- a/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart +++ b/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:carp_connectivity_package/connectivity.dart'; +import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart'; import 'package:test/test.dart'; import 'package:carp_serializable/carp_serializable.dart'; @@ -11,85 +12,84 @@ void main() { late StudyProtocol protocol; Smartphone phone; - setUp(() { - // Initialization of serialization - CarpMobileSensing(); + setUp( + () { + // Initialization of serialization + CarpMobileSensing(); - // register the context sampling package - SamplingPackageRegistry().register(ConnectivitySamplingPackage()); + // register the context sampling package + SamplingPackageRegistry().register(ConnectivitySamplingPackage()); - // Create a new study protocol. - protocol = StudyProtocol( - ownerId: 'alex@uni.dk', - name: 'Connectivity package test', - ); - - // Define which devices are used for data collection. - phone = Smartphone(); + // Create a new study protocol. + protocol = StudyProtocol( + ownerId: 'alex@uni.dk', + name: 'Connectivity package test', + ); - protocol.addPrimaryDevice(phone); + // Define which devices are used for data collection. + phone = Smartphone(); - // adding all available measures to one one trigger and one task - protocol.addTaskControl( - ImmediateTrigger(), - BackgroundTask() - ..measures = SamplingPackageRegistry() - .dataTypes - .map((type) => Measure(type: type.type)) - .toList(), - phone, - ); + protocol.addPrimaryDevice(phone); - // also add a PeriodicSamplingConfiguration - protocol.addTaskControl( + // adding all available measures to one one trigger and one task + protocol.addTaskControl( + ImmediateTrigger(), + BackgroundTask() + ..measures = SamplingPackageRegistry().dataTypes.map((type) => Measure(type: type.type)).toList(), + phone, + ); + + // also add a PeriodicSamplingConfiguration + protocol.addTaskControl( + ImmediateTrigger(), + BackgroundTask( + measures: [ + Measure(type: ConnectivitySamplingPackage.BLUETOOTH) + ..overrideSamplingConfiguration = PeriodicSamplingConfiguration( + interval: const Duration(minutes: 10), + duration: const Duration(seconds: 10), + ), + ], + ), + phone); + + // also add a BluetoothScanPeriodicSamplingConfiguration + protocol.addTaskControl( + ImmediateTrigger(), + BackgroundTask(measures: [ + Measure( + type: ConnectivitySamplingPackage.BLUETOOTH, + samplingConfiguration: BluetoothScanPeriodicSamplingConfiguration( + interval: const Duration(minutes: 10), + duration: const Duration(seconds: 10), + withRemoteIds: ['123', '456'], + withServices: ['service1', 'service2'], + )) + ]), + phone); + + protocol.addTaskControl( ImmediateTrigger(), BackgroundTask( measures: [ - Measure(type: ConnectivitySamplingPackage.BLUETOOTH) - ..overrideSamplingConfiguration = PeriodicSamplingConfiguration( - interval: const Duration(minutes: 10), - duration: const Duration(seconds: 10), - ), - ], - ), - phone); - - // also add a BluetoothScanPeriodicSamplingConfiguration - protocol.addTaskControl( - ImmediateTrigger(), - BackgroundTask(measures: [ - Measure( - type: ConnectivitySamplingPackage.BLUETOOTH, - samplingConfiguration: BluetoothScanPeriodicSamplingConfiguration( - interval: const Duration(minutes: 10), - duration: const Duration(seconds: 10), - withRemoteIds: ['123', '456'], - withServices: ['service1', 'service2'], - )) - ]), - phone); - - protocol.addTaskControl( - ImmediateTrigger(), - BackgroundTask(measures: [ - Measure( + Measure( type: ConnectivitySamplingPackage.BEACON, samplingConfiguration: BeaconRangingPeriodicSamplingConfiguration( beaconDistance: 2, beaconRegions: [ BeaconRegion( - identifier: 'region1', - uuid: '12345678-1234-1234-1234-123456789012', - ), - BeaconRegion( - identifier: 'region2', - uuid: '12345678-1234-1234-1234-123456789012', + identifier: 'TestB1', + uuid: 'fda50693-a4e2-4fb1-afcf-c6eb07647825', ), ], - )) - ]), - phone); - }); + ), + ), + ], + ), + phone, + ); + }, + ); test('CAMSStudyProtocol -> JSON', () async { print(protocol); @@ -101,8 +101,7 @@ void main() { print('#1 : $protocol'); final studyJson = toJsonString(protocol); - StudyProtocol protocolFromJson = - StudyProtocol.fromJson(json.decode(studyJson) as Map); + StudyProtocol protocolFromJson = StudyProtocol.fromJson(json.decode(studyJson) as Map); expect(toJsonString(protocolFromJson), equals(studyJson)); print('#2 : $protocolFromJson'); }); @@ -111,8 +110,7 @@ void main() { // Read the study protocol from json file String plainJson = File('test/json/study_protocol.json').readAsStringSync(); - StudyProtocol protocol = - StudyProtocol.fromJson(json.decode(plainJson) as Map); + StudyProtocol protocol = StudyProtocol.fromJson(json.decode(plainJson) as Map); expect(protocol.ownerId, 'alex@uni.dk'); expect(protocol.primaryDevice.roleName, Smartphone.DEFAULT_ROLE_NAME); @@ -134,9 +132,23 @@ void main() { print(toJsonString(measurement)); }); + + test('Beacon -> JSON', () async { + BeaconData data = BeaconData(region: "TestB1")..addBeaconDevice( + BeaconDevice( + uuid: 'fda50693-a4e2-4fb1-afcf-c6eb07647825', + rssi: -60, + proximity: Proximity.near, + ), + ); + + final measurement = Measurement.fromData(data); + + print(toJsonString(measurement)); + }); + test('Connectivity -> JSON', () async { - Connectivity data = Connectivity() - ..connectivityStatus = [ConnectivityStatus.bluetooth]; + Connectivity data = Connectivity()..connectivityStatus = [ConnectivityStatus.bluetooth]; final measurement = Measurement.fromData(data); From acba94d7211eab30c783d33a8bf4c05b57f45c6d Mon Sep 17 00:00:00 2001 From: bardram Date: Fri, 5 Sep 2025 10:14:15 +0200 Subject: [PATCH 30/30] update to linter stuff --- carp_mobile_sensing/analysis_options.yaml | 4 +++ carp_mobile_sensing/example/lib/example.dart | 2 ++ .../example/lib/example.dart | 2 ++ .../test/carp_connectivity_package_test.dart | 36 +++++++++++-------- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/carp_mobile_sensing/analysis_options.yaml b/carp_mobile_sensing/analysis_options.yaml index 07e99cb85..e30fa1b76 100644 --- a/carp_mobile_sensing/analysis_options.yaml +++ b/carp_mobile_sensing/analysis_options.yaml @@ -4,6 +4,8 @@ include: package:flutter_lints/flutter.yaml analyzer: + errors: + deprecated_member_use_from_same_package: ignore exclude: [build/**] language: strict-casts: true @@ -17,3 +19,5 @@ linter: depend_on_referenced_packages: true avoid_print: false use_string_in_part_of_directives: true + deprecated_member_use_from_same_package: false + \ No newline at end of file diff --git a/carp_mobile_sensing/example/lib/example.dart b/carp_mobile_sensing/example/lib/example.dart index 35ad14b68..d4c4cb06c 100644 --- a/carp_mobile_sensing/example/lib/example.dart +++ b/carp_mobile_sensing/example/lib/example.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unused_local_variable + /* * Copyright 2018-2024 the Technical University of Denmark (DTU). * Use of this source code is governed by a MIT-style license that can be diff --git a/packages/carp_connectivity_package/example/lib/example.dart b/packages/carp_connectivity_package/example/lib/example.dart index 133541c4b..7f4e08fb2 100644 --- a/packages/carp_connectivity_package/example/lib/example.dart +++ b/packages/carp_connectivity_package/example/lib/example.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unused_local_variable + import 'package:carp_core/carp_core.dart'; import 'package:carp_mobile_sensing/carp_mobile_sensing.dart'; import 'package:carp_connectivity_package/connectivity.dart'; diff --git a/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart b/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart index 36fc34df5..1b850967e 100644 --- a/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart +++ b/packages/carp_connectivity_package/test/carp_connectivity_package_test.dart @@ -15,7 +15,7 @@ void main() { setUp( () { // Initialization of serialization - CarpMobileSensing(); + CarpMobileSensing.ensureInitialized(); // register the context sampling package SamplingPackageRegistry().register(ConnectivitySamplingPackage()); @@ -35,7 +35,10 @@ void main() { protocol.addTaskControl( ImmediateTrigger(), BackgroundTask() - ..measures = SamplingPackageRegistry().dataTypes.map((type) => Measure(type: type.type)).toList(), + ..measures = SamplingPackageRegistry() + .dataTypes + .map((type) => Measure(type: type.type)) + .toList(), phone, ); @@ -59,7 +62,8 @@ void main() { BackgroundTask(measures: [ Measure( type: ConnectivitySamplingPackage.BLUETOOTH, - samplingConfiguration: BluetoothScanPeriodicSamplingConfiguration( + samplingConfiguration: + BluetoothScanPeriodicSamplingConfiguration( interval: const Duration(minutes: 10), duration: const Duration(seconds: 10), withRemoteIds: ['123', '456'], @@ -101,7 +105,8 @@ void main() { print('#1 : $protocol'); final studyJson = toJsonString(protocol); - StudyProtocol protocolFromJson = StudyProtocol.fromJson(json.decode(studyJson) as Map); + StudyProtocol protocolFromJson = + StudyProtocol.fromJson(json.decode(studyJson) as Map); expect(toJsonString(protocolFromJson), equals(studyJson)); print('#2 : $protocolFromJson'); }); @@ -110,7 +115,8 @@ void main() { // Read the study protocol from json file String plainJson = File('test/json/study_protocol.json').readAsStringSync(); - StudyProtocol protocol = StudyProtocol.fromJson(json.decode(plainJson) as Map); + StudyProtocol protocol = + StudyProtocol.fromJson(json.decode(plainJson) as Map); expect(protocol.ownerId, 'alex@uni.dk'); expect(protocol.primaryDevice.roleName, Smartphone.DEFAULT_ROLE_NAME); @@ -134,21 +140,23 @@ void main() { }); test('Beacon -> JSON', () async { - BeaconData data = BeaconData(region: "TestB1")..addBeaconDevice( - BeaconDevice( - uuid: 'fda50693-a4e2-4fb1-afcf-c6eb07647825', - rssi: -60, - proximity: Proximity.near, - ), - ); - + BeaconData data = BeaconData(region: "TestB1") + ..addBeaconDevice( + BeaconDevice( + uuid: 'fda50693-a4e2-4fb1-afcf-c6eb07647825', + rssi: -60, + proximity: Proximity.near, + ), + ); + final measurement = Measurement.fromData(data); print(toJsonString(measurement)); }); test('Connectivity -> JSON', () async { - Connectivity data = Connectivity()..connectivityStatus = [ConnectivityStatus.bluetooth]; + Connectivity data = Connectivity() + ..connectivityStatus = [ConnectivityStatus.bluetooth]; final measurement = Measurement.fromData(data);