diff --git a/lib/flutter_opendroneid.dart b/lib/flutter_opendroneid.dart index a9d6dff..94bf3c3 100644 --- a/lib/flutter_opendroneid.dart +++ b/lib/flutter_opendroneid.dart @@ -5,8 +5,8 @@ import 'package:dart_opendroneid/dart_opendroneid.dart'; import 'package:flutter/services.dart'; import 'package:flutter_opendroneid/exceptions/odid_message_parsing_exception.dart'; import 'package:flutter_opendroneid/models/dri_source_type.dart'; -import 'package:flutter_opendroneid/models/message_container.dart'; import 'package:flutter_opendroneid/models/permissions_missing_exception.dart'; +import 'package:flutter_opendroneid/models/received_odid_message.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; @@ -26,26 +26,19 @@ class FlutterOpenDroneId { static const _wifiStateEventChannel = const EventChannel('flutter_odid_state_wifi'); - static StreamSubscription? _bluetoothOdidDataSubscription; - static StreamSubscription? _wifiOdidDataSubscription; + static StreamSubscription? _receivedBluetoothMessagesSubscription; + static StreamSubscription? _receivedWiFiMessagesSubscription; - static final _wifiMessagesController = - StreamController.broadcast(); - static final _bluetoothMessagesController = - StreamController.broadcast(); + static final _receivedMessagesController = + StreamController.broadcast(); - static Map _storedPacks = {}; + static Stream get receivedMessages => + _receivedMessagesController.stream; static Stream get bluetoothState => _btStateEventChannel .receiveBroadcastStream() .map((event) => event as bool); - static Stream get bluetoothMessages => - _bluetoothMessagesController.stream; - - static Stream get wifiMessages => - _wifiMessagesController.stream; - static Stream get wifiState => _wifiStateEventChannel .receiveBroadcastStream() .map((event) => event as bool); @@ -59,32 +52,31 @@ class FlutterOpenDroneId { pigeon.WifiState.values.indexOf(pigeon.WifiState.Enabled); /// Starts scanning for nearby traffic - /// For Bluetooth scanning, bluetooth permissions are required on both platforms, - /// Android requires Bluetooth scan permission location permission on ver. < 12 + /// For Bluetooth scanning, bluetooth perm. are required on both platforms, + /// Android requires Bluetooth scan permission location permission on ver.< 12 /// - /// For Wi-Fi scanning, location permission is required on Android + /// For Wi-Fi scanning, location permission is required on Android. /// - /// Throws PermissionMissingException if permissions were not granted + /// Throws [PermissionsMissingException] if permissions were not granted. /// - /// To further receive data, listen to - /// streams. + /// To further receive data, listen to [receivedMessages] + /// stream. static Future startScan(DriSourceType sourceType) async { if (sourceType == DriSourceType.Bluetooth) { await _assertBluetoothPermissions(); - _bluetoothOdidDataSubscription?.cancel(); - _bluetoothOdidDataSubscription = bluetoothOdidPayloadEventChannel + _receivedBluetoothMessagesSubscription?.cancel(); + _receivedBluetoothMessagesSubscription = bluetoothOdidPayloadEventChannel .receiveBroadcastStream() - .listen((payload) => _updatePacks( - pigeon.ODIDPayload.decode(payload), DriSourceType.Bluetooth)); + .listen( + (payload) => _handlePayload(pigeon.ODIDPayload.decode(payload))); await _api.startScanBluetooth(); } else if (sourceType == DriSourceType.Wifi) { await _assertWifiPermissions(); - _wifiOdidDataSubscription?.cancel(); - - _wifiOdidDataSubscription = wifiOdidPayloadEventChannel + _receivedWiFiMessagesSubscription?.cancel(); + _receivedWiFiMessagesSubscription = wifiOdidPayloadEventChannel .receiveBroadcastStream() - .listen((payload) => _updatePacks( - pigeon.ODIDPayload.decode(payload), DriSourceType.Wifi)); + .listen( + (payload) => _handlePayload(pigeon.ODIDPayload.decode(payload))); await _api.startScanWifi(); } } @@ -94,11 +86,11 @@ class FlutterOpenDroneId { if (sourceType == DriSourceType.Bluetooth && (await _api.isScanningBluetooth())) { await _api.stopScanBluetooth(); - _bluetoothOdidDataSubscription?.cancel(); + _receivedBluetoothMessagesSubscription?.cancel(); } if (sourceType == DriSourceType.Wifi && await _api.isScanningWifi()) { await _api.stopScanWifi(); - _wifiOdidDataSubscription?.cancel(); + _receivedWiFiMessagesSubscription?.cancel(); } } @@ -120,15 +112,7 @@ class FlutterOpenDroneId { static Future get isScanningWifi async => await _api.isScanningWifi(); - static void _updatePacks( - pigeon.ODIDPayload payload, DriSourceType sourceType) { - final storedPack = _storedPacks[payload.macAddress] ?? - MessageContainer( - macAddress: payload.macAddress, - source: payload.source, - lastUpdate: - DateTime.fromMillisecondsSinceEpoch(payload.receivedTimestamp), - ); + static void _handlePayload(pigeon.ODIDPayload payload) { ODIDMessage? message; try { message = parseODIDMessage(payload.rawData); @@ -145,21 +129,16 @@ class FlutterOpenDroneId { if (message == null) return; - final updatedPack = storedPack.update( - message: message, - receivedTimestamp: payload.receivedTimestamp, - rssi: payload.rssi, - source: payload.source, + _receivedMessagesController.add( + ReceivedODIDMessage( + odidMessage: message, + macAddress: payload.macAddress, + source: payload.source, + rssi: payload.rssi, + receivedTimestamp: + DateTime.fromMillisecondsSinceEpoch(payload.receivedTimestamp), + ), ); - // update was refused if updatedPack is null - if (updatedPack != null) { - _storedPacks[payload.macAddress] = updatedPack; - return switch (sourceType) { - DriSourceType.Bluetooth => - _bluetoothMessagesController.add(updatedPack), - DriSourceType.Wifi => _wifiMessagesController.add(updatedPack), - }; - } } /// Checks all required Bluetooth permissions and throws diff --git a/lib/models/message_container.dart b/lib/models/message_container.dart deleted file mode 100644 index 17dd948..0000000 --- a/lib/models/message_container.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'package:dart_opendroneid/dart_opendroneid.dart'; -import 'package:flutter_opendroneid/extensions/compare_extension.dart'; -import 'package:flutter_opendroneid/models/constants.dart'; -import 'package:flutter_opendroneid/pigeon.dart' as pigeon; -import 'package:flutter_opendroneid/utils/conversions.dart'; - -/// The [MessageContainer] groups together messages of different types -/// from one device. It contains one instance of each message. The container is -/// then sent using stream to client of the library. -class MessageContainer { - final String macAddress; - final DateTime lastUpdate; - final pigeon.MessageSource source; - final int? lastMessageRssi; - - final Map? basicIdMessages; - final LocationMessage? locationMessage; - final OperatorIDMessage? operatorIdMessage; - final SelfIDMessage? selfIdMessage; - final AuthMessage? authenticationMessage; - final SystemMessage? systemDataMessage; - - MessageContainer({ - required this.macAddress, - required this.lastUpdate, - required this.source, - this.lastMessageRssi, - this.basicIdMessages, - this.locationMessage, - this.operatorIdMessage, - this.selfIdMessage, - this.authenticationMessage, - this.systemDataMessage, - }); - - @override - String toString() { - final singleMessages = [ - locationMessage, - operatorIdMessage, - selfIdMessage, - authenticationMessage, - systemDataMessage - ]; - - final descriptionString = singleMessages - .where((msg) => msg != null) - .map((msg) => msg.runtimeType) - .join(' + '); - - return 'MessageContainer { ' - '$descriptionString, ' - '${basicIdMessages?.length} basic ID messages, ' - 'last update: $lastUpdate, ' - 'source: $source, ' - 'rssi: $lastMessageRssi }'; - } - - MessageContainer copyWith({ - String? macAddress, - int? lastMessageRssi, - DateTime? lastUpdate, - pigeon.MessageSource? source, - Map? basicIdMessage, - LocationMessage? locationMessage, - OperatorIDMessage? operatorIdMessage, - SelfIDMessage? selfIdMessage, - AuthMessage? authenticationMessage, - SystemMessage? systemDataMessage, - }) => - MessageContainer( - macAddress: macAddress ?? this.macAddress, - lastMessageRssi: lastMessageRssi ?? this.lastMessageRssi, - lastUpdate: lastUpdate ?? DateTime.now(), - source: source ?? this.source, - basicIdMessages: basicIdMessage ?? this.basicIdMessages, - locationMessage: locationMessage ?? this.locationMessage, - operatorIdMessage: operatorIdMessage ?? this.operatorIdMessage, - selfIdMessage: selfIdMessage ?? this.selfIdMessage, - authenticationMessage: - authenticationMessage ?? this.authenticationMessage, - systemDataMessage: systemDataMessage ?? this.systemDataMessage, - ); - - /// Returns new MessageContainer updated with message. - /// Null is returned if update is refused, because it contains duplicate or - /// corrupted data. - MessageContainer? update({ - required ODIDMessage message, - required int receivedTimestamp, - required pigeon.MessageSource source, - int? rssi, - }) { - if (message.runtimeType == MessagePack) { - final messages = (message as MessagePack).messages; - var result = this; - for (var packMessage in messages) { - final update = result.update( - message: packMessage, - receivedTimestamp: receivedTimestamp, - source: source, - rssi: rssi, - ); - if (update != null) result = update; - } - return result; - } - // update pack only if new data differ from saved ones - return switch (message.runtimeType) { - LocationMessage => locationMessage != null && - locationMessage!.containsEqualData(message as LocationMessage) - ? null - : copyWith( - locationMessage: message as LocationMessage, - lastMessageRssi: rssi, - lastUpdate: - DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), - source: source, - ), - BasicIDMessage => _updateBasicIDMessages( - message: message as BasicIDMessage, - receivedTimestamp: receivedTimestamp, - source: source, - rssi: rssi, - ), - SelfIDMessage => selfIdMessage != null && - selfIdMessage!.containsEqualData(message as SelfIDMessage) - ? null - : copyWith( - selfIdMessage: message as SelfIDMessage, - lastMessageRssi: rssi, - lastUpdate: - DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), - source: source, - ), - OperatorIDMessage => operatorIdMessage != null && - operatorIdMessage!.containsEqualData(message as OperatorIDMessage) - ? null - : copyWith( - operatorIdMessage: message as OperatorIDMessage, - lastMessageRssi: rssi, - lastUpdate: - DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), - source: source, - ), - AuthMessage => authenticationMessage != null && - authenticationMessage!.containsEqualData(message as AuthMessage) - ? null - : copyWith( - authenticationMessage: message as AuthMessage, - lastMessageRssi: rssi, - lastUpdate: - DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), - source: source, - ), - SystemMessage => systemDataMessage != null && - systemDataMessage!.containsEqualData(message as SystemMessage) - ? null - : copyWith( - systemDataMessage: message as SystemMessage, - lastMessageRssi: rssi, - lastUpdate: - DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), - source: source, - ), - _ => null - }; - } - - pigeon.MessageSource get packSource => source; - - bool get operatorIDSet => - operatorIdMessage != null && - operatorIdMessage!.operatorID != OPERATOR_ID_NOT_SET; - - bool get operatorIDValid { - final validCharacters = RegExp(r'^[a-zA-Z0-9]+$'); - return operatorIdMessage != null && - operatorIdMessage!.operatorID.length == 16 && - validCharacters.hasMatch(operatorIdMessage!.operatorID); - } - - bool get systemDataValid => - systemDataMessage != null && - systemDataMessage?.operatorLocation != null && - systemDataMessage!.operatorLocation!.latitude != INV_LAT && - systemDataMessage?.operatorLocation!.longitude != INV_LON && - systemDataMessage!.operatorLocation!.latitude <= MAX_LAT && - systemDataMessage!.operatorLocation!.latitude >= MIN_LAT && - systemDataMessage!.operatorLocation!.longitude <= MAX_LON && - systemDataMessage!.operatorLocation!.longitude >= MIN_LON; - - bool get locationValid => - locationMessage != null && - locationMessage?.location != null && - locationMessage!.location!.latitude != INV_LAT && - locationMessage!.location!.longitude != INV_LON && - locationMessage!.location!.latitude <= MAX_LAT && - locationMessage!.location!.longitude <= MAX_LON && - locationMessage!.location!.latitude >= MIN_LAT && - locationMessage!.location!.longitude >= MIN_LON; - - /// Check if container contains basic id message with given uas id - bool containsUasId(String uasId) => - basicIdMessages?.values - .any((element) => element.uasID.asString() == uasId) ?? - false; - - // preferably return message with SerialNumber uas id, which is the default - BasicIDMessage? get preferredBasicIdMessage { - if (basicIdMessages == null || basicIdMessages!.isEmpty) return null; - - return basicIdMessages![IDType.serialNumber] ?? - basicIdMessages!.values.first; - } - - String? get serialNumberUasId => - basicIdMessages?[IDType.serialNumber]?.uasID.asString(); - - MessageContainer? _updateBasicIDMessages({ - required BasicIDMessage message, - required int receivedTimestamp, - required pigeon.MessageSource source, - int? rssi, - }) { - if (basicIdMessages != null && - basicIdMessages![message.uasID.type] != null && - basicIdMessages![message.uasID.type]!.containsEqualData(message)) - return null; - - final newEntry = {message.uasID.type: message}; - return copyWith( - basicIdMessage: basicIdMessages == null ? newEntry : basicIdMessages! - ..addAll(newEntry), - lastMessageRssi: rssi, - lastUpdate: DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), - source: source, - ); - } -} diff --git a/lib/models/received_odid_message.dart b/lib/models/received_odid_message.dart new file mode 100644 index 0000000..b40c149 --- /dev/null +++ b/lib/models/received_odid_message.dart @@ -0,0 +1,26 @@ +import 'package:dart_opendroneid/dart_opendroneid.dart'; +import 'package:flutter_opendroneid/pigeon.dart'; + +typedef MacAddress = String; + +class ReceivedODIDMessage { + final ODIDMessage odidMessage; + final DateTime receivedTimestamp; + final MessageSource source; + final MacAddress macAddress; + final int? rssi; + + ReceivedODIDMessage({ + required this.odidMessage, + required this.receivedTimestamp, + required this.source, + required this.macAddress, + this.rssi, + }); + + @override + String toString() => 'ReceivedODIDMessage{ ' + 'odidMessage type: ${odidMessage.runtimeType}, ' + 'macAddress: $macAddress, source: ${source.name}, rssi: $rssi, ' + 'receivedTimestamp: $receivedTimestamp }'; +} diff --git a/pubspec.yaml b/pubspec.yaml index f4336a8..9cdc548 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_opendroneid description: A flutter plugin for reading Wi-Fi and Bluetooth Remote ID advertisements using native Android and iOS platform-specific implementation. -version: 0.19.4 +version: 1.0.0-rc.1 repository: https://github.com/dronetag/flutter-opendroneid environment: sdk: ">=3.0.0 <4.0.0"