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/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..b5f9fac7d 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
@@ -81,7 +84,23 @@ 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:
+
+````xml
+
+NSLocationWhenInUseUsageDescription
+Reason why app needs location
+
+
+NSLocationAlwaysAndWhenInUseUsageDescription
+Reason why app needs location
+
+NSLocationAlwaysUsageDescription
+Reason why app needs location
+````
## Using it
@@ -116,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);
```
@@ -136,11 +155,42 @@ 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'],
))
]),
phone);
```
+
+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(
+ 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..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';
@@ -22,13 +24,59 @@ 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);
+
+ // 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: 20),
+ duration: const Duration(seconds: 15),
+ 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,
+ 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.dart b/packages/carp_connectivity_package/lib/connectivity.dart
index 87460d824..4095965b8 100644
--- a/packages/carp_connectivity_package/lib/connectivity.dart
+++ b/packages/carp_connectivity_package/lib/connectivity.dart
@@ -7,6 +7,7 @@ library;
import 'dart:async';
import 'dart:convert';
+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.g.dart b/packages/carp_connectivity_package/lib/connectivity.g.dart
index 8d9c25524..e9cdd6e45 100644
--- a/packages/carp_connectivity_package/lib/connectivity.g.dart
+++ b/packages/carp_connectivity_package/lib/connectivity.g.dart
@@ -84,6 +84,48 @@ Map _$WifiToJson(Wifi instance) => {
if (instance.ip case final value?) 'ip': value,
};
+BeaconData _$BeaconDataFromJson(Map json) => BeaconData(
+ region: json['region'] 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,
+ 'region': instance.region,
+ 'scanResult': instance.scanResult.map((e) => e.toJson()).toList(),
+ };
+
+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(),
+ proximity: $enumDecodeNullable(_$ProximityEnumMap, json['proximity']),
+ );
+
+Map _$BeaconDeviceToJson(BeaconDevice instance) =>
+ {
+ '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) =>
@@ -115,3 +157,37 @@ Map _$BluetoothScanPeriodicSamplingConfigurationToJson(
'withServices': instance.withServices,
'withRemoteIds': instance.withRemoteIds,
};
+
+BeaconRangingPeriodicSamplingConfiguration
+ _$BeaconRangingPeriodicSamplingConfigurationFromJson(
+ Map json) =>
+ BeaconRangingPeriodicSamplingConfiguration(
+ beaconRegions: (json['beaconRegions'] as List?)
+ ?.map((e) => BeaconRegion.fromJson(e as Map))
+ .toList() ??
+ const [],
+ beaconDistance: (json['beaconDistance'] as num?)?.toInt() ?? 2,
+ )..$type = json['__type'] as String?;
+
+Map _$BeaconRangingPeriodicSamplingConfigurationToJson(
+ BeaconRangingPeriodicSamplingConfiguration instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ '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 c36bf80d9..d53bbd12a 100644
--- a/packages/carp_connectivity_package/lib/connectivity_data.dart
+++ b/packages/carp_connectivity_package/lib/connectivity_data.dart
@@ -114,6 +114,13 @@ 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) =>
@@ -164,6 +171,16 @@ class BluetoothDevice {
rssi: result.rssi,
);
+ 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);
@@ -211,3 +228,111 @@ class Wifi extends Data {
String toString() =>
'${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip';
}
+
+/// A [Data] holding information of nearby Beacon devices.
+@JsonSerializable(includeIfNull: false, explicitToJson: true)
+class BeaconData extends Data {
+ static const dataType = ConnectivitySamplingPackage.BEACON;
+
+ /// The unique identifier of the region that this beacon belongs to.
+ String region;
+
+ /// 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)));
+
+ /// 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;
+
+ 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);
+ @override
+ Map toJson() => _$BeaconDataToJson(this);
+
+ @override
+ String toString() => '${super.toString()}, scanResult: $scanResult';
+}
+
+/// Beacon device data.
+@JsonSerializable(includeIfNull: false, explicitToJson: true)
+class BeaconDevice {
+ /// The proximity UUID of beacon.
+ String uuid;
+
+ /// The RSSI signal strength to the device.
+ int rssi;
+
+ /// Major value (for iBeacon).
+ int? major;
+
+ /// Minor value (for iBeacon).
+ int? minor;
+
+ /// 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,
+ this.accuracy,
+ this.proximity,
+ }) : super();
+
+ 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);
+ Map toJson() => _$BeaconDeviceToJson(this);
+
+ @override
+ 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 6ec45b2b9..5792b5f74 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).
@@ -20,6 +20,15 @@ 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 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([
@@ -50,6 +59,15 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage {
IntervalSamplingConfiguration(
interval: const Duration(minutes: 10),
)),
+ DataTypeSamplingScheme(
+ CamsDataTypeMetaData(
+ type: BEACON,
+ displayName: "Ranging iBeacons",
+ timeType: DataTimeType.POINT,
+ permissions: [Permission.bluetoothScan, Permission.locationAlways],
+ ),
+ BeaconRangingPeriodicSamplingConfiguration(),
+ ),
]);
@override
@@ -61,6 +79,8 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage {
return BluetoothProbe();
case WIFI:
return WifiProbe();
+ case BEACON:
+ return BeaconProbe();
default:
return null;
}
@@ -73,10 +93,13 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage {
Connectivity(),
Bluetooth(),
Wifi(),
+ BeaconData(region: ''),
BluetoothScanPeriodicSamplingConfiguration(
interval: const Duration(minutes: 10),
duration: const Duration(seconds: 10),
),
+ BeaconRangingPeriodicSamplingConfiguration(
+ beaconDistance: 2, beaconRegions: [])
]);
// registering default privacy functions
@@ -126,3 +149,79 @@ class BluetoothScanPeriodicSamplingConfiguration
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 SamplingConfiguration {
+ /// List of beacon regions to monitor and range.
+ List beaconRegions;
+
+ /// The distance in meters to consider a beacon as "in range".
+ int beaconDistance;
+
+ BeaconRangingPeriodicSamplingConfiguration({
+ this.beaconRegions = const [],
+ this.beaconDistance = 2,
+ }) : super();
+
+ @override
+ 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
+ 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 0649113ba..e4b09c6c9 100644
--- a/packages/carp_connectivity_package/lib/connectivity_probes.dart
+++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart
@@ -75,7 +75,6 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe {
// 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)
@@ -96,10 +95,11 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe {
try {
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();
_data = Error(message: 'Error scanning for bluetooth - $error');
@@ -109,6 +109,7 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe {
@override
void onSamplingEnd() {
FlutterBluePlus.stopScan();
+
if (_data is Bluetooth) (_data as Bluetooth).endScan = DateTime.now();
}
@@ -119,3 +120,74 @@ 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 {
+ @override
+ BeaconRangingPeriodicSamplingConfiguration? get samplingConfiguration =>
+ super.samplingConfiguration as BeaconRangingPeriodicSamplingConfiguration;
+
+ List get beaconRegions =>
+ samplingConfiguration?.beaconRegions
+ .map((region) => region.toRegion())
+ .toList() ??
+ [];
+
+ int get beaconDistance => samplingConfiguration?.beaconDistance ?? 2;
+
+ @override
+ bool onInitialize() {
+ super.onInitialize();
+ if (beaconRegions.isEmpty) {
+ warning(
+ '$runtimeType - No beacon regions specified for monitoring. Will not start monitoring.');
+ return false;
+ }
+
+ try {
+ 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 (error) {
+ warning('$runtimeType - Error while initializing scanner - $error');
+ return false;
+ }
+ return true;
+ }
+
+ @override
+ Stream get stream async* {
+ await for (final monitoringResult
+ in flutterBeacon.monitoring(beaconRegions)) {
+ if (monitoringResult.monitoringState == MonitoringState.inside) {
+ debug(
+ '$runtimeType - Entered region: ${monitoringResult.region.identifier}');
+
+ yield* flutterBeacon.ranging(beaconRegions).map(
+ (rangingResult) {
+ 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) {
+ debug(
+ '$runtimeType - Exited region: ${monitoringResult.region.identifier}');
+ }
+ }
+ }
+}
diff --git a/packages/carp_connectivity_package/pubspec.yaml b/packages/carp_connectivity_package/pubspec.yaml
index 4e90cb066..f3577bad7 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:
@@ -20,8 +20,10 @@ 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'
+ permission_handler: '>=11.0.0 <13.0.0'
+
# Overriding carp libraries to use the local copy
# Remove this before release of package
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..1b850967e 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,64 +12,88 @@ void main() {
late StudyProtocol protocol;
Smartphone phone;
- setUp(() {
- // Initialization of serialization
- CarpMobileSensing();
-
- // 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();
-
- protocol.addPrimaryDevice(phone);
-
- // 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(
+ setUp(
+ () {
+ // Initialization of serialization
+ CarpMobileSensing.ensureInitialized();
+
+ // 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();
+
+ protocol.addPrimaryDevice(phone);
+
+ // 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),
+ Measure(
+ type: ConnectivitySamplingPackage.BEACON,
+ samplingConfiguration: BeaconRangingPeriodicSamplingConfiguration(
+ beaconDistance: 2,
+ beaconRegions: [
+ BeaconRegion(
+ identifier: 'TestB1',
+ uuid: 'fda50693-a4e2-4fb1-afcf-c6eb07647825',
+ ),
+ ],
),
+ ),
],
),
- 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);
- });
+ phone,
+ );
+ },
+ );
test('CAMSStudyProtocol -> JSON', () async {
print(protocol);
@@ -113,6 +138,22 @@ 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];
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": []
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