diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14aff2f..d850d77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
## 1.3.0
+* Add Linux BLE peripheral/GATT-server support through BlueZ GATT application and LE advertisement registration
* Add `requestConnectionPriority` to allow tuning BLE connection intervals on Android
* Add SPM support on Apple
diff --git a/README.md b/README.md
index 73ea212..7e9b1c3 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
- [Connecting](#connecting)
- [Discovering Services](#discovering-services)
- [Reading & Writing data](#reading--writing-data)
+- [Peripheral Mode](#peripheral-mode)
- [Pairing](#pairing)
- [Bluetooth Availability](#bluetooth-availability)
- [Requesting MTU](#requesting-mtu)
@@ -57,6 +58,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
| requestConnectionPriority | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
| readRssi | ✔️ | ✔️ | ✔️ | ❌ | 🚧 | ❌ |
| requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
+| peripheral/GATT server | ✔️ | 🚧 | 🚧 | 🚧 | ✔️ | ❌ |
## Getting Started
@@ -338,6 +340,49 @@ Unsubscribe from notifications and indications of this characteristic.
await characteristic.unsubscribe();
```
+## Peripheral Mode
+
+`UniversalBlePeripheral` exposes a local GATT-server API for apps that need to advertise services and accept central/client writes. Android and Linux are implemented; Apple and Windows currently report `notSupported` until their platform server implementations are added. Web browsers do not expose a standard GATT-server API.
+
+On Linux, peripheral mode uses BlueZ `GattManager1` and `LEAdvertisingManager1`. The Bluetooth adapter must be powered, support the peripheral role, and the app process must be allowed to register GATT applications and LE advertisements on the system bus.
+
+```dart
+await UniversalBle.requestPermissions(withAndroidBluetoothAdvertise: true);
+
+await UniversalBlePeripheral.start(
+ BlePeripheralConfig(
+ advertisedName: 'DALI Relay',
+ services: [
+ BlePeripheralService(
+ uuid: 'fff0',
+ characteristics: [
+ BlePeripheralCharacteristic(
+ uuid: 'fff1',
+ properties: const [
+ CharacteristicProperty.read,
+ CharacteristicProperty.write,
+ CharacteristicProperty.writeWithoutResponse,
+ CharacteristicProperty.notify,
+ ],
+ permissions: const [
+ BlePeripheralCharacteristicPermission.read,
+ BlePeripheralCharacteristicPermission.write,
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+);
+
+UniversalBlePeripheral.writeStream.listen((event) {
+ debugPrint('Central ${event.deviceId} wrote ${event.value}');
+});
+
+await UniversalBlePeripheral.notify('fff0', 'fff1', Uint8List.fromList([1, 2]));
+await UniversalBlePeripheral.stop();
+```
+
### Pairing
#### Trigger pairing
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 067f1cb..766c9e8 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -7,4 +7,5 @@
+
diff --git a/android/src/main/kotlin/com/navideck/universal_ble/PermissionHandler.kt b/android/src/main/kotlin/com/navideck/universal_ble/PermissionHandler.kt
index 2245ba5..0c80c82 100644
--- a/android/src/main/kotlin/com/navideck/universal_ble/PermissionHandler.kt
+++ b/android/src/main/kotlin/com/navideck/universal_ble/PermissionHandler.kt
@@ -35,12 +35,12 @@ class PermissionHandler(
/**
* Check if we have required permissions
*/
- fun hasPermissions(withFineLocation: Boolean): Boolean {
- val validationError = validateRequiredPermissions(withFineLocation)
+ fun hasPermissions(withFineLocation: Boolean, withAdvertise: Boolean): Boolean {
+ val validationError = validateRequiredPermissions(withFineLocation, withAdvertise)
if (validationError != null) {
throw validationError
}
- val permissionsToRequest = getRequiredPermissions(withFineLocation)
+ val permissionsToRequest = getRequiredPermissions(withFineLocation, withAdvertise)
return permissionsToRequest.isEmpty()
}
@@ -55,17 +55,18 @@ class PermissionHandler(
*/
fun requestPermissions(
withFineLocation: Boolean,
+ withAdvertise: Boolean,
callback: (Result) -> Unit,
) {
// Validate required permissions are declared in manifest
- val validationError = validateRequiredPermissions(withFineLocation)
+ val validationError = validateRequiredPermissions(withFineLocation, withAdvertise)
if (validationError != null) {
callback(Result.failure(validationError))
return
}
// Check which permissions need to be requested
- val permissionsToRequest = getRequiredPermissions(withFineLocation)
+ val permissionsToRequest = getRequiredPermissions(withFineLocation, withAdvertise)
if (permissionsToRequest.isEmpty()) {
// All required permissions are already granted
@@ -163,7 +164,7 @@ class PermissionHandler(
*
* Returns a list of permissions that need to be requested (excluding already granted ones)
*/
- private fun getRequiredPermissions(withFineLocation: Boolean): List {
+ private fun getRequiredPermissions(withFineLocation: Boolean, withAdvertise: Boolean): List {
val permissionsToRequest = mutableListOf()
// Android 12+ (API 31+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -175,6 +176,9 @@ class PermissionHandler(
if (!hasPermissionGranted(Manifest.permission.BLUETOOTH_CONNECT)) {
permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT)
}
+ if (withAdvertise && !hasPermissionGranted(Manifest.permission.BLUETOOTH_ADVERTISE)) {
+ permissionsToRequest.add(Manifest.permission.BLUETOOTH_ADVERTISE)
+ }
// Location permission is optional - only request if user wants it
if (withFineLocation) {
// Prefer ACCESS_FINE_LOCATION over ACCESS_COARSE_LOCATION
@@ -236,7 +240,7 @@ class PermissionHandler(
* @param withFineLocation Whether location permission should be requested
* @return FlutterError if validation fails, null if all required permissions are declared
*/
- private fun validateRequiredPermissions(withFineLocation: Boolean): FlutterError? {
+ private fun validateRequiredPermissions(withFineLocation: Boolean, withAdvertise: Boolean): FlutterError? {
val sdkInt = Build.VERSION.SDK_INT
val missingPermissions = mutableListOf()
@@ -259,6 +263,10 @@ class PermissionHandler(
missingPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
}
+ if (withAdvertise && !hasPermissionInManifest(Manifest.permission.BLUETOOTH_ADVERTISE)) {
+ missingPermissions.add(Manifest.permission.BLUETOOTH_ADVERTISE)
+ }
+
// Location permission is optional on Android 12+ (depends on neverForLocation and withFineLocation)
// Only validate if it's actually needed
if (withFineLocation && !hasDeclaredLocationPermission) {
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 2118809..6accfb0 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
@@ -1,4 +1,4 @@
-// Autogenerated from Pigeon (v26.1.4), do not edit directly.
+// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -37,36 +37,150 @@ private object UniversalBlePigeonUtils {
)
}
}
+ fun doubleEquals(a: Double, b: Double): Boolean {
+ // Normalize -0.0 to 0.0 and handle NaN equality.
+ return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
+ }
+
+ fun floatEquals(a: Float, b: Float): Boolean {
+ // Normalize -0.0 to 0.0 and handle NaN equality.
+ return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
+ }
+
+ fun doubleHash(d: Double): Int {
+ // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
+ val normalized = if (d == 0.0) 0.0 else d
+ val bits = java.lang.Double.doubleToLongBits(normalized)
+ return (bits xor (bits ushr 32)).toInt()
+ }
+
+ fun floatHash(f: Float): Int {
+ // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
+ val normalized = if (f == 0.0f) 0.0f else f
+ return java.lang.Float.floatToIntBits(normalized)
+ }
+
fun deepEquals(a: Any?, b: Any?): Boolean {
+ if (a === b) {
+ return true
+ }
+ if (a == null || b == null) {
+ return false
+ }
if (a is ByteArray && b is ByteArray) {
- return a.contentEquals(b)
+ return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
- return a.contentEquals(b)
+ return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
- return a.contentEquals(b)
+ return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
- return a.contentEquals(b)
+ if (a.size != b.size) return false
+ for (i in a.indices) {
+ if (!doubleEquals(a[i], b[i])) return false
+ }
+ return true
+ }
+ if (a is FloatArray && b is FloatArray) {
+ if (a.size != b.size) return false
+ for (i in a.indices) {
+ if (!floatEquals(a[i], b[i])) return false
+ }
+ return true
}
if (a is Array<*> && b is Array<*>) {
- return a.size == b.size &&
- a.indices.all{ deepEquals(a[it], b[it]) }
+ if (a.size != b.size) return false
+ for (i in a.indices) {
+ if (!deepEquals(a[i], b[i])) return false
+ }
+ return true
}
if (a is List<*> && b is List<*>) {
- return a.size == b.size &&
- a.indices.all{ deepEquals(a[it], b[it]) }
+ if (a.size != b.size) return false
+ val iterA = a.iterator()
+ val iterB = b.iterator()
+ while (iterA.hasNext() && iterB.hasNext()) {
+ if (!deepEquals(iterA.next(), iterB.next())) return false
+ }
+ return true
}
if (a is Map<*, *> && b is Map<*, *>) {
- return a.size == b.size && a.all {
- (b as Map).contains(it.key) &&
- deepEquals(it.value, b[it.key])
+ if (a.size != b.size) return false
+ for (entry in a) {
+ val key = entry.key
+ var found = false
+ for (bEntry in b) {
+ if (deepEquals(key, bEntry.key)) {
+ if (deepEquals(entry.value, bEntry.value)) {
+ found = true
+ break
+ } else {
+ return false
+ }
+ }
+ }
+ if (!found) return false
}
+ return true
+ }
+ if (a is Double && b is Double) {
+ return doubleEquals(a, b)
+ }
+ if (a is Float && b is Float) {
+ return floatEquals(a, b)
}
return a == b
}
-
+
+ fun deepHash(value: Any?): Int {
+ return when (value) {
+ null -> 0
+ is ByteArray -> value.contentHashCode()
+ is IntArray -> value.contentHashCode()
+ is LongArray -> value.contentHashCode()
+ is DoubleArray -> {
+ var result = 1
+ for (item in value) {
+ result = 31 * result + doubleHash(item)
+ }
+ result
+ }
+ is FloatArray -> {
+ var result = 1
+ for (item in value) {
+ result = 31 * result + floatHash(item)
+ }
+ result
+ }
+ is Array<*> -> {
+ var result = 1
+ for (item in value) {
+ result = 31 * result + deepHash(item)
+ }
+ result
+ }
+ is List<*> -> {
+ var result = 1
+ for (item in value) {
+ result = 31 * result + deepHash(item)
+ }
+ result
+ }
+ is Map<*, *> -> {
+ var result = 0
+ for (entry in value) {
+ result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
+ }
+ result
+ }
+ is Double -> doubleHash(value)
+ is Float -> floatHash(value)
+ else -> value.hashCode()
+ }
+ }
+
}
/**
@@ -79,7 +193,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
-) : Throwable()
+) : RuntimeException()
enum class UniversalBleLogLevel(val raw: Int) {
NONE(0),
@@ -219,15 +333,28 @@ data class UniversalBleScanResult (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is UniversalBleScanResult) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as UniversalBleScanResult
+ return UniversalBlePigeonUtils.deepEquals(this.deviceId, other.deviceId) && UniversalBlePigeonUtils.deepEquals(this.name, other.name) && UniversalBlePigeonUtils.deepEquals(this.isPaired, other.isPaired) && UniversalBlePigeonUtils.deepEquals(this.rssi, other.rssi) && UniversalBlePigeonUtils.deepEquals(this.manufacturerDataList, other.manufacturerDataList) && UniversalBlePigeonUtils.deepEquals(this.serviceData, other.serviceData) && UniversalBlePigeonUtils.deepEquals(this.services, other.services) && UniversalBlePigeonUtils.deepEquals(this.timestamp, other.timestamp)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.deviceId)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.name)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.isPaired)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.rssi)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.manufacturerDataList)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.serviceData)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.services)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.timestamp)
+ return result
+ }
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -250,15 +377,22 @@ data class UniversalBleService (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is UniversalBleService) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as UniversalBleService
+ return UniversalBlePigeonUtils.deepEquals(this.uuid, other.uuid) && UniversalBlePigeonUtils.deepEquals(this.characteristics, other.characteristics)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.uuid)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.characteristics)
+ return result
+ }
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -284,15 +418,23 @@ data class UniversalBleCharacteristic (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is UniversalBleCharacteristic) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as UniversalBleCharacteristic
+ return UniversalBlePigeonUtils.deepEquals(this.uuid, other.uuid) && UniversalBlePigeonUtils.deepEquals(this.properties, other.properties) && UniversalBlePigeonUtils.deepEquals(this.descriptors, other.descriptors)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.uuid)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.properties)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.descriptors)
+ return result
+ }
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -312,15 +454,235 @@ data class UniversalBleDescriptor (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is UniversalBleDescriptor) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as UniversalBleDescriptor
+ return UniversalBlePigeonUtils.deepEquals(this.uuid, other.uuid)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.uuid)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class UniversalBlePeripheralConfig (
+ val advertisedName: String,
+ val services: List
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): UniversalBlePeripheralConfig {
+ val advertisedName = pigeonVar_list[0] as String
+ val services = pigeonVar_list[1] as List
+ return UniversalBlePeripheralConfig(advertisedName, services)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ advertisedName,
+ services,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as UniversalBlePeripheralConfig
+ return UniversalBlePigeonUtils.deepEquals(this.advertisedName, other.advertisedName) && UniversalBlePigeonUtils.deepEquals(this.services, other.services)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.advertisedName)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.services)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class UniversalBlePeripheralService (
+ val uuid: String,
+ val characteristics: List
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): UniversalBlePeripheralService {
+ val uuid = pigeonVar_list[0] as String
+ val characteristics = pigeonVar_list[1] as List
+ return UniversalBlePeripheralService(uuid, characteristics)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ uuid,
+ characteristics,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as UniversalBlePeripheralService
+ return UniversalBlePigeonUtils.deepEquals(this.uuid, other.uuid) && UniversalBlePigeonUtils.deepEquals(this.characteristics, other.characteristics)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.uuid)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.characteristics)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class UniversalBlePeripheralCharacteristic (
+ val uuid: String,
+ val properties: List,
+ val permissions: List,
+ val descriptors: List,
+ val initialValue: ByteArray? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): UniversalBlePeripheralCharacteristic {
+ val uuid = pigeonVar_list[0] as String
+ val properties = pigeonVar_list[1] as List
+ val permissions = pigeonVar_list[2] as List
+ val descriptors = pigeonVar_list[3] as List
+ val initialValue = pigeonVar_list[4] as ByteArray?
+ return UniversalBlePeripheralCharacteristic(uuid, properties, permissions, descriptors, initialValue)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ uuid,
+ properties,
+ permissions,
+ descriptors,
+ initialValue,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as UniversalBlePeripheralCharacteristic
+ return UniversalBlePigeonUtils.deepEquals(this.uuid, other.uuid) && UniversalBlePigeonUtils.deepEquals(this.properties, other.properties) && UniversalBlePigeonUtils.deepEquals(this.permissions, other.permissions) && UniversalBlePigeonUtils.deepEquals(this.descriptors, other.descriptors) && UniversalBlePigeonUtils.deepEquals(this.initialValue, other.initialValue)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.uuid)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.properties)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.permissions)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.descriptors)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.initialValue)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class UniversalBlePeripheralDescriptor (
+ val uuid: String,
+ val permissions: List,
+ val initialValue: ByteArray? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): UniversalBlePeripheralDescriptor {
+ val uuid = pigeonVar_list[0] as String
+ val permissions = pigeonVar_list[1] as List
+ val initialValue = pigeonVar_list[2] as ByteArray?
+ return UniversalBlePeripheralDescriptor(uuid, permissions, initialValue)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ uuid,
+ permissions,
+ initialValue,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as UniversalBlePeripheralDescriptor
+ return UniversalBlePigeonUtils.deepEquals(this.uuid, other.uuid) && UniversalBlePigeonUtils.deepEquals(this.permissions, other.permissions) && UniversalBlePigeonUtils.deepEquals(this.initialValue, other.initialValue)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.uuid)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.permissions)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.initialValue)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class UniversalBlePeripheralWriteEvent (
+ val deviceId: String,
+ val service: String,
+ val characteristic: String,
+ val value: ByteArray
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): UniversalBlePeripheralWriteEvent {
+ val deviceId = pigeonVar_list[0] as String
+ val service = pigeonVar_list[1] as String
+ val characteristic = pigeonVar_list[2] as String
+ val value = pigeonVar_list[3] as ByteArray
+ return UniversalBlePeripheralWriteEvent(deviceId, service, characteristic, value)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ deviceId,
+ service,
+ characteristic,
+ value,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as UniversalBlePeripheralWriteEvent
+ return UniversalBlePigeonUtils.deepEquals(this.deviceId, other.deviceId) && UniversalBlePigeonUtils.deepEquals(this.service, other.service) && UniversalBlePigeonUtils.deepEquals(this.characteristic, other.characteristic) && UniversalBlePigeonUtils.deepEquals(this.value, other.value)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.deviceId)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.service)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.characteristic)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.value)
+ return result
+ }
}
/**
@@ -355,15 +717,23 @@ data class AndroidOptions (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is AndroidOptions) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as AndroidOptions
+ return UniversalBlePigeonUtils.deepEquals(this.requestLocationPermission, other.requestLocationPermission) && UniversalBlePigeonUtils.deepEquals(this.scanMode, other.scanMode) && UniversalBlePigeonUtils.deepEquals(this.reportDelayMillis, other.reportDelayMillis)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.requestLocationPermission)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.scanMode)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.reportDelayMillis)
+ return result
+ }
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -383,15 +753,21 @@ data class UniversalScanConfig (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is UniversalScanConfig) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as UniversalScanConfig
+ return UniversalBlePigeonUtils.deepEquals(this.android, other.android)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.android)
+ return result
+ }
}
/**
@@ -421,15 +797,23 @@ data class UniversalScanFilter (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is UniversalScanFilter) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as UniversalScanFilter
+ return UniversalBlePigeonUtils.deepEquals(this.withServices, other.withServices) && UniversalBlePigeonUtils.deepEquals(this.withNamePrefix, other.withNamePrefix) && UniversalBlePigeonUtils.deepEquals(this.withManufacturerData, other.withManufacturerData)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.withServices)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.withNamePrefix)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.withManufacturerData)
+ return result
+ }
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -455,15 +839,23 @@ data class UniversalManufacturerDataFilter (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is UniversalManufacturerDataFilter) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as UniversalManufacturerDataFilter
+ return UniversalBlePigeonUtils.deepEquals(this.companyIdentifier, other.companyIdentifier) && UniversalBlePigeonUtils.deepEquals(this.data, other.data) && UniversalBlePigeonUtils.deepEquals(this.mask, other.mask)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.companyIdentifier)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.data)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.mask)
+ return result
+ }
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -486,15 +878,22 @@ data class UniversalManufacturerData (
)
}
override fun equals(other: Any?): Boolean {
- if (other !is UniversalManufacturerData) {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as UniversalManufacturerData
+ return UniversalBlePigeonUtils.deepEquals(this.companyIdentifier, other.companyIdentifier) && UniversalBlePigeonUtils.deepEquals(this.data, other.data)
+ }
- override fun hashCode(): Int = toList().hashCode()
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.companyIdentifier)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.data)
+ return result
+ }
}
private open class UniversalBlePigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
@@ -536,25 +935,50 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
}
136.toByte() -> {
return (readValue(buffer) as? List)?.let {
- AndroidOptions.fromList(it)
+ UniversalBlePeripheralConfig.fromList(it)
}
}
137.toByte() -> {
return (readValue(buffer) as? List)?.let {
- UniversalScanConfig.fromList(it)
+ UniversalBlePeripheralService.fromList(it)
}
}
138.toByte() -> {
return (readValue(buffer) as? List)?.let {
- UniversalScanFilter.fromList(it)
+ UniversalBlePeripheralCharacteristic.fromList(it)
}
}
139.toByte() -> {
return (readValue(buffer) as? List)?.let {
- UniversalManufacturerDataFilter.fromList(it)
+ UniversalBlePeripheralDescriptor.fromList(it)
}
}
140.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ UniversalBlePeripheralWriteEvent.fromList(it)
+ }
+ }
+ 141.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ AndroidOptions.fromList(it)
+ }
+ }
+ 142.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ UniversalScanConfig.fromList(it)
+ }
+ }
+ 143.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ UniversalScanFilter.fromList(it)
+ }
+ }
+ 144.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ UniversalManufacturerDataFilter.fromList(it)
+ }
+ }
+ 145.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalManufacturerData.fromList(it)
}
@@ -592,26 +1016,46 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
stream.write(135)
writeValue(stream, value.toList())
}
- is AndroidOptions -> {
+ is UniversalBlePeripheralConfig -> {
stream.write(136)
writeValue(stream, value.toList())
}
- is UniversalScanConfig -> {
+ is UniversalBlePeripheralService -> {
stream.write(137)
writeValue(stream, value.toList())
}
- is UniversalScanFilter -> {
+ is UniversalBlePeripheralCharacteristic -> {
stream.write(138)
writeValue(stream, value.toList())
}
- is UniversalManufacturerDataFilter -> {
+ is UniversalBlePeripheralDescriptor -> {
stream.write(139)
writeValue(stream, value.toList())
}
- is UniversalManufacturerData -> {
+ is UniversalBlePeripheralWriteEvent -> {
stream.write(140)
writeValue(stream, value.toList())
}
+ is AndroidOptions -> {
+ stream.write(141)
+ writeValue(stream, value.toList())
+ }
+ is UniversalScanConfig -> {
+ stream.write(142)
+ writeValue(stream, value.toList())
+ }
+ is UniversalScanFilter -> {
+ stream.write(143)
+ writeValue(stream, value.toList())
+ }
+ is UniversalManufacturerDataFilter -> {
+ stream.write(144)
+ writeValue(stream, value.toList())
+ }
+ is UniversalManufacturerData -> {
+ stream.write(145)
+ writeValue(stream, value.toList())
+ }
else -> super.writeValue(stream, value)
}
}
@@ -625,8 +1069,8 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
*/
interface UniversalBlePlatformChannel {
fun getBluetoothAvailabilityState(callback: (Result) -> Unit)
- fun hasPermissions(withAndroidFineLocation: Boolean): Boolean
- fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result) -> Unit)
+ fun hasPermissions(withAndroidFineLocation: Boolean, withAndroidBluetoothAdvertise: Boolean): Boolean
+ fun requestPermissions(withAndroidFineLocation: Boolean, withAndroidBluetoothAdvertise: Boolean, callback: (Result) -> Unit)
fun enableBluetooth(callback: (Result) -> Unit)
fun disableBluetooth(callback: (Result) -> Unit)
fun startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?)
@@ -646,6 +1090,11 @@ interface UniversalBlePlatformChannel {
fun getConnectionState(deviceId: String): Long
fun readRssi(deviceId: String, callback: (Result) -> Unit)
fun requestConnectionPriority(deviceId: String, priority: Long, callback: (Result) -> Unit)
+ fun isPeripheralSupported(): Boolean
+ fun startPeripheral(config: UniversalBlePeripheralConfig, callback: (Result) -> Unit)
+ fun stopPeripheral(callback: (Result) -> Unit)
+ fun updatePeripheralCharacteristicValue(service: String, characteristic: String, value: ByteArray, callback: (Result) -> Unit)
+ fun notifyPeripheralCharacteristic(service: String, characteristic: String, value: ByteArray, indicate: Boolean, callback: (Result) -> Unit)
fun setLogLevel(logLevel: UniversalBleLogLevel)
companion object {
@@ -681,8 +1130,9 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler { message, reply ->
val args = message as List
val withAndroidFineLocationArg = args[0] as Boolean
+ val withAndroidBluetoothAdvertiseArg = args[1] as Boolean
val wrapped: List = try {
- listOf(api.hasPermissions(withAndroidFineLocationArg))
+ listOf(api.hasPermissions(withAndroidFineLocationArg, withAndroidBluetoothAdvertiseArg))
} catch (exception: Throwable) {
UniversalBlePigeonUtils.wrapError(exception)
}
@@ -698,7 +1148,8 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler { message, reply ->
val args = message as List
val withAndroidFineLocationArg = args[0] as Boolean
- api.requestPermissions(withAndroidFineLocationArg) { result: Result ->
+ val withAndroidBluetoothAdvertiseArg = args[1] as Boolean
+ api.requestPermissions(withAndroidFineLocationArg, withAndroidBluetoothAdvertiseArg) { result: Result ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(UniversalBlePigeonUtils.wrapError(error))
@@ -1078,6 +1529,100 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler(null)
}
}
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPeripheralSupported$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.isPeripheralSupported())
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startPeripheral$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val configArg = args[0] as UniversalBlePeripheralConfig
+ api.startPeripheral(configArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(UniversalBlePigeonUtils.wrapError(error))
+ } else {
+ reply.reply(UniversalBlePigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopPeripheral$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ api.stopPeripheral{ result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(UniversalBlePigeonUtils.wrapError(error))
+ } else {
+ reply.reply(UniversalBlePigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.updatePeripheralCharacteristicValue$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val serviceArg = args[0] as String
+ val characteristicArg = args[1] as String
+ val valueArg = args[2] as ByteArray
+ api.updatePeripheralCharacteristicValue(serviceArg, characteristicArg, valueArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(UniversalBlePigeonUtils.wrapError(error))
+ } else {
+ reply.reply(UniversalBlePigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.notifyPeripheralCharacteristic$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val serviceArg = args[0] as String
+ val characteristicArg = args[1] as String
+ val valueArg = args[2] as ByteArray
+ val indicateArg = args[3] as Boolean
+ api.notifyPeripheralCharacteristic(serviceArg, characteristicArg, valueArg, indicateArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(UniversalBlePigeonUtils.wrapError(error))
+ } else {
+ reply.reply(UniversalBlePigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
run {
val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -1125,7 +1670,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger,
}
} else {
callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
- }
+ }
}
}
fun onPairStateChange(deviceIdArg: String, isPairedArg: Boolean, errorArg: String?, callback: (Result) -> Unit)
@@ -1142,7 +1687,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger,
}
} else {
callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
- }
+ }
}
}
fun onScanResult(resultArg: UniversalBleScanResult, callback: (Result) -> Unit)
@@ -1159,7 +1704,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger,
}
} else {
callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
- }
+ }
}
}
fun onValueChanged(deviceIdArg: String, characteristicIdArg: String, valueArg: ByteArray, timestampArg: Long?, callback: (Result) -> Unit)
@@ -1176,7 +1721,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger,
}
} else {
callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
- }
+ }
}
}
fun onConnectionChanged(deviceIdArg: String, connectedArg: Boolean, errorArg: String?, callback: (Result) -> Unit)
@@ -1193,7 +1738,58 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger,
}
} else {
callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
- }
+ }
+ }
+ }
+ fun onPeripheralConnectionChanged(deviceIdArg: String, connectedArg: Boolean, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPeripheralConnectionChanged$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(deviceIdArg, connectedArg)) {
+ if (it is List<*>) {
+ if (it.size > 1) {
+ callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
+ } else {
+ callback(Result.success(Unit))
+ }
+ } else {
+ callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+ fun onPeripheralWrite(eventArg: UniversalBlePeripheralWriteEvent, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPeripheralWrite$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(eventArg)) {
+ if (it is List<*>) {
+ if (it.size > 1) {
+ callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
+ } else {
+ callback(Result.success(Unit))
+ }
+ } else {
+ callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+ fun onPeripheralSubscriptionChanged(deviceIdArg: String, serviceArg: String, characteristicArg: String, subscribedArg: Boolean, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPeripheralSubscriptionChanged$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(deviceIdArg, serviceArg, characteristicArg, subscribedArg)) {
+ if (it is List<*>) {
+ if (it.size > 1) {
+ callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
+ } else {
+ callback(Result.success(Unit))
+ }
+ } else {
+ callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
+ }
}
}
}
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 beac594..36dd956 100644
--- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt
+++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt
@@ -9,9 +9,16 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
+import android.bluetooth.BluetoothGattServer
+import android.bluetooth.BluetoothGattServerCallback
+import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothStatusCodes
+import android.bluetooth.le.AdvertiseCallback
+import android.bluetooth.le.AdvertiseData
+import android.bluetooth.le.AdvertiseSettings
+import android.bluetooth.le.BluetoothLeAdvertiser
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
@@ -24,6 +31,7 @@ import android.content.IntentFilter
import android.os.Build
import android.os.Handler
import android.os.Looper
+import android.os.ParcelUuid
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -63,6 +71,12 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
private val rssiResultFutureList = mutableListOf()
private val autoConnectDevices = mutableSetOf()
+ private var peripheralGattServer: BluetoothGattServer? = null
+ private var peripheralAdvertiser: BluetoothLeAdvertiser? = null
+ private var peripheralPendingStartCallback: ((Result) -> Unit)? = null
+ private val peripheralConnectedDevices = mutableSetOf()
+ private val peripheralCharacteristics = mutableMapOf()
+
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this)
callbackChannel = UniversalBleCallbackChannel(flutterPluginBinding.binaryMessenger)
@@ -84,6 +98,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback)
+ stopPeripheralInternal()
context.unregisterReceiver(broadcastReceiver)
callbackChannel = null
mainThreadHandler = null
@@ -99,12 +114,19 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
)
}
- override fun hasPermissions(withAndroidFineLocation: Boolean): Boolean {
- return permissionHandler?.hasPermissions(withAndroidFineLocation) ?: false
+ override fun hasPermissions(
+ withAndroidFineLocation: Boolean,
+ withAndroidBluetoothAdvertise: Boolean,
+ ): Boolean {
+ return permissionHandler?.hasPermissions(
+ withAndroidFineLocation,
+ withAndroidBluetoothAdvertise,
+ ) ?: false
}
override fun requestPermissions(
withAndroidFineLocation: Boolean,
+ withAndroidBluetoothAdvertise: Boolean,
callback: (Result) -> Unit,
) {
if (permissionHandler == null) {
@@ -117,7 +139,11 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
)
)
}
- permissionHandler?.requestPermissions(withAndroidFineLocation, callback)
+ permissionHandler?.requestPermissions(
+ withAndroidFineLocation,
+ withAndroidBluetoothAdvertise,
+ callback,
+ )
}
override fun enableBluetooth(callback: (Result) -> Unit) {
@@ -801,6 +827,319 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
+ override fun isPeripheralSupported(): Boolean {
+ val adapter = bluetoothManager.adapter ?: return false
+ return adapter.isEnabled && adapter.isMultipleAdvertisementSupported
+ }
+
+ override fun startPeripheral(
+ config: UniversalBlePeripheralConfig,
+ callback: (Result) -> Unit,
+ ) {
+ val adapter = bluetoothManager.adapter
+ if (adapter == null || !adapter.isEnabled) {
+ callback(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.BLUETOOTH_NOT_ENABLED,
+ "Bluetooth not enabled"
+ )
+ )
+ )
+ return
+ }
+ if (!adapter.isMultipleAdvertisementSupported) {
+ callback(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.NOT_SUPPORTED,
+ "BLE peripheral advertising is not supported on this device"
+ )
+ )
+ )
+ return
+ }
+ if (peripheralPendingStartCallback != null) {
+ callback(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.OPERATION_IN_PROGRESS,
+ "Peripheral start already in progress"
+ )
+ )
+ )
+ return
+ }
+
+ stopPeripheralInternal()
+
+ val server = bluetoothManager.openGattServer(context, peripheralGattServerCallback)
+ if (server == null) {
+ callback(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.FAILED,
+ "Unable to open GATT server"
+ )
+ )
+ )
+ return
+ }
+
+ try {
+ peripheralCharacteristics.clear()
+ config.services.forEach { serviceConfig ->
+ val service = BluetoothGattService(
+ UUID.fromString(serviceConfig.uuid),
+ BluetoothGattService.SERVICE_TYPE_PRIMARY,
+ )
+ serviceConfig.characteristics.forEach { characteristicConfig ->
+ val characteristic = BluetoothGattCharacteristic(
+ UUID.fromString(characteristicConfig.uuid),
+ characteristicConfig.properties.toPeripheralPropertyFlags(),
+ characteristicConfig.permissions.toPeripheralPermissionFlags(),
+ )
+ @Suppress("DEPRECATION")
+ characteristic.value = characteristicConfig.initialValue ?: ByteArray(0)
+ characteristicConfig.descriptors.forEach { descriptorConfig ->
+ val descriptor = BluetoothGattDescriptor(
+ UUID.fromString(descriptorConfig.uuid),
+ descriptorConfig.permissions.toPeripheralPermissionFlags(),
+ )
+ @Suppress("DEPRECATION")
+ descriptor.value = descriptorConfig.initialValue ?: ByteArray(0)
+ characteristic.addDescriptor(descriptor)
+ }
+ if (characteristicConfig.properties.any {
+ it == CharacteristicProperty.Notify.value ||
+ it == CharacteristicProperty.Indicate.value
+ } && characteristic.getDescriptor(ccdCharacteristic) == null
+ ) {
+ characteristic.addDescriptor(
+ BluetoothGattDescriptor(
+ ccdCharacteristic,
+ BluetoothGattDescriptor.PERMISSION_READ or
+ BluetoothGattDescriptor.PERMISSION_WRITE,
+ )
+ )
+ }
+ service.addCharacteristic(characteristic)
+ peripheralCharacteristics[peripheralCharacteristicKey(serviceConfig.uuid, characteristicConfig.uuid)] = characteristic
+ }
+ if (!server.addService(service)) {
+ throw createFlutterError(
+ UniversalBleErrorCode.FAILED,
+ "Failed to add GATT service ${serviceConfig.uuid}"
+ )
+ }
+ }
+ } catch (e: FlutterError) {
+ server.close()
+ peripheralCharacteristics.clear()
+ callback(Result.failure(e))
+ return
+ } catch (e: Exception) {
+ server.close()
+ peripheralCharacteristics.clear()
+ callback(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.FAILED,
+ "Failed to create GATT services",
+ e.toString()
+ )
+ )
+ )
+ return
+ }
+
+ peripheralGattServer = server
+ try {
+ adapter.name = config.advertisedName
+ } catch (_: SecurityException) {
+ }
+
+ val advertiser = adapter.bluetoothLeAdvertiser
+ if (advertiser == null) {
+ stopPeripheralInternal()
+ callback(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.NOT_SUPPORTED,
+ "BLE advertiser not available"
+ )
+ )
+ )
+ return
+ }
+
+ val settings = AdvertiseSettings.Builder()
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
+ .setConnectable(true)
+ .setTimeout(0)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+ .build()
+ val dataBuilder = AdvertiseData.Builder().setIncludeDeviceName(true)
+ config.services.forEach { service ->
+ dataBuilder.addServiceUuid(ParcelUuid(UUID.fromString(service.uuid)))
+ }
+ peripheralAdvertiser = advertiser
+ peripheralPendingStartCallback = callback
+ advertiser.startAdvertising(settings, dataBuilder.build(), peripheralAdvertiseCallback)
+ }
+
+ override fun stopPeripheral(callback: (Result) -> Unit) {
+ peripheralPendingStartCallback?.invoke(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.OPERATION_CANCELLED,
+ "Peripheral start cancelled"
+ )
+ )
+ )
+ peripheralPendingStartCallback = null
+ stopPeripheralInternal()
+ callback(Result.success(Unit))
+ }
+
+ override fun updatePeripheralCharacteristicValue(
+ service: String,
+ characteristic: String,
+ value: ByteArray,
+ callback: (Result) -> Unit,
+ ) {
+ val gattCharacteristic = peripheralCharacteristics[peripheralCharacteristicKey(service, characteristic)]
+ if (gattCharacteristic == null) {
+ callback(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND,
+ "Unknown peripheral characteristic $characteristic"
+ )
+ )
+ )
+ return
+ }
+ @Suppress("DEPRECATION")
+ gattCharacteristic.value = value
+ callback(Result.success(Unit))
+ }
+
+ override fun notifyPeripheralCharacteristic(
+ service: String,
+ characteristic: String,
+ value: ByteArray,
+ indicate: Boolean,
+ callback: (Result) -> Unit,
+ ) {
+ val server = peripheralGattServer
+ val gattCharacteristic = peripheralCharacteristics[peripheralCharacteristicKey(service, characteristic)]
+ if (server == null || gattCharacteristic == null) {
+ callback(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND,
+ "Unknown peripheral characteristic $characteristic"
+ )
+ )
+ )
+ return
+ }
+ @Suppress("DEPRECATION")
+ gattCharacteristic.value = value
+ val devices = synchronized(peripheralConnectedDevices) {
+ peripheralConnectedDevices.toList()
+ }
+ devices.forEach { device ->
+ server.notifyCharacteristicChanged(device, gattCharacteristic, indicate)
+ }
+ callback(Result.success(Unit))
+ }
+
+ private fun stopPeripheralInternal() {
+ peripheralAdvertiser?.stopAdvertising(peripheralAdvertiseCallback)
+ peripheralAdvertiser = null
+ peripheralGattServer?.close()
+ peripheralGattServer = null
+ synchronized(peripheralConnectedDevices) {
+ peripheralConnectedDevices.clear()
+ }
+ peripheralCharacteristics.clear()
+ }
+
+ private fun peripheralCharacteristicKey(service: String, characteristic: String): String {
+ return "${service.lowercase()}|${characteristic.lowercase()}"
+ }
+
+ private fun List.toPeripheralPropertyFlags(): Int {
+ var flags = 0
+ forEach { property ->
+ when (CharacteristicProperty.entries.firstOrNull { it.value == property }) {
+ CharacteristicProperty.Broadcast -> flags = flags or BluetoothGattCharacteristic.PROPERTY_BROADCAST
+ CharacteristicProperty.Read -> flags = flags or BluetoothGattCharacteristic.PROPERTY_READ
+ CharacteristicProperty.WriteWithoutResponse -> flags = flags or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
+ CharacteristicProperty.Write -> flags = flags or BluetoothGattCharacteristic.PROPERTY_WRITE
+ CharacteristicProperty.Notify -> flags = flags or BluetoothGattCharacteristic.PROPERTY_NOTIFY
+ CharacteristicProperty.Indicate -> flags = flags or BluetoothGattCharacteristic.PROPERTY_INDICATE
+ CharacteristicProperty.AuthenticatedSignedWrites -> flags = flags or BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE
+ CharacteristicProperty.ExtendedProperties -> flags = flags or BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS
+ null -> {}
+ }
+ }
+ return flags
+ }
+
+ private fun List.toPeripheralPermissionFlags(): Int {
+ var flags = 0
+ forEach { permission ->
+ when (permission) {
+ 0L -> flags = flags or BluetoothGattCharacteristic.PERMISSION_READ
+ 1L -> flags = flags or BluetoothGattCharacteristic.PERMISSION_WRITE
+ }
+ }
+ return flags
+ }
+
+ private fun callbackPeripheralConnection(deviceId: String, connected: Boolean) {
+ mainThreadHandler?.post {
+ callbackChannel?.onPeripheralConnectionChanged(deviceId, connected) {}
+ }
+ }
+
+ private fun callbackPeripheralSubscription(
+ deviceId: String,
+ service: String,
+ characteristic: String,
+ subscribed: Boolean,
+ ) {
+ mainThreadHandler?.post {
+ callbackChannel?.onPeripheralSubscriptionChanged(
+ deviceId,
+ service,
+ characteristic,
+ subscribed,
+ ) {}
+ }
+ }
+
+ private fun callbackPeripheralWrite(
+ deviceId: String,
+ service: String,
+ characteristic: String,
+ value: ByteArray,
+ ) {
+ mainThreadHandler?.post {
+ callbackChannel?.onPeripheralWrite(
+ UniversalBlePeripheralWriteEvent(
+ deviceId,
+ service,
+ characteristic,
+ value,
+ )
+ ) {}
+ }
+ }
+
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
val deviceId = gatt?.device?.address ?: return
mtuResultFutureList.removeAll {
@@ -1191,6 +1530,157 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
+ private val peripheralAdvertiseCallback = object : AdvertiseCallback() {
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
+ peripheralPendingStartCallback?.invoke(Result.success(Unit))
+ peripheralPendingStartCallback = null
+ }
+
+ override fun onStartFailure(errorCode: Int) {
+ peripheralPendingStartCallback?.invoke(
+ Result.failure(
+ createFlutterError(
+ UniversalBleErrorCode.FAILED,
+ "BLE advertising failed with code $errorCode",
+ errorCode.toString()
+ )
+ )
+ )
+ peripheralPendingStartCallback = null
+ stopPeripheralInternal()
+ }
+ }
+
+ private val peripheralGattServerCallback = object : BluetoothGattServerCallback() {
+ override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ synchronized(peripheralConnectedDevices) {
+ peripheralConnectedDevices.add(device)
+ }
+ callbackPeripheralConnection(device.address, true)
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ synchronized(peripheralConnectedDevices) {
+ peripheralConnectedDevices.remove(device)
+ }
+ callbackPeripheralConnection(device.address, false)
+ }
+ }
+
+ override fun onCharacteristicReadRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ offset: Int,
+ characteristic: BluetoothGattCharacteristic,
+ ) {
+ @Suppress("DEPRECATION")
+ val value = characteristic.value ?: ByteArray(0)
+ if (offset > value.size) {
+ peripheralGattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_INVALID_OFFSET,
+ offset,
+ null,
+ )
+ return
+ }
+ peripheralGattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ value.copyOfRange(offset, value.size),
+ )
+ }
+
+ override fun onCharacteristicWriteRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ characteristic: BluetoothGattCharacteristic,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray,
+ ) {
+ @Suppress("DEPRECATION")
+ characteristic.value = value
+ if (responseNeeded) {
+ peripheralGattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ null,
+ )
+ }
+ callbackPeripheralWrite(
+ device.address,
+ characteristic.service.uuid.toString(),
+ characteristic.uuid.toString(),
+ value,
+ )
+ }
+
+ override fun onDescriptorReadRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ offset: Int,
+ descriptor: BluetoothGattDescriptor,
+ ) {
+ @Suppress("DEPRECATION")
+ val value = descriptor.value ?: ByteArray(0)
+ if (offset > value.size) {
+ peripheralGattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_INVALID_OFFSET,
+ offset,
+ null,
+ )
+ return
+ }
+ peripheralGattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ value.copyOfRange(offset, value.size),
+ )
+ }
+
+ override fun onDescriptorWriteRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ descriptor: BluetoothGattDescriptor,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray,
+ ) {
+ @Suppress("DEPRECATION")
+ descriptor.value = value
+ if (responseNeeded) {
+ peripheralGattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ null,
+ )
+ }
+ if (descriptor.uuid == ccdCharacteristic) {
+ val subscribed = value.contentEquals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) ||
+ value.contentEquals(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)
+ callbackPeripheralSubscription(
+ device.address,
+ descriptor.characteristic.service.uuid.toString(),
+ descriptor.characteristic.uuid.toString(),
+ subscribed,
+ )
+ }
+ }
+ }
+
override fun onConnectionStateChange(
gatt: BluetoothGatt,
diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift
index dc40b39..7dd1cd5 100644
--- a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift
+++ b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift
@@ -1,4 +1,4 @@
-// Autogenerated from Pigeon (v26.1.4), do not edit directly.
+// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -50,7 +50,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
- "\(type(of: error))",
+ "\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
@@ -68,6 +68,19 @@ private func nilOrValue(_ value: Any?) -> T? {
return value as! T?
}
+private func doubleEqualsUniversalBle(_ lhs: Double, _ rhs: Double) -> Bool {
+ return (lhs.isNaN && rhs.isNaN) || lhs == rhs
+}
+
+private func doubleHashUniversalBle(_ value: Double, _ hasher: inout Hasher) {
+ if value.isNaN {
+ hasher.combine(0x7FF8000000000000)
+ } else {
+ // Normalize -0.0 to 0.0
+ hasher.combine(value == 0 ? 0 : value)
+ }
+}
+
func deepEqualsUniversalBle(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
@@ -78,59 +91,92 @@ func deepEqualsUniversalBle(_ lhs: Any?, _ rhs: Any?) -> Bool {
case (nil, _), (_, nil):
return false
+ case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
+ return true
+
case is (Void, Void):
return true
- case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
- return cleanLhsHashable == cleanRhsHashable
-
- case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
- guard cleanLhsArray.count == cleanRhsArray.count else { return false }
- for (index, element) in cleanLhsArray.enumerated() {
- if !deepEqualsUniversalBle(element, cleanRhsArray[index]) {
+ case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
+ guard lhsArray.count == rhsArray.count else { return false }
+ for (index, element) in lhsArray.enumerated() {
+ if !deepEqualsUniversalBle(element, rhsArray[index]) {
return false
}
}
return true
- case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
- guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
- for (key, cleanLhsValue) in cleanLhsDictionary {
- guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
- if !deepEqualsUniversalBle(cleanLhsValue, cleanRhsDictionary[key]!) {
+ case (let lhsArray, let rhsArray) as ([Double], [Double]):
+ guard lhsArray.count == rhsArray.count else { return false }
+ for (index, element) in lhsArray.enumerated() {
+ if !doubleEqualsUniversalBle(element, rhsArray[index]) {
return false
}
}
return true
+ case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
+ guard lhsDictionary.count == rhsDictionary.count else { return false }
+ for (lhsKey, lhsValue) in lhsDictionary {
+ var found = false
+ for (rhsKey, rhsValue) in rhsDictionary {
+ if deepEqualsUniversalBle(lhsKey, rhsKey) {
+ if deepEqualsUniversalBle(lhsValue, rhsValue) {
+ found = true
+ break
+ } else {
+ return false
+ }
+ }
+ }
+ if !found { return false }
+ }
+ return true
+
+ case (let lhs as Double, let rhs as Double):
+ return doubleEqualsUniversalBle(lhs, rhs)
+
+ case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
+ return lhsHashable == rhsHashable
+
default:
- // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashUniversalBle(value: Any?, hasher: inout Hasher) {
- if let valueList = value as? [AnyHashable] {
- for item in valueList { deepHashUniversalBle(value: item, hasher: &hasher) }
- return
- }
-
- if let valueDict = value as? [AnyHashable: AnyHashable] {
- for key in valueDict.keys {
- hasher.combine(key)
- deepHashUniversalBle(value: valueDict[key]!, hasher: &hasher)
+ let cleanValue = nilOrValue(value) as Any?
+ if let cleanValue = cleanValue {
+ if let doubleValue = cleanValue as? Double {
+ doubleHashUniversalBle(doubleValue, &hasher)
+ } else if let valueList = cleanValue as? [Any?] {
+ for item in valueList {
+ deepHashUniversalBle(value: item, hasher: &hasher)
+ }
+ } else if let valueList = cleanValue as? [Double] {
+ for item in valueList {
+ doubleHashUniversalBle(item, &hasher)
+ }
+ } else if let valueDict = cleanValue as? [AnyHashable: Any?] {
+ var result = 0
+ for (key, value) in valueDict {
+ var entryKeyHasher = Hasher()
+ deepHashUniversalBle(value: key, hasher: &entryKeyHasher)
+ var entryValueHasher = Hasher()
+ deepHashUniversalBle(value: value, hasher: &entryValueHasher)
+ result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
+ }
+ hasher.combine(result)
+ } else if let hashableValue = cleanValue as? AnyHashable {
+ hasher.combine(hashableValue)
+ } else {
+ hasher.combine(String(describing: cleanValue))
}
- return
+ } else {
+ hasher.combine(0)
}
-
- if let hashableValue = value as? AnyHashable {
- hasher.combine(hashableValue.hashValue)
- }
-
- return hasher.combine(String(describing: value))
}
-
enum UniversalBleLogLevel: Int {
case none = 0
@@ -261,9 +307,22 @@ struct UniversalBleScanResult: Hashable {
]
}
static func == (lhs: UniversalBleScanResult, rhs: UniversalBleScanResult) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.deviceId, rhs.deviceId) && deepEqualsUniversalBle(lhs.name, rhs.name) && deepEqualsUniversalBle(lhs.isPaired, rhs.isPaired) && deepEqualsUniversalBle(lhs.rssi, rhs.rssi) && deepEqualsUniversalBle(lhs.manufacturerDataList, rhs.manufacturerDataList) && deepEqualsUniversalBle(lhs.serviceData, rhs.serviceData) && deepEqualsUniversalBle(lhs.services, rhs.services) && deepEqualsUniversalBle(lhs.timestamp, rhs.timestamp)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("UniversalBleScanResult")
+ deepHashUniversalBle(value: deviceId, hasher: &hasher)
+ deepHashUniversalBle(value: name, hasher: &hasher)
+ deepHashUniversalBle(value: isPaired, hasher: &hasher)
+ deepHashUniversalBle(value: rssi, hasher: &hasher)
+ deepHashUniversalBle(value: manufacturerDataList, hasher: &hasher)
+ deepHashUniversalBle(value: serviceData, hasher: &hasher)
+ deepHashUniversalBle(value: services, hasher: &hasher)
+ deepHashUniversalBle(value: timestamp, hasher: &hasher)
}
}
@@ -290,9 +349,16 @@ struct UniversalBleService: Hashable {
]
}
static func == (lhs: UniversalBleService, rhs: UniversalBleService) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.characteristics, rhs.characteristics)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("UniversalBleService")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ deepHashUniversalBle(value: characteristics, hasher: &hasher)
}
}
@@ -323,9 +389,17 @@ struct UniversalBleCharacteristic: Hashable {
]
}
static func == (lhs: UniversalBleCharacteristic, rhs: UniversalBleCharacteristic) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.properties, rhs.properties) && deepEqualsUniversalBle(lhs.descriptors, rhs.descriptors)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("UniversalBleCharacteristic")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ deepHashUniversalBle(value: properties, hasher: &hasher)
+ deepHashUniversalBle(value: descriptors, hasher: &hasher)
}
}
@@ -348,9 +422,225 @@ struct UniversalBleDescriptor: Hashable {
]
}
static func == (lhs: UniversalBleDescriptor, rhs: UniversalBleDescriptor) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.uuid, rhs.uuid)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("UniversalBleDescriptor")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct UniversalBlePeripheralConfig: Hashable {
+ var advertisedName: String
+ var services: [UniversalBlePeripheralService]
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBlePeripheralConfig? {
+ let advertisedName = pigeonVar_list[0] as! String
+ let services = pigeonVar_list[1] as! [UniversalBlePeripheralService]
+
+ return UniversalBlePeripheralConfig(
+ advertisedName: advertisedName,
+ services: services
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ advertisedName,
+ services,
+ ]
+ }
+ static func == (lhs: UniversalBlePeripheralConfig, rhs: UniversalBlePeripheralConfig) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.advertisedName, rhs.advertisedName) && deepEqualsUniversalBle(lhs.services, rhs.services)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("UniversalBlePeripheralConfig")
+ deepHashUniversalBle(value: advertisedName, hasher: &hasher)
+ deepHashUniversalBle(value: services, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct UniversalBlePeripheralService: Hashable {
+ var uuid: String
+ var characteristics: [UniversalBlePeripheralCharacteristic]
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBlePeripheralService? {
+ let uuid = pigeonVar_list[0] as! String
+ let characteristics = pigeonVar_list[1] as! [UniversalBlePeripheralCharacteristic]
+
+ return UniversalBlePeripheralService(
+ uuid: uuid,
+ characteristics: characteristics
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ uuid,
+ characteristics,
+ ]
+ }
+ static func == (lhs: UniversalBlePeripheralService, rhs: UniversalBlePeripheralService) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.characteristics, rhs.characteristics)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("UniversalBlePeripheralService")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ deepHashUniversalBle(value: characteristics, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct UniversalBlePeripheralCharacteristic: Hashable {
+ var uuid: String
+ var properties: [Int64]
+ var permissions: [Int64]
+ var descriptors: [UniversalBlePeripheralDescriptor]
+ var initialValue: FlutterStandardTypedData? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBlePeripheralCharacteristic? {
+ let uuid = pigeonVar_list[0] as! String
+ let properties = pigeonVar_list[1] as! [Int64]
+ let permissions = pigeonVar_list[2] as! [Int64]
+ let descriptors = pigeonVar_list[3] as! [UniversalBlePeripheralDescriptor]
+ let initialValue: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[4])
+
+ return UniversalBlePeripheralCharacteristic(
+ uuid: uuid,
+ properties: properties,
+ permissions: permissions,
+ descriptors: descriptors,
+ initialValue: initialValue
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ uuid,
+ properties,
+ permissions,
+ descriptors,
+ initialValue,
+ ]
+ }
+ static func == (lhs: UniversalBlePeripheralCharacteristic, rhs: UniversalBlePeripheralCharacteristic) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.properties, rhs.properties) && deepEqualsUniversalBle(lhs.permissions, rhs.permissions) && deepEqualsUniversalBle(lhs.descriptors, rhs.descriptors) && deepEqualsUniversalBle(lhs.initialValue, rhs.initialValue)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("UniversalBlePeripheralCharacteristic")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ deepHashUniversalBle(value: properties, hasher: &hasher)
+ deepHashUniversalBle(value: permissions, hasher: &hasher)
+ deepHashUniversalBle(value: descriptors, hasher: &hasher)
+ deepHashUniversalBle(value: initialValue, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct UniversalBlePeripheralDescriptor: Hashable {
+ var uuid: String
+ var permissions: [Int64]
+ var initialValue: FlutterStandardTypedData? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBlePeripheralDescriptor? {
+ let uuid = pigeonVar_list[0] as! String
+ let permissions = pigeonVar_list[1] as! [Int64]
+ let initialValue: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[2])
+
+ return UniversalBlePeripheralDescriptor(
+ uuid: uuid,
+ permissions: permissions,
+ initialValue: initialValue
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ uuid,
+ permissions,
+ initialValue,
+ ]
+ }
+ static func == (lhs: UniversalBlePeripheralDescriptor, rhs: UniversalBlePeripheralDescriptor) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.permissions, rhs.permissions) && deepEqualsUniversalBle(lhs.initialValue, rhs.initialValue)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("UniversalBlePeripheralDescriptor")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ deepHashUniversalBle(value: permissions, hasher: &hasher)
+ deepHashUniversalBle(value: initialValue, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct UniversalBlePeripheralWriteEvent: Hashable {
+ var deviceId: String
+ var service: String
+ var characteristic: String
+ var value: FlutterStandardTypedData
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBlePeripheralWriteEvent? {
+ let deviceId = pigeonVar_list[0] as! String
+ let service = pigeonVar_list[1] as! String
+ let characteristic = pigeonVar_list[2] as! String
+ let value = pigeonVar_list[3] as! FlutterStandardTypedData
+
+ return UniversalBlePeripheralWriteEvent(
+ deviceId: deviceId,
+ service: service,
+ characteristic: characteristic,
+ value: value
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ deviceId,
+ service,
+ characteristic,
+ value,
+ ]
+ }
+ static func == (lhs: UniversalBlePeripheralWriteEvent, rhs: UniversalBlePeripheralWriteEvent) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.deviceId, rhs.deviceId) && deepEqualsUniversalBle(lhs.service, rhs.service) && deepEqualsUniversalBle(lhs.characteristic, rhs.characteristic) && deepEqualsUniversalBle(lhs.value, rhs.value)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("UniversalBlePeripheralWriteEvent")
+ deepHashUniversalBle(value: deviceId, hasher: &hasher)
+ deepHashUniversalBle(value: service, hasher: &hasher)
+ deepHashUniversalBle(value: characteristic, hasher: &hasher)
+ deepHashUniversalBle(value: value, hasher: &hasher)
}
}
@@ -388,9 +678,17 @@ struct AndroidOptions: Hashable {
]
}
static func == (lhs: AndroidOptions, rhs: AndroidOptions) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.requestLocationPermission, rhs.requestLocationPermission) && deepEqualsUniversalBle(lhs.scanMode, rhs.scanMode) && deepEqualsUniversalBle(lhs.reportDelayMillis, rhs.reportDelayMillis)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("AndroidOptions")
+ deepHashUniversalBle(value: requestLocationPermission, hasher: &hasher)
+ deepHashUniversalBle(value: scanMode, hasher: &hasher)
+ deepHashUniversalBle(value: reportDelayMillis, hasher: &hasher)
}
}
@@ -413,9 +711,15 @@ struct UniversalScanConfig: Hashable {
]
}
static func == (lhs: UniversalScanConfig, rhs: UniversalScanConfig) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.android, rhs.android)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("UniversalScanConfig")
+ deepHashUniversalBle(value: android, hasher: &hasher)
}
}
@@ -448,9 +752,17 @@ struct UniversalScanFilter: Hashable {
]
}
static func == (lhs: UniversalScanFilter, rhs: UniversalScanFilter) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.withServices, rhs.withServices) && deepEqualsUniversalBle(lhs.withNamePrefix, rhs.withNamePrefix) && deepEqualsUniversalBle(lhs.withManufacturerData, rhs.withManufacturerData)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("UniversalScanFilter")
+ deepHashUniversalBle(value: withServices, hasher: &hasher)
+ deepHashUniversalBle(value: withNamePrefix, hasher: &hasher)
+ deepHashUniversalBle(value: withManufacturerData, hasher: &hasher)
}
}
@@ -481,9 +793,17 @@ struct UniversalManufacturerDataFilter: Hashable {
]
}
static func == (lhs: UniversalManufacturerDataFilter, rhs: UniversalManufacturerDataFilter) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.companyIdentifier, rhs.companyIdentifier) && deepEqualsUniversalBle(lhs.data, rhs.data) && deepEqualsUniversalBle(lhs.mask, rhs.mask)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("UniversalManufacturerDataFilter")
+ deepHashUniversalBle(value: companyIdentifier, hasher: &hasher)
+ deepHashUniversalBle(value: data, hasher: &hasher)
+ deepHashUniversalBle(value: mask, hasher: &hasher)
}
}
@@ -510,9 +830,16 @@ struct UniversalManufacturerData: Hashable {
]
}
static func == (lhs: UniversalManufacturerData, rhs: UniversalManufacturerData) -> Bool {
- return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) }
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.companyIdentifier, rhs.companyIdentifier) && deepEqualsUniversalBle(lhs.data, rhs.data)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("UniversalManufacturerData")
+ deepHashUniversalBle(value: companyIdentifier, hasher: &hasher)
+ deepHashUniversalBle(value: data, hasher: &hasher)
}
}
@@ -546,14 +873,24 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader {
case 135:
return UniversalBleDescriptor.fromList(self.readValue() as! [Any?])
case 136:
- return AndroidOptions.fromList(self.readValue() as! [Any?])
+ return UniversalBlePeripheralConfig.fromList(self.readValue() as! [Any?])
case 137:
- return UniversalScanConfig.fromList(self.readValue() as! [Any?])
+ return UniversalBlePeripheralService.fromList(self.readValue() as! [Any?])
case 138:
- return UniversalScanFilter.fromList(self.readValue() as! [Any?])
+ return UniversalBlePeripheralCharacteristic.fromList(self.readValue() as! [Any?])
case 139:
- return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?])
+ return UniversalBlePeripheralDescriptor.fromList(self.readValue() as! [Any?])
case 140:
+ return UniversalBlePeripheralWriteEvent.fromList(self.readValue() as! [Any?])
+ case 141:
+ return AndroidOptions.fromList(self.readValue() as! [Any?])
+ case 142:
+ return UniversalScanConfig.fromList(self.readValue() as! [Any?])
+ case 143:
+ return UniversalScanFilter.fromList(self.readValue() as! [Any?])
+ case 144:
+ return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?])
+ case 145:
return UniversalManufacturerData.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
@@ -584,21 +921,36 @@ private class UniversalBlePigeonCodecWriter: FlutterStandardWriter {
} else if let value = value as? UniversalBleDescriptor {
super.writeByte(135)
super.writeValue(value.toList())
- } else if let value = value as? AndroidOptions {
+ } else if let value = value as? UniversalBlePeripheralConfig {
super.writeByte(136)
super.writeValue(value.toList())
- } else if let value = value as? UniversalScanConfig {
+ } else if let value = value as? UniversalBlePeripheralService {
super.writeByte(137)
super.writeValue(value.toList())
- } else if let value = value as? UniversalScanFilter {
+ } else if let value = value as? UniversalBlePeripheralCharacteristic {
super.writeByte(138)
super.writeValue(value.toList())
- } else if let value = value as? UniversalManufacturerDataFilter {
+ } else if let value = value as? UniversalBlePeripheralDescriptor {
super.writeByte(139)
super.writeValue(value.toList())
- } else if let value = value as? UniversalManufacturerData {
+ } else if let value = value as? UniversalBlePeripheralWriteEvent {
super.writeByte(140)
super.writeValue(value.toList())
+ } else if let value = value as? AndroidOptions {
+ super.writeByte(141)
+ super.writeValue(value.toList())
+ } else if let value = value as? UniversalScanConfig {
+ super.writeByte(142)
+ super.writeValue(value.toList())
+ } else if let value = value as? UniversalScanFilter {
+ super.writeByte(143)
+ super.writeValue(value.toList())
+ } else if let value = value as? UniversalManufacturerDataFilter {
+ super.writeByte(144)
+ super.writeValue(value.toList())
+ } else if let value = value as? UniversalManufacturerData {
+ super.writeByte(145)
+ super.writeValue(value.toList())
} else {
super.writeValue(value)
}
@@ -625,8 +977,8 @@ class UniversalBlePigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol UniversalBlePlatformChannel {
func getBluetoothAvailabilityState(completion: @escaping (Result) -> Void)
- func hasPermissions(withAndroidFineLocation: Bool) throws -> Bool
- func requestPermissions(withAndroidFineLocation: Bool, completion: @escaping (Result) -> Void)
+ func hasPermissions(withAndroidFineLocation: Bool, withAndroidBluetoothAdvertise: Bool) throws -> Bool
+ func requestPermissions(withAndroidFineLocation: Bool, withAndroidBluetoothAdvertise: Bool, completion: @escaping (Result) -> Void)
func enableBluetooth(completion: @escaping (Result) -> Void)
func disableBluetooth(completion: @escaping (Result) -> Void)
func startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) throws
@@ -646,6 +998,11 @@ protocol UniversalBlePlatformChannel {
func getConnectionState(deviceId: String) throws -> Int64
func readRssi(deviceId: String, completion: @escaping (Result) -> Void)
func requestConnectionPriority(deviceId: String, priority: Int64, completion: @escaping (Result) -> Void)
+ func isPeripheralSupported() throws -> Bool
+ func startPeripheral(config: UniversalBlePeripheralConfig, completion: @escaping (Result) -> Void)
+ func stopPeripheral(completion: @escaping (Result) -> Void)
+ func updatePeripheralCharacteristicValue(service: String, characteristic: String, value: FlutterStandardTypedData, completion: @escaping (Result) -> Void)
+ func notifyPeripheralCharacteristic(service: String, characteristic: String, value: FlutterStandardTypedData, indicate: Bool, completion: @escaping (Result) -> Void)
func setLogLevel(logLevel: UniversalBleLogLevel) throws
}
@@ -675,8 +1032,9 @@ class UniversalBlePlatformChannelSetup {
hasPermissionsChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let withAndroidFineLocationArg = args[0] as! Bool
+ let withAndroidBluetoothAdvertiseArg = args[1] as! Bool
do {
- let result = try api.hasPermissions(withAndroidFineLocation: withAndroidFineLocationArg)
+ let result = try api.hasPermissions(withAndroidFineLocation: withAndroidFineLocationArg, withAndroidBluetoothAdvertise: withAndroidBluetoothAdvertiseArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
@@ -690,7 +1048,8 @@ class UniversalBlePlatformChannelSetup {
requestPermissionsChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let withAndroidFineLocationArg = args[0] as! Bool
- api.requestPermissions(withAndroidFineLocation: withAndroidFineLocationArg) { result in
+ let withAndroidBluetoothAdvertiseArg = args[1] as! Bool
+ api.requestPermissions(withAndroidFineLocation: withAndroidFineLocationArg, withAndroidBluetoothAdvertise: withAndroidBluetoothAdvertiseArg) { result in
switch result {
case .success:
reply(wrapResult(nil))
@@ -1017,6 +1376,90 @@ class UniversalBlePlatformChannelSetup {
} else {
requestConnectionPriorityChannel.setMessageHandler(nil)
}
+ let isPeripheralSupportedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPeripheralSupported\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ isPeripheralSupportedChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.isPeripheralSupported()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ isPeripheralSupportedChannel.setMessageHandler(nil)
+ }
+ let startPeripheralChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startPeripheral\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ startPeripheralChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let configArg = args[0] as! UniversalBlePeripheralConfig
+ api.startPeripheral(config: configArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ startPeripheralChannel.setMessageHandler(nil)
+ }
+ let stopPeripheralChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopPeripheral\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ stopPeripheralChannel.setMessageHandler { _, reply in
+ api.stopPeripheral { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ stopPeripheralChannel.setMessageHandler(nil)
+ }
+ let updatePeripheralCharacteristicValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.updatePeripheralCharacteristicValue\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ updatePeripheralCharacteristicValueChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let serviceArg = args[0] as! String
+ let characteristicArg = args[1] as! String
+ let valueArg = args[2] as! FlutterStandardTypedData
+ api.updatePeripheralCharacteristicValue(service: serviceArg, characteristic: characteristicArg, value: valueArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ updatePeripheralCharacteristicValueChannel.setMessageHandler(nil)
+ }
+ let notifyPeripheralCharacteristicChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.notifyPeripheralCharacteristic\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ notifyPeripheralCharacteristicChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let serviceArg = args[0] as! String
+ let characteristicArg = args[1] as! String
+ let valueArg = args[2] as! FlutterStandardTypedData
+ let indicateArg = args[3] as! Bool
+ api.notifyPeripheralCharacteristic(service: serviceArg, characteristic: characteristicArg, value: valueArg, indicate: indicateArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ notifyPeripheralCharacteristicChannel.setMessageHandler(nil)
+ }
let setLogLevelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
setLogLevelChannel.setMessageHandler { message, reply in
@@ -1043,6 +1486,9 @@ protocol UniversalBleCallbackChannelProtocol {
func onScanResult(result resultArg: UniversalBleScanResult, completion: @escaping (Result) -> Void)
func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, timestamp timestampArg: Int64?, completion: @escaping (Result) -> Void)
func onConnectionChanged(deviceId deviceIdArg: String, connected connectedArg: Bool, error errorArg: String?, completion: @escaping (Result) -> Void)
+ func onPeripheralConnectionChanged(deviceId deviceIdArg: String, connected connectedArg: Bool, completion: @escaping (Result) -> Void)
+ func onPeripheralWrite(event eventArg: UniversalBlePeripheralWriteEvent, completion: @escaping (Result) -> Void)
+ func onPeripheralSubscriptionChanged(deviceId deviceIdArg: String, service serviceArg: String, characteristic characteristicArg: String, subscribed subscribedArg: Bool, completion: @escaping (Result) -> Void)
}
class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol {
private let binaryMessenger: FlutterBinaryMessenger
@@ -1144,4 +1590,58 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol {
}
}
}
+ func onPeripheralConnectionChanged(deviceId deviceIdArg: String, connected connectedArg: Bool, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPeripheralConnectionChanged\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([deviceIdArg, connectedArg] as [Any?]) { response in
+ guard let listResponse = response as? [Any?] else {
+ completion(.failure(createConnectionError(withChannelName: channelName)))
+ return
+ }
+ if listResponse.count > 1 {
+ let code: String = listResponse[0] as! String
+ let message: String? = nilOrValue(listResponse[1])
+ let details: String? = nilOrValue(listResponse[2])
+ completion(.failure(PigeonError(code: code, message: message, details: details)))
+ } else {
+ completion(.success(()))
+ }
+ }
+ }
+ func onPeripheralWrite(event eventArg: UniversalBlePeripheralWriteEvent, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPeripheralWrite\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([eventArg] as [Any?]) { response in
+ guard let listResponse = response as? [Any?] else {
+ completion(.failure(createConnectionError(withChannelName: channelName)))
+ return
+ }
+ if listResponse.count > 1 {
+ let code: String = listResponse[0] as! String
+ let message: String? = nilOrValue(listResponse[1])
+ let details: String? = nilOrValue(listResponse[2])
+ completion(.failure(PigeonError(code: code, message: message, details: details)))
+ } else {
+ completion(.success(()))
+ }
+ }
+ }
+ func onPeripheralSubscriptionChanged(deviceId deviceIdArg: String, service serviceArg: String, characteristic characteristicArg: String, subscribed subscribedArg: Bool, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPeripheralSubscriptionChanged\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([deviceIdArg, serviceArg, characteristicArg, subscribedArg] as [Any?]) { response in
+ guard let listResponse = response as? [Any?] else {
+ completion(.failure(createConnectionError(withChannelName: channelName)))
+ return
+ }
+ if listResponse.count > 1 {
+ let code: String = listResponse[0] as! String
+ let message: String? = nilOrValue(listResponse[1])
+ let details: String? = nilOrValue(listResponse[2])
+ completion(.failure(PigeonError(code: code, message: message, details: details)))
+ } else {
+ completion(.success(()))
+ }
+ }
+ }
}
diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift
index 4f6b1fb..7005b68 100644
--- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift
+++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift
@@ -58,11 +58,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
}
}
- func hasPermissions(withAndroidFineLocation _: Bool) throws -> Bool {
+ func hasPermissions(withAndroidFineLocation _: Bool, withAndroidBluetoothAdvertise _: Bool) throws -> Bool {
return CBCentralManager.authorization == .allowedAlways
}
- func requestPermissions(withAndroidFineLocation _: Bool, completion: @escaping (Result) -> Void) {
+ func requestPermissions(
+ withAndroidFineLocation _: Bool,
+ withAndroidBluetoothAdvertise _: Bool,
+ completion: @escaping (Result) -> Void
+ ) {
if manager.state != .unknown {
completePermissionRequest(completion: completion)
} else {
@@ -379,6 +383,37 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
completion(.failure(createFlutterError(code: .notSupported, message: "requestConnectionPriority is not supported on Apple platforms")))
}
+ func isPeripheralSupported() throws -> Bool {
+ false
+ }
+
+ func startPeripheral(config _: UniversalBlePeripheralConfig, completion: @escaping (Result) -> Void) {
+ completion(.failure(createFlutterError(code: .notSupported, message: "BLE peripheral mode is not implemented on Apple platforms yet")))
+ }
+
+ func stopPeripheral(completion: @escaping (Result) -> Void) {
+ completion(.success(()))
+ }
+
+ func updatePeripheralCharacteristicValue(
+ service _: String,
+ characteristic _: String,
+ value _: FlutterStandardTypedData,
+ completion: @escaping (Result) -> Void
+ ) {
+ completion(.failure(createFlutterError(code: .notSupported, message: "BLE peripheral mode is not implemented on Apple platforms yet")))
+ }
+
+ func notifyPeripheralCharacteristic(
+ service _: String,
+ characteristic _: String,
+ value _: FlutterStandardTypedData,
+ indicate _: Bool,
+ completion: @escaping (Result) -> Void
+ ) {
+ completion(.failure(createFlutterError(code: .notSupported, message: "BLE peripheral mode is not implemented on Apple platforms yet")))
+ }
+
func readRssi(deviceId: String, completion: @escaping (Result) -> Void) {
UniversalBleLogger.shared.logDebug("READ_RSSI -> \(deviceId)")
guard let peripheral = deviceId.findPeripheral(manager: manager) else {
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 191b9de..b4ba912 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,7 @@
+
diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart
index 44a681a..4df4876 100644
--- a/example/lib/data/mock_universal_ble.dart
+++ b/example/lib/data/mock_universal_ble.dart
@@ -54,8 +54,7 @@ class MockUniversalBle extends UniversalBlePlatform {
}
@override
- Future> discoverServices(
- String deviceId, bool withDescriptors) async {
+ Future> discoverServices(String deviceId, bool withDescriptors) async {
return [_mockService];
}
@@ -87,11 +86,7 @@ class MockUniversalBle extends UniversalBlePlatform {
}
@override
- Future writeValue(
- String deviceId,
- String service,
- String characteristic,
- Uint8List value,
+ Future writeValue(String deviceId, String service, String characteristic, Uint8List value,
BleOutputProperty bleOutputProperty) async {
await Future.delayed(const Duration(milliseconds: 500));
_serviceValue = value;
@@ -110,8 +105,8 @@ class MockUniversalBle extends UniversalBlePlatform {
) async {}
@override
- Future setNotifiable(String deviceId, String service,
- String characteristic, BleInputProperty bleInputProperty) async {}
+ Future setNotifiable(String deviceId, String service, String characteristic,
+ BleInputProperty bleInputProperty) async {}
@override
Future isPaired(String deviceId) async {
@@ -141,7 +136,10 @@ class MockUniversalBle extends UniversalBlePlatform {
}
@override
- Future requestPermissions({bool withAndroidFineLocation = false}) {
+ Future requestPermissions({
+ bool withAndroidFineLocation = false,
+ bool withAndroidBluetoothAdvertise = false,
+ }) {
throw UnimplementedError();
}
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 7c0e28f..374f080 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -333,7 +333,7 @@ packages:
path: ".."
relative: true
source: path
- version: "1.2.0"
+ version: "1.3.0"
vector_math:
dependency: transitive
description:
diff --git a/lib/src/models/ble_peripheral.dart b/lib/src/models/ble_peripheral.dart
new file mode 100644
index 0000000..752a7fd
--- /dev/null
+++ b/lib/src/models/ble_peripheral.dart
@@ -0,0 +1,88 @@
+import 'dart:typed_data';
+
+import 'package:universal_ble/universal_ble.dart';
+
+enum BlePeripheralCharacteristicPermission { read, write }
+
+class BlePeripheralConfig {
+ BlePeripheralConfig({required this.advertisedName, required this.services});
+
+ final String advertisedName;
+ final List services;
+}
+
+class BlePeripheralService {
+ BlePeripheralService({required String uuid, required this.characteristics})
+ : uuid = BleUuidParser.string(uuid);
+
+ final String uuid;
+ final List characteristics;
+}
+
+class BlePeripheralCharacteristic {
+ BlePeripheralCharacteristic({
+ required String uuid,
+ required this.properties,
+ required this.permissions,
+ this.descriptors = const [],
+ Uint8List? initialValue,
+ }) : uuid = BleUuidParser.string(uuid),
+ initialValue = initialValue == null ? null : Uint8List.fromList(initialValue);
+
+ final String uuid;
+ final List properties;
+ final List permissions;
+ final List descriptors;
+ final Uint8List? initialValue;
+}
+
+class BlePeripheralDescriptor {
+ BlePeripheralDescriptor({
+ required String uuid,
+ required this.permissions,
+ Uint8List? initialValue,
+ }) : uuid = BleUuidParser.string(uuid),
+ initialValue = initialValue == null ? null : Uint8List.fromList(initialValue);
+
+ final String uuid;
+ final List permissions;
+ final Uint8List? initialValue;
+}
+
+class BlePeripheralWriteEvent {
+ BlePeripheralWriteEvent({
+ required this.deviceId,
+ required String service,
+ required String characteristic,
+ required Uint8List value,
+ }) : service = BleUuidParser.string(service),
+ characteristic = BleUuidParser.string(characteristic),
+ value = Uint8List.fromList(value);
+
+ final String deviceId;
+ final String service;
+ final String characteristic;
+ final Uint8List value;
+}
+
+class BlePeripheralConnectionEvent {
+ BlePeripheralConnectionEvent({required this.deviceId, required this.connected});
+
+ final String deviceId;
+ final bool connected;
+}
+
+class BlePeripheralSubscriptionEvent {
+ BlePeripheralSubscriptionEvent({
+ required this.deviceId,
+ required String service,
+ required String characteristic,
+ required this.subscribed,
+ }) : service = BleUuidParser.string(service),
+ characteristic = BleUuidParser.string(characteristic);
+
+ final String deviceId;
+ final String service;
+ final String characteristic;
+ final bool subscribed;
+}
diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart
index 7533772..6195762 100644
--- a/lib/src/models/model_exports.dart
+++ b/lib/src/models/model_exports.dart
@@ -13,3 +13,4 @@ export 'package:universal_ble/src/models/ble_device.dart';
export 'package:universal_ble/src/models/ble_command.dart';
export 'package:universal_ble/src/models/ble_capabilities.dart';
export 'package:universal_ble/src/models/ble_connection_priority.dart';
+export 'package:universal_ble/src/models/ble_peripheral.dart';
diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart
index 70627e6..555340b 100644
--- a/lib/src/universal_ble.dart
+++ b/lib/src/universal_ble.dart
@@ -14,8 +14,7 @@ class UniversalBle {
static final BleCommandQueue _bleCommandQueue = BleCommandQueue();
/// Set custom platform specific implementation (e.g. for testing).
- static void setInstance(UniversalBlePlatform instance) =>
- _platform = instance;
+ static void setInstance(UniversalBlePlatform instance) => _platform = instance;
/// Set global timeout for all commands.
/// Default timeout is 10 seconds.
@@ -47,40 +46,36 @@ class UniversalBle {
static Stream get scanStream => _platform.scanStream;
/// Bluetooth availability state stream
- static Stream get availabilityStream =>
- _platform.availabilityStream;
+ static Stream get availabilityStream => _platform.availabilityStream;
/// Connection stream of a device
- static Stream connectionStream(String deviceId) =>
- _platform.connectionStream(deviceId);
+ static Stream connectionStream(String deviceId) => _platform.connectionStream(deviceId);
/// Characteristic value stream
- static Stream characteristicValueStream(
- String deviceId,
- String characteristicId,
- ) => _platform.characteristicValueStream(deviceId, characteristicId);
+ static Stream characteristicValueStream(String deviceId, String characteristicId) =>
+ _platform.characteristicValueStream(deviceId, characteristicId);
/// Pairing state stream
- static Stream pairingStateStream(String deviceId) =>
- _platform.pairingStateStream(deviceId);
+ static Stream pairingStateStream(String deviceId) => _platform.pairingStateStream(deviceId);
/// Get Bluetooth availability state.
/// To be notified of updates, set [onAvailabilityChange] listener.
static Future getBluetoothAvailabilityState() async {
- return await _bleCommandQueue.queueCommand(
- () => _platform.getBluetoothAvailabilityState(),
- );
+ return await _bleCommandQueue.queueCommand(() => _platform.getBluetoothAvailabilityState());
}
/// Check if has permissions.
/// [withAndroidFineLocation] is used to check fine location permission on Android 12+ (API 31+).
+ /// [withAndroidBluetoothAdvertise] is used to check Android 12+ advertise permission for peripheral mode.
/// On Android lower than 12, this method will check location permission regardless of the [withAndroidFineLocation] value.
/// `Windows`, `Linux` and `Web` will always return true.
static Future hasPermissions({
bool withAndroidFineLocation = false,
+ bool withAndroidBluetoothAdvertise = false,
}) async {
return _platform.hasPermissions(
withAndroidFineLocation: withAndroidFineLocation,
+ withAndroidBluetoothAdvertise: withAndroidBluetoothAdvertise,
);
}
@@ -88,13 +83,16 @@ class UniversalBle {
/// if all permissions are already granted or granted by user, this method will succeed.
/// it will throw exception if permissions are denied by user.
/// [withAndroidFineLocation] is used to request fine location permission on Android 12+ (API 31+).
+ /// [withAndroidBluetoothAdvertise] is used to request Android 12+ advertise permission for peripheral mode.
/// on Android lower than 12, this method will request location permission regardless of the [withAndroidFineLocation] value.
/// `Windows`, `Linux` and `Web` will always succeed.
static Future requestPermissions({
bool withAndroidFineLocation = false,
+ bool withAndroidBluetoothAdvertise = false,
}) async {
return _platform.requestPermissions(
withAndroidFineLocation: withAndroidFineLocation,
+ withAndroidBluetoothAdvertise: withAndroidBluetoothAdvertise,
);
}
@@ -102,15 +100,9 @@ class UniversalBle {
/// Scan results will arrive in [onScanResult] listener.
/// It might throw errors if Bluetooth is not available.
/// `webRequestOptions` is supported on Web only.
- static Future startScan({
- ScanFilter? scanFilter,
- PlatformConfig? platformConfig,
- }) async {
+ static Future startScan({ScanFilter? scanFilter, PlatformConfig? platformConfig}) async {
return await _bleCommandQueue.queueCommandWithoutTimeout(
- () => _platform.startScan(
- scanFilter: scanFilter,
- platformConfig: platformConfig,
- ),
+ () => _platform.startScan(scanFilter: scanFilter, platformConfig: platformConfig),
);
}
@@ -118,9 +110,7 @@ class UniversalBle {
/// Set [onScanResult] listener to `null` if you don't need it anymore.
/// It might throw errors if Bluetooth is not available.
static Future stopScan() async {
- return await _bleCommandQueue.queueCommandWithoutTimeout(
- () => _platform.stopScan(),
- );
+ return await _bleCommandQueue.queueCommandWithoutTimeout(() => _platform.stopScan());
}
/// Check if currently scanning for devices.
@@ -147,17 +137,14 @@ class UniversalBle {
bool autoConnect = false,
}) async {
timeout ??= const Duration(seconds: 60);
- Completer completer = _connectionEventCompleter(
- deviceId,
- timeout: timeout,
- );
+ Completer completer = _connectionEventCompleter(deviceId, timeout: timeout);
- _platform
- .connect(deviceId, connectionTimeout: timeout, autoConnect: autoConnect)
- .catchError((error) {
- if (completer.isCompleted) return;
- completer.completeError(ConnectionException(error));
- });
+ _platform.connect(deviceId, connectionTimeout: timeout, autoConnect: autoConnect).catchError((
+ error,
+ ) {
+ if (completer.isCompleted) return;
+ completer.completeError(ConnectionException(error));
+ });
if (!await completer.future.timeout(timeout)) {
throw ConnectionException("Failed to connect");
@@ -176,17 +163,10 @@ class UniversalBle {
}
try {
- Completer completer = _connectionEventCompleter(
- deviceId,
- timeout: timeout,
- );
+ Completer completer = _connectionEventCompleter(deviceId, timeout: timeout);
await _bleCommandQueue
- .queueCommand(
- () => _platform.disconnect(deviceId),
- timeout: timeout,
- deviceId: deviceId,
- )
+ .queueCommand(() => _platform.disconnect(deviceId), timeout: timeout, deviceId: deviceId)
.catchError((error) {
if (completer.isCompleted) return;
completer.completeError(ConnectionException(error));
@@ -204,9 +184,7 @@ class UniversalBle {
}
if (await completer.future.timeout(timeout)) {
- UniversalLogger.logError(
- "Device $deviceId is still connected after disconnect attempt",
- );
+ UniversalLogger.logError("Device $deviceId is still connected after disconnect attempt");
}
} catch (e) {
UniversalLogger.logError("Disconnect failed: $e");
@@ -315,9 +293,7 @@ class UniversalBle {
BleUuidParser.string(service),
BleUuidParser.string(characteristic),
value,
- withoutResponse
- ? BleOutputProperty.withoutResponse
- : BleOutputProperty.withResponse,
+ withoutResponse ? BleOutputProperty.withoutResponse : BleOutputProperty.withResponse,
),
timeout: timeout,
deviceId: deviceId,
@@ -339,11 +315,7 @@ class UniversalBle {
///
/// **Best Practices:** Design for default ATT MTU (23 bytes), treat requests as
/// opportunistic, and implement fragmentation for larger payloads.
- static Future requestMtu(
- String deviceId,
- int expectedMtu, {
- Duration? timeout,
- }) async {
+ static Future requestMtu(String deviceId, int expectedMtu, {Duration? timeout}) async {
return await _bleCommandQueue.queueCommand(
() => _platform.requestMtu(deviceId, expectedMtu),
timeout: timeout,
@@ -445,11 +417,7 @@ class UniversalBle {
///
/// On `Web/Windows` and `Web/Linux`, it does not work for devices that use `ConfirmOnly` pairing.
/// Can throw `PairingException`, `ConnectionException` or `PlatformException`.
- static Future pair(
- String deviceId, {
- BleCommand? pairingCommand,
- Duration? timeout,
- }) async {
+ static Future pair(String deviceId, {BleCommand? pairingCommand, Duration? timeout}) async {
if (BleCapabilities.hasSystemPairingApi) {
bool paired = await _bleCommandQueue.queueCommand(
() => _platform.pair(deviceId),
@@ -461,11 +429,7 @@ class UniversalBle {
if (pairingCommand == null) {
UniversalLogger.logWarning("PairingCommand required to get result");
}
- await _connectAndExecuteBleCommand(
- deviceId,
- pairingCommand,
- timeout: timeout,
- );
+ await _connectAndExecuteBleCommand(deviceId, pairingCommand, timeout: timeout);
}
}
@@ -497,10 +461,7 @@ class UniversalBle {
/// Returns connection state of the device.
/// All platforms will return `Connected/Disconnected` states.
/// `Android` and `Apple` can also return `Connecting/Disconnecting` states.
- static Future getConnectionState(
- String deviceId, {
- Duration? timeout,
- }) async {
+ static Future getConnectionState(String deviceId, {Duration? timeout}) async {
return await _bleCommandQueue.queueCommand(
() => _platform.getConnectionState(deviceId),
timeout: timeout,
@@ -511,10 +472,7 @@ class UniversalBle {
/// It might throw errors if Bluetooth is not available.
/// Not supported on `Web` and `Apple`.
static Future enableBluetooth({Duration? timeout}) async {
- return await _bleCommandQueue.queueCommand(
- () => _platform.enableBluetooth(),
- timeout: timeout,
- );
+ return await _bleCommandQueue.queueCommand(() => _platform.enableBluetooth(), timeout: timeout);
}
/// Disable Bluetooth.
@@ -540,8 +498,7 @@ class UniversalBle {
/// For this feature to work, you need to enable the `chrome://flags/#enable-experimental-web-platform-features` flag.
/// Not every browser supports this API yet.
/// Even if the browser supports it, sometimes it won't fire any advertisement events even though the device may be sending them.
- static bool receivesAdvertisements(String deviceId) =>
- _platform.receivesAdvertisements(deviceId);
+ static bool receivesAdvertisements(String deviceId) => _platform.receivesAdvertisements(deviceId);
/// Get Bluetooth state availability.
static set onAvailabilityChange(OnAvailabilityChange? onAvailabilityChange) {
@@ -555,21 +512,14 @@ class UniversalBle {
}
}
- @Deprecated(
- "Use [subscribeNotifications] or [subscribeIndications] or [unsubscribe] instead",
- )
+ @Deprecated("Use [subscribeNotifications] or [subscribeIndications] or [unsubscribe] instead")
static Future setNotifiable(
String deviceId,
String service,
String characteristic,
BleInputProperty bleInputProperty,
) async {
- return _sendBleInputPropertyCommand(
- deviceId,
- service,
- characteristic,
- bleInputProperty,
- );
+ return _sendBleInputPropertyCommand(deviceId, service, characteristic, bleInputProperty);
}
@Deprecated("Use [write] instead")
@@ -599,10 +549,7 @@ class UniversalBle {
return read(deviceId, service, characteristic, timeout: timeout);
}
- static Completer _connectionEventCompleter(
- String deviceId, {
- Duration? timeout,
- }) {
+ static Completer _connectionEventCompleter(String deviceId, {Duration? timeout}) {
timeout ??= const Duration(seconds: 60);
StreamSubscription? connectionSubscription;
Completer completer = Completer();
@@ -618,9 +565,7 @@ class UniversalBle {
completer.completeError(ConnectionException(error));
}
- connectionSubscription = _platform
- .bleConnectionUpdateStreamController
- .stream
+ connectionSubscription = _platform.bleConnectionUpdateStreamController.stream
.where((e) => e.deviceId == deviceId)
.listen(
(e) {
@@ -681,10 +626,7 @@ class UniversalBle {
await connect(deviceId, timeout: timeout);
}
- List services = await discoverServices(
- deviceId,
- timeout: timeout,
- );
+ List services = await discoverServices(deviceId, timeout: timeout);
UniversalLogger.logInfo("Discovered services: ${services.length}");
if (bleCommand == null) {
@@ -737,10 +679,7 @@ class UniversalBle {
for (BleService service in services) {
if (BleUuidParser.compareStrings(service.uuid, bleCommand.service)) {
for (BleCharacteristic char in service.characteristics) {
- if (BleUuidParser.compareStrings(
- char.uuid,
- bleCommand.characteristic,
- )) {
+ if (BleUuidParser.compareStrings(char.uuid, bleCommand.characteristic)) {
characteristic = char;
break;
}
@@ -756,16 +695,10 @@ class UniversalBle {
bool? withoutResponse;
if (characteristic.properties.contains(CharacteristicProperty.write)) {
withoutResponse = false;
- } else if (characteristic.properties.contains(
- CharacteristicProperty.writeWithoutResponse,
- )) {
+ } else if (characteristic.properties.contains(CharacteristicProperty.writeWithoutResponse)) {
withoutResponse = true;
- } else if (!characteristic.properties.contains(
- CharacteristicProperty.read,
- )) {
- throw PairingException(
- "BleCommand does not support read or write operation",
- );
+ } else if (!characteristic.properties.contains(CharacteristicProperty.read)) {
+ throw PairingException("BleCommand does not support read or write operation");
}
Uint8List? value = bleCommand.writeValue;
@@ -799,16 +732,14 @@ class UniversalBle {
_bleCommandQueue.onQueueUpdate = onQueueUpdate;
/// Get scan results.
- static set onScanResult(OnScanResult? onScanResult) =>
- _platform.onScanResult = onScanResult;
+ static set onScanResult(OnScanResult? onScanResult) => _platform.onScanResult = onScanResult;
/// Get connection state changes.
static set onConnectionChange(OnConnectionChange? onConnectionChange) =>
_platform.onConnectionChange = onConnectionChange;
/// Get characteristic value updates, after calling [subscribeNotifications] or [subscribeIndications]
- static set onValueChange(OnValueChange? onValueChange) =>
- _platform.onValueChange = onValueChange;
+ static set onValueChange(OnValueChange? onValueChange) => _platform.onValueChange = onValueChange;
/// Get pair state changes.
static set onPairingStateChange(OnPairingStateChange pairingStateChange) =>
@@ -822,3 +753,43 @@ class UniversalBle {
return UniversalBlePigeonChannel.instance;
}
}
+
+class UniversalBlePeripheral {
+ static Stream get connectionStream =>
+ UniversalBle._platform.peripheralConnectionStream;
+
+ static Stream get writeStream =>
+ UniversalBle._platform.peripheralWriteStream;
+
+ static Stream get subscriptionStream =>
+ UniversalBle._platform.peripheralSubscriptionStream;
+
+ static Future isSupported() => UniversalBle._platform.isPeripheralSupported();
+
+ static Future start(BlePeripheralConfig config) =>
+ UniversalBle._platform.startPeripheral(config);
+
+ static Future stop() => UniversalBle._platform.stopPeripheral();
+
+ static Future updateCharacteristicValue(
+ String service,
+ String characteristic,
+ Uint8List value,
+ ) => UniversalBle._platform.updatePeripheralCharacteristicValue(
+ BleUuidParser.string(service),
+ BleUuidParser.string(characteristic),
+ value,
+ );
+
+ static Future notify(
+ String service,
+ String characteristic,
+ Uint8List value, {
+ bool indicate = false,
+ }) => UniversalBle._platform.notifyPeripheralCharacteristic(
+ BleUuidParser.string(service),
+ BleUuidParser.string(characteristic),
+ value,
+ indicate: indicate,
+ );
+}
diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart
index aa94693..0850432 100644
--- a/lib/src/universal_ble_linux/universal_ble_linux.dart
+++ b/lib/src/universal_ble_linux/universal_ble_linux.dart
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:bluez/bluez.dart';
+import 'package:dbus/dbus.dart';
import 'package:flutter/services.dart';
import 'package:universal_ble/src/models/model_exports.dart';
import 'package:universal_ble/src/utils/universal_ble_error_parser.dart';
@@ -27,8 +28,15 @@ class UniversalBleLinux extends UniversalBlePlatform {
final Map _deviceUpdateStreamSubscriptions = {};
final Map _deviceAdvertisementSubscriptions = {};
- final Map _characteristicPropertiesSubscriptions =
- {};
+ final Map _characteristicPropertiesSubscriptions = {};
+
+ DBusClient? _peripheralBus;
+ _BlueZPeripheralApplication? _peripheralApplication;
+ final List _peripheralObjects = [];
+ final Map _peripheralCharacteristics = {};
+ BlueZAdvertisement? _peripheralAdvertisement;
+ bool _peripheralApplicationRegistered = false;
+ DBusObjectPath? _peripheralAdapterPath;
@override
Future getBluetoothAvailabilityState() async {
@@ -38,9 +46,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
if (adapter == null) {
return AvailabilityState.unsupported;
}
- return adapter.powered
- ? AvailabilityState.poweredOn
- : AvailabilityState.poweredOff;
+ return adapter.powered ? AvailabilityState.poweredOn : AvailabilityState.poweredOff;
}
@override
@@ -74,10 +80,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
@override
- Future startScan({
- ScanFilter? scanFilter,
- PlatformConfig? platformConfig,
- }) async {
+ Future startScan({ScanFilter? scanFilter, PlatformConfig? platformConfig}) async {
await _ensureInitialized();
var adapter = _activeAdapter;
if (adapter == null) {
@@ -135,9 +138,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
Future getConnectionState(String deviceId) async {
BlueZDevice? device = _getDeviceById(deviceId);
bool connected = device?.connected ?? false;
- return connected
- ? BleConnectionState.connected
- : BleConnectionState.disconnected;
+ return connected ? BleConnectionState.connected : BleConnectionState.disconnected;
}
@override
@@ -165,19 +166,14 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
@override
- Future> discoverServices(
- String deviceId,
- bool withDescriptors,
- ) async {
+ Future> discoverServices(String deviceId, bool withDescriptors) async {
final device = _findDeviceById(deviceId);
if (device.gattServices.isEmpty && !device.servicesResolved) {
await device.propertiesChanged
.firstWhere((element) {
if (element.contains(BluezProperty.connected)) {
if (!device.connected) {
- UniversalLogger.logInfo(
- "DiscoverServicesFailed: Device disconnected",
- );
+ UniversalLogger.logInfo("DiscoverServicesFailed: Device disconnected");
return true;
}
}
@@ -219,9 +215,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
uuid: e.uuid.toString(),
properties: properties,
descriptors: withDescriptors
- ? e.descriptors
- .map((e) => BleDescriptor(e.uuid.toString()))
- .toList()
+ ? e.descriptors.map((e) => BleDescriptor(e.uuid.toString())).toList()
: [],
);
}).toList();
@@ -281,35 +275,31 @@ class UniversalBleLinux extends UniversalBlePlatform {
_characteristicPropertiesSubscriptions[characteristicKey]?.cancel();
}
- _characteristicPropertiesSubscriptions[characteristicKey] = char
- .propertiesChanged
- .listen((List properties) {
- for (String property in properties) {
- switch (property) {
- case BluezProperty.value:
- UniversalLogger.logVerbose(
- "NOTIFY <- $deviceId $service $characteristic len=${char.value.length} data=${char.value}",
- withTimestamp: true,
- );
- updateCharacteristicValue(
- deviceId,
- characteristic,
- Uint8List.fromList(char.value),
- DateTime.now().millisecondsSinceEpoch,
- );
- break;
- default:
- UniversalLogger.logInfo(
- "UnhandledCharValuePropertyChange: $property",
- );
- }
- }
- });
+ _characteristicPropertiesSubscriptions[characteristicKey] = char.propertiesChanged.listen((
+ List properties,
+ ) {
+ for (String property in properties) {
+ switch (property) {
+ case BluezProperty.value:
+ UniversalLogger.logVerbose(
+ "NOTIFY <- $deviceId $service $characteristic len=${char.value.length} data=${char.value}",
+ withTimestamp: true,
+ );
+ updateCharacteristicValue(
+ deviceId,
+ characteristic,
+ Uint8List.fromList(char.value),
+ DateTime.now().millisecondsSinceEpoch,
+ );
+ break;
+ default:
+ UniversalLogger.logInfo("UnhandledCharValuePropertyChange: $property");
+ }
+ }
+ });
} else {
if (char.notifying) await char.stopNotify();
- _characteristicPropertiesSubscriptions
- .remove(characteristicKey)
- ?.cancel();
+ _characteristicPropertiesSubscriptions.remove(characteristicKey)?.cancel();
}
}
@@ -320,10 +310,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
String characteristic, {
final Duration? timeout,
}) async {
- UniversalLogger.logDebug(
- "READ -> $deviceId $service $characteristic",
- withTimestamp: true,
- );
+ UniversalLogger.logDebug("READ -> $deviceId $service $characteristic", withTimestamp: true);
try {
final c = _getCharacteristic(deviceId, service, characteristic);
final data = await c.readValue();
@@ -333,9 +320,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
"READ_FAILED <- $deviceId $service $characteristic ${e.message}",
withTimestamp: true,
);
- throw e.toUniversalBleException(
- defaultCode: UniversalBleErrorCode.readFailed,
- );
+ throw e.toUniversalBleException(defaultCode: UniversalBleErrorCode.readFailed);
}
}
@@ -354,24 +339,16 @@ class UniversalBleLinux extends UniversalBlePlatform {
try {
final c = _getCharacteristic(deviceId, service, characteristic);
if (bleOutputProperty == BleOutputProperty.withResponse) {
- await c.writeValue(
- value,
- type: BlueZGattCharacteristicWriteType.request,
- );
+ await c.writeValue(value, type: BlueZGattCharacteristicWriteType.request);
} else {
- await c.writeValue(
- value,
- type: BlueZGattCharacteristicWriteType.command,
- );
+ await c.writeValue(value, type: BlueZGattCharacteristicWriteType.command);
}
} on BlueZFailedException catch (e) {
UniversalLogger.logError(
"WRITE_FAILED <- $deviceId $service $characteristic ${e.message}",
withTimestamp: true,
);
- throw e.toUniversalBleException(
- defaultCode: UniversalBleErrorCode.writeFailed,
- );
+ throw e.toUniversalBleException(defaultCode: UniversalBleErrorCode.writeFailed);
}
}
@@ -398,16 +375,142 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
@override
- Future requestConnectionPriority(
- String deviceId,
- BleConnectionPriority priority,
- ) {
+ Future requestConnectionPriority(String deviceId, BleConnectionPriority priority) {
throw UniversalBleException(
code: UniversalBleErrorCode.notSupported,
message: "requestConnectionPriority is not supported on Linux platform",
);
}
+ @override
+ Future isPeripheralSupported() async {
+ await _ensureInitialized();
+ return _isLinuxPeripheralSupported();
+ }
+
+ @override
+ Future startPeripheral(BlePeripheralConfig config) async {
+ await _ensureInitialized();
+
+ final adapter = _activeAdapter;
+ if (adapter == null) {
+ throw UniversalBleException(
+ code: UniversalBleErrorCode.bluetoothNotAvailable,
+ message: 'Bluetooth adapter unavailable',
+ );
+ }
+ if (!adapter.powered) {
+ throw UniversalBleException(
+ code: UniversalBleErrorCode.bluetoothNotEnabled,
+ message: 'Bluetooth not enabled',
+ );
+ }
+ if (!await _isLinuxPeripheralSupported()) {
+ throw UniversalBleException(
+ code: UniversalBleErrorCode.notSupported,
+ message: 'BLE peripheral mode is not supported by this Linux adapter',
+ );
+ }
+
+ await _stopPeripheralInternal();
+
+ final bus = DBusClient.system();
+ final application = _BlueZPeripheralApplication(
+ DBusObjectPath('/com/navideck/universal_ble/peripheral'),
+ );
+ late final DBusObjectPath? adapterPath;
+ try {
+ adapterPath = await _resolveActiveAdapterPath(bus);
+ } catch (error) {
+ await bus.close();
+ throw UniversalBleException(
+ code: UniversalBleErrorCode.bluetoothNotAvailable,
+ message: 'Bluetooth adapter unavailable',
+ details: error,
+ );
+ }
+ if (adapterPath == null) {
+ await bus.close();
+ throw UniversalBleException(
+ code: UniversalBleErrorCode.bluetoothNotAvailable,
+ message: 'Bluetooth adapter unavailable',
+ );
+ }
+
+ _peripheralBus = bus;
+ _peripheralApplication = application;
+ _peripheralAdapterPath = adapterPath;
+ _peripheralObjects.clear();
+ _peripheralCharacteristics.clear();
+
+ try {
+ await bus.registerObject(application);
+ await _registerPeripheralObjects(bus, application.path, config);
+
+ await _blueZAdapterObject(bus, adapterPath).callMethod(
+ 'org.bluez.GattManager1',
+ 'RegisterApplication',
+ [application.path, DBusDict.stringVariant({})],
+ replySignature: DBusSignature(''),
+ );
+ _peripheralApplicationRegistered = true;
+
+ _peripheralAdvertisement = await adapter.advertisingManager.registerAdvertisement(
+ type: BlueZAdvertisementType.peripheral,
+ serviceUuids: config.services.map((service) => service.uuid).toList(),
+ localName: config.advertisedName,
+ );
+ } catch (error) {
+ await _stopPeripheralInternal();
+ if (error is UniversalBleException) rethrow;
+ throw UniversalBleException(
+ code: UniversalBleErrorCode.failed,
+ message: 'Failed to start Linux BLE peripheral',
+ details: error,
+ );
+ }
+ }
+
+ @override
+ Future stopPeripheral() async {
+ await _stopPeripheralInternal();
+ }
+
+ @override
+ Future updatePeripheralCharacteristicValue(
+ String service,
+ String characteristic,
+ Uint8List value,
+ ) async {
+ final localCharacteristic =
+ _peripheralCharacteristics[_peripheralCharacteristicKey(service, characteristic)];
+ if (localCharacteristic == null) {
+ throw UniversalBleException(
+ code: UniversalBleErrorCode.characteristicNotFound,
+ message: 'Unknown peripheral characteristic $characteristic',
+ );
+ }
+ localCharacteristic.updateValue(value);
+ }
+
+ @override
+ Future notifyPeripheralCharacteristic(
+ String service,
+ String characteristic,
+ Uint8List value, {
+ bool indicate = false,
+ }) async {
+ final localCharacteristic =
+ _peripheralCharacteristics[_peripheralCharacteristicKey(service, characteristic)];
+ if (localCharacteristic == null) {
+ throw UniversalBleException(
+ code: UniversalBleErrorCode.characteristicNotFound,
+ message: 'Unknown peripheral characteristic $characteristic',
+ );
+ }
+ await localCharacteristic.notifyValue(value);
+ }
+
@override
Future readRssi(String deviceId) async {
throw UniversalBleException(
@@ -446,9 +549,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
@override
Future> getSystemDevices(List? withServices) async {
await _ensureInitialized();
- List devices = _client.devices
- .where((device) => device.connected)
- .toList();
+ List devices = _client.devices.where((device) => device.connected).toList();
if (withServices != null && withServices.isNotEmpty) {
devices = devices.where((device) {
if (device.servicesResolved) {
@@ -456,16 +557,12 @@ class UniversalBleLinux extends UniversalBlePlatform {
.map((e) => e.uuid.toString())
.any((service) => withServices.contains(service));
} else {
- UniversalLogger.logInfo(
- 'Skipping: ${device.address}: Services not resolved yet.',
- );
+ UniversalLogger.logInfo('Skipping: ${device.address}: Services not resolved yet.');
return false;
}
}).toList();
}
- return devices
- .map((device) => device.toBleDevice(isSystemDevice: true))
- .toList();
+ return devices.map((device) => device.toBleDevice(isSystemDevice: true)).toList();
}
AvailabilityState get _availabilityState {
@@ -511,9 +608,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
_activeAdapter ??= _client.adapters.first;
- UniversalLogger.logInfo(
- 'BleAdapter: ${_activeAdapter?.name} - ${_activeAdapter?.address}',
- );
+ UniversalLogger.logInfo('BleAdapter: ${_activeAdapter?.name} - ${_activeAdapter?.address}');
_activeAdapter?.propertiesChanged.listen((List properties) {
// Handle pairing state change
@@ -562,6 +657,174 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
}
+ Future _isLinuxPeripheralSupported() async {
+ final adapter = _activeAdapter;
+ if (adapter == null || !adapter.powered) return false;
+ if (adapter.roles.isNotEmpty && !adapter.roles.contains('peripheral')) {
+ return false;
+ }
+
+ final bus = DBusClient.system();
+ try {
+ final adapterPath = await _resolveActiveAdapterPath(bus);
+ if (adapterPath == null) return false;
+
+ final adapterObject = _blueZAdapterObject(bus, adapterPath);
+ final introspection = await adapterObject.introspect();
+ final interfaces = introspection.interfaces.map((interface) => interface.name).toSet();
+ return interfaces.contains('org.bluez.GattManager1') &&
+ interfaces.contains('org.bluez.LEAdvertisingManager1');
+ } catch (error) {
+ UniversalLogger.logInfo('Linux peripheral support check failed: $error');
+ return false;
+ } finally {
+ await bus.close();
+ }
+ }
+
+ Future _resolveActiveAdapterPath(DBusClient bus) async {
+ final adapter = _activeAdapter;
+ if (adapter == null) return null;
+
+ final root = DBusRemoteObjectManager(bus, name: 'org.bluez', path: DBusObjectPath('/'));
+ final managedObjects = await root.getManagedObjects();
+ final adapterAddress = adapter.address.toUpperCase();
+
+ for (final entry in managedObjects.entries) {
+ final adapterProperties = entry.value['org.bluez.Adapter1'];
+ if (adapterProperties == null) continue;
+ try {
+ final candidateAddress = adapterProperties['Address']?.asString().toUpperCase();
+ if (candidateAddress == adapterAddress) return entry.key;
+ } catch (_) {}
+ }
+
+ final adapterName = adapter.name;
+ if (RegExp(r'^hci\d+$').hasMatch(adapterName)) {
+ return DBusObjectPath('/org/bluez/$adapterName');
+ }
+ return null;
+ }
+
+ DBusRemoteObject _blueZAdapterObject(DBusClient bus, DBusObjectPath adapterPath) {
+ return DBusRemoteObject(bus, name: 'org.bluez', path: adapterPath);
+ }
+
+ Future _registerPeripheralObjects(
+ DBusClient bus,
+ DBusObjectPath applicationPath,
+ BlePeripheralConfig config,
+ ) async {
+ for (var serviceIndex = 0; serviceIndex < config.services.length; serviceIndex++) {
+ final serviceConfig = config.services[serviceIndex];
+ final servicePath = DBusObjectPath('${applicationPath.value}/service$serviceIndex');
+ final service = _BlueZPeripheralService(servicePath, uuid: serviceConfig.uuid);
+ await bus.registerObject(service);
+ _peripheralObjects.add(service);
+
+ for (
+ var characteristicIndex = 0;
+ characteristicIndex < serviceConfig.characteristics.length;
+ characteristicIndex++
+ ) {
+ final characteristicConfig = serviceConfig.characteristics[characteristicIndex];
+ final characteristicPath = DBusObjectPath('${servicePath.value}/char$characteristicIndex');
+ final characteristic = _BlueZPeripheralCharacteristic(
+ characteristicPath,
+ servicePath: servicePath,
+ serviceUuid: serviceConfig.uuid,
+ uuid: characteristicConfig.uuid,
+ properties: characteristicConfig.properties,
+ initialValue: characteristicConfig.initialValue,
+ onWrite: updatePeripheralWrite,
+ onSubscription: updatePeripheralSubscription,
+ );
+ await bus.registerObject(characteristic);
+ _peripheralObjects.add(characteristic);
+ _peripheralCharacteristics[_peripheralCharacteristicKey(
+ serviceConfig.uuid,
+ characteristicConfig.uuid,
+ )] =
+ characteristic;
+
+ for (
+ var descriptorIndex = 0;
+ descriptorIndex < characteristicConfig.descriptors.length;
+ descriptorIndex++
+ ) {
+ final descriptorConfig = characteristicConfig.descriptors[descriptorIndex];
+ final descriptorPath = DBusObjectPath('${characteristicPath.value}/desc$descriptorIndex');
+ final descriptor = _BlueZPeripheralDescriptor(
+ descriptorPath,
+ characteristicPath: characteristicPath,
+ uuid: descriptorConfig.uuid,
+ permissions: descriptorConfig.permissions,
+ initialValue: descriptorConfig.initialValue,
+ );
+ await bus.registerObject(descriptor);
+ _peripheralObjects.add(descriptor);
+ }
+ }
+ }
+ }
+
+ Future _stopPeripheralInternal() async {
+ final adapter = _activeAdapter;
+ final advertisement = _peripheralAdvertisement;
+ _peripheralAdvertisement = null;
+ if (advertisement != null && adapter != null) {
+ try {
+ await adapter.advertisingManager.unregisterAdvertisement(advertisement);
+ } catch (error) {
+ UniversalLogger.logInfo('Linux peripheral advertisement cleanup failed: $error');
+ }
+ }
+
+ final bus = _peripheralBus;
+ final application = _peripheralApplication;
+ final adapterPath = _peripheralAdapterPath;
+ if (bus != null &&
+ application != null &&
+ adapterPath != null &&
+ _peripheralApplicationRegistered) {
+ try {
+ await _blueZAdapterObject(bus, adapterPath).callMethod(
+ 'org.bluez.GattManager1',
+ 'UnregisterApplication',
+ [application.path],
+ replySignature: DBusSignature(''),
+ );
+ } catch (error) {
+ UniversalLogger.logInfo('Linux peripheral GATT cleanup failed: $error');
+ }
+ }
+ _peripheralApplicationRegistered = false;
+
+ if (bus != null) {
+ for (final object in _peripheralObjects.reversed) {
+ try {
+ await bus.unregisterObject(object);
+ } catch (_) {}
+ }
+ _peripheralObjects.clear();
+
+ if (application != null) {
+ try {
+ await bus.unregisterObject(application);
+ } catch (_) {}
+ }
+ await bus.close();
+ }
+
+ _peripheralBus = null;
+ _peripheralApplication = null;
+ _peripheralAdapterPath = null;
+ _peripheralCharacteristics.clear();
+ }
+
+ String _peripheralCharacteristicKey(String service, String characteristic) =>
+ '${BleUuidParser.string(service)}|${BleUuidParser.string(characteristic)}';
+
void _onDeviceAdd(BlueZDevice device) {
BleDevice bleDevice = device.toBleDevice();
if (!_bleFilter.shouldAcceptDevice(bleDevice)) {
@@ -577,8 +840,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
_devices[device.address] = device;
// Setup advertisements Listener
- _deviceAdvertisementSubscriptions[device.address] ??= device
- .propertiesChanged
+ _deviceAdvertisementSubscriptions[device.address] ??= device.propertiesChanged
.where((e) {
return e.contains(BluezProperty.rssi) ||
e.contains(BluezProperty.manufacturerData) ||
@@ -592,8 +854,9 @@ class UniversalBleLinux extends UniversalBlePlatform {
});
// Setup update listener
- _deviceUpdateStreamSubscriptions[device
- .address] ??= device.propertiesChanged.listen((properties) {
+ _deviceUpdateStreamSubscriptions[device.address] ??= device.propertiesChanged.listen((
+ properties,
+ ) {
for (final property in properties) {
switch (property) {
// Connection/Pair updates
@@ -646,6 +909,440 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
}
+class _BlueZPeripheralApplication extends DBusObject {
+ _BlueZPeripheralApplication(super.path) : super(isObjectManager: true);
+}
+
+class _BlueZPeripheralService extends DBusObject {
+ _BlueZPeripheralService(super.path, {required this.uuid});
+
+ static const _interfaceName = 'org.bluez.GattService1';
+
+ final String uuid;
+
+ Map _properties() => {'UUID': DBusString(uuid), 'Primary': DBusBoolean(true)};
+
+ @override
+ Map> get interfacesAndProperties => {
+ _interfaceName: _properties(),
+ };
+
+ @override
+ Future getProperty(String interface, String name) async {
+ if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
+ final value = _properties()[name];
+ if (value == null) return DBusMethodErrorResponse.unknownProperty();
+ return DBusGetPropertyResponse(value);
+ }
+
+ @override
+ Future getAllProperties(String interface) async {
+ return DBusGetAllPropertiesResponse(
+ interface == _interfaceName ? _properties() : {},
+ );
+ }
+
+ @override
+ Future setProperty(String interface, String name, DBusValue value) async {
+ if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
+ return _properties().containsKey(name)
+ ? DBusMethodErrorResponse.propertyReadOnly()
+ : DBusMethodErrorResponse.unknownProperty();
+ }
+
+ @override
+ List introspect() => [
+ DBusIntrospectInterface(
+ _interfaceName,
+ properties: [
+ DBusIntrospectProperty('UUID', DBusSignature('s'), access: DBusPropertyAccess.read),
+ DBusIntrospectProperty('Primary', DBusSignature('b'), access: DBusPropertyAccess.read),
+ ],
+ ),
+ ];
+}
+
+class _BlueZPeripheralCharacteristic extends DBusObject {
+ _BlueZPeripheralCharacteristic(
+ super.path, {
+ required this.servicePath,
+ required this.serviceUuid,
+ required this.uuid,
+ required this.properties,
+ required Uint8List? initialValue,
+ required this.onWrite,
+ required this.onSubscription,
+ }) : _value = Uint8List.fromList(initialValue ?? Uint8List(0));
+
+ static const _interfaceName = 'org.bluez.GattCharacteristic1';
+
+ final DBusObjectPath servicePath;
+ final String serviceUuid;
+ final String uuid;
+ final List properties;
+ final void Function(BlePeripheralWriteEvent event) onWrite;
+ final void Function(BlePeripheralSubscriptionEvent event) onSubscription;
+
+ Uint8List _value;
+ bool _notifying = false;
+
+ Map _properties() => {
+ 'UUID': DBusString(uuid),
+ 'Service': servicePath,
+ 'Value': DBusArray.byte(_value),
+ 'Notifying': DBusBoolean(_notifying),
+ 'Flags': DBusArray.string(properties.map((property) => property.bluezFlag)),
+ };
+
+ @override
+ Map> get interfacesAndProperties => {
+ _interfaceName: _properties(),
+ };
+
+ void updateValue(Uint8List value) {
+ _value = Uint8List.fromList(value);
+ }
+
+ Future notifyValue(Uint8List value) async {
+ updateValue(value);
+ await emitPropertiesChanged(
+ _interfaceName,
+ changedProperties: {'Value': DBusArray.byte(_value)},
+ );
+ }
+
+ @override
+ Future handleMethodCall(DBusMethodCall methodCall) async {
+ if (methodCall.interface != _interfaceName) {
+ return DBusMethodErrorResponse.unknownInterface();
+ }
+
+ switch (methodCall.name) {
+ case 'ReadValue':
+ if (methodCall.values.length != 1) return DBusMethodErrorResponse.invalidArgs();
+ final options = methodCall.values[0].asStringVariantDict();
+ final offset = _offsetFromOptions(options);
+ if (offset > _value.length) {
+ return DBusMethodErrorResponse('org.bluez.Error.InvalidOffset', [
+ DBusString('Invalid offset'),
+ ]);
+ }
+ return DBusMethodSuccessResponse([DBusArray.byte(_value.sublist(offset))]);
+ case 'WriteValue':
+ if (methodCall.values.length != 2) return DBusMethodErrorResponse.invalidArgs();
+ final incoming = Uint8List.fromList(methodCall.values[0].asByteArray().toList());
+ final options = methodCall.values[1].asStringVariantDict();
+ final offset = _offsetFromOptions(options);
+ if (offset > _value.length) {
+ return DBusMethodErrorResponse('org.bluez.Error.InvalidOffset', [
+ DBusString('Invalid offset'),
+ ]);
+ }
+ _writeValue(offset, incoming);
+ onWrite(
+ BlePeripheralWriteEvent(
+ deviceId: _deviceIdFromOptions(options, methodCall),
+ service: serviceUuid,
+ characteristic: uuid,
+ value: incoming,
+ ),
+ );
+ return DBusMethodSuccessResponse();
+ case 'StartNotify':
+ if (methodCall.values.isNotEmpty) return DBusMethodErrorResponse.invalidArgs();
+ _notifying = true;
+ await emitPropertiesChanged(
+ _interfaceName,
+ changedProperties: {'Notifying': DBusBoolean(true)},
+ );
+ onSubscription(
+ BlePeripheralSubscriptionEvent(
+ deviceId: methodCall.sender ?? '',
+ service: serviceUuid,
+ characteristic: uuid,
+ subscribed: true,
+ ),
+ );
+ return DBusMethodSuccessResponse();
+ case 'StopNotify':
+ if (methodCall.values.isNotEmpty) return DBusMethodErrorResponse.invalidArgs();
+ _notifying = false;
+ await emitPropertiesChanged(
+ _interfaceName,
+ changedProperties: {'Notifying': DBusBoolean(false)},
+ );
+ onSubscription(
+ BlePeripheralSubscriptionEvent(
+ deviceId: methodCall.sender ?? '',
+ service: serviceUuid,
+ characteristic: uuid,
+ subscribed: false,
+ ),
+ );
+ return DBusMethodSuccessResponse();
+ default:
+ return DBusMethodErrorResponse.unknownMethod();
+ }
+ }
+
+ @override
+ Future getProperty(String interface, String name) async {
+ if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
+ final value = _properties()[name];
+ if (value == null) return DBusMethodErrorResponse.unknownProperty();
+ return DBusGetPropertyResponse(value);
+ }
+
+ @override
+ Future getAllProperties(String interface) async {
+ return DBusGetAllPropertiesResponse(
+ interface == _interfaceName ? _properties() : {},
+ );
+ }
+
+ @override
+ Future setProperty(String interface, String name, DBusValue value) async {
+ if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
+ return _properties().containsKey(name)
+ ? DBusMethodErrorResponse.propertyReadOnly()
+ : DBusMethodErrorResponse.unknownProperty();
+ }
+
+ @override
+ List introspect() => [
+ DBusIntrospectInterface(
+ _interfaceName,
+ methods: [
+ DBusIntrospectMethod(
+ 'ReadValue',
+ args: [
+ DBusIntrospectArgument(
+ DBusSignature('a{sv}'),
+ DBusArgumentDirection.in_,
+ name: 'options',
+ ),
+ DBusIntrospectArgument(DBusSignature('ay'), DBusArgumentDirection.out, name: 'value'),
+ ],
+ ),
+ DBusIntrospectMethod(
+ 'WriteValue',
+ args: [
+ DBusIntrospectArgument(DBusSignature('ay'), DBusArgumentDirection.in_, name: 'value'),
+ DBusIntrospectArgument(
+ DBusSignature('a{sv}'),
+ DBusArgumentDirection.in_,
+ name: 'options',
+ ),
+ ],
+ ),
+ DBusIntrospectMethod('StartNotify'),
+ DBusIntrospectMethod('StopNotify'),
+ ],
+ properties: [
+ DBusIntrospectProperty('UUID', DBusSignature('s'), access: DBusPropertyAccess.read),
+ DBusIntrospectProperty('Service', DBusSignature('o'), access: DBusPropertyAccess.read),
+ DBusIntrospectProperty('Value', DBusSignature('ay'), access: DBusPropertyAccess.read),
+ DBusIntrospectProperty('Notifying', DBusSignature('b'), access: DBusPropertyAccess.read),
+ DBusIntrospectProperty('Flags', DBusSignature('as'), access: DBusPropertyAccess.read),
+ ],
+ ),
+ ];
+
+ void _writeValue(int offset, Uint8List incoming) {
+ if (offset == 0) {
+ _value = Uint8List.fromList(incoming);
+ return;
+ }
+
+ final nextLength = offset + incoming.length > _value.length
+ ? offset + incoming.length
+ : _value.length;
+ final nextValue = Uint8List(nextLength);
+ nextValue.setRange(0, _value.length, _value);
+ nextValue.setRange(offset, offset + incoming.length, incoming);
+ _value = nextValue;
+ }
+}
+
+class _BlueZPeripheralDescriptor extends DBusObject {
+ _BlueZPeripheralDescriptor(
+ super.path, {
+ required this.characteristicPath,
+ required this.uuid,
+ required this.permissions,
+ required Uint8List? initialValue,
+ }) : _value = Uint8List.fromList(initialValue ?? Uint8List(0));
+
+ static const _interfaceName = 'org.bluez.GattDescriptor1';
+
+ final DBusObjectPath characteristicPath;
+ final String uuid;
+ final List permissions;
+ Uint8List _value;
+
+ Map _properties() => {
+ 'UUID': DBusString(uuid),
+ 'Characteristic': characteristicPath,
+ 'Value': DBusArray.byte(_value),
+ 'Flags': DBusArray.string(permissions.map((permission) => permission.bluezFlag)),
+ };
+
+ @override
+ Map> get interfacesAndProperties => {
+ _interfaceName: _properties(),
+ };
+
+ @override
+ Future handleMethodCall(DBusMethodCall methodCall) async {
+ if (methodCall.interface != _interfaceName) {
+ return DBusMethodErrorResponse.unknownInterface();
+ }
+
+ switch (methodCall.name) {
+ case 'ReadValue':
+ if (methodCall.values.length != 1) return DBusMethodErrorResponse.invalidArgs();
+ final offset = _offsetFromOptions(methodCall.values[0].asStringVariantDict());
+ if (offset > _value.length) {
+ return DBusMethodErrorResponse('org.bluez.Error.InvalidOffset', [
+ DBusString('Invalid offset'),
+ ]);
+ }
+ return DBusMethodSuccessResponse([DBusArray.byte(_value.sublist(offset))]);
+ case 'WriteValue':
+ if (methodCall.values.length != 2) return DBusMethodErrorResponse.invalidArgs();
+ final incoming = Uint8List.fromList(methodCall.values[0].asByteArray().toList());
+ final offset = _offsetFromOptions(methodCall.values[1].asStringVariantDict());
+ if (offset > _value.length) {
+ return DBusMethodErrorResponse('org.bluez.Error.InvalidOffset', [
+ DBusString('Invalid offset'),
+ ]);
+ }
+ if (offset == 0) {
+ _value = incoming;
+ } else {
+ final nextLength = offset + incoming.length > _value.length
+ ? offset + incoming.length
+ : _value.length;
+ final nextValue = Uint8List(nextLength);
+ nextValue.setRange(0, _value.length, _value);
+ nextValue.setRange(offset, offset + incoming.length, incoming);
+ _value = nextValue;
+ }
+ return DBusMethodSuccessResponse();
+ default:
+ return DBusMethodErrorResponse.unknownMethod();
+ }
+ }
+
+ @override
+ Future getProperty(String interface, String name) async {
+ if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
+ final value = _properties()[name];
+ if (value == null) return DBusMethodErrorResponse.unknownProperty();
+ return DBusGetPropertyResponse(value);
+ }
+
+ @override
+ Future getAllProperties(String interface) async {
+ return DBusGetAllPropertiesResponse(
+ interface == _interfaceName ? _properties() : {},
+ );
+ }
+
+ @override
+ Future setProperty(String interface, String name, DBusValue value) async {
+ if (interface != _interfaceName) return DBusMethodErrorResponse.unknownInterface();
+ return _properties().containsKey(name)
+ ? DBusMethodErrorResponse.propertyReadOnly()
+ : DBusMethodErrorResponse.unknownProperty();
+ }
+
+ @override
+ List introspect() => [
+ DBusIntrospectInterface(
+ _interfaceName,
+ methods: [
+ DBusIntrospectMethod(
+ 'ReadValue',
+ args: [
+ DBusIntrospectArgument(
+ DBusSignature('a{sv}'),
+ DBusArgumentDirection.in_,
+ name: 'options',
+ ),
+ DBusIntrospectArgument(DBusSignature('ay'), DBusArgumentDirection.out, name: 'value'),
+ ],
+ ),
+ DBusIntrospectMethod(
+ 'WriteValue',
+ args: [
+ DBusIntrospectArgument(DBusSignature('ay'), DBusArgumentDirection.in_, name: 'value'),
+ DBusIntrospectArgument(
+ DBusSignature('a{sv}'),
+ DBusArgumentDirection.in_,
+ name: 'options',
+ ),
+ ],
+ ),
+ ],
+ properties: [
+ DBusIntrospectProperty('UUID', DBusSignature('s'), access: DBusPropertyAccess.read),
+ DBusIntrospectProperty(
+ 'Characteristic',
+ DBusSignature('o'),
+ access: DBusPropertyAccess.read,
+ ),
+ DBusIntrospectProperty('Value', DBusSignature('ay'), access: DBusPropertyAccess.read),
+ DBusIntrospectProperty('Flags', DBusSignature('as'), access: DBusPropertyAccess.read),
+ ],
+ ),
+ ];
+}
+
+int _offsetFromOptions(Map options) {
+ final offset = options['offset'];
+ if (offset == null) return 0;
+ try {
+ return offset.asUint16();
+ } catch (_) {
+ return 0;
+ }
+}
+
+String _deviceIdFromOptions(Map options, DBusMethodCall methodCall) {
+ try {
+ final devicePath = options['device']?.asObjectPath().value;
+ if (devicePath != null) {
+ final markerIndex = devicePath.lastIndexOf('/dev_');
+ if (markerIndex >= 0) {
+ return devicePath.substring(markerIndex + 5).replaceAll('_', ':').toUpperCase();
+ }
+ return devicePath;
+ }
+ } catch (_) {}
+ return methodCall.sender ?? '';
+}
+
+extension on CharacteristicProperty {
+ String get bluezFlag => switch (this) {
+ CharacteristicProperty.broadcast => 'broadcast',
+ CharacteristicProperty.read => 'read',
+ CharacteristicProperty.writeWithoutResponse => 'write-without-response',
+ CharacteristicProperty.write => 'write',
+ CharacteristicProperty.notify => 'notify',
+ CharacteristicProperty.indicate => 'indicate',
+ CharacteristicProperty.authenticatedSignedWrites => 'authenticated-signed-writes',
+ CharacteristicProperty.extendedProperties => 'extended-properties',
+ };
+}
+
+extension on BlePeripheralCharacteristicPermission {
+ String get bluezFlag => switch (this) {
+ BlePeripheralCharacteristicPermission.read => 'read',
+ BlePeripheralCharacteristicPermission.write => 'write',
+ };
+}
+
class BluezProperty {
static const String rssi = 'RSSI';
static const String connected = 'Connected';
@@ -679,27 +1376,20 @@ extension on BlueZGattCharacteristicFlag {
BlueZGattCharacteristicFlag.indicate => CharacteristicProperty.indicate,
BlueZGattCharacteristicFlag.authenticatedSignedWrites =>
CharacteristicProperty.authenticatedSignedWrites,
- BlueZGattCharacteristicFlag.extendedProperties =>
- CharacteristicProperty.extendedProperties,
+ BlueZGattCharacteristicFlag.extendedProperties => CharacteristicProperty.extendedProperties,
_ => null,
};
}
}
extension on BlueZFailedException {
- UniversalBleException toUniversalBleException({
- required UniversalBleErrorCode defaultCode,
- }) {
+ UniversalBleException toUniversalBleException({required UniversalBleErrorCode defaultCode}) {
// Map BlueZ error code to UniversalBleErrorCode
UniversalBleErrorCode code = UniversalBleErrorParser.getCode(errorCode);
if (code == UniversalBleErrorCode.unknownError) {
code = defaultCode;
}
- throw UniversalBleException(
- code: code,
- message: message,
- details: errorCode,
- );
+ throw UniversalBleException(code: code, message: message, details: errorCode);
}
/// Extract error code from message and parse into decimal
diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart
index 9e023e0..22768b9 100644
--- a/lib/src/universal_ble_pigeon/universal_ble.g.dart
+++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart
@@ -1,18 +1,37 @@
-// Autogenerated from Pigeon (v26.1.4), do not edit directly.
+// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
-// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
+// ignore_for_file: unused_import, unused_shown_name
+// ignore_for_file: type=lint
import 'dart:async';
-import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+import 'dart:typed_data' show Float64List, Int32List, Int64List;
-import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
+import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
-PlatformException _createConnectionError(String channelName) {
- return PlatformException(
- code: 'channel-error',
- message: 'Unable to establish connection on channel: "$channelName".',
- );
+Object? _extractReplyValueOrThrow(
+ List