diff --git a/bluetooth_low_energy/CHANGELOG.md b/bluetooth_low_energy/CHANGELOG.md index 4b4abff9..13dcd2f7 100644 --- a/bluetooth_low_energy/CHANGELOG.md +++ b/bluetooth_low_energy/CHANGELOG.md @@ -1,9 +1,15 @@ +## 3.0.2 + +* `Android` `iOS` Fix the issue that `getMaximumWriteLength` is wrong and coerce the value from 20 to 512. +* `Android` `iOS` Fix the issue that the peripheral manager response is wrong. +* `Android` Request MTU with 517 automatically. + ## 3.0.1 -* [Android] Clear cache when disconnected. -* [Android] Fix GATT server error aftter bluetooth reopened. -* [iOS] Fix the issue that write characteristic will never complete when write without response. -* [iOS] Fix the issue that write characteristic will never complete after disconnected. +* `Android` Clear cache when disconnected. +* `Android` Fix GATT server error aftter bluetooth reopened. +* `iOS` Fix the issue that write characteristic will never complete when write without response. +* `iOS` Fix the issue that write characteristic will never complete after disconnected. ## 3.0.0 diff --git a/bluetooth_low_energy/README.md b/bluetooth_low_energy/README.md index 69c38cbf..8b0d017a 100644 --- a/bluetooth_low_energy/README.md +++ b/bluetooth_low_energy/README.md @@ -47,13 +47,11 @@ Remember to call `await CentralController.setUp()` before use any apis of this p Make sure you have a `miniSdkVersion` with 21 or higher in your `android/app/build.gradle` file. -*Note:* Don't call `getMaximumWriteLength` immediately when connected to a peripheral after Android 13, the `onMtuChanged` callback maybe called with connection events after Android 13, and `getMaximumWriteLength` will call `requestMtu` will also triggered `onMtuChanged`, if you called this before the connection `onMtuChanged`, you will get a fake completion and cause all methods you called before the real `onMtuChanged` triggered will never complete! - ### iOS and macOS According to Apple's [documents](https://developer.apple.com/documentation/corebluetooth/), you must include the [`NSBluetoothAlwaysUsageDescription`](https://developer.apple.com/documentation/bundleresources/information_property_list/nsbluetoothalwaysusagedescription) on or after iOS 13, and include the [`NSBluetoothPeripheralUsageDescription`](https://developer.apple.com/documentation/bundleresources/information_property_list/nsbluetoothperipheralusagedescription) key before iOS 13. -The `PeripheralManager#startAdvertising` only support `name` and `serviceUUIDs`, see [the startAdvertising document](https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393252-startadvertising) +*Note:* The `PeripheralManager#startAdvertising` only support `name` and `serviceUUIDs`, see [the startAdvertising document](https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393252-startadvertising) ### Linux diff --git a/bluetooth_low_energy/example/lib/main.dart b/bluetooth_low_energy/example/lib/main.dart index 6adb4e91..3503d74b 100644 --- a/bluetooth_low_energy/example/lib/main.dart +++ b/bluetooth_low_energy/example/lib/main.dart @@ -329,7 +329,7 @@ class _ScannerViewState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text('$rssi'), + trailing: RssiWidget(rssi), ); }, separatorBuilder: (context, i) { @@ -375,10 +375,13 @@ class _PeripheralViewState extends State { late final ValueNotifier characteristic; late final ValueNotifier writeType; late final ValueNotifier maximumWriteLength; + late final ValueNotifier rssi; late final ValueNotifier> logs; late final TextEditingController writeController; late final StreamSubscription stateChangedSubscription; late final StreamSubscription valueChangedSubscription; + late final StreamSubscription rssiChangedSubscription; + late final Timer rssiTimer; @override void initState() { @@ -390,7 +393,8 @@ class _PeripheralViewState extends State { service = ValueNotifier(null); characteristic = ValueNotifier(null); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); - maximumWriteLength = ValueNotifier(20); + maximumWriteLength = ValueNotifier(0); + rssi = ValueNotifier(-100); logs = ValueNotifier([]); writeController = TextEditingController(); stateChangedSubscription = centralManager.peripheralStateChanged.listen( @@ -423,6 +427,17 @@ class _PeripheralViewState extends State { ]; }, ); + rssiTimer = Timer.periodic( + const Duration(seconds: 1), + (timer) async { + final state = this.state.value; + if (state) { + rssi.value = await centralManager.readRSSI(eventArgs.peripheral); + } else { + rssi.value = -100; + } + }, + ); } @override @@ -455,10 +470,18 @@ class _PeripheralViewState extends State { final peripheral = eventArgs.peripheral; if (state) { await centralManager.disconnect(peripheral); + maximumWriteLength.value = 0; + rssi.value = 0; } else { await centralManager.connect(peripheral); services.value = await centralManager.discoverGATT(peripheral); + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + peripheral, + type: writeType.value, + ); + rssi.value = await centralManager.readRSSI(peripheral); } }, child: Text(state ? 'DISCONNECT' : 'CONNECT'), @@ -592,72 +615,82 @@ class _PeripheralViewState extends State { ), Row( children: [ - Expanded( - child: Center( - child: ValueListenableBuilder( - valueListenable: writeType, - builder: (context, writeType, child) { - final items = - GattCharacteristicWriteType.values.map((type) { - return DropdownMenuItem( - value: type, - child: Text( - type.name, - style: theme.textTheme.bodyMedium, - ), - ); - }).toList(); - return DropdownButton( - items: items, - onChanged: (type) { - if (type == null) { - return; - } - this.writeType.value = type; - }, - value: writeType, - underline: const Offstage(), + ValueListenableBuilder( + valueListenable: writeType, + builder: (context, writeType, child) { + return ToggleButtons( + onPressed: (i) async { + final type = GattCharacteristicWriteType.values[i]; + this.writeType.value = type; + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + eventArgs.peripheral, + type: type, ); }, - ), - ), + constraints: const BoxConstraints( + minWidth: 0.0, + minHeight: 0.0, + ), + borderRadius: BorderRadius.circular(4.0), + isSelected: GattCharacteristicWriteType.values + .map((type) => type == writeType) + .toList(), + children: GattCharacteristicWriteType.values.map((type) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Text(type.name), + ); + }).toList(), + ); + // final segments = + // GattCharacteristicWriteType.values.map((type) { + // return ButtonSegment( + // value: type, + // label: Text(type.name), + // ); + // }).toList(); + // return SegmentedButton( + // segments: segments, + // selected: {writeType}, + // showSelectedIcon: false, + // style: OutlinedButton.styleFrom( + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // padding: EdgeInsets.zero, + // visualDensity: VisualDensity.compact, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8.0), + // ), + // ), + // ); + }, ), - Expanded( - child: ValueListenableBuilder( - valueListenable: state, - builder: (context, state, child) { - return TextButton( - onPressed: state - ? () async { - maximumWriteLength.value = - await centralManager.getMaximumWriteLength( - eventArgs.peripheral, - type: writeType.value, - ); - } - : null, - child: ValueListenableBuilder( - valueListenable: maximumWriteLength, - builder: (context, maximumWriteLength, child) { - return Text('MTU: $maximumWriteLength'); - }, - ), - ); - }, - ), + const SizedBox(width: 8.0), + ValueListenableBuilder( + valueListenable: state, + builder: (context, state, child) { + return ValueListenableBuilder( + valueListenable: maximumWriteLength, + builder: (context, maximumWriteLength, child) { + return Text('$maximumWriteLength'); + }, + ); + }, ), - IconButton( - onPressed: () async { - final rssi = - await centralManager.readRSSI(eventArgs.peripheral); - log('RSSI: $rssi'); + const Spacer(), + ValueListenableBuilder( + valueListenable: rssi, + builder: (context, rssi, child) { + return RssiWidget(rssi); }, - icon: const Icon(Icons.signal_wifi_4_bar), ), ], ), Container( - margin: const EdgeInsets.symmetric(vertical: 16.0), + margin: const EdgeInsets.only(bottom: 16.0), height: 160.0, child: ValueListenableBuilder( valueListenable: characteristic, @@ -755,6 +788,7 @@ class _PeripheralViewState extends State { @override void dispose() { super.dispose(); + rssiTimer.cancel(); stateChangedSubscription.cancel(); valueChangedSubscription.cancel(); state.dispose(); @@ -764,6 +798,7 @@ class _PeripheralViewState extends State { characteristic.dispose(); writeType.dispose(); maximumWriteLength.dispose(); + rssi.dispose(); logs.dispose(); writeController.dispose(); } @@ -1070,3 +1105,25 @@ enum LogType { notify, error, } + +class RssiWidget extends StatelessWidget { + final int rssi; + + const RssiWidget( + this.rssi, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final IconData icon; + if (rssi > -70) { + icon = Icons.wifi_rounded; + } else if (rssi > -100) { + icon = Icons.wifi_2_bar_rounded; + } else { + icon = Icons.wifi_1_bar_rounded; + } + return Icon(icon); + } +} diff --git a/bluetooth_low_energy/example/pubspec.lock b/bluetooth_low_energy/example/pubspec.lock index d44f61de..40e7f8ec 100644 --- a/bluetooth_low_energy/example/pubspec.lock +++ b/bluetooth_low_energy/example/pubspec.lock @@ -23,23 +23,23 @@ packages: path: ".." relative: true source: path - version: "3.0.1" + version: "3.0.2" bluetooth_low_energy_android: dependency: transitive description: name: bluetooth_low_energy_android - sha256: "54fbbc1ce1a25ae20692a71585e551f1d5e2d565f41c6f9dce13457f00e9bae8" + sha256: "0fa5c4625ac01b6d4bbf78b10d0389d8e9907ae7c3fbc9601b481ddd4eaa86b6" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" bluetooth_low_energy_darwin: dependency: transitive description: name: bluetooth_low_energy_darwin - sha256: "114e492a020ac1efaa3b169eba8b6c01f50f97f2bbcda2f1fff890a5abf353ae" + sha256: "797d3803de3b124ffb13267910f8d727ae4884fdcc621ccc0995076107468bb6" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" bluetooth_low_energy_linux: dependency: transitive description: diff --git a/bluetooth_low_energy/pubspec.yaml b/bluetooth_low_energy/pubspec.yaml index 44969f41..dba31de9 100644 --- a/bluetooth_low_energy/pubspec.yaml +++ b/bluetooth_low_energy/pubspec.yaml @@ -1,6 +1,6 @@ name: bluetooth_low_energy description: A Flutter plugin for controlling the bluetooth low energy. -version: 3.0.1 +version: 3.0.2 homepage: https://github.com/yanshouwang/bluetooth_low_energy environment: @@ -11,8 +11,8 @@ dependencies: flutter: sdk: flutter bluetooth_low_energy_platform_interface: ^3.0.0 - bluetooth_low_energy_android: ^3.0.1 - bluetooth_low_energy_darwin: ^3.0.1 + bluetooth_low_energy_android: ^3.0.3 + bluetooth_low_energy_darwin: ^3.0.2 bluetooth_low_energy_linux: ^3.0.0 dev_dependencies: diff --git a/bluetooth_low_energy_android/CHANGELOG.md b/bluetooth_low_energy_android/CHANGELOG.md index eea8645b..d684a6e7 100644 --- a/bluetooth_low_energy_android/CHANGELOG.md +++ b/bluetooth_low_energy_android/CHANGELOG.md @@ -1,3 +1,13 @@ +## 3.0.3 + +* Fix the issue that `getMaximumWriteLength` is wrong and coerce the value from 20 to 512. + +## 3.0.2 + +* Request MTU with 517 automatically. +* Fix the issue taht `CentralManager#getMaximumWriteLength` is wrong when write with response and coerce the value from 20 to 512. +* Fix the issue that the GATT server response is wrong. + ## 3.0.1 * Clear cache when disconnected. diff --git a/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyApi.g.kt b/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyApi.g.kt index 3cdb6ad4..d77bd909 100644 --- a/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyApi.g.kt +++ b/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyApi.g.kt @@ -351,9 +351,10 @@ interface MyCentralManagerHostApi { fun stopDiscovery() fun connect(peripheralHashCodeArgs: Long, callback: (Result) -> Unit) fun disconnect(peripheralHashCodeArgs: Long, callback: (Result) -> Unit) - fun getMaximumWriteLength(peripheralHashCodeArgs: Long, callback: (Result) -> Unit) + fun getMaximumWriteLength(peripheralHashCodeArgs: Long, typeNumberArgs: Long): Long fun readRSSI(peripheralHashCodeArgs: Long, callback: (Result) -> Unit) fun discoverGATT(peripheralHashCodeArgs: Long, callback: (Result>) -> Unit) + fun requestMTU(peripheralHashCodeArgs: Long, mtuArgs: Long, callback: (Result) -> Unit) fun readCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, callback: (Result) -> Unit) fun writeCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, valueArgs: ByteArray, typeNumberArgs: Long, callback: (Result) -> Unit) fun notifyCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, stateArgs: Boolean, callback: (Result) -> Unit) @@ -464,7 +465,26 @@ interface MyCentralManagerHostApi { channel.setMessageHandler { message, reply -> val args = message as List val peripheralHashCodeArgsArg = args[0].let { if (it is Int) it.toLong() else it as Long } - api.getMaximumWriteLength(peripheralHashCodeArgsArg) { result: Result -> + val typeNumberArgsArg = args[1].let { if (it is Int) it.toLong() else it as Long } + var wrapped: List + try { + wrapped = listOf(api.getMaximumWriteLength(peripheralHashCodeArgsArg, typeNumberArgsArg)) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.readRSSI", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val peripheralHashCodeArgsArg = args[0].let { if (it is Int) it.toLong() else it as Long } + api.readRSSI(peripheralHashCodeArgsArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { reply.reply(wrapError(error)) @@ -479,12 +499,12 @@ interface MyCentralManagerHostApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.readRSSI", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.discoverGATT", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val peripheralHashCodeArgsArg = args[0].let { if (it is Int) it.toLong() else it as Long } - api.readRSSI(peripheralHashCodeArgsArg) { result: Result -> + api.discoverGATT(peripheralHashCodeArgsArg) { result: Result> -> val error = result.exceptionOrNull() if (error != null) { reply.reply(wrapError(error)) @@ -499,12 +519,13 @@ interface MyCentralManagerHostApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.discoverGATT", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.requestMTU", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val peripheralHashCodeArgsArg = args[0].let { if (it is Int) it.toLong() else it as Long } - api.discoverGATT(peripheralHashCodeArgsArg) { result: Result> -> + val mtuArgsArg = args[1].let { if (it is Int) it.toLong() else it as Long } + api.requestMTU(peripheralHashCodeArgsArg, mtuArgsArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { reply.reply(wrapError(error)) diff --git a/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyBluetoothGattServerCallback.kt b/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyBluetoothGattServerCallback.kt index 633b3da4..2a5cbc10 100644 --- a/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyBluetoothGattServerCallback.kt +++ b/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyBluetoothGattServerCallback.kt @@ -15,6 +15,13 @@ class MyBluetoothGattServerCallback(private val peripheralManager: MyPeripheralM } } + override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { + super.onConnectionStateChange(device, status, newState) + executor.execute { + peripheralManager.onConnectionStateChange(device, status, newState) + } + } + override fun onMtuChanged(device: BluetoothDevice, mtu: Int) { super.onMtuChanged(device, mtu) executor.execute { @@ -36,6 +43,13 @@ class MyBluetoothGattServerCallback(private val peripheralManager: MyPeripheralM } } + override fun onExecuteWrite(device: BluetoothDevice, requestId: Int, execute: Boolean) { + super.onExecuteWrite(device, requestId, execute) + executor.execute { + peripheralManager.onExecuteWrite(device, requestId, execute) + } + } + override fun onNotificationSent(device: BluetoothDevice, status: Int) { super.onNotificationSent(device, status) executor.execute { diff --git a/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyCentralManager.kt b/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyCentralManager.kt index 98dfce8e..0cf76da3 100644 --- a/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyCentralManager.kt +++ b/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyCentralManager.kt @@ -29,6 +29,7 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess private val services = mutableMapOf() private val characteristics = mutableMapOf() private val descriptors = mutableMapOf() + private val mtus = mutableMapOf() private val peripheralsArgs = mutableMapOf() private val servicesArgsOfPeripherals = mutableMapOf>() @@ -43,7 +44,7 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess private var startDiscoveryCallback: ((Result) -> Unit)? = null private val connectCallbacks = mutableMapOf) -> Unit>() private val disconnectCallbacks = mutableMapOf) -> Unit>() - private val getMaximumWriteLengthCallbacks = mutableMapOf) -> Unit>() + private val requestMtuCallbacks = mutableMapOf) -> Unit>() private val readRssiCallbacks = mutableMapOf) -> Unit>() private val discoverGattCallbacks = mutableMapOf>) -> Unit>() private val readCharacteristicCallbacks = mutableMapOf) -> Unit>() @@ -89,6 +90,7 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess services.clear() characteristics.clear() descriptors.clear() + mtus.clear() peripheralsArgs.clear() servicesArgsOfPeripherals.clear() servicesArgs.clear() @@ -98,7 +100,7 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess startDiscoveryCallback = null connectCallbacks.clear() disconnectCallbacks.clear() - getMaximumWriteLengthCallbacks.clear() + requestMtuCallbacks.clear() readRssiCallbacks.clear() discoverGattCallbacks.clear() readCharacteristicCallbacks.clear() @@ -172,23 +174,33 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess } } - override fun getMaximumWriteLength(peripheralHashCodeArgs: Long, callback: (Result) -> Unit) { + override fun requestMTU(peripheralHashCodeArgs: Long, mtuArgs: Long, callback: (Result) -> Unit) { try { - val unfinishedCallback = getMaximumWriteLengthCallbacks[peripheralHashCodeArgs] + val unfinishedCallback = requestMtuCallbacks[peripheralHashCodeArgs] if (unfinishedCallback != null) { throw IllegalStateException() } val gatt = bluetoothGATTs[peripheralHashCodeArgs] as BluetoothGatt - val requesting = gatt.requestMtu(517) + val mtu = mtuArgs.toInt() + val requesting = gatt.requestMtu(mtu) if (!requesting) { throw IllegalStateException() } - getMaximumWriteLengthCallbacks[peripheralHashCodeArgs] = callback + requestMtuCallbacks[peripheralHashCodeArgs] = callback } catch (e: Throwable) { callback(Result.failure(e)) } } + override fun getMaximumWriteLength(peripheralHashCodeArgs: Long, typeNumberArgs: Long): Long { + val mtu = mtus[peripheralHashCodeArgs] ?: 23 + val maximumWriteLength = when (typeNumberArgs.toWriteTypeArgs()) { + MyGattCharacteristicWriteTypeArgs.WITHRESPONSE -> 512 + MyGattCharacteristicWriteTypeArgs.WITHOUTRESPONSE -> (mtu - 3).coerceIn(20, 512) + } + return maximumWriteLength.toLong() + } + override fun readRSSI(peripheralHashCodeArgs: Long, callback: (Result) -> Unit) { try { val unfinishedCallback = readRssiCallbacks[peripheralHashCodeArgs] @@ -414,14 +426,15 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess val deviceHashCode = device.hashCode() val peripheralArgs = peripheralsArgs[deviceHashCode] as MyPeripheralArgs val peripheralHashCodeArgs = peripheralArgs.hashCodeArgs - // Check callbacks - if (newState != BluetoothProfile.STATE_CONNECTED) { + // check connection state. + if (newState == BluetoothProfile.STATE_DISCONNECTED) { gatt.close() bluetoothGATTs.remove(peripheralHashCodeArgs) + mtus.remove(peripheralHashCodeArgs) val error = IllegalStateException("GATT is disconnected with status: $status") - val getMaximumWriteLengthCallback = getMaximumWriteLengthCallbacks.remove(peripheralHashCodeArgs) - if (getMaximumWriteLengthCallback != null) { - getMaximumWriteLengthCallback(Result.failure(error)) + val requestMtuCallback = requestMtuCallbacks.remove(peripheralHashCodeArgs) + if (requestMtuCallback != null) { + requestMtuCallback(Result.failure(error)) } val readRssiCallback = readRssiCallbacks.remove(peripheralHashCodeArgs) if (readRssiCallback != null) { @@ -470,35 +483,25 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess } } } - // Check state + val stateArgs = newState == BluetoothProfile.STATE_CONNECTED + api.onPeripheralStateChanged(peripheralArgs, stateArgs) {} + // check connect & disconnect callbacks. val connectCallback = connectCallbacks.remove(peripheralHashCodeArgs) val disconnectCallback = disconnectCallbacks.remove(peripheralHashCodeArgs) - if (connectCallback == null && disconnectCallback == null) { - // State changed. - val stateArgs = newState == BluetoothProfile.STATE_CONNECTED - api.onPeripheralStateChanged(peripheralArgs, stateArgs) {} - } else { - if (connectCallback != null) { - if (status == BluetoothGatt.GATT_SUCCESS) { - // Connect succeed. - connectCallback(Result.success(Unit)) - api.onPeripheralStateChanged(peripheralArgs, true) {} - } else { - // Connect failed. - val error = IllegalStateException("Connect failed with status: $status") - connectCallback(Result.failure(error)) - } + if (connectCallback != null) { + if (status == BluetoothGatt.GATT_SUCCESS) { + connectCallback(Result.success(Unit)) + } else { + val error = IllegalStateException("Connect failed with status: $status") + connectCallback(Result.failure(error)) } - if (disconnectCallback != null) { - if (status == BluetoothGatt.GATT_SUCCESS) { - // Disconnect succeed. - disconnectCallback(Result.success(Unit)) - api.onPeripheralStateChanged(peripheralArgs, false) {} - } else { - // Disconnect failed. - val error = IllegalStateException("Connect failed with status: $status") - disconnectCallback(Result.failure(error)) - } + } + if (disconnectCallback != null) { + if (status == BluetoothGatt.GATT_SUCCESS) { + disconnectCallback(Result.success(Unit)) + } else { + val error = IllegalStateException("Disconnect failed with status: $status") + disconnectCallback(Result.failure(error)) } } } @@ -508,12 +511,15 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess val hashCode = device.hashCode() val peripheralArgs = peripheralsArgs[hashCode] as MyPeripheralArgs val peripheralHashCodeArgs = peripheralArgs.hashCodeArgs - val callback = getMaximumWriteLengthCallbacks.remove(peripheralHashCodeArgs) ?: return if (status == BluetoothGatt.GATT_SUCCESS) { - val maximumWriteLengthArgs = (mtu - 3).toLong() - callback(Result.success(maximumWriteLengthArgs)) + mtus[peripheralHashCodeArgs] = mtu + } + val callback = requestMtuCallbacks.remove(peripheralHashCodeArgs) ?: return + if (status == BluetoothGatt.GATT_SUCCESS) { + val mtuArgs = mtu.toLong() + callback(Result.success(mtuArgs)) } else { - val error = IllegalStateException("Get maximum write length failed with status: $status") + val error = IllegalStateException("Request MTU failed with status: $status") callback(Result.failure(error)) } } diff --git a/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyPeripheralManager.kt b/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyPeripheralManager.kt index c6071d3c..3c285505 100644 --- a/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyPeripheralManager.kt +++ b/bluetooth_low_energy_android/android/src/main/kotlin/dev/yanshouwang/bluetooth_low_energy_android/MyPeripheralManager.kt @@ -29,6 +29,9 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM private val descriptors = mutableMapOf() private val mtus = mutableMapOf() private val confirms = mutableMapOf() + private val preparedCharacteristics = mutableMapOf() + private val preparedValues = mutableMapOf() + private val values = mutableMapOf() private val centralsArgs = mutableMapOf() private val servicesArgs = mutableMapOf() @@ -81,6 +84,9 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM descriptors.clear() mtus.clear() confirms.clear() + preparedCharacteristics.clear() + preparedValues.clear() + values.clear() centralsArgs.clear() servicesArgs.clear() characteristicsArgs.clear() @@ -218,7 +224,8 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM override fun getMaximumWriteLength(centralHashCodeArgs: Long): Long { val mtu = mtus[centralHashCodeArgs] ?: 23 - return (mtu - 3).toLong() + val maximumWriteLength = (mtu - 3).coerceIn(20, 512) + return maximumWriteLength.toLong() } override fun sendReadCharacteristicReply(centralHashCodeArgs: Long, characteristicHashCodeArgs: Long, idArgs: Long, offsetArgs: Long, statusArgs: Boolean, valueArgs: ByteArray) { @@ -239,7 +246,7 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM val status = if (statusArgs) BluetoothGatt.GATT_SUCCESS else BluetoothGatt.GATT_FAILURE val offset = offsetArgs.toInt() - val value = null + val value = values.remove(idArgs) as ByteArray val sent = server.sendResponse(device, requestId, status, offset, value) if (!sent) { throw IllegalStateException("Send write characteristic reply failed.") @@ -335,6 +342,13 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM callback(Result.failure(error)) } + fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { + val hashCode = device.hashCode() + val centralArgs = centralsArgs.getOrPut(hashCode) { device.toCentralArgs() } + val centralHashCodeArgs = centralArgs.hashCodeArgs + devices[centralHashCodeArgs] = device + } + fun onMtuChanged(device: BluetoothDevice, mtu: Int) { val hashCode = device.hashCode() val centralArgs = centralsArgs.getOrPut(hashCode) { device.toCentralArgs() } @@ -360,11 +374,49 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM val centralArgs = centralsArgs.getOrPut(deviceHashCode) { device.toCentralArgs() } val centralHashCodeArgs = centralArgs.hashCodeArgs devices[centralHashCodeArgs] = device + if (preparedWrite) { + val preparedCharacteristic = preparedCharacteristics[deviceHashCode] + if (preparedCharacteristic != null && preparedCharacteristic != characteristic) { + val status = BluetoothGatt.GATT_CONNECTION_CONGESTED + server.sendResponse(device, requestId, status, offset, value) + return + } + val preparedValue = preparedValues[deviceHashCode] + if (preparedValue == null) { + preparedCharacteristics[deviceHashCode] = characteristic + preparedValues[deviceHashCode] = value + } else { + preparedValues[deviceHashCode] = preparedValue.plus(value) + } + val status = BluetoothGatt.GATT_SUCCESS + server.sendResponse(device, requestId, status, offset, value) + } else { + val characteristicHashCode = characteristic.hashCode() + val characteristicArgs = characteristicsArgs[characteristicHashCode] as MyGattCharacteristicArgs + val idArgs = requestId.toLong() + val offsetArgs = offset.toLong() + values[idArgs] = value + api.onWriteCharacteristicCommandReceived(centralArgs, characteristicArgs, idArgs, offsetArgs, value) {} + } + } + + fun onExecuteWrite(device: BluetoothDevice, requestId: Int, execute: Boolean) { + val deviceHashCode = device.hashCode() + val centralArgs = centralsArgs[deviceHashCode] as MyCentralArgs + val characteristic = preparedCharacteristics.remove(deviceHashCode) as BluetoothGattCharacteristic val characteristicHashCode = characteristic.hashCode() val characteristicArgs = characteristicsArgs[characteristicHashCode] as MyGattCharacteristicArgs - val idArgs = requestId.toLong() - val offsetArgs = offset.toLong() - api.onWriteCharacteristicCommandReceived(centralArgs, characteristicArgs, idArgs, offsetArgs, value) {} + val value = preparedValues.remove(deviceHashCode) as ByteArray + if (execute) { + val idArgs = requestId.toLong() + val offsetArgs = 0L + values[idArgs] = value + api.onWriteCharacteristicCommandReceived(centralArgs, characteristicArgs, idArgs, offsetArgs, value) {} + } else { + val status = BluetoothGatt.GATT_SUCCESS + val offset = 0 + server.sendResponse(device, requestId, status, offset, value) + } } fun onDescriptorReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, descriptor: BluetoothGattDescriptor) { diff --git a/bluetooth_low_energy_android/example/lib/main.dart b/bluetooth_low_energy_android/example/lib/main.dart index 6b26c799..e5399426 100644 --- a/bluetooth_low_energy_android/example/lib/main.dart +++ b/bluetooth_low_energy_android/example/lib/main.dart @@ -329,7 +329,7 @@ class _ScannerViewState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text('$rssi'), + trailing: RssiWidget(rssi), ); }, separatorBuilder: (context, i) { @@ -375,10 +375,13 @@ class _PeripheralViewState extends State { late final ValueNotifier characteristic; late final ValueNotifier writeType; late final ValueNotifier maximumWriteLength; + late final ValueNotifier rssi; late final ValueNotifier> logs; late final TextEditingController writeController; late final StreamSubscription stateChangedSubscription; late final StreamSubscription valueChangedSubscription; + late final StreamSubscription rssiChangedSubscription; + late final Timer rssiTimer; @override void initState() { @@ -390,7 +393,8 @@ class _PeripheralViewState extends State { service = ValueNotifier(null); characteristic = ValueNotifier(null); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); - maximumWriteLength = ValueNotifier(20); + maximumWriteLength = ValueNotifier(0); + rssi = ValueNotifier(-100); logs = ValueNotifier([]); writeController = TextEditingController(); stateChangedSubscription = centralManager.peripheralStateChanged.listen( @@ -423,6 +427,17 @@ class _PeripheralViewState extends State { ]; }, ); + rssiTimer = Timer.periodic( + const Duration(seconds: 1), + (timer) async { + final state = this.state.value; + if (state) { + rssi.value = await centralManager.readRSSI(eventArgs.peripheral); + } else { + rssi.value = -100; + } + }, + ); } @override @@ -455,10 +470,18 @@ class _PeripheralViewState extends State { final peripheral = eventArgs.peripheral; if (state) { await centralManager.disconnect(peripheral); + maximumWriteLength.value = 0; + rssi.value = 0; } else { await centralManager.connect(peripheral); services.value = await centralManager.discoverGATT(peripheral); + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + peripheral, + type: writeType.value, + ); + rssi.value = await centralManager.readRSSI(peripheral); } }, child: Text(state ? 'DISCONNECT' : 'CONNECT'), @@ -592,72 +615,82 @@ class _PeripheralViewState extends State { ), Row( children: [ - Expanded( - child: Center( - child: ValueListenableBuilder( - valueListenable: writeType, - builder: (context, writeType, child) { - final items = - GattCharacteristicWriteType.values.map((type) { - return DropdownMenuItem( - value: type, - child: Text( - type.name, - style: theme.textTheme.bodyMedium, - ), - ); - }).toList(); - return DropdownButton( - items: items, - onChanged: (type) { - if (type == null) { - return; - } - this.writeType.value = type; - }, - value: writeType, - underline: const Offstage(), + ValueListenableBuilder( + valueListenable: writeType, + builder: (context, writeType, child) { + return ToggleButtons( + onPressed: (i) async { + final type = GattCharacteristicWriteType.values[i]; + this.writeType.value = type; + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + eventArgs.peripheral, + type: type, ); }, - ), - ), + constraints: const BoxConstraints( + minWidth: 0.0, + minHeight: 0.0, + ), + borderRadius: BorderRadius.circular(4.0), + isSelected: GattCharacteristicWriteType.values + .map((type) => type == writeType) + .toList(), + children: GattCharacteristicWriteType.values.map((type) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Text(type.name), + ); + }).toList(), + ); + // final segments = + // GattCharacteristicWriteType.values.map((type) { + // return ButtonSegment( + // value: type, + // label: Text(type.name), + // ); + // }).toList(); + // return SegmentedButton( + // segments: segments, + // selected: {writeType}, + // showSelectedIcon: false, + // style: OutlinedButton.styleFrom( + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // padding: EdgeInsets.zero, + // visualDensity: VisualDensity.compact, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8.0), + // ), + // ), + // ); + }, ), - Expanded( - child: ValueListenableBuilder( - valueListenable: state, - builder: (context, state, child) { - return TextButton( - onPressed: state - ? () async { - maximumWriteLength.value = - await centralManager.getMaximumWriteLength( - eventArgs.peripheral, - type: writeType.value, - ); - } - : null, - child: ValueListenableBuilder( - valueListenable: maximumWriteLength, - builder: (context, maximumWriteLength, child) { - return Text('MTU: $maximumWriteLength'); - }, - ), - ); - }, - ), + const SizedBox(width: 8.0), + ValueListenableBuilder( + valueListenable: state, + builder: (context, state, child) { + return ValueListenableBuilder( + valueListenable: maximumWriteLength, + builder: (context, maximumWriteLength, child) { + return Text('$maximumWriteLength'); + }, + ); + }, ), - IconButton( - onPressed: () async { - final rssi = - await centralManager.readRSSI(eventArgs.peripheral); - log('RSSI: $rssi'); + const Spacer(), + ValueListenableBuilder( + valueListenable: rssi, + builder: (context, rssi, child) { + return RssiWidget(rssi); }, - icon: const Icon(Icons.signal_wifi_4_bar), ), ], ), Container( - margin: const EdgeInsets.symmetric(vertical: 16.0), + margin: const EdgeInsets.only(bottom: 16.0), height: 160.0, child: ValueListenableBuilder( valueListenable: characteristic, @@ -755,6 +788,7 @@ class _PeripheralViewState extends State { @override void dispose() { super.dispose(); + rssiTimer.cancel(); stateChangedSubscription.cancel(); valueChangedSubscription.cancel(); state.dispose(); @@ -764,6 +798,7 @@ class _PeripheralViewState extends State { characteristic.dispose(); writeType.dispose(); maximumWriteLength.dispose(); + rssi.dispose(); logs.dispose(); writeController.dispose(); } @@ -1070,3 +1105,25 @@ enum LogType { notify, error, } + +class RssiWidget extends StatelessWidget { + final int rssi; + + const RssiWidget( + this.rssi, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final IconData icon; + if (rssi > -70) { + icon = Icons.wifi_rounded; + } else if (rssi > -100) { + icon = Icons.wifi_2_bar_rounded; + } else { + icon = Icons.wifi_1_bar_rounded; + } + return Icon(icon); + } +} diff --git a/bluetooth_low_energy_android/example/pubspec.lock b/bluetooth_low_energy_android/example/pubspec.lock index 45283dc8..de7650bf 100644 --- a/bluetooth_low_energy_android/example/pubspec.lock +++ b/bluetooth_low_energy_android/example/pubspec.lock @@ -15,7 +15,7 @@ packages: path: ".." relative: true source: path - version: "3.0.0" + version: "3.0.3" bluetooth_low_energy_platform_interface: dependency: "direct main" description: diff --git a/bluetooth_low_energy_android/lib/src/my_api.g.dart b/bluetooth_low_energy_android/lib/src/my_api.g.dart index 3c7ee653..21217026 100644 --- a/bluetooth_low_energy_android/lib/src/my_api.g.dart +++ b/bluetooth_low_energy_android/lib/src/my_api.g.dart @@ -448,12 +448,12 @@ class MyCentralManagerHostApi { } } - Future getMaximumWriteLength(int arg_peripheralHashCodeArgs) async { + Future getMaximumWriteLength(int arg_peripheralHashCodeArgs, int arg_typeNumberArgs) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.getMaximumWriteLength', codec, binaryMessenger: _binaryMessenger); final List? replyList = - await channel.send([arg_peripheralHashCodeArgs]) as List?; + await channel.send([arg_peripheralHashCodeArgs, arg_typeNumberArgs]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -529,6 +529,33 @@ class MyCentralManagerHostApi { } } + Future requestMTU(int arg_peripheralHashCodeArgs, int arg_mtuArgs) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.requestMTU', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_peripheralHashCodeArgs, arg_mtuArgs]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } + Future readCharacteristic(int arg_peripheralHashCodeArgs, int arg_characteristicHashCodeArgs) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.readCharacteristic', codec, diff --git a/bluetooth_low_energy_android/lib/src/my_central_manager.dart b/bluetooth_low_energy_android/lib/src/my_central_manager.dart index 0f9f4a77..1eca6960 100644 --- a/bluetooth_low_energy_android/lib/src/my_central_manager.dart +++ b/bluetooth_low_energy_android/lib/src/my_central_manager.dart @@ -75,8 +75,12 @@ class MyCentralManager extends MyBluetoothLowEnergyManager }) async { await throwWithoutState(BluetoothLowEnergyState.poweredOn); final peripheralHashCodeArgs = peripheral.hashCode; - final maximumWriteLength = - await _api.getMaximumWriteLength(peripheralHashCodeArgs); + final typeArgs = type.toArgs(); + final typeNumberArgs = typeArgs.index; + final maximumWriteLength = await _api.getMaximumWriteLength( + peripheralHashCodeArgs, + typeNumberArgs, + ); return maximumWriteLength; } @@ -96,6 +100,12 @@ class MyCentralManager extends MyBluetoothLowEnergyManager } final peripheralHashCodeArgs = peripheral.hashCode; final servicesArgs = await _api.discoverGATT(peripheralHashCodeArgs); + // 部分外围设备连接后会触发 onMtuChanged 回调,若在此之前调用协商 MTU 的方法,会在协商完成前返回, + // 此时如果继续调用其他方法(如发现服务)会导致回调无法触发, + // 因此为避免此情况发生,需要延迟到发现服务完成后再协商 MTU。 + // TODO: 思考更好的解决方式,可以在连接后立即协商 MTU。 + const mtuArgs = 517; + await _api.requestMTU(peripheralHashCodeArgs, mtuArgs); final services = servicesArgs .cast() .map((args) => args.toService2()) diff --git a/bluetooth_low_energy_android/my_api.dart b/bluetooth_low_energy_android/my_api.dart index b5d2b057..686ca5b1 100644 --- a/bluetooth_low_energy_android/my_api.dart +++ b/bluetooth_low_energy_android/my_api.dart @@ -22,13 +22,14 @@ abstract class MyCentralManagerHostApi { void connect(int peripheralHashCodeArgs); @async void disconnect(int peripheralHashCodeArgs); - @async - int getMaximumWriteLength(int peripheralHashCodeArgs); + int getMaximumWriteLength(int peripheralHashCodeArgs, int typeNumberArgs); @async int readRSSI(int peripheralHashCodeArgs); @async List discoverGATT(int peripheralHashCodeArgs); @async + int requestMTU(int peripheralHashCodeArgs, int mtuArgs); + @async Uint8List readCharacteristic( int peripheralHashCodeArgs, int characteristicHashCodeArgs, diff --git a/bluetooth_low_energy_android/pubspec.yaml b/bluetooth_low_energy_android/pubspec.yaml index 64eaf200..ee7c539d 100644 --- a/bluetooth_low_energy_android/pubspec.yaml +++ b/bluetooth_low_energy_android/pubspec.yaml @@ -1,6 +1,6 @@ name: bluetooth_low_energy_android description: Android implementation of the bluetooth_low_energy plugin. -version: 3.0.1 +version: 3.0.3 homepage: https://github.com/yanshouwang/bluetooth_low_energy environment: diff --git a/bluetooth_low_energy_darwin/CHANGELOG.md b/bluetooth_low_energy_darwin/CHANGELOG.md index cad496d4..b9818cea 100644 --- a/bluetooth_low_energy_darwin/CHANGELOG.md +++ b/bluetooth_low_energy_darwin/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.2 + +* Fix the issue that `getMaximumWriteLength` is wrong and coerce the value from 20 to 512. +* Fix the issue that the peripheral manager response is wrong. + ## 3.0.1 * Fix the issue that write characteristic will never complete when write without response. diff --git a/bluetooth_low_energy_darwin/darwin/Classes/MyApi.swift b/bluetooth_low_energy_darwin/darwin/Classes/MyApi.swift index c1a6f014..7c80b6bd 100644 --- a/bluetooth_low_energy_darwin/darwin/Classes/MyApi.swift +++ b/bluetooth_low_energy_darwin/darwin/Classes/MyApi.swift @@ -274,6 +274,21 @@ extension MyGattDescriptorArgs { } } +extension Int { + func coerceIn(_ minimum: Int, _ maximum: Int) throws -> Int { + if minimum > maximum { + throw MyError.illegalArgument + } + if self < minimum { + return minimum + } + if self > maximum { + return maximum + } + return self + } +} + extension Dictionary { mutating func getOrPut(_ key: Key, _ defaultValue: () -> Value) -> Value { guard let value = self[key] else { diff --git a/bluetooth_low_energy_darwin/darwin/Classes/MyCentralManager.swift b/bluetooth_low_energy_darwin/darwin/Classes/MyCentralManager.swift index cb7703d6..84fbeb44 100644 --- a/bluetooth_low_energy_darwin/darwin/Classes/MyCentralManager.swift +++ b/bluetooth_low_energy_darwin/darwin/Classes/MyCentralManager.swift @@ -154,7 +154,7 @@ class MyCentralManager: MyCentralManagerHostApi { throw MyError.illegalArgument } let type = typeArgs.toWriteType() - let maximumWriteLength = peripheral.maximumWriteValueLength(for: type) + let maximumWriteLength = try peripheral.maximumWriteValueLength(for: type).coerceIn(20, 512) let maximumWriteLengthArgs = Int64(maximumWriteLength) return maximumWriteLengthArgs } @@ -310,7 +310,6 @@ class MyCentralManager: MyCentralManagerHostApi { api.onStateChanged(stateNumberArgs: stateNumberArgs) {} } - func didDiscover(_ peripheral: CBPeripheral, _ advertisementData: [String : Any], _ rssi: NSNumber) { let peripheralArgs = peripheral.toArgs() let peripheralHashCode = peripheral.hash @@ -329,10 +328,12 @@ class MyCentralManager: MyCentralManagerHostApi { return } let peripheralHashCodeArgs = peripheralArgs.hashCodeArgs - let completion = connectCompletions.removeValue(forKey: peripheralHashCodeArgs) - completion?(.success(())) let stateArgs = true api.onPeripheralStateChanged(peripheralArgs: peripheralArgs, stateArgs: stateArgs) {} + guard let completion = connectCompletions.removeValue(forKey: peripheralHashCodeArgs) else { + return + } + completion(.success(())) } func didFailToConnect(_ peripheral: CBPeripheral, _ error: Error?) { diff --git a/bluetooth_low_energy_darwin/darwin/Classes/MyError.swift b/bluetooth_low_energy_darwin/darwin/Classes/MyError.swift index 12068c0f..d07649e4 100644 --- a/bluetooth_low_energy_darwin/darwin/Classes/MyError.swift +++ b/bluetooth_low_energy_darwin/darwin/Classes/MyError.swift @@ -7,6 +7,7 @@ import Foundation +// TODO: 优化错误内容 enum MyError: Error { case illegalArgument case illegalState diff --git a/bluetooth_low_energy_darwin/darwin/Classes/MyPeripheralManager.swift b/bluetooth_low_energy_darwin/darwin/Classes/MyPeripheralManager.swift index e382922d..ad4c9cff 100644 --- a/bluetooth_low_energy_darwin/darwin/Classes/MyPeripheralManager.swift +++ b/bluetooth_low_energy_darwin/darwin/Classes/MyPeripheralManager.swift @@ -202,13 +202,13 @@ class MyPeripheralManager: MyPeripheralManagerHostApi { guard let central = centrals[centralHashCodeArgs] else { throw MyError.illegalArgument } - let maximumWriteLength = central.maximumUpdateValueLength + let maximumWriteLength = try central.maximumUpdateValueLength.coerceIn(20, 512) let maximumWriteLengthArgs = Int64(maximumWriteLength) return maximumWriteLengthArgs } func sendReadCharacteristicReply(centralHashCodeArgs: Int64, characteristicHashCodeArgs: Int64, idArgs: Int64, offsetArgs: Int64, statusArgs: Bool, valueArgs: FlutterStandardTypedData) throws { - guard let request = requests[idArgs] else { + guard let request = requests.removeValue(forKey: idArgs) else { throw MyError.illegalArgument } request.value = valueArgs.data @@ -217,7 +217,7 @@ class MyPeripheralManager: MyPeripheralManagerHostApi { } func sendWriteCharacteristicReply(centralHashCodeArgs: Int64, characteristicHashCodeArgs: Int64, idArgs: Int64, offsetArgs: Int64, statusArgs: Bool) throws { - guard let request = requests[idArgs] else { + guard let request = requests.removeValue(forKey: idArgs) else { throw MyError.illegalArgument } let result = statusArgs ? CBATTError.Code.success : CBATTError.Code.requestNotSupported @@ -315,28 +315,35 @@ class MyPeripheralManager: MyPeripheralManagerHostApi { } func didReceiveWrite(_ requests: [CBATTRequest]) { - for request in requests { - let central = request.central - let centralHashCode = central.hash - let centralArgs = centralsArgs.getOrPut(centralHashCode) { central.toArgs() } - let centralHashCodeArgs = centralArgs.hashCodeArgs - centrals[centralHashCodeArgs] = central - let characteristic = request.characteristic - let characteristicHashCode = characteristic.hash - guard let characteristicArgs = characteristicsArgs[characteristicHashCode] else { - peripheralManager.respond(to: request, withResult: .attributeNotFound) - return - } - let idArgs = Int64(request.hash) - self.requests[idArgs] = request - let offsetArgs = Int64(request.offset) - guard let value = request.value else { - peripheralManager.respond(to: request, withResult: .requestNotSupported) - return - } - let valueArgs = FlutterStandardTypedData(bytes: value) - api.onWriteCharacteristicCommandReceived(centralArgs: centralArgs, characteristicArgs: characteristicArgs, idArgs: idArgs, offsetArgs: offsetArgs, valueArgs: valueArgs) {} + // 根据官方文档,仅响应第一个写入请求 + guard let request = requests.first else { + return + } + if requests.count > 1 { + // TODO: 支持多写入请求,暂时不清楚此处应如何处理 + let result = CBATTError.requestNotSupported + peripheralManager.respond(to: request, withResult: result) + } + let central = request.central + let centralHashCode = central.hash + let centralArgs = centralsArgs.getOrPut(centralHashCode) { central.toArgs() } + let centralHashCodeArgs = centralArgs.hashCodeArgs + centrals[centralHashCodeArgs] = central + let characteristic = request.characteristic + let characteristicHashCode = characteristic.hash + guard let characteristicArgs = characteristicsArgs[characteristicHashCode] else { + peripheralManager.respond(to: request, withResult: .attributeNotFound) + return + } + let idArgs = Int64(request.hash) + self.requests[idArgs] = request + let offsetArgs = Int64(request.offset) + guard let value = request.value else { + peripheralManager.respond(to: request, withResult: .requestNotSupported) + return } + let valueArgs = FlutterStandardTypedData(bytes: value) + api.onWriteCharacteristicCommandReceived(centralArgs: centralArgs, characteristicArgs: characteristicArgs, idArgs: idArgs, offsetArgs: offsetArgs, valueArgs: valueArgs) {} } func didSubscribeTo(_ central: CBCentral, _ characteristic: CBCharacteristic) { diff --git a/bluetooth_low_energy_darwin/example/lib/main.dart b/bluetooth_low_energy_darwin/example/lib/main.dart index 6b26c799..e5399426 100644 --- a/bluetooth_low_energy_darwin/example/lib/main.dart +++ b/bluetooth_low_energy_darwin/example/lib/main.dart @@ -329,7 +329,7 @@ class _ScannerViewState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text('$rssi'), + trailing: RssiWidget(rssi), ); }, separatorBuilder: (context, i) { @@ -375,10 +375,13 @@ class _PeripheralViewState extends State { late final ValueNotifier characteristic; late final ValueNotifier writeType; late final ValueNotifier maximumWriteLength; + late final ValueNotifier rssi; late final ValueNotifier> logs; late final TextEditingController writeController; late final StreamSubscription stateChangedSubscription; late final StreamSubscription valueChangedSubscription; + late final StreamSubscription rssiChangedSubscription; + late final Timer rssiTimer; @override void initState() { @@ -390,7 +393,8 @@ class _PeripheralViewState extends State { service = ValueNotifier(null); characteristic = ValueNotifier(null); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); - maximumWriteLength = ValueNotifier(20); + maximumWriteLength = ValueNotifier(0); + rssi = ValueNotifier(-100); logs = ValueNotifier([]); writeController = TextEditingController(); stateChangedSubscription = centralManager.peripheralStateChanged.listen( @@ -423,6 +427,17 @@ class _PeripheralViewState extends State { ]; }, ); + rssiTimer = Timer.periodic( + const Duration(seconds: 1), + (timer) async { + final state = this.state.value; + if (state) { + rssi.value = await centralManager.readRSSI(eventArgs.peripheral); + } else { + rssi.value = -100; + } + }, + ); } @override @@ -455,10 +470,18 @@ class _PeripheralViewState extends State { final peripheral = eventArgs.peripheral; if (state) { await centralManager.disconnect(peripheral); + maximumWriteLength.value = 0; + rssi.value = 0; } else { await centralManager.connect(peripheral); services.value = await centralManager.discoverGATT(peripheral); + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + peripheral, + type: writeType.value, + ); + rssi.value = await centralManager.readRSSI(peripheral); } }, child: Text(state ? 'DISCONNECT' : 'CONNECT'), @@ -592,72 +615,82 @@ class _PeripheralViewState extends State { ), Row( children: [ - Expanded( - child: Center( - child: ValueListenableBuilder( - valueListenable: writeType, - builder: (context, writeType, child) { - final items = - GattCharacteristicWriteType.values.map((type) { - return DropdownMenuItem( - value: type, - child: Text( - type.name, - style: theme.textTheme.bodyMedium, - ), - ); - }).toList(); - return DropdownButton( - items: items, - onChanged: (type) { - if (type == null) { - return; - } - this.writeType.value = type; - }, - value: writeType, - underline: const Offstage(), + ValueListenableBuilder( + valueListenable: writeType, + builder: (context, writeType, child) { + return ToggleButtons( + onPressed: (i) async { + final type = GattCharacteristicWriteType.values[i]; + this.writeType.value = type; + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + eventArgs.peripheral, + type: type, ); }, - ), - ), + constraints: const BoxConstraints( + minWidth: 0.0, + minHeight: 0.0, + ), + borderRadius: BorderRadius.circular(4.0), + isSelected: GattCharacteristicWriteType.values + .map((type) => type == writeType) + .toList(), + children: GattCharacteristicWriteType.values.map((type) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Text(type.name), + ); + }).toList(), + ); + // final segments = + // GattCharacteristicWriteType.values.map((type) { + // return ButtonSegment( + // value: type, + // label: Text(type.name), + // ); + // }).toList(); + // return SegmentedButton( + // segments: segments, + // selected: {writeType}, + // showSelectedIcon: false, + // style: OutlinedButton.styleFrom( + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // padding: EdgeInsets.zero, + // visualDensity: VisualDensity.compact, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8.0), + // ), + // ), + // ); + }, ), - Expanded( - child: ValueListenableBuilder( - valueListenable: state, - builder: (context, state, child) { - return TextButton( - onPressed: state - ? () async { - maximumWriteLength.value = - await centralManager.getMaximumWriteLength( - eventArgs.peripheral, - type: writeType.value, - ); - } - : null, - child: ValueListenableBuilder( - valueListenable: maximumWriteLength, - builder: (context, maximumWriteLength, child) { - return Text('MTU: $maximumWriteLength'); - }, - ), - ); - }, - ), + const SizedBox(width: 8.0), + ValueListenableBuilder( + valueListenable: state, + builder: (context, state, child) { + return ValueListenableBuilder( + valueListenable: maximumWriteLength, + builder: (context, maximumWriteLength, child) { + return Text('$maximumWriteLength'); + }, + ); + }, ), - IconButton( - onPressed: () async { - final rssi = - await centralManager.readRSSI(eventArgs.peripheral); - log('RSSI: $rssi'); + const Spacer(), + ValueListenableBuilder( + valueListenable: rssi, + builder: (context, rssi, child) { + return RssiWidget(rssi); }, - icon: const Icon(Icons.signal_wifi_4_bar), ), ], ), Container( - margin: const EdgeInsets.symmetric(vertical: 16.0), + margin: const EdgeInsets.only(bottom: 16.0), height: 160.0, child: ValueListenableBuilder( valueListenable: characteristic, @@ -755,6 +788,7 @@ class _PeripheralViewState extends State { @override void dispose() { super.dispose(); + rssiTimer.cancel(); stateChangedSubscription.cancel(); valueChangedSubscription.cancel(); state.dispose(); @@ -764,6 +798,7 @@ class _PeripheralViewState extends State { characteristic.dispose(); writeType.dispose(); maximumWriteLength.dispose(); + rssi.dispose(); logs.dispose(); writeController.dispose(); } @@ -1070,3 +1105,25 @@ enum LogType { notify, error, } + +class RssiWidget extends StatelessWidget { + final int rssi; + + const RssiWidget( + this.rssi, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final IconData icon; + if (rssi > -70) { + icon = Icons.wifi_rounded; + } else if (rssi > -100) { + icon = Icons.wifi_2_bar_rounded; + } else { + icon = Icons.wifi_1_bar_rounded; + } + return Icon(icon); + } +} diff --git a/bluetooth_low_energy_darwin/example/pubspec.lock b/bluetooth_low_energy_darwin/example/pubspec.lock index 7c9e6e8c..a7f2cf61 100644 --- a/bluetooth_low_energy_darwin/example/pubspec.lock +++ b/bluetooth_low_energy_darwin/example/pubspec.lock @@ -15,7 +15,7 @@ packages: path: ".." relative: true source: path - version: "3.0.0" + version: "3.0.2" bluetooth_low_energy_platform_interface: dependency: "direct main" description: diff --git a/bluetooth_low_energy_darwin/pubspec.yaml b/bluetooth_low_energy_darwin/pubspec.yaml index 447d3b20..3e1101f5 100644 --- a/bluetooth_low_energy_darwin/pubspec.yaml +++ b/bluetooth_low_energy_darwin/pubspec.yaml @@ -1,6 +1,6 @@ name: bluetooth_low_energy_darwin description: iOS and macOS implementation of the bluetooth_low_energy plugin. -version: 3.0.1 +version: 3.0.2 homepage: https://github.com/yanshouwang/bluetooth_low_energy environment: diff --git a/bluetooth_low_energy_linux/example/lib/main.dart b/bluetooth_low_energy_linux/example/lib/main.dart index 6b26c799..e5399426 100644 --- a/bluetooth_low_energy_linux/example/lib/main.dart +++ b/bluetooth_low_energy_linux/example/lib/main.dart @@ -329,7 +329,7 @@ class _ScannerViewState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text('$rssi'), + trailing: RssiWidget(rssi), ); }, separatorBuilder: (context, i) { @@ -375,10 +375,13 @@ class _PeripheralViewState extends State { late final ValueNotifier characteristic; late final ValueNotifier writeType; late final ValueNotifier maximumWriteLength; + late final ValueNotifier rssi; late final ValueNotifier> logs; late final TextEditingController writeController; late final StreamSubscription stateChangedSubscription; late final StreamSubscription valueChangedSubscription; + late final StreamSubscription rssiChangedSubscription; + late final Timer rssiTimer; @override void initState() { @@ -390,7 +393,8 @@ class _PeripheralViewState extends State { service = ValueNotifier(null); characteristic = ValueNotifier(null); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); - maximumWriteLength = ValueNotifier(20); + maximumWriteLength = ValueNotifier(0); + rssi = ValueNotifier(-100); logs = ValueNotifier([]); writeController = TextEditingController(); stateChangedSubscription = centralManager.peripheralStateChanged.listen( @@ -423,6 +427,17 @@ class _PeripheralViewState extends State { ]; }, ); + rssiTimer = Timer.periodic( + const Duration(seconds: 1), + (timer) async { + final state = this.state.value; + if (state) { + rssi.value = await centralManager.readRSSI(eventArgs.peripheral); + } else { + rssi.value = -100; + } + }, + ); } @override @@ -455,10 +470,18 @@ class _PeripheralViewState extends State { final peripheral = eventArgs.peripheral; if (state) { await centralManager.disconnect(peripheral); + maximumWriteLength.value = 0; + rssi.value = 0; } else { await centralManager.connect(peripheral); services.value = await centralManager.discoverGATT(peripheral); + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + peripheral, + type: writeType.value, + ); + rssi.value = await centralManager.readRSSI(peripheral); } }, child: Text(state ? 'DISCONNECT' : 'CONNECT'), @@ -592,72 +615,82 @@ class _PeripheralViewState extends State { ), Row( children: [ - Expanded( - child: Center( - child: ValueListenableBuilder( - valueListenable: writeType, - builder: (context, writeType, child) { - final items = - GattCharacteristicWriteType.values.map((type) { - return DropdownMenuItem( - value: type, - child: Text( - type.name, - style: theme.textTheme.bodyMedium, - ), - ); - }).toList(); - return DropdownButton( - items: items, - onChanged: (type) { - if (type == null) { - return; - } - this.writeType.value = type; - }, - value: writeType, - underline: const Offstage(), + ValueListenableBuilder( + valueListenable: writeType, + builder: (context, writeType, child) { + return ToggleButtons( + onPressed: (i) async { + final type = GattCharacteristicWriteType.values[i]; + this.writeType.value = type; + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + eventArgs.peripheral, + type: type, ); }, - ), - ), + constraints: const BoxConstraints( + minWidth: 0.0, + minHeight: 0.0, + ), + borderRadius: BorderRadius.circular(4.0), + isSelected: GattCharacteristicWriteType.values + .map((type) => type == writeType) + .toList(), + children: GattCharacteristicWriteType.values.map((type) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Text(type.name), + ); + }).toList(), + ); + // final segments = + // GattCharacteristicWriteType.values.map((type) { + // return ButtonSegment( + // value: type, + // label: Text(type.name), + // ); + // }).toList(); + // return SegmentedButton( + // segments: segments, + // selected: {writeType}, + // showSelectedIcon: false, + // style: OutlinedButton.styleFrom( + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // padding: EdgeInsets.zero, + // visualDensity: VisualDensity.compact, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8.0), + // ), + // ), + // ); + }, ), - Expanded( - child: ValueListenableBuilder( - valueListenable: state, - builder: (context, state, child) { - return TextButton( - onPressed: state - ? () async { - maximumWriteLength.value = - await centralManager.getMaximumWriteLength( - eventArgs.peripheral, - type: writeType.value, - ); - } - : null, - child: ValueListenableBuilder( - valueListenable: maximumWriteLength, - builder: (context, maximumWriteLength, child) { - return Text('MTU: $maximumWriteLength'); - }, - ), - ); - }, - ), + const SizedBox(width: 8.0), + ValueListenableBuilder( + valueListenable: state, + builder: (context, state, child) { + return ValueListenableBuilder( + valueListenable: maximumWriteLength, + builder: (context, maximumWriteLength, child) { + return Text('$maximumWriteLength'); + }, + ); + }, ), - IconButton( - onPressed: () async { - final rssi = - await centralManager.readRSSI(eventArgs.peripheral); - log('RSSI: $rssi'); + const Spacer(), + ValueListenableBuilder( + valueListenable: rssi, + builder: (context, rssi, child) { + return RssiWidget(rssi); }, - icon: const Icon(Icons.signal_wifi_4_bar), ), ], ), Container( - margin: const EdgeInsets.symmetric(vertical: 16.0), + margin: const EdgeInsets.only(bottom: 16.0), height: 160.0, child: ValueListenableBuilder( valueListenable: characteristic, @@ -755,6 +788,7 @@ class _PeripheralViewState extends State { @override void dispose() { super.dispose(); + rssiTimer.cancel(); stateChangedSubscription.cancel(); valueChangedSubscription.cancel(); state.dispose(); @@ -764,6 +798,7 @@ class _PeripheralViewState extends State { characteristic.dispose(); writeType.dispose(); maximumWriteLength.dispose(); + rssi.dispose(); logs.dispose(); writeController.dispose(); } @@ -1070,3 +1105,25 @@ enum LogType { notify, error, } + +class RssiWidget extends StatelessWidget { + final int rssi; + + const RssiWidget( + this.rssi, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final IconData icon; + if (rssi > -70) { + icon = Icons.wifi_rounded; + } else if (rssi > -100) { + icon = Icons.wifi_2_bar_rounded; + } else { + icon = Icons.wifi_1_bar_rounded; + } + return Icon(icon); + } +} diff --git a/bluetooth_low_energy_windows/example/lib/main.dart b/bluetooth_low_energy_windows/example/lib/main.dart index 6b26c799..e5399426 100644 --- a/bluetooth_low_energy_windows/example/lib/main.dart +++ b/bluetooth_low_energy_windows/example/lib/main.dart @@ -329,7 +329,7 @@ class _ScannerViewState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text('$rssi'), + trailing: RssiWidget(rssi), ); }, separatorBuilder: (context, i) { @@ -375,10 +375,13 @@ class _PeripheralViewState extends State { late final ValueNotifier characteristic; late final ValueNotifier writeType; late final ValueNotifier maximumWriteLength; + late final ValueNotifier rssi; late final ValueNotifier> logs; late final TextEditingController writeController; late final StreamSubscription stateChangedSubscription; late final StreamSubscription valueChangedSubscription; + late final StreamSubscription rssiChangedSubscription; + late final Timer rssiTimer; @override void initState() { @@ -390,7 +393,8 @@ class _PeripheralViewState extends State { service = ValueNotifier(null); characteristic = ValueNotifier(null); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); - maximumWriteLength = ValueNotifier(20); + maximumWriteLength = ValueNotifier(0); + rssi = ValueNotifier(-100); logs = ValueNotifier([]); writeController = TextEditingController(); stateChangedSubscription = centralManager.peripheralStateChanged.listen( @@ -423,6 +427,17 @@ class _PeripheralViewState extends State { ]; }, ); + rssiTimer = Timer.periodic( + const Duration(seconds: 1), + (timer) async { + final state = this.state.value; + if (state) { + rssi.value = await centralManager.readRSSI(eventArgs.peripheral); + } else { + rssi.value = -100; + } + }, + ); } @override @@ -455,10 +470,18 @@ class _PeripheralViewState extends State { final peripheral = eventArgs.peripheral; if (state) { await centralManager.disconnect(peripheral); + maximumWriteLength.value = 0; + rssi.value = 0; } else { await centralManager.connect(peripheral); services.value = await centralManager.discoverGATT(peripheral); + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + peripheral, + type: writeType.value, + ); + rssi.value = await centralManager.readRSSI(peripheral); } }, child: Text(state ? 'DISCONNECT' : 'CONNECT'), @@ -592,72 +615,82 @@ class _PeripheralViewState extends State { ), Row( children: [ - Expanded( - child: Center( - child: ValueListenableBuilder( - valueListenable: writeType, - builder: (context, writeType, child) { - final items = - GattCharacteristicWriteType.values.map((type) { - return DropdownMenuItem( - value: type, - child: Text( - type.name, - style: theme.textTheme.bodyMedium, - ), - ); - }).toList(); - return DropdownButton( - items: items, - onChanged: (type) { - if (type == null) { - return; - } - this.writeType.value = type; - }, - value: writeType, - underline: const Offstage(), + ValueListenableBuilder( + valueListenable: writeType, + builder: (context, writeType, child) { + return ToggleButtons( + onPressed: (i) async { + final type = GattCharacteristicWriteType.values[i]; + this.writeType.value = type; + maximumWriteLength.value = + await centralManager.getMaximumWriteLength( + eventArgs.peripheral, + type: type, ); }, - ), - ), + constraints: const BoxConstraints( + minWidth: 0.0, + minHeight: 0.0, + ), + borderRadius: BorderRadius.circular(4.0), + isSelected: GattCharacteristicWriteType.values + .map((type) => type == writeType) + .toList(), + children: GattCharacteristicWriteType.values.map((type) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Text(type.name), + ); + }).toList(), + ); + // final segments = + // GattCharacteristicWriteType.values.map((type) { + // return ButtonSegment( + // value: type, + // label: Text(type.name), + // ); + // }).toList(); + // return SegmentedButton( + // segments: segments, + // selected: {writeType}, + // showSelectedIcon: false, + // style: OutlinedButton.styleFrom( + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // padding: EdgeInsets.zero, + // visualDensity: VisualDensity.compact, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8.0), + // ), + // ), + // ); + }, ), - Expanded( - child: ValueListenableBuilder( - valueListenable: state, - builder: (context, state, child) { - return TextButton( - onPressed: state - ? () async { - maximumWriteLength.value = - await centralManager.getMaximumWriteLength( - eventArgs.peripheral, - type: writeType.value, - ); - } - : null, - child: ValueListenableBuilder( - valueListenable: maximumWriteLength, - builder: (context, maximumWriteLength, child) { - return Text('MTU: $maximumWriteLength'); - }, - ), - ); - }, - ), + const SizedBox(width: 8.0), + ValueListenableBuilder( + valueListenable: state, + builder: (context, state, child) { + return ValueListenableBuilder( + valueListenable: maximumWriteLength, + builder: (context, maximumWriteLength, child) { + return Text('$maximumWriteLength'); + }, + ); + }, ), - IconButton( - onPressed: () async { - final rssi = - await centralManager.readRSSI(eventArgs.peripheral); - log('RSSI: $rssi'); + const Spacer(), + ValueListenableBuilder( + valueListenable: rssi, + builder: (context, rssi, child) { + return RssiWidget(rssi); }, - icon: const Icon(Icons.signal_wifi_4_bar), ), ], ), Container( - margin: const EdgeInsets.symmetric(vertical: 16.0), + margin: const EdgeInsets.only(bottom: 16.0), height: 160.0, child: ValueListenableBuilder( valueListenable: characteristic, @@ -755,6 +788,7 @@ class _PeripheralViewState extends State { @override void dispose() { super.dispose(); + rssiTimer.cancel(); stateChangedSubscription.cancel(); valueChangedSubscription.cancel(); state.dispose(); @@ -764,6 +798,7 @@ class _PeripheralViewState extends State { characteristic.dispose(); writeType.dispose(); maximumWriteLength.dispose(); + rssi.dispose(); logs.dispose(); writeController.dispose(); } @@ -1070,3 +1105,25 @@ enum LogType { notify, error, } + +class RssiWidget extends StatelessWidget { + final int rssi; + + const RssiWidget( + this.rssi, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final IconData icon; + if (rssi > -70) { + icon = Icons.wifi_rounded; + } else if (rssi > -100) { + icon = Icons.wifi_2_bar_rounded; + } else { + icon = Icons.wifi_1_bar_rounded; + } + return Icon(icon); + } +}