* fix: 优化界面

* fix: 修复已知问题

* fix: 代码优化

* fix: 修复已知问题

* fix: 优化 getMaximumWriteLength 方法

* fix: 修改版本号

* fix: 修改版本号

* fix: 修改 CHANGELOG
This commit is contained in:
iAMD
2023-10-18 17:26:24 +08:00
committed by GitHub
parent 4bec1c9f3a
commit 728402ac16
26 changed files with 872 additions and 413 deletions

View File

@ -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 ## 3.0.1
* [Android] Clear cache when disconnected. * `Android` Clear cache when disconnected.
* [Android] Fix GATT server error aftter bluetooth reopened. * `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 when write without response.
* [iOS] Fix the issue that write characteristic will never complete after disconnected. * `iOS` Fix the issue that write characteristic will never complete after disconnected.
## 3.0.0 ## 3.0.0

View File

@ -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. 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 ### 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. 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 ### Linux

View File

@ -329,7 +329,7 @@ class _ScannerViewState extends State<ScannerView> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: Text('$rssi'), trailing: RssiWidget(rssi),
); );
}, },
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
@ -375,10 +375,13 @@ class _PeripheralViewState extends State<PeripheralView> {
late final ValueNotifier<GattCharacteristic?> characteristic; late final ValueNotifier<GattCharacteristic?> characteristic;
late final ValueNotifier<GattCharacteristicWriteType> writeType; late final ValueNotifier<GattCharacteristicWriteType> writeType;
late final ValueNotifier<int> maximumWriteLength; late final ValueNotifier<int> maximumWriteLength;
late final ValueNotifier<int> rssi;
late final ValueNotifier<List<Log>> logs; late final ValueNotifier<List<Log>> logs;
late final TextEditingController writeController; late final TextEditingController writeController;
late final StreamSubscription stateChangedSubscription; late final StreamSubscription stateChangedSubscription;
late final StreamSubscription valueChangedSubscription; late final StreamSubscription valueChangedSubscription;
late final StreamSubscription rssiChangedSubscription;
late final Timer rssiTimer;
@override @override
void initState() { void initState() {
@ -390,7 +393,8 @@ class _PeripheralViewState extends State<PeripheralView> {
service = ValueNotifier(null); service = ValueNotifier(null);
characteristic = ValueNotifier(null); characteristic = ValueNotifier(null);
writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse);
maximumWriteLength = ValueNotifier(20); maximumWriteLength = ValueNotifier(0);
rssi = ValueNotifier(-100);
logs = ValueNotifier([]); logs = ValueNotifier([]);
writeController = TextEditingController(); writeController = TextEditingController();
stateChangedSubscription = centralManager.peripheralStateChanged.listen( stateChangedSubscription = centralManager.peripheralStateChanged.listen(
@ -423,6 +427,17 @@ class _PeripheralViewState extends State<PeripheralView> {
]; ];
}, },
); );
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 @override
@ -455,10 +470,18 @@ class _PeripheralViewState extends State<PeripheralView> {
final peripheral = eventArgs.peripheral; final peripheral = eventArgs.peripheral;
if (state) { if (state) {
await centralManager.disconnect(peripheral); await centralManager.disconnect(peripheral);
maximumWriteLength.value = 0;
rssi.value = 0;
} else { } else {
await centralManager.connect(peripheral); await centralManager.connect(peripheral);
services.value = services.value =
await centralManager.discoverGATT(peripheral); await centralManager.discoverGATT(peripheral);
maximumWriteLength.value =
await centralManager.getMaximumWriteLength(
peripheral,
type: writeType.value,
);
rssi.value = await centralManager.readRSSI(peripheral);
} }
}, },
child: Text(state ? 'DISCONNECT' : 'CONNECT'), child: Text(state ? 'DISCONNECT' : 'CONNECT'),
@ -592,72 +615,82 @@ class _PeripheralViewState extends State<PeripheralView> {
), ),
Row( Row(
children: [ children: [
Expanded( ValueListenableBuilder(
child: Center(
child: ValueListenableBuilder(
valueListenable: writeType, valueListenable: writeType,
builder: (context, writeType, child) { builder: (context, writeType, child) {
final items = return ToggleButtons(
GattCharacteristicWriteType.values.map((type) { onPressed: (i) async {
return DropdownMenuItem( final type = GattCharacteristicWriteType.values[i];
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; this.writeType.value = type;
},
value: writeType,
underline: const Offstage(),
);
},
),
),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return TextButton(
onPressed: state
? () async {
maximumWriteLength.value = maximumWriteLength.value =
await centralManager.getMaximumWriteLength( await centralManager.getMaximumWriteLength(
eventArgs.peripheral, eventArgs.peripheral,
type: writeType.value, type: type,
); );
} },
: null, constraints: const BoxConstraints(
child: ValueListenableBuilder( 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),
// ),
// ),
// );
},
),
const SizedBox(width: 8.0),
ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return ValueListenableBuilder(
valueListenable: maximumWriteLength, valueListenable: maximumWriteLength,
builder: (context, maximumWriteLength, child) { builder: (context, maximumWriteLength, child) {
return Text('MTU: $maximumWriteLength'); return Text('$maximumWriteLength');
}, },
),
); );
}, },
), ),
), const Spacer(),
IconButton( ValueListenableBuilder(
onPressed: () async { valueListenable: rssi,
final rssi = builder: (context, rssi, child) {
await centralManager.readRSSI(eventArgs.peripheral); return RssiWidget(rssi);
log('RSSI: $rssi');
}, },
icon: const Icon(Icons.signal_wifi_4_bar),
), ),
], ],
), ),
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
height: 160.0, height: 160.0,
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: characteristic, valueListenable: characteristic,
@ -755,6 +788,7 @@ class _PeripheralViewState extends State<PeripheralView> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
rssiTimer.cancel();
stateChangedSubscription.cancel(); stateChangedSubscription.cancel();
valueChangedSubscription.cancel(); valueChangedSubscription.cancel();
state.dispose(); state.dispose();
@ -764,6 +798,7 @@ class _PeripheralViewState extends State<PeripheralView> {
characteristic.dispose(); characteristic.dispose();
writeType.dispose(); writeType.dispose();
maximumWriteLength.dispose(); maximumWriteLength.dispose();
rssi.dispose();
logs.dispose(); logs.dispose();
writeController.dispose(); writeController.dispose();
} }
@ -1070,3 +1105,25 @@ enum LogType {
notify, notify,
error, 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);
}
}

View File

@ -23,23 +23,23 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "3.0.1" version: "3.0.2"
bluetooth_low_energy_android: bluetooth_low_energy_android:
dependency: transitive dependency: transitive
description: description:
name: bluetooth_low_energy_android name: bluetooth_low_energy_android
sha256: "54fbbc1ce1a25ae20692a71585e551f1d5e2d565f41c6f9dce13457f00e9bae8" sha256: "0fa5c4625ac01b6d4bbf78b10d0389d8e9907ae7c3fbc9601b481ddd4eaa86b6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.3"
bluetooth_low_energy_darwin: bluetooth_low_energy_darwin:
dependency: transitive dependency: transitive
description: description:
name: bluetooth_low_energy_darwin name: bluetooth_low_energy_darwin
sha256: "114e492a020ac1efaa3b169eba8b6c01f50f97f2bbcda2f1fff890a5abf353ae" sha256: "797d3803de3b124ffb13267910f8d727ae4884fdcc621ccc0995076107468bb6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
bluetooth_low_energy_linux: bluetooth_low_energy_linux:
dependency: transitive dependency: transitive
description: description:

View File

@ -1,6 +1,6 @@
name: bluetooth_low_energy name: bluetooth_low_energy
description: A Flutter plugin for controlling the 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 homepage: https://github.com/yanshouwang/bluetooth_low_energy
environment: environment:
@ -11,8 +11,8 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
bluetooth_low_energy_platform_interface: ^3.0.0 bluetooth_low_energy_platform_interface: ^3.0.0
bluetooth_low_energy_android: ^3.0.1 bluetooth_low_energy_android: ^3.0.3
bluetooth_low_energy_darwin: ^3.0.1 bluetooth_low_energy_darwin: ^3.0.2
bluetooth_low_energy_linux: ^3.0.0 bluetooth_low_energy_linux: ^3.0.0
dev_dependencies: dev_dependencies:

View File

@ -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 ## 3.0.1
* Clear cache when disconnected. * Clear cache when disconnected.

View File

@ -351,9 +351,10 @@ interface MyCentralManagerHostApi {
fun stopDiscovery() fun stopDiscovery()
fun connect(peripheralHashCodeArgs: Long, callback: (Result<Unit>) -> Unit) fun connect(peripheralHashCodeArgs: Long, callback: (Result<Unit>) -> Unit)
fun disconnect(peripheralHashCodeArgs: Long, callback: (Result<Unit>) -> Unit) fun disconnect(peripheralHashCodeArgs: Long, callback: (Result<Unit>) -> Unit)
fun getMaximumWriteLength(peripheralHashCodeArgs: Long, callback: (Result<Long>) -> Unit) fun getMaximumWriteLength(peripheralHashCodeArgs: Long, typeNumberArgs: Long): Long
fun readRSSI(peripheralHashCodeArgs: Long, callback: (Result<Long>) -> Unit) fun readRSSI(peripheralHashCodeArgs: Long, callback: (Result<Long>) -> Unit)
fun discoverGATT(peripheralHashCodeArgs: Long, callback: (Result<List<MyGattServiceArgs>>) -> Unit) fun discoverGATT(peripheralHashCodeArgs: Long, callback: (Result<List<MyGattServiceArgs>>) -> Unit)
fun requestMTU(peripheralHashCodeArgs: Long, mtuArgs: Long, callback: (Result<Long>) -> Unit)
fun readCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, callback: (Result<ByteArray>) -> Unit) fun readCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, callback: (Result<ByteArray>) -> Unit)
fun writeCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, valueArgs: ByteArray, typeNumberArgs: Long, callback: (Result<Unit>) -> Unit) fun writeCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, valueArgs: ByteArray, typeNumberArgs: Long, callback: (Result<Unit>) -> Unit)
fun notifyCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, stateArgs: Boolean, callback: (Result<Unit>) -> Unit) fun notifyCharacteristic(peripheralHashCodeArgs: Long, characteristicHashCodeArgs: Long, stateArgs: Boolean, callback: (Result<Unit>) -> Unit)
@ -464,15 +465,14 @@ interface MyCentralManagerHostApi {
channel.setMessageHandler { message, reply -> channel.setMessageHandler { message, reply ->
val args = message as List<Any?> val args = message as List<Any?>
val peripheralHashCodeArgsArg = args[0].let { if (it is Int) it.toLong() else it as Long } val peripheralHashCodeArgsArg = args[0].let { if (it is Int) it.toLong() else it as Long }
api.getMaximumWriteLength(peripheralHashCodeArgsArg) { result: Result<Long> -> val typeNumberArgsArg = args[1].let { if (it is Int) it.toLong() else it as Long }
val error = result.exceptionOrNull() var wrapped: List<Any?>
if (error != null) { try {
reply.reply(wrapError(error)) wrapped = listOf<Any?>(api.getMaximumWriteLength(peripheralHashCodeArgsArg, typeNumberArgsArg))
} else { } catch (exception: Throwable) {
val data = result.getOrNull() wrapped = wrapError(exception)
reply.reply(wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@ -518,6 +518,27 @@ interface MyCentralManagerHostApi {
channel.setMessageHandler(null) channel.setMessageHandler(null)
} }
} }
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.requestMTU", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val peripheralHashCodeArgsArg = args[0].let { if (it is Int) it.toLong() else it as Long }
val mtuArgsArg = args[1].let { if (it is Int) it.toLong() else it as Long }
api.requestMTU(peripheralHashCodeArgsArg, mtuArgsArg) { result: Result<Long> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run { run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.readCharacteristic", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.readCharacteristic", codec)
if (api != null) { if (api != null) {

View File

@ -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) { override fun onMtuChanged(device: BluetoothDevice, mtu: Int) {
super.onMtuChanged(device, mtu) super.onMtuChanged(device, mtu)
executor.execute { 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) { override fun onNotificationSent(device: BluetoothDevice, status: Int) {
super.onNotificationSent(device, status) super.onNotificationSent(device, status)
executor.execute { executor.execute {

View File

@ -29,6 +29,7 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess
private val services = mutableMapOf<Long, BluetoothGattService>() private val services = mutableMapOf<Long, BluetoothGattService>()
private val characteristics = mutableMapOf<Long, BluetoothGattCharacteristic>() private val characteristics = mutableMapOf<Long, BluetoothGattCharacteristic>()
private val descriptors = mutableMapOf<Long, BluetoothGattDescriptor>() private val descriptors = mutableMapOf<Long, BluetoothGattDescriptor>()
private val mtus = mutableMapOf<Long, Int>()
private val peripheralsArgs = mutableMapOf<Int, MyPeripheralArgs>() private val peripheralsArgs = mutableMapOf<Int, MyPeripheralArgs>()
private val servicesArgsOfPeripherals = mutableMapOf<Long, List<MyGattServiceArgs>>() private val servicesArgsOfPeripherals = mutableMapOf<Long, List<MyGattServiceArgs>>()
@ -43,7 +44,7 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess
private var startDiscoveryCallback: ((Result<Unit>) -> Unit)? = null private var startDiscoveryCallback: ((Result<Unit>) -> Unit)? = null
private val connectCallbacks = mutableMapOf<Long, (Result<Unit>) -> Unit>() private val connectCallbacks = mutableMapOf<Long, (Result<Unit>) -> Unit>()
private val disconnectCallbacks = mutableMapOf<Long, (Result<Unit>) -> Unit>() private val disconnectCallbacks = mutableMapOf<Long, (Result<Unit>) -> Unit>()
private val getMaximumWriteLengthCallbacks = mutableMapOf<Long, (Result<Long>) -> Unit>() private val requestMtuCallbacks = mutableMapOf<Long, (Result<Long>) -> Unit>()
private val readRssiCallbacks = mutableMapOf<Long, (Result<Long>) -> Unit>() private val readRssiCallbacks = mutableMapOf<Long, (Result<Long>) -> Unit>()
private val discoverGattCallbacks = mutableMapOf<Long, (Result<List<MyGattServiceArgs>>) -> Unit>() private val discoverGattCallbacks = mutableMapOf<Long, (Result<List<MyGattServiceArgs>>) -> Unit>()
private val readCharacteristicCallbacks = mutableMapOf<Long, (Result<ByteArray>) -> Unit>() private val readCharacteristicCallbacks = mutableMapOf<Long, (Result<ByteArray>) -> Unit>()
@ -89,6 +90,7 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess
services.clear() services.clear()
characteristics.clear() characteristics.clear()
descriptors.clear() descriptors.clear()
mtus.clear()
peripheralsArgs.clear() peripheralsArgs.clear()
servicesArgsOfPeripherals.clear() servicesArgsOfPeripherals.clear()
servicesArgs.clear() servicesArgs.clear()
@ -98,7 +100,7 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess
startDiscoveryCallback = null startDiscoveryCallback = null
connectCallbacks.clear() connectCallbacks.clear()
disconnectCallbacks.clear() disconnectCallbacks.clear()
getMaximumWriteLengthCallbacks.clear() requestMtuCallbacks.clear()
readRssiCallbacks.clear() readRssiCallbacks.clear()
discoverGattCallbacks.clear() discoverGattCallbacks.clear()
readCharacteristicCallbacks.clear() readCharacteristicCallbacks.clear()
@ -172,23 +174,33 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess
} }
} }
override fun getMaximumWriteLength(peripheralHashCodeArgs: Long, callback: (Result<Long>) -> Unit) { override fun requestMTU(peripheralHashCodeArgs: Long, mtuArgs: Long, callback: (Result<Long>) -> Unit) {
try { try {
val unfinishedCallback = getMaximumWriteLengthCallbacks[peripheralHashCodeArgs] val unfinishedCallback = requestMtuCallbacks[peripheralHashCodeArgs]
if (unfinishedCallback != null) { if (unfinishedCallback != null) {
throw IllegalStateException() throw IllegalStateException()
} }
val gatt = bluetoothGATTs[peripheralHashCodeArgs] as BluetoothGatt val gatt = bluetoothGATTs[peripheralHashCodeArgs] as BluetoothGatt
val requesting = gatt.requestMtu(517) val mtu = mtuArgs.toInt()
val requesting = gatt.requestMtu(mtu)
if (!requesting) { if (!requesting) {
throw IllegalStateException() throw IllegalStateException()
} }
getMaximumWriteLengthCallbacks[peripheralHashCodeArgs] = callback requestMtuCallbacks[peripheralHashCodeArgs] = callback
} catch (e: Throwable) { } catch (e: Throwable) {
callback(Result.failure(e)) 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<Long>) -> Unit) { override fun readRSSI(peripheralHashCodeArgs: Long, callback: (Result<Long>) -> Unit) {
try { try {
val unfinishedCallback = readRssiCallbacks[peripheralHashCodeArgs] val unfinishedCallback = readRssiCallbacks[peripheralHashCodeArgs]
@ -414,14 +426,15 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess
val deviceHashCode = device.hashCode() val deviceHashCode = device.hashCode()
val peripheralArgs = peripheralsArgs[deviceHashCode] as MyPeripheralArgs val peripheralArgs = peripheralsArgs[deviceHashCode] as MyPeripheralArgs
val peripheralHashCodeArgs = peripheralArgs.hashCodeArgs val peripheralHashCodeArgs = peripheralArgs.hashCodeArgs
// Check callbacks // check connection state.
if (newState != BluetoothProfile.STATE_CONNECTED) { if (newState == BluetoothProfile.STATE_DISCONNECTED) {
gatt.close() gatt.close()
bluetoothGATTs.remove(peripheralHashCodeArgs) bluetoothGATTs.remove(peripheralHashCodeArgs)
mtus.remove(peripheralHashCodeArgs)
val error = IllegalStateException("GATT is disconnected with status: $status") val error = IllegalStateException("GATT is disconnected with status: $status")
val getMaximumWriteLengthCallback = getMaximumWriteLengthCallbacks.remove(peripheralHashCodeArgs) val requestMtuCallback = requestMtuCallbacks.remove(peripheralHashCodeArgs)
if (getMaximumWriteLengthCallback != null) { if (requestMtuCallback != null) {
getMaximumWriteLengthCallback(Result.failure(error)) requestMtuCallback(Result.failure(error))
} }
val readRssiCallback = readRssiCallbacks.remove(peripheralHashCodeArgs) val readRssiCallback = readRssiCallbacks.remove(peripheralHashCodeArgs)
if (readRssiCallback != null) { if (readRssiCallback != null) {
@ -470,50 +483,43 @@ class MyCentralManager(private val context: Context, binaryMessenger: BinaryMess
} }
} }
} }
// Check state
val connectCallback = connectCallbacks.remove(peripheralHashCodeArgs)
val disconnectCallback = disconnectCallbacks.remove(peripheralHashCodeArgs)
if (connectCallback == null && disconnectCallback == null) {
// State changed.
val stateArgs = newState == BluetoothProfile.STATE_CONNECTED val stateArgs = newState == BluetoothProfile.STATE_CONNECTED
api.onPeripheralStateChanged(peripheralArgs, stateArgs) {} api.onPeripheralStateChanged(peripheralArgs, stateArgs) {}
} else { // check connect & disconnect callbacks.
val connectCallback = connectCallbacks.remove(peripheralHashCodeArgs)
val disconnectCallback = disconnectCallbacks.remove(peripheralHashCodeArgs)
if (connectCallback != null) { if (connectCallback != null) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
// Connect succeed.
connectCallback(Result.success(Unit)) connectCallback(Result.success(Unit))
api.onPeripheralStateChanged(peripheralArgs, true) {}
} else { } else {
// Connect failed.
val error = IllegalStateException("Connect failed with status: $status") val error = IllegalStateException("Connect failed with status: $status")
connectCallback(Result.failure(error)) connectCallback(Result.failure(error))
} }
} }
if (disconnectCallback != null) { if (disconnectCallback != null) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
// Disconnect succeed.
disconnectCallback(Result.success(Unit)) disconnectCallback(Result.success(Unit))
api.onPeripheralStateChanged(peripheralArgs, false) {}
} else { } else {
// Disconnect failed. val error = IllegalStateException("Disconnect failed with status: $status")
val error = IllegalStateException("Connect failed with status: $status")
disconnectCallback(Result.failure(error)) disconnectCallback(Result.failure(error))
} }
} }
} }
}
fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
val device = gatt.device val device = gatt.device
val hashCode = device.hashCode() val hashCode = device.hashCode()
val peripheralArgs = peripheralsArgs[hashCode] as MyPeripheralArgs val peripheralArgs = peripheralsArgs[hashCode] as MyPeripheralArgs
val peripheralHashCodeArgs = peripheralArgs.hashCodeArgs val peripheralHashCodeArgs = peripheralArgs.hashCodeArgs
val callback = getMaximumWriteLengthCallbacks.remove(peripheralHashCodeArgs) ?: return
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
val maximumWriteLengthArgs = (mtu - 3).toLong() mtus[peripheralHashCodeArgs] = mtu
callback(Result.success(maximumWriteLengthArgs)) }
val callback = requestMtuCallbacks.remove(peripheralHashCodeArgs) ?: return
if (status == BluetoothGatt.GATT_SUCCESS) {
val mtuArgs = mtu.toLong()
callback(Result.success(mtuArgs))
} else { } 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)) callback(Result.failure(error))
} }
} }

View File

@ -29,6 +29,9 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM
private val descriptors = mutableMapOf<Long, BluetoothGattDescriptor>() private val descriptors = mutableMapOf<Long, BluetoothGattDescriptor>()
private val mtus = mutableMapOf<Long, Int>() private val mtus = mutableMapOf<Long, Int>()
private val confirms = mutableMapOf<Long, Boolean>() private val confirms = mutableMapOf<Long, Boolean>()
private val preparedCharacteristics = mutableMapOf<Int, BluetoothGattCharacteristic>()
private val preparedValues = mutableMapOf<Int, ByteArray>()
private val values = mutableMapOf<Long, ByteArray>()
private val centralsArgs = mutableMapOf<Int, MyCentralArgs>() private val centralsArgs = mutableMapOf<Int, MyCentralArgs>()
private val servicesArgs = mutableMapOf<Int, MyGattServiceArgs>() private val servicesArgs = mutableMapOf<Int, MyGattServiceArgs>()
@ -81,6 +84,9 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM
descriptors.clear() descriptors.clear()
mtus.clear() mtus.clear()
confirms.clear() confirms.clear()
preparedCharacteristics.clear()
preparedValues.clear()
values.clear()
centralsArgs.clear() centralsArgs.clear()
servicesArgs.clear() servicesArgs.clear()
characteristicsArgs.clear() characteristicsArgs.clear()
@ -218,7 +224,8 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM
override fun getMaximumWriteLength(centralHashCodeArgs: Long): Long { override fun getMaximumWriteLength(centralHashCodeArgs: Long): Long {
val mtu = mtus[centralHashCodeArgs] ?: 23 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) { 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 val status = if (statusArgs) BluetoothGatt.GATT_SUCCESS
else BluetoothGatt.GATT_FAILURE else BluetoothGatt.GATT_FAILURE
val offset = offsetArgs.toInt() val offset = offsetArgs.toInt()
val value = null val value = values.remove(idArgs) as ByteArray
val sent = server.sendResponse(device, requestId, status, offset, value) val sent = server.sendResponse(device, requestId, status, offset, value)
if (!sent) { if (!sent) {
throw IllegalStateException("Send write characteristic reply failed.") throw IllegalStateException("Send write characteristic reply failed.")
@ -335,6 +342,13 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM
callback(Result.failure(error)) 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) { fun onMtuChanged(device: BluetoothDevice, mtu: Int) {
val hashCode = device.hashCode() val hashCode = device.hashCode()
val centralArgs = centralsArgs.getOrPut(hashCode) { device.toCentralArgs() } val centralArgs = centralsArgs.getOrPut(hashCode) { device.toCentralArgs() }
@ -360,12 +374,50 @@ class MyPeripheralManager(private val context: Context, binaryMessenger: BinaryM
val centralArgs = centralsArgs.getOrPut(deviceHashCode) { device.toCentralArgs() } val centralArgs = centralsArgs.getOrPut(deviceHashCode) { device.toCentralArgs() }
val centralHashCodeArgs = centralArgs.hashCodeArgs val centralHashCodeArgs = centralArgs.hashCodeArgs
devices[centralHashCodeArgs] = device 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 characteristicHashCode = characteristic.hashCode()
val characteristicArgs = characteristicsArgs[characteristicHashCode] as MyGattCharacteristicArgs val characteristicArgs = characteristicsArgs[characteristicHashCode] as MyGattCharacteristicArgs
val idArgs = requestId.toLong() val idArgs = requestId.toLong()
val offsetArgs = offset.toLong() val offsetArgs = offset.toLong()
values[idArgs] = value
api.onWriteCharacteristicCommandReceived(centralArgs, characteristicArgs, idArgs, offsetArgs, 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 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) { fun onDescriptorReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, descriptor: BluetoothGattDescriptor) {
val status = BluetoothGatt.GATT_SUCCESS val status = BluetoothGatt.GATT_SUCCESS

View File

@ -329,7 +329,7 @@ class _ScannerViewState extends State<ScannerView> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: Text('$rssi'), trailing: RssiWidget(rssi),
); );
}, },
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
@ -375,10 +375,13 @@ class _PeripheralViewState extends State<PeripheralView> {
late final ValueNotifier<GattCharacteristic?> characteristic; late final ValueNotifier<GattCharacteristic?> characteristic;
late final ValueNotifier<GattCharacteristicWriteType> writeType; late final ValueNotifier<GattCharacteristicWriteType> writeType;
late final ValueNotifier<int> maximumWriteLength; late final ValueNotifier<int> maximumWriteLength;
late final ValueNotifier<int> rssi;
late final ValueNotifier<List<Log>> logs; late final ValueNotifier<List<Log>> logs;
late final TextEditingController writeController; late final TextEditingController writeController;
late final StreamSubscription stateChangedSubscription; late final StreamSubscription stateChangedSubscription;
late final StreamSubscription valueChangedSubscription; late final StreamSubscription valueChangedSubscription;
late final StreamSubscription rssiChangedSubscription;
late final Timer rssiTimer;
@override @override
void initState() { void initState() {
@ -390,7 +393,8 @@ class _PeripheralViewState extends State<PeripheralView> {
service = ValueNotifier(null); service = ValueNotifier(null);
characteristic = ValueNotifier(null); characteristic = ValueNotifier(null);
writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse);
maximumWriteLength = ValueNotifier(20); maximumWriteLength = ValueNotifier(0);
rssi = ValueNotifier(-100);
logs = ValueNotifier([]); logs = ValueNotifier([]);
writeController = TextEditingController(); writeController = TextEditingController();
stateChangedSubscription = centralManager.peripheralStateChanged.listen( stateChangedSubscription = centralManager.peripheralStateChanged.listen(
@ -423,6 +427,17 @@ class _PeripheralViewState extends State<PeripheralView> {
]; ];
}, },
); );
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 @override
@ -455,10 +470,18 @@ class _PeripheralViewState extends State<PeripheralView> {
final peripheral = eventArgs.peripheral; final peripheral = eventArgs.peripheral;
if (state) { if (state) {
await centralManager.disconnect(peripheral); await centralManager.disconnect(peripheral);
maximumWriteLength.value = 0;
rssi.value = 0;
} else { } else {
await centralManager.connect(peripheral); await centralManager.connect(peripheral);
services.value = services.value =
await centralManager.discoverGATT(peripheral); await centralManager.discoverGATT(peripheral);
maximumWriteLength.value =
await centralManager.getMaximumWriteLength(
peripheral,
type: writeType.value,
);
rssi.value = await centralManager.readRSSI(peripheral);
} }
}, },
child: Text(state ? 'DISCONNECT' : 'CONNECT'), child: Text(state ? 'DISCONNECT' : 'CONNECT'),
@ -592,72 +615,82 @@ class _PeripheralViewState extends State<PeripheralView> {
), ),
Row( Row(
children: [ children: [
Expanded( ValueListenableBuilder(
child: Center(
child: ValueListenableBuilder(
valueListenable: writeType, valueListenable: writeType,
builder: (context, writeType, child) { builder: (context, writeType, child) {
final items = return ToggleButtons(
GattCharacteristicWriteType.values.map((type) { onPressed: (i) async {
return DropdownMenuItem( final type = GattCharacteristicWriteType.values[i];
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; this.writeType.value = type;
},
value: writeType,
underline: const Offstage(),
);
},
),
),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return TextButton(
onPressed: state
? () async {
maximumWriteLength.value = maximumWriteLength.value =
await centralManager.getMaximumWriteLength( await centralManager.getMaximumWriteLength(
eventArgs.peripheral, eventArgs.peripheral,
type: writeType.value, type: type,
); );
} },
: null, constraints: const BoxConstraints(
child: ValueListenableBuilder( 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),
// ),
// ),
// );
},
),
const SizedBox(width: 8.0),
ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return ValueListenableBuilder(
valueListenable: maximumWriteLength, valueListenable: maximumWriteLength,
builder: (context, maximumWriteLength, child) { builder: (context, maximumWriteLength, child) {
return Text('MTU: $maximumWriteLength'); return Text('$maximumWriteLength');
}, },
),
); );
}, },
), ),
), const Spacer(),
IconButton( ValueListenableBuilder(
onPressed: () async { valueListenable: rssi,
final rssi = builder: (context, rssi, child) {
await centralManager.readRSSI(eventArgs.peripheral); return RssiWidget(rssi);
log('RSSI: $rssi');
}, },
icon: const Icon(Icons.signal_wifi_4_bar),
), ),
], ],
), ),
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
height: 160.0, height: 160.0,
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: characteristic, valueListenable: characteristic,
@ -755,6 +788,7 @@ class _PeripheralViewState extends State<PeripheralView> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
rssiTimer.cancel();
stateChangedSubscription.cancel(); stateChangedSubscription.cancel();
valueChangedSubscription.cancel(); valueChangedSubscription.cancel();
state.dispose(); state.dispose();
@ -764,6 +798,7 @@ class _PeripheralViewState extends State<PeripheralView> {
characteristic.dispose(); characteristic.dispose();
writeType.dispose(); writeType.dispose();
maximumWriteLength.dispose(); maximumWriteLength.dispose();
rssi.dispose();
logs.dispose(); logs.dispose();
writeController.dispose(); writeController.dispose();
} }
@ -1070,3 +1105,25 @@ enum LogType {
notify, notify,
error, 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);
}
}

View File

@ -15,7 +15,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "3.0.0" version: "3.0.3"
bluetooth_low_energy_platform_interface: bluetooth_low_energy_platform_interface:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -448,12 +448,12 @@ class MyCentralManagerHostApi {
} }
} }
Future<int> getMaximumWriteLength(int arg_peripheralHashCodeArgs) async { Future<int> getMaximumWriteLength(int arg_peripheralHashCodeArgs, int arg_typeNumberArgs) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.getMaximumWriteLength', codec, 'dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.getMaximumWriteLength', codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList =
await channel.send(<Object?>[arg_peripheralHashCodeArgs]) as List<Object?>?; await channel.send(<Object?>[arg_peripheralHashCodeArgs, arg_typeNumberArgs]) as List<Object?>?;
if (replyList == null) { if (replyList == null) {
throw PlatformException( throw PlatformException(
code: 'channel-error', code: 'channel-error',
@ -529,6 +529,33 @@ class MyCentralManagerHostApi {
} }
} }
Future<int> requestMTU(int arg_peripheralHashCodeArgs, int arg_mtuArgs) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.requestMTU', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_peripheralHashCodeArgs, arg_mtuArgs]) as List<Object?>?;
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<Uint8List> readCharacteristic(int arg_peripheralHashCodeArgs, int arg_characteristicHashCodeArgs) async { Future<Uint8List> readCharacteristic(int arg_peripheralHashCodeArgs, int arg_characteristicHashCodeArgs) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.readCharacteristic', codec, 'dev.flutter.pigeon.bluetooth_low_energy_android.MyCentralManagerHostApi.readCharacteristic', codec,

View File

@ -75,8 +75,12 @@ class MyCentralManager extends MyBluetoothLowEnergyManager
}) async { }) async {
await throwWithoutState(BluetoothLowEnergyState.poweredOn); await throwWithoutState(BluetoothLowEnergyState.poweredOn);
final peripheralHashCodeArgs = peripheral.hashCode; final peripheralHashCodeArgs = peripheral.hashCode;
final maximumWriteLength = final typeArgs = type.toArgs();
await _api.getMaximumWriteLength(peripheralHashCodeArgs); final typeNumberArgs = typeArgs.index;
final maximumWriteLength = await _api.getMaximumWriteLength(
peripheralHashCodeArgs,
typeNumberArgs,
);
return maximumWriteLength; return maximumWriteLength;
} }
@ -96,6 +100,12 @@ class MyCentralManager extends MyBluetoothLowEnergyManager
} }
final peripheralHashCodeArgs = peripheral.hashCode; final peripheralHashCodeArgs = peripheral.hashCode;
final servicesArgs = await _api.discoverGATT(peripheralHashCodeArgs); final servicesArgs = await _api.discoverGATT(peripheralHashCodeArgs);
// 部分外围设备连接后会触发 onMtuChanged 回调,若在此之前调用协商 MTU 的方法,会在协商完成前返回,
// 此时如果继续调用其他方法(如发现服务)会导致回调无法触发,
// 因此为避免此情况发生,需要延迟到发现服务完成后再协商 MTU。
// TODO: 思考更好的解决方式,可以在连接后立即协商 MTU。
const mtuArgs = 517;
await _api.requestMTU(peripheralHashCodeArgs, mtuArgs);
final services = servicesArgs final services = servicesArgs
.cast<MyGattServiceArgs>() .cast<MyGattServiceArgs>()
.map((args) => args.toService2()) .map((args) => args.toService2())

View File

@ -22,13 +22,14 @@ abstract class MyCentralManagerHostApi {
void connect(int peripheralHashCodeArgs); void connect(int peripheralHashCodeArgs);
@async @async
void disconnect(int peripheralHashCodeArgs); void disconnect(int peripheralHashCodeArgs);
@async int getMaximumWriteLength(int peripheralHashCodeArgs, int typeNumberArgs);
int getMaximumWriteLength(int peripheralHashCodeArgs);
@async @async
int readRSSI(int peripheralHashCodeArgs); int readRSSI(int peripheralHashCodeArgs);
@async @async
List<MyGattServiceArgs> discoverGATT(int peripheralHashCodeArgs); List<MyGattServiceArgs> discoverGATT(int peripheralHashCodeArgs);
@async @async
int requestMTU(int peripheralHashCodeArgs, int mtuArgs);
@async
Uint8List readCharacteristic( Uint8List readCharacteristic(
int peripheralHashCodeArgs, int peripheralHashCodeArgs,
int characteristicHashCodeArgs, int characteristicHashCodeArgs,

View File

@ -1,6 +1,6 @@
name: bluetooth_low_energy_android name: bluetooth_low_energy_android
description: Android implementation of the bluetooth_low_energy plugin. 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 homepage: https://github.com/yanshouwang/bluetooth_low_energy
environment: environment:

View File

@ -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 ## 3.0.1
* Fix the issue that write characteristic will never complete when write without response. * Fix the issue that write characteristic will never complete when write without response.

View File

@ -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 { extension Dictionary {
mutating func getOrPut(_ key: Key, _ defaultValue: () -> Value) -> Value { mutating func getOrPut(_ key: Key, _ defaultValue: () -> Value) -> Value {
guard let value = self[key] else { guard let value = self[key] else {

View File

@ -154,7 +154,7 @@ class MyCentralManager: MyCentralManagerHostApi {
throw MyError.illegalArgument throw MyError.illegalArgument
} }
let type = typeArgs.toWriteType() let type = typeArgs.toWriteType()
let maximumWriteLength = peripheral.maximumWriteValueLength(for: type) let maximumWriteLength = try peripheral.maximumWriteValueLength(for: type).coerceIn(20, 512)
let maximumWriteLengthArgs = Int64(maximumWriteLength) let maximumWriteLengthArgs = Int64(maximumWriteLength)
return maximumWriteLengthArgs return maximumWriteLengthArgs
} }
@ -310,7 +310,6 @@ class MyCentralManager: MyCentralManagerHostApi {
api.onStateChanged(stateNumberArgs: stateNumberArgs) {} api.onStateChanged(stateNumberArgs: stateNumberArgs) {}
} }
func didDiscover(_ peripheral: CBPeripheral, _ advertisementData: [String : Any], _ rssi: NSNumber) { func didDiscover(_ peripheral: CBPeripheral, _ advertisementData: [String : Any], _ rssi: NSNumber) {
let peripheralArgs = peripheral.toArgs() let peripheralArgs = peripheral.toArgs()
let peripheralHashCode = peripheral.hash let peripheralHashCode = peripheral.hash
@ -329,10 +328,12 @@ class MyCentralManager: MyCentralManagerHostApi {
return return
} }
let peripheralHashCodeArgs = peripheralArgs.hashCodeArgs let peripheralHashCodeArgs = peripheralArgs.hashCodeArgs
let completion = connectCompletions.removeValue(forKey: peripheralHashCodeArgs)
completion?(.success(()))
let stateArgs = true let stateArgs = true
api.onPeripheralStateChanged(peripheralArgs: peripheralArgs, stateArgs: stateArgs) {} api.onPeripheralStateChanged(peripheralArgs: peripheralArgs, stateArgs: stateArgs) {}
guard let completion = connectCompletions.removeValue(forKey: peripheralHashCodeArgs) else {
return
}
completion(.success(()))
} }
func didFailToConnect(_ peripheral: CBPeripheral, _ error: Error?) { func didFailToConnect(_ peripheral: CBPeripheral, _ error: Error?) {

View File

@ -7,6 +7,7 @@
import Foundation import Foundation
// TODO:
enum MyError: Error { enum MyError: Error {
case illegalArgument case illegalArgument
case illegalState case illegalState

View File

@ -202,13 +202,13 @@ class MyPeripheralManager: MyPeripheralManagerHostApi {
guard let central = centrals[centralHashCodeArgs] else { guard let central = centrals[centralHashCodeArgs] else {
throw MyError.illegalArgument throw MyError.illegalArgument
} }
let maximumWriteLength = central.maximumUpdateValueLength let maximumWriteLength = try central.maximumUpdateValueLength.coerceIn(20, 512)
let maximumWriteLengthArgs = Int64(maximumWriteLength) let maximumWriteLengthArgs = Int64(maximumWriteLength)
return maximumWriteLengthArgs return maximumWriteLengthArgs
} }
func sendReadCharacteristicReply(centralHashCodeArgs: Int64, characteristicHashCodeArgs: Int64, idArgs: Int64, offsetArgs: Int64, statusArgs: Bool, valueArgs: FlutterStandardTypedData) throws { 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 throw MyError.illegalArgument
} }
request.value = valueArgs.data request.value = valueArgs.data
@ -217,7 +217,7 @@ class MyPeripheralManager: MyPeripheralManagerHostApi {
} }
func sendWriteCharacteristicReply(centralHashCodeArgs: Int64, characteristicHashCodeArgs: Int64, idArgs: Int64, offsetArgs: Int64, statusArgs: Bool) throws { 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 throw MyError.illegalArgument
} }
let result = statusArgs ? CBATTError.Code.success : CBATTError.Code.requestNotSupported let result = statusArgs ? CBATTError.Code.success : CBATTError.Code.requestNotSupported
@ -315,7 +315,15 @@ class MyPeripheralManager: MyPeripheralManagerHostApi {
} }
func didReceiveWrite(_ requests: [CBATTRequest]) { func didReceiveWrite(_ requests: [CBATTRequest]) {
for request in requests { //
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 central = request.central
let centralHashCode = central.hash let centralHashCode = central.hash
let centralArgs = centralsArgs.getOrPut(centralHashCode) { central.toArgs() } let centralArgs = centralsArgs.getOrPut(centralHashCode) { central.toArgs() }
@ -337,7 +345,6 @@ class MyPeripheralManager: MyPeripheralManagerHostApi {
let valueArgs = FlutterStandardTypedData(bytes: value) let valueArgs = FlutterStandardTypedData(bytes: value)
api.onWriteCharacteristicCommandReceived(centralArgs: centralArgs, characteristicArgs: characteristicArgs, idArgs: idArgs, offsetArgs: offsetArgs, valueArgs: valueArgs) {} api.onWriteCharacteristicCommandReceived(centralArgs: centralArgs, characteristicArgs: characteristicArgs, idArgs: idArgs, offsetArgs: offsetArgs, valueArgs: valueArgs) {}
} }
}
func didSubscribeTo(_ central: CBCentral, _ characteristic: CBCharacteristic) { func didSubscribeTo(_ central: CBCentral, _ characteristic: CBCharacteristic) {
let centralHashCode = central.hash let centralHashCode = central.hash

View File

@ -329,7 +329,7 @@ class _ScannerViewState extends State<ScannerView> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: Text('$rssi'), trailing: RssiWidget(rssi),
); );
}, },
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
@ -375,10 +375,13 @@ class _PeripheralViewState extends State<PeripheralView> {
late final ValueNotifier<GattCharacteristic?> characteristic; late final ValueNotifier<GattCharacteristic?> characteristic;
late final ValueNotifier<GattCharacteristicWriteType> writeType; late final ValueNotifier<GattCharacteristicWriteType> writeType;
late final ValueNotifier<int> maximumWriteLength; late final ValueNotifier<int> maximumWriteLength;
late final ValueNotifier<int> rssi;
late final ValueNotifier<List<Log>> logs; late final ValueNotifier<List<Log>> logs;
late final TextEditingController writeController; late final TextEditingController writeController;
late final StreamSubscription stateChangedSubscription; late final StreamSubscription stateChangedSubscription;
late final StreamSubscription valueChangedSubscription; late final StreamSubscription valueChangedSubscription;
late final StreamSubscription rssiChangedSubscription;
late final Timer rssiTimer;
@override @override
void initState() { void initState() {
@ -390,7 +393,8 @@ class _PeripheralViewState extends State<PeripheralView> {
service = ValueNotifier(null); service = ValueNotifier(null);
characteristic = ValueNotifier(null); characteristic = ValueNotifier(null);
writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse);
maximumWriteLength = ValueNotifier(20); maximumWriteLength = ValueNotifier(0);
rssi = ValueNotifier(-100);
logs = ValueNotifier([]); logs = ValueNotifier([]);
writeController = TextEditingController(); writeController = TextEditingController();
stateChangedSubscription = centralManager.peripheralStateChanged.listen( stateChangedSubscription = centralManager.peripheralStateChanged.listen(
@ -423,6 +427,17 @@ class _PeripheralViewState extends State<PeripheralView> {
]; ];
}, },
); );
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 @override
@ -455,10 +470,18 @@ class _PeripheralViewState extends State<PeripheralView> {
final peripheral = eventArgs.peripheral; final peripheral = eventArgs.peripheral;
if (state) { if (state) {
await centralManager.disconnect(peripheral); await centralManager.disconnect(peripheral);
maximumWriteLength.value = 0;
rssi.value = 0;
} else { } else {
await centralManager.connect(peripheral); await centralManager.connect(peripheral);
services.value = services.value =
await centralManager.discoverGATT(peripheral); await centralManager.discoverGATT(peripheral);
maximumWriteLength.value =
await centralManager.getMaximumWriteLength(
peripheral,
type: writeType.value,
);
rssi.value = await centralManager.readRSSI(peripheral);
} }
}, },
child: Text(state ? 'DISCONNECT' : 'CONNECT'), child: Text(state ? 'DISCONNECT' : 'CONNECT'),
@ -592,72 +615,82 @@ class _PeripheralViewState extends State<PeripheralView> {
), ),
Row( Row(
children: [ children: [
Expanded( ValueListenableBuilder(
child: Center(
child: ValueListenableBuilder(
valueListenable: writeType, valueListenable: writeType,
builder: (context, writeType, child) { builder: (context, writeType, child) {
final items = return ToggleButtons(
GattCharacteristicWriteType.values.map((type) { onPressed: (i) async {
return DropdownMenuItem( final type = GattCharacteristicWriteType.values[i];
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; this.writeType.value = type;
},
value: writeType,
underline: const Offstage(),
);
},
),
),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return TextButton(
onPressed: state
? () async {
maximumWriteLength.value = maximumWriteLength.value =
await centralManager.getMaximumWriteLength( await centralManager.getMaximumWriteLength(
eventArgs.peripheral, eventArgs.peripheral,
type: writeType.value, type: type,
); );
} },
: null, constraints: const BoxConstraints(
child: ValueListenableBuilder( 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),
// ),
// ),
// );
},
),
const SizedBox(width: 8.0),
ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return ValueListenableBuilder(
valueListenable: maximumWriteLength, valueListenable: maximumWriteLength,
builder: (context, maximumWriteLength, child) { builder: (context, maximumWriteLength, child) {
return Text('MTU: $maximumWriteLength'); return Text('$maximumWriteLength');
}, },
),
); );
}, },
), ),
), const Spacer(),
IconButton( ValueListenableBuilder(
onPressed: () async { valueListenable: rssi,
final rssi = builder: (context, rssi, child) {
await centralManager.readRSSI(eventArgs.peripheral); return RssiWidget(rssi);
log('RSSI: $rssi');
}, },
icon: const Icon(Icons.signal_wifi_4_bar),
), ),
], ],
), ),
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
height: 160.0, height: 160.0,
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: characteristic, valueListenable: characteristic,
@ -755,6 +788,7 @@ class _PeripheralViewState extends State<PeripheralView> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
rssiTimer.cancel();
stateChangedSubscription.cancel(); stateChangedSubscription.cancel();
valueChangedSubscription.cancel(); valueChangedSubscription.cancel();
state.dispose(); state.dispose();
@ -764,6 +798,7 @@ class _PeripheralViewState extends State<PeripheralView> {
characteristic.dispose(); characteristic.dispose();
writeType.dispose(); writeType.dispose();
maximumWriteLength.dispose(); maximumWriteLength.dispose();
rssi.dispose();
logs.dispose(); logs.dispose();
writeController.dispose(); writeController.dispose();
} }
@ -1070,3 +1105,25 @@ enum LogType {
notify, notify,
error, 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);
}
}

View File

@ -15,7 +15,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "3.0.0" version: "3.0.2"
bluetooth_low_energy_platform_interface: bluetooth_low_energy_platform_interface:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -1,6 +1,6 @@
name: bluetooth_low_energy_darwin name: bluetooth_low_energy_darwin
description: iOS and macOS implementation of the bluetooth_low_energy plugin. 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 homepage: https://github.com/yanshouwang/bluetooth_low_energy
environment: environment:

View File

@ -329,7 +329,7 @@ class _ScannerViewState extends State<ScannerView> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: Text('$rssi'), trailing: RssiWidget(rssi),
); );
}, },
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
@ -375,10 +375,13 @@ class _PeripheralViewState extends State<PeripheralView> {
late final ValueNotifier<GattCharacteristic?> characteristic; late final ValueNotifier<GattCharacteristic?> characteristic;
late final ValueNotifier<GattCharacteristicWriteType> writeType; late final ValueNotifier<GattCharacteristicWriteType> writeType;
late final ValueNotifier<int> maximumWriteLength; late final ValueNotifier<int> maximumWriteLength;
late final ValueNotifier<int> rssi;
late final ValueNotifier<List<Log>> logs; late final ValueNotifier<List<Log>> logs;
late final TextEditingController writeController; late final TextEditingController writeController;
late final StreamSubscription stateChangedSubscription; late final StreamSubscription stateChangedSubscription;
late final StreamSubscription valueChangedSubscription; late final StreamSubscription valueChangedSubscription;
late final StreamSubscription rssiChangedSubscription;
late final Timer rssiTimer;
@override @override
void initState() { void initState() {
@ -390,7 +393,8 @@ class _PeripheralViewState extends State<PeripheralView> {
service = ValueNotifier(null); service = ValueNotifier(null);
characteristic = ValueNotifier(null); characteristic = ValueNotifier(null);
writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse);
maximumWriteLength = ValueNotifier(20); maximumWriteLength = ValueNotifier(0);
rssi = ValueNotifier(-100);
logs = ValueNotifier([]); logs = ValueNotifier([]);
writeController = TextEditingController(); writeController = TextEditingController();
stateChangedSubscription = centralManager.peripheralStateChanged.listen( stateChangedSubscription = centralManager.peripheralStateChanged.listen(
@ -423,6 +427,17 @@ class _PeripheralViewState extends State<PeripheralView> {
]; ];
}, },
); );
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 @override
@ -455,10 +470,18 @@ class _PeripheralViewState extends State<PeripheralView> {
final peripheral = eventArgs.peripheral; final peripheral = eventArgs.peripheral;
if (state) { if (state) {
await centralManager.disconnect(peripheral); await centralManager.disconnect(peripheral);
maximumWriteLength.value = 0;
rssi.value = 0;
} else { } else {
await centralManager.connect(peripheral); await centralManager.connect(peripheral);
services.value = services.value =
await centralManager.discoverGATT(peripheral); await centralManager.discoverGATT(peripheral);
maximumWriteLength.value =
await centralManager.getMaximumWriteLength(
peripheral,
type: writeType.value,
);
rssi.value = await centralManager.readRSSI(peripheral);
} }
}, },
child: Text(state ? 'DISCONNECT' : 'CONNECT'), child: Text(state ? 'DISCONNECT' : 'CONNECT'),
@ -592,72 +615,82 @@ class _PeripheralViewState extends State<PeripheralView> {
), ),
Row( Row(
children: [ children: [
Expanded( ValueListenableBuilder(
child: Center(
child: ValueListenableBuilder(
valueListenable: writeType, valueListenable: writeType,
builder: (context, writeType, child) { builder: (context, writeType, child) {
final items = return ToggleButtons(
GattCharacteristicWriteType.values.map((type) { onPressed: (i) async {
return DropdownMenuItem( final type = GattCharacteristicWriteType.values[i];
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; this.writeType.value = type;
},
value: writeType,
underline: const Offstage(),
);
},
),
),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return TextButton(
onPressed: state
? () async {
maximumWriteLength.value = maximumWriteLength.value =
await centralManager.getMaximumWriteLength( await centralManager.getMaximumWriteLength(
eventArgs.peripheral, eventArgs.peripheral,
type: writeType.value, type: type,
); );
} },
: null, constraints: const BoxConstraints(
child: ValueListenableBuilder( 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),
// ),
// ),
// );
},
),
const SizedBox(width: 8.0),
ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return ValueListenableBuilder(
valueListenable: maximumWriteLength, valueListenable: maximumWriteLength,
builder: (context, maximumWriteLength, child) { builder: (context, maximumWriteLength, child) {
return Text('MTU: $maximumWriteLength'); return Text('$maximumWriteLength');
}, },
),
); );
}, },
), ),
), const Spacer(),
IconButton( ValueListenableBuilder(
onPressed: () async { valueListenable: rssi,
final rssi = builder: (context, rssi, child) {
await centralManager.readRSSI(eventArgs.peripheral); return RssiWidget(rssi);
log('RSSI: $rssi');
}, },
icon: const Icon(Icons.signal_wifi_4_bar),
), ),
], ],
), ),
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
height: 160.0, height: 160.0,
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: characteristic, valueListenable: characteristic,
@ -755,6 +788,7 @@ class _PeripheralViewState extends State<PeripheralView> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
rssiTimer.cancel();
stateChangedSubscription.cancel(); stateChangedSubscription.cancel();
valueChangedSubscription.cancel(); valueChangedSubscription.cancel();
state.dispose(); state.dispose();
@ -764,6 +798,7 @@ class _PeripheralViewState extends State<PeripheralView> {
characteristic.dispose(); characteristic.dispose();
writeType.dispose(); writeType.dispose();
maximumWriteLength.dispose(); maximumWriteLength.dispose();
rssi.dispose();
logs.dispose(); logs.dispose();
writeController.dispose(); writeController.dispose();
} }
@ -1070,3 +1105,25 @@ enum LogType {
notify, notify,
error, 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);
}
}

View File

@ -329,7 +329,7 @@ class _ScannerViewState extends State<ScannerView> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: Text('$rssi'), trailing: RssiWidget(rssi),
); );
}, },
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
@ -375,10 +375,13 @@ class _PeripheralViewState extends State<PeripheralView> {
late final ValueNotifier<GattCharacteristic?> characteristic; late final ValueNotifier<GattCharacteristic?> characteristic;
late final ValueNotifier<GattCharacteristicWriteType> writeType; late final ValueNotifier<GattCharacteristicWriteType> writeType;
late final ValueNotifier<int> maximumWriteLength; late final ValueNotifier<int> maximumWriteLength;
late final ValueNotifier<int> rssi;
late final ValueNotifier<List<Log>> logs; late final ValueNotifier<List<Log>> logs;
late final TextEditingController writeController; late final TextEditingController writeController;
late final StreamSubscription stateChangedSubscription; late final StreamSubscription stateChangedSubscription;
late final StreamSubscription valueChangedSubscription; late final StreamSubscription valueChangedSubscription;
late final StreamSubscription rssiChangedSubscription;
late final Timer rssiTimer;
@override @override
void initState() { void initState() {
@ -390,7 +393,8 @@ class _PeripheralViewState extends State<PeripheralView> {
service = ValueNotifier(null); service = ValueNotifier(null);
characteristic = ValueNotifier(null); characteristic = ValueNotifier(null);
writeType = ValueNotifier(GattCharacteristicWriteType.withResponse); writeType = ValueNotifier(GattCharacteristicWriteType.withResponse);
maximumWriteLength = ValueNotifier(20); maximumWriteLength = ValueNotifier(0);
rssi = ValueNotifier(-100);
logs = ValueNotifier([]); logs = ValueNotifier([]);
writeController = TextEditingController(); writeController = TextEditingController();
stateChangedSubscription = centralManager.peripheralStateChanged.listen( stateChangedSubscription = centralManager.peripheralStateChanged.listen(
@ -423,6 +427,17 @@ class _PeripheralViewState extends State<PeripheralView> {
]; ];
}, },
); );
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 @override
@ -455,10 +470,18 @@ class _PeripheralViewState extends State<PeripheralView> {
final peripheral = eventArgs.peripheral; final peripheral = eventArgs.peripheral;
if (state) { if (state) {
await centralManager.disconnect(peripheral); await centralManager.disconnect(peripheral);
maximumWriteLength.value = 0;
rssi.value = 0;
} else { } else {
await centralManager.connect(peripheral); await centralManager.connect(peripheral);
services.value = services.value =
await centralManager.discoverGATT(peripheral); await centralManager.discoverGATT(peripheral);
maximumWriteLength.value =
await centralManager.getMaximumWriteLength(
peripheral,
type: writeType.value,
);
rssi.value = await centralManager.readRSSI(peripheral);
} }
}, },
child: Text(state ? 'DISCONNECT' : 'CONNECT'), child: Text(state ? 'DISCONNECT' : 'CONNECT'),
@ -592,72 +615,82 @@ class _PeripheralViewState extends State<PeripheralView> {
), ),
Row( Row(
children: [ children: [
Expanded( ValueListenableBuilder(
child: Center(
child: ValueListenableBuilder(
valueListenable: writeType, valueListenable: writeType,
builder: (context, writeType, child) { builder: (context, writeType, child) {
final items = return ToggleButtons(
GattCharacteristicWriteType.values.map((type) { onPressed: (i) async {
return DropdownMenuItem( final type = GattCharacteristicWriteType.values[i];
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; this.writeType.value = type;
},
value: writeType,
underline: const Offstage(),
);
},
),
),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return TextButton(
onPressed: state
? () async {
maximumWriteLength.value = maximumWriteLength.value =
await centralManager.getMaximumWriteLength( await centralManager.getMaximumWriteLength(
eventArgs.peripheral, eventArgs.peripheral,
type: writeType.value, type: type,
); );
} },
: null, constraints: const BoxConstraints(
child: ValueListenableBuilder( 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),
// ),
// ),
// );
},
),
const SizedBox(width: 8.0),
ValueListenableBuilder(
valueListenable: state,
builder: (context, state, child) {
return ValueListenableBuilder(
valueListenable: maximumWriteLength, valueListenable: maximumWriteLength,
builder: (context, maximumWriteLength, child) { builder: (context, maximumWriteLength, child) {
return Text('MTU: $maximumWriteLength'); return Text('$maximumWriteLength');
}, },
),
); );
}, },
), ),
), const Spacer(),
IconButton( ValueListenableBuilder(
onPressed: () async { valueListenable: rssi,
final rssi = builder: (context, rssi, child) {
await centralManager.readRSSI(eventArgs.peripheral); return RssiWidget(rssi);
log('RSSI: $rssi');
}, },
icon: const Icon(Icons.signal_wifi_4_bar),
), ),
], ],
), ),
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
height: 160.0, height: 160.0,
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: characteristic, valueListenable: characteristic,
@ -755,6 +788,7 @@ class _PeripheralViewState extends State<PeripheralView> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
rssiTimer.cancel();
stateChangedSubscription.cancel(); stateChangedSubscription.cancel();
valueChangedSubscription.cancel(); valueChangedSubscription.cancel();
state.dispose(); state.dispose();
@ -764,6 +798,7 @@ class _PeripheralViewState extends State<PeripheralView> {
characteristic.dispose(); characteristic.dispose();
writeType.dispose(); writeType.dispose();
maximumWriteLength.dispose(); maximumWriteLength.dispose();
rssi.dispose();
logs.dispose(); logs.dispose();
writeController.dispose(); writeController.dispose();
} }
@ -1070,3 +1105,25 @@ enum LogType {
notify, notify,
error, 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);
}
}