diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14aff2f8..26bc38c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
-## 1.3.0
+## 2.0.0
+* Add peripheral mode on Android, iOS, macOS, and Windows
* Add `requestConnectionPriority` to allow tuning BLE connection intervals on Android
+* BREAKING CHANGE: `getBluetoothAvailabilityState` renamed to `getAvailabilityState`
* Add SPM support on Apple
## 1.2.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..db1daf57
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,71 @@
+# Contributing
+
+Thank you for helping improve Universal BLE. This document describes how we work in this repository and what we expect from contributions.
+
+## Reporting issues
+
+Use [GitHub Issues](https://github.com/Navideck/universal_ble/issues). Include the platform (Android, iOS, macOS, Windows, Linux, Web), Flutter/Dart versions, and a minimal way to reproduce the problem when possible.
+
+## Pull requests
+
+- Open PRs against the `main` branch.
+- Keep changes focused: one logical concern per PR is easier to review than a large mixed refactor.
+- Update `CHANGELOG.md` when the change is user-visible (new behavior, fixes, or breaking API changes). Follow the existing bullet style (`* …`, `* BREAKING CHANGE: …` where appropriate).
+- Do not commit secrets, local paths, or generated build artifacts unrelated to the change.
+
+## Environment
+
+Requirements are defined in `pubspec.yaml` (Dart SDK and Flutter). Use a stable Flutter channel unless a maintainer asks otherwise.
+
+From the repo root:
+
+```sh
+flutter pub get
+```
+
+## Checks to run before opening a PR
+
+These mirror [.github/workflows/pull_request.yml](.github/workflows/pull_request.yml):
+
+```sh
+flutter analyze
+flutter test
+flutter test --platform chrome
+```
+
+Fix any analyzer issues. The project uses [flutter_lints](https://pub.dev/packages/flutter_lints) via [analysis_options.yaml](analysis_options.yaml) (which includes `package:flutter_lints/flutter.yaml`).
+
+Format Dart code with the SDK formatter:
+
+```sh
+dart format .
+```
+
+If you only touched specific files, you may format those paths instead of the whole tree.
+
+## Pigeon and generated code
+
+Host–native APIs are defined in [pigeon/universal_ble.dart](pigeon/universal_ble.dart). If you change that file, regenerate outputs and include them in the same PR:
+
+```sh
+./build_pigeon.sh
+```
+
+That runs `dart run pigeon --input pigeon/universal_ble.dart` and formats `lib/src/universal_ble_pigeon/universal_ble.g.dart`. Regenerated Kotlin, Swift, and C++ files land under `android/`, `darwin/`, and `windows/` as configured in the Pigeon `@ConfigurePigeon` block—keep those in sync with the Dart definitions.
+
+## Code conventions
+
+- **Dart:** Follow effective Dart style, existing naming in `lib/`, and analyzer rules. Prefer extending existing patterns (platform interface → pigeon channel → native implementations) over new parallel abstractions unless discussed first.
+- **Native:** Match the style and structure of the surrounding file on each platform (Kotlin, Swift, C++). When a Pigeon API changes, update every generated implementation and any hand-written glue so all targets stay consistent.
+- **Tests:** Add or extend tests under `test/` when behavior is non-trivial or regression-prone. Use `flutter_test` like the existing suite.
+- **Example:** If the change affects how integrators use the plugin, consider updating the `example/` app so it stays a working reference.
+
+## Platform-specific APIs and parameters
+
+- **Single-platform features:** Prefer not adding a new public API when only one platform can implement it, unless none of the existing APIs can be extended or adapted to cover the behavior. For example, something like Android-only `requestConnectionPriority` should only become its own method if `connect`, `platformConfig`, or another existing entry point cannot reasonably subsume it.
+- **Single-platform parameters:** When a value applies to one platform only, attach it via a platform-scoped bag (for example `startScan` takes an optional `platformConfig` object for options that only affect a given OS). That keeps the main method signature stable and makes it obvious which settings are platform-specific.
+- **Shared parameters:** If more than one platform supports the same option, add it as a normal method parameter on the shared API. Document that implementations on platforms without that capability must ignore the parameter (no-op or documented limitation).
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the same terms as the project: [BSD 3-Clause License](LICENSE).
diff --git a/README.md b/README.md
index 73ea2124..acf1c78f 100644
--- a/README.md
+++ b/README.md
@@ -33,9 +33,12 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
- [Error Handling](#error-handling)
- [UUID Format Agnostic](#uuid-format-agnostic)
- [Permissions](#permissions)
+- [Peripheral Mode](#peripheral-mode)
## API Support
+### Client Mode (`UniversalBle`)
+
| | Android | iOS | macOS | Windows | Linux | Web |
| :---------------------------- | :-----: | :-: | :---: | :-----: | :---: | :-: |
| startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
@@ -50,7 +53,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
| unpair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| onPairingStateChange | ✔️ | ⏺ | ⏺ | ✔️ | ✔️ | ⏺ |
-| getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
+| getAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
@@ -58,6 +61,28 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
| readRssi | ✔️ | ✔️ | ✔️ | ❌ | 🚧 | ❌ |
| requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
+### Peripheral Mode (`UniversalBlePeripheral`)
+
+| API | Android | iOS | macOS | Windows | Linux | Web |
+| :----------------------- | :-----: | :-: | :---: | :-----: | :---: | :-: |
+| getStaticCapabilities | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| getReadinessState\* | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| getAdvertisingState | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| addService | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| removeService | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| clearServices | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| getServices | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| startAdvertising | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| stopAdvertising | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| updateCharacteristicValue\*\* | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| getSubscribedClients | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| getMaximumNotifyLength | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+| events stream\*\*\* | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
+
+\* `getReadinessState` returns a snapshot state. Use `eventStream` for ongoing runtime changes.
+\*\* `updateCharacteristicValue` supports broadcast to all subscribed devices or a specific device via `PeripheralUpdateTarget`.
+\*\*\* events include advertising state changes, MTU changes, subscription changes, and related peripheral events.
+
## Getting Started
Add universal_ble in your pubspec.yaml:
@@ -70,6 +95,7 @@ dependencies:
and import it wherever you want to use it:
```dart
+import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
```
@@ -110,7 +136,7 @@ UniversalBle.isScanning();
Before initiating a scan, ensure that Bluetooth is available:
```dart
-AvailabilityState state = await UniversalBle.getBluetoothAvailabilityState();
+AvailabilityState state = await UniversalBle.getAvailabilityState();
// Start scan only if Bluetooth is powered on
if (state == AvailabilityState.poweredOn) {
UniversalBle.startScan();
@@ -402,7 +428,7 @@ bleDevice.unpair();
```dart
// Get current Bluetooth availability state
-AvailabilityState availabilityState = UniversalBle.getBluetoothAvailabilityState(); // e.g. poweredOff or poweredOn,
+AvailabilityState availabilityState = UniversalBle.getAvailabilityState(); // e.g. poweredOff or poweredOn,
// Receive Bluetooth availability changes
UniversalBle.onAvailabilityChange = (state) {
@@ -440,7 +466,7 @@ explicitly controlled by applications:
* **Android ≤ 13**: Apps may request MTU once per connection (up to 517).
If never requested, the default MTU is 23.
- * **Android 14+**: The first GATT client effectively drives MTU negotiation
+ * **Android 14+**: The first Bluetooth client effectively drives MTU negotiation
to 517 (or the link’s maximum); subsequent MTU requests are ignored.
* **Windows**
@@ -451,7 +477,7 @@ explicitly controlled by applications:
* **Linux (BlueZ)**
* MTU is negotiated automatically by default.
- * The standard D-Bus GATT API does not expose MTU control.
+ * The standard D-Bus Bluetooth API does not expose MTU control.
* MTU can be requested via BlueZ tools or lower-level APIs, but most apps
treat it as stack-defined.
@@ -606,6 +632,175 @@ try {
The error parser automatically converts platform-specific error formats (strings, numeric codes, PlatformExceptions) into the unified `UniversalBleErrorCode` enum, ensuring consistent error handling across all platforms.
+## Peripheral Mode
+
+`universal_ble` provides peripheral mode through `UniversalBlePeripheralClient`, so your app can advertise as a peripheral "server" in addition to client mode.
+
+### Setup
+
+```dart
+import 'dart:typed_data';
+import 'package:flutter/foundation.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+final peripheral = UniversalBlePeripheralClient();
+
+final caps = await peripheral.getStaticCapabilities();
+if (!caps.supportsPeripheralMode) return;
+
+final readiness = await peripheral.getReadinessState();
+if (readiness != UniversalBlePeripheralReadinessState.ready) return;
+```
+
+### Service Management
+
+```dart
+await peripheral.addService(
+ BleService("0000180F-0000-1000-8000-00805F9B34FB", [
+ BleCharacteristic(
+ "00002A19-0000-1000-8000-00805F9B34FB",
+ [CharacteristicProperty.read, CharacteristicProperty.notify],
+ [BleDescriptor("00002902-0000-1000-8000-00805F9B34FB")],
+ ),
+ ]),
+ primary: true,
+);
+
+await peripheral.addService(
+ BleService("0000180D-0000-1000-8000-00805F9B34FB", [
+ BleCharacteristic(
+ "00002A37-0000-1000-8000-00805F9B34FB",
+ [
+ CharacteristicProperty.read,
+ CharacteristicProperty.notify,
+ CharacteristicProperty.write,
+ ],
+ [],
+ ),
+ ]),
+);
+
+final services = await peripheral.getServices();
+await peripheral.removeService(const PeripheralServiceId("0000180D-0000-1000-8000-00805F9B34FB"));
+```
+
+### Advertising
+
+On **Android**, passing `localName` may temporarily change the system Bluetooth device name (so it can appear in the advertisement). The plugin restores the previous name when advertising stops, if starting advertising fails, or when the plugin is disposed.
+
+On **Windows**, `GattServiceProvider`-based advertising does not support `localName`, manufacturer data, or a scan-response flag; omit them (as below) or the call will return a not-supported error.
+
+```dart
+final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
+
+await peripheral.startAdvertising(
+ services: const [
+ PeripheralServiceId("0000180F-0000-1000-8000-00805F9B34FB"),
+ ],
+ localName: isWindows ? null : "UniversalBlePeripheral",
+ manufacturerData: isWindows
+ ? null
+ : ManufacturerData(
+ 0x012D,
+ Uint8List.fromList([0x03, 0x00, 0x64, 0x00]),
+ ),
+ addManufacturerDataInScanResponse: isWindows
+ ? false
+ : caps.supportsManufacturerDataInScanResponse,
+);
+
+final advertisingState = await peripheral.getAdvertisingState();
+if (advertisingState == UniversalBlePeripheralAdvertisingState.advertising) {
+ // Peripheral is advertising.
+}
+
+await peripheral.stopAdvertising();
+```
+
+### Request Handlers
+
+```dart
+peripheral.setRequestHandlers(
+ PeripheralRequestHandlers(
+ onReadRequest: (deviceId, characteristicId, offset, value) {
+ return BleReadRequestResult(value: value ?? Uint8List(0));
+ },
+ onWriteRequest: (deviceId, characteristicId, offset, value) {
+ return const BleWriteRequestResult();
+ },
+ onDescriptorReadRequest:
+ (deviceId, characteristicId, descriptorId, offset, value) {
+ return BleReadRequestResult(value: value ?? Uint8List(0));
+ },
+ onDescriptorWriteRequest:
+ (deviceId, characteristicId, descriptorId, offset, value) {
+ return const BleWriteRequestResult();
+ },
+ ),
+);
+```
+
+### Characteristic Updates
+
+```dart
+await peripheral.updateCharacteristicValue(
+ characteristicId: const PeripheralCharacteristicId(
+ "00002A19-0000-1000-8000-00805F9B34FB",
+ ),
+ value: Uint8List.fromList([92]),
+);
+```
+
+### Subscribed Clients and Notify Length
+
+```dart
+final subscribers = await peripheral.getSubscribedClients(
+ const PeripheralCharacteristicId("00002A19-0000-1000-8000-00805F9B34FB"),
+);
+
+for (final deviceId in subscribers) {
+ final maxNotifyLength = await peripheral.getMaximumNotifyLength(deviceId);
+ // maxNotifyLength can be null when unknown for this device.
+}
+```
+
+### Event Stream
+
+```dart
+final sub = peripheral.eventStream.listen((event) {
+ switch (event) {
+ case UniversalBlePeripheralAdvertisingStateChanged():
+ // event.state / event.error
+ break;
+ case UniversalBlePeripheralCharacteristicSubscriptionChanged():
+ // event.deviceId / event.characteristicId / event.isSubscribed
+ break;
+ case UniversalBlePeripheralConnectionStateChanged():
+ // event.deviceId / event.connected
+ break;
+ case UniversalBlePeripheralMtuChanged():
+ // event.deviceId / event.mtu
+ break;
+ case UniversalBlePeripheralServiceAdded():
+ // event.serviceId / event.error
+ break;
+ }
+});
+```
+
+### Breaking changes
+
+- `isSupported()` was replaced by `getStaticCapabilities().supportsPeripheralMode`.
+- `isAdvertising()` was replaced by `getAdvertisingState()`.
+- Static callback setters were replaced by `eventStream` + `setRequestHandlers(...)`.
+- `UniversalBlePeripheralClient` is the recommended API; `UniversalBlePeripheral` remains as a singleton facade.
+
+### Platform notes
+
+- Linux/Web currently return unsupported for peripheral mode.
+- Windows peripheral advertising does not expose all advertising payload customization options from Android/Apple stacks.
+- iOS/macOS setup (including required `Info.plist` keys for peripheral usage) is documented in [Permissions → iOS / macOS](#ios--macos).
+
## UUID Format Agnostic
Universal BLE is agnostic to the UUID format of services and characteristics regardless of the platform the app runs on. When passing a UUID, you can pass it in any format (long/short) or character case (upper/lower case) you want. Universal BLE will take care of necessary conversions, across all platforms, so that you don't need to worry about underlying platform differences.
@@ -692,7 +887,21 @@ await UniversalBle.startScan();
### iOS / macOS
-Add `NSBluetoothPeripheralUsageDescription` and `NSBluetoothAlwaysUsageDescription` to Info.plist of your iOS and macOS app.
+For Bluetooth usage (including peripheral mode), add both keys to your app's `Info.plist`:
+
+- `NSBluetoothAlwaysUsageDescription`: message shown when the app requests Bluetooth access.
+- `NSBluetoothPeripheralUsageDescription`: message used for peripheral role access on Apple platforms.
+
+Example:
+
+```xml
+NSBluetoothAlwaysUsageDescription
+This app uses Bluetooth to scan, connect, and advertise to nearby devices.
+NSBluetoothPeripheralUsageDescription
+This app uses Bluetooth to advertise services to nearby devices.
+```
+
+Use clear, user-facing text that explains why Bluetooth is needed in your app.
Add the `Bluetooth` capability to the macOS app from Xcode.
@@ -825,7 +1034,7 @@ Future resetBleState() async {
// Check Bluetooth availability
AvailabilityState availabilityState =
- await UniversalBle.getBluetoothAvailabilityState();
+ await UniversalBle.getAvailabilityState();
// Skip if Bluetooth is not powered on
if (availabilityState != AvailabilityState.poweredOn) {
@@ -865,7 +1074,12 @@ Future resetBleState() async {
## Example app
-This repo includes an [example app](example/) you can run to try the API. For a full-blown app, check [Universal-BLE](https://github.com/Navideck/Universal-BLE).
+This repo includes an [example app](example/) with two tabs:
+
+- `Client`: scanning and device communication workflows.
+- `Peripheral`: peripheral server and advertising workflows.
+
+For a full-blown app, check [Universal-BLE](https://github.com/Navideck/Universal-BLE).
## Low level API
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 21188097..946b2d51 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.3), 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()
+ }
+ }
+
}
/**
@@ -110,6 +224,34 @@ enum class AndroidScanMode(val raw: Int) {
}
}
+enum class PeripheralReadinessState(val raw: Int) {
+ UNKNOWN(0),
+ READY(1),
+ BLUETOOTH_OFF(2),
+ UNAUTHORIZED(3),
+ UNSUPPORTED(4);
+
+ companion object {
+ fun ofRaw(raw: Int): PeripheralReadinessState? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
+enum class PeripheralAdvertisingState(val raw: Int) {
+ IDLE(0),
+ STARTING(1),
+ ADVERTISING(2),
+ STOPPING(3),
+ ERROR(4);
+
+ companion object {
+ fun ofRaw(raw: Int): PeripheralAdvertisingState? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
/** Unified error codes for all platforms */
enum class UniversalBleErrorCode(val raw: Int) {
UNKNOWN_ERROR(0),
@@ -219,15 +361,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 +405,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 +446,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 +482,21 @@ 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
+ }
}
/**
@@ -355,15 +531,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 +567,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 +611,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 +653,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 +692,278 @@ 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
+ }
+ val other = other as UniversalManufacturerData
+ return UniversalBlePigeonUtils.deepEquals(this.companyIdentifier, other.companyIdentifier) && UniversalBlePigeonUtils.deepEquals(this.data, other.data)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.companyIdentifier)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.data)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class PeripheralService (
+ val uuid: String,
+ val primary: Boolean,
+ val characteristics: List
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): PeripheralService {
+ val uuid = pigeonVar_list[0] as String
+ val primary = pigeonVar_list[1] as Boolean
+ val characteristics = pigeonVar_list[2] as List
+ return PeripheralService(uuid, primary, characteristics)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ uuid,
+ primary,
+ characteristics,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as PeripheralService
+ return UniversalBlePigeonUtils.deepEquals(this.uuid, other.uuid) && UniversalBlePigeonUtils.deepEquals(this.primary, other.primary) && 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.primary)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.characteristics)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class PeripheralCharacteristic (
+ val uuid: String,
+ val properties: List,
+ val permissions: List,
+ val descriptors: List? = null,
+ val value: ByteArray? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): PeripheralCharacteristic {
+ 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 value = pigeonVar_list[4] as ByteArray?
+ return PeripheralCharacteristic(uuid, properties, permissions, descriptors, value)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ uuid,
+ properties,
+ permissions,
+ descriptors,
+ value,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as PeripheralCharacteristic
+ 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.value, other.value)
+ }
+
+ 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.value)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class PeripheralDescriptor (
+ val uuid: String,
+ val value: ByteArray? = null,
+ val permissions: List? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): PeripheralDescriptor {
+ val uuid = pigeonVar_list[0] as String
+ val value = pigeonVar_list[1] as ByteArray?
+ val permissions = pigeonVar_list[2] as List?
+ return PeripheralDescriptor(uuid, value, permissions)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ uuid,
+ value,
+ permissions,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as PeripheralDescriptor
+ return UniversalBlePigeonUtils.deepEquals(this.uuid, other.uuid) && UniversalBlePigeonUtils.deepEquals(this.value, other.value) && UniversalBlePigeonUtils.deepEquals(this.permissions, other.permissions)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.uuid)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.value)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.permissions)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class PeripheralReadRequestResult (
+ val value: ByteArray,
+ val offset: Long? = null,
+ val status: Long? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): PeripheralReadRequestResult {
+ val value = pigeonVar_list[0] as ByteArray
+ val offset = pigeonVar_list[1] as Long?
+ val status = pigeonVar_list[2] as Long?
+ return PeripheralReadRequestResult(value, offset, status)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ value,
+ offset,
+ status,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as PeripheralReadRequestResult
+ return UniversalBlePigeonUtils.deepEquals(this.value, other.value) && UniversalBlePigeonUtils.deepEquals(this.offset, other.offset) && UniversalBlePigeonUtils.deepEquals(this.status, other.status)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.value)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.offset)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.status)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class PeripheralWriteRequestResult (
+ val value: ByteArray? = null,
+ val offset: Long? = null,
+ val status: Long? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): PeripheralWriteRequestResult {
+ val value = pigeonVar_list[0] as ByteArray?
+ val offset = pigeonVar_list[1] as Long?
+ val status = pigeonVar_list[2] as Long?
+ return PeripheralWriteRequestResult(value, offset, status)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ value,
+ offset,
+ status,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ val other = other as PeripheralWriteRequestResult
+ return UniversalBlePigeonUtils.deepEquals(this.value, other.value) && UniversalBlePigeonUtils.deepEquals(this.offset, other.offset) && UniversalBlePigeonUtils.deepEquals(this.status, other.status)
+ }
+
+ override fun hashCode(): Int {
+ var result = javaClass.hashCode()
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.value)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.offset)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.status)
+ return result
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class PeripheralManufacturerData (
+ val manufacturerId: Long,
+ val data: ByteArray
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): PeripheralManufacturerData {
+ val manufacturerId = pigeonVar_list[0] as Long
+ val data = pigeonVar_list[1] as ByteArray
+ return PeripheralManufacturerData(manufacturerId, data)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ manufacturerId,
+ data,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
- return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) }
+ val other = other as PeripheralManufacturerData
+ return UniversalBlePigeonUtils.deepEquals(this.manufacturerId, other.manufacturerId) && 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.manufacturerId)
+ result = 31 * result + UniversalBlePigeonUtils.deepHash(this.data)
+ return result
+ }
}
private open class UniversalBlePigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
@@ -511,105 +980,177 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
}
131.toByte() -> {
return (readValue(buffer) as Long?)?.let {
- UniversalBleErrorCode.ofRaw(it.toInt())
+ PeripheralReadinessState.ofRaw(it.toInt())
}
}
132.toByte() -> {
+ return (readValue(buffer) as Long?)?.let {
+ PeripheralAdvertisingState.ofRaw(it.toInt())
+ }
+ }
+ 133.toByte() -> {
+ return (readValue(buffer) as Long?)?.let {
+ UniversalBleErrorCode.ofRaw(it.toInt())
+ }
+ }
+ 134.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalBleScanResult.fromList(it)
}
}
- 133.toByte() -> {
+ 135.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalBleService.fromList(it)
}
}
- 134.toByte() -> {
+ 136.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalBleCharacteristic.fromList(it)
}
}
- 135.toByte() -> {
+ 137.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalBleDescriptor.fromList(it)
}
}
- 136.toByte() -> {
+ 138.toByte() -> {
return (readValue(buffer) as? List)?.let {
AndroidOptions.fromList(it)
}
}
- 137.toByte() -> {
+ 139.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalScanConfig.fromList(it)
}
}
- 138.toByte() -> {
+ 140.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalScanFilter.fromList(it)
}
}
- 139.toByte() -> {
+ 141.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalManufacturerDataFilter.fromList(it)
}
}
- 140.toByte() -> {
+ 142.toByte() -> {
return (readValue(buffer) as? List)?.let {
UniversalManufacturerData.fromList(it)
}
}
- else -> super.readValueOfType(type, buffer)
- }
- }
- override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
- when (value) {
- is UniversalBleLogLevel -> {
- stream.write(129)
- writeValue(stream, value.raw.toLong())
+ 143.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ PeripheralService.fromList(it)
+ }
}
- is AndroidScanMode -> {
- stream.write(130)
- writeValue(stream, value.raw.toLong())
+ 144.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ PeripheralCharacteristic.fromList(it)
+ }
}
- is UniversalBleErrorCode -> {
+ 145.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ PeripheralDescriptor.fromList(it)
+ }
+ }
+ 146.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ PeripheralReadRequestResult.fromList(it)
+ }
+ }
+ 147.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ PeripheralWriteRequestResult.fromList(it)
+ }
+ }
+ 148.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ PeripheralManufacturerData.fromList(it)
+ }
+ }
+ else -> super.readValueOfType(type, buffer)
+ }
+ }
+ override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
+ when (value) {
+ is UniversalBleLogLevel -> {
+ stream.write(129)
+ writeValue(stream, value.raw.toLong())
+ }
+ is AndroidScanMode -> {
+ stream.write(130)
+ writeValue(stream, value.raw.toLong())
+ }
+ is PeripheralReadinessState -> {
stream.write(131)
writeValue(stream, value.raw.toLong())
}
- is UniversalBleScanResult -> {
+ is PeripheralAdvertisingState -> {
stream.write(132)
+ writeValue(stream, value.raw.toLong())
+ }
+ is UniversalBleErrorCode -> {
+ stream.write(133)
+ writeValue(stream, value.raw.toLong())
+ }
+ is UniversalBleScanResult -> {
+ stream.write(134)
writeValue(stream, value.toList())
}
is UniversalBleService -> {
- stream.write(133)
+ stream.write(135)
writeValue(stream, value.toList())
}
is UniversalBleCharacteristic -> {
- stream.write(134)
+ stream.write(136)
writeValue(stream, value.toList())
}
is UniversalBleDescriptor -> {
- stream.write(135)
+ stream.write(137)
writeValue(stream, value.toList())
}
is AndroidOptions -> {
- stream.write(136)
+ stream.write(138)
writeValue(stream, value.toList())
}
is UniversalScanConfig -> {
- stream.write(137)
+ stream.write(139)
writeValue(stream, value.toList())
}
is UniversalScanFilter -> {
- stream.write(138)
+ stream.write(140)
writeValue(stream, value.toList())
}
is UniversalManufacturerDataFilter -> {
- stream.write(139)
+ stream.write(141)
writeValue(stream, value.toList())
}
is UniversalManufacturerData -> {
- stream.write(140)
+ stream.write(142)
+ writeValue(stream, value.toList())
+ }
+ is PeripheralService -> {
+ stream.write(143)
+ writeValue(stream, value.toList())
+ }
+ is PeripheralCharacteristic -> {
+ stream.write(144)
+ writeValue(stream, value.toList())
+ }
+ is PeripheralDescriptor -> {
+ stream.write(145)
+ writeValue(stream, value.toList())
+ }
+ is PeripheralReadRequestResult -> {
+ stream.write(146)
+ writeValue(stream, value.toList())
+ }
+ is PeripheralWriteRequestResult -> {
+ stream.write(147)
+ writeValue(stream, value.toList())
+ }
+ is PeripheralManufacturerData -> {
+ stream.write(148)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
@@ -1197,3 +1738,397 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger,
}
}
}
+/**
+ * Flutter -> Native (peripheral)
+ *
+ * Generated interface from Pigeon that represents a handler of messages from Flutter.
+ */
+interface UniversalBlePeripheralChannel {
+ fun getAdvertisingState(): PeripheralAdvertisingState
+ fun getReadinessState(): PeripheralReadinessState
+ fun stopAdvertising()
+ fun addService(service: PeripheralService)
+ fun removeService(serviceId: String)
+ fun clearServices()
+ fun getServices(): List
+ fun startAdvertising(services: List, localName: String?, timeout: Long?, manufacturerData: PeripheralManufacturerData?, addManufacturerDataInScanResponse: Boolean)
+ fun updateCharacteristic(characteristicId: String, value: ByteArray, deviceId: String?)
+ /**
+ * Returns peripheral-client device ids currently subscribed to [characteristicId]
+ * (e.g. HID report characteristic). Used to restore app state after restart.
+ */
+ fun getSubscribedClients(characteristicId: String): List
+ /** Returns max characteristic notify payload length for a connected client. */
+ fun getMaximumNotifyLength(deviceId: String): Long?
+
+ companion object {
+ /** The codec used by UniversalBlePeripheralChannel. */
+ val codec: MessageCodec by lazy {
+ UniversalBlePigeonCodec()
+ }
+ /** Sets up an instance of `UniversalBlePeripheralChannel` to handle messages through the `binaryMessenger`. */
+ @JvmOverloads
+ fun setUp(binaryMessenger: BinaryMessenger, api: UniversalBlePeripheralChannel?, messageChannelSuffix: String = "") {
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getAdvertisingState$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.getAdvertisingState())
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getReadinessState$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.getReadinessState())
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.stopAdvertising$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ api.stopAdvertising()
+ listOf(null)
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.addService$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val serviceArg = args[0] as PeripheralService
+ val wrapped: List = try {
+ api.addService(serviceArg)
+ listOf(null)
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.removeService$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val serviceIdArg = args[0] as String
+ val wrapped: List = try {
+ api.removeService(serviceIdArg)
+ listOf(null)
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.clearServices$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ api.clearServices()
+ listOf(null)
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getServices$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.getServices())
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.startAdvertising$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val servicesArg = args[0] as List
+ val localNameArg = args[1] as String?
+ val timeoutArg = args[2] as Long?
+ val manufacturerDataArg = args[3] as PeripheralManufacturerData?
+ val addManufacturerDataInScanResponseArg = args[4] as Boolean
+ val wrapped: List = try {
+ api.startAdvertising(servicesArg, localNameArg, timeoutArg, manufacturerDataArg, addManufacturerDataInScanResponseArg)
+ listOf(null)
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.updateCharacteristic$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val characteristicIdArg = args[0] as String
+ val valueArg = args[1] as ByteArray
+ val deviceIdArg = args[2] as String?
+ val wrapped: List = try {
+ api.updateCharacteristic(characteristicIdArg, valueArg, deviceIdArg)
+ listOf(null)
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getSubscribedClients$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val characteristicIdArg = args[0] as String
+ val wrapped: List = try {
+ listOf(api.getSubscribedClients(characteristicIdArg))
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getMaximumNotifyLength$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val deviceIdArg = args[0] as String
+ val wrapped: List = try {
+ listOf(api.getMaximumNotifyLength(deviceIdArg))
+ } catch (exception: Throwable) {
+ UniversalBlePigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
+/**
+ * Native -> Flutter (peripheral)
+ *
+ * Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
+ */
+class UniversalBlePeripheralCallback(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
+ companion object {
+ /** The codec used by UniversalBlePeripheralCallback. */
+ val codec: MessageCodec by lazy {
+ UniversalBlePigeonCodec()
+ }
+ }
+ fun onReadRequest(deviceIdArg: String, characteristicIdArg: String, offsetArg: Long, valueArg: ByteArray?, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onReadRequest$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(deviceIdArg, characteristicIdArg, offsetArg, valueArg)) {
+ 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 {
+ val output = it[0] as PeripheralReadRequestResult?
+ callback(Result.success(output))
+ }
+ } else {
+ callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+ fun onWriteRequest(deviceIdArg: String, characteristicIdArg: String, offsetArg: Long, valueArg: ByteArray?, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onWriteRequest$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(deviceIdArg, characteristicIdArg, offsetArg, valueArg)) {
+ 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 {
+ val output = it[0] as PeripheralWriteRequestResult?
+ callback(Result.success(output))
+ }
+ } else {
+ callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+ fun onDescriptorReadRequest(deviceIdArg: String, characteristicIdArg: String, descriptorIdArg: String, offsetArg: Long, valueArg: ByteArray?, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorReadRequest$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg)) {
+ 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 {
+ val output = it[0] as PeripheralReadRequestResult?
+ callback(Result.success(output))
+ }
+ } else {
+ callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+ fun onDescriptorWriteRequest(deviceIdArg: String, characteristicIdArg: String, descriptorIdArg: String, offsetArg: Long, valueArg: ByteArray?, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorWriteRequest$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg)) {
+ 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 {
+ val output = it[0] as PeripheralWriteRequestResult?
+ callback(Result.success(output))
+ }
+ } else {
+ callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+ fun onCharacteristicSubscriptionChange(deviceIdArg: String, characteristicIdArg: String, isSubscribedArg: Boolean, nameArg: String?, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onCharacteristicSubscriptionChange$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(deviceIdArg, characteristicIdArg, isSubscribedArg, nameArg)) {
+ 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 onAdvertisingStateChange(stateArg: PeripheralAdvertisingState, errorArg: String?, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onAdvertisingStateChange$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(stateArg, errorArg)) {
+ 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 onServiceAdded(serviceIdArg: String, errorArg: String?, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onServiceAdded$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(serviceIdArg, errorArg)) {
+ 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 onMtuChange(deviceIdArg: String, mtuArg: Long, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onMtuChange$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(deviceIdArg, mtuArg)) {
+ 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 onConnectionStateChange(deviceIdArg: String, connectedArg: Boolean, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onConnectionStateChange$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)))
+ }
+ }
+ }
+}
diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralExtensions.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralExtensions.kt
new file mode 100644
index 00000000..86e96ba3
--- /dev/null
+++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralExtensions.kt
@@ -0,0 +1,158 @@
+package com.navideck.universal_ble
+
+import android.app.Activity
+import android.bluetooth.BluetoothGattCharacteristic
+import android.bluetooth.BluetoothGattDescriptor
+import android.bluetooth.BluetoothGattService
+import android.content.pm.PackageManager
+import android.util.Log
+import java.util.Collections
+import java.util.UUID
+
+private val bluetoothGattCharacteristics:
+ MutableMap = HashMap()
+private val descriptorValueReadMap: MutableMap = HashMap()
+val subscribedCharDevicesMap: MutableMap> = HashMap()
+const val peripheralDescriptorCCUUID = "00002902-0000-1000-8000-00805f9b34fb"
+
+fun clearPeripheralCaches() {
+ synchronized(bluetoothGattCharacteristics) {
+ bluetoothGattCharacteristics.clear()
+ }
+ synchronized(descriptorValueReadMap) {
+ descriptorValueReadMap.clear()
+ }
+ synchronized(subscribedCharDevicesMap) {
+ subscribedCharDevicesMap.clear()
+ }
+}
+
+fun Activity.havePermission(permissions: Array): Boolean {
+ for (perm in permissions) {
+ if (checkCallingOrSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
+ return false
+ }
+ }
+ return true
+}
+
+fun PeripheralService.toGattService(): BluetoothGattService {
+ val service = BluetoothGattService(
+ UUID.fromString(uuid),
+ if (primary) BluetoothGattService.SERVICE_TYPE_PRIMARY else BluetoothGattService.SERVICE_TYPE_SECONDARY,
+ )
+ characteristics.forEach {
+ service.addCharacteristic(it.toGattCharacteristic())
+ }
+ return service
+}
+
+fun PeripheralCharacteristic.toGattCharacteristic(): BluetoothGattCharacteristic {
+ val characteristic = BluetoothGattCharacteristic(
+ UUID.fromString(uuid),
+ properties.toPropertiesList(),
+ permissions.toPermissionsList(),
+ )
+ value?.let { characteristic.value = it }
+ descriptors?.forEach {
+ characteristic.addDescriptor(it.toGattDescriptor())
+ }
+
+ addCCDescriptorIfRequired(this, characteristic)
+ synchronized(bluetoothGattCharacteristics) {
+ if (bluetoothGattCharacteristics[uuid] == null) {
+ bluetoothGattCharacteristics[uuid] = characteristic
+ }
+ }
+ return characteristic
+}
+
+private fun addCCDescriptorIfRequired(
+ peripheralCharacteristic: PeripheralCharacteristic,
+ characteristic: BluetoothGattCharacteristic,
+) {
+ val hasNotifyOrIndicate =
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0 ||
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0
+ if (!hasNotifyOrIndicate) return
+
+ var hasCccd = false
+ for (descriptor in peripheralCharacteristic.descriptors ?: Collections.emptyList()) {
+ if (descriptor.uuid.equals(peripheralDescriptorCCUUID, ignoreCase = true)) {
+ hasCccd = true
+ break
+ }
+ }
+ if (hasCccd) return
+
+ val cccd = BluetoothGattDescriptor(
+ UUID.fromString(peripheralDescriptorCCUUID),
+ BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE,
+ )
+ cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
+ characteristic.addDescriptor(cccd)
+ Log.d("UniversalBlePeripheral", "Added CCCD for ${characteristic.uuid}")
+}
+
+fun PeripheralDescriptor.toGattDescriptor(): BluetoothGattDescriptor {
+ val permission = permissions?.toPermissionsList()
+ ?: BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
+ val descriptor = BluetoothGattDescriptor(UUID.fromString(uuid), permission)
+ value?.let {
+ descriptor.value = it
+ synchronized(descriptorValueReadMap) {
+ descriptorValueReadMap[uuid.lowercase()] = it
+ }
+ }
+ return descriptor
+}
+
+fun BluetoothGattDescriptor.getCacheValue(): ByteArray? =
+ synchronized(descriptorValueReadMap) {
+ descriptorValueReadMap[uuid.toString().lowercase()]
+ }
+
+fun String.findCharacteristic(): BluetoothGattCharacteristic? =
+ synchronized(bluetoothGattCharacteristics) {
+ bluetoothGattCharacteristics[this]
+ }
+
+fun String.findService(): BluetoothGattService? {
+ synchronized(bluetoothGattCharacteristics) {
+ for (characteristic in bluetoothGattCharacteristics.values) {
+ if (characteristic.service?.uuid.toString() == this) {
+ return characteristic.service
+ }
+ }
+ }
+ return null
+}
+
+private fun List.toPropertiesList(): Int =
+ map { it.toInt() }.fold(0) { acc, i -> acc or i.toPropertyBits() }
+
+private fun List.toPermissionsList(): Int =
+ map { it.toInt() }.fold(0) { acc, i -> acc or i.toPermissionBits() }
+
+private fun Int.toPropertyBits(): Int = when (this) {
+ 0 -> BluetoothGattCharacteristic.PROPERTY_BROADCAST
+ 1 -> BluetoothGattCharacteristic.PROPERTY_READ
+ 2 -> BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
+ 3 -> BluetoothGattCharacteristic.PROPERTY_WRITE
+ 4 -> BluetoothGattCharacteristic.PROPERTY_NOTIFY
+ 5 -> BluetoothGattCharacteristic.PROPERTY_INDICATE
+ 6 -> BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE
+ 7 -> BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS
+ // Android uses dedicated encrypted notify/indicate property bits.
+ 8 -> 0x100 // PROPERTY_NOTIFY_ENCRYPTED
+ 9 -> 0x200 // PROPERTY_INDICATE_ENCRYPTED
+ else -> 0
+}
+
+private fun Int.toPermissionBits(): Int = when (this) {
+ 0 -> BluetoothGattCharacteristic.PERMISSION_READ
+ 1 -> BluetoothGattCharacteristic.PERMISSION_WRITE
+ 2 -> BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED
+ 3 -> BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED
+ else -> 0
+}
diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt
new file mode 100644
index 00000000..0a2fe387
--- /dev/null
+++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt
@@ -0,0 +1,550 @@
+package com.navideck.universal_ble
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+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.le.AdvertiseCallback
+import android.bluetooth.le.AdvertiseData
+import android.bluetooth.le.AdvertiseSettings
+import android.bluetooth.le.BluetoothLeAdvertiser
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Context.RECEIVER_NOT_EXPORTED
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import android.os.Handler
+import android.os.ParcelUuid
+import android.util.Log
+import io.flutter.plugin.common.BinaryMessenger
+
+private const val TAG = "UniversalBlePeripheral"
+
+@SuppressLint("MissingPermission")
+class UniversalBlePeripheralPlugin(
+ private val applicationContext: Context,
+ messenger: BinaryMessenger,
+) : UniversalBlePeripheralChannel {
+ private var activity: Activity? = null
+ private val callback = UniversalBlePeripheralCallback(messenger)
+ private val handler = Handler(applicationContext.mainLooper)
+ private val bluetoothManager =
+ applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ private var bluetoothLeAdvertiser: BluetoothLeAdvertiser? = null
+ private var gattServer: BluetoothGattServer? = null
+ private val bluetoothDevicesMap: MutableMap = HashMap()
+ private val mtuByDeviceId: MutableMap = HashMap()
+ private val listOfDevicesWaitingForBond = mutableListOf()
+ private val emptyBytes = byteArrayOf()
+ private var advertisingState: PeripheralAdvertisingState = PeripheralAdvertisingState.IDLE
+ private var receiverRegistered = false
+ private var originalAdapterName: String? = null
+ private var adapterNameOverridden = false
+
+ init {
+ kotlin.runCatching { initializePeripheral() }.onFailure {
+ Log.w(TAG, "Deferred peripheral init: ${it.message}")
+ }
+ }
+
+ fun attachActivity(activity: Activity?) {
+ this.activity = activity
+ }
+
+ fun dispose() {
+ if (receiverRegistered) {
+ kotlin.runCatching { applicationContext.unregisterReceiver(broadcastReceiver) }
+ receiverRegistered = false
+ }
+ kotlin.runCatching { bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback) }
+ restoreAdapterNameIfNeeded()
+ gattServer?.close()
+ gattServer = null
+ bluetoothDevicesMap.clear()
+ synchronized(listOfDevicesWaitingForBond) {
+ listOfDevicesWaitingForBond.clear()
+ }
+ synchronized(mtuByDeviceId) { mtuByDeviceId.clear() }
+ clearPeripheralCaches()
+ }
+
+ private fun initializePeripheral() {
+ val adapter = bluetoothManager.adapter
+ ?: throw UnsupportedOperationException("Bluetooth is not available.")
+ if (bluetoothLeAdvertiser != null && gattServer != null && receiverRegistered) return
+ bluetoothLeAdvertiser = adapter.bluetoothLeAdvertiser
+ ?: throw UnsupportedOperationException(
+ "Bluetooth LE Advertising not supported on this device.",
+ )
+ gattServer = bluetoothManager.openGattServer(applicationContext, gattServerCallback)
+ ?: throw UnsupportedOperationException("gattServer is null, check Bluetooth is ON.")
+
+ if (!receiverRegistered) {
+ val intentFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ applicationContext.registerReceiver(
+ broadcastReceiver,
+ intentFilter,
+ RECEIVER_NOT_EXPORTED,
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ applicationContext.registerReceiver(broadcastReceiver, intentFilter)
+ }
+ receiverRegistered = true
+ }
+ }
+
+ override fun getAdvertisingState(): PeripheralAdvertisingState = advertisingState
+
+ override fun getReadinessState(): PeripheralReadinessState {
+ val adapter = bluetoothManager.adapter ?: return PeripheralReadinessState.UNSUPPORTED
+ if (!adapter.isMultipleAdvertisementSupported) return PeripheralReadinessState.UNSUPPORTED
+ if (!adapter.isEnabled) return PeripheralReadinessState.BLUETOOTH_OFF
+ return PeripheralReadinessState.READY
+ }
+
+ override fun addService(service: PeripheralService) {
+ initializePeripheral()
+ gattServer?.addService(service.toGattService())
+ }
+
+ override fun removeService(serviceId: String) {
+ serviceId.findService()?.let { gattServer?.removeService(it) }
+ }
+
+ override fun clearServices() {
+ initializePeripheral()
+ gattServer?.clearServices()
+ }
+
+ override fun getServices(): List =
+ runCatching {
+ initializePeripheral()
+ gattServer?.services?.map { it.uuid.toString() } ?: emptyList()
+ }.getOrDefault(emptyList())
+
+ override fun startAdvertising(
+ services: List,
+ localName: String?,
+ timeout: Long?,
+ manufacturerData: PeripheralManufacturerData?,
+ addManufacturerDataInScanResponse: Boolean,
+ ) {
+ initializePeripheral()
+ advertisingState = PeripheralAdvertisingState.STARTING
+ callback.onAdvertisingStateChange(PeripheralAdvertisingState.STARTING, null) {}
+ if (!isBluetoothEnabled()) {
+ activity?.startActivityForResult(
+ Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE),
+ 0xB1E,
+ )
+ throw Exception("Bluetooth is not enabled")
+ }
+
+ handler.post {
+ val adapter = bluetoothManager.adapter
+ if (localName != null && adapter != null && adapter.name != localName) {
+ if (!adapterNameOverridden) {
+ originalAdapterName = adapter.name
+ }
+ adapter.name = localName
+ adapterNameOverridden = true
+ }
+ val advertiseSettings = AdvertiseSettings.Builder()
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
+ .setConnectable(true)
+ .setTimeout(timeout?.toInt() ?: 0)
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
+ .build()
+
+ val advertiseDataBuilder = AdvertiseData.Builder()
+ .setIncludeTxPowerLevel(false)
+ .setIncludeDeviceName(localName != null)
+ val scanResponseBuilder = AdvertiseData.Builder()
+ .setIncludeTxPowerLevel(false)
+ .setIncludeDeviceName(localName != null)
+
+ manufacturerData?.let {
+ if (addManufacturerDataInScanResponse) {
+ scanResponseBuilder.addManufacturerData(
+ it.manufacturerId.toInt(),
+ it.data,
+ )
+ } else {
+ advertiseDataBuilder.addManufacturerData(
+ it.manufacturerId.toInt(),
+ it.data,
+ )
+ }
+ }
+ services.forEach { advertiseDataBuilder.addServiceUuid(ParcelUuid.fromString(it)) }
+
+ bluetoothLeAdvertiser?.startAdvertising(
+ advertiseSettings,
+ advertiseDataBuilder.build(),
+ scanResponseBuilder.build(),
+ advertiseCallback,
+ )
+ }
+ }
+
+ override fun stopAdvertising() {
+ initializePeripheral()
+ advertisingState = PeripheralAdvertisingState.STOPPING
+ callback.onAdvertisingStateChange(PeripheralAdvertisingState.STOPPING, null) {}
+ handler.post {
+ bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback)
+ restoreAdapterNameIfNeeded()
+ advertisingState = PeripheralAdvertisingState.IDLE
+ callback.onAdvertisingStateChange(PeripheralAdvertisingState.IDLE, null) {}
+ }
+ }
+
+ override fun updateCharacteristic(characteristicId: String, value: ByteArray, deviceId: String?) {
+ initializePeripheral()
+ val characteristic =
+ characteristicId.findCharacteristic() ?: throw Exception("Characteristic not found")
+ characteristic.value = value
+ val targetDevices = synchronized(bluetoothDevicesMap) {
+ if (deviceId != null) {
+ listOf(bluetoothDevicesMap[deviceId] ?: throw Exception("Device not found"))
+ } else {
+ bluetoothDevicesMap.values.toList()
+ }
+ }
+ val indicate =
+ (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0
+ targetDevices.forEach { device ->
+ handler.post {
+ gattServer?.notifyCharacteristicChanged(device, characteristic, indicate)
+ }
+ }
+ }
+
+ override fun getSubscribedClients(characteristicId: String): List {
+ synchronized(subscribedCharDevicesMap) {
+ return subscribedCharDevicesMap.entries
+ .filter { (_, chars) ->
+ chars.any { it.equals(characteristicId, ignoreCase = true) }
+ }
+ .map { it.key }
+ }
+ }
+
+ override fun getMaximumNotifyLength(deviceId: String): Long? {
+ val mtu = synchronized(mtuByDeviceId) { mtuByDeviceId[deviceId] } ?: return null
+ return (mtu - 3).coerceAtLeast(0).toLong()
+ }
+
+ private fun isBluetoothEnabled(): Boolean =
+ bluetoothManager.adapter?.isEnabled ?: false
+
+ private fun onConnectionUpdate(device: BluetoothDevice, status: Int, newState: Int) {
+ Log.e(TAG, "onConnectionStateChange: $status -> $newState")
+ handler.post {
+ callback.onConnectionStateChange(
+ device.address,
+ newState == BluetoothProfile.STATE_CONNECTED,
+ ) {}
+ }
+ if (newState == BluetoothProfile.STATE_DISCONNECTED) cleanConnection(device)
+ }
+
+ private fun cleanConnection(device: BluetoothDevice) {
+ val deviceAddress = device.address
+ val subscribedCharUUID = synchronized(subscribedCharDevicesMap) {
+ val current = subscribedCharDevicesMap[deviceAddress]?.toList() ?: emptyList()
+ subscribedCharDevicesMap.remove(deviceAddress)
+ current
+ }
+ subscribedCharUUID.forEach { charUUID ->
+ handler.post {
+ callback.onCharacteristicSubscriptionChange(
+ deviceAddress,
+ charUUID,
+ false,
+ device.name,
+ ) {}
+ }
+ }
+ }
+
+ private val advertiseCallback = object : AdvertiseCallback() {
+ override fun onStartFailure(errorCode: Int) {
+ super.onStartFailure(errorCode)
+ handler.post {
+ restoreAdapterNameIfNeeded()
+ val errorMessage = when (errorCode) {
+ ADVERTISE_FAILED_ALREADY_STARTED -> "Already started"
+ ADVERTISE_FAILED_DATA_TOO_LARGE -> "Data too large"
+ ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "Feature unsupported"
+ ADVERTISE_FAILED_INTERNAL_ERROR -> "Internal error"
+ ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "Too many advertisers"
+ else -> "Failed to start advertising: $errorCode"
+ }
+ advertisingState = PeripheralAdvertisingState.ERROR
+ callback.onAdvertisingStateChange(PeripheralAdvertisingState.ERROR, errorMessage) {}
+ }
+ }
+
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
+ super.onStartSuccess(settingsInEffect)
+ advertisingState = PeripheralAdvertisingState.ADVERTISING
+ handler.post {
+ callback.onAdvertisingStateChange(PeripheralAdvertisingState.ADVERTISING, null) {}
+ }
+ }
+ }
+
+ private val gattServerCallback = object : BluetoothGattServerCallback() {
+ override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
+ super.onConnectionStateChange(device, status, newState)
+ when (newState) {
+ BluetoothProfile.STATE_CONNECTED -> {
+ synchronized(bluetoothDevicesMap) {
+ bluetoothDevicesMap[device.address] = device
+ }
+ if (device.bondState == BluetoothDevice.BOND_NONE) {
+ synchronized(listOfDevicesWaitingForBond) {
+ listOfDevicesWaitingForBond.add(device.address)
+ }
+ device.createBond()
+ } else if (device.bondState == BluetoothDevice.BOND_BONDED) {
+ handler.post { gattServer?.connect(device, true) }
+ }
+ onConnectionUpdate(device, status, newState)
+ }
+ BluetoothProfile.STATE_DISCONNECTED -> {
+ synchronized(bluetoothDevicesMap) {
+ bluetoothDevicesMap.remove(device.address)
+ }
+ onConnectionUpdate(device, status, newState)
+ }
+ }
+ }
+
+ override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) {
+ super.onMtuChanged(device, mtu)
+ device?.address?.let { address ->
+ synchronized(mtuByDeviceId) { mtuByDeviceId[address] = mtu }
+ handler.post { callback.onMtuChange(address, mtu.toLong()) {} }
+ }
+ }
+
+ override fun onCharacteristicReadRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ offset: Int,
+ characteristic: BluetoothGattCharacteristic,
+ ) {
+ super.onCharacteristicReadRequest(device, requestId, offset, characteristic)
+ if (gattServer == null) return
+ handler.post {
+ callback.onReadRequest(
+ deviceIdArg = device.address,
+ characteristicIdArg = characteristic.uuid.toString(),
+ offsetArg = offset.toLong(),
+ valueArg = characteristic.value,
+ ) { result ->
+ val readResult = result.getOrNull()
+ if (readResult == null) {
+ gattServer?.sendResponse(
+ device, requestId, BluetoothGatt.GATT_FAILURE, 0, emptyBytes,
+ )
+ } else {
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ readResult.status?.toInt() ?: BluetoothGatt.GATT_SUCCESS,
+ readResult.offset?.toInt() ?: 0,
+ readResult.value,
+ )
+ }
+ }
+ }
+ }
+
+ override fun onCharacteristicWriteRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ characteristic: BluetoothGattCharacteristic,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray,
+ ) {
+ super.onCharacteristicWriteRequest(
+ device, requestId, characteristic, preparedWrite, responseNeeded, offset, value,
+ )
+ handler.post {
+ callback.onWriteRequest(
+ deviceIdArg = device.address,
+ characteristicIdArg = characteristic.uuid.toString(),
+ offsetArg = offset.toLong(),
+ valueArg = value,
+ ) { writeResponse ->
+ val writeResult = writeResponse.getOrNull()
+ if (responseNeeded) {
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ writeResult?.status?.toInt() ?: BluetoothGatt.GATT_SUCCESS,
+ writeResult?.offset?.toInt() ?: 0,
+ writeResult?.value ?: emptyBytes,
+ )
+ }
+ }
+ }
+ }
+
+ override fun onServiceAdded(status: Int, service: BluetoothGattService) {
+ super.onServiceAdded(status, service)
+ val error = if (status != BluetoothGatt.GATT_SUCCESS) "Adding Service failed" else null
+ handler.post { callback.onServiceAdded(service.uuid.toString(), error) {} }
+ }
+
+ override fun onDescriptorReadRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ offset: Int,
+ descriptor: BluetoothGattDescriptor,
+ ) {
+ super.onDescriptorReadRequest(device, requestId, offset, descriptor)
+ handler.post {
+ callback.onDescriptorReadRequest(
+ deviceIdArg = device.address,
+ characteristicIdArg = descriptor.characteristic.uuid.toString(),
+ descriptorIdArg = descriptor.uuid.toString(),
+ offsetArg = offset.toLong(),
+ valueArg = descriptor.getCacheValue(),
+ ) { result ->
+ val readResult = result.getOrNull()
+ if (readResult == null) {
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_FAILURE,
+ offset,
+ emptyBytes,
+ )
+ } else {
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ readResult.status?.toInt() ?: BluetoothGatt.GATT_SUCCESS,
+ readResult.offset?.toInt() ?: offset,
+ readResult.value,
+ )
+ }
+ }
+ }
+ }
+
+ override fun onDescriptorWriteRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ descriptor: BluetoothGattDescriptor,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray?,
+ ) {
+ super.onDescriptorWriteRequest(
+ device, requestId, descriptor, preparedWrite, responseNeeded, offset, value,
+ )
+ descriptor.value = value
+ if (descriptor.uuid.toString().lowercase() == peripheralDescriptorCCUUID.lowercase()) {
+ val isSubscribed =
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value) ||
+ BluetoothGattDescriptor.ENABLE_INDICATION_VALUE.contentEquals(value)
+ val characteristicId = descriptor.characteristic.uuid.toString()
+ device?.address?.let { address ->
+ handler.post {
+ callback.onCharacteristicSubscriptionChange(
+ address, characteristicId, isSubscribed, device.name,
+ ) {}
+ }
+ synchronized(subscribedCharDevicesMap) {
+ val charList = subscribedCharDevicesMap[address] ?: mutableListOf()
+ if (isSubscribed) {
+ charList.add(characteristicId)
+ } else {
+ charList.remove(characteristicId)
+ }
+ subscribedCharDevicesMap[address] = charList
+ }
+ }
+ }
+ if (responseNeeded) {
+ handler.post {
+ callback.onDescriptorWriteRequest(
+ deviceIdArg = device?.address ?: "",
+ characteristicIdArg = descriptor.characteristic.uuid.toString(),
+ descriptorIdArg = descriptor.uuid.toString(),
+ offsetArg = offset.toLong(),
+ valueArg = value,
+ ) { writeResponse ->
+ val writeResult = writeResponse.getOrNull()
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ writeResult?.status?.toInt() ?: BluetoothGatt.GATT_SUCCESS,
+ writeResult?.offset?.toInt() ?: offset,
+ writeResult?.value ?: value ?: emptyBytes,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private val broadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
+ val state = intent.getIntExtra(
+ BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR,
+ )
+ val device: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ }
+ val waitingForConnection = synchronized(listOfDevicesWaitingForBond) {
+ listOfDevicesWaitingForBond.contains(device?.address)
+ }
+ if (state == BluetoothDevice.BOND_BONDED && device != null && waitingForConnection) {
+ synchronized(listOfDevicesWaitingForBond) {
+ listOfDevicesWaitingForBond.remove(device.address)
+ }
+ synchronized(bluetoothDevicesMap) {
+ bluetoothDevicesMap[device.address] = device
+ }
+ handler.post { gattServer?.connect(device, true) }
+ }
+ }
+ }
+ }
+ }
+
+ private fun restoreAdapterNameIfNeeded() {
+ if (!adapterNameOverridden) return
+ val adapter = bluetoothManager.adapter ?: return
+ kotlin.runCatching {
+ adapter.name = originalAdapterName
+ }
+ adapterNameOverridden = false
+ originalAdapterName = null
+ }
+}
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 beac594f..eb5247d4 100644
--- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt
+++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt
@@ -50,6 +50,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
private lateinit var safeScanner: SafeScanner
private val cachedServicesMap = mutableMapOf>()
private val universalBleFilterUtil = UniversalBleFilterUtil()
+ private lateinit var peripheralPlugin: UniversalBlePeripheralPlugin
// Flutter Futures
private var bluetoothEnableRequestFuture: ((Result) -> Unit)? = null
@@ -65,6 +66,11 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this)
+ peripheralPlugin = UniversalBlePeripheralPlugin(
+ flutterPluginBinding.applicationContext,
+ flutterPluginBinding.binaryMessenger
+ )
+ UniversalBlePeripheralChannel.setUp(flutterPluginBinding.binaryMessenger, peripheralPlugin)
callbackChannel = UniversalBleCallbackChannel(flutterPluginBinding.binaryMessenger)
context = flutterPluginBinding.applicationContext
mainThreadHandler = Handler(Looper.getMainLooper())
@@ -85,6 +91,8 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback)
context.unregisterReceiver(broadcastReceiver)
+ peripheralPlugin.dispose()
+ UniversalBlePeripheralChannel.setUp(binding.binaryMessenger, null)
callbackChannel = null
mainThreadHandler = null
permissionHandler = null
@@ -1355,6 +1363,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
+ peripheralPlugin.attachActivity(binding.activity)
binding.addActivityResultListener(this)
binding.addRequestPermissionsResultListener(this)
permissionHandler?.attachActivity(binding.activity)
@@ -1362,6 +1371,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
override fun onDetachedFromActivity() {
activity = null
+ peripheralPlugin.attachActivity(null)
permissionHandler?.attachActivity(null)
}
@@ -1371,6 +1381,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
+ peripheralPlugin.attachActivity(binding.activity)
permissionHandler?.attachActivity(binding.activity)
}
diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift
index dc40b39f..45a74ea2 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.3), 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 is (Void, Void):
+ case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
- case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
- return cleanLhsHashable == cleanRhsHashable
+ case is (Void, Void):
+ return true
- 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
- }
-
- if let hashableValue = value as? AnyHashable {
- hasher.combine(hashableValue.hashValue)
+ } else {
+ hasher.combine(0)
}
-
- return hasher.combine(String(describing: value))
}
-
enum UniversalBleLogLevel: Int {
case none = 0
@@ -149,6 +195,22 @@ enum AndroidScanMode: Int {
case opportunistic = 3
}
+enum PeripheralReadinessState: Int {
+ case unknown = 0
+ case ready = 1
+ case bluetoothOff = 2
+ case unauthorized = 3
+ case unsupported = 4
+}
+
+enum PeripheralAdvertisingState: Int {
+ case idle = 0
+ case starting = 1
+ case advertising = 2
+ case stopping = 3
+ case error = 4
+}
+
/// Unified error codes for all platforms
enum UniversalBleErrorCode: Int {
case unknownError = 0
@@ -261,9 +323,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 +365,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 +405,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 +438,15 @@ 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)
}
}
@@ -388,9 +484,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 +517,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 +558,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 +599,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 +636,267 @@ 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) {
+ hasher.combine("UniversalManufacturerData")
+ deepHashUniversalBle(value: companyIdentifier, hasher: &hasher)
+ deepHashUniversalBle(value: data, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct PeripheralService: Hashable {
+ var uuid: String
+ var primary: Bool
+ var characteristics: [PeripheralCharacteristic]
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> PeripheralService? {
+ let uuid = pigeonVar_list[0] as! String
+ let primary = pigeonVar_list[1] as! Bool
+ let characteristics = pigeonVar_list[2] as! [PeripheralCharacteristic]
+
+ return PeripheralService(
+ uuid: uuid,
+ primary: primary,
+ characteristics: characteristics
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ uuid,
+ primary,
+ characteristics,
+ ]
+ }
+ static func == (lhs: PeripheralService, rhs: PeripheralService) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.primary, rhs.primary) && deepEqualsUniversalBle(lhs.characteristics, rhs.characteristics)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("PeripheralService")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ deepHashUniversalBle(value: primary, hasher: &hasher)
+ deepHashUniversalBle(value: characteristics, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct PeripheralCharacteristic: Hashable {
+ var uuid: String
+ var properties: [Int64]
+ var permissions: [Int64]
+ var descriptors: [PeripheralDescriptor]? = nil
+ var value: FlutterStandardTypedData? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> PeripheralCharacteristic? {
+ let uuid = pigeonVar_list[0] as! String
+ let properties = pigeonVar_list[1] as! [Int64]
+ let permissions = pigeonVar_list[2] as! [Int64]
+ let descriptors: [PeripheralDescriptor]? = nilOrValue(pigeonVar_list[3])
+ let value: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[4])
+
+ return PeripheralCharacteristic(
+ uuid: uuid,
+ properties: properties,
+ permissions: permissions,
+ descriptors: descriptors,
+ value: value
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ uuid,
+ properties,
+ permissions,
+ descriptors,
+ value,
+ ]
+ }
+ static func == (lhs: PeripheralCharacteristic, rhs: PeripheralCharacteristic) -> 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.value, rhs.value)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("PeripheralCharacteristic")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ deepHashUniversalBle(value: properties, hasher: &hasher)
+ deepHashUniversalBle(value: permissions, hasher: &hasher)
+ deepHashUniversalBle(value: descriptors, hasher: &hasher)
+ deepHashUniversalBle(value: value, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct PeripheralDescriptor: Hashable {
+ var uuid: String
+ var value: FlutterStandardTypedData? = nil
+ var permissions: [Int64]? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> PeripheralDescriptor? {
+ let uuid = pigeonVar_list[0] as! String
+ let value: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[1])
+ let permissions: [Int64]? = nilOrValue(pigeonVar_list[2])
+
+ return PeripheralDescriptor(
+ uuid: uuid,
+ value: value,
+ permissions: permissions
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ uuid,
+ value,
+ permissions,
+ ]
+ }
+ static func == (lhs: PeripheralDescriptor, rhs: PeripheralDescriptor) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.value, rhs.value) && deepEqualsUniversalBle(lhs.permissions, rhs.permissions)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("PeripheralDescriptor")
+ deepHashUniversalBle(value: uuid, hasher: &hasher)
+ deepHashUniversalBle(value: value, hasher: &hasher)
+ deepHashUniversalBle(value: permissions, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct PeripheralReadRequestResult: Hashable {
+ var value: FlutterStandardTypedData
+ var offset: Int64? = nil
+ var status: Int64? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> PeripheralReadRequestResult? {
+ let value = pigeonVar_list[0] as! FlutterStandardTypedData
+ let offset: Int64? = nilOrValue(pigeonVar_list[1])
+ let status: Int64? = nilOrValue(pigeonVar_list[2])
+
+ return PeripheralReadRequestResult(
+ value: value,
+ offset: offset,
+ status: status
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ value,
+ offset,
+ status,
+ ]
+ }
+ static func == (lhs: PeripheralReadRequestResult, rhs: PeripheralReadRequestResult) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.value, rhs.value) && deepEqualsUniversalBle(lhs.offset, rhs.offset) && deepEqualsUniversalBle(lhs.status, rhs.status)
+ }
+
func hash(into hasher: inout Hasher) {
- deepHashUniversalBle(value: toList(), hasher: &hasher)
+ hasher.combine("PeripheralReadRequestResult")
+ deepHashUniversalBle(value: value, hasher: &hasher)
+ deepHashUniversalBle(value: offset, hasher: &hasher)
+ deepHashUniversalBle(value: status, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct PeripheralWriteRequestResult: Hashable {
+ var value: FlutterStandardTypedData? = nil
+ var offset: Int64? = nil
+ var status: Int64? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> PeripheralWriteRequestResult? {
+ let value: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[0])
+ let offset: Int64? = nilOrValue(pigeonVar_list[1])
+ let status: Int64? = nilOrValue(pigeonVar_list[2])
+
+ return PeripheralWriteRequestResult(
+ value: value,
+ offset: offset,
+ status: status
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ value,
+ offset,
+ status,
+ ]
+ }
+ static func == (lhs: PeripheralWriteRequestResult, rhs: PeripheralWriteRequestResult) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.value, rhs.value) && deepEqualsUniversalBle(lhs.offset, rhs.offset) && deepEqualsUniversalBle(lhs.status, rhs.status)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("PeripheralWriteRequestResult")
+ deepHashUniversalBle(value: value, hasher: &hasher)
+ deepHashUniversalBle(value: offset, hasher: &hasher)
+ deepHashUniversalBle(value: status, hasher: &hasher)
+ }
+}
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct PeripheralManufacturerData: Hashable {
+ var manufacturerId: Int64
+ var data: FlutterStandardTypedData
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> PeripheralManufacturerData? {
+ let manufacturerId = pigeonVar_list[0] as! Int64
+ let data = pigeonVar_list[1] as! FlutterStandardTypedData
+
+ return PeripheralManufacturerData(
+ manufacturerId: manufacturerId,
+ data: data
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ manufacturerId,
+ data,
+ ]
+ }
+ static func == (lhs: PeripheralManufacturerData, rhs: PeripheralManufacturerData) -> Bool {
+ if Swift.type(of: lhs) != Swift.type(of: rhs) {
+ return false
+ }
+ return deepEqualsUniversalBle(lhs.manufacturerId, rhs.manufacturerId) && deepEqualsUniversalBle(lhs.data, rhs.data)
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine("PeripheralManufacturerData")
+ deepHashUniversalBle(value: manufacturerId, hasher: &hasher)
+ deepHashUniversalBle(value: data, hasher: &hasher)
}
}
@@ -534,27 +918,51 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader {
case 131:
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
if let enumResultAsInt = enumResultAsInt {
- return UniversalBleErrorCode(rawValue: enumResultAsInt)
+ return PeripheralReadinessState(rawValue: enumResultAsInt)
}
return nil
case 132:
- return UniversalBleScanResult.fromList(self.readValue() as! [Any?])
+ let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
+ if let enumResultAsInt = enumResultAsInt {
+ return PeripheralAdvertisingState(rawValue: enumResultAsInt)
+ }
+ return nil
case 133:
- return UniversalBleService.fromList(self.readValue() as! [Any?])
+ let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
+ if let enumResultAsInt = enumResultAsInt {
+ return UniversalBleErrorCode(rawValue: enumResultAsInt)
+ }
+ return nil
case 134:
- return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?])
+ return UniversalBleScanResult.fromList(self.readValue() as! [Any?])
case 135:
- return UniversalBleDescriptor.fromList(self.readValue() as! [Any?])
+ return UniversalBleService.fromList(self.readValue() as! [Any?])
case 136:
- return AndroidOptions.fromList(self.readValue() as! [Any?])
+ return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?])
case 137:
- return UniversalScanConfig.fromList(self.readValue() as! [Any?])
+ return UniversalBleDescriptor.fromList(self.readValue() as! [Any?])
case 138:
- return UniversalScanFilter.fromList(self.readValue() as! [Any?])
+ return AndroidOptions.fromList(self.readValue() as! [Any?])
case 139:
- return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?])
+ return UniversalScanConfig.fromList(self.readValue() as! [Any?])
case 140:
+ return UniversalScanFilter.fromList(self.readValue() as! [Any?])
+ case 141:
+ return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?])
+ case 142:
return UniversalManufacturerData.fromList(self.readValue() as! [Any?])
+ case 143:
+ return PeripheralService.fromList(self.readValue() as! [Any?])
+ case 144:
+ return PeripheralCharacteristic.fromList(self.readValue() as! [Any?])
+ case 145:
+ return PeripheralDescriptor.fromList(self.readValue() as! [Any?])
+ case 146:
+ return PeripheralReadRequestResult.fromList(self.readValue() as! [Any?])
+ case 147:
+ return PeripheralWriteRequestResult.fromList(self.readValue() as! [Any?])
+ case 148:
+ return PeripheralManufacturerData.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
@@ -569,35 +977,59 @@ private class UniversalBlePigeonCodecWriter: FlutterStandardWriter {
} else if let value = value as? AndroidScanMode {
super.writeByte(130)
super.writeValue(value.rawValue)
- } else if let value = value as? UniversalBleErrorCode {
+ } else if let value = value as? PeripheralReadinessState {
super.writeByte(131)
super.writeValue(value.rawValue)
- } else if let value = value as? UniversalBleScanResult {
+ } else if let value = value as? PeripheralAdvertisingState {
super.writeByte(132)
+ super.writeValue(value.rawValue)
+ } else if let value = value as? UniversalBleErrorCode {
+ super.writeByte(133)
+ super.writeValue(value.rawValue)
+ } else if let value = value as? UniversalBleScanResult {
+ super.writeByte(134)
super.writeValue(value.toList())
} else if let value = value as? UniversalBleService {
- super.writeByte(133)
+ super.writeByte(135)
super.writeValue(value.toList())
} else if let value = value as? UniversalBleCharacteristic {
- super.writeByte(134)
+ super.writeByte(136)
super.writeValue(value.toList())
} else if let value = value as? UniversalBleDescriptor {
- super.writeByte(135)
+ super.writeByte(137)
super.writeValue(value.toList())
} else if let value = value as? AndroidOptions {
- super.writeByte(136)
+ super.writeByte(138)
super.writeValue(value.toList())
} else if let value = value as? UniversalScanConfig {
- super.writeByte(137)
+ super.writeByte(139)
super.writeValue(value.toList())
} else if let value = value as? UniversalScanFilter {
- super.writeByte(138)
+ super.writeByte(140)
super.writeValue(value.toList())
} else if let value = value as? UniversalManufacturerDataFilter {
- super.writeByte(139)
+ super.writeByte(141)
super.writeValue(value.toList())
} else if let value = value as? UniversalManufacturerData {
- super.writeByte(140)
+ super.writeByte(142)
+ super.writeValue(value.toList())
+ } else if let value = value as? PeripheralService {
+ super.writeByte(143)
+ super.writeValue(value.toList())
+ } else if let value = value as? PeripheralCharacteristic {
+ super.writeByte(144)
+ super.writeValue(value.toList())
+ } else if let value = value as? PeripheralDescriptor {
+ super.writeByte(145)
+ super.writeValue(value.toList())
+ } else if let value = value as? PeripheralReadRequestResult {
+ super.writeByte(146)
+ super.writeValue(value.toList())
+ } else if let value = value as? PeripheralWriteRequestResult {
+ super.writeByte(147)
+ super.writeValue(value.toList())
+ } else if let value = value as? PeripheralManufacturerData {
+ super.writeByte(148)
super.writeValue(value.toList())
} else {
super.writeValue(value)
@@ -1145,3 +1577,386 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol {
}
}
}
+/// Flutter -> Native (peripheral)
+///
+/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
+protocol UniversalBlePeripheralChannel {
+ func getAdvertisingState() throws -> PeripheralAdvertisingState
+ func getReadinessState() throws -> PeripheralReadinessState
+ func stopAdvertising() throws
+ func addService(service: PeripheralService) throws
+ func removeService(serviceId: String) throws
+ func clearServices() throws
+ func getServices() throws -> [String]
+ func startAdvertising(services: [String], localName: String?, timeout: Int64?, manufacturerData: PeripheralManufacturerData?, addManufacturerDataInScanResponse: Bool) throws
+ func updateCharacteristic(characteristicId: String, value: FlutterStandardTypedData, deviceId: String?) throws
+ /// Returns peripheral-client device ids currently subscribed to [characteristicId]
+ /// (e.g. HID report characteristic). Used to restore app state after restart.
+ func getSubscribedClients(characteristicId: String) throws -> [String]
+ /// Returns max characteristic notify payload length for a connected client.
+ func getMaximumNotifyLength(deviceId: String) throws -> Int64?
+}
+
+/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
+class UniversalBlePeripheralChannelSetup {
+ static var codec: FlutterStandardMessageCodec { UniversalBlePigeonCodec.shared }
+ /// Sets up an instance of `UniversalBlePeripheralChannel` to handle messages through the `binaryMessenger`.
+ static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UniversalBlePeripheralChannel?, messageChannelSuffix: String = "") {
+ let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
+ let getAdvertisingStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getAdvertisingState\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getAdvertisingStateChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.getAdvertisingState()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getAdvertisingStateChannel.setMessageHandler(nil)
+ }
+ let getReadinessStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getReadinessState\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getReadinessStateChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.getReadinessState()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getReadinessStateChannel.setMessageHandler(nil)
+ }
+ let stopAdvertisingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.stopAdvertising\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ stopAdvertisingChannel.setMessageHandler { _, reply in
+ do {
+ try api.stopAdvertising()
+ reply(wrapResult(nil))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ stopAdvertisingChannel.setMessageHandler(nil)
+ }
+ let addServiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.addService\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ addServiceChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let serviceArg = args[0] as! PeripheralService
+ do {
+ try api.addService(service: serviceArg)
+ reply(wrapResult(nil))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ addServiceChannel.setMessageHandler(nil)
+ }
+ let removeServiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.removeService\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ removeServiceChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let serviceIdArg = args[0] as! String
+ do {
+ try api.removeService(serviceId: serviceIdArg)
+ reply(wrapResult(nil))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ removeServiceChannel.setMessageHandler(nil)
+ }
+ let clearServicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.clearServices\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ clearServicesChannel.setMessageHandler { _, reply in
+ do {
+ try api.clearServices()
+ reply(wrapResult(nil))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ clearServicesChannel.setMessageHandler(nil)
+ }
+ let getServicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getServices\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getServicesChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.getServices()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getServicesChannel.setMessageHandler(nil)
+ }
+ let startAdvertisingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.startAdvertising\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ startAdvertisingChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let servicesArg = args[0] as! [String]
+ let localNameArg: String? = nilOrValue(args[1])
+ let timeoutArg: Int64? = nilOrValue(args[2])
+ let manufacturerDataArg: PeripheralManufacturerData? = nilOrValue(args[3])
+ let addManufacturerDataInScanResponseArg = args[4] as! Bool
+ do {
+ try api.startAdvertising(services: servicesArg, localName: localNameArg, timeout: timeoutArg, manufacturerData: manufacturerDataArg, addManufacturerDataInScanResponse: addManufacturerDataInScanResponseArg)
+ reply(wrapResult(nil))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ startAdvertisingChannel.setMessageHandler(nil)
+ }
+ let updateCharacteristicChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.updateCharacteristic\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ updateCharacteristicChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let characteristicIdArg = args[0] as! String
+ let valueArg = args[1] as! FlutterStandardTypedData
+ let deviceIdArg: String? = nilOrValue(args[2])
+ do {
+ try api.updateCharacteristic(characteristicId: characteristicIdArg, value: valueArg, deviceId: deviceIdArg)
+ reply(wrapResult(nil))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ updateCharacteristicChannel.setMessageHandler(nil)
+ }
+ /// Returns peripheral-client device ids currently subscribed to [characteristicId]
+ /// (e.g. HID report characteristic). Used to restore app state after restart.
+ let getSubscribedClientsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getSubscribedClients\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getSubscribedClientsChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let characteristicIdArg = args[0] as! String
+ do {
+ let result = try api.getSubscribedClients(characteristicId: characteristicIdArg)
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getSubscribedClientsChannel.setMessageHandler(nil)
+ }
+ /// Returns max characteristic notify payload length for a connected client.
+ let getMaximumNotifyLengthChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralChannel.getMaximumNotifyLength\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getMaximumNotifyLengthChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let deviceIdArg = args[0] as! String
+ do {
+ let result = try api.getMaximumNotifyLength(deviceId: deviceIdArg)
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getMaximumNotifyLengthChannel.setMessageHandler(nil)
+ }
+ }
+}
+/// Native -> Flutter (peripheral)
+///
+/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
+protocol UniversalBlePeripheralCallbackProtocol {
+ func onReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void)
+ func onWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void)
+ func onDescriptorReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void)
+ func onDescriptorWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void)
+ func onCharacteristicSubscriptionChange(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, isSubscribed isSubscribedArg: Bool, name nameArg: String?, completion: @escaping (Result) -> Void)
+ func onAdvertisingStateChange(state stateArg: PeripheralAdvertisingState, error errorArg: String?, completion: @escaping (Result) -> Void)
+ func onServiceAdded(serviceId serviceIdArg: String, error errorArg: String?, completion: @escaping (Result) -> Void)
+ func onMtuChange(deviceId deviceIdArg: String, mtu mtuArg: Int64, completion: @escaping (Result) -> Void)
+ func onConnectionStateChange(deviceId deviceIdArg: String, connected connectedArg: Bool, completion: @escaping (Result) -> Void)
+}
+class UniversalBlePeripheralCallback: UniversalBlePeripheralCallbackProtocol {
+ private let binaryMessenger: FlutterBinaryMessenger
+ private let messageChannelSuffix: String
+ init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") {
+ self.binaryMessenger = binaryMessenger
+ self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
+ }
+ var codec: UniversalBlePigeonCodec {
+ return UniversalBlePigeonCodec.shared
+ }
+ func onReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onReadRequest\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([deviceIdArg, characteristicIdArg, offsetArg, valueArg] 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 {
+ let result: PeripheralReadRequestResult? = nilOrValue(listResponse[0])
+ completion(.success(result))
+ }
+ }
+ }
+ func onWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onWriteRequest\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([deviceIdArg, characteristicIdArg, offsetArg, valueArg] 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 {
+ let result: PeripheralWriteRequestResult? = nilOrValue(listResponse[0])
+ completion(.success(result))
+ }
+ }
+ }
+ func onDescriptorReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorReadRequest\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg] 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 {
+ let result: PeripheralReadRequestResult? = nilOrValue(listResponse[0])
+ completion(.success(result))
+ }
+ }
+ }
+ func onDescriptorWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorWriteRequest\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg] 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 {
+ let result: PeripheralWriteRequestResult? = nilOrValue(listResponse[0])
+ completion(.success(result))
+ }
+ }
+ }
+ func onCharacteristicSubscriptionChange(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, isSubscribed isSubscribedArg: Bool, name nameArg: String?, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onCharacteristicSubscriptionChange\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([deviceIdArg, characteristicIdArg, isSubscribedArg, nameArg] 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 onAdvertisingStateChange(state stateArg: PeripheralAdvertisingState, error errorArg: String?, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onAdvertisingStateChange\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([stateArg, errorArg] 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 onServiceAdded(serviceId serviceIdArg: String, error errorArg: String?, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onServiceAdded\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([serviceIdArg, errorArg] 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 onMtuChange(deviceId deviceIdArg: String, mtu mtuArg: Int64, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onMtuChange\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([deviceIdArg, mtuArg] 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 onConnectionStateChange(deviceId deviceIdArg: String, connected connectedArg: Bool, completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onConnectionStateChange\(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(()))
+ }
+ }
+ }
+}
diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralExtensions.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralExtensions.swift
new file mode 100644
index 00000000..3d3fd86b
--- /dev/null
+++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralExtensions.swift
@@ -0,0 +1,137 @@
+#if os(iOS)
+ import Flutter
+#elseif os(macOS)
+ import FlutterMacOS
+#else
+ #error("Unsupported platform.")
+#endif
+import CoreBluetooth
+import Foundation
+
+enum UniversalBlePeripheralError: Error {
+ case notFound(String)
+}
+
+var peripheralCharacteristicsList = [CBMutableCharacteristic]()
+var peripheralServicesList = [CBMutableService]()
+
+func clearPeripheralCaches() {
+ peripheralCharacteristicsList.removeAll()
+ peripheralServicesList.removeAll()
+}
+
+extension PeripheralService {
+ func toCBService() -> CBMutableService {
+ let service = CBMutableService(type: CBUUID(string: uuid), primary: primary)
+ let chars = characteristics.compactMap { $0.toCBCharacteristic() }
+ if !chars.isEmpty {
+ service.characteristics = chars
+ }
+ peripheralServicesList.removeAll {
+ $0.uuid.uuidString.lowercased() == service.uuid.uuidString.lowercased()
+ }
+ peripheralServicesList.append(service)
+ return service
+ }
+}
+
+extension PeripheralDescriptor {
+ func toCBMutableDescriptor() -> CBMutableDescriptor {
+ return CBMutableDescriptor(type: CBUUID(string: uuid), value: value?.toData())
+ }
+}
+
+extension PeripheralCharacteristic {
+ func toCBCharacteristic() -> CBMutableCharacteristic {
+ let properties = self.properties.compactMap { $0.toCBCharacteristicProperties() }
+ let permissions = self.permissions.compactMap { $0.toCBAttributePermissions() }
+ let combinedProperties = properties.reduce(CBCharacteristicProperties()) { $0.union($1) }
+ let combinedPermissions = permissions.reduce(CBAttributePermissions()) { $0.union($1) }
+ let characteristic = CBMutableCharacteristic(
+ type: CBUUID(string: uuid),
+ properties: combinedProperties,
+ value: value?.toData(),
+ permissions: combinedPermissions
+ )
+ characteristic.descriptors = descriptors?.compactMap { $0.toCBMutableDescriptor() }
+ peripheralCharacteristicsList.removeAll {
+ $0.uuid.uuidString.lowercased() == characteristic.uuid.uuidString.lowercased()
+ }
+ peripheralCharacteristicsList.append(characteristic)
+ return characteristic
+ }
+}
+
+extension String {
+ func findPeripheralCharacteristic() -> CBMutableCharacteristic? {
+ let target = lowercased()
+ return peripheralCharacteristicsList.first {
+ $0.uuid.uuidString.lowercased() == target
+ }
+ }
+
+ func findPeripheralService() -> CBMutableService? {
+ let target = lowercased()
+ return peripheralServicesList.first {
+ $0.uuid.uuidString.lowercased() == target
+ }
+ }
+}
+
+extension Int64 {
+ func toCBCharacteristicProperties() -> CBCharacteristicProperties? {
+ switch self {
+ case 0: return .broadcast
+ case 1: return .read
+ case 2: return .writeWithoutResponse
+ case 3: return .write
+ case 4: return .notify
+ case 5: return .indicate
+ case 6: return .authenticatedSignedWrites
+ case 7: return .extendedProperties
+ case 8: return .notifyEncryptionRequired
+ case 9: return .indicateEncryptionRequired
+ default: return nil
+ }
+ }
+
+ func toCBAttributePermissions() -> CBAttributePermissions? {
+ switch self {
+ case 0: return .readable
+ case 1: return .writeable
+ case 2: return .readEncryptionRequired
+ case 3: return .writeEncryptionRequired
+ default: return nil
+ }
+ }
+
+ func toCBATTErrorCode() -> CBATTError.Code {
+ switch self {
+ case 0: return .success
+ case 1: return .invalidHandle
+ case 2: return .readNotPermitted
+ case 3: return .writeNotPermitted
+ case 4: return .invalidPdu
+ case 5: return .insufficientAuthentication
+ case 6: return .requestNotSupported
+ case 7: return .invalidOffset
+ case 8: return .insufficientAuthorization
+ case 9: return .prepareQueueFull
+ case 10: return .attributeNotFound
+ case 11: return .attributeNotLong
+ case 12: return .insufficientEncryptionKeySize
+ case 13: return .invalidAttributeValueLength
+ case 14: return .unlikelyError
+ case 15: return .insufficientEncryption
+ case 16: return .unsupportedGroupType
+ case 17: return .insufficientResources
+ default: return .success
+ }
+ }
+}
+
+extension Data {
+ func toFlutterBytes() -> FlutterStandardTypedData {
+ FlutterStandardTypedData(bytes: self)
+ }
+}
diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift
new file mode 100644
index 00000000..c97cbee6
--- /dev/null
+++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift
@@ -0,0 +1,330 @@
+#if os(iOS)
+ import Flutter
+#elseif os(macOS)
+ import Cocoa
+ import FlutterMacOS
+#endif
+import CoreBluetooth
+import Foundation
+
+final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChannel,
+ CBPeripheralManagerDelegate
+{
+ private let callbackChannel: UniversalBlePeripheralCallback
+ private var advertisingState: PeripheralAdvertisingState = .idle
+ private lazy var peripheralManager: CBPeripheralManager =
+ .init(delegate: self, queue: nil, options: nil)
+ private let centralsLock = NSLock()
+ private var centralsById = [String: CBCentral]()
+ private var centralCharacteristicSubscriptions = [String: Set]()
+
+ init(callbackChannel: UniversalBlePeripheralCallback) {
+ self.callbackChannel = callbackChannel
+ super.init()
+ _ = peripheralManager.isAdvertising
+ }
+
+ deinit {
+ peripheralManager.stopAdvertising()
+ peripheralManager.removeAllServices()
+ clearPeripheralCaches()
+ clearCentrals()
+ }
+
+ func getAdvertisingState() throws -> PeripheralAdvertisingState {
+ advertisingState
+ }
+
+ func getReadinessState() throws -> PeripheralReadinessState {
+ switch peripheralManager.state {
+ case .poweredOn:
+ return .ready
+ case .poweredOff:
+ return .bluetoothOff
+ case .unauthorized:
+ return .unauthorized
+ case .unsupported:
+ return .unsupported
+ case .unknown, .resetting:
+ return .unknown
+ @unknown default:
+ return .unknown
+ }
+ }
+
+ func stopAdvertising() throws {
+ advertisingState = .stopping
+ callbackChannel.onAdvertisingStateChange(state: .stopping, error: nil) { _ in }
+ peripheralManager.stopAdvertising()
+ advertisingState = .idle
+ callbackChannel.onAdvertisingStateChange(state: .idle, error: nil) { _ in }
+ }
+
+ func addService(service: PeripheralService) throws {
+ peripheralManager.add(service.toCBService())
+ }
+
+ func removeService(serviceId: String) throws {
+ if let service = serviceId.findPeripheralService() {
+ peripheralManager.remove(service)
+ peripheralServicesList.removeAll {
+ $0.uuid.uuidString.lowercased() == service.uuid.uuidString.lowercased()
+ }
+ }
+ }
+
+ func clearServices() throws {
+ peripheralManager.removeAllServices()
+ peripheralServicesList.removeAll()
+ }
+
+ func getServices() throws -> [String] {
+ peripheralServicesList.map { $0.uuid.uuidString }
+ }
+
+ func startAdvertising(
+ services: [String],
+ localName: String?,
+ timeout: Int64?,
+ manufacturerData: PeripheralManufacturerData?,
+ addManufacturerDataInScanResponse: Bool
+ ) throws {
+ if let timeout, timeout > 0 {
+ throw NSError(
+ domain: "UniversalBlePeripheral",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "Advertising timeout is not supported on Darwin."]
+ )
+ }
+ if addManufacturerDataInScanResponse {
+ throw NSError(
+ domain: "UniversalBlePeripheral",
+ code: -1,
+ userInfo: [
+ NSLocalizedDescriptionKey:
+ "Scan response manufacturer data placement is not supported on Darwin."
+ ]
+ )
+ }
+ let cbServices = services.map { CBUUID(string: $0) }
+ var advertisementData: [String: Any] = [CBAdvertisementDataServiceUUIDsKey: cbServices]
+ if let localName {
+ advertisementData[CBAdvertisementDataLocalNameKey] = localName
+ }
+ if let manufacturerData {
+ // CoreBluetooth expects the full AD manufacturer field: company ID (LE uint16)
+ // followed by payload. Dart's `ManufacturerData` keeps id and payload separate.
+ let companyId = UInt16(truncatingIfNeeded: manufacturerData.manufacturerId)
+ var manufacturerField = Data()
+ manufacturerField.append(UInt8(companyId & 0x00ff))
+ manufacturerField.append(UInt8((companyId >> 8) & 0x00ff))
+ manufacturerField.append(manufacturerData.data.data)
+ advertisementData[CBAdvertisementDataManufacturerDataKey] = manufacturerField
+ }
+ advertisingState = .starting
+ callbackChannel.onAdvertisingStateChange(state: .starting, error: nil) { _ in }
+ peripheralManager.startAdvertising(advertisementData)
+ }
+
+ func updateCharacteristic(
+ characteristicId: String,
+ value: FlutterStandardTypedData,
+ deviceId: String?
+ ) throws {
+ guard let characteristic = characteristicId.findPeripheralCharacteristic() else {
+ throw UniversalBlePeripheralError.notFound("\(characteristicId) characteristic not found")
+ }
+ if let deviceId {
+ guard let central = central(for: deviceId) else {
+ throw UniversalBlePeripheralError.notFound("\(deviceId) device not found")
+ }
+ peripheralManager.updateValue(
+ value.toData(),
+ for: characteristic,
+ onSubscribedCentrals: [central]
+ )
+ } else {
+ peripheralManager.updateValue(value.toData(), for: characteristic, onSubscribedCentrals: nil)
+ }
+ }
+
+ nonisolated func peripheralManagerDidStartAdvertising(
+ _: CBPeripheralManager,
+ error: Error?
+ ) {
+ advertisingState = error == nil ? .advertising : .error
+ callbackChannel.onAdvertisingStateChange(
+ state: advertisingState,
+ error: error?.localizedDescription
+ ) { _ in }
+ }
+
+ nonisolated func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
+ if peripheral.state != .poweredOn {
+ advertisingState = .idle
+ callbackChannel.onAdvertisingStateChange(state: .idle, error: nil) { _ in }
+ }
+ }
+
+ nonisolated func peripheralManager(
+ _: CBPeripheralManager,
+ didAdd service: CBService,
+ error: Error?
+ ) {
+ callbackChannel.onServiceAdded(
+ serviceId: service.uuid.uuidString,
+ error: error?.localizedDescription
+ ) { _ in }
+ }
+
+ nonisolated func peripheralManager(
+ _: CBPeripheralManager,
+ central: CBCentral,
+ didSubscribeTo characteristic: CBCharacteristic
+ ) {
+ upsertCentralSubscription(
+ central: central,
+ characteristicId: characteristic.uuid.uuidString
+ )
+ callbackChannel.onCharacteristicSubscriptionChange(
+ deviceId: central.identifier.uuidString,
+ characteristicId: characteristic.uuid.uuidString,
+ isSubscribed: true,
+ name: nil
+ ) { _ in }
+ callbackChannel.onMtuChange(
+ deviceId: central.identifier.uuidString,
+ mtu: Int64(central.maximumUpdateValueLength)
+ ) { _ in }
+ }
+
+ nonisolated func peripheralManager(
+ _: CBPeripheralManager,
+ central: CBCentral,
+ didUnsubscribeFrom characteristic: CBCharacteristic
+ ) {
+ removeCentralSubscription(
+ centralId: central.identifier.uuidString,
+ characteristicId: characteristic.uuid.uuidString
+ )
+ callbackChannel.onCharacteristicSubscriptionChange(
+ deviceId: central.identifier.uuidString,
+ characteristicId: characteristic.uuid.uuidString,
+ isSubscribed: false,
+ name: nil
+ ) { _ in }
+ }
+
+ nonisolated func peripheralManager(
+ _: CBPeripheralManager,
+ didReceiveRead request: CBATTRequest
+ ) {
+ callbackChannel.onReadRequest(
+ deviceId: request.central.identifier.uuidString,
+ characteristicId: request.characteristic.uuid.uuidString,
+ offset: Int64(request.offset),
+ value: request.value?.toFlutterBytes()
+ ) { readReq in
+ do {
+ let result = try readReq.get()
+ let status = result?.status?.toCBATTErrorCode() ?? .success
+ guard status == .success else {
+ self.peripheralManager.respond(to: request, withResult: status)
+ return
+ }
+ guard let fullData = result?.value.toData() as Data? else {
+ self.peripheralManager.respond(to: request, withResult: .requestNotSupported)
+ return
+ }
+ let offset = Int(request.offset)
+ guard offset <= fullData.count else {
+ self.peripheralManager.respond(to: request, withResult: .invalidOffset)
+ return
+ }
+ let sliced = fullData.subdata(in: offset..()
+ subscriptions.insert(characteristicId)
+ centralCharacteristicSubscriptions[centralId] = subscriptions
+ centralsLock.unlock()
+ }
+
+ private func removeCentralSubscription(centralId: String, characteristicId: String) {
+ centralsLock.lock()
+ var subscriptions = centralCharacteristicSubscriptions[centralId] ?? Set()
+ subscriptions.remove(characteristicId)
+ if subscriptions.isEmpty {
+ centralCharacteristicSubscriptions.removeValue(forKey: centralId)
+ centralsById.removeValue(forKey: centralId)
+ } else {
+ centralCharacteristicSubscriptions[centralId] = subscriptions
+ }
+ centralsLock.unlock()
+ }
+
+ private func central(for id: String) -> CBCentral? {
+ centralsLock.lock()
+ let central = centralsById[id]
+ centralsLock.unlock()
+ return central
+ }
+
+ private func clearCentrals() {
+ centralsLock.lock()
+ centralsById.removeAll()
+ centralCharacteristicSubscriptions.removeAll()
+ centralsLock.unlock()
+ }
+
+ func getSubscribedClients(characteristicId: String) throws -> [String] {
+ let target = characteristicId.uppercased()
+ centralsLock.lock()
+ defer { centralsLock.unlock() }
+ return centralCharacteristicSubscriptions.compactMap { centralId, chars in
+ chars.contains(where: { $0.uppercased() == target }) ? centralId : nil
+ }
+ }
+
+ func getMaximumNotifyLength(deviceId: String) throws -> Int64? {
+ guard let central = central(for: deviceId) else {
+ return nil
+ }
+ return Int64(central.maximumUpdateValueLength)
+ }
+}
diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift
index 4f6b1fb3..042a2e97 100644
--- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift
+++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift
@@ -18,7 +18,13 @@ public class UniversalBlePlugin: NSObject, FlutterPlugin {
#endif
let callbackChannel = UniversalBleCallbackChannel(binaryMessenger: messenger)
let api = BleCentralDarwin(callbackChannel: callbackChannel)
+ let peripheralCallbackChannel = UniversalBlePeripheralCallback(binaryMessenger: messenger)
+ let peripheralApi = UniversalBlePeripheralPlugin(callbackChannel: peripheralCallbackChannel)
UniversalBlePlatformChannelSetup.setUp(binaryMessenger: messenger, api: api)
+ UniversalBlePeripheralChannelSetup.setUp(
+ binaryMessenger: messenger,
+ api: peripheralApi
+ )
}
}
diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig
index 592ceee8..ec97fc6f 100644
--- a/example/ios/Flutter/Debug.xcconfig
+++ b/example/ios/Flutter/Debug.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig
index 592ceee8..c4855bfe 100644
--- a/example/ios/Flutter/Release.xcconfig
+++ b/example/ios/Flutter/Release.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
diff --git a/example/ios/Podfile b/example/ios/Podfile
new file mode 100644
index 00000000..620e46eb
--- /dev/null
+++ b/example/ios/Podfile
@@ -0,0 +1,43 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '13.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
new file mode 100644
index 00000000..0b74389f
--- /dev/null
+++ b/example/ios/Podfile.lock
@@ -0,0 +1,29 @@
+PODS:
+ - Flutter (1.0.0)
+ - integration_test (0.0.1):
+ - Flutter
+ - universal_ble (0.0.1):
+ - Flutter
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - integration_test (from `.symlinks/plugins/integration_test/ios`)
+ - universal_ble (from `.symlinks/plugins/universal_ble/darwin`)
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ integration_test:
+ :path: ".symlinks/plugins/integration_test/ios"
+ universal_ble:
+ :path: ".symlinks/plugins/universal_ble/darwin"
+
+SPEC CHECKSUMS:
+ Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
+ integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
+ universal_ble: a322ebabee64f0ec27a313e89a5dd6967f37a60f
+
+PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
+
+COCOAPODS: 1.16.2
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index ea2a2e8a..730c7de2 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -10,11 +10,13 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 3C020DDC565FF8D37F3235C5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E92536821956EA00892DD639 /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
- 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ B2742FE9E6C1C51A78D26865 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -41,14 +43,18 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 30B9BF32CD25D1ECB8BB5DCA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ 4414252E2351EBDB7761F83A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
@@ -57,6 +63,10 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A11EED44BE0324AD23055698 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ E92536821956EA00892DD639 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -64,6 +74,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ B2742FE9E6C1C51A78D26865 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -72,6 +83,7 @@
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
+ 3C020DDC565FF8D37F3235C5 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -86,6 +98,15 @@
path = RunnerTests;
sourceTree = "";
};
+ 68033A1F47BF85C19A1ED513 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ E92536821956EA00892DD639 /* Pods_Runner.framework */,
+ 9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -106,6 +127,7 @@
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
FAF8A3758B0770B81319B3AE /* Pods */,
+ 68033A1F47BF85C19A1ED513 /* Frameworks */,
);
sourceTree = "";
};
@@ -136,6 +158,12 @@
FAF8A3758B0770B81319B3AE /* Pods */ = {
isa = PBXGroup;
children = (
+ 30B9BF32CD25D1ECB8BB5DCA /* Pods-Runner.debug.xcconfig */,
+ 4414252E2351EBDB7761F83A /* Pods-Runner.release.xcconfig */,
+ A11EED44BE0324AD23055698 /* Pods-Runner.profile.xcconfig */,
+ 411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */,
+ 0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */,
+ 6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "";
@@ -147,6 +175,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
+ 7E79F47357B64513A488AC3E /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
8EE4584064C335A1F66BB6D9 /* Frameworks */,
@@ -162,24 +191,26 @@
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
+ 3CE2E408302E9BF272A3BE07 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 2F5EDC3CC74E7431E4CB95DF /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
- packageProductDependencies = (
- 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
- );
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
@@ -188,6 +219,9 @@
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ );
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
@@ -213,9 +247,6 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
- packageReferences = (
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
- );
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -248,6 +279,23 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
+ 2F5EDC3CC74E7431E4CB95DF /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -264,6 +312,50 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
+ 3CE2E408302E9BF272A3BE07 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 7E79F47357B64513A488AC3E /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -370,7 +462,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.1;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -403,6 +495,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -420,6 +513,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -435,6 +529,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -495,7 +590,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.1;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -544,7 +639,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.1;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -634,14 +729,12 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
-
/* Begin XCLocalSwiftPackageReference section */
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
-
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart
index 44a681ac..63413d4b 100644
--- a/example/lib/data/mock_universal_ble.dart
+++ b/example/lib/data/mock_universal_ble.dart
@@ -66,7 +66,7 @@ class MockUniversalBle extends UniversalBlePlatform {
}
@override
- Future getBluetoothAvailabilityState() async {
+ Future getAvailabilityState() async {
return AvailabilityState.poweredOn;
}
diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart
index 0ab9c57b..28117c1b 100644
--- a/example/lib/home/home.dart
+++ b/example/lib/home/home.dart
@@ -11,14 +11,15 @@ import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.
import 'package:universal_ble_example/widgets/platform_button.dart';
import 'package:universal_ble_example/widgets/responsive_buttons_grid.dart';
-class MyApp extends StatefulWidget {
- const MyApp({super.key});
+class CentralHome extends StatefulWidget {
+ final bool showAppBar;
+ const CentralHome({super.key, this.showAppBar = true});
@override
- State createState() => _MyAppState();
+ State createState() => _CentralHomeState();
}
-class _MyAppState extends State {
+class _CentralHomeState extends State {
final _bleDevices = [];
final _hiddenDevices = [];
bool _isScanning = false;
@@ -150,22 +151,24 @@ class _MyAppState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(
- title: const Text('Universal BLE'),
- elevation: 4,
- actions: [
- if (_isScanning)
- const Padding(
- padding: EdgeInsets.all(8.0),
- child: SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator.adaptive(
- strokeWidth: 2,
- )),
- ),
- ],
- ),
+ appBar: widget.showAppBar
+ ? AppBar(
+ title: const Text('Universal BLE - Central'),
+ elevation: 4,
+ actions: [
+ if (_isScanning)
+ const Padding(
+ padding: EdgeInsets.all(8.0),
+ child: SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator.adaptive(
+ strokeWidth: 2,
+ )),
+ ),
+ ],
+ )
+ : null,
body: Column(
children: [
Padding(
@@ -218,13 +221,14 @@ class _MyAppState extends State {
),
if (BleCapabilities.requiresRuntimePermission) ...[
PlatformButton(
- text: 'Has Permissions',
+ text: 'Is Permission Granted',
onPressed: () async {
try {
- bool hasPermissions = await UniversalBle.hasPermissions(
+ bool granted =
+ await UniversalBle.hasPermissions(
withAndroidFineLocation: false,
);
- showSnackbar("Has Permissions: $hasPermissions");
+ showSnackbar("Is Permission Granted: $granted");
} catch (e) {
showSnackbar(e.toString());
}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 01f1d477..c73bb0ea 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:universal_ble/universal_ble.dart';
-import 'package:universal_ble_example/home/home.dart';
+import 'package:universal_ble_example/home/home.dart' show CentralHome;
+import 'package:universal_ble_example/peripheral/peripheral_home.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -11,7 +12,35 @@ void main() async {
debugShowCheckedModeBanner: false,
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
- home: const MyApp(),
+ home: const _TabbedExample(),
),
);
}
+
+class _TabbedExample extends StatelessWidget {
+ const _TabbedExample();
+
+ @override
+ Widget build(BuildContext context) {
+ return DefaultTabController(
+ length: 2,
+ child: Scaffold(
+ appBar: AppBar(
+ title: const Text('Universal BLE'),
+ bottom: const TabBar(
+ tabs: [
+ Tab(text: 'Central'),
+ Tab(text: 'Peripheral'),
+ ],
+ ),
+ ),
+ body: const TabBarView(
+ children: [
+ CentralHome(showAppBar: false),
+ PeripheralHome(),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/example/lib/peripheral/peripheral_home.dart b/example/lib/peripheral/peripheral_home.dart
new file mode 100644
index 00000000..a0b90ce4
--- /dev/null
+++ b/example/lib/peripheral/peripheral_home.dart
@@ -0,0 +1,211 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+class PeripheralHome extends StatefulWidget {
+ const PeripheralHome({super.key});
+
+ @override
+ State createState() => _PeripheralHomeState();
+}
+
+class _PeripheralHomeState extends State {
+ final UniversalBlePeripheralClient _peripheral = UniversalBlePeripheralClient();
+ final List _logs = [];
+ StreamSubscription? _eventSub;
+ bool _initialized = false;
+ UniversalBlePeripheralAdvertisingState _advertisingState =
+ UniversalBlePeripheralAdvertisingState.idle;
+
+ static const String _serviceBattery = '0000180F-0000-1000-8000-00805F9B34FB';
+ static const String _charBattery = '00002A19-0000-1000-8000-00805F9B34FB';
+ static const String _serviceTest = '0000180D-0000-1000-8000-00805F9B34FB';
+ static const String _charTest = '00002A18-0000-1000-8000-00805F9B34FB';
+
+ @override
+ void initState() {
+ super.initState();
+ _eventSub = _peripheral.eventStream.listen((event) {
+ switch (event) {
+ case UniversalBlePeripheralAdvertisingStateChanged():
+ setState(() {
+ _advertisingState = event.state;
+ });
+ _log('Advertising state: ${event.state.name} ${event.error ?? ''}'.trim());
+ case UniversalBlePeripheralServiceAdded():
+ _log('Service added: ${event.serviceId} ${event.error ?? ''}'.trim());
+ case UniversalBlePeripheralCharacteristicSubscriptionChanged():
+ _log(
+ 'Subscription ${event.isSubscribed ? 'on' : 'off'}: '
+ '${event.name ?? event.deviceId} -> ${event.characteristicId}',
+ );
+ case UniversalBlePeripheralConnectionStateChanged():
+ _log(
+ 'Connection: ${event.deviceId} connected=${event.connected}',
+ );
+ case UniversalBlePeripheralMtuChanged():
+ _log('MTU: ${event.deviceId} mtu=${event.mtu}');
+ }
+ });
+ _peripheral.setRequestHandlers(
+ PeripheralRequestHandlers(
+ onReadRequest: (deviceId, characteristicId, _, __) {
+ _log('Read request: $deviceId ${characteristicId.value}');
+ return BleReadRequestResult(
+ value: Uint8List.fromList(utf8.encode('Hello World')),
+ );
+ },
+ onWriteRequest: (deviceId, characteristicId, _, value) {
+ _log('Write request: $deviceId ${characteristicId.value} $value');
+ return const BleWriteRequestResult();
+ },
+ ),
+ );
+ }
+
+ void _log(String text) {
+ setState(() {
+ _logs.insert(0, text);
+ });
+ }
+
+ Future _initialize() async {
+ final supported =
+ (await _peripheral.getStaticCapabilities()).supportsPeripheralMode;
+ setState(() {
+ _initialized = supported;
+ });
+ final readiness = await _peripheral.getReadinessState();
+ _log('Peripheral ready check. supported=$supported readiness=${readiness.name}');
+ }
+
+ Future _addServices() async {
+ await _peripheral.addService(
+ BleService(_serviceBattery, [
+ BleCharacteristic(
+ _charBattery,
+ [CharacteristicProperty.read, CharacteristicProperty.notify],
+ [],
+ ),
+ ]),
+ );
+ await _peripheral.addService(
+ BleService(_serviceTest, [
+ BleCharacteristic(
+ _charTest,
+ [
+ CharacteristicProperty.read,
+ CharacteristicProperty.notify,
+ CharacteristicProperty.write,
+ ],
+ [BleDescriptor('00002908-0000-1000-8000-00805F9B34FB')],
+ ),
+ ]),
+ );
+ _log('Services queued');
+ }
+
+ Future _startAdvertising() async {
+ final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
+ await _peripheral.startAdvertising(
+ services: [
+ const PeripheralServiceId(_serviceBattery),
+ const PeripheralServiceId(_serviceTest),
+ ],
+ localName: isWindows ? null : 'UniversalBlePeripheral',
+ manufacturerData: isWindows
+ ? null
+ : ManufacturerData(
+ 0x012D,
+ Uint8List.fromList([0x03, 0x00, 0x64, 0x00]),
+ ),
+ addManufacturerDataInScanResponse: !isWindows,
+ );
+ _log('Start advertising requested');
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ ElevatedButton(
+ onPressed: _initialize,
+ child: Text(_initialized ? 'Reinitialize' : 'Initialize'),
+ ),
+ ElevatedButton(
+ onPressed: _initialized ? _addServices : null,
+ child: const Text('Add Services'),
+ ),
+ ElevatedButton(
+ onPressed: _initialized ? _startAdvertising : null,
+ child: const Text('Start Advertising'),
+ ),
+ ElevatedButton(
+ onPressed: _initialized
+ ? () async {
+ await _peripheral.stopAdvertising();
+ setState(() {
+ _advertisingState =
+ UniversalBlePeripheralAdvertisingState.idle;
+ });
+ _log('Stop advertising requested');
+ }
+ : null,
+ child: const Text('Stop Advertising'),
+ ),
+ ElevatedButton(
+ onPressed: _initialized
+ ? () async {
+ await _peripheral.updateCharacteristicValue(
+ characteristicId: const PeripheralCharacteristicId(
+ _charTest,
+ ),
+ value: Uint8List.fromList(utf8.encode('Test Data')),
+ );
+ _log('Characteristic updated');
+ }
+ : null,
+ child: const Text('Update Characteristic'),
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ child: Row(
+ children: [
+ Text('Initialized: $_initialized'),
+ const SizedBox(width: 16),
+ Text('Advertising: ${_advertisingState.name}'),
+ ],
+ ),
+ ),
+ const Divider(),
+ Expanded(
+ child: ListView.builder(
+ itemCount: _logs.length,
+ itemBuilder: (context, index) => ListTile(
+ dense: true,
+ title: Text(_logs[index]),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ @override
+ void dispose() {
+ _eventSub?.cancel();
+ super.dispose();
+ }
+}
diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig
index c2efd0b6..4b81f9b2 100644
--- a/example/macos/Flutter/Flutter-Debug.xcconfig
+++ b/example/macos/Flutter/Flutter-Debug.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig
index c2efd0b6..5caa9d15 100644
--- a/example/macos/Flutter/Flutter-Release.xcconfig
+++ b/example/macos/Flutter/Flutter-Release.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/example/macos/Podfile b/example/macos/Podfile
new file mode 100644
index 00000000..ff5ddb3b
--- /dev/null
+++ b/example/macos/Podfile
@@ -0,0 +1,42 @@
+platform :osx, '10.15'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+
+ flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_macos_build_settings(target)
+ end
+end
diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock
new file mode 100644
index 00000000..f496afd0
--- /dev/null
+++ b/example/macos/Podfile.lock
@@ -0,0 +1,23 @@
+PODS:
+ - FlutterMacOS (1.0.0)
+ - universal_ble (0.0.1):
+ - Flutter
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - FlutterMacOS (from `Flutter/ephemeral`)
+ - universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`)
+
+EXTERNAL SOURCES:
+ FlutterMacOS:
+ :path: Flutter/ephemeral
+ universal_ble:
+ :path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
+
+SPEC CHECKSUMS:
+ FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
+ universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914
+
+PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3
+
+COCOAPODS: 1.16.2
diff --git a/lib/src/extensions/ble_characteristic_extension.dart b/lib/src/extensions/ble_characteristic_extension.dart
index f277ce63..6107abb2 100644
--- a/lib/src/extensions/ble_characteristic_extension.dart
+++ b/lib/src/extensions/ble_characteristic_extension.dart
@@ -85,7 +85,7 @@ class CharacteristicSubscription {
final bool isSupported;
CharacteristicSubscription(this._characteristic, this._property)
- : isSupported = _characteristic.properties.contains(_property);
+ : isSupported = _characteristic.properties.contains(_property);
/// Registers a listener for incoming data from the characteristic.
StreamSubscription listen(
diff --git a/lib/src/models/ble_capabilities.dart b/lib/src/models/ble_capabilities.dart
index baa93e3d..f54a7ae6 100644
--- a/lib/src/models/ble_capabilities.dart
+++ b/lib/src/models/ble_capabilities.dart
@@ -22,11 +22,10 @@ class BleCapabilities {
/// but it very unreliable, therefore we return false.
static final triggersConfirmOnlyPairing =
defaultTargetPlatform != TargetPlatform.windows &&
- defaultTargetPlatform != TargetPlatform.linux;
+ defaultTargetPlatform != TargetPlatform.linux;
/// Returns true if pair()/unpair() are supported on the platform.
- static bool hasSystemPairingApi =
- !kIsWeb &&
+ static bool hasSystemPairingApi = !kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.linux);
diff --git a/lib/src/models/ble_device.dart b/lib/src/models/ble_device.dart
index 464d7e8d..87aefbda 100644
--- a/lib/src/models/ble_device.dart
+++ b/lib/src/models/ble_device.dart
@@ -55,9 +55,9 @@ class BleDevice {
this.manufacturerDataList = const [],
Map serviceData = const {},
this.timestamp,
- }) : serviceData = _validateServiceData(serviceData),
- rawName = name,
- name = name?.replaceAll(RegExp(r'[^ -~]'), '').trim();
+ }) : serviceData = _validateServiceData(serviceData),
+ rawName = name,
+ name = name?.replaceAll(RegExp(r'[^ -~]'), '').trim();
DateTime? get timestampDateTime => timestamp != null
? DateTime.fromMillisecondsSinceEpoch(timestamp!)
diff --git a/lib/src/models/ble_service.dart b/lib/src/models/ble_service.dart
index ee179e29..c1a97c8c 100644
--- a/lib/src/models/ble_service.dart
+++ b/lib/src/models/ble_service.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/foundation.dart';
import 'package:universal_ble/universal_ble.dart';
class BleService {
@@ -5,7 +6,7 @@ class BleService {
List characteristics;
BleService(String uuid, this.characteristics)
- : uuid = BleUuidParser.string(uuid);
+ : uuid = BleUuidParser.string(uuid);
@override
String toString() {
@@ -20,7 +21,7 @@ class BleCharacteristic {
({String deviceId, String serviceId})? metaData;
BleCharacteristic(String uuid, this.properties, this.descriptors)
- : uuid = BleUuidParser.string(uuid);
+ : uuid = BleUuidParser.string(uuid);
BleCharacteristic.withMetaData({
required String deviceId,
@@ -28,11 +29,11 @@ class BleCharacteristic {
required String uuid,
required this.properties,
required this.descriptors,
- }) : uuid = BleUuidParser.string(uuid),
- metaData = (
- deviceId: deviceId,
- serviceId: BleUuidParser.string(serviceId),
- );
+ }) : uuid = BleUuidParser.string(uuid),
+ metaData = (
+ deviceId: deviceId,
+ serviceId: BleUuidParser.string(serviceId),
+ );
@override
String toString() {
@@ -55,19 +56,23 @@ class BleCharacteristic {
class BleDescriptor {
String uuid;
- BleDescriptor(String uuid) : uuid = BleUuidParser.string(uuid);
+ /// Initial attribute value for this descriptor (e.g. HID Report Reference 0x2908).
+ Uint8List? value;
+
+ BleDescriptor(String uuid, {this.value}) : uuid = BleUuidParser.string(uuid);
@override
- String toString() => 'BleDescriptor{uuid: $uuid}';
+ String toString() => 'BleDescriptor{uuid: $uuid, value: $value}';
@override
bool operator ==(Object other) {
if (other is! BleDescriptor) return false;
- return other.uuid == uuid;
+ return other.uuid == uuid && listEquals(other.value, value);
}
@override
- int get hashCode => uuid.hashCode;
+ int get hashCode =>
+ Object.hash(uuid, value == null ? null : Object.hashAll(value!));
}
enum CharacteristicProperty {
diff --git a/lib/src/models/ble_uuid_parser.dart b/lib/src/models/ble_uuid_parser.dart
index c157b7aa..8ab3ba10 100644
--- a/lib/src/models/ble_uuid_parser.dart
+++ b/lib/src/models/ble_uuid_parser.dart
@@ -20,8 +20,7 @@ class BleUuidParser {
if (!uuid.contains("-")) {
if (uuid.length != 32) throw const FormatException("Invalid UUID");
- uuid =
- "${uuid.substring(0, 8)}-${uuid.substring(8, 12)}"
+ uuid = "${uuid.substring(0, 8)}-${uuid.substring(8, 12)}"
"-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20, 32)}";
}
diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart
index 7533772d..55ffbd9b 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/peripheral/peripheral_exports.dart';
diff --git a/lib/src/models/peripheral/attribute_permissions.dart b/lib/src/models/peripheral/attribute_permissions.dart
new file mode 100644
index 00000000..0d665601
--- /dev/null
+++ b/lib/src/models/peripheral/attribute_permissions.dart
@@ -0,0 +1,6 @@
+enum AttributePermission {
+ readable,
+ writeable,
+ readEncryptionRequired,
+ writeEncryptionRequired;
+}
diff --git a/lib/src/models/peripheral/peripheral_exports.dart b/lib/src/models/peripheral/peripheral_exports.dart
new file mode 100644
index 00000000..e2956739
--- /dev/null
+++ b/lib/src/models/peripheral/peripheral_exports.dart
@@ -0,0 +1,2 @@
+export 'package:universal_ble/src/models/peripheral/attribute_permissions.dart';
+export 'package:universal_ble/src/models/peripheral/peripheral_request_result.dart';
diff --git a/lib/src/models/peripheral/peripheral_request_result.dart b/lib/src/models/peripheral/peripheral_request_result.dart
new file mode 100644
index 00000000..28f6ccf2
--- /dev/null
+++ b/lib/src/models/peripheral/peripheral_request_result.dart
@@ -0,0 +1,25 @@
+import 'dart:typed_data';
+
+class BleReadRequestResult {
+ final Uint8List value;
+ final int? offset;
+ final int? status;
+
+ const BleReadRequestResult({
+ required this.value,
+ this.offset,
+ this.status,
+ });
+}
+
+class BleWriteRequestResult {
+ final Uint8List? value;
+ final int? offset;
+ final int? status;
+
+ const BleWriteRequestResult({
+ this.value,
+ this.offset,
+ this.status,
+ });
+}
diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart
index 70627e61..52ccff08 100644
--- a/lib/src/universal_ble.dart
+++ b/lib/src/universal_ble.dart
@@ -58,7 +58,8 @@ class UniversalBle {
static Stream characteristicValueStream(
String deviceId,
String characteristicId,
- ) => _platform.characteristicValueStream(deviceId, characteristicId);
+ ) =>
+ _platform.characteristicValueStream(deviceId, characteristicId);
/// Pairing state stream
static Stream pairingStateStream(String deviceId) =>
@@ -66,9 +67,9 @@ class UniversalBle {
/// Get Bluetooth availability state.
/// To be notified of updates, set [onAvailabilityChange] listener.
- static Future getBluetoothAvailabilityState() async {
+ static Future getAvailabilityState() async {
return await _bleCommandQueue.queueCommand(
- () => _platform.getBluetoothAvailabilityState(),
+ () => _platform.getAvailabilityState(),
);
}
@@ -155,9 +156,9 @@ class UniversalBle {
_platform
.connect(deviceId, connectionTimeout: timeout, autoConnect: autoConnect)
.catchError((error) {
- if (completer.isCompleted) return;
- completer.completeError(ConnectionException(error));
- });
+ if (completer.isCompleted) return;
+ completer.completeError(ConnectionException(error));
+ });
if (!await completer.future.timeout(timeout)) {
throw ConnectionException("Failed to connect");
@@ -183,14 +184,14 @@ class UniversalBle {
await _bleCommandQueue
.queueCommand(
- () => _platform.disconnect(deviceId),
- timeout: timeout,
- deviceId: deviceId,
- )
+ () => _platform.disconnect(deviceId),
+ timeout: timeout,
+ deviceId: deviceId,
+ )
.catchError((error) {
- if (completer.isCompleted) return;
- completer.completeError(ConnectionException(error));
- });
+ if (completer.isCompleted) return;
+ completer.completeError(ConnectionException(error));
+ });
if (connectionState == BleConnectionState.disconnected ||
connectionState == BleConnectionState.disconnecting) {
@@ -547,11 +548,9 @@ class UniversalBle {
static set onAvailabilityChange(OnAvailabilityChange? onAvailabilityChange) {
_platform.onAvailabilityChange = onAvailabilityChange;
if (onAvailabilityChange != null) {
- getBluetoothAvailabilityState()
- .then((value) {
- onAvailabilityChange(value);
- })
- .onError((error, stackTrace) => null);
+ getAvailabilityState().then((value) {
+ onAvailabilityChange(value);
+ }).onError((error, stackTrace) => null);
}
}
@@ -619,32 +618,28 @@ class UniversalBle {
}
connectionSubscription = _platform
- .bleConnectionUpdateStreamController
- .stream
+ .bleConnectionUpdateStreamController.stream
.where((e) => e.deviceId == deviceId)
.listen(
- (e) {
- cancelSubscription();
- if (e.error != null) {
- handleError(e.error);
- } else {
- if (!completer.isCompleted) {
- completer.complete(e.isConnected);
- }
- }
- },
- onError: handleError,
- cancelOnError: true,
- );
+ (e) {
+ cancelSubscription();
+ if (e.error != null) {
+ handleError(e.error);
+ } else {
+ if (!completer.isCompleted) {
+ completer.complete(e.isConnected);
+ }
+ }
+ },
+ onError: handleError,
+ cancelOnError: true,
+ );
- completer.future
- .timeout(timeout)
- .then((_) {
- cancelSubscription();
- })
- .catchError((_) {
- cancelSubscription();
- });
+ completer.future.timeout(timeout).then((_) {
+ cancelSubscription();
+ }).catchError((_) {
+ cancelSubscription();
+ });
return completer;
}
diff --git a/lib/src/universal_ble_exceptions.dart b/lib/src/universal_ble_exceptions.dart
index 0ce8c82d..194284d7 100644
--- a/lib/src/universal_ble_exceptions.dart
+++ b/lib/src/universal_ble_exceptions.dart
@@ -42,11 +42,11 @@ class ConnectionException extends UniversalBleException {
});
ConnectionException([dynamic error])
- : this._(
- code: UniversalBleErrorParser.getCode(error),
- message: _errorParser(error),
- details: error,
- );
+ : this._(
+ code: UniversalBleErrorParser.getCode(error),
+ message: _errorParser(error),
+ details: error,
+ );
}
/// Exception thrown when pairing-related errors occur
@@ -59,11 +59,11 @@ class PairingException extends UniversalBleException {
/// Legacy constructor for backward compatibility
PairingException([dynamic error])
- : this._(
- code: UniversalBleErrorParser.getCode(error),
- message: _errorParser(error),
- details: error,
- );
+ : this._(
+ code: UniversalBleErrorParser.getCode(error),
+ message: _errorParser(error),
+ details: error,
+ );
}
/// Exception thrown when Web Bluetooth API is globally disabled
diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart
index aa94693b..3ade9bad 100644
--- a/lib/src/universal_ble_linux/universal_ble_linux.dart
+++ b/lib/src/universal_ble_linux/universal_ble_linux.dart
@@ -31,7 +31,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
{};
@override
- Future getBluetoothAvailabilityState() async {
+ Future getAvailabilityState() async {
await _ensureInitialized();
BlueZAdapter? adapter = _activeAdapter;
@@ -171,25 +171,23 @@ class UniversalBleLinux extends UniversalBlePlatform {
) 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",
- );
- return true;
- }
- }
- return element.contains(BluezProperty.servicesResolved);
- })
- .timeout(
- const Duration(seconds: 10),
- onTimeout: () {
- UniversalLogger.logInfo("DiscoverServicesFailed: Timeout");
- return [];
- },
- );
+ await device.propertiesChanged.firstWhere((element) {
+ if (element.contains(BluezProperty.connected)) {
+ if (!device.connected) {
+ UniversalLogger.logInfo(
+ "DiscoverServicesFailed: Device disconnected",
+ );
+ return true;
+ }
+ }
+ return element.contains(BluezProperty.servicesResolved);
+ }).timeout(
+ const Duration(seconds: 10),
+ onTimeout: () {
+ UniversalLogger.logInfo("DiscoverServicesFailed: Timeout");
+ return [];
+ },
+ );
}
// Few ble devices requires delay to perform operations after discovering services
@@ -220,8 +218,8 @@ class UniversalBleLinux extends UniversalBlePlatform {
properties: properties,
descriptors: withDescriptors
? e.descriptors
- .map((e) => BleDescriptor(e.uuid.toString()))
- .toList()
+ .map((e) => BleDescriptor(e.uuid.toString()))
+ .toList()
: [],
);
}).toList();
@@ -237,13 +235,13 @@ class UniversalBleLinux extends UniversalBlePlatform {
) {
final device = _findDeviceById(deviceId);
final s = device.gattServices.cast().firstWhere(
- (s) => s?.uuid.toString() == service,
- orElse: () => null,
- );
+ (s) => s?.uuid.toString() == service,
+ orElse: () => null,
+ );
final c = s?.characteristics.cast().firstWhere(
- (c) => c?.uuid.toString() == characteristic,
- orElse: () => null,
- );
+ (c) => c?.uuid.toString() == characteristic,
+ orElse: () => null,
+ );
if (c == null) {
throw UniversalBleException(
@@ -281,30 +279,29 @@ 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
@@ -446,9 +443,8 @@ 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) {
@@ -491,9 +487,9 @@ class UniversalBleLinux extends UniversalBlePlatform {
BlueZDevice? _getDeviceById(String deviceId) {
return _devices[deviceId] ??
_client.devices.cast().firstWhere(
- (device) => device?.address == deviceId,
- orElse: () => null,
- );
+ (device) => device?.address == deviceId,
+ orElse: () => null,
+ );
}
Future _ensureInitialized() async {
@@ -577,23 +573,21 @@ class UniversalBleLinux extends UniversalBlePlatform {
_devices[device.address] = device;
// Setup advertisements Listener
- _deviceAdvertisementSubscriptions[device.address] ??= device
- .propertiesChanged
- .where((e) {
- return e.contains(BluezProperty.rssi) ||
- e.contains(BluezProperty.manufacturerData) ||
- e.contains(BluezProperty.uuids) ||
- e.contains(BluezProperty.serviceData);
- })
- .listen((_) {
- if (_bleFilter.shouldAcceptDevice(bleDevice)) {
- updateScanResult(device.toBleDevice());
- }
- });
+ _deviceAdvertisementSubscriptions[device.address] ??=
+ device.propertiesChanged.where((e) {
+ return e.contains(BluezProperty.rssi) ||
+ e.contains(BluezProperty.manufacturerData) ||
+ e.contains(BluezProperty.uuids) ||
+ e.contains(BluezProperty.serviceData);
+ }).listen((_) {
+ if (_bleFilter.shouldAcceptDevice(bleDevice)) {
+ updateScanResult(device.toBleDevice());
+ }
+ });
// 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
diff --git a/lib/src/universal_ble_peripheral/universal_ble_peripheral.dart b/lib/src/universal_ble_peripheral/universal_ble_peripheral.dart
new file mode 100644
index 00000000..44672e2a
--- /dev/null
+++ b/lib/src/universal_ble_peripheral/universal_ble_peripheral.dart
@@ -0,0 +1,166 @@
+import 'package:flutter/foundation.dart';
+import 'package:universal_ble/src/universal_ble_peripheral/universal_ble_peripheral_pigeon.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+class UniversalBlePeripheralClient {
+ UniversalBlePeripheralPlatform _platform;
+
+ UniversalBlePeripheralClient({UniversalBlePeripheralPlatform? platform})
+ : _platform = platform ?? UniversalBlePeripheral._defaultPlatform();
+
+ void setPlatform(UniversalBlePeripheralPlatform platform) {
+ _platform.dispose();
+ _platform = platform;
+ }
+
+ Stream get eventStream => _platform.eventStream;
+
+ void setRequestHandlers(PeripheralRequestHandlers handlers) =>
+ _platform.setRequestHandlers(handlers);
+
+ Future getReadinessState() =>
+ _platform.getReadinessState();
+
+ Future getAdvertisingState() =>
+ _platform.getAdvertisingState();
+
+ Future getStaticCapabilities() =>
+ _platform.getStaticCapabilities();
+
+ Future addService(
+ BleService service, {
+ bool primary = true,
+ Duration? timeout,
+ }) =>
+ _platform.addService(service, primary: primary, timeout: timeout);
+
+ Future removeService(PeripheralServiceId serviceId) =>
+ _platform.removeService(
+ PeripheralServiceId(BleUuidParser.string(serviceId.value)),
+ );
+
+ Future clearServices() => _platform.clearServices();
+
+ Future> getServices() => _platform.getServices();
+
+ Future startAdvertising({
+ required List services,
+ String? localName,
+ int? timeout,
+ ManufacturerData? manufacturerData,
+ bool addManufacturerDataInScanResponse = false,
+ }) =>
+ _platform.startAdvertising(
+ services: services
+ .map((e) => PeripheralServiceId(BleUuidParser.string(e.value)))
+ .toList(),
+ localName: localName,
+ timeout: timeout,
+ manufacturerData: manufacturerData,
+ addManufacturerDataInScanResponse: addManufacturerDataInScanResponse,
+ );
+
+ Future stopAdvertising() => _platform.stopAdvertising();
+
+ Future updateCharacteristicValue({
+ required PeripheralCharacteristicId characteristicId,
+ required Uint8List value,
+ PeripheralUpdateTarget target = const PeripheralUpdateAllSubscribed(),
+ }) =>
+ _platform.updateCharacteristicValue(
+ characteristicId: PeripheralCharacteristicId(
+ BleUuidParser.string(characteristicId.value),
+ ),
+ value: value,
+ target: target,
+ );
+
+ Future> getSubscribedClients(PeripheralCharacteristicId characteristicId) =>
+ _platform.getSubscribedClients(
+ PeripheralCharacteristicId(BleUuidParser.string(characteristicId.value)),
+ );
+
+ Future getMaximumNotifyLength(String deviceId) =>
+ _platform.getMaximumNotifyLength(deviceId);
+}
+
+class UniversalBlePeripheral {
+ static UniversalBlePeripheralClient? _clientInstance;
+ static UniversalBlePeripheralClient get _client =>
+ _clientInstance ??= UniversalBlePeripheralClient();
+
+ static UniversalBlePeripheralPlatform _defaultPlatform() {
+ if (kIsWeb || defaultTargetPlatform == TargetPlatform.linux) {
+ return UniversalBlePeripheralUnsupported();
+ }
+ return UniversalBlePeripheralPigeon.instance;
+ }
+
+ static void setInstance(UniversalBlePeripheralPlatform instance) {
+ if (_clientInstance == null) {
+ _clientInstance = UniversalBlePeripheralClient(platform: instance);
+ return;
+ }
+ _client.setPlatform(instance);
+ }
+
+ static Stream get eventStream =>
+ _client.eventStream;
+
+ static void setRequestHandlers(PeripheralRequestHandlers handlers) =>
+ _client.setRequestHandlers(handlers);
+
+ static Future getReadinessState() =>
+ _client.getReadinessState();
+
+ static Future getAdvertisingState() =>
+ _client.getAdvertisingState();
+
+ static Future getStaticCapabilities() =>
+ _client.getStaticCapabilities();
+
+ static Future addService(
+ BleService service, {
+ bool primary = true,
+ Duration? timeout,
+ }) =>
+ _client.addService(service, primary: primary, timeout: timeout);
+
+ static Future removeService(PeripheralServiceId serviceId) =>
+ _client.removeService(serviceId);
+ static Future clearServices() => _client.clearServices();
+ static Future> getServices() => _client.getServices();
+
+ static Future startAdvertising({
+ required List services,
+ String? localName,
+ int? timeout,
+ ManufacturerData? manufacturerData,
+ bool addManufacturerDataInScanResponse = false,
+ }) =>
+ _client.startAdvertising(
+ services: services,
+ localName: localName,
+ timeout: timeout,
+ manufacturerData: manufacturerData,
+ addManufacturerDataInScanResponse: addManufacturerDataInScanResponse,
+ );
+
+ static Future stopAdvertising() => _client.stopAdvertising();
+
+ static Future updateCharacteristicValue({
+ required PeripheralCharacteristicId characteristicId,
+ required Uint8List value,
+ PeripheralUpdateTarget target = const PeripheralUpdateAllSubscribed(),
+ }) =>
+ _client.updateCharacteristicValue(
+ characteristicId: characteristicId, value: value, target: target);
+
+ /// Returns client device ids currently subscribed to [characteristicId]
+ /// (e.g. HID report characteristic). Used to restore in-app state after restart.
+ static Future> getSubscribedClients(PeripheralCharacteristicId characteristicId) =>
+ _client.getSubscribedClients(characteristicId);
+
+ static Future getMaximumNotifyLength(String deviceId) =>
+ _client.getMaximumNotifyLength(deviceId);
+}
diff --git a/lib/src/universal_ble_peripheral/universal_ble_peripheral_mapper.dart b/lib/src/universal_ble_peripheral/universal_ble_peripheral_mapper.dart
new file mode 100644
index 00000000..231eeb6f
--- /dev/null
+++ b/lib/src/universal_ble_peripheral/universal_ble_peripheral_mapper.dart
@@ -0,0 +1,45 @@
+import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'
+ as pigeon;
+import 'package:universal_ble/universal_ble.dart';
+
+class UniversalBlePeripheralMapper {
+ static pigeon.PeripheralService toPigeonService(
+ BleService service, {
+ required bool primary,
+ }) {
+ return pigeon.PeripheralService(
+ uuid: BleUuidParser.string(service.uuid),
+ primary: primary,
+ characteristics: service.characteristics
+ .map(
+ (c) => pigeon.PeripheralCharacteristic(
+ uuid: BleUuidParser.string(c.uuid),
+ properties: c.properties.map((e) => e.index).toList(),
+ // Attribute permissions are peripheral-only and optional.
+ permissions: const [],
+ descriptors: c.descriptors
+ .map(
+ (d) => pigeon.PeripheralDescriptor(
+ uuid: BleUuidParser.string(d.uuid),
+ value: d.value,
+ permissions: const [],
+ ),
+ )
+ .toList(),
+ value: null,
+ ),
+ )
+ .toList(),
+ );
+ }
+
+ static pigeon.PeripheralManufacturerData? toPigeonManufacturerData(
+ ManufacturerData? manufacturerData,
+ ) {
+ if (manufacturerData == null) return null;
+ return pigeon.PeripheralManufacturerData(
+ manufacturerId: manufacturerData.companyId,
+ data: manufacturerData.payload,
+ );
+ }
+}
diff --git a/lib/src/universal_ble_peripheral/universal_ble_peripheral_pigeon.dart b/lib/src/universal_ble_peripheral/universal_ble_peripheral_pigeon.dart
new file mode 100644
index 00000000..92e4a6ae
--- /dev/null
+++ b/lib/src/universal_ble_peripheral/universal_ble_peripheral_pigeon.dart
@@ -0,0 +1,361 @@
+import 'dart:async';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'
+ as pigeon;
+import 'package:universal_ble/src/universal_ble_peripheral/universal_ble_peripheral_mapper.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+class UniversalBlePeripheralPigeon extends UniversalBlePeripheralPlatform
+ implements pigeon.UniversalBlePeripheralCallback {
+ UniversalBlePeripheralPigeon._() {
+ pigeon.UniversalBlePeripheralCallback.setUp(this);
+ }
+
+ static UniversalBlePeripheralPigeon? _instance;
+ static UniversalBlePeripheralPigeon get instance =>
+ _instance ??= UniversalBlePeripheralPigeon._();
+
+ final pigeon.UniversalBlePeripheralChannel _channel =
+ pigeon.UniversalBlePeripheralChannel();
+ final StreamController<({String serviceId, String? error})>
+ _serviceResultController =
+ StreamController<({String serviceId, String? error})>.broadcast();
+ final StreamController _eventController =
+ StreamController.broadcast();
+ bool _disposed = false;
+ OnPeripheralReadRequest? _readRequestHandler;
+ OnPeripheralWriteRequest? _writeRequestHandler;
+ OnPeripheralDescriptorReadRequest? _descriptorReadRequestHandler;
+ OnPeripheralDescriptorWriteRequest? _descriptorWriteRequestHandler;
+
+ @override
+ Stream get eventStream =>
+ _eventController.stream;
+
+ @override
+ void setRequestHandlers(PeripheralRequestHandlers handlers) {
+ _readRequestHandler = handlers.onReadRequest;
+ _writeRequestHandler = handlers.onWriteRequest;
+ _descriptorReadRequestHandler = handlers.onDescriptorReadRequest;
+ _descriptorWriteRequestHandler = handlers.onDescriptorWriteRequest;
+ }
+
+ @override
+ Future getReadinessState() async {
+ final state = await _channel.getReadinessState();
+ return switch (state) {
+ pigeon.PeripheralReadinessState.ready =>
+ UniversalBlePeripheralReadinessState.ready,
+ pigeon.PeripheralReadinessState.bluetoothOff =>
+ UniversalBlePeripheralReadinessState.bluetoothOff,
+ pigeon.PeripheralReadinessState.unauthorized =>
+ UniversalBlePeripheralReadinessState.unauthorized,
+ pigeon.PeripheralReadinessState.unsupported =>
+ UniversalBlePeripheralReadinessState.unsupported,
+ pigeon.PeripheralReadinessState.unknown =>
+ UniversalBlePeripheralReadinessState.unknown,
+ };
+ }
+
+ @override
+ Future getAdvertisingState() async {
+ final state = await _channel.getAdvertisingState();
+ return switch (state) {
+ pigeon.PeripheralAdvertisingState.idle =>
+ UniversalBlePeripheralAdvertisingState.idle,
+ pigeon.PeripheralAdvertisingState.starting =>
+ UniversalBlePeripheralAdvertisingState.starting,
+ pigeon.PeripheralAdvertisingState.advertising =>
+ UniversalBlePeripheralAdvertisingState.advertising,
+ pigeon.PeripheralAdvertisingState.stopping =>
+ UniversalBlePeripheralAdvertisingState.stopping,
+ pigeon.PeripheralAdvertisingState.error =>
+ UniversalBlePeripheralAdvertisingState.error,
+ };
+ }
+
+ @override
+ Future getStaticCapabilities() async {
+ final readiness = await getReadinessState();
+ final platform = defaultTargetPlatform;
+ final supported = readiness != UniversalBlePeripheralReadinessState.unsupported;
+
+ final supportsManufacturerDataInScanResponse =
+ platform == TargetPlatform.android;
+ final supportsAdvertisingTimeout = platform == TargetPlatform.android;
+
+ return UniversalBlePeripheralCapabilities(
+ supportsPeripheralMode: supported,
+ supportsManufacturerDataInAdvertisement: supported,
+ supportsManufacturerDataInScanResponse:
+ supported && supportsManufacturerDataInScanResponse,
+ supportsServiceDataInAdvertisement: false,
+ supportsServiceDataInScanResponse: false,
+ supportsTargetedCharacteristicUpdate: supported,
+ supportsAdvertisingTimeout: supported && supportsAdvertisingTimeout,
+ );
+ }
+
+ @override
+ Future addService(
+ BleService service, {
+ bool primary = true,
+ Duration? timeout,
+ }) async {
+ final String serviceId = BleUuidParser.string(service.uuid);
+ final Completer completer = Completer();
+ _serviceResultController.stream
+ .where((e) => BleUuidParser.compareStrings(e.serviceId, serviceId))
+ .first
+ .timeout(
+ timeout ?? const Duration(seconds: 5),
+ onTimeout: () =>
+ (serviceId: serviceId, error: 'Service add timed out'),
+ )
+ .then((e) {
+ if (completer.isCompleted) return;
+ if (e.error != null) {
+ completer.completeError(
+ PlatformException(code: 'service-add-failed', message: e.error),
+ );
+ } else {
+ completer.complete();
+ }
+ });
+
+ await _channel.addService(
+ UniversalBlePeripheralMapper.toPigeonService(service, primary: primary),
+ );
+ await completer.future;
+ }
+
+ @override
+ Future removeService(PeripheralServiceId serviceId) =>
+ _channel.removeService(BleUuidParser.string(serviceId.value));
+
+ @override
+ Future clearServices() => _channel.clearServices();
+
+ @override
+ Future> getServices() async =>
+ (await _channel.getServices()).map(PeripheralServiceId.new).toList();
+
+ @override
+ Future startAdvertising({
+ required List services,
+ String? localName,
+ int? timeout,
+ ManufacturerData? manufacturerData,
+ bool addManufacturerDataInScanResponse = false,
+ }) {
+ return _channel.startAdvertising(
+ services.map((e) => BleUuidParser.string(e.value)).toList(),
+ localName,
+ timeout,
+ UniversalBlePeripheralMapper.toPigeonManufacturerData(manufacturerData),
+ addManufacturerDataInScanResponse,
+ );
+ }
+
+ @override
+ Future stopAdvertising() => _channel.stopAdvertising();
+
+ @override
+ Future updateCharacteristicValue({
+ required PeripheralCharacteristicId characteristicId,
+ required Uint8List value,
+ PeripheralUpdateTarget target = const PeripheralUpdateAllSubscribed(),
+ }) {
+ final String? deviceId = switch (target) {
+ PeripheralUpdateAllSubscribed() => null,
+ PeripheralUpdateSingleDevice(deviceId: final id) => id,
+ };
+ return _channel.updateCharacteristic(
+ BleUuidParser.string(characteristicId.value),
+ value,
+ deviceId,
+ );
+ }
+
+ @override
+ Future> getSubscribedClients(PeripheralCharacteristicId characteristicId) =>
+ _channel.getSubscribedClients(characteristicId.value);
+
+ @override
+ Future getMaximumNotifyLength(String deviceId) =>
+ _channel.getMaximumNotifyLength(deviceId);
+
+ @override
+ void onAdvertisingStateChange(
+ pigeon.PeripheralAdvertisingState state,
+ String? error,
+ ) {
+ final mapped = switch (state) {
+ pigeon.PeripheralAdvertisingState.idle =>
+ UniversalBlePeripheralAdvertisingState.idle,
+ pigeon.PeripheralAdvertisingState.starting =>
+ UniversalBlePeripheralAdvertisingState.starting,
+ pigeon.PeripheralAdvertisingState.advertising =>
+ UniversalBlePeripheralAdvertisingState.advertising,
+ pigeon.PeripheralAdvertisingState.stopping =>
+ UniversalBlePeripheralAdvertisingState.stopping,
+ pigeon.PeripheralAdvertisingState.error =>
+ UniversalBlePeripheralAdvertisingState.error,
+ };
+ if (!_eventController.isClosed) {
+ _eventController.add(
+ UniversalBlePeripheralAdvertisingStateChanged(mapped, error),
+ );
+ }
+ }
+
+ @override
+ void onCharacteristicSubscriptionChange(
+ String deviceId,
+ String characteristicId,
+ bool isSubscribed,
+ String? name,
+ ) {
+ if (!_eventController.isClosed) {
+ _eventController.add(
+ UniversalBlePeripheralCharacteristicSubscriptionChanged(
+ deviceId: deviceId,
+ characteristicId: characteristicId,
+ isSubscribed: isSubscribed,
+ name: name,
+ ),
+ );
+ }
+ }
+
+ @override
+ void onConnectionStateChange(String deviceId, bool connected) {
+ if (!_eventController.isClosed) {
+ _eventController.add(
+ UniversalBlePeripheralConnectionStateChanged(deviceId, connected),
+ );
+ }
+ }
+
+ @override
+ void onMtuChange(String deviceId, int mtu) {
+ if (!_eventController.isClosed) {
+ _eventController.add(UniversalBlePeripheralMtuChanged(deviceId, mtu));
+ }
+ }
+
+ @override
+ pigeon.PeripheralReadRequestResult? onReadRequest(
+ String deviceId,
+ String characteristicId,
+ int offset,
+ Uint8List? value,
+ ) {
+ final result = _readRequestHandler?.call(
+ deviceId,
+ PeripheralCharacteristicId(characteristicId),
+ offset,
+ value,
+ );
+ if (result == null) return null;
+ return pigeon.PeripheralReadRequestResult(
+ value: result.value,
+ offset: result.offset,
+ status: result.status,
+ );
+ }
+
+ @override
+ void onServiceAdded(String serviceId, String? error) {
+ if (!_eventController.isClosed) {
+ _eventController.add(UniversalBlePeripheralServiceAdded(serviceId, error));
+ }
+ if (!_serviceResultController.isClosed) {
+ _serviceResultController.add((serviceId: serviceId, error: error));
+ }
+ }
+
+ @override
+ pigeon.PeripheralWriteRequestResult? onWriteRequest(
+ String deviceId,
+ String characteristicId,
+ int offset,
+ Uint8List? value,
+ ) {
+ final result = _writeRequestHandler?.call(
+ deviceId,
+ PeripheralCharacteristicId(characteristicId),
+ offset,
+ value,
+ );
+ if (result == null) return null;
+ return pigeon.PeripheralWriteRequestResult(
+ value: result.value,
+ offset: result.offset,
+ status: result.status,
+ );
+ }
+
+ @override
+ pigeon.PeripheralReadRequestResult? onDescriptorReadRequest(
+ String deviceId,
+ String characteristicId,
+ String descriptorId,
+ int offset,
+ Uint8List? value,
+ ) {
+ final result = _descriptorReadRequestHandler?.call(
+ deviceId,
+ PeripheralCharacteristicId(characteristicId),
+ PeripheralDescriptorId(descriptorId),
+ offset,
+ value,
+ );
+ if (result == null) return null;
+ return pigeon.PeripheralReadRequestResult(
+ value: result.value,
+ offset: result.offset,
+ status: result.status,
+ );
+ }
+
+ @override
+ pigeon.PeripheralWriteRequestResult? onDescriptorWriteRequest(
+ String deviceId,
+ String characteristicId,
+ String descriptorId,
+ int offset,
+ Uint8List? value,
+ ) {
+ final result = _descriptorWriteRequestHandler?.call(
+ deviceId,
+ PeripheralCharacteristicId(characteristicId),
+ PeripheralDescriptorId(descriptorId),
+ offset,
+ value,
+ );
+ if (result == null) return null;
+ return pigeon.PeripheralWriteRequestResult(
+ value: result.value,
+ offset: result.offset,
+ status: result.status,
+ );
+ }
+
+ @override
+ void dispose() {
+ if (_disposed) return;
+ _disposed = true;
+ pigeon.UniversalBlePeripheralCallback.setUp(null);
+ if (!_serviceResultController.isClosed) {
+ _serviceResultController.close();
+ }
+ if (!_eventController.isClosed) {
+ _eventController.close();
+ }
+ if (identical(_instance, this)) {
+ _instance = null;
+ }
+ }
+}
diff --git a/lib/src/universal_ble_peripheral/universal_ble_peripheral_platform_interface.dart b/lib/src/universal_ble_peripheral/universal_ble_peripheral_platform_interface.dart
new file mode 100644
index 00000000..9be35a8a
--- /dev/null
+++ b/lib/src/universal_ble_peripheral/universal_ble_peripheral_platform_interface.dart
@@ -0,0 +1,286 @@
+import 'dart:typed_data';
+
+import 'package:universal_ble/universal_ble.dart';
+
+typedef OnPeripheralReadRequest = BleReadRequestResult? Function(
+ String deviceId,
+ PeripheralCharacteristicId characteristicId,
+ int offset,
+ Uint8List? value,
+);
+typedef OnPeripheralWriteRequest = BleWriteRequestResult? Function(
+ String deviceId,
+ PeripheralCharacteristicId characteristicId,
+ int offset,
+ Uint8List? value,
+);
+typedef OnPeripheralDescriptorReadRequest = BleReadRequestResult? Function(
+ String deviceId,
+ PeripheralCharacteristicId characteristicId,
+ PeripheralDescriptorId descriptorId,
+ int offset,
+ Uint8List? value,
+);
+typedef OnPeripheralDescriptorWriteRequest = BleWriteRequestResult? Function(
+ String deviceId,
+ PeripheralCharacteristicId characteristicId,
+ PeripheralDescriptorId descriptorId,
+ int offset,
+ Uint8List? value,
+);
+
+enum UniversalBlePeripheralReadinessState {
+ unknown,
+ ready,
+ bluetoothOff,
+ unauthorized,
+ unsupported,
+}
+
+enum UniversalBlePeripheralAdvertisingState {
+ idle,
+ starting,
+ advertising,
+ stopping,
+ error,
+}
+
+sealed class UniversalBlePeripheralEvent {}
+
+class UniversalBlePeripheralAdvertisingStateChanged
+ extends UniversalBlePeripheralEvent {
+ final UniversalBlePeripheralAdvertisingState state;
+ final String? error;
+ UniversalBlePeripheralAdvertisingStateChanged(this.state, this.error);
+}
+
+class UniversalBlePeripheralCharacteristicSubscriptionChanged
+ extends UniversalBlePeripheralEvent {
+ final String deviceId;
+ final String characteristicId;
+ final bool isSubscribed;
+ final String? name;
+ UniversalBlePeripheralCharacteristicSubscriptionChanged({
+ required this.deviceId,
+ required this.characteristicId,
+ required this.isSubscribed,
+ required this.name,
+ });
+}
+
+class UniversalBlePeripheralConnectionStateChanged
+ extends UniversalBlePeripheralEvent {
+ final String deviceId;
+ final bool connected;
+ UniversalBlePeripheralConnectionStateChanged(this.deviceId, this.connected);
+}
+
+class UniversalBlePeripheralServiceAdded extends UniversalBlePeripheralEvent {
+ final String serviceId;
+ final String? error;
+ UniversalBlePeripheralServiceAdded(this.serviceId, this.error);
+}
+
+class UniversalBlePeripheralMtuChanged extends UniversalBlePeripheralEvent {
+ final String deviceId;
+ final int mtu;
+ UniversalBlePeripheralMtuChanged(this.deviceId, this.mtu);
+}
+
+class UniversalBlePeripheralCapabilities {
+ final bool supportsPeripheralMode;
+ final bool supportsManufacturerDataInAdvertisement;
+ final bool supportsManufacturerDataInScanResponse;
+ final bool supportsServiceDataInAdvertisement;
+ final bool supportsServiceDataInScanResponse;
+ final bool supportsTargetedCharacteristicUpdate;
+ final bool supportsAdvertisingTimeout;
+
+ const UniversalBlePeripheralCapabilities({
+ required this.supportsPeripheralMode,
+ required this.supportsManufacturerDataInAdvertisement,
+ required this.supportsManufacturerDataInScanResponse,
+ required this.supportsServiceDataInAdvertisement,
+ required this.supportsServiceDataInScanResponse,
+ required this.supportsTargetedCharacteristicUpdate,
+ required this.supportsAdvertisingTimeout,
+ });
+}
+
+class PeripheralServiceId {
+ final String value;
+ const PeripheralServiceId(this.value);
+}
+
+class PeripheralCharacteristicId {
+ final String value;
+ const PeripheralCharacteristicId(this.value);
+}
+
+class PeripheralDescriptorId {
+ final String value;
+ const PeripheralDescriptorId(this.value);
+}
+
+class PeripheralRequestHandlers {
+ final OnPeripheralReadRequest? onReadRequest;
+ final OnPeripheralWriteRequest? onWriteRequest;
+ final OnPeripheralDescriptorReadRequest? onDescriptorReadRequest;
+ final OnPeripheralDescriptorWriteRequest? onDescriptorWriteRequest;
+
+ const PeripheralRequestHandlers({
+ this.onReadRequest,
+ this.onWriteRequest,
+ this.onDescriptorReadRequest,
+ this.onDescriptorWriteRequest,
+ });
+}
+
+sealed class PeripheralUpdateTarget {
+ const PeripheralUpdateTarget();
+}
+
+class PeripheralUpdateAllSubscribed extends PeripheralUpdateTarget {
+ const PeripheralUpdateAllSubscribed();
+}
+
+class PeripheralUpdateSingleDevice extends PeripheralUpdateTarget {
+ final String deviceId;
+ const PeripheralUpdateSingleDevice(this.deviceId);
+}
+
+abstract class UniversalBlePeripheralPlatform {
+ Stream get eventStream;
+
+ void setRequestHandlers(PeripheralRequestHandlers handlers);
+
+ Future getReadinessState();
+ Future getAdvertisingState();
+ Future getStaticCapabilities();
+
+ Future addService(
+ BleService service, {
+ bool primary = true,
+ Duration? timeout,
+ });
+
+ Future removeService(PeripheralServiceId serviceId);
+ Future clearServices();
+ Future> getServices();
+ Future startAdvertising({
+ required List services,
+ String? localName,
+ int? timeout,
+ ManufacturerData? manufacturerData,
+ bool addManufacturerDataInScanResponse = false,
+ });
+ Future stopAdvertising();
+ Future updateCharacteristicValue({
+ required PeripheralCharacteristicId characteristicId,
+ required Uint8List value,
+ PeripheralUpdateTarget target = const PeripheralUpdateAllSubscribed(),
+ });
+
+ /// Returns GATT client device ids currently subscribed to [characteristicId].
+ Future> getSubscribedClients(PeripheralCharacteristicId characteristicId);
+ Future getMaximumNotifyLength(String deviceId);
+
+ /// Called when this platform implementation is being replaced.
+ ///
+ /// Default is no-op so existing custom implementations remain compatible.
+ void dispose() {}
+}
+
+class UniversalBlePeripheralUnsupported extends UniversalBlePeripheralPlatform {
+ UnsupportedError _notSupported() =>
+ UnsupportedError('BLE peripheral mode is not supported on this platform');
+
+ @override
+ Stream get eventStream =>
+ const Stream.empty();
+
+ @override
+ void setRequestHandlers(PeripheralRequestHandlers handlers) {}
+
+ @override
+ Future addService(
+ BleService service, {
+ bool primary = true,
+ Duration? timeout,
+ }) async {
+ throw _notSupported();
+ }
+
+ @override
+ Future clearServices() async {
+ throw _notSupported();
+ }
+
+ @override
+ Future> getServices() async {
+ throw _notSupported();
+ }
+
+ @override
+ Future getAdvertisingState() async {
+ throw _notSupported();
+ }
+
+ @override
+ Future