Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/carp_connectivity_package/lib/connectivity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
91 changes: 67 additions & 24 deletions packages/carp_connectivity_package/lib/connectivity_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,17 @@ class Connectivity extends Data {

Connectivity() : super();

Connectivity.fromConnectivityResult(
List<connectivity.ConnectivityResult> result)
: super() {
connectivityStatus = result
.map((connectivity.ConnectivityResult e) => _parseConnectivityStatus(e))
.toList();
Connectivity.fromConnectivityResult(List<connectivity.ConnectivityResult> result) : super() {
connectivityStatus = result.map((connectivity.ConnectivityResult e) => _parseConnectivityStatus(e)).toList();
}

@override
Function get fromJsonFunction => _$ConnectivityFromJson;
factory Connectivity.fromJson(Map<String, dynamic> json) =>
FromJsonFactory().fromJson<Connectivity>(json);
factory Connectivity.fromJson(Map<String, dynamic> json) => FromJsonFactory().fromJson<Connectivity>(json);
@override
Map<String, dynamic> toJson() => _$ConnectivityToJson(this);

static ConnectivityStatus _parseConnectivityStatus(
connectivity.ConnectivityResult result) {
static ConnectivityStatus _parseConnectivityStatus(connectivity.ConnectivityResult result) {
switch (result) {
case connectivity.ConnectivityResult.bluetooth:
return ConnectivityStatus.bluetooth;
Expand All @@ -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.
Expand All @@ -98,26 +91,31 @@ class Bluetooth extends Data {

/// The list of [BluetoothDevice] found in a scan.
List<BluetoothDevice> get scanResult => _scanResult.values.toList();
set scanResult(List<BluetoothDevice> devices) => _scanResult.addEntries(
devices.map((device) => MapEntry(device.bluetoothDeviceId, device)));
set scanResult(List<BluetoothDevice> 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<ScanResult> results) {
for (var scanResult in results) {
addBluetoothDevice(BluetoothDevice.fromScanResult(scanResult));
}
}

void addBluetoothDevicesFromRangingResults(
Beacon result,
String beaconName,
) {
addBluetoothDevice(BluetoothDevice.fromRangingResult(result, beaconName));
}

@override
Function get fromJsonFunction => _$BluetoothFromJson;
factory Bluetooth.fromJson(Map<String, dynamic> json) =>
FromJsonFactory().fromJson<Bluetooth>(json);
factory Bluetooth.fromJson(Map<String, dynamic> json) => FromJsonFactory().fromJson<Bluetooth>(json);
@override
Map<String, dynamic> toJson() => _$BluetoothToJson(this);

Expand Down Expand Up @@ -164,8 +162,16 @@ class BluetoothDevice {
rssi: result.rssi,
);

factory BluetoothDevice.fromJson(Map<String, dynamic> 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<String, dynamic> json) => _$BluetoothDeviceFromJson(json);
Map<String, dynamic> toJson() => _$BluetoothDeviceToJson(this);

@override
Expand Down Expand Up @@ -202,12 +208,49 @@ class Wifi extends Data {

@override
Function get fromJsonFunction => _$WifiFromJson;
factory Wifi.fromJson(Map<String, dynamic> json) =>
FromJsonFactory().fromJson<Wifi>(json);
factory Wifi.fromJson(Map<String, dynamic> json) => FromJsonFactory().fromJson<Wifi>(json);
@override
Map<String, dynamic> 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 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({
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';
}
41 changes: 23 additions & 18 deletions packages/carp_connectivity_package/lib/connectivity_package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -100,29 +95,39 @@ 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<String> withServices;

/// List of remote device IDs to filter the scan results.
List<String> 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<BeaconRegion?> 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
Map<String, dynamic> toJson() =>
_$BluetoothScanPeriodicSamplingConfigurationToJson(this);
Map<String, dynamic> toJson() => _$BluetoothScanPeriodicSamplingConfigurationToJson(this);
@override
Function get fromJsonFunction =>
_$BluetoothScanPeriodicSamplingConfigurationFromJson;
factory BluetoothScanPeriodicSamplingConfiguration.fromJson(
Map<String, dynamic> json) =>
FromJsonFactory()
.fromJson<BluetoothScanPeriodicSamplingConfiguration>(json);
Function get fromJsonFunction => _$BluetoothScanPeriodicSamplingConfigurationFromJson;
factory BluetoothScanPeriodicSamplingConfiguration.fromJson(Map<String, dynamic> json) =>
FromJsonFactory().fromJson<BluetoothScanPeriodicSamplingConfiguration>(json);
}
111 changes: 88 additions & 23 deletions packages/carp_connectivity_package/lib/connectivity_probes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ class ConnectivityProbe extends StreamProbe {
@override
Future<bool> 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<Measurement> get stream =>
connectivity.Connectivity().onConnectivityChanged.map((event) =>
Measurement.fromData(Connectivity.fromConnectivityResult(event)));
Stream<Measurement> get stream => connectivity.Connectivity()
.onConnectivityChanged
.map((event) => Measurement.fromData(Connectivity.fromConnectivityResult(event)));
}

// This probe requests access to location permissions (both on Android and iOS).
Expand Down Expand Up @@ -69,37 +67,50 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe {
Stream<dynamic> get bufferingStream => FlutterBluePlus.scanResults;

@override
Future<Measurement?> getMeasurement() async =>
_data != null ? Measurement.fromData(_data!) : null;
Future<Measurement?> 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<Guid> get services => (samplingConfiguration
is BluetoothScanPeriodicSamplingConfiguration)
? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration)
.withServices
.map((e) => Guid(e))
.toList()
List<Guid> get services => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration)
? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withServices.map((e) => Guid(e)).toList()
: [];

List<String> get remoteIds => (samplingConfiguration
is BluetoothScanPeriodicSamplingConfiguration)
? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration)
.withRemoteIds
List<String> get remoteIds => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration)
? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withRemoteIds
: [];

bool get useBeaconMonitoring => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration)
? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).useBeaconMonitoring
: false;

List<BeaconRegion?> get beaconRegions => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration)
? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconRegions
: [];

int get beaconDistance => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration)
? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconDistance
: 2;

StreamSubscription<MonitoringResult>? _streamMonitoring;
StreamSubscription<RangingResult>? _streamRanging;

@override
void onSamplingStart() {
_data = Bluetooth();

try {
FlutterBluePlus.startScan(
if (useBeaconMonitoring) {
info('Using beacon monitoring.');
_startMonitoring();
} else {
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();
_data = Error(message: 'Error scanning for bluetooth - $error');
Expand All @@ -108,7 +119,13 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe {

@override
void onSamplingEnd() {
FlutterBluePlus.stopScan();
if (useBeaconMonitoring) {
info('stopping monitoring');
_stopMonitoring();
} else {
FlutterBluePlus.stopScan();
}

if (_data is Bluetooth) (_data as Bluetooth).endScan = DateTime.now();
}

Expand All @@ -118,4 +135,52 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe {
(_data as Bluetooth).addBluetoothDevicesFromScanResults(event);
}
}

Future<void> _startMonitoring() async {
info('start monitoring & initializing scanning.');
try {
await flutterBeacon.initializeScanning;
} catch (e) {
warning('error happened while initializing scanner $e');
}
info('initialized scanner');

List<Region> 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;
}
}
Loading