From 1a4a95d8a896e26f938d160ebe2da44662c14b2f Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 16 Jan 2026 15:57:04 +0530 Subject: [PATCH 1/6] Add service-data in advertisements --- .../navideck/universal_ble/UniversalBle.g.kt | 9 ++- .../universal_ble/UniversalBleHelper.kt | 5 ++ .../universal_ble/UniversalBlePlugin.kt | 3 + darwin/Classes/UniversalBle.g.swift | 8 +- darwin/Classes/UniversalBlePlugin.swift | 14 +++- .../lib/home/widgets/scanned_item_widget.dart | 11 +++ example/macos/Podfile.lock | 10 +-- example/pubspec.lock | 2 +- lib/src/models/ble_device.dart | 5 +- .../universal_ble_linux.dart | 23 +++++- .../universal_ble_pigeon/universal_ble.g.dart | 10 ++- .../universal_ble_pigeon_channel.dart | 1 + .../universal_ble_web/universal_ble_web.dart | 6 ++ pigeon/universal_ble.dart | 2 + windows/src/generated/universal_ble.g.cpp | 26 ++++++- windows/src/generated/universal_ble.g.h | 6 ++ windows/src/universal_ble_plugin.cpp | 76 ++++++++++++++++++- 17 files changed, 195 insertions(+), 22 deletions(-) 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 0c9a6cee..388b5a56 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 cf042286..de08e6a1 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 9c7647e6..9e7ce04b 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 19c91db8..c370e810 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 477ace00..8d494f74 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -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,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral manufacturerDataList.append(universalManufacturerData!) } + var serviceData: [String: FlutterStandardTypedData]? = nil + if let serviceDataDict = serviceDataDict { + var convertedServiceData: [String: FlutterStandardTypedData] = [:] + for (uuid, data) in serviceDataDict { + convertedServiceData[uuid.uuidStr] = FlutterStandardTypedData(bytes: data) + } + serviceData = convertedServiceData + } + let advertisedName = advertisementData[CBAdvertisementDataLocalNameKey] as? String let displayName = advertisedName ?? peripheral.name advertisementNameCache[peripheral.uuid.uuidString] = displayName @@ -463,6 +474,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 } @@ -486,7 +498,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral error: Error? ) { let deviceId = peripheral.uuid.uuidString - + if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { if isReconnecting { return diff --git a/example/lib/home/widgets/scanned_item_widget.dart b/example/lib/home/widgets/scanned_item_widget.dart index 1d8a73c3..75d90fb5 100644 --- a/example/lib/home/widgets/scanned_item_widget.dart +++ b/example/lib/home/widgets/scanned_item_widget.dart @@ -132,6 +132,17 @@ class ScannedItemWidget extends StatelessWidget { ], ), const SizedBox(height: 6), + // TODO: Remove after testing + if (bleDevice.serviceData.isNotEmpty) ...[ + Text( + 'Service Data: ${bleDevice.serviceData}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ], Wrap( spacing: 4, runSpacing: 4, diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index ec214e6c..70d92e8b 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 470487ad..549707b0 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 4488be27..f6b7759a 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,6 +53,7 @@ class BleDevice { this.services = const [], this.isSystemDevice, this.manufacturerDataList = const [], + this.serviceData = const {}, this.timestamp, }) { rawName = name; @@ -72,6 +74,7 @@ class BleDevice { 'services: $services, ' 'isSystemDevice: $isSystemDevice, ' 'timestamp: $timestamp, ' - 'manufacturerDataList: $manufacturerDataList'; + 'manufacturerDataList: $manufacturerDataList, ' + 'serviceData: $serviceData'; } } diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 8ec9627b..fd4505de 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,22 @@ extension BlueZDeviceExtension on BlueZDevice { ManufacturerData(data.key.id, Uint8List.fromList(data.value))) .toList(); + Map get serviceDataMap { + try { + return serviceData.entries + .map((entry) => MapEntry( + entry.key.toString(), + Uint8List.fromList(entry.value), + )) + .fold>( + {}, + (map, entry) => map..[entry.key] = entry.value, + ); + } catch (e) { + return {}; + } + } + BleDevice toBleDevice({bool? isSystemDevice}) { return BleDevice( name: name, @@ -705,6 +725,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 f450d0c6..ab07edc9 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 73629cd4..f8330b5c 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 a436d3c2..876c1390 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 2263a8fb..f8b4a5f0 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 453231bd..fc54e8b3 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 b504e706..e7296c2e 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 07b95a56..49408b66 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -903,22 +903,87 @@ 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() == + 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)) { + // Parse UUID and data from service data section + if (data_bytes.size() >= 2) { + std::string service_uuid; + std::vector service_data_bytes; + + if (data_type == static_cast( + AdvertisementSectionType::ServiceData16BitUuids)) { + // 16-bit UUID: 2 bytes UUID + data + if (data_bytes.size() >= 2) { + uint16_t uuid_16 = (data_bytes[1] << 8) | data_bytes[0]; + char uuid_str[37]; + sprintf_s(uuid_str, "%04x0000-0000-1000-8000-00805f9b34fb", uuid_16); + service_uuid = std::string(uuid_str); + if (data_bytes.size() > 2) { + service_data_bytes = std::vector( + data_bytes.begin() + 2, data_bytes.end()); + } + } + } else if (data_type == static_cast( + AdvertisementSectionType:: + ServiceData32BitUuids)) { + // 32-bit UUID: 4 bytes UUID + data + if (data_bytes.size() >= 4) { + uint32_t uuid_32 = (data_bytes[3] << 24) | (data_bytes[2] << 16) | + (data_bytes[1] << 8) | data_bytes[0]; + char uuid_str[37]; + sprintf_s(uuid_str, "%08x-0000-1000-8000-00805f9b34fb", uuid_32); + service_uuid = std::string(uuid_str); + if (data_bytes.size() > 4) { + service_data_bytes = std::vector( + data_bytes.begin() + 4, data_bytes.end()); + } + } + } else if (data_type == static_cast( + AdvertisementSectionType:: + ServiceData128BitUuids)) { + // 128-bit UUID: 16 bytes UUID + data + if (data_bytes.size() >= 16) { + guid uuid_guid; + memcpy(&uuid_guid, data_bytes.data(), 16); + service_uuid = guid_to_uuid(uuid_guid); + if (data_bytes.size() > 16) { + service_data_bytes = std::vector( + data_bytes.begin() + 16, data_bytes.end()); + } + } + } + + if (!service_uuid.empty()) { + service_data_map[service_uuid] = flutter::EncodableValue( + flutter::EncodableList(service_data_bytes.begin(), + service_data_bytes.end())); + } + } + } } if (!name.empty()) { @@ -938,6 +1003,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()) { From 3477cbd03c33ec75f4057bd7bee22770ceefd0d6 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 16 Jan 2026 16:29:48 +0530 Subject: [PATCH 2/6] Fix windows serviceData parsing --- .../lib/home/widgets/scanned_item_widget.dart | 2 +- windows/src/universal_ble_plugin.cpp | 158 ++++++++++-------- windows/src/universal_ble_plugin.h | 2 + 3 files changed, 95 insertions(+), 67 deletions(-) diff --git a/example/lib/home/widgets/scanned_item_widget.dart b/example/lib/home/widgets/scanned_item_widget.dart index 75d90fb5..87af3880 100644 --- a/example/lib/home/widgets/scanned_item_widget.dart +++ b/example/lib/home/widgets/scanned_item_widget.dart @@ -135,7 +135,7 @@ class ScannedItemWidget extends StatelessWidget { // TODO: Remove after testing if (bleDevice.serviceData.isNotEmpty) ...[ Text( - 'Service Data: ${bleDevice.serviceData}', + 'Service Data: ${bleDevice.serviceData.toString().split(',').join('\n')}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 49408b66..339c64be 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,41 @@ 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, "%04x0000-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: convert directly + if (uuid_bytes.size() >= 16) { + guid uuid_guid; + memcpy(&uuid_guid, uuid_bytes.data(), 16); + 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( @@ -908,80 +946,68 @@ void UniversalBlePlugin::BluetoothLeWatcherReceived( 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_type == static_cast( - AdvertisementSectionType::CompleteLocalName)) { + AdvertisementSectionType::CompleteLocalName)) { name = std::string(data_bytes.begin(), data_bytes.end()); } // Use ShortenedLocalName from dataType if localName is empty else if (name.empty() && - data_type == - 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)) { - // Parse UUID and data from service data section - if (data_bytes.size() >= 2) { - std::string service_uuid; - std::vector service_data_bytes; - - if (data_type == static_cast( - AdvertisementSectionType::ServiceData16BitUuids)) { - // 16-bit UUID: 2 bytes UUID + data - if (data_bytes.size() >= 2) { - uint16_t uuid_16 = (data_bytes[1] << 8) | data_bytes[0]; - char uuid_str[37]; - sprintf_s(uuid_str, "%04x0000-0000-1000-8000-00805f9b34fb", uuid_16); - service_uuid = std::string(uuid_str); - if (data_bytes.size() > 2) { - service_data_bytes = std::vector( - data_bytes.begin() + 2, data_bytes.end()); - } - } - } else if (data_type == static_cast( - AdvertisementSectionType:: - ServiceData32BitUuids)) { - // 32-bit UUID: 4 bytes UUID + data - if (data_bytes.size() >= 4) { - uint32_t uuid_32 = (data_bytes[3] << 24) | (data_bytes[2] << 16) | - (data_bytes[1] << 8) | data_bytes[0]; - char uuid_str[37]; - sprintf_s(uuid_str, "%08x-0000-1000-8000-00805f9b34fb", uuid_32); - service_uuid = std::string(uuid_str); - if (data_bytes.size() > 4) { - service_data_bytes = std::vector( - data_bytes.begin() + 4, data_bytes.end()); - } - } - } else if (data_type == static_cast( - AdvertisementSectionType:: - ServiceData128BitUuids)) { - // 128-bit UUID: 16 bytes UUID + data - if (data_bytes.size() >= 16) { - guid uuid_guid; - memcpy(&uuid_guid, data_bytes.data(), 16); - service_uuid = guid_to_uuid(uuid_guid); - if (data_bytes.size() > 16) { - service_data_bytes = std::vector( - data_bytes.begin() + 16, data_bytes.end()); - } - } + 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 (!service_uuid.empty()) { - service_data_map[service_uuid] = flutter::EncodableValue( - flutter::EncodableList(service_data_bytes.begin(), - service_data_bytes.end())); + + 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); } } } diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 28688a17..2e033c35 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); From 34c822633ca139ec5a5722ae2f6921e0cdb1d1e3 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 16 Jan 2026 16:37:26 +0530 Subject: [PATCH 3/6] Resolve Ai comments --- windows/src/universal_ble_plugin.cpp | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 339c64be..7c164e5c 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -672,7 +672,8 @@ void UniversalBlePlugin::PairingRequestedHandler( event_args.Accept(pin); } -std::string UniversalBlePlugin::ExpandServiceUuid(const std::vector &uuid_bytes, +std::string +UniversalBlePlugin::ExpandServiceUuid(const std::vector &uuid_bytes, uint8_t uuid_type) { if (uuid_type == static_cast(AdvertisementSectionType::ServiceData16BitUuids)) { @@ -680,7 +681,7 @@ std::string UniversalBlePlugin::ExpandServiceUuid(const std::vector &uu if (uuid_bytes.size() >= 2) { uint16_t uuid_16 = (uuid_bytes[1] << 8) | uuid_bytes[0]; char uuid_str[37]; - sprintf_s(uuid_str, "%04x0000-0000-1000-8000-00805f9b34fb", uuid_16); + sprintf_s(uuid_str, "0000%04x-0000-1000-8000-00805f9b34fb", uuid_16); return std::string(uuid_str); } } else if (uuid_type == @@ -697,10 +698,36 @@ std::string UniversalBlePlugin::ExpandServiceUuid(const std::vector &uu } else if (uuid_type == static_cast( AdvertisementSectionType::ServiceData128BitUuids)) { - // 128-bit UUID: convert directly + // 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; - memcpy(&uuid_guid, uuid_bytes.data(), 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); } } From ac03745ada28030c25eb99860ece0343ae3ac220 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 16 Jan 2026 16:52:09 +0530 Subject: [PATCH 4/6] Resolve Ai comments and validate service data --- darwin/Classes/UniversalBlePlugin.swift | 20 ++++++++---------- lib/src/models/ble_device.dart | 21 ++++++++++++++----- lib/src/models/ble_uuid_parser.dart | 9 ++++++++ .../universal_ble_linux.dart | 13 ++++-------- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index 8d494f74..d761f040 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) } @@ -452,11 +452,9 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral var serviceData: [String: FlutterStandardTypedData]? = nil if let serviceDataDict = serviceDataDict { - var convertedServiceData: [String: FlutterStandardTypedData] = [:] - for (uuid, data) in serviceDataDict { - convertedServiceData[uuid.uuidStr] = FlutterStandardTypedData(bytes: data) - } - serviceData = convertedServiceData + serviceData = Dictionary(uniqueKeysWithValues: serviceDataDict.map { uuid, data in + (uuid.uuidStr, FlutterStandardTypedData(bytes: data)) + }) } let advertisedName = advertisementData[CBAdvertisementDataLocalNameKey] as? String @@ -493,7 +491,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral public func centralManager( _: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, - timestamp: CFAbsoluteTime, + timestamp _: CFAbsoluteTime, isReconnecting: Bool, error: Error? ) { @@ -504,7 +502,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return } } - + handlePeripheralDisconnection(deviceId: deviceId, error: error) } diff --git a/lib/src/models/ble_device.dart b/lib/src/models/ble_device.dart index f6b7759a..125062e3 100644 --- a/lib/src/models/ble_device.dart +++ b/lib/src/models/ble_device.dart @@ -53,17 +53,28 @@ class BleDevice { this.services = const [], this.isSystemDevice, this.manufacturerDataList = const [], - this.serviceData = 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: ' diff --git a/lib/src/models/ble_uuid_parser.dart b/lib/src/models/ble_uuid_parser.dart index be775f50..8ab3ba10 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 fd4505de..16e1a5cf 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -702,15 +702,10 @@ extension BlueZDeviceExtension on BlueZDevice { Map get serviceDataMap { try { - return serviceData.entries - .map((entry) => MapEntry( - entry.key.toString(), - Uint8List.fromList(entry.value), - )) - .fold>( - {}, - (map, entry) => map..[entry.key] = entry.value, - ); + return { + for (final entry in serviceData.entries) + entry.key.toString(): Uint8List.fromList(entry.value), + }; } catch (e) { return {}; } From 9753db8b4167da78a7a54742c571ba799620681d Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 16 Jan 2026 16:53:39 +0530 Subject: [PATCH 5/6] Cleanup and Update changelog --- CHANGELOG.md | 1 + example/lib/home/widgets/scanned_item_widget.dart | 11 ----------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603443c6..e44d5bce 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/example/lib/home/widgets/scanned_item_widget.dart b/example/lib/home/widgets/scanned_item_widget.dart index 87af3880..1d8a73c3 100644 --- a/example/lib/home/widgets/scanned_item_widget.dart +++ b/example/lib/home/widgets/scanned_item_widget.dart @@ -132,17 +132,6 @@ class ScannedItemWidget extends StatelessWidget { ], ), const SizedBox(height: 6), - // TODO: Remove after testing - if (bleDevice.serviceData.isNotEmpty) ...[ - Text( - 'Service Data: ${bleDevice.serviceData.toString().split(',').join('\n')}', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - ], Wrap( spacing: 4, runSpacing: 4, From 56b45e49971d6657cb36bbc44261b818a3282b1f Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Tue, 20 Jan 2026 16:46:54 +0530 Subject: [PATCH 6/6] Use monotonic timestamp in scan result --- .../com/navideck/universal_ble/UniversalBlePlugin.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 9e7ce04b..06c96dc6 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -171,6 +171,9 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), builder.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED) builder.setLegacy(false) } + // Use low latency for scanning + builder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + val settings = builder.build() val usesCustomFilters = filter?.usesCustomFilters() ?: false @@ -846,7 +849,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), manufacturerDataList = null, serviceData = null, rssi = null, - timestamp = System.currentTimeMillis() + timestamp = null ) } ) @@ -1121,7 +1124,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), serviceData = serviceData, rssi = result.rssi.toLong(), services = serviceUuids.map { it.toString() }.toList(), - timestamp = System.currentTimeMillis() + timestamp = result.timestampNanos ) ) {} } @@ -1186,7 +1189,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), deviceIdArg = gatt.device.address, characteristicIdArg = characteristic.uuid.toString(), valueArg = value, - timestampArg = System.currentTimeMillis() + timestampArg = null ) {} } }