diff --git a/CHANGELOG.md b/CHANGELOG.md index 603443c..e44d5bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.2.0 * Add `autoConnect` parameter to `connect()` method for automatic reconnection support on Android and iOS/macOS +* Add `serviceData` in `BleDevice` ## 1.1.0 * Add readRssi method diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt index 0c9a6ce..388b5a5 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt @@ -174,6 +174,7 @@ data class UniversalBleScanResult ( val isPaired: Boolean? = null, val rssi: Long? = null, val manufacturerDataList: List? = null, + val serviceData: Map? = null, val services: List? = null, val timestamp: Long? = null ) @@ -185,9 +186,10 @@ data class UniversalBleScanResult ( val isPaired = pigeonVar_list[2] as Boolean? val rssi = pigeonVar_list[3] as Long? val manufacturerDataList = pigeonVar_list[4] as List? - val services = pigeonVar_list[5] as List? - val timestamp = pigeonVar_list[6] as Long? - return UniversalBleScanResult(deviceId, name, isPaired, rssi, manufacturerDataList, services, timestamp) + val serviceData = pigeonVar_list[5] as Map? + val services = pigeonVar_list[6] as List? + val timestamp = pigeonVar_list[7] as Long? + return UniversalBleScanResult(deviceId, name, isPaired, rssi, manufacturerDataList, serviceData, services, timestamp) } } fun toList(): List { @@ -197,6 +199,7 @@ data class UniversalBleScanResult ( isPaired, rssi, manufacturerDataList, + serviceData, services, timestamp, ) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt index cf04228..de08e6a 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -133,6 +133,11 @@ val ScanResult.manufacturerDataList: List } ?: emptyList() } +val ScanResult.serviceData: Map + get() { + return scanRecord?.serviceData?.mapKeys { it.key.uuid.toString() } ?: emptyMap() + } + fun SparseArray.toList(): List> { return (0 until size).map { index -> keyAt(index) to valueAt(index) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index 9c7647e..9e7ce04 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -844,6 +844,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), deviceId = it.address, isPaired = it.bondState == BOND_BONDED, manufacturerDataList = null, + serviceData = null, rssi = null, timestamp = System.currentTimeMillis() ) @@ -1100,6 +1101,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), val name = result.device.name val manufacturerDataList = result.manufacturerDataList + val serviceData = result.serviceData if (!universalBleFilterUtil.filterDevice( name, @@ -1116,6 +1118,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), deviceId = result.device.address, isPaired = result.device.bondState == BOND_BONDED, manufacturerDataList = manufacturerDataList, + serviceData = serviceData, rssi = result.rssi.toLong(), services = serviceUuids.map { it.toString() }.toList(), timestamp = System.currentTimeMillis() diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index 19c91db..c370e81 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -213,6 +213,7 @@ struct UniversalBleScanResult: Hashable { var isPaired: Bool? = nil var rssi: Int64? = nil var manufacturerDataList: [UniversalManufacturerData]? = nil + var serviceData: [String: FlutterStandardTypedData]? = nil var services: [String]? = nil var timestamp: Int64? = nil @@ -224,8 +225,9 @@ struct UniversalBleScanResult: Hashable { let isPaired: Bool? = nilOrValue(pigeonVar_list[2]) let rssi: Int64? = nilOrValue(pigeonVar_list[3]) let manufacturerDataList: [UniversalManufacturerData]? = nilOrValue(pigeonVar_list[4]) - let services: [String]? = nilOrValue(pigeonVar_list[5]) - let timestamp: Int64? = nilOrValue(pigeonVar_list[6]) + let serviceData: [String: FlutterStandardTypedData]? = nilOrValue(pigeonVar_list[5]) + let services: [String]? = nilOrValue(pigeonVar_list[6]) + let timestamp: Int64? = nilOrValue(pigeonVar_list[7]) return UniversalBleScanResult( deviceId: deviceId, @@ -233,6 +235,7 @@ struct UniversalBleScanResult: Hashable { isPaired: isPaired, rssi: rssi, manufacturerDataList: manufacturerDataList, + serviceData: serviceData, services: services, timestamp: timestamp ) @@ -244,6 +247,7 @@ struct UniversalBleScanResult: Hashable { isPaired, rssi, manufacturerDataList, + serviceData, services, timestamp, ] diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index 477ace0..d761f04 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -134,7 +134,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral let peripheral = try deviceId.getPeripheral(manager: manager) peripheral.delegate = self let shouldAutoConnect = autoConnect ?? false - + if shouldAutoConnect { autoConnectDevices.insert(deviceId) if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { @@ -148,9 +148,9 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // (e.g., in central manager delegate callbacks). UniversalBleLogger.shared.logInfo( "autoConnect requested for device \(deviceId), " + - "but automatic reconnection via CBConnectPeripheralOptionEnableAutoReconnect " + - "is only available on iOS 17+/macOS 14+/watchOS 10+/tvOS 17+. " + - "On this OS version, reconnections must be handled manually." + "but automatic reconnection via CBConnectPeripheralOptionEnableAutoReconnect " + + "is only available on iOS 17+/macOS 14+/watchOS 10+/tvOS 17+. " + + "On this OS version, reconnections must be handled manually." ) manager.connect(peripheral) } @@ -410,6 +410,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return UniversalBleScanResult( deviceId: id, name: name, + serviceData: nil, timestamp: Int64(Date().timeIntervalSince1970 * 1000) ) })) @@ -437,6 +438,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Extract manufacturer data and service UUIDs from the advertisement data let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data let services = (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]) + let serviceDataDict = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] var manufacturerDataList: [UniversalManufacturerData] = [] var universalManufacturerData: UniversalManufacturerData? = nil @@ -448,6 +450,13 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral manufacturerDataList.append(universalManufacturerData!) } + var serviceData: [String: FlutterStandardTypedData]? = nil + if let serviceDataDict = serviceDataDict { + serviceData = Dictionary(uniqueKeysWithValues: serviceDataDict.map { uuid, data in + (uuid.uuidStr, FlutterStandardTypedData(bytes: data)) + }) + } + let advertisedName = advertisementData[CBAdvertisementDataLocalNameKey] as? String let displayName = advertisedName ?? peripheral.name advertisementNameCache[peripheral.uuid.uuidString] = displayName @@ -463,6 +472,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral isPaired: nil, rssi: RSSI as? Int64, manufacturerDataList: manufacturerDataList, + serviceData: serviceData, services: services?.map { $0.uuidStr }, timestamp: Int64(Date().timeIntervalSince1970 * 1000) )) { _ in } @@ -481,18 +491,18 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral public func centralManager( _: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, - timestamp: CFAbsoluteTime, + timestamp _: CFAbsoluteTime, isReconnecting: Bool, error: Error? ) { let deviceId = peripheral.uuid.uuidString - + if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { if isReconnecting { return } } - + handlePeripheralDisconnection(deviceId: deviceId, error: error) } diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index ec214e6..70d92e8 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -38,11 +38,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb - universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914 - url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba + shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 + universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92 + url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 diff --git a/example/pubspec.lock b/example/pubspec.lock index 470487a..549707b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -632,7 +632,7 @@ packages: path: ".." relative: true source: path - version: "1.1.0" + version: "1.2.0" url_launcher: dependency: "direct main" description: diff --git a/lib/src/models/ble_device.dart b/lib/src/models/ble_device.dart index 4488be2..125062e 100644 --- a/lib/src/models/ble_device.dart +++ b/lib/src/models/ble_device.dart @@ -14,6 +14,7 @@ class BleDevice { List services; bool? isSystemDevice; List manufacturerDataList; + Map serviceData; @Deprecated("Use `manufacturerDataList` instead") Uint8List? get manufacturerData => manufacturerDataList.isEmpty @@ -52,16 +53,28 @@ class BleDevice { this.services = const [], this.isSystemDevice, this.manufacturerDataList = const [], + Map serviceData = const {}, this.timestamp, - }) { - rawName = name; - this.name = name?.replaceAll(RegExp(r'[^ -~]'), '').trim(); - } + }) : serviceData = _validateServiceData(serviceData), + rawName = name, + name = name?.replaceAll(RegExp(r'[^ -~]'), '').trim(); DateTime? get timestampDateTime => timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp!) : null; + static Map _validateServiceData( + Map data, + ) { + if (data.isEmpty) return const {}; + return data.map( + (key, value) => MapEntry( + BleUuidParser.stringOrNull(key) ?? key, + Uint8List.fromList(value), + ), + ); + } + @override String toString() { return 'BleDevice: ' @@ -72,6 +85,7 @@ class BleDevice { 'services: $services, ' 'isSystemDevice: $isSystemDevice, ' 'timestamp: $timestamp, ' - 'manufacturerDataList: $manufacturerDataList'; + 'manufacturerDataList: $manufacturerDataList, ' + 'serviceData: $serviceData'; } } diff --git a/lib/src/models/ble_uuid_parser.dart b/lib/src/models/ble_uuid_parser.dart index be775f5..8ab3ba1 100644 --- a/lib/src/models/ble_uuid_parser.dart +++ b/lib/src/models/ble_uuid_parser.dart @@ -48,6 +48,15 @@ class BleUuidParser { return uuid.toLowerCase(); } + /// Parse a string to a valid 128-bit UUID or return null if the string is null or invalid. + static String? stringOrNull(String uuid) { + try { + return string(uuid); + } catch (e) { + return null; + } + } + /// Parse an int number into a 128-bit UUID string. /// e.g. `0x1800` to `00001800-0000-1000-8000-00805f9b34fb`. static String number(int short) { diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 8ec9627..16e1a5c 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -141,7 +141,11 @@ class UniversalBleLinux extends UniversalBlePlatform { } @override - Future connect(String deviceId, {Duration? connectionTimeout, bool autoConnect = false}) async { + Future connect( + String deviceId, { + Duration? connectionTimeout, + bool autoConnect = false, + }) async { // Note: autoConnect is not directly supported on Linux platform final device = _findDeviceById(deviceId); if (device.connected) { @@ -696,6 +700,17 @@ extension BlueZDeviceExtension on BlueZDevice { ManufacturerData(data.key.id, Uint8List.fromList(data.value))) .toList(); + Map get serviceDataMap { + try { + return { + for (final entry in serviceData.entries) + entry.key.toString(): Uint8List.fromList(entry.value), + }; + } catch (e) { + return {}; + } + } + BleDevice toBleDevice({bool? isSystemDevice}) { return BleDevice( name: name, @@ -705,6 +720,7 @@ extension BlueZDeviceExtension on BlueZDevice { isSystemDevice: isSystemDevice, services: uuids.map((e) => e.toString()).toList(), manufacturerDataList: manufacturerDataList, + serviceData: serviceDataMap, timestamp: DateTime.now().millisecondsSinceEpoch, ); } diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index f450d0c..ab07edc 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -122,6 +122,7 @@ class UniversalBleScanResult { this.isPaired, this.rssi, this.manufacturerDataList, + this.serviceData, this.services, this.timestamp, }); @@ -136,6 +137,8 @@ class UniversalBleScanResult { List? manufacturerDataList; + Map? serviceData; + List? services; int? timestamp; @@ -147,6 +150,7 @@ class UniversalBleScanResult { isPaired, rssi, manufacturerDataList, + serviceData, services, timestamp, ]; @@ -165,8 +169,10 @@ class UniversalBleScanResult { rssi: result[3] as int?, manufacturerDataList: (result[4] as List?)?.cast(), - services: (result[5] as List?)?.cast(), - timestamp: result[6] as int?, + serviceData: + (result[5] as Map?)?.cast(), + services: (result[6] as List?)?.cast(), + timestamp: result[7] as int?, ); } diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart index 73629cd..f8330b5 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -294,6 +294,7 @@ extension _UniversalBleScanResultExtension on UniversalBleScanResult { ?.map((e) => ManufacturerData(e.companyIdentifier, e.data)) .toList() ?? [], + serviceData: serviceData ?? {}, ); } } diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index a436d3c..876c139 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -134,11 +134,15 @@ class UniversalBleWeb extends UniversalBlePlatform { _deviceAdvertisementStreamList[device.id] = device.advertisements.listen((event) { + final serviceDataMap = event.serviceData.map( + (key, value) => MapEntry(key, value.buffer.asUint8List()), + ); updateScanResult( device.toBleScanResult( rssi: event.rssi, manufacturerDataMap: event.manufacturerData, services: event.uuids.toSet().toList(), + serviceDataMap: serviceDataMap, ), ); }); @@ -519,6 +523,7 @@ extension _BluetoothDeviceExtension on BluetoothDevice { int? rssi, UnmodifiableMapView? manufacturerDataMap, List services = const [], + Map? serviceDataMap, }) { return BleDevice( name: name, @@ -526,6 +531,7 @@ extension _BluetoothDeviceExtension on BluetoothDevice { manufacturerDataList: manufacturerDataMap?.toManufacturerDataList() ?? [], rssi: rssi, services: services, + serviceData: serviceDataMap ?? {}, timestamp: DateTime.now().millisecondsSinceEpoch, ); } diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 2263a8f..f8b4a5f 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -128,6 +128,7 @@ class UniversalBleScanResult { final bool? isPaired; final int? rssi; final List? manufacturerDataList; + final Map? serviceData; final List? services; final int? timestamp; @@ -137,6 +138,7 @@ class UniversalBleScanResult { required this.isPaired, required this.rssi, required this.manufacturerDataList, + required this.serviceData, required this.services, required this.timestamp, }); diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 453231b..fc54e8b 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -39,6 +39,7 @@ UniversalBleScanResult::UniversalBleScanResult( const bool* is_paired, const int64_t* rssi, const EncodableList* manufacturer_data_list, + const EncodableMap* service_data, const EncodableList* services, const int64_t* timestamp) : device_id_(device_id), @@ -46,6 +47,7 @@ UniversalBleScanResult::UniversalBleScanResult( is_paired_(is_paired ? std::optional(*is_paired) : std::nullopt), rssi_(rssi ? std::optional(*rssi) : std::nullopt), manufacturer_data_list_(manufacturer_data_list ? std::optional(*manufacturer_data_list) : std::nullopt), + service_data_(service_data ? std::optional(*service_data) : std::nullopt), services_(services ? std::optional(*services) : std::nullopt), timestamp_(timestamp ? std::optional(*timestamp) : std::nullopt) {} @@ -110,6 +112,19 @@ void UniversalBleScanResult::set_manufacturer_data_list(const EncodableList& val } +const EncodableMap* UniversalBleScanResult::service_data() const { + return service_data_ ? &(*service_data_) : nullptr; +} + +void UniversalBleScanResult::set_service_data(const EncodableMap* value_arg) { + service_data_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void UniversalBleScanResult::set_service_data(const EncodableMap& value_arg) { + service_data_ = value_arg; +} + + const EncodableList* UniversalBleScanResult::services() const { return services_ ? &(*services_) : nullptr; } @@ -138,12 +153,13 @@ void UniversalBleScanResult::set_timestamp(int64_t value_arg) { EncodableList UniversalBleScanResult::ToEncodableList() const { EncodableList list; - list.reserve(7); + list.reserve(8); list.push_back(EncodableValue(device_id_)); list.push_back(name_ ? EncodableValue(*name_) : EncodableValue()); list.push_back(is_paired_ ? EncodableValue(*is_paired_) : EncodableValue()); list.push_back(rssi_ ? EncodableValue(*rssi_) : EncodableValue()); list.push_back(manufacturer_data_list_ ? EncodableValue(*manufacturer_data_list_) : EncodableValue()); + list.push_back(service_data_ ? EncodableValue(*service_data_) : EncodableValue()); list.push_back(services_ ? EncodableValue(*services_) : EncodableValue()); list.push_back(timestamp_ ? EncodableValue(*timestamp_) : EncodableValue()); return list; @@ -168,11 +184,15 @@ UniversalBleScanResult UniversalBleScanResult::FromEncodableList(const Encodable if (!encodable_manufacturer_data_list.IsNull()) { decoded.set_manufacturer_data_list(std::get(encodable_manufacturer_data_list)); } - auto& encodable_services = list[5]; + auto& encodable_service_data = list[5]; + if (!encodable_service_data.IsNull()) { + decoded.set_service_data(std::get(encodable_service_data)); + } + auto& encodable_services = list[6]; if (!encodable_services.IsNull()) { decoded.set_services(std::get(encodable_services)); } - auto& encodable_timestamp = list[6]; + auto& encodable_timestamp = list[7]; if (!encodable_timestamp.IsNull()) { decoded.set_timestamp(std::get(encodable_timestamp)); } diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index b504e70..e7296c2 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -145,6 +145,7 @@ class UniversalBleScanResult { const bool* is_paired, const int64_t* rssi, const flutter::EncodableList* manufacturer_data_list, + const flutter::EncodableMap* service_data, const flutter::EncodableList* services, const int64_t* timestamp); @@ -167,6 +168,10 @@ class UniversalBleScanResult { void set_manufacturer_data_list(const flutter::EncodableList* value_arg); void set_manufacturer_data_list(const flutter::EncodableList& value_arg); + const flutter::EncodableMap* service_data() const; + void set_service_data(const flutter::EncodableMap* value_arg); + void set_service_data(const flutter::EncodableMap& value_arg); + const flutter::EncodableList* services() const; void set_services(const flutter::EncodableList* value_arg); void set_services(const flutter::EncodableList& value_arg); @@ -186,6 +191,7 @@ class UniversalBleScanResult { std::optional is_paired_; std::optional rssi_; std::optional manufacturer_data_list_; + std::optional service_data_; std::optional services_; std::optional timestamp_; }; diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 07b95a5..7c164e5 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -119,7 +119,8 @@ void UniversalBlePlugin::DisableBluetooth( }); } -ErrorOr UniversalBlePlugin::HasPermissions(bool with_android_fine_location) { +ErrorOr +UniversalBlePlugin::HasPermissions(bool with_android_fine_location) { // Windows does not require runtime permissions for Bluetooth return true; } @@ -261,7 +262,8 @@ UniversalBlePlugin::SetLogLevel(const UniversalBleLogLevel &log_level) { } std::optional -UniversalBlePlugin::Connect(const std::string &device_id, const bool *auto_connect) { +UniversalBlePlugin::Connect(const std::string &device_id, + const bool *auto_connect) { // Note: autoConnect is not directly supported on Windows platform ConnectAsync(str_to_mac_address(device_id)); return std::nullopt; @@ -455,8 +457,9 @@ void UniversalBlePlugin::RequestMtu( void UniversalBlePlugin::ReadRssi( const std::string &device_id, std::function reply)> result) { - result(create_flutter_error(UniversalBleErrorCode::kNotImplemented, - "readRssi is not implemented on Windows platform")); + result( + create_flutter_error(UniversalBleErrorCode::kNotImplemented, + "readRssi is not implemented on Windows platform")); } void UniversalBlePlugin::IsPaired( @@ -669,6 +672,68 @@ void UniversalBlePlugin::PairingRequestedHandler( event_args.Accept(pin); } +std::string +UniversalBlePlugin::ExpandServiceUuid(const std::vector &uuid_bytes, + uint8_t uuid_type) { + if (uuid_type == + static_cast(AdvertisementSectionType::ServiceData16BitUuids)) { + // 16-bit UUID: expand to full 128-bit format + if (uuid_bytes.size() >= 2) { + uint16_t uuid_16 = (uuid_bytes[1] << 8) | uuid_bytes[0]; + char uuid_str[37]; + sprintf_s(uuid_str, "0000%04x-0000-1000-8000-00805f9b34fb", uuid_16); + return std::string(uuid_str); + } + } else if (uuid_type == + static_cast( + AdvertisementSectionType::ServiceData32BitUuids)) { + // 32-bit UUID: expand to full 128-bit format + if (uuid_bytes.size() >= 4) { + uint32_t uuid_32 = (uuid_bytes[3] << 24) | (uuid_bytes[2] << 16) | + (uuid_bytes[1] << 8) | uuid_bytes[0]; + char uuid_str[37]; + sprintf_s(uuid_str, "%08x-0000-1000-8000-00805f9b34fb", uuid_32); + return std::string(uuid_str); + } + } else if (uuid_type == + static_cast( + AdvertisementSectionType::ServiceData128BitUuids)) { + // 128-bit UUID: parse with proper endianness handling + // BLE service data stores UUIDs in little-endian byte order + // guid_to_uuid reads: Data1/Data2/Data3 as big-endian (reverse), Data4 as + // little-endian (forward) + if (uuid_bytes.size() >= 16) { + guid uuid_guid{}; + + // Data1: bytes [0-3] - guid_to_uuid reads in reverse (big-endian), so + // store in reverse + uuid_guid.Data1 = static_cast(uuid_bytes[3]) | + (static_cast(uuid_bytes[2]) << 8) | + (static_cast(uuid_bytes[1]) << 16) | + (static_cast(uuid_bytes[0]) << 24); + + // Data2: bytes [4-5] - guid_to_uuid reads in reverse (big-endian), so + // store in reverse + uuid_guid.Data2 = static_cast(uuid_bytes[5]) | + (static_cast(uuid_bytes[4]) << 8); + + // Data3: bytes [6-7] - guid_to_uuid reads in reverse (big-endian), so + // store in reverse + uuid_guid.Data3 = static_cast(uuid_bytes[7]) | + (static_cast(uuid_bytes[6]) << 8); + + // Data4: bytes [8-15] - guid_to_uuid reads in order (little-endian), so + // store in order + for (size_t i = 0; i < 8; i++) { + uuid_guid.Data4[i] = uuid_bytes[8 + i]; + } + + return guid_to_uuid(uuid_guid); + } + } + return std::string(); +} + // Send device to callback channel // if device is already discovered in deviceWatcher then merge the scan result void UniversalBlePlugin::PushUniversalScanResult( @@ -903,22 +968,75 @@ void UniversalBlePlugin::BluetoothLeWatcherReceived( } } + auto service_data_map = flutter::EncodableMap(); auto data_section = args.Advertisement().DataSections(); for (auto &&data : data_section) { auto data_bytes = to_bytevc(data.Data()); + auto data_type = data.DataType(); + // Use CompleteName from dataType if localName is empty if (name.empty() && - data.DataType() == static_cast( - AdvertisementSectionType::CompleteLocalName)) { + data_type == static_cast( + AdvertisementSectionType::CompleteLocalName)) { name = std::string(data_bytes.begin(), data_bytes.end()); } // Use ShortenedLocalName from dataType if localName is empty else if (name.empty() && - data.DataType() == - static_cast( - AdvertisementSectionType::ShortenedLocalName)) { + data_type == static_cast( + AdvertisementSectionType::ShortenedLocalName)) { name = std::string(data_bytes.begin(), data_bytes.end()); } + // Extract service data + else if (data_type == + static_cast( + AdvertisementSectionType::ServiceData16BitUuids) || + data_type == + static_cast( + AdvertisementSectionType::ServiceData32BitUuids) || + data_type == + static_cast( + AdvertisementSectionType::ServiceData128BitUuids)) { + // Helper lambda to parse UUID and extract service data + auto parse_service_data = + [&](const std::vector &bytes, + uint8_t type) -> std::pair> { + std::string uuid; + std::vector data_payload; + size_t uuid_size = 0; + + if (type == static_cast( + AdvertisementSectionType::ServiceData16BitUuids)) { + uuid_size = 2; + } else if (type == + static_cast( + AdvertisementSectionType::ServiceData32BitUuids)) { + uuid_size = 4; + } else if (type == + static_cast( + AdvertisementSectionType::ServiceData128BitUuids)) { + uuid_size = 16; + } + + if (bytes.size() >= uuid_size) { + std::vector uuid_bytes(bytes.begin(), + bytes.begin() + uuid_size); + uuid = ExpandServiceUuid(uuid_bytes, type); + if (bytes.size() > uuid_size) { + data_payload = + std::vector(bytes.begin() + uuid_size, bytes.end()); + } + } + + return {uuid, data_payload}; + }; + + auto [service_uuid, service_data_bytes] = + parse_service_data(data_bytes, data_type); + if (!service_uuid.empty()) { + service_data_map[service_uuid] = + flutter::EncodableValue(service_data_bytes); + } + } } if (!name.empty()) { @@ -938,6 +1056,11 @@ void UniversalBlePlugin::BluetoothLeWatcherReceived( services.push_back(guid_to_uuid(uuid)); universal_scan_result.set_services(services); + // Add service data + if (!service_data_map.empty()) { + universal_scan_result.set_service_data(&service_data_map); + } + // check if this device already discovered in deviceWatcher auto it = device_watcher_devices_.get(device_id); if (it.has_value()) { diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 28688a1..2e033c3 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -148,6 +148,8 @@ class UniversalBlePlugin : public flutter::Plugin, void DisposeDeviceWatcher(); void PushUniversalScanResult(UniversalBleScanResult scan_result, bool is_connectable); + static std::string ExpandServiceUuid(const std::vector& uuid_bytes, + uint8_t uuid_type); void BluetoothLeWatcherReceived( const BluetoothLEAdvertisementWatcher &sender, const BluetoothLEAdvertisementReceivedEventArgs &args);