From 849c8bb314fd841bdb1b4e096bb013dcb3c674fc Mon Sep 17 00:00:00 2001 From: Jonas Bark Date: Thu, 25 Jun 2026 08:37:13 +0200 Subject: [PATCH 1/3] feat(android,peripheral): add addServicesInScanResponse option Android caps the primary BLE advertisement at 31 bytes. A 128-bit service UUID (18 bytes) plus the device name overflows it, so startAdvertising fails with ADVERTISE_FAILED_DATA_TOO_LARGE. Add PeripheralAndroidOptions.addServicesInScanResponse: when true, advertised service UUIDs are placed in the scan response instead of the primary packet (and the device name is kept out of the scan response so it can't overflow there either). Active scanners still receive every UUID. Other platforms manage the advertisement/scan-response split themselves and are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../navideck/universal_ble/UniversalBle.g.kt | 18 +++++++++-- .../UniversalBlePeripheralPlugin.kt | 10 +++++-- .../universal_ble/UniversalBle.g.swift | 17 +++++++++-- lib/src/universal_ble.g.dart | 26 ++++++++++++---- pigeon/universal_ble.dart | 13 +++++++- windows/src/generated/universal_ble.g.cpp | 30 ++++++++++++++++--- windows/src/generated/universal_ble.g.h | 15 +++++++++- 7 files changed, 110 insertions(+), 19 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 bf74181d..14007452 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 @@ -970,18 +970,29 @@ data class UniversalManufacturerData ( /** Generated class from Pigeon that represents data sent in messages. */ data class PeripheralAndroidOptions ( - val addManufacturerDataInScanResponse: Boolean? = null + val addManufacturerDataInScanResponse: Boolean? = null, + /** + * Put advertised service UUIDs in the scan response instead of the primary + * advertisement. The Android primary advertisement is capped at 31 bytes; + * a 128-bit service UUID (18 bytes) plus a device name can overflow it + * ("ADVERTISE_FAILED_DATA_TOO_LARGE"). Moving the UUIDs to the scan + * response keeps them discoverable to active scanners while freeing the + * primary packet. + */ + val addServicesInScanResponse: Boolean? = null ) { companion object { fun fromList(pigeonVar_list: List): PeripheralAndroidOptions { val addManufacturerDataInScanResponse = pigeonVar_list[0] as Boolean? - return PeripheralAndroidOptions(addManufacturerDataInScanResponse) + val addServicesInScanResponse = pigeonVar_list[1] as Boolean? + return PeripheralAndroidOptions(addManufacturerDataInScanResponse, addServicesInScanResponse) } } fun toList(): List { return listOf( addManufacturerDataInScanResponse, + addServicesInScanResponse, ) } override fun equals(other: Any?): Boolean { @@ -992,12 +1003,13 @@ data class PeripheralAndroidOptions ( return true } val other = other as PeripheralAndroidOptions - return UniversalBlePigeonUtils.deepEquals(this.addManufacturerDataInScanResponse, other.addManufacturerDataInScanResponse) + return UniversalBlePigeonUtils.deepEquals(this.addManufacturerDataInScanResponse, other.addManufacturerDataInScanResponse) && UniversalBlePigeonUtils.deepEquals(this.addServicesInScanResponse, other.addServicesInScanResponse) } override fun hashCode(): Int { var result = javaClass.hashCode() result = 31 * result + UniversalBlePigeonUtils.deepHash(this.addManufacturerDataInScanResponse) + result = 31 * result + UniversalBlePigeonUtils.deepHash(this.addServicesInScanResponse) return result } } diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt index c222c96d..9a542fb7 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt @@ -144,12 +144,17 @@ class UniversalBlePeripheralPlugin( .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) .build() + val addServicesInScanResponse = + platformConfig?.android?.addServicesInScanResponse == true val advertiseDataBuilder = AdvertiseData.Builder() .setIncludeTxPowerLevel(false) .setIncludeDeviceName(localName != null) val scanResponseBuilder = AdvertiseData.Builder() .setIncludeTxPowerLevel(false) - .setIncludeDeviceName(localName != null) + // When the scan response also carries the service UUIDs, keep the + // device name out of it: a name plus a 128-bit UUID can overflow + // the 31-byte scan response just like the primary advertisement. + .setIncludeDeviceName(localName != null && !addServicesInScanResponse) val addManufacturerDataInScanResponse = platformConfig?.android?.addManufacturerDataInScanResponse == true @@ -166,7 +171,8 @@ class UniversalBlePeripheralPlugin( ) } } - services.forEach { advertiseDataBuilder.addServiceUuid(ParcelUuid.fromString(it)) } + val servicesBuilder = if (addServicesInScanResponse) scanResponseBuilder else advertiseDataBuilder + services.forEach { servicesBuilder.addServiceUuid(ParcelUuid.fromString(it)) } bluetoothLeAdvertiser?.startAdvertising( advertiseSettings, diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift index 88409a42..18d4efb6 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift @@ -845,31 +845,42 @@ struct UniversalManufacturerData: Hashable { /// Generated class from Pigeon that represents data sent in messages. struct PeripheralAndroidOptions: Hashable { var addManufacturerDataInScanResponse: Bool? = nil + /// Put advertised service UUIDs in the scan response instead of the primary + /// advertisement. The Android primary advertisement is capped at 31 bytes; + /// a 128-bit service UUID (18 bytes) plus a device name can overflow it + /// ("ADVERTISE_FAILED_DATA_TOO_LARGE"). Moving the UUIDs to the scan + /// response keeps them discoverable to active scanners while freeing the + /// primary packet. + var addServicesInScanResponse: Bool? = nil // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> PeripheralAndroidOptions? { let addManufacturerDataInScanResponse: Bool? = nilOrValue(pigeonVar_list[0]) + let addServicesInScanResponse: Bool? = nilOrValue(pigeonVar_list[1]) return PeripheralAndroidOptions( - addManufacturerDataInScanResponse: addManufacturerDataInScanResponse + addManufacturerDataInScanResponse: addManufacturerDataInScanResponse, + addServicesInScanResponse: addServicesInScanResponse ) } func toList() -> [Any?] { return [ - addManufacturerDataInScanResponse + addManufacturerDataInScanResponse, + addServicesInScanResponse, ] } static func == (lhs: PeripheralAndroidOptions, rhs: PeripheralAndroidOptions) -> Bool { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.addManufacturerDataInScanResponse, rhs.addManufacturerDataInScanResponse) + return deepEqualsUniversalBle(lhs.addManufacturerDataInScanResponse, rhs.addManufacturerDataInScanResponse) && deepEqualsUniversalBle(lhs.addServicesInScanResponse, rhs.addServicesInScanResponse) } func hash(into hasher: inout Hasher) { hasher.combine("PeripheralAndroidOptions") deepHashUniversalBle(value: addManufacturerDataInScanResponse, hasher: &hasher) + deepHashUniversalBle(value: addServicesInScanResponse, hasher: &hasher) } } diff --git a/lib/src/universal_ble.g.dart b/lib/src/universal_ble.g.dart index 09632f17..ec822729 100644 --- a/lib/src/universal_ble.g.dart +++ b/lib/src/universal_ble.g.dart @@ -834,12 +834,26 @@ class UniversalManufacturerData { } class PeripheralAndroidOptions { - PeripheralAndroidOptions({this.addManufacturerDataInScanResponse}); + PeripheralAndroidOptions({ + this.addManufacturerDataInScanResponse, + this.addServicesInScanResponse, + }); bool? addManufacturerDataInScanResponse; + /// Put advertised service UUIDs in the scan response instead of the primary + /// advertisement. The Android primary advertisement is capped at 31 bytes; + /// a 128-bit service UUID (18 bytes) plus a device name can overflow it + /// ("ADVERTISE_FAILED_DATA_TOO_LARGE"). Moving the UUIDs to the scan + /// response keeps them discoverable to active scanners while freeing the + /// primary packet. + bool? addServicesInScanResponse; + List _toList() { - return [addManufacturerDataInScanResponse]; + return [ + addManufacturerDataInScanResponse, + addServicesInScanResponse, + ]; } Object encode() { @@ -850,6 +864,7 @@ class PeripheralAndroidOptions { result as List; return PeripheralAndroidOptions( addManufacturerDataInScanResponse: result[0] as bool?, + addServicesInScanResponse: result[1] as bool?, ); } @@ -864,9 +879,10 @@ class PeripheralAndroidOptions { return true; } return _deepEquals( - addManufacturerDataInScanResponse, - other.addManufacturerDataInScanResponse, - ); + addManufacturerDataInScanResponse, + other.addManufacturerDataInScanResponse, + ) && + _deepEquals(addServicesInScanResponse, other.addServicesInScanResponse); } @override diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 4e02845a..42b64319 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -261,7 +261,18 @@ class UniversalManufacturerData { class PeripheralAndroidOptions { bool? addManufacturerDataInScanResponse; - PeripheralAndroidOptions({this.addManufacturerDataInScanResponse}); + + /// Put advertised service UUIDs in the scan response instead of the primary + /// advertisement. The Android primary advertisement is capped at 31 bytes; + /// a 128-bit service UUID (18 bytes) plus a device name can overflow it + /// ("ADVERTISE_FAILED_DATA_TOO_LARGE"). Moving the UUIDs to the scan + /// response keeps them discoverable to active scanners while freeing the + /// primary packet. + bool? addServicesInScanResponse; + PeripheralAndroidOptions({ + this.addManufacturerDataInScanResponse, + this.addServicesInScanResponse, + }); } class PeripheralPlatformConfig { diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 03e4caab..981a6c83 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -1184,8 +1184,11 @@ size_t PigeonInternalDeepHash(const UniversalManufacturerData& v) { PeripheralAndroidOptions::PeripheralAndroidOptions() {} -PeripheralAndroidOptions::PeripheralAndroidOptions(const bool* add_manufacturer_data_in_scan_response) - : add_manufacturer_data_in_scan_response_(add_manufacturer_data_in_scan_response ? std::optional(*add_manufacturer_data_in_scan_response) : std::nullopt) {} +PeripheralAndroidOptions::PeripheralAndroidOptions( + const bool* add_manufacturer_data_in_scan_response, + const bool* add_services_in_scan_response) + : add_manufacturer_data_in_scan_response_(add_manufacturer_data_in_scan_response ? std::optional(*add_manufacturer_data_in_scan_response) : std::nullopt), + add_services_in_scan_response_(add_services_in_scan_response ? std::optional(*add_services_in_scan_response) : std::nullopt) {} const bool* PeripheralAndroidOptions::add_manufacturer_data_in_scan_response() const { return add_manufacturer_data_in_scan_response_ ? &(*add_manufacturer_data_in_scan_response_) : nullptr; @@ -1200,10 +1203,24 @@ void PeripheralAndroidOptions::set_add_manufacturer_data_in_scan_response(bool v } +const bool* PeripheralAndroidOptions::add_services_in_scan_response() const { + return add_services_in_scan_response_ ? &(*add_services_in_scan_response_) : nullptr; +} + +void PeripheralAndroidOptions::set_add_services_in_scan_response(const bool* value_arg) { + add_services_in_scan_response_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void PeripheralAndroidOptions::set_add_services_in_scan_response(bool value_arg) { + add_services_in_scan_response_ = value_arg; +} + + EncodableList PeripheralAndroidOptions::ToEncodableList() const { EncodableList list; - list.reserve(1); + list.reserve(2); list.push_back(add_manufacturer_data_in_scan_response_ ? EncodableValue(*add_manufacturer_data_in_scan_response_) : EncodableValue()); + list.push_back(add_services_in_scan_response_ ? EncodableValue(*add_services_in_scan_response_) : EncodableValue()); return list; } @@ -1213,11 +1230,15 @@ PeripheralAndroidOptions PeripheralAndroidOptions::FromEncodableList(const Encod if (!encodable_add_manufacturer_data_in_scan_response.IsNull()) { decoded.set_add_manufacturer_data_in_scan_response(std::get(encodable_add_manufacturer_data_in_scan_response)); } + auto& encodable_add_services_in_scan_response = list[1]; + if (!encodable_add_services_in_scan_response.IsNull()) { + decoded.set_add_services_in_scan_response(std::get(encodable_add_services_in_scan_response)); + } return decoded; } bool PeripheralAndroidOptions::operator==(const PeripheralAndroidOptions& other) const { - return PigeonInternalDeepEquals(add_manufacturer_data_in_scan_response_, other.add_manufacturer_data_in_scan_response_); + return PigeonInternalDeepEquals(add_manufacturer_data_in_scan_response_, other.add_manufacturer_data_in_scan_response_) && PigeonInternalDeepEquals(add_services_in_scan_response_, other.add_services_in_scan_response_); } bool PeripheralAndroidOptions::operator!=(const PeripheralAndroidOptions& other) const { @@ -1227,6 +1248,7 @@ bool PeripheralAndroidOptions::operator!=(const PeripheralAndroidOptions& other) size_t PeripheralAndroidOptions::Hash() const { size_t result = 1; result = result * 31 + PigeonInternalDeepHash(add_manufacturer_data_in_scan_response_); + result = result * 31 + PigeonInternalDeepHash(add_services_in_scan_response_); return result; } diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index dc43c599..d34fb1a7 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -729,12 +729,24 @@ class PeripheralAndroidOptions { PeripheralAndroidOptions(); // Constructs an object setting all fields. - explicit PeripheralAndroidOptions(const bool* add_manufacturer_data_in_scan_response); + explicit PeripheralAndroidOptions( + const bool* add_manufacturer_data_in_scan_response, + const bool* add_services_in_scan_response); const bool* add_manufacturer_data_in_scan_response() const; void set_add_manufacturer_data_in_scan_response(const bool* value_arg); void set_add_manufacturer_data_in_scan_response(bool value_arg); + // Put advertised service UUIDs in the scan response instead of the primary + // advertisement. The Android primary advertisement is capped at 31 bytes; + // a 128-bit service UUID (18 bytes) plus a device name can overflow it + // ("ADVERTISE_FAILED_DATA_TOO_LARGE"). Moving the UUIDs to the scan + // response keeps them discoverable to active scanners while freeing the + // primary packet. + const bool* add_services_in_scan_response() const; + void set_add_services_in_scan_response(const bool* value_arg); + void set_add_services_in_scan_response(bool value_arg); + bool operator==(const PeripheralAndroidOptions& other) const; bool operator!=(const PeripheralAndroidOptions& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. @@ -750,6 +762,7 @@ class PeripheralAndroidOptions { friend class UniversalBlePeripheralCallback; friend class PigeonInternalCodecSerializer; std::optional add_manufacturer_data_in_scan_response_; + std::optional add_services_in_scan_response_; }; From b2413a0168ae1a62dd0342b981ff494d8986be79 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Thu, 25 Jun 2026 14:11:26 +0530 Subject: [PATCH 2/3] Update pigeon/universal_ble.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- pigeon/universal_ble.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 42b64319..c3056a39 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -263,11 +263,11 @@ class PeripheralAndroidOptions { bool? addManufacturerDataInScanResponse; /// Put advertised service UUIDs in the scan response instead of the primary - /// advertisement. The Android primary advertisement is capped at 31 bytes; - /// a 128-bit service UUID (18 bytes) plus a device name can overflow it - /// ("ADVERTISE_FAILED_DATA_TOO_LARGE"). Moving the UUIDs to the scan - /// response keeps them discoverable to active scanners while freeing the - /// primary packet. + /// advertisement. The Android primary advertisement and scan response are + /// both capped at 31 bytes. A 128-bit service UUID (18 bytes) plus a + /// device name can overflow the primary packet. + /// Note: If this is enabled with `addManufacturerDataInScanResponse`, ensure + /// the combined data fits within the scan response's 31-byte limit. bool? addServicesInScanResponse; PeripheralAndroidOptions({ this.addManufacturerDataInScanResponse, From 42326099d9319cb3473d0d3dc6cf9d37e9a99064 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Thu, 25 Jun 2026 14:18:49 +0530 Subject: [PATCH 3/3] Update android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt --- .../navideck/universal_ble/UniversalBlePeripheralPlugin.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt index 9a542fb7..7a4ec148 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt @@ -151,10 +151,9 @@ class UniversalBlePeripheralPlugin( .setIncludeDeviceName(localName != null) val scanResponseBuilder = AdvertiseData.Builder() .setIncludeTxPowerLevel(false) - // When the scan response also carries the service UUIDs, keep the - // device name out of it: a name plus a 128-bit UUID can overflow - // the 31-byte scan response just like the primary advertisement. - .setIncludeDeviceName(localName != null && !addServicesInScanResponse) +// Device name is already included in the primary advertisement when localName != null. +// Keeping it out of the scan response saves space for manufacturer data / service UUIDs. +.setIncludeDeviceName(false) val addManufacturerDataInScanResponse = platformConfig?.android?.addManufacturerDataInScanResponse == true