From b7d534511a44e6073b9a661399783cdd922d5745 Mon Sep 17 00:00:00 2001 From: Nebojsa Cvetkovic Date: Sat, 9 Nov 2024 22:08:38 -0500 Subject: [PATCH] WIP --- .../FlutterBluePlusPlugin.java | 380 ++++----- ios/Classes/FlutterBluePlusPlugin.m | 455 +++++------ lib/flutter_blue_plus.dart | 5 +- lib/src/bluetooth_attribute.dart | 77 ++ lib/src/bluetooth_characteristic.dart | 222 +----- lib/src/bluetooth_descriptor.dart | 156 +--- lib/src/bluetooth_device.dart | 309 +++---- lib/src/bluetooth_events.dart | 361 ++++----- lib/src/bluetooth_msgs.dart | 753 ++++++------------ lib/src/bluetooth_service.dart | 62 +- lib/src/flutter_blue_plus.dart | 303 +++---- 11 files changed, 1199 insertions(+), 1884 deletions(-) create mode 100644 lib/src/bluetooth_attribute.dart diff --git a/android/src/main/java/com/lib/flutter_blue_plus/FlutterBluePlusPlugin.java b/android/src/main/java/com/lib/flutter_blue_plus/FlutterBluePlusPlugin.java index fa655af2..0d10a723 100644 --- a/android/src/main/java/com/lib/flutter_blue_plus/FlutterBluePlusPlugin.java +++ b/android/src/main/java/com/lib/flutter_blue_plus/FlutterBluePlusPlugin.java @@ -42,6 +42,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; import java.util.UUID; import java.util.Timer; @@ -282,9 +283,7 @@ public void onDetachedFromActivity() @Override @SuppressWarnings({"deprecation", "unchecked"}) // needed for compatibility, type safety uses bluetooth_msgs.dart - public void onMethodCall(@NonNull MethodCall call, - @NonNull Result result) - { + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { try { acquireMutex(mMethodCallMutex); @@ -299,7 +298,7 @@ public void onMethodCall(@NonNull MethodCall call, // check that we have an adapter, except for // the functions that do not need it - if(mBluetoothAdapter == null && + if (mBluetoothAdapter == null && "flutterRestart".equals(call.method) == false && "connectedCount".equals(call.method) == false && "setLogLevel".equals(call.method) == false && @@ -836,9 +835,7 @@ public void onMethodCall(@NonNull MethodCall call, // see: BmReadCharacteristicRequest HashMap data = call.arguments(); String remoteId = (String) data.get("remote_id"); - String serviceUuid = (String) data.get("service_uuid"); - String characteristicUuid = (String) data.get("characteristic_uuid"); - String primaryServiceUuid = (String) data.get("primary_service_uuid"); + String identifier = (String) data.get("identifier"); // check connection BluetoothGatt gatt = mConnectedDevices.get(remoteId); @@ -851,13 +848,12 @@ public void onMethodCall(@NonNull MethodCall call, waitIfBonding(); // find characteristic - ChrFound found = locateCharacteristic(gatt, serviceUuid, characteristicUuid, primaryServiceUuid); - if (found.error != null) { - result.error("readCharacteristic", found.error, null); - break; - } - - BluetoothGattCharacteristic characteristic = found.characteristic; + BluetoothGattCharacteristic characteristic = locateCharacteristic(gatt, identifier); + // TODO CRITICAL catch exceptions +// if (found.error != null) { +// result.error("readCharacteristic", found.error, null); +// break; +// } // check readable if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0) { @@ -867,7 +863,7 @@ public void onMethodCall(@NonNull MethodCall call, } // read - if(gatt.readCharacteristic(characteristic) == false) { + if (gatt.readCharacteristic(characteristic) == false) { result.error("readCharacteristic", "gatt.readCharacteristic() returned false", null); break; @@ -882,9 +878,7 @@ public void onMethodCall(@NonNull MethodCall call, // see: BmWriteCharacteristicRequest HashMap data = call.arguments(); String remoteId = (String) data.get("remote_id"); - String serviceUuid = (String) data.get("service_uuid"); - String characteristicUuid = (String) data.get("characteristic_uuid"); - String primaryServiceUuid = (String) data.get("primary_service_uuid"); + String identifier = (String) data.get("identifier"); String value = (String) data.get("value"); int writeTypeInt = (int) data.get("write_type"); boolean allowLongWrite = ((int) data.get("allow_long_write")) != 0; @@ -904,16 +898,15 @@ public void onMethodCall(@NonNull MethodCall call, waitIfBonding(); // find characteristic - ChrFound found = locateCharacteristic(gatt, serviceUuid, characteristicUuid, primaryServiceUuid); - if (found.error != null) { - result.error("writeCharacteristic", found.error, null); - break; - } - - BluetoothGattCharacteristic characteristic = found.characteristic; + BluetoothGattCharacteristic characteristic = locateCharacteristic(gatt, identifier); + // TODO CRITICAL catch exceptions +// if (found.error != null) { +// result.error("writeCharacteristic", found.error, null); +// break; +// } // check writeable - if(writeType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) { + if (writeType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) { if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) { result.error("writeCharacteristic", "The WRITE_NO_RESPONSE property is not supported by this BLE characteristic", null); @@ -939,13 +932,11 @@ public void onMethodCall(@NonNull MethodCall call, } // remember the data we are writing - if (primaryServiceUuid == null) {primaryServiceUuid = "";} - String key = remoteId + ":" + serviceUuid + ":" + characteristicUuid + ":" + primaryServiceUuid; + String key = remoteId + "/" + identifier; mWriteChr.put(key, value); // write characteristic if (Build.VERSION.SDK_INT >= 33) { // Android 13 (August 2022) - int rv = gatt.writeCharacteristic(characteristic, hexToBytes(value), writeType); if (rv != BluetoothStatusCodes.SUCCESS) { @@ -980,10 +971,7 @@ public void onMethodCall(@NonNull MethodCall call, // see: BmReadDescriptorRequest HashMap data = call.arguments(); String remoteId = (String) data.get("remote_id"); - String serviceUuid = (String) data.get("service_uuid"); - String characteristicUuid = (String) data.get("characteristic_uuid"); - String descriptorUuid = (String) data.get("descriptor_uuid"); - String primaryServiceUuid = (String) data.get("primary_service_uuid"); + String identifier = (String) data.get("identifier"); // check connection BluetoothGatt gatt = mConnectedDevices.get(remoteId); @@ -996,21 +984,17 @@ public void onMethodCall(@NonNull MethodCall call, waitIfBonding(); // find characteristic - ChrFound found = locateCharacteristic(gatt, serviceUuid, characteristicUuid, primaryServiceUuid); - if (found.error != null) { - result.error("readDescriptor", found.error, null); - break; - } - - BluetoothGattCharacteristic characteristic = found.characteristic; - - // find descriptor - BluetoothGattDescriptor descriptor = getDescriptorFromArray(descriptorUuid, characteristic.getDescriptors()); - if(descriptor == null) { - String s = "descriptor not found on characteristic. (desc: " + descriptorUuid + " chr: " + characteristicUuid + ")"; - result.error("writeDescriptor", s, null); - break; - } + BluetoothGattDescriptor descriptor = locateDescriptor(gatt, identifier); + // TODO CRITICAL catch exceptions +// if (found.error != null) { +// result.error("readDescriptor", found.error, null); +// break; +// } +// if(descriptor == null) { +// String s = "descriptor not found on characteristic. (desc: " + descriptorUuid + " chr: " + characteristicUuid + ")"; +// result.error("writeDescriptor", s, null); +// break; +// } // read descriptor if(gatt.readDescriptor(descriptor) == false) { @@ -1027,10 +1011,7 @@ public void onMethodCall(@NonNull MethodCall call, // see: BmWriteDescriptorRequest HashMap data = call.arguments(); String remoteId = (String) data.get("remote_id"); - String serviceUuid = (String) data.get("service_uuid"); - String characteristicUuid = (String) data.get("characteristic_uuid"); - String descriptorUuid = (String) data.get("descriptor_uuid"); - String primaryServiceUuid = (String) data.get("primary_service_uuid"); + String identifier = (String) data.get("identifier"); String value = (String) data.get("value"); // check connection @@ -1044,21 +1025,17 @@ public void onMethodCall(@NonNull MethodCall call, waitIfBonding(); // find characteristic - ChrFound found = locateCharacteristic(gatt, serviceUuid, characteristicUuid, primaryServiceUuid); - if (found.error != null) { - result.error("writeDescriptor", found.error, null); - break; - } - - BluetoothGattCharacteristic characteristic = found.characteristic; - - // find descriptor - BluetoothGattDescriptor descriptor = getDescriptorFromArray(descriptorUuid, characteristic.getDescriptors()); - if(descriptor == null) { - String s = "descriptor not found on characteristic. (desc: " + descriptorUuid + " chr: " + characteristicUuid + ")"; - result.error("writeDescriptor", s, null); - break; - } + BluetoothGattDescriptor descriptor = locateDescriptor(gatt, identifier); + // TODO CRITICAL catch exceptions +// if (found.error != null) { +// result.error("writeDescriptor", found.error, null); +// break; +// } +// if(descriptor == null) { +// String s = "descriptor not found on characteristic. (desc: " + descriptorUuid + " chr: " + characteristicUuid + ")"; +// result.error("writeDescriptor", s, null); +// break; +// } // check mtu int mtu = mMtu.get(remoteId); @@ -1070,22 +1047,18 @@ public void onMethodCall(@NonNull MethodCall call, } // remember the data we are writing - if (primaryServiceUuid == null) {primaryServiceUuid = "";} - String key = remoteId + ":" + serviceUuid + ":" + characteristicUuid + ":" + descriptorUuid + ":" + primaryServiceUuid; + String key = remoteId + "/" + identifier; mWriteDesc.put(key, value); // write descriptor if (Build.VERSION.SDK_INT >= 33) { // Android 13 (August 2022) - int rv = gatt.writeDescriptor(descriptor, hexToBytes(value)); if (rv != BluetoothStatusCodes.SUCCESS) { String s = "gatt.writeDescriptor() returned " + rv + " : " + bluetoothStatusString(rv); result.error("writeDescriptor", s, null); return; } - } else { - // Set descriptor if(!descriptor.setValue(hexToBytes(value))){ result.error("writeDescriptor", "descriptor.setValue() returned false", null); @@ -1108,9 +1081,7 @@ public void onMethodCall(@NonNull MethodCall call, // see: BmSetNotifyValueRequest HashMap data = call.arguments(); String remoteId = (String) data.get("remote_id"); - String serviceUuid = (String) data.get("service_uuid"); - String characteristicUuid = (String) data.get("characteristic_uuid"); - String primaryServiceUuid = (String) data.get("primary_service_uuid"); + String identifier = (String) data.get("identifier"); boolean forceIndications = (boolean) data.get("force_indications"); boolean enable = (boolean) data.get("enable"); @@ -1125,13 +1096,12 @@ public void onMethodCall(@NonNull MethodCall call, waitIfBonding(); // find characteristic - ChrFound found = locateCharacteristic(gatt, serviceUuid, characteristicUuid, primaryServiceUuid); - if (found.error != null) { - result.error("setNotifyValue", found.error, null); - break; - } - - BluetoothGattCharacteristic characteristic = found.characteristic; + BluetoothGattCharacteristic characteristic = locateCharacteristic(gatt, identifier); + // TODO CRITICAL catch exceptions +// if (found.error != null) { +// result.error("setNotifyValue", found.error, null); +// break; +// } // configure local Android device to listen for characteristic changes if(!gatt.setCharacteristicNotification(characteristic, enable)){ @@ -1183,22 +1153,18 @@ public void onMethodCall(@NonNull MethodCall call, } // remember the data we are writing - if (primaryServiceUuid == null) {primaryServiceUuid = "";} - String key = remoteId + ":" + serviceUuid + ":" + characteristicUuid + ":" + CCCD + ":" + primaryServiceUuid; + String key = remoteId + "/" + identifier; mWriteDesc.put(key, bytesToHex(descriptorValue)); // write descriptor if (Build.VERSION.SDK_INT >= 33) { // Android 13 (August 2022) - int rv = gatt.writeDescriptor(cccd, descriptorValue); if (rv != BluetoothStatusCodes.SUCCESS) { String s = "gatt.writeDescriptor() returned " + rv + " : " + bluetoothStatusString(rv); result.error("setNotifyValue", s, null); break; } - } else { - // set new value if (!cccd.setValue(descriptorValue)) { result.error("setNotifyValue", "cccd.setValue() returned false", null); @@ -1298,7 +1264,7 @@ public void onMethodCall(@NonNull MethodCall call, case "getPhySupport": { - if(Build.VERSION.SDK_INT < 26) { // Android 8.0 (August 2017) + if (Build.VERSION.SDK_INT < 26) { // Android 8.0 (August 2017) result.error("getPhySupport", "Only supported on devices >= API 26. This device == " + Build.VERSION.SDK_INT, null); @@ -1309,7 +1275,6 @@ public void onMethodCall(@NonNull MethodCall call, HashMap map = new HashMap<>(); map.put("le_2M", mBluetoothAdapter.isLe2MPhySupported()); map.put("le_coded", mBluetoothAdapter.isLeCodedPhySupported()); - result.success(map); break; } @@ -1606,85 +1571,88 @@ private void waitIfBonding() { } } - class ChrFound { - public BluetoothGattCharacteristic characteristic; - public String error; + private BluetoothGattService locateService(BluetoothGatt gatt, String identifier) { + return getServiceFromArray(identifier, gatt.getServices()); + } - public ChrFound(BluetoothGattCharacteristic characteristic, String error) { - this.characteristic = characteristic; - this.error = error; - } + private BluetoothGattCharacteristic locateCharacteristic(BluetoothGatt gatt, String identifier) throws IllegalArgumentException { + // Split into components + // 00000000-0000-0000-0000-000000000000:1234/00000000-0000-0000-0000-000000000000:1234 + String[] parts = identifier.split("/"); + if (parts.length != 2) throw new IllegalArgumentException("Invalid characteristic identifier: '" + identifier + "'"); + + BluetoothGattService service = locateService(gatt, parts[0]); + return getCharacteristicFromArray(parts[1], service.getCharacteristics()); } - private ChrFound locateCharacteristic(BluetoothGatt gatt, - String serviceUuid, - String characteristicUuid, - String primaryServiceUuid) - { - // remember this - boolean isSecondaryService = primaryServiceUuid != null; + private BluetoothGattDescriptor locateDescriptor(BluetoothGatt gatt, String identifier) throws IllegalArgumentException { + // Split into components + // 00000000-0000-0000-0000-000000000000:1234/00000000-0000-0000-0000-000000000000:1234/00000000-0000-0000-0000-000000000000:1234") + String[] parts = identifier.split("/"); + if (parts.length != 3) throw new IllegalArgumentException("Invalid descriptor identifier: '" + identifier + "'"); - // primary service - if (primaryServiceUuid == null) { - primaryServiceUuid = serviceUuid; - } + BluetoothGattCharacteristic characteristic = locateCharacteristic(gatt, parts[0] + "/" + parts[1]); + return getDescriptorFromArray(parts[2], characteristic.getDescriptors()); + } - // primary - BluetoothGattService primaryService = getServiceFromArray(primaryServiceUuid, gatt.getServices()); - if(primaryService == null) { - return new ChrFound(null, "primary service not found '" + primaryServiceUuid + "'"); - } + private BluetoothGattService getServiceFromArray(String identifier, List services) throws IllegalArgumentException { + // Split into components ("00000000-0000-0000-0000-000000000000:1234") + String[] parts = identifier.split(":"); + if (parts.length != 2) throw new IllegalArgumentException("Invalid service identifier: '" + identifier + "'"); + String uuid = uuid128(parts[0]); + int instanceId = Integer.parseInt(parts[1]); - // secondary - BluetoothGattService secondaryService = null; - if(isSecondaryService) { - secondaryService = getServiceFromArray(serviceUuid, primaryService.getIncludedServices()); - if(secondaryService == null) { - return new ChrFound(null, "secondary service not found '" + serviceUuid + "' (primary service '" + primaryServiceUuid + "')"); + for (BluetoothGattService service : services) { + if (uuid128(service.getUuid()).equals(uuid) && service.getInstanceId() == instanceId) { + return service; } } + throw new NoSuchElementException("Service not found: '" + identifier + "'"); + } - // which service? - BluetoothGattService service = (secondaryService != null) ? secondaryService : primaryService; + private BluetoothGattCharacteristic getCharacteristicFromArray(String identifier, List characteristics) throws IllegalArgumentException { + // Split into components ("00000000-0000-0000-0000-000000000000:1234") + String[] parts = identifier.split(":"); + if (parts.length != 2) throw new IllegalArgumentException("Invalid characteristic identifier: '" + identifier + "'"); + String uuid = uuid128(parts[0]); + int instanceId = Integer.parseInt(parts[1]); - // characteristic - BluetoothGattCharacteristic characteristic = getCharacteristicFromArray(characteristicUuid, service.getCharacteristics()); - if(characteristic == null) { - return new ChrFound(null, "characteristic not found in service " + - "(chr: '" + characteristicUuid + "' svc: '" + serviceUuid + "')"); + for (BluetoothGattCharacteristic characteristic : characteristics) { + if (uuid128(characteristic.getUuid()).equals(uuid) && characteristic.getInstanceId() == instanceId) { + return characteristic; + } } - - return new ChrFound(characteristic, null); + throw new NoSuchElementException("Characteristic not found: '" + identifier + "'"); } - private BluetoothGattService getServiceFromArray(String uuid, List array) - { - for (BluetoothGattService s : array) { - if (uuid128(s.getUuid()).equals(uuid128(uuid))) { - return s; + private BluetoothGattDescriptor getDescriptorFromArray(String uuid, List descriptors) { + uuid = uuid128(uuid); + for (BluetoothGattDescriptor descriptor : descriptors) { + if (uuid128(descriptor.getUuid()).equals(uuid)) { + return descriptor; } } - return null; + throw new NoSuchElementException("Descriptor not found: '" + uuid + "'"); } - private BluetoothGattCharacteristic getCharacteristicFromArray(String uuid, List array) - { - for (BluetoothGattCharacteristic c : array) { - if (uuid128(c.getUuid()).equals(uuid128(uuid))) { - return c; - } - } - return null; + private String serviceIdentifier(BluetoothGattService service) { + return uuidStr(service.getUuid()) + ":" + service.getInstanceId(); } - private BluetoothGattDescriptor getDescriptorFromArray(String uuid, List array) - { - for (BluetoothGattDescriptor d : array) { - if (uuid128(d.getUuid()).equals(uuid128(uuid))) { - return d; - } - } - return null; + private String characteristicIdentifier(BluetoothGattCharacteristic characteristic) { + return uuidStr(characteristic.getUuid()) + ":" + characteristic.getInstanceId(); + } + + private String descriptorIdentifier(BluetoothGattDescriptor descriptor) { + return uuidStr(descriptor.getUuid()); + } + + private String characteristicIdentifierPath(BluetoothGattCharacteristic characteristic) { + return serviceIdentifier(characteristic.getService()) + "/" + characteristicIdentifier(characteristic); + } + + private String descriptorIdentifierPath(BluetoothGattDescriptor descriptor) { + return characteristicIdentifierPath(descriptor.getCharacteristic()) + "/" + descriptorIdentifier(descriptor); } private boolean filterKeywords(List keywords, String target) { @@ -2076,6 +2044,7 @@ public void onScanResult(int callbackType, ScanResult result) // see BmScanResponse HashMap response = new HashMap<>(); response.put("advertisements", Arrays.asList(bmScanAdvertisement(device, result))); + response.put("success", 1); invokeMethodUIThread("OnScanResponse", response); } @@ -2252,10 +2221,7 @@ public void onServicesDiscovered(BluetoothGatt gatt, int status) List services = new ArrayList(); for(BluetoothGattService s : gatt.getServices()) { - services.add(bmBluetoothService(gatt.getDevice(), s, null, gatt)); - for(BluetoothGattService s2 : s.getIncludedServices()) { - services.add(bmBluetoothService(gatt.getDevice(), s2, s, gatt)); - } + services.add(bmBluetoothService(s)); } // see: BmDiscoverServicesResult @@ -2282,21 +2248,17 @@ public void onCharacteristicReceived(BluetoothGatt gatt, BluetoothGattCharacteri } } - // has associated primary service? - BluetoothGattService primaryService = getPrimaryService(gatt, characteristic); + // for convenience + String identifier = characteristicIdentifierPath(characteristic); // see: BmCharacteristicData HashMap response = new HashMap<>(); response.put("remote_id", gatt.getDevice().getAddress()); - response.put("service_uuid", uuidStr(characteristic.getService().getUuid())); - response.put("characteristic_uuid", uuidStr(characteristic.getUuid())); + response.put("identifier", identifier); response.put("value", bytesToHex(value)); response.put("success", status == BluetoothGatt.GATT_SUCCESS ? 1 : 0); response.put("error_code", status); response.put("error_string", gattErrorString(status)); - if (primaryService != null) { - response.put("primary_service_uuid", uuidStr(primaryService.getUuid())); - } invokeMethodUIThread("OnCharacteristicReceived", response); } @@ -2337,32 +2299,23 @@ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristi // in android's internal buffer. When the buffer is full, it delays calling onCharacteristicWrite // until there is at least ~50% free space again. - // has associated primary service? - BluetoothGattService primaryService = getPrimaryService(gatt, characteristic); - // for convenience String remoteId = gatt.getDevice().getAddress(); - String serviceUuid = uuidStr(characteristic.getService().getUuid()); - String characteristicUuid = uuidStr(characteristic.getUuid()); - String primaryServiceUuid = primaryService != null ? uuidStr(primaryService.getUuid()) : ""; + String identifier = characteristicIdentifierPath(characteristic); // what data did we write? - String key = remoteId + ":" + serviceUuid + ":" + characteristicUuid + ":" + primaryServiceUuid; + String key = remoteId + ":" + identifier; String value = mWriteChr.get(key) != null ? mWriteChr.get(key) : ""; mWriteChr.remove(key); // see: BmCharacteristicData HashMap response = new HashMap<>(); response.put("remote_id", remoteId); - response.put("service_uuid", serviceUuid); - response.put("characteristic_uuid", characteristicUuid); + response.put("identifier", identifier); response.put("value", value); response.put("success", status == BluetoothGatt.GATT_SUCCESS ? 1 : 0); response.put("error_code", status); response.put("error_string", gattErrorString(status)); - if (primaryService != null) { - response.put("primary_service_uuid", uuidStr(primaryService.getUuid())); - } invokeMethodUIThread("OnCharacteristicWritten", response); } @@ -2377,22 +2330,17 @@ public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descrip log(level, " desc: " + uuidStr(descriptor.getUuid())); log(level, " status: " + gattErrorString(status) + " (" + status + ")"); - // has associated primary service? - BluetoothGattService primaryService = getPrimaryService(gatt, descriptor.getCharacteristic()); + // for convenience + String identifier = descriptorIdentifierPath(descriptor); // see: BmDescriptorData HashMap response = new HashMap<>(); response.put("remote_id", gatt.getDevice().getAddress()); - response.put("service_uuid", uuidStr(descriptor.getCharacteristic().getService().getUuid())); - response.put("characteristic_uuid", uuidStr(descriptor.getCharacteristic().getUuid())); - response.put("descriptor_uuid", uuidStr(descriptor.getUuid())); + response.put("identifier", identifier); response.put("value", bytesToHex(value)); response.put("success", status == BluetoothGatt.GATT_SUCCESS ? 1 : 0); response.put("error_code", status); response.put("error_string", gattErrorString(status)); - if (primaryService != null) { - response.put("primary_service_uuid", uuidStr(primaryService.getUuid())); - } invokeMethodUIThread("OnDescriptorRead", response); } @@ -2406,34 +2354,23 @@ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descri log(level, " desc: " + uuidStr(descriptor.getUuid())); log(level, " status: " + gattErrorString(status) + " (" + status + ")"); - // has associated primary service? - BluetoothGattService primaryService = getPrimaryService(gatt, descriptor.getCharacteristic()); - // for convenience String remoteId = gatt.getDevice().getAddress(); - String serviceUuid = uuidStr(descriptor.getCharacteristic().getService().getUuid()); - String characteristicUuid = uuidStr(descriptor.getCharacteristic().getUuid()); - String descriptorUuid = uuidStr(descriptor.getUuid()); - String primaryServiceUuid = primaryService != null ? uuidStr(primaryService.getUuid()) : ""; + String identifier = descriptorIdentifierPath(descriptor); // what data did we write? - String key = remoteId + ":" + serviceUuid + ":" + characteristicUuid + ":" + descriptorUuid + ":" + primaryServiceUuid; + String key = remoteId + "/" + identifier; String value = mWriteDesc.get(key) != null ? mWriteDesc.get(key) : ""; mWriteDesc.remove(key); // see: BmDescriptorData HashMap response = new HashMap<>(); response.put("remote_id", remoteId); - response.put("service_uuid", serviceUuid); - response.put("characteristic_uuid", characteristicUuid); - response.put("descriptor_uuid", descriptorUuid); + response.put("identifier", identifier); response.put("value", value); response.put("success", status == BluetoothGatt.GATT_SUCCESS ? 1 : 0); response.put("error_code", status); response.put("error_string", gattErrorString(status)); - if (primaryService != null) { - response.put("primary_service_uuid", uuidStr(primaryService.getUuid())); - } invokeMethodUIThread("OnDescriptorWritten", response); } @@ -2607,66 +2544,47 @@ HashMap bmBluetoothDevice(BluetoothDevice device) { return map; } - HashMap bmBluetoothService( - BluetoothDevice device, - BluetoothGattService service, - BluetoothGattService primaryService, - BluetoothGatt gatt) - { + HashMap bmBluetoothService(BluetoothGattService service) { List characteristics = new ArrayList(); - for(BluetoothGattCharacteristic c : service.getCharacteristics()) { - characteristics.add(bmBluetoothCharacteristic(device, c, gatt)); + for (BluetoothGattCharacteristic c : service.getCharacteristics()) { + characteristics.add(bmBluetoothCharacteristic(c)); + } + + List includedServices = new ArrayList(); + for (BluetoothGattService s : service.getIncludedServices()) { + includedServices.add(serviceIdentifier(s)); } // See: BmBluetoothService HashMap map = new HashMap<>(); - map.put("remote_id", device.getAddress()); - map.put("service_uuid", uuidStr(service.getUuid())); + map.put("uuid", uuidStr(service.getUuid())); + map.put("index", service.getInstanceId()); + map.put("is_primary", service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY ? 1 : 0); map.put("characteristics", characteristics); - if (primaryService != null) { - map.put("primary_service_uuid", uuidStr(primaryService.getUuid())); - } + map.put("included_services", includedServices); return map; } - HashMap bmBluetoothCharacteristic(BluetoothDevice device, BluetoothGattCharacteristic characteristic, BluetoothGatt gatt) { - + HashMap bmBluetoothCharacteristic(BluetoothGattCharacteristic characteristic) { List descriptors = new ArrayList(); for(BluetoothGattDescriptor d : characteristic.getDescriptors()) { - descriptors.add(bmBluetoothDescriptor(device, d, gatt)); + descriptors.add(bmBluetoothDescriptor(d)); } - // has associated primary service? - BluetoothGattService primaryService = getPrimaryService(gatt, characteristic); - // See: BmBluetoothCharacteristic HashMap map = new HashMap<>(); - map.put("remote_id", device.getAddress()); - map.put("service_uuid", uuidStr(characteristic.getService().getUuid())); - map.put("characteristic_uuid", uuidStr(characteristic.getUuid())); + map.put("uuid", uuidStr(characteristic.getUuid())); + map.put("index", characteristic.getInstanceId()); map.put("descriptors", descriptors); map.put("properties", bmCharacteristicProperties(characteristic.getProperties())); - if (primaryService != null) { - map.put("primary_service_uuid", uuidStr(primaryService.getUuid())); - } return map; } // See: BmBluetoothDescriptor - HashMap bmBluetoothDescriptor(BluetoothDevice device, BluetoothGattDescriptor descriptor, BluetoothGatt gatt) { - - // has associated primary service? - BluetoothGattService primaryService = getPrimaryService(gatt, descriptor.getCharacteristic()); - + HashMap bmBluetoothDescriptor(BluetoothGattDescriptor descriptor) { HashMap map = new HashMap<>(); - map.put("remote_id", device.getAddress()); - map.put("descriptor_uuid", uuidStr(descriptor.getUuid())); - map.put("characteristic_uuid", uuidStr(descriptor.getCharacteristic().getUuid())); - map.put("service_uuid", uuidStr(descriptor.getCharacteristic().getService().getUuid())); - if (primaryService != null) { - map.put("primary_service_uuid", uuidStr(primaryService.getUuid())); - } + map.put("uuid", uuidStr(descriptor.getUuid())); return map; } diff --git a/ios/Classes/FlutterBluePlusPlugin.m b/ios/Classes/FlutterBluePlusPlugin.m index c4f407f0..9ea61543 100644 --- a/ios/Classes/FlutterBluePlusPlugin.m +++ b/ios/Classes/FlutterBluePlusPlugin.m @@ -35,6 +35,7 @@ @interface FlutterBluePlusPlugin () @property(nonatomic) NSMutableDictionary *knownPeripherals; @property(nonatomic) NSMutableDictionary *connectedPeripherals; @property(nonatomic) NSMutableDictionary *currentlyConnectingPeripherals; +@property(nonatomic) NSMutableDictionary *peripheralServices; @property(nonatomic) NSMutableArray *servicesToDiscover; @property(nonatomic) NSMutableArray *characteristicsToDiscover; @property(nonatomic) NSMutableDictionary *didWriteWithoutResponse; @@ -59,6 +60,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar instance.knownPeripherals = [NSMutableDictionary new]; instance.connectedPeripherals = [NSMutableDictionary new]; instance.currentlyConnectingPeripherals = [NSMutableDictionary new]; + instance.peripheralServices = [NSMutableDictionary new]; instance.servicesToDiscover = [NSMutableArray new]; instance.characteristicsToDiscover = [NSMutableArray new]; instance.didWriteWithoutResponse = [NSMutableDictionary new]; @@ -449,9 +451,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // See BmReadCharacteristicRequest NSDictionary *args = (NSDictionary*)call.arguments; NSString *remoteId = args[@"remote_id"]; - NSString *serviceUuid = args[@"service_uuid"]; - NSString *characteristicUuid = args[@"characteristic_uuid"]; - NSString *primaryServiceUuid = args[@"primary_service_uuid"]; + NSString *identifier = args[@"identifier"]; // Find peripheral CBPeripheral *peripheral = [self getConnectedPeripheral:remoteId]; @@ -463,10 +463,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // Find characteristic NSError *error = nil; - CBCharacteristic *characteristic = [self locateCharacteristic:characteristicUuid + CBCharacteristic *characteristic = [self locateCharacteristic:identifier peripheral:peripheral - serviceUuid:serviceUuid - primaryServiceUuid:primaryServiceUuid error:&error]; if (characteristic == nil) { result([FlutterError errorWithCode:@"readCharacteristic" message:error.localizedDescription details:NULL]); @@ -490,9 +488,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // See BmWriteCharacteristicRequest NSDictionary *args = (NSDictionary*)call.arguments; NSString *remoteId = args[@"remote_id"]; - NSString *serviceUuid = args[@"service_uuid"]; - NSString *characteristicUuid = args[@"characteristic_uuid"]; - NSString *primaryServiceUuid = args[@"primary_service_uuid"]; + NSString *identifier = args[@"identifier"]; NSNumber *writeTypeNumber = args[@"write_type"]; NSNumber *allowLongWrite = args[@"allow_long_write"]; NSString *value = args[@"value"]; @@ -534,10 +530,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // Find characteristic NSError *error = nil; - CBCharacteristic *characteristic = [self locateCharacteristic:characteristicUuid + CBCharacteristic *characteristic = [self locateCharacteristic:identifier peripheral:peripheral - serviceUuid:serviceUuid - primaryServiceUuid:primaryServiceUuid error:&error]; if (characteristic == nil) { result([FlutterError errorWithCode:@"writeCharacteristic" message:error.localizedDescription details:NULL]); @@ -560,8 +554,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } // remember the data we are writing - NSString *key = [NSString stringWithFormat:@"%@:%@:%@:%@", remoteId, serviceUuid, characteristicUuid, - primaryServiceUuid != nil ? primaryServiceUuid : @""]; + NSString *key = [NSString stringWithFormat:@"%@/%@", remoteId, [self characteristicIdentifierPath:characteristic]]; [self.writeChrs setObject:value forKey:key]; // Write to characteristic @@ -579,10 +572,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // See BmReadDescriptorRequest NSDictionary *args = (NSDictionary*)call.arguments; NSString *remoteId = args[@"remote_id"]; - NSString *serviceUuid = args[@"service_uuid"]; - NSString *characteristicUuid = args[@"characteristic_uuid"]; - NSString *descriptorUuid = args[@"descriptor_uuid"]; - NSString *primaryServiceUuid = args[@"primary_service_uuid"]; + NSString *identifier = args[@"identifier"]; // Find peripheral CBPeripheral *peripheral = [self getConnectedPeripheral:remoteId]; @@ -592,20 +582,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result return; } - // Find characteristic - NSError *error = nil; - CBCharacteristic *characteristic = [self locateCharacteristic:characteristicUuid - peripheral:peripheral - serviceUuid:serviceUuid - primaryServiceUuid:primaryServiceUuid - error:&error]; - if (characteristic == nil) { - result([FlutterError errorWithCode:@"readDescriptor" message:error.localizedDescription details:NULL]); - return; - } - // Find descriptor - CBDescriptor *descriptor = [self locateDescriptor:descriptorUuid characteristic:characteristic error:&error]; + NSError *error = nil; + CBDescriptor *descriptor = [self locateDescriptor:identifier peripheral:peripheral error:&error]; if (descriptor == nil) { result([FlutterError errorWithCode:@"readDescriptor" message:error.localizedDescription details:NULL]); return; @@ -620,10 +599,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // See BmWriteDescriptorRequest NSDictionary *args = (NSDictionary*)call.arguments; NSString *remoteId = args[@"remote_id"]; - NSString *serviceUuid = args[@"service_uuid"]; - NSString *characteristicUuid = args[@"characteristic_uuid"]; - NSString *descriptorUuid = args[@"descriptor_uuid"]; - NSString *primaryServiceUuid = args[@"primary_service_uuid"]; + NSString *identifier = args[@"identifier"]; NSString *value = args[@"value"]; // Find peripheral @@ -644,27 +620,15 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result return; } - // Find characteristic - NSError *error = nil; - CBCharacteristic *characteristic = [self locateCharacteristic:characteristicUuid - peripheral:peripheral - serviceUuid:serviceUuid - primaryServiceUuid:primaryServiceUuid - error:&error]; - if (characteristic == nil) { - result([FlutterError errorWithCode:@"writeDescriptor" message:error.localizedDescription details:NULL]); - return; - } - // Find descriptor - CBDescriptor *descriptor = [self locateDescriptor:descriptorUuid characteristic:characteristic error:&error]; + NSError *error = nil; + CBDescriptor *descriptor = [self locateDescriptor:identifier peripheral:peripheral error:&error]; if (descriptor == nil) { result([FlutterError errorWithCode:@"writeDescriptor" message:error.localizedDescription details:NULL]); return; } // remember the data we are writing - NSString *key = [NSString stringWithFormat:@"%@:%@:%@:%@:%@", remoteId, serviceUuid, characteristicUuid, - descriptorUuid, primaryServiceUuid != nil ? primaryServiceUuid : @""]; + NSString *key = [NSString stringWithFormat:@"%@/%@", remoteId, [self descriptorIdentifierPath:descriptor]]; [self.writeDescs setObject:value forKey:key]; // Write descriptor @@ -677,9 +641,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // See BmSetNotifyValueRequest NSDictionary *args = (NSDictionary*)call.arguments; NSString *remoteId = args[@"remote_id"]; - NSString *serviceUuid = args[@"service_uuid"]; - NSString *characteristicUuid = args[@"characteristic_uuid"]; - NSString *primaryServiceUuid = args[@"primary_service_uuid"]; + NSString *identifier = args[@"identifier"]; NSNumber *enable = args[@"enable"]; // Find peripheral @@ -692,10 +654,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // Find characteristic NSError *error = nil; - CBCharacteristic *characteristic = [self locateCharacteristic:characteristicUuid + CBCharacteristic *characteristic = [self locateCharacteristic:identifier peripheral:peripheral - serviceUuid:serviceUuid - primaryServiceUuid:primaryServiceUuid error:&error]; if (characteristic == nil) { result([FlutterError errorWithCode:@"setNotifyValue" message:error.localizedDescription details:NULL]); @@ -712,9 +672,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } // Check that CCCD is found, this is necessary for subscribing - CBDescriptor *descriptor = [self locateDescriptor:CCCD characteristic:characteristic error:nil]; + CBDescriptor *descriptor = [self getDescriptorFromArray:CCCD array:[characteristic descriptors]]; if (descriptor == nil) { - Log(LWARNING, @"Warning: CCCD descriptor for characteristic not found: %@", characteristicUuid); + Log(LWARNING, @"Warning: CCCD descriptor for characteristic not found: %@", [characteristic.UUID uuidStr]); } // Set notification value @@ -818,79 +778,102 @@ - (CBPeripheral *)getConnectedPeripheral:(NSString *)remoteId return [self.connectedPeripherals objectForKey:remoteId]; } -- (CBCharacteristic *)locateCharacteristic:(NSString *)characteristicId - peripheral:(CBPeripheral *)peripheral - serviceUuid:(NSString *)serviceUuid - primaryServiceUuid:(NSString *)primaryServiceUuid - error:(NSError **)error +- (CBService *)locateService:(NSString *)identifier peripheral:(CBPeripheral *)peripheral error:(NSError **)error { - // remember - bool isSecondaryService = primaryServiceUuid != nil; - - // primary service - if (primaryServiceUuid == nil) { - primaryServiceUuid = serviceUuid; - } - - // primary service - CBService *primaryService = [self getServiceFromArray:primaryServiceUuid array:[peripheral services]]; - if (primaryService == nil || !primaryService.isPrimary) + // service + CBService *service = [self getServiceFromArray:identifier array:[peripheral services]]; + if (service == nil) { - NSString* s = [NSString stringWithFormat:@"primary service not found '%@'", primaryServiceUuid]; + NSString* format = @"service not found in peripheral (svc: '%@')"; + NSString* s = [NSString stringWithFormat:format, identifier]; NSDictionary* d = @{NSLocalizedDescriptionKey : s}; - *error = [NSError errorWithDomain:@"flutterBluePlus" code:1000 userInfo:d]; - return nil; + *error = [NSError errorWithDomain:@"flutterBluePlus" code:1002 userInfo:d]; } + return service; +} - // associated primary service - CBService *secondaryService = nil; - if (isSecondaryService) - { - secondaryService = [self getServiceFromArray:serviceUuid array:[primaryService includedServices]]; - if (error && !secondaryService) { - NSString* s = [NSString stringWithFormat:@"secondary service not found '%@' (primary service %@)", serviceUuid, primaryServiceUuid]; - NSDictionary* d = @{NSLocalizedDescriptionKey : s}; - *error = [NSError errorWithDomain:@"flutterBluePlus" code:1001 userInfo:d]; - return nil; +- (CBCharacteristic *)locateCharacteristic:(NSString *)identifier + peripheral:(CBPeripheral *)peripheral + error:(NSError **)error +{ + // split identifier path + // 00000000-0000-0000-0000-000000000000:12345678/00000000-0000-0000-0000-000000000000:12345678 + NSArray *parts = [identifier componentsSeparatedByString:@"/"]; + if (parts.count < 2) { + if (error != nil) { + NSString *format = @"invalid characteristic identifier (chr: '%@')"; + NSString *s = [NSString stringWithFormat:format, identifier]; + NSDictionary *d = @{NSLocalizedDescriptionKey: s}; + *error = [NSError errorWithDomain:@"flutterBluePlus" code:1002 userInfo:d]; } + return nil; } - // which service? - CBService *service = (secondaryService != nil) ? secondaryService : primaryService; + NSString *serviceIdentifier = parts[0]; + NSString *characteristicIdentifier = parts[1]; + + // service + CBService *service = [self locateService:serviceIdentifier peripheral:peripheral error:error]; + if (service == nil) return nil; // characteristic - CBCharacteristic *characteristic = [self getCharacteristicFromArray:characteristicId array:[service characteristics]]; + CBCharacteristic *characteristic = [self getCharacteristicFromArray:characteristicIdentifier array:[service characteristics]]; if (characteristic == nil) { NSString* format = @"characteristic not found in service (chr: '%@', svc: '%@')"; - NSString* s = [NSString stringWithFormat:format, characteristicId, serviceUuid]; + NSString* s = [NSString stringWithFormat:format, characteristicIdentifier, serviceIdentifier]; NSDictionary* d = @{NSLocalizedDescriptionKey : s}; *error = [NSError errorWithDomain:@"flutterBluePlus" code:1002 userInfo:d]; - return nil; } return characteristic; } -- (CBDescriptor *)locateDescriptor:(NSString *)descriptorId characteristic:(CBCharacteristic *)characteristic error:(NSError**)error +- (CBDescriptor *)locateDescriptor:(NSString *)identifier peripheral:(CBPeripheral *)peripheral error:(NSError**)error { - CBDescriptor *descriptor = [self getDescriptorFromArray:descriptorId array:[characteristic descriptors]]; + // split identifier path + // 00000000-0000-0000-0000-000000000000:12345678/00000000-0000-0000-0000-000000000000:12345678/00000000-0000-0000-0000-000000000000:12345678 + NSArray *parts = [identifier componentsSeparatedByString:@"/"]; + if (parts.count < 3) { + if (error != nil) { + NSString *format = @"invalid descriptor identifier (desc: '%@')"; + NSString *s = [NSString stringWithFormat:format, identifier]; + NSDictionary *d = @{NSLocalizedDescriptionKey: s}; + *error = [NSError errorWithDomain:@"flutterBluePlus" code:1002 userInfo:d]; + } + return nil; + } + + NSString *characteristicIdentifierPath = [NSString stringWithFormat:@"%@/%@", parts[0], parts[1]]; + NSString *descriptorIdentifier = parts[2]; + + // characteristic + CBCharacteristic *characteristic = [self locateCharacteristic:characteristicIdentifierPath peripheral:peripheral error:error]; + if (characteristic == nil) return nil; + + CBDescriptor *descriptor = [self getDescriptorFromArray:descriptorIdentifier array:[characteristic descriptors]]; if (descriptor == nil && error != nil) { NSString* format = @"descriptor not found in characteristic (desc: '%@', chr: '%@')"; - NSString* s = [NSString stringWithFormat:format, descriptorId, [characteristic.UUID uuidStr]]; + NSString* s = [NSString stringWithFormat:format, descriptorIdentifier, [characteristic.UUID uuidStr]]; NSDictionary* d = @{NSLocalizedDescriptionKey : s}; *error = [NSError errorWithDomain:@"flutterBluePlus" code:1002 userInfo:d]; - return nil; } return descriptor; } -- (CBService *)getServiceFromArray:(NSString *)uuid array:(NSArray *)array +- (CBService *)getServiceFromArray:(NSString *)identifier array:(NSArray *)array { + // split identifier (00000000-0000-0000-0000-000000000000:12345678) to uuid and pointer (parse int) + NSArray *parts = [identifier componentsSeparatedByString:@":"]; + if (parts.count < 1) return nil; + NSString *uuid = parts[0]; + NSString *index = parts.count > 1 ? parts[1] : nil; + NSNumber *pointer = index == nil ? nil : [NSNumber numberWithLongLong: [index longLongValue]]; + for (CBService *s in array) { - if ([s.UUID isEqual:[CBUUID UUIDWithString:uuid]]) + if ([s.UUID isEqual:[CBUUID UUIDWithString:uuid]] && (pointer == nil || [pointer unsignedLongValue] == (uintptr_t)s)) { return s; } @@ -898,11 +881,18 @@ - (CBService *)getServiceFromArray:(NSString *)uuid array:(NSArray return nil; } -- (CBCharacteristic *)getCharacteristicFromArray:(NSString *)uuid array:(NSArray *)array +- (CBCharacteristic *)getCharacteristicFromArray:(NSString *)identifier array:(NSArray *)array { + // split identifier (00000000-0000-0000-0000-000000000000:12345678) to uuid and pointer (parse int) + NSArray *parts = [identifier componentsSeparatedByString:@":"]; + if (parts.count < 1) return nil; + NSString *uuid = parts[0]; + NSString *index = parts.count > 1 ? parts[1] : nil; + NSNumber *pointer = index == nil ? nil : [NSNumber numberWithLongLong: [index longLongValue]]; + for (CBCharacteristic *c in array) { - if ([c.UUID isEqual:[CBUUID UUIDWithString:uuid]]) + if ([c.UUID isEqual:[CBUUID UUIDWithString:uuid]] && (pointer == nil || [pointer unsignedLongValue] == (uintptr_t)c)) { return c; } @@ -922,6 +912,31 @@ - (CBDescriptor *)getDescriptorFromArray:(NSString *)uuid array:(NSArray device.remoteId; + + @Deprecated('Use remoteId instead') + DeviceIdentifier get deviceId => remoteId; + + BluetoothAttribute? get _parentAttribute => null; + + String get identifier => "$uuid:$index"; + + String get identifierPath => + _parentAttribute != null ? "${_parentAttribute!.identifierPath}/$identifier" : identifier; +} + +abstract class BluetoothValueAttribute extends BluetoothAttribute { + List _lastValue = []; + + BluetoothValueAttribute({ + required BluetoothDevice device, + required Guid uuid, + int? index, + }) : super(device: device, uuid: uuid, index: index); + + /// this variable is updated: + /// - anytime `read()` is called + /// - anytime `write()` is called + /// - anytime a notification arrives (characteristics, if subscribed) + /// - when the device is disconnected it is cleared + List get lastValue => _lastValue; + + /// this stream emits values: + /// - anytime `read()` is called + /// - anytime `write()` is called + /// - anytime a notification arrives (characteristics, if subscribed) + /// - and when first listened to, it re-emits the last value for convenience + Stream> get lastValueStream => FlutterBluePlus._methodStream.stream + .where((e) => + e is OnCharacteristicReceivedEvent || + e is OnCharacteristicWrittenEvent || + e is OnDescriptorReadEvent || + e is OnDescriptorWrittenEvent) + .map((e) => e as GetAttributeValueMixin) + .where((e) => e.attribute == this) + .map((e) => e.value) + .newStreamWithInitialValue(lastValue); + + /// this stream emits values: + /// - anytime `read()` is called + /// - anytime a notification arrives (if subscribed) + Stream> get onValueReceived => FlutterBluePlus._methodStream.stream + .where((e) => e is OnCharacteristicReceivedEvent || e is OnDescriptorReadEvent) + .map((e) => e as GetAttributeValueMixin) + .where((e) => e.attribute == this) + .map((e) => e.value); + + @Deprecated('Use onValueReceived instead') + Stream> get value => onValueReceived; + + @Deprecated('Use onValueReceived instead') + Stream> get onValueChangedStream => onValueReceived; +} diff --git a/lib/src/bluetooth_characteristic.dart b/lib/src/bluetooth_characteristic.dart index 2257145a..357d1285 100644 --- a/lib/src/bluetooth_characteristic.dart +++ b/lib/src/bluetooth_characteristic.dart @@ -6,92 +6,34 @@ part of flutter_blue_plus; final Guid cccdUuid = Guid("00002902-0000-1000-8000-00805f9b34fb"); -class BluetoothCharacteristic { - final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid? primaryServiceUuid; - - BluetoothCharacteristic({ - required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - this.primaryServiceUuid, - }); - - BluetoothCharacteristic.fromProto(BmBluetoothCharacteristic p) - : remoteId = p.remoteId, - serviceUuid = p.serviceUuid, - characteristicUuid = p.characteristicUuid, - primaryServiceUuid = p.primaryServiceUuid; +class BluetoothCharacteristic extends BluetoothValueAttribute { + final BluetoothService service; + final CharacteristicProperties properties; + late final List descriptors; + + BluetoothCharacteristic.fromProto(BmBluetoothCharacteristic p, BluetoothService service) + : service = service, + properties = CharacteristicProperties.fromProto(p.properties), + super(device: service.device, uuid: p.uuid, index: p.index) { + descriptors = p.descriptors.map((d) => BluetoothDescriptor.fromProto(d, this)).toList(); + } - /// convenience accessor - Guid get uuid => characteristicUuid; + @override + BluetoothAttribute? get _parentAttribute => service; /// convenience accessor - BluetoothDevice get device => BluetoothDevice(remoteId: remoteId); - - /// Get Properties from known services - CharacteristicProperties get properties { - return _bmchr != null ? CharacteristicProperties.fromProto(_bmchr!.properties) : CharacteristicProperties(); - } + Guid get characteristicUuid => uuid; - /// Get Descriptors from known services - List get descriptors { - return _bmchr != null ? _bmchr!.descriptors.map((d) => BluetoothDescriptor.fromProto(d)).toList() : []; - } - - /// this variable is updated: - /// - anytime `read()` is called - /// - anytime `write()` is called - /// - anytime a notification arrives (if subscribed) - /// - when the device is disconnected it is cleared - List get lastValue { - String key = "$serviceUuid:$characteristicUuid"; - return FlutterBluePlus._lastChrs[remoteId]?[key] ?? []; + /// convenience accessor + BluetoothDescriptor? get cccd { + return descriptors._firstWhereOrNull((d) => d.uuid == cccdUuid); } - /// this stream emits values: - /// - anytime `read()` is called - /// - anytime `write()` is called - /// - anytime a notification arrives (if subscribed) - /// - and when first listened to, it re-emits the last value for convenience - Stream> get lastValueStream => FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnCharacteristicReceived" || m.method == "OnCharacteristicWritten") - .map((m) => m.arguments) - .map((args) => BmCharacteristicData.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .where((p) => p.serviceUuid == serviceUuid) - .where((p) => p.characteristicUuid == characteristicUuid) - .where((p) => p.primaryServiceUuid == primaryServiceUuid) - .where((p) => p.success == true) - .map((c) => c.value) - .newStreamWithInitialValue(lastValue); - - /// this stream emits values: - /// - anytime `read()` is called - /// - anytime a notification arrives (if subscribed) - Stream> get onValueReceived => FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnCharacteristicReceived") - .map((m) => m.arguments) - .map((args) => BmCharacteristicData.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .where((p) => p.serviceUuid == serviceUuid) - .where((p) => p.characteristicUuid == characteristicUuid) - .where((p) => p.primaryServiceUuid == primaryServiceUuid) - .where((p) => p.success == true) - .map((c) => c.value); - /// return true if we're subscribed to this characteristic /// - you can subscribe using setNotifyValue(true) bool get isNotifying { - var cccd = descriptors._firstWhereOrNull((d) => d.descriptorUuid == cccdUuid); - if (cccd == null) { - return false; - } - var hasNotify = cccd.lastValue.isNotEmpty && (cccd.lastValue[0] & 0x01) > 0; - var hasIndicate = cccd.lastValue.isNotEmpty && (cccd.lastValue[0] & 0x02) > 0; - return hasNotify || hasIndicate; + List lastValue = cccd?.lastValue ?? []; + return lastValue.isNotEmpty && (lastValue[0] & 0x03) > 0; } /// read a characteristic @@ -106,50 +48,33 @@ class BluetoothCharacteristic { _Mutex mtx = _MutexFactory.getMutexForKey("global"); await mtx.take(); - // return value - List responseValue = []; - try { var request = BmReadCharacteristicRequest( remoteId: remoteId, - characteristicUuid: characteristicUuid, - serviceUuid: serviceUuid, - primaryServiceUuid: primaryServiceUuid, + identifier: identifierPath, ); - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnCharacteristicReceived") - .map((m) => m.arguments) - .map((args) => BmCharacteristicData.fromMap(args)) - .where((p) => p.remoteId == request.remoteId) - .where((p) => p.serviceUuid == request.serviceUuid) - .where((p) => p.characteristicUuid == request.characteristicUuid) - .where((p) => p.primaryServiceUuid == request.primaryServiceUuid); - - // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; - // invoke - await FlutterBluePlus._invokeMethod('readCharacteristic', request.toMap()); + final futureResponse = FlutterBluePlus._invokeMethodAndWaitForEvent( + 'readCharacteristic', + request.toMap(), + (e) => e.characteristic == this, + ); // wait for response - BmCharacteristicData response = await futureResponse + OnCharacteristicReceivedEvent response = await futureResponse .fbpEnsureAdapterIsOn("readCharacteristic") .fbpEnsureDeviceIsConnected(device, "readCharacteristic") .fbpTimeout(timeout, "readCharacteristic"); // failed? - if (!response.success) { - throw FlutterBluePlusException(_nativeError, "readCharacteristic", response.errorCode, response.errorString); - } + response.ensureSuccess('readCharacteristic'); // set return value - responseValue = response.value; + return response.value; } finally { mtx.give(); } - - return responseValue; } /// Writes a characteristic. @@ -184,41 +109,29 @@ class BluetoothCharacteristic { var request = BmWriteCharacteristicRequest( remoteId: remoteId, - characteristicUuid: characteristicUuid, - serviceUuid: serviceUuid, + identifier: identifierPath, writeType: writeType, allowLongWrite: allowLongWrite, value: value, - primaryServiceUuid: primaryServiceUuid, ); - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnCharacteristicWritten") - .map((m) => m.arguments) - .map((args) => BmCharacteristicData.fromMap(args)) - .where((p) => p.remoteId == request.remoteId) - .where((p) => p.serviceUuid == request.serviceUuid) - .where((p) => p.characteristicUuid == request.characteristicUuid) - .where((p) => p.primaryServiceUuid == request.primaryServiceUuid); - - // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; - // invoke - await FlutterBluePlus._invokeMethod('writeCharacteristic', request.toMap()); + final futureResponse = FlutterBluePlus._invokeMethodAndWaitForEvent( + 'writeCharacteristic', + request.toMap(), + (e) => e.characteristic == this, + ); // wait for response so that we can: // 1. check for success (writeWithResponse) // 2. wait until the packet has been sent, to prevent iOS & Android dropping packets (writeWithoutResponse) - BmCharacteristicData response = await futureResponse + OnCharacteristicWrittenEvent response = await futureResponse .fbpEnsureAdapterIsOn("writeCharacteristic") .fbpEnsureDeviceIsConnected(device, "writeCharacteristic") .fbpTimeout(timeout, "writeCharacteristic"); // failed? - if (!response.success) { - throw FlutterBluePlusException(_nativeError, "writeCharacteristic", response.errorCode, response.errorString); - } + response.ensureSuccess('writeCharacteristic'); return Future.value(); } finally { @@ -249,42 +162,31 @@ class BluetoothCharacteristic { try { var request = BmSetNotifyValueRequest( remoteId: remoteId, - serviceUuid: serviceUuid, - characteristicUuid: characteristicUuid, + identifier: identifierPath, forceIndications: forceIndications, enable: notify, - primaryServiceUuid: primaryServiceUuid, ); // Notifications & Indications are configured by writing to the // Client Characteristic Configuration Descriptor (CCCD) - Stream responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDescriptorWritten") - .map((m) => m.arguments) - .map((args) => BmDescriptorData.fromMap(args)) - .where((p) => p.remoteId == request.remoteId) - .where((p) => p.serviceUuid == request.serviceUuid) - .where((p) => p.characteristicUuid == request.characteristicUuid) - .where((p) => p.descriptorUuid == cccdUuid) - .where((p) => p.primaryServiceUuid == request.primaryServiceUuid); + Stream responseStream = + FlutterBluePlus._extractEventStream((m) => m.descriptor == cccd); // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; + Future futureResponse = responseStream.first; // invoke bool hasCCCD = await FlutterBluePlus._invokeMethod('setNotifyValue', request.toMap()); // wait for CCCD descriptor to be written? if (hasCCCD) { - BmDescriptorData response = await futureResponse + OnDescriptorWrittenEvent response = await futureResponse .fbpEnsureAdapterIsOn("setNotifyValue") .fbpEnsureDeviceIsConnected(device, "setNotifyValue") .fbpTimeout(timeout, "setNotifyValue"); // failed? - if (!response.success) { - throw FlutterBluePlusException(_nativeError, "setNotifyValue", response.errorCode, response.errorString); - } + response.ensureSuccess("setNotifyValue"); } } finally { mtx.give(); @@ -293,53 +195,17 @@ class BluetoothCharacteristic { return true; } - // get known service - BmBluetoothService? get _bmsvc { - if (FlutterBluePlus._knownServices[remoteId] != null) { - for (var s in FlutterBluePlus._knownServices[remoteId]!.services) { - if (s.serviceUuid == serviceUuid) { - if (s.primaryServiceUuid == primaryServiceUuid) { - return s; - } - } - } - } - return null; - } - - /// get known characteristic - BmBluetoothCharacteristic? get _bmchr { - if (_bmsvc != null) { - for (var c in _bmsvc!.characteristics) { - if (c.characteristicUuid == uuid) { - return c; - } - } - } - return null; - } - @override String toString() { return 'BluetoothCharacteristic{' 'remoteId: $remoteId, ' - 'serviceUuid: $serviceUuid, ' - 'characteristicUuid: $characteristicUuid, ' - 'primaryServiceUuid: $primaryServiceUuid, ' + 'uuid: $uuid, ' + 'serviceUuid: ${service.uuid}, ' 'descriptors: $descriptors, ' 'properties: $properties, ' 'value: $lastValue' '}'; } - - @Deprecated('Use remoteId instead') - DeviceIdentifier get deviceId => remoteId; - - @Deprecated('Use lastValueStream instead') - Stream> get value => lastValueStream; - - @Deprecated('Use onValueReceived instead') - Stream> get onValueChangedStream => onValueReceived; } class CharacteristicProperties { diff --git a/lib/src/bluetooth_descriptor.dart b/lib/src/bluetooth_descriptor.dart index 1a8b2d9c..5d55030d 100644 --- a/lib/src/bluetooth_descriptor.dart +++ b/lib/src/bluetooth_descriptor.dart @@ -4,73 +4,18 @@ part of flutter_blue_plus; -class BluetoothDescriptor { - final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid descriptorUuid; - final Guid? primaryServiceUuid; - - BluetoothDescriptor({ - required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.descriptorUuid, - this.primaryServiceUuid, - }); - - BluetoothDescriptor.fromProto(BmBluetoothDescriptor p) - : remoteId = p.remoteId, - serviceUuid = p.serviceUuid, - characteristicUuid = p.characteristicUuid, - descriptorUuid = p.descriptorUuid, - primaryServiceUuid = p.primaryServiceUuid; +class BluetoothDescriptor extends BluetoothValueAttribute { + final BluetoothCharacteristic characteristic; - /// convenience accessor - Guid get uuid => descriptorUuid; + BluetoothDescriptor.fromProto(BmBluetoothDescriptor p, BluetoothCharacteristic characteristic) + : characteristic = characteristic, + super(device: characteristic.device, uuid: p.uuid); - /// convenience accessor - BluetoothDevice get device => BluetoothDevice(remoteId: remoteId); - - /// this variable is updated: - /// - anytime `read()` is called - /// - anytime `write()` is called - /// - when the device is disconnected it is cleared - List get lastValue { - String key = "$serviceUuid:$characteristicUuid:$descriptorUuid"; - return FlutterBluePlus._lastDescs[remoteId]?[key] ?? []; - } + @override + BluetoothAttribute? get _parentAttribute => characteristic; - /// this stream emits values: - /// - anytime `read()` is called - /// - anytime `write()` is called - /// - and when first listened to, it re-emits the last value for convenience - Stream> get lastValueStream => FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDescriptorRead" || m.method == "OnDescriptorWritten") - .map((m) => m.arguments) - .map((args) => BmDescriptorData.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .where((p) => p.characteristicUuid == characteristicUuid) - .where((p) => p.serviceUuid == serviceUuid) - .where((p) => p.descriptorUuid == descriptorUuid) - .where((p) => p.primaryServiceUuid == primaryServiceUuid) - .where((p) => p.success == true) - .map((p) => p.value) - .newStreamWithInitialValue(lastValue); - - /// this stream emits values: - /// - anytime `read()` is called - Stream> get onValueReceived => FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDescriptorRead") - .map((m) => m.arguments) - .map((args) => BmDescriptorData.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .where((p) => p.characteristicUuid == characteristicUuid) - .where((p) => p.serviceUuid == serviceUuid) - .where((p) => p.descriptorUuid == descriptorUuid) - .where((p) => p.primaryServiceUuid == primaryServiceUuid) - .where((p) => p.success == true) - .map((p) => p.value); + /// convenience accessor + Guid get descriptorUuid => uuid; /// Retrieves the value of a specified descriptor Future> read({int timeout = 15}) async { @@ -84,51 +29,32 @@ class BluetoothDescriptor { _Mutex mtx = _MutexFactory.getMutexForKey("global"); await mtx.take(); - // return value - List readValue = []; - try { var request = BmReadDescriptorRequest( remoteId: remoteId, - serviceUuid: serviceUuid, - characteristicUuid: characteristicUuid, - descriptorUuid: descriptorUuid, - primaryServiceUuid: primaryServiceUuid, + identifier: identifierPath, ); - Stream responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDescriptorRead") - .map((m) => m.arguments) - .map((args) => BmDescriptorData.fromMap(args)) - .where((p) => p.remoteId == request.remoteId) - .where((p) => p.serviceUuid == request.serviceUuid) - .where((p) => p.characteristicUuid == request.characteristicUuid) - .where((p) => p.descriptorUuid == request.descriptorUuid) - .where((p) => p.primaryServiceUuid == request.primaryServiceUuid); - - // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; - - // invoke - await FlutterBluePlus._invokeMethod('readDescriptor', request.toMap()); + // Invoke + final futureResponse = FlutterBluePlus._invokeMethodAndWaitForEvent( + 'readDescriptor', + request.toMap(), + (e) => e.descriptor == this, + ); // wait for response - BmDescriptorData response = await futureResponse + OnDescriptorReadEvent response = await futureResponse .fbpEnsureAdapterIsOn("readDescriptor") .fbpEnsureDeviceIsConnected(device, "readDescriptor") .fbpTimeout(timeout, "readDescriptor"); // failed? - if (!response.success) { - throw FlutterBluePlusException(_nativeError, "readDescriptor", response.errorCode, response.errorString); - } + response.ensureSuccess("readDescriptor"); - readValue = response.value; + return response.value; } finally { mtx.give(); } - - return readValue; } /// Writes the value of a descriptor @@ -146,61 +72,37 @@ class BluetoothDescriptor { try { var request = BmWriteDescriptorRequest( remoteId: remoteId, - serviceUuid: serviceUuid, - characteristicUuid: characteristicUuid, - descriptorUuid: descriptorUuid, + identifier: identifierPath, value: value, - primaryServiceUuid: primaryServiceUuid, ); - Stream responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDescriptorWritten") - .map((m) => m.arguments) - .map((args) => BmDescriptorData.fromMap(args)) - .where((p) => p.remoteId == request.remoteId) - .where((p) => p.serviceUuid == request.serviceUuid) - .where((p) => p.characteristicUuid == request.characteristicUuid) - .where((p) => p.descriptorUuid == request.descriptorUuid) - .where((p) => p.primaryServiceUuid == request.primaryServiceUuid); - - // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; - // invoke - await FlutterBluePlus._invokeMethod('writeDescriptor', request.toMap()); + final futureResponse = FlutterBluePlus._invokeMethodAndWaitForEvent( + 'writeDescriptor', + request.toMap(), + (e) => e.descriptor == this, + ); // wait for response - BmDescriptorData response = await futureResponse + OnDescriptorWrittenEvent response = await futureResponse .fbpEnsureAdapterIsOn("writeDescriptor") .fbpEnsureDeviceIsConnected(device, "writeDescriptor") .fbpTimeout(timeout, "writeDescriptor"); // failed? - if (!response.success) { - throw FlutterBluePlusException(_nativeError, "writeDescriptor", response.errorCode, response.errorString); - } + response.ensureSuccess("writeDescriptor"); } finally { mtx.give(); } - - return Future.value(); } @override String toString() { return 'BluetoothDescriptor{' 'remoteId: $remoteId, ' - 'serviceUuid: $serviceUuid, ' - 'characteristicUuid: $characteristicUuid, ' - 'descriptorUuid: $descriptorUuid, ' - 'primaryServiceUuid: $primaryServiceUuid' + 'uuid: $uuid, ' + 'characteristicUuid: ${characteristic.uuid}, ' 'lastValue: $lastValue' '}'; } - - @Deprecated('Use onValueReceived instead') - Stream> get value => onValueReceived; - - @Deprecated('Use remoteId instead') - DeviceIdentifier get deviceId => remoteId; } diff --git a/lib/src/bluetooth_device.dart b/lib/src/bluetooth_device.dart index 3b59ba74..0710145e 100644 --- a/lib/src/bluetooth_device.dart +++ b/lib/src/bluetooth_device.dart @@ -7,15 +7,36 @@ part of flutter_blue_plus; class BluetoothDevice { final DeviceIdentifier remoteId; - BluetoothDevice({ + List _services = []; + + int? _mtu; + BluetoothConnectionState? _connectionState; + DisconnectReason? _disconnectReason; + DateTime? _connectTimestamp; + BluetoothBondState? _bondState; + BluetoothBondState? _prevBondState; + + String? _platformName; + String? _advName; + + final List _subscriptions = []; + final List _delayedSubscriptions = []; + + BluetoothDevice._internal({ required this.remoteId, }); + factory BluetoothDevice({required DeviceIdentifier remoteId}) { + return FlutterBluePlus._deviceForId(remoteId); + } + /// Create a device from an id /// - to connect, this device must have been discovered by your app in a previous scan /// - iOS uses 128-bit uuids the remoteId, e.g. e006b3a7-ef7b-4980-a668-1f8005f84383 /// - Android uses 48-bit mac addresses as the remoteId, e.g. 06:E5:28:3B:FD:E0 - BluetoothDevice.fromId(String remoteId) : remoteId = DeviceIdentifier(remoteId); + factory BluetoothDevice.fromId(String remoteId) { + return FlutterBluePlus._deviceForId(DeviceIdentifier(remoteId)); + } /// platform name /// - this name is kept track of by the platform @@ -23,26 +44,19 @@ class BluetoothDevice { /// - iOS: after you connect, iOS uses the GAP name characteristic (0x2A00) /// if it exists. Otherwise iOS use the advertised name. /// - Android: always uses the advertised name - String get platformName => FlutterBluePlus._platformNames[remoteId] ?? ""; + String get platformName => _platformName ?? ""; /// Advertised Named /// - this is the name advertised by the device during scanning /// - it is only available after you scan with FlutterBluePlus /// - it is cleared when the app restarts. /// - not all devices advertise a name - String get advName => FlutterBluePlus._advNames[remoteId] ?? ""; + String get advName => _advName ?? ""; /// Get services /// - returns empty if discoverServices() has not been called /// or if your device does not have any services (rare) - List get servicesList { - BmDiscoverServicesResult? result = FlutterBluePlus._knownServices[remoteId]; - if (result == null) { - return []; - } else { - return result.services.map((p) => BluetoothService.fromProto(p)).where((p) => p.isPrimary).toList(); - } - } + List get servicesList => _services; /// Register a subscription to be canceled when the device is disconnected. /// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions. @@ -57,28 +71,17 @@ class BluetoothDevice { if (isConnected == false && next == false) { subscription.cancel(); // cancel immediately if already disconnected. } else if (delayed) { - FlutterBluePlus._delayedSubscriptions[remoteId] ??= []; - FlutterBluePlus._delayedSubscriptions[remoteId]!.add(subscription); + _delayedSubscriptions.add(subscription); } else { - FlutterBluePlus._deviceSubscriptions[remoteId] ??= []; - FlutterBluePlus._deviceSubscriptions[remoteId]!.add(subscription); + _subscriptions.add(subscription); } } /// Returns true if autoConnect is currently enabled for this device - bool get isAutoConnectEnabled { - return FlutterBluePlus._autoConnect.contains(remoteId); - } + bool get isAutoConnectEnabled => FlutterBluePlus._autoConnect.contains(remoteId); /// Returns true if this device is currently connected to your app - bool get isConnected { - if (FlutterBluePlus._connectionStates[remoteId] == null) { - return false; - } else { - var state = FlutterBluePlus._connectionStates[remoteId]!.connectionState; - return state == BmConnectionStateEnum.connected; - } - } + bool get isConnected => _connectionState == BluetoothConnectionState.connected; /// Returns true if this device is currently disconnected from your app bool get isDisconnected => isConnected == false; @@ -120,18 +123,14 @@ class BluetoothDevice { autoConnect: autoConnect, ); - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnConnectionStateChanged") - .map((m) => m.arguments) - .map((args) => BmConnectionStateResponse.fromMap(args)) - .where((p) => p.remoteId == remoteId); + var responseStream = FlutterBluePlus._extractEventStream((m) => m.device == this); // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureState = responseStream.first; + Future futureState = responseStream.first; // record connection time if (Platform.isAndroid) { - FlutterBluePlus._connectTimestamp[remoteId] = DateTime.now(); + _connectTimestamp = DateTime.now(); } // invoke @@ -143,7 +142,7 @@ class BluetoothDevice { // only wait for connection if we weren't already connected if (changed && !autoConnect) { - BmConnectionStateResponse response = await futureState + OnConnectionStateChangedEvent response = await futureState .fbpEnsureAdapterIsOn("connect") .fbpTimeout(timeout.inSeconds, "connect") .catchError((e) async { @@ -155,13 +154,13 @@ class BluetoothDevice { }); // failure? - if (response.connectionState == BmConnectionStateEnum.disconnected) { - if (response.disconnectReasonCode == bmUserCanceledErrorCode) { + if (response.connectionState == BluetoothConnectionState.disconnected) { + if (response._response.disconnectReasonCode == bmUserCanceledErrorCode) { throw FlutterBluePlusException( ErrorPlatform.fbp, "connect", FbpErrorCode.connectionCanceled.index, "connection canceled"); } else { - throw FlutterBluePlusException( - _nativeError, "connect", response.disconnectReasonCode, response.disconnectReasonString); + throw FlutterBluePlusException(_nativeError, "connect", response._response.disconnectReasonCode, + response._response.disconnectReasonString); } } } @@ -207,15 +206,11 @@ class BluetoothDevice { // remove from auto connect list if there FlutterBluePlus._autoConnect.remove(remoteId); - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnConnectionStateChanged") - .map((m) => m.arguments) - .map((args) => BmConnectionStateResponse.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .where((p) => p.connectionState == BmConnectionStateEnum.disconnected); + var responseStream = FlutterBluePlus._extractEventStream((e) => e.device == this) + .where((p) => p.connectionState == BluetoothConnectionState.disconnected); // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureState = responseStream.first; + Future futureState = responseStream.first; // Workaround Android race condition await _ensureAndroidDisconnectionDelay(androidDelay); @@ -230,7 +225,7 @@ class BluetoothDevice { if (Platform.isAndroid) { // Disconnected, remove connect timestamp - FlutterBluePlus._connectTimestamp.remove(remoteId); + _connectTimestamp = null; } } finally { dtx.give(); @@ -255,36 +250,22 @@ class BluetoothDevice { _Mutex mtx = _MutexFactory.getMutexForKey("global"); await mtx.take(); - List result = []; - try { - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDiscoveredServices") - .map((m) => m.arguments) - .map((args) => BmDiscoverServicesResult.fromMap(args)) - .where((p) => p.remoteId == remoteId); - - // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; - // invoke - await FlutterBluePlus._invokeMethod('discoverServices', remoteId.str); + final futureResponse = FlutterBluePlus._invokeMethodAndWaitForEvent( + 'discoverServices', + remoteId.str, + (e) => e.device == this, + ); // wait for response - BmDiscoverServicesResult response = await futureResponse + OnDiscoveredServicesEvent response = await futureResponse .fbpEnsureAdapterIsOn("discoverServices") .fbpEnsureDeviceIsConnected(this, "discoverServices") .fbpTimeout(timeout, "discoverServices"); // failed? - if (!response.success) { - throw FlutterBluePlusException(_nativeError, "discoverServices", response.errorCode, response.errorString); - } - - // return primary services - result = response.services.map((p) => BluetoothService.fromProto(p)).where((p) => p.isPrimary).toList(); - - result = result; + response.ensureSuccess("discoverServices"); } finally { mtx.give(); } @@ -300,68 +281,39 @@ class BluetoothDevice { } } - return result; + return _services; } /// The most recent disconnection reason DisconnectReason? get disconnectReason { - if (FlutterBluePlus._connectionStates[remoteId] == null) { - return null; - } - int? code = FlutterBluePlus._connectionStates[remoteId]!.disconnectReasonCode; - String? description = FlutterBluePlus._connectionStates[remoteId]!.disconnectReasonString; - return DisconnectReason(_nativeError, code, description); + return _disconnectReason; } /// The current connection state *of our app* to the device Stream get connectionState { // initial value - Note: we only care about the current connection state of // *our* app, which is why we can use our cached value, or assume disconnected - BluetoothConnectionState initialValue = BluetoothConnectionState.disconnected; - if (FlutterBluePlus._connectionStates[remoteId] != null) { - initialValue = _bmToConnectionState(FlutterBluePlus._connectionStates[remoteId]!.connectionState); - } - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnConnectionStateChanged") - .map((m) => m.arguments) - .map((args) => BmConnectionStateResponse.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .map((p) => _bmToConnectionState(p.connectionState)) + BluetoothConnectionState initialValue = _connectionState ?? BluetoothConnectionState.disconnected; + return FlutterBluePlus._extractEventStream((m) => m.device == this) + .map((e) => e.connectionState) .newStreamWithInitialValue(initialValue); } /// The current MTU size in bytes - int get mtuNow { - // get initial value from our cache - return FlutterBluePlus._mtuValues[remoteId]?.mtu ?? 23; - } + int get mtuNow => _mtu ?? 23; /// Stream emits a value: /// - immediately when first listened to /// - whenever the mtu changes - Stream get mtu { - // get initial value from our cache - int initialValue = FlutterBluePlus._mtuValues[remoteId]?.mtu ?? 23; - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnMtuChanged") - .map((m) => m.arguments) - .map((args) => BmMtuChangedResponse.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .map((p) => p.mtu) - .newStreamWithInitialValue(initialValue); - } + Stream get mtu => FlutterBluePlus._extractEventStream((e) => e.device == this) + .map((e) => e.mtu) + .newStreamWithInitialValue(mtuNow); /// Services Reset Stream /// - uses the GAP Services Changed characteristic (0x2A05) /// - you must re-call discoverServices() when services are reset - Stream get onServicesReset { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnServicesReset") - .map((m) => m.arguments) - .map((args) => BmBluetoothDevice.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .map((m) => null); - } + Stream get onServicesReset => + FlutterBluePlus._extractEventStream((e) => e.device == this).map((m) => null); /// Read the RSSI of connected remote device Future readRssi({int timeout = 15}) async { @@ -375,37 +327,23 @@ class BluetoothDevice { _Mutex mtx = _MutexFactory.getMutexForKey("global"); await mtx.take(); - int rssi = 0; - try { - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnReadRssi") - .map((m) => m.arguments) - .map((args) => BmReadRssiResult.fromMap(args)) - .where((p) => (p.remoteId == remoteId)); - - // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; - - // invoke - await FlutterBluePlus._invokeMethod('readRssi', remoteId.str); + var futureResponse = FlutterBluePlus._invokeMethodAndWaitForEvent( + 'readRssi', remoteId.str, (e) => e.device == this); // wait for response - BmReadRssiResult response = await futureResponse + OnReadRssiEvent response = await futureResponse .fbpEnsureAdapterIsOn("readRssi") .fbpEnsureDeviceIsConnected(this, "readRssi") .fbpTimeout(timeout, "readRssi"); // failed? - if (!response.success) { - throw FlutterBluePlusException(_nativeError, "readRssi", response.errorCode, response.errorString); - } - rssi = response.rssi; + response.ensureSuccess("readRssi"); + + return response.rssi; } finally { mtx.give(); } - - return rssi; } /// Request to change MTU (Android Only) @@ -448,37 +386,28 @@ class BluetoothDevice { await Future.delayed(Duration(milliseconds: (predelay * 1000).toInt())); } - var mtu = 0; - try { var request = BmMtuChangeRequest( remoteId: remoteId, mtu: desiredMtu, ); - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnMtuChanged") - .map((m) => m.arguments) - .map((args) => BmMtuChangedResponse.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .map((p) => p.mtu); - - // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; - // invoke - await FlutterBluePlus._invokeMethod('requestMtu', request.toMap()); + var futureResponse = FlutterBluePlus._invokeMethodAndWaitForEvent( + 'requestMtu', + request.toMap(), + (e) => e.device == this, + ); // wait for response - mtu = await futureResponse + return await futureResponse .fbpEnsureAdapterIsOn("requestMtu") .fbpEnsureDeviceIsConnected(this, "requestMtu") - .fbpTimeout(timeout, "requestMtu"); + .fbpTimeout(timeout, "requestMtu") + .then((e) => e.mtu); } finally { mtx.give(); } - - return mtu; } /// Request connection priority update (Android only) @@ -556,28 +485,24 @@ class BluetoothDevice { await mtx.take(); try { - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnBondStateChanged") - .map((m) => m.arguments) - .map((args) => BmBondStateResponse.fromMap(args)) - .where((p) => p.remoteId == remoteId) + var responseStream = FlutterBluePlus._extractEventStream((m) => m.device == this) .where((p) => p.bondState != BmBondStateEnum.bonding); // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; + Future futureResponse = responseStream.first; // invoke bool changed = await FlutterBluePlus._invokeMethod('createBond', remoteId.str); // only wait for 'bonded' if we weren't already bonded if (changed) { - BmBondStateResponse bs = await futureResponse + OnBondStateChangedEvent bs = await futureResponse .fbpEnsureAdapterIsOn("createBond") .fbpEnsureDeviceIsConnected(this, "createBond") .fbpTimeout(timeout, "createBond"); // success? - if (bs.bondState != BmBondStateEnum.bonded) { + if (bs.bondState != BluetoothBondState.bonded) { throw FlutterBluePlusException(ErrorPlatform.fbp, "createBond", FbpErrorCode.createBondFailed.hashCode, "Failed to create bond. ${bs.bondState}"); } @@ -599,28 +524,24 @@ class BluetoothDevice { await mtx.take(); try { - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnBondStateChanged") - .map((m) => m.arguments) - .map((args) => BmBondStateResponse.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .where((p) => p.bondState != BmBondStateEnum.bonding); + var responseStream = FlutterBluePlus._extractEventStream((m) => m.device == this) + .where((p) => p.bondState != BluetoothBondState.bonding); // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; + Future futureResponse = responseStream.first; // invoke bool changed = await FlutterBluePlus._invokeMethod('removeBond', remoteId.str); // only wait for 'unbonded' state if we weren't already unbonded if (changed) { - BmBondStateResponse bs = await futureResponse + OnBondStateChangedEvent bs = await futureResponse .fbpEnsureAdapterIsOn("removeBond") .fbpEnsureDeviceIsConnected(this, "removeBond") .fbpTimeout(timeout, "removeBond"); // success? - if (bs.bondState != BmBondStateEnum.none) { + if (bs.bondState != BluetoothBondState.none) { throw FlutterBluePlusException(ErrorPlatform.fbp, "createBond", FbpErrorCode.removeBondFailed.hashCode, "Failed to remove bond. ${bs.bondState}"); } @@ -656,30 +577,21 @@ class BluetoothDevice { } // get current state if needed - if (FlutterBluePlus._bondStates[remoteId] == null) { + if (_bondState == null) { var val = await FlutterBluePlus._methodChannel .invokeMethod('getBondState', remoteId.str) .then((args) => BmBondStateResponse.fromMap(args)); // update _bondStates if it is still null after the await - if (FlutterBluePlus._bondStates[remoteId] == null) { - FlutterBluePlus._bondStates[remoteId] = val; - } + _bondState ??= _bmToBondState(val.bondState); } - yield* FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnBondStateChanged") - .map((m) => m.arguments) - .map((args) => BmBondStateResponse.fromMap(args)) - .where((p) => p.remoteId == remoteId) - .map((p) => _bmToBondState(p.bondState)) - .newStreamWithInitialValue(_bmToBondState(FlutterBluePlus._bondStates[remoteId]!.bondState)); + yield* FlutterBluePlus._extractEventStream((m) => m.device == this) + .map((e) => e.bondState) + .newStreamWithInitialValue(_bondState!); } /// Get the previous bondState of the device (Android Only) - BluetoothBondState? get prevBondState { - var b = FlutterBluePlus._bondStates[remoteId]?.prevState; - return b != null ? _bmToBondState(b) : null; - } + BluetoothBondState? get prevBondState => _prevBondState; /// Get the Services Changed characteristic (0x2A05) BluetoothCharacteristic? get _servicesChangedCharacteristic { @@ -696,9 +608,9 @@ class BluetoothDevice { /// https://issuetracker.google.com/issues/37121040 Future _ensureAndroidDisconnectionDelay(int androidDelay) async { if (Platform.isAndroid) { - if (FlutterBluePlus._connectTimestamp.containsKey(remoteId)) { + if (_connectTimestamp != null) { Duration minGap = Duration(milliseconds: androidDelay); - Duration elapsed = DateTime.now().difference(FlutterBluePlus._connectTimestamp[remoteId]!); + Duration elapsed = DateTime.now().difference(_connectTimestamp!); if (elapsed.compareTo(minGap) < 0) { Duration timeLeft = minGap - elapsed; print("[FBP] disconnect: enforcing ${minGap.inMilliseconds}ms disconnect gap, delaying " @@ -709,6 +621,40 @@ class BluetoothDevice { } } + T _getAttributeFromList(List list, String identifier) { + final parts = identifier.split(":"); + if (parts.length != 2) { + throw ArgumentError.value(identifier, "identifier", "must be in the form 'uuid:index'"); + } + final uuid = Guid(parts[0]); + final index = int.parse(parts[1]); + return list.firstWhere((s) => s.uuid == uuid && s.index == index); + } + + BluetoothService _serviceForIdentifier(String identifier) { + return _getAttributeFromList(_services, identifier); + } + + BluetoothCharacteristic _characteristicForIdentifier(String identifier) { + final parts = identifier.split("/"); + if (parts.length != 2) { + throw ArgumentError.value( + identifier, "identifier", "must be in the form 'serviceUuid:index/characteristicUuid:index'"); + } + final service = _serviceForIdentifier(parts[0]); + return _getAttributeFromList(service.characteristics, parts[1]); + } + + BluetoothDescriptor _descriptorForIdentifier(String identifier) { + final parts = identifier.split("/"); + if (parts.length != 3) { + throw ArgumentError.value(identifier, "identifier", + "must be in the form 'serviceUuid:index/characteristicUuid:index/descriptorUuid:index'"); + } + final characteristic = _characteristicForIdentifier(parts[0] + "/" + parts[1]); + return characteristic.descriptors.firstWhere((d) => d.uuid == Guid(parts[2])); + } + @override bool operator ==(Object other) => identical(this, other) || @@ -722,7 +668,7 @@ class BluetoothDevice { return 'BluetoothDevice{' 'remoteId: $remoteId, ' 'platformName: $platformName, ' - 'services: ${FlutterBluePlus._knownServices[remoteId]}' + 'services: ${_services}' '}'; } @@ -755,7 +701,4 @@ class BluetoothDevice { Stream> get services async* { yield []; } - - @Deprecated('Use fromId instead') - BluetoothDevice.fromProto(BmBluetoothDevice p) : remoteId = p.remoteId; } diff --git a/lib/src/bluetooth_events.dart b/lib/src/bluetooth_events.dart index 84e092a9..0e9b045f 100644 --- a/lib/src/bluetooth_events.dart +++ b/lib/src/bluetooth_events.dart @@ -1,93 +1,33 @@ part of flutter_blue_plus; class BluetoothEvents { - Stream get onConnectionStateChanged { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnConnectionStateChanged") - .map((m) => m.arguments) - .map((args) => BmConnectionStateResponse.fromMap(args)) - .map((p) => OnConnectionStateChangedEvent(p)); - } + Stream get onConnectionStateChanged => + FlutterBluePlus._extractEventStream(); - Stream get onMtuChanged { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnMtuChanged") - .map((m) => m.arguments) - .map((args) => BmMtuChangedResponse.fromMap(args)) - .map((p) => OnMtuChangedEvent(p)); - } + Stream get onMtuChanged => FlutterBluePlus._extractEventStream(); - Stream get onReadRssi { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnReadRssi") - .map((m) => m.arguments) - .map((args) => BmReadRssiResult.fromMap(args)) - .map((p) => OnReadRssiEvent(p)); - } + Stream get onReadRssi => FlutterBluePlus._extractEventStream(); - Stream get onServicesReset { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnServicesReset") - .map((m) => m.arguments) - .map((args) => BmBluetoothDevice.fromMap(args)) - .map((p) => OnServicesResetEvent(p)); - } + Stream get onServicesReset => FlutterBluePlus._extractEventStream(); - Stream get onDiscoveredServices { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDiscoveredServices") - .map((m) => m.arguments) - .map((args) => BmDiscoverServicesResult.fromMap(args)) - .map((p) => OnDiscoveredServicesEvent(p)); - } + Stream get onDiscoveredServices => + FlutterBluePlus._extractEventStream(); - Stream get onCharacteristicReceived { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnCharacteristicReceived") - .map((m) => m.arguments) - .map((args) => BmCharacteristicData.fromMap(args)) - .map((p) => OnCharacteristicReceivedEvent(p)); - } + Stream get onCharacteristicReceived => + FlutterBluePlus._extractEventStream(); - Stream get onCharacteristicWritten { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnCharacteristicWritten") - .map((m) => m.arguments) - .map((args) => BmCharacteristicData.fromMap(args)) - .map((p) => OnCharacteristicWrittenEvent(p)); - } + Stream get onCharacteristicWritten => + FlutterBluePlus._extractEventStream(); - Stream get onDescriptorRead { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDescriptorRead") - .map((m) => m.arguments) - .map((args) => BmDescriptorData.fromMap(args)) - .map((p) => OnDescriptorReadEvent(p)); - } + Stream get onDescriptorRead => FlutterBluePlus._extractEventStream(); - Stream get onDescriptorWritten { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnDescriptorWritten") - .map((m) => m.arguments) - .map((args) => BmDescriptorData.fromMap(args)) - .map((p) => OnDescriptorWrittenEvent(p)); - } + Stream get onDescriptorWritten => + FlutterBluePlus._extractEventStream(); - Stream get onNameChanged { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnNameChanged") - .map((m) => m.arguments) - .map((args) => BmBluetoothDevice.fromMap(args)) - .map((p) => OnNameChangedEvent(p)); - } + Stream get onNameChanged => FlutterBluePlus._extractEventStream(); - Stream get onBondStateChanged { - return FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnBondStateChanged") - .map((m) => m.arguments) - .map((args) => BmBondStateResponse.fromMap(args)) - .map((p) => OnBondStateChangedEvent(p)); - } + Stream get onBondStateChanged => + FlutterBluePlus._extractEventStream(); } class FbpError { @@ -97,195 +37,244 @@ class FbpError { FbpError(this.errorCode, this.errorString); } +// +// Mixins +// +mixin GetDeviceMixin { + dynamic get _response; + + /// the relevant device + BluetoothDevice get device => FlutterBluePlus._deviceForId(_response.remoteId); +} + +mixin GetAttributeValueMixin { + dynamic get _response; + BluetoothValueAttribute get attribute; + + /// the new data + List get value => _response.value; +} + +mixin GetCharacteristicMixin on GetAttributeValueMixin, GetDeviceMixin { + /// the relevant characteristic + BluetoothCharacteristic get characteristic => device._characteristicForIdentifier(_response.identifier); + + /// the relevant attribute + BluetoothValueAttribute get attribute => characteristic; +} + +mixin GetDescriptorMixin on GetAttributeValueMixin, GetDeviceMixin { + /// the relevant descriptor + BluetoothDescriptor get descriptor => device._descriptorForIdentifier(_response.identifier); + + /// the relevant attribute + BluetoothValueAttribute get attribute => descriptor; +} + +mixin GetExceptionMixin { + BmStatus get _response; + + FbpError? get error => _response.success ? null : FbpError(_response.errorCode, _response.errorString); + + FlutterBluePlusException? exception(String method) => _response.success + ? null + : FlutterBluePlusException(_nativeError, method, _response.errorCode, _response.errorString); + + void ensureSuccess(String method) { + if (!_response.success) { + throw exception(method)!; + } + } +} + // // Event Classes // +// On Detached From Engine +class OnDetachedFromEngineEvent { + static const String method = "OnDetachedFromEngine"; +} + +// On Turn On Response +class OnTurnOnResponseEvent { + static const String method = "OnTurnOnResponse"; + + final BmTurnOnResponse _response; + + OnTurnOnResponseEvent(this._response); + + /// user accepted response + bool get userAccepted => _response.userAccepted; +} + +// On Scan Response +class OnScanResponseEvent with GetExceptionMixin { + static const String method = "OnScanResponse"; + + final BmScanResponse _response; + + OnScanResponseEvent(this._response); + + /// the new scan state + List get advertisements => _response.advertisements.map((a) => ScanResult.fromProto(a)).toList(); +} + // On Connection State Changed -class OnConnectionStateChangedEvent { +class OnConnectionStateChangedEvent with GetDeviceMixin { + static const String method = "OnConnectionStateChanged"; + final BmConnectionStateResponse _response; OnConnectionStateChangedEvent(this._response); - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - /// the new connection state BluetoothConnectionState get connectionState => _bmToConnectionState(_response.connectionState); + + /// the disconnect reason + DisconnectReason? get disconnectReason => + DisconnectReason(_nativeError, _response.disconnectReasonCode, _response.disconnectReasonString); +} + +// On Adapter State Changed +class OnAdapterStateChangedEvent { + static const String method = "OnAdapterStateChanged"; + + final BmBluetoothAdapterState _response; + + OnAdapterStateChangedEvent(this._response); + + /// the new adapter state + BluetoothAdapterState get adapterState => _bmToAdapterState(_response.adapterState); } // On Mtu Changed -class OnMtuChangedEvent { +class OnMtuChangedEvent with GetDeviceMixin, GetExceptionMixin { + static const String method = "OnMtuChanged"; + final BmMtuChangedResponse _response; OnMtuChangedEvent(this._response); - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - /// the new mtu int get mtu => _response.mtu; - - /// failed? - FbpError? get error => _response.success ? null : FbpError(_response.errorCode, _response.errorString); } // On Read Rssi -class OnReadRssiEvent { +class OnReadRssiEvent with GetDeviceMixin, GetExceptionMixin { + static const String method = "OnReadRssi"; + final BmReadRssiResult _response; OnReadRssiEvent(this._response); - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - /// rssi int get rssi => _response.rssi; - - /// failed? - FbpError? get error => _response.success ? null : FbpError(_response.errorCode, _response.errorString); } // On Services Reset -class OnServicesResetEvent { +class OnServicesResetEvent with GetDeviceMixin { + static const String method = "OnServicesReset"; + final BmBluetoothDevice _response; OnServicesResetEvent(this._response); - - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); } // On Discovered Services -class OnDiscoveredServicesEvent { +class OnDiscoveredServicesEvent with GetDeviceMixin, GetExceptionMixin { + static const String method = "OnDiscoveredServices"; + final BmDiscoverServicesResult _response; OnDiscoveredServicesEvent(this._response); - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - - /// the discovered services - List get services => _response.services.map((p) => BluetoothService.fromProto(p)).toList(); - - /// failed? - FbpError? get error => _response.success ? null : FbpError(_response.errorCode, _response.errorString); + /// the new services + List _constructServices() { + final List services = []; + Map> includedServicesMap = {}; + for (final bmService in _response.services) { + final service = BluetoothService.fromProto(device, bmService); + services.add(service); + includedServicesMap[service] = bmService.includedServices; + } + + for (final entry in includedServicesMap.entries) { + final service = entry.key; + final includedServices = entry.value; + service.includedServices = includedServices.map((uuid) { + final includedService = services._firstWhereOrNull((s) => s.identifier == uuid); + if (includedService == null) { + throw FlutterBluePlusException( + ErrorPlatform.fbp, method, FbpErrorCode.serviceNotFound.index, "service not found: $uuid"); + } + return includedService; + }).toList(); + } + + return services; + } } // On Characteristic Received -class OnCharacteristicReceivedEvent { +class OnCharacteristicReceivedEvent + with GetDeviceMixin, GetAttributeValueMixin, GetCharacteristicMixin, GetExceptionMixin { + static const String method = "OnCharacteristicReceived"; + final BmCharacteristicData _response; OnCharacteristicReceivedEvent(this._response); - - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - - /// the relevant characteristic - BluetoothCharacteristic get characteristic => BluetoothCharacteristic( - remoteId: _response.remoteId, - characteristicUuid: _response.characteristicUuid, - serviceUuid: _response.serviceUuid, - primaryServiceUuid: _response.primaryServiceUuid); - - /// the new data - List get value => _response.value; - - /// failed? - FbpError? get error => _response.success ? null : FbpError(_response.errorCode, _response.errorString); } // On Characteristic Written -class OnCharacteristicWrittenEvent { +class OnCharacteristicWrittenEvent + with GetDeviceMixin, GetAttributeValueMixin, GetCharacteristicMixin, GetExceptionMixin { + static const String method = "OnCharacteristicWritten"; + final BmCharacteristicData _response; OnCharacteristicWrittenEvent(this._response); - - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - - /// the relevant characteristic - BluetoothCharacteristic get characteristic => BluetoothCharacteristic( - remoteId: _response.remoteId, - characteristicUuid: _response.characteristicUuid, - serviceUuid: _response.serviceUuid, - primaryServiceUuid: _response.primaryServiceUuid); - - /// the new data - List get value => _response.value; - - /// failed? - FbpError? get error => _response.success ? null : FbpError(_response.errorCode, _response.errorString); } // On Descriptor Received -class OnDescriptorReadEvent { +class OnDescriptorReadEvent with GetDeviceMixin, GetAttributeValueMixin, GetDescriptorMixin, GetExceptionMixin { + static const String method = "OnDescriptorRead"; + final BmDescriptorData _response; OnDescriptorReadEvent(this._response); - - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - - /// the relevant descriptor - BluetoothDescriptor get descriptor => BluetoothDescriptor( - remoteId: _response.remoteId, - serviceUuid: _response.serviceUuid, - characteristicUuid: _response.characteristicUuid, - descriptorUuid: _response.descriptorUuid); - - /// the new data - List get value => _response.value; - - /// failed? - FbpError? get error => _response.success ? null : FbpError(_response.errorCode, _response.errorString); } // On Descriptor Written -class OnDescriptorWrittenEvent { +class OnDescriptorWrittenEvent with GetDeviceMixin, GetAttributeValueMixin, GetDescriptorMixin, GetExceptionMixin { + static const String method = "OnDescriptorWritten"; + final BmDescriptorData _response; OnDescriptorWrittenEvent(this._response); - - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - - /// the relevant descriptor - BluetoothDescriptor get descriptor => BluetoothDescriptor( - remoteId: _response.remoteId, - serviceUuid: _response.serviceUuid, - characteristicUuid: _response.characteristicUuid, - descriptorUuid: _response.descriptorUuid); - - /// the new data - List get value => _response.value; - - /// failed? - FbpError? get error => _response.success ? null : FbpError(_response.errorCode, _response.errorString); } // On Name Changed -class OnNameChangedEvent { - final BmBluetoothDevice _response; +class OnNameChangedEvent with GetDeviceMixin { + static const String method = "OnNameChanged"; - OnNameChangedEvent(this._response); + final BmNameChanged _response; // TODO: Used to be BmBluetoothDevice?? - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); + OnNameChangedEvent(this._response); /// the new name - String? get name => _response.platformName; + String? get name => _response.name; // TODO: Used to be BmBluetoothDevice?? } // On Bond State Changed -class OnBondStateChangedEvent { +class OnBondStateChangedEvent with GetDeviceMixin { + static const String method = "OnBondStateChanged"; + final BmBondStateResponse _response; OnBondStateChangedEvent(this._response); - /// the relevant device - BluetoothDevice get device => BluetoothDevice(remoteId: _response.remoteId); - /// the new bond state BluetoothBondState get bondState => _bmToBondState(_response.bondState); + BluetoothBondState? get prevState => _response.prevState == null ? null : _bmToBondState(_response.prevState!); } diff --git a/lib/src/bluetooth_msgs.dart b/lib/src/bluetooth_msgs.dart index ebdd9863..a3131b2e 100644 --- a/lib/src/bluetooth_msgs.dart +++ b/lib/src/bluetooth_msgs.dart @@ -15,17 +15,13 @@ class BmBluetoothAdapterState { BmBluetoothAdapterState({required this.adapterState}); - Map toMap() { - final Map data = {}; - data['adapter_state'] = adapterState.index; - return data; - } + Map toMap() => { + 'adapter_state': adapterState.index, + }; - factory BmBluetoothAdapterState.fromMap(Map json) { - return BmBluetoothAdapterState( - adapterState: BmAdapterStateEnum.values[json['adapter_state']], - ); - } + factory BmBluetoothAdapterState.fromMap(Map json) => BmBluetoothAdapterState( + adapterState: BmAdapterStateEnum.values[json['adapter_state']], + ); } class BmMsdFilter { @@ -33,13 +29,11 @@ class BmMsdFilter { List? data; List? mask; BmMsdFilter(this.manufacturerId, this.data, this.mask); - Map toMap() { - final Map map = {}; - map['manufacturer_id'] = manufacturerId; - map['data'] = _hexEncode(data ?? []); - map['mask'] = _hexEncode(mask ?? []); - return map; - } + Map toMap() => { + 'manufacturer_id': manufacturerId, + 'data': _hexEncode(data ?? []), + 'mask': _hexEncode(mask ?? []), + }; } class BmServiceDataFilter { @@ -47,13 +41,11 @@ class BmServiceDataFilter { List data; List mask; BmServiceDataFilter(this.service, this.data, this.mask); - Map toMap() { - final Map map = {}; - map['service'] = service.str; - map['data'] = _hexEncode(data); - map['mask'] = _hexEncode(mask); - return map; - } + Map toMap() => { + 'service': service.str, + 'data': _hexEncode(data), + 'mask': _hexEncode(mask), + }; } class BmScanSettings { @@ -83,21 +75,19 @@ class BmScanSettings { required this.androidUsesFineLocation, }); - Map toMap() { - final Map data = {}; - data['with_services'] = withServices.map((s) => s.str).toList(); - data['with_remote_ids'] = withRemoteIds; - data['with_names'] = withNames; - data['with_keywords'] = withKeywords; - data['with_msd'] = withMsd.map((d) => d.toMap()).toList(); - data['with_service_data'] = withServiceData.map((d) => d.toMap()).toList(); - data['continuous_updates'] = continuousUpdates; - data['continuous_divisor'] = continuousDivisor; - data['android_legacy'] = androidLegacy; - data['android_scan_mode'] = androidScanMode; - data['android_uses_fine_location'] = androidUsesFineLocation; - return data; - } + Map toMap() => { + 'with_services': withServices.map((s) => s.str).toList(), + 'with_remote_ids': withRemoteIds, + 'with_names': withNames, + 'with_keywords': withKeywords, + 'with_msd': withMsd.map((d) => d.toMap()).toList(), + 'with_service_data': withServiceData.map((d) => d.toMap()).toList(), + 'continuous_updates': continuousUpdates, + 'continuous_divisor': continuousDivisor, + 'android_legacy': androidLegacy, + 'android_scan_mode': androidScanMode, + 'android_uses_fine_location': androidUsesFineLocation, + }; } class BmScanAdvertisement { @@ -125,73 +115,48 @@ class BmScanAdvertisement { required this.rssi, }); - factory BmScanAdvertisement.fromMap(Map json) { - // Get raw data - var rawManufacturerData = json['manufacturer_data'] ?? {}; - var rawServiceData = json['service_data'] ?? {}; - var rawServiceUuids = json['service_uuids'] ?? []; - - // Cast the data to the right type - Map> manufacturerData = {}; - for (var key in rawManufacturerData.keys) { - manufacturerData[key] = _hexDecode(rawManufacturerData[key]); - } - - // Cast the data to the right type - Map> serviceData = {}; - for (var key in rawServiceData.keys) { - serviceData[Guid(key)] = _hexDecode(rawServiceData[key]); - } - - // Cast the data to the right type - List serviceUuids = []; - for (var val in rawServiceUuids) { - serviceUuids.add(Guid(val)); - } - - return BmScanAdvertisement( - remoteId: DeviceIdentifier(json['remote_id']), - platformName: json['platform_name'], - advName: json['adv_name'], - connectable: json['connectable'] != null ? json['connectable'] != 0 : false, - txPowerLevel: json['tx_power_level'], - appearance: json['appearance'], - manufacturerData: manufacturerData, - serviceData: serviceData, - serviceUuids: serviceUuids, - rssi: json['rssi'] != null ? json['rssi'] : 0, - ); - } -} - -class BmScanResponse { - final List advertisements; + factory BmScanAdvertisement.fromMap(Map json) => BmScanAdvertisement( + remoteId: DeviceIdentifier(json['remote_id']), + platformName: json['platform_name'], + advName: json['adv_name'], + connectable: json['connectable'] != null ? json['connectable'] != 0 : false, + txPowerLevel: json['tx_power_level'], + appearance: json['appearance'], + manufacturerData: + json['manufacturer_data']?.map>((key, value) => MapEntry(key as int, _hexDecode(value))) ?? + {}, + serviceData: + json['service_data']?.map>((key, value) => MapEntry(Guid(key), _hexDecode(value))) ?? {}, + serviceUuids: json['service_uuids']?.map((v) => Guid(v)).toList() ?? [], + rssi: json['rssi'] ?? 0, + ); +} + +class BmStatus { final bool success; final int errorCode; final String errorString; - BmScanResponse({ - required this.advertisements, - required this.success, - required this.errorCode, - required this.errorString, + BmStatus({ + this.success = true, + this.errorCode = 0, + this.errorString = "", }); - factory BmScanResponse.fromMap(Map json) { - List advertisements = []; - for (var item in json['advertisements']) { - advertisements.add(BmScanAdvertisement.fromMap(item)); - } + BmStatus.fromMap(Map json) + : success = json['success'] != 0, + errorCode = json['error_code'] ?? 0, + errorString = json['error_string'] ?? ""; +} - bool success = json['success'] == null || json['success'] != 0; +class BmScanResponse extends BmStatus { + final List advertisements; - return BmScanResponse( - advertisements: advertisements, - success: success, - errorCode: !success ? json['error_code'] : 0, - errorString: !success ? json['error_string'] : "", - ); - } + BmScanResponse.fromMap(Map json) + : advertisements = json['advertisements'] + .map((v) => BmScanAdvertisement.fromMap(v as Map)) + .toList(), + super.fromMap(json); } class BmConnectRequest { @@ -203,12 +168,10 @@ class BmConnectRequest { required this.autoConnect, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['auto_connect'] = autoConnect ? 1 : 0; - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'auto_connect': autoConnect ? 1 : 0, + }; } class BmBluetoothDevice { @@ -220,19 +183,15 @@ class BmBluetoothDevice { required this.platformName, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['platform_name'] = platformName; - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'platform_name': platformName, + }; - factory BmBluetoothDevice.fromMap(Map json) { - return BmBluetoothDevice( - remoteId: DeviceIdentifier(json['remote_id']), - platformName: json['platform_name'], - ); - } + factory BmBluetoothDevice.fromMap(Map json) => BmBluetoothDevice( + remoteId: DeviceIdentifier(json['remote_id']), + platformName: json['platform_name'], + ); } class BmNameChanged { @@ -244,110 +203,51 @@ class BmNameChanged { required this.name, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['name'] = name; - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'name': name, + }; - factory BmNameChanged.fromMap(Map json) { - return BmNameChanged( - remoteId: DeviceIdentifier(json['remote_id']), - name: json['name'], - ); - } + factory BmNameChanged.fromMap(Map json) => BmNameChanged( + remoteId: DeviceIdentifier(json['remote_id']), + name: json['name'], + ); } class BmBluetoothService { - final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid? primaryServiceUuid; + final Guid uuid; + final int index; + final bool isPrimary; List characteristics; + List includedServices; - BmBluetoothService({ - required this.serviceUuid, - required this.remoteId, - required this.characteristics, - required this.primaryServiceUuid, - }); - - factory BmBluetoothService.fromMap(Map json) { - // convert characteristics - List chrs = []; - for (var v in json['characteristics']) { - chrs.add(BmBluetoothCharacteristic.fromMap(v)); - } - - return BmBluetoothService( - remoteId: DeviceIdentifier(json['remote_id']), - serviceUuid: Guid(json['service_uuid']), - primaryServiceUuid: Guid.parse(json['primary_service_uuid']), - characteristics: chrs, - ); - } + BmBluetoothService.fromMap(Map json) + : uuid = Guid(json['uuid']), + index = json['index'], + isPrimary = json['primary'] != 0, + characteristics = (json['characteristics'] as List) + .map((v) => BmBluetoothCharacteristic.fromMap(v)) + .toList(), + includedServices = (json['included_services'] as List).map((v) => v as String).toList(); } class BmBluetoothCharacteristic { - final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid? primaryServiceUuid; + final Guid uuid; + final int index; List descriptors; BmCharacteristicProperties properties; - - BmBluetoothCharacteristic({ - required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.primaryServiceUuid, - required this.descriptors, - required this.properties, - }); - - factory BmBluetoothCharacteristic.fromMap(Map json) { - // convert descriptors - List descs = []; - for (var v in json['descriptors']) { - descs.add(BmBluetoothDescriptor.fromMap(v)); - } - - return BmBluetoothCharacteristic( - remoteId: DeviceIdentifier(json['remote_id']), - serviceUuid: Guid(json['service_uuid']), - characteristicUuid: Guid(json['characteristic_uuid']), - primaryServiceUuid: Guid.parse(json['primary_service_uuid']), - descriptors: descs, - properties: BmCharacteristicProperties.fromMap(json['properties']), - ); - } + BmBluetoothCharacteristic.fromMap(Map json) + : uuid = Guid(json['uuid']), + index = json['index'], + descriptors = (json['descriptors'] as List).map((v) => BmBluetoothDescriptor.fromMap(v)).toList(), + properties = BmCharacteristicProperties.fromMap(json['properties']); } class BmBluetoothDescriptor { - final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid descriptorUuid; - final Guid? primaryServiceUuid; + final Guid uuid; - BmBluetoothDescriptor({ - required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.descriptorUuid, - required this.primaryServiceUuid, - }); - - factory BmBluetoothDescriptor.fromMap(Map json) { - return BmBluetoothDescriptor( - remoteId: DeviceIdentifier(json['remote_id']), - serviceUuid: Guid(json['service_uuid']), - characteristicUuid: Guid(json['characteristic_uuid']), - descriptorUuid: Guid(json['descriptor_uuid']), - primaryServiceUuid: Guid.parse(json['primary_service_uuid']), - ); - } + BmBluetoothDescriptor.fromMap(Map json) : uuid = Guid(json['uuid']); } class BmCharacteristicProperties { @@ -362,148 +262,71 @@ class BmCharacteristicProperties { bool notifyEncryptionRequired; bool indicateEncryptionRequired; - BmCharacteristicProperties({ - required this.broadcast, - required this.read, - required this.writeWithoutResponse, - required this.write, - required this.notify, - required this.indicate, - required this.authenticatedSignedWrites, - required this.extendedProperties, - required this.notifyEncryptionRequired, - required this.indicateEncryptionRequired, - }); + BmCharacteristicProperties.fromMap(Map json) + : broadcast = json['broadcast'] != 0, + read = json['read'] != 0, + writeWithoutResponse = json['write_without_response'] != 0, + write = json['write'] != 0, + notify = json['notify'] != 0, + indicate = json['indicate'] != 0, + authenticatedSignedWrites = json['authenticated_signed_writes'] != 0, + extendedProperties = json['extended_properties'] != 0, + notifyEncryptionRequired = json['notify_encryption_required'] != 0, + indicateEncryptionRequired = json['indicate_encryption_required'] != 0; +} - factory BmCharacteristicProperties.fromMap(Map json) { - return BmCharacteristicProperties( - broadcast: json['broadcast'] != 0, - read: json['read'] != 0, - writeWithoutResponse: json['write_without_response'] != 0, - write: json['write'] != 0, - notify: json['notify'] != 0, - indicate: json['indicate'] != 0, - authenticatedSignedWrites: json['authenticated_signed_writes'] != 0, - extendedProperties: json['extended_properties'] != 0, - notifyEncryptionRequired: json['notify_encryption_required'] != 0, - indicateEncryptionRequired: json['indicate_encryption_required'] != 0, - ); - } -} - -class BmDiscoverServicesResult { +class BmDiscoverServicesResult extends BmStatus { final DeviceIdentifier remoteId; final List services; - final bool success; - final int errorCode; - final String errorString; - BmDiscoverServicesResult({ - required this.remoteId, - required this.services, - required this.success, - required this.errorCode, - required this.errorString, - }); - - factory BmDiscoverServicesResult.fromMap(Map json) { - return BmDiscoverServicesResult( - remoteId: DeviceIdentifier(json['remote_id']), - services: (json['services'] as List) - .map((e) => BmBluetoothService.fromMap(e as Map)) - .toList(), - success: json['success'] != 0, - errorCode: json['error_code'], - errorString: json['error_string'], - ); - } + BmDiscoverServicesResult.fromMap(Map json) + : remoteId = DeviceIdentifier(json['remote_id']), + services = (json['services'] as List) + .map((e) => BmBluetoothService.fromMap(e as Map)) + .toList(), + super.fromMap(json); } class BmReadCharacteristicRequest { final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid? primaryServiceUuid; + final String identifier; BmReadCharacteristicRequest({ required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - this.primaryServiceUuid, + required this.identifier, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['service_uuid'] = serviceUuid.str; - data['characteristic_uuid'] = characteristicUuid.str; - data['primary_service_uuid'] = primaryServiceUuid?.str; - data.removeWhere((key, value) => value == null); - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'identifier': identifier, + }; } -class BmCharacteristicData { +class BmCharacteristicData extends BmStatus { final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid? primaryServiceUuid; + final String identifier; final List value; - final bool success; - final int errorCode; - final String errorString; - - BmCharacteristicData({ - required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.primaryServiceUuid, - required this.value, - required this.success, - required this.errorCode, - required this.errorString, - }); - - factory BmCharacteristicData.fromMap(Map json) { - return BmCharacteristicData( - remoteId: DeviceIdentifier(json['remote_id']), - serviceUuid: Guid(json['service_uuid']), - characteristicUuid: Guid(json['characteristic_uuid']), - primaryServiceUuid: Guid.parse(json['primary_service_uuid']), - value: _hexDecode(json['value']), - success: json['success'] != 0, - errorCode: json['error_code'], - errorString: json['error_string'], - ); - } + BmCharacteristicData.fromMap(Map json) + : remoteId = DeviceIdentifier(json['remote_id']), + identifier = json['identifier'], + value = _hexDecode(json['value']), + super.fromMap(json); } class BmReadDescriptorRequest { final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid descriptorUuid; - final Guid? primaryServiceUuid; + final String identifier; BmReadDescriptorRequest({ required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.descriptorUuid, - required this.primaryServiceUuid, + required this.identifier, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['service_uuid'] = serviceUuid.str; - data['characteristic_uuid'] = characteristicUuid.str; - data['descriptor_uuid'] = descriptorUuid.str; - data['primary_service_uuid'] = primaryServiceUuid?.str; - data.removeWhere((key, value) => value == null); - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'identifier': identifier, + }; } enum BmWriteType { @@ -513,135 +336,77 @@ enum BmWriteType { class BmWriteCharacteristicRequest { final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid? primaryServiceUuid; + final String identifier; final BmWriteType writeType; final bool allowLongWrite; final List value; - BmWriteCharacteristicRequest({ required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.primaryServiceUuid, + required this.identifier, required this.writeType, required this.allowLongWrite, required this.value, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['service_uuid'] = serviceUuid.str; - data['characteristic_uuid'] = characteristicUuid.str; - data['primary_service_uuid'] = primaryServiceUuid?.str; - data['write_type'] = writeType.index; - data['allow_long_write'] = allowLongWrite ? 1 : 0; - data['value'] = _hexEncode(value); - data.removeWhere((key, value) => value == null); - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'identifier': identifier, + 'write_type': writeType.index, + 'allow_long_write': allowLongWrite ? 1 : 0, + 'value': _hexEncode(value), + }; } class BmWriteDescriptorRequest { final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid? primaryServiceUuid; - final Guid descriptorUuid; + final String identifier; final List value; BmWriteDescriptorRequest({ required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.primaryServiceUuid, - required this.descriptorUuid, + required this.identifier, required this.value, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['service_uuid'] = serviceUuid.str; - data['characteristic_uuid'] = characteristicUuid.str; - data['descriptor_uuid'] = descriptorUuid.str; - data['primary_service_uuid'] = primaryServiceUuid?.str; - data['value'] = _hexEncode(value); - data.removeWhere((key, value) => value == null); - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'identifier': identifier, + 'value': _hexEncode(value), + }; } -class BmDescriptorData { +class BmDescriptorData extends BmStatus { final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid descriptorUuid; - final Guid? primaryServiceUuid; + final String identifier; final List value; - final bool success; - final int errorCode; - final String errorString; - BmDescriptorData({ - required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.descriptorUuid, - required this.primaryServiceUuid, - required this.value, - required this.success, - required this.errorCode, - required this.errorString, - }); - - factory BmDescriptorData.fromMap(Map json) { - return BmDescriptorData( - remoteId: DeviceIdentifier(json['remote_id']), - serviceUuid: Guid(json['service_uuid']), - characteristicUuid: Guid(json['characteristic_uuid']), - descriptorUuid: Guid(json['descriptor_uuid']), - primaryServiceUuid: Guid.parse(json['primary_service_uuid']), - value: _hexDecode(json['value']), - success: json['success'] != 0, - errorCode: json['error_code'], - errorString: json['error_string'], - ); - } + BmDescriptorData.fromMap(Map json) + : remoteId = DeviceIdentifier(json['remote_id']), + identifier = json['identifier'], + value = _hexDecode(json['value']), + super.fromMap(json); } class BmSetNotifyValueRequest { final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid characteristicUuid; - final Guid? primaryServiceUuid; + final String identifier; final bool forceIndications; final bool enable; - BmSetNotifyValueRequest({ required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.primaryServiceUuid, + required this.identifier, required this.forceIndications, required this.enable, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['service_uuid'] = serviceUuid.str; - data['characteristic_uuid'] = characteristicUuid.str; - data['primary_service_uuid'] = primaryServiceUuid?.str; - data['force_indications'] = forceIndications; - data['enable'] = enable; - data.removeWhere((key, value) => value == null); - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'identifier': identifier, + 'force_indications': forceIndications, + 'enable': enable, + }; } enum BmConnectionStateEnum { @@ -662,14 +427,12 @@ class BmConnectionStateResponse { required this.disconnectReasonString, }); - factory BmConnectionStateResponse.fromMap(Map json) { - return BmConnectionStateResponse( - remoteId: DeviceIdentifier(json['remote_id']), - connectionState: BmConnectionStateEnum.values[json['connection_state'] as int], - disconnectReasonCode: json['disconnect_reason_code'], - disconnectReasonString: json['disconnect_reason_string'], - ); - } + factory BmConnectionStateResponse.fromMap(Map json) => BmConnectionStateResponse( + remoteId: DeviceIdentifier(json['remote_id']), + connectionState: BmConnectionStateEnum.values[json['connection_state'] as int], + disconnectReasonCode: json['disconnect_reason_code'], + disconnectReasonString: json['disconnect_reason_string'], + ); } class BmDevicesList { @@ -677,14 +440,8 @@ class BmDevicesList { BmDevicesList({required this.devices}); - factory BmDevicesList.fromMap(Map json) { - // convert to BmBluetoothDevice - List devices = []; - for (var i = 0; i < json['devices'].length; i++) { - devices.add(BmBluetoothDevice.fromMap(json['devices'][i])); - } - return BmDevicesList(devices: devices); - } + factory BmDevicesList.fromMap(Map json) => + BmDevicesList(devices: json['devices'].map(BmBluetoothDevice.fromMap).toList()); } class BmMtuChangeRequest { @@ -693,74 +450,43 @@ class BmMtuChangeRequest { BmMtuChangeRequest({required this.remoteId, required this.mtu}); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['mtu'] = mtu; - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'mtu': mtu, + }; } -class BmMtuChangedResponse { +class BmMtuChangedResponse extends BmStatus { final DeviceIdentifier remoteId; final int mtu; - final bool success; - final int errorCode; - final String errorString; BmMtuChangedResponse({ required this.remoteId, required this.mtu, - this.success = true, - this.errorCode = 0, - this.errorString = "", - }); + }) : super(); + + BmMtuChangedResponse.fromMap(Map json) + : remoteId = DeviceIdentifier(json['remote_id']), + mtu = json['mtu'], + super.fromMap(json); - factory BmMtuChangedResponse.fromMap(Map json) { - return BmMtuChangedResponse( - remoteId: DeviceIdentifier(json['remote_id']), - mtu: json['mtu'], - success: json['success'] != 0, - errorCode: json['error_code'], - errorString: json['error_string'], - ); - } - - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['mtu'] = mtu; - data['success'] = success ? 1 : 0; - data['error_code'] = errorCode; - data['error_string'] = errorString; - return data; - } -} - -class BmReadRssiResult { + Map toMap() => { + 'remote_id': remoteId.str, + 'mtu': mtu, + 'success': success ? 1 : 0, + 'error_code': errorCode, + 'error_string': errorString, + }; +} + +class BmReadRssiResult extends BmStatus { final DeviceIdentifier remoteId; final int rssi; - final bool success; - final int errorCode; - final String errorString; - BmReadRssiResult({ - required this.remoteId, - required this.rssi, - required this.success, - required this.errorCode, - required this.errorString, - }); - - factory BmReadRssiResult.fromMap(Map json) { - return BmReadRssiResult( - remoteId: DeviceIdentifier(json['remote_id']), - rssi: json['rssi'], - success: json['success'] != 0, - errorCode: json['error_code'], - errorString: json['error_string'], - ); - } + BmReadRssiResult.fromMap(Map json) + : remoteId = DeviceIdentifier(json['remote_id']), + rssi = json['rssi'], + super.fromMap(json); } enum BmConnectionPriorityEnum { @@ -778,12 +504,10 @@ class BmConnectionPriorityRequest { required this.connectionPriority, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['connection_priority'] = connectionPriority.index; - return data; - } + Map toMap() => { + 'remote_id': remoteId.str, + 'connection_priority': connectionPriority.index, + }; } class BmPreferredPhy { @@ -799,23 +523,19 @@ class BmPreferredPhy { required this.phyOptions, }); - Map toMap() { - final Map data = {}; - data['remote_id'] = remoteId.str; - data['tx_phy'] = txPhy; - data['rx_phy'] = rxPhy; - data['phy_options'] = phyOptions; - return data; - } - - factory BmPreferredPhy.fromMap(Map json) { - return BmPreferredPhy( - remoteId: DeviceIdentifier(json['remote_id']), - txPhy: json['tx_phy'], - rxPhy: json['rx_phy'], - phyOptions: json['phy_options'], - ); - } + Map toMap() => { + 'remote_id': remoteId.str, + 'tx_phy': txPhy, + 'rx_phy': rxPhy, + 'phy_options': phyOptions, + }; + + factory BmPreferredPhy.fromMap(Map json) => BmPreferredPhy( + remoteId: DeviceIdentifier(json['remote_id']), + txPhy: json['tx_phy'], + rxPhy: json['rx_phy'], + phyOptions: json['phy_options'], + ); } enum BmBondStateEnum { @@ -829,34 +549,17 @@ class BmBondStateResponse { final BmBondStateEnum bondState; final BmBondStateEnum? prevState; - BmBondStateResponse({ - required this.remoteId, - required this.bondState, - required this.prevState, - }); - - factory BmBondStateResponse.fromMap(Map json) { - return BmBondStateResponse( - remoteId: DeviceIdentifier(json['remote_id']), - bondState: BmBondStateEnum.values[json['bond_state']], - prevState: json['prev_state'] != null ? BmBondStateEnum.values[json['prev_state']] : null, - ); - } + BmBondStateResponse.fromMap(Map json) + : remoteId = DeviceIdentifier(json['remote_id']), + bondState = BmBondStateEnum.values[json['bond_state']], + prevState = json['prev_state'] != null ? BmBondStateEnum.values[json['prev_state']] : null; } // BmTurnOnResponse class BmTurnOnResponse { bool userAccepted; - BmTurnOnResponse({ - required this.userAccepted, - }); - - factory BmTurnOnResponse.fromMap(Map json) { - return BmTurnOnResponse( - userAccepted: json['user_accepted'], - ); - } + BmTurnOnResponse.fromMap(Map json) : userAccepted = json['user_accepted'] != 0; } // random number defined by flutter blue plus. diff --git a/lib/src/bluetooth_service.dart b/lib/src/bluetooth_service.dart index d7c54c8f..9bc923a3 100644 --- a/lib/src/bluetooth_service.dart +++ b/lib/src/bluetooth_service.dart @@ -4,67 +4,31 @@ part of flutter_blue_plus; -class BluetoothService { - final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid? primaryServiceUuid; - final List characteristics; +class BluetoothService extends BluetoothAttribute { + final bool isPrimary; + late final List includedServices; + late final List characteristics; /// convenience accessor - Guid get uuid => serviceUuid; + Guid get serviceUuid => uuid; /// for convenience - bool get isPrimary => primaryServiceUuid == null; - - /// for convenience - bool get isSecondary => primaryServiceUuid != null; - - /// (for primary services) - /// get it's secondary services (i.e. includedServices) - List get includedServices { - List out = []; - if (FlutterBluePlus._knownServices[remoteId] != null) { - for (var s in FlutterBluePlus._knownServices[remoteId]!.services) { - if (s.primaryServiceUuid == serviceUuid) { - out.add(BluetoothService.fromProto(s)); - } - } - } - return out; - } - - /// (for secondary services) - /// get the primary service it is associated with - BluetoothService? get primaryService { - if (primaryServiceUuid != null) { - if (FlutterBluePlus._knownServices[remoteId] != null) { - for (var s in FlutterBluePlus._knownServices[remoteId]!.services) { - if (s.serviceUuid == primaryServiceUuid) { - return BluetoothService.fromProto(s); - } - } - } - } - return null; - } + bool get isSecondary => !isPrimary; /// for internal use - BluetoothService.fromProto(BmBluetoothService p) - : remoteId = p.remoteId, - serviceUuid = p.serviceUuid, - primaryServiceUuid = p.primaryServiceUuid, - characteristics = p.characteristics.map((c) => BluetoothCharacteristic.fromProto(c)).toList(); + BluetoothService.fromProto(BluetoothDevice device, BmBluetoothService p) + : isPrimary = p.isPrimary, + super(device: device, uuid: p.uuid, index: p.index) { + characteristics = p.characteristics.map((c) => BluetoothCharacteristic.fromProto(c, this)).toList(); + } @override String toString() { return 'BluetoothService{' 'remoteId: $remoteId, ' - 'serviceUuid: $serviceUuid, ' - 'primaryServiceUuid: $primaryServiceUuid, ' + 'isPrimary: $isPrimary, ' 'characteristics: $characteristics, ' + 'includedServices: $includedServices' '}'; } - - @Deprecated('Use remoteId instead') - DeviceIdentifier get deviceId => remoteId; } diff --git a/lib/src/flutter_blue_plus.dart b/lib/src/flutter_blue_plus.dart index fef3aa05..25740e82 100644 --- a/lib/src/flutter_blue_plus.dart +++ b/lib/src/flutter_blue_plus.dart @@ -16,20 +16,10 @@ class FlutterBluePlus { /// a broadcast stream version of the MethodChannel // ignore: close_sinks - static final StreamController _methodStream = StreamController.broadcast(); + static final StreamController _methodStream = StreamController.broadcast(); // always keep track of these device variables - static final Map _connectionStates = {}; - static final Map _knownServices = {}; - static final Map _bondStates = {}; - static final Map _mtuValues = {}; - static final Map _platformNames = {}; - static final Map _advNames = {}; - static final Map>> _lastChrs = {}; - static final Map>> _lastDescs = {}; - static final Map> _deviceSubscriptions = {}; - static final Map> _delayedSubscriptions = {}; - static final Map _connectTimestamp = {}; + static final Map _devices = {}; static final List _scanSubscriptions = []; static final Set _autoConnect = {}; @@ -40,16 +30,16 @@ class FlutterBluePlus { static final _scanResults = _StreamControllerReEmit>(initialValue: []); /// buffers the scan results - static _BufferStream? _scanBuffer; + static _BufferStream? _scanBuffer; /// the subscription to the merged scan results stream - static StreamSubscription? _scanSubscription; + static StreamSubscription? _scanSubscription; /// timeout for scanning that can be cancelled by stopScan static Timer? _scanTimeout; /// the last known adapter state - static BmAdapterStateEnum? _adapterStateNow; + static BluetoothAdapterState? _adapterStateNow; /// FlutterBluePlus log level static LogLevel _logLevel = LogLevel.debug; @@ -65,8 +55,7 @@ class FlutterBluePlus { static Future get isSupported async => await _invokeMethod('isSupported'); /// The current adapter state - static BluetoothAdapterState get adapterStateNow => - _adapterStateNow != null ? _bmToAdapterState(_adapterStateNow!) : BluetoothAdapterState.unknown; + static BluetoothAdapterState get adapterStateNow => _adapterStateNow ?? BluetoothAdapterState.unknown; /// Return the friendly Bluetooth name of the local Bluetooth adapter static Future get adapterName async => await _invokeMethod('getAdapterName'); @@ -117,13 +106,10 @@ class FlutterBluePlus { /// Turn on Bluetooth (Android only), static Future turnOn({int timeout = 60}) async { - var responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnTurnOnResponse") - .map((m) => m.arguments) - .map((args) => BmTurnOnResponse.fromMap(args)); + var responseStream = FlutterBluePlus._extractEventStream(); // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureResponse = responseStream.first; + Future futureResponse = responseStream.first; // invoke bool changed = await _invokeMethod('turnOn'); @@ -131,7 +117,7 @@ class FlutterBluePlus { // only wait if bluetooth was off if (changed) { // wait for response - BmTurnOnResponse response = await futureResponse.fbpTimeout(timeout, "turnOn"); + OnTurnOnResponseEvent response = await futureResponse.fbpTimeout(timeout, "turnOn"); // check response if (response.userAccepted == false) { @@ -151,24 +137,17 @@ class FlutterBluePlus { var value = BmBluetoothAdapterState.fromMap(result).adapterState; // update _adapterStateNow if it is still null after the await if (_adapterStateNow == null) { - _adapterStateNow = value; + _adapterStateNow = _bmToAdapterState(value); } } - yield* FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnAdapterStateChanged") - .map((m) => m.arguments) - .map((args) => BmBluetoothAdapterState.fromMap(args)) - .map((s) => _bmToAdapterState(s.adapterState)) - .newStreamWithInitialValue(_bmToAdapterState(_adapterStateNow!)); + yield* FlutterBluePlus._extractEventStream() + .map((s) => s.adapterState) + .newStreamWithInitialValue(_adapterStateNow!); } /// Retrieve a list of devices currently connected to your app - static List get connectedDevices { - var copy = Map.from(_connectionStates); - copy.removeWhere((key, value) => value.connectionState == BmConnectionStateEnum.disconnected); - return copy.values.map((v) => BluetoothDevice(remoteId: v.remoteId)).toList(); - } + static List get connectedDevices => _devices.values.where((d) => d.isConnected).toList(); /// Retrieve a list of devices currently connected to the system /// - The list includes devices connected to by *any* app @@ -177,24 +156,14 @@ class FlutterBluePlus { static Future> systemDevices(List withServices) async { var result = await _invokeMethod('getSystemDevices', {"with_services": withServices.map((s) => s.str).toList()}); var r = BmDevicesList.fromMap(result); - for (BmBluetoothDevice device in r.devices) { - if (device.platformName != null) { - _platformNames[device.remoteId] = device.platformName!; - } - } - return r.devices.map((d) => BluetoothDevice.fromId(d.remoteId.str)).toList(); + return r.devices.map((d) => FlutterBluePlus._deviceForId(d.remoteId).._platformName = d.platformName).toList(); } /// Retrieve a list of bonded devices (Android only) static Future> get bondedDevices async { var result = await _invokeMethod('getBondedDevices'); var r = BmDevicesList.fromMap(result); - for (BmBluetoothDevice device in r.devices) { - if (device.platformName != null) { - _platformNames[device.remoteId] = device.platformName!; - } - } - return r.devices.map((d) => BluetoothDevice.fromId(d.remoteId.str)).toList(); + return r.devices.map((d) => FlutterBluePlus._deviceForId(d.remoteId).._platformName = d.platformName).toList(); } /// Start a scan, and return a stream of results @@ -282,10 +251,7 @@ class FlutterBluePlus { androidScanMode: androidScanMode.value, androidUsesFineLocation: androidUsesFineLocation); - Stream responseStream = FlutterBluePlus._methodStream.stream - .where((m) => m.method == "OnScanResponse") - .map((m) => m.arguments) - .map((args) => BmScanResponse.fromMap(args)); + Stream responseStream = FlutterBluePlus._extractEventStream(); // Start listening now, before invokeMethod, so we do not miss any results _scanBuffer = _BufferStream.listen(responseStream); @@ -297,7 +263,7 @@ class FlutterBluePlus { }); // check every 250ms for gone devices? - late Stream outputStream = removeIfGone != null + late Stream outputStream = removeIfGone != null ? _mergeStreams([_scanBuffer!.stream, Stream.periodic(Duration(milliseconds: 250))]) : _scanBuffer!.stream; @@ -307,7 +273,7 @@ class FlutterBluePlus { List output = []; // listen & push to `scanResults` stream - _scanSubscription = outputStream.listen((BmScanResponse? response) { + _scanSubscription = outputStream.listen((OnScanResponseEvent? response) { if (response == null) { // if null, this is just a periodic update to remove old results if (output._removeWhere((elm) => DateTime.now().difference(elm.timeStamp) > removeIfGone!)) { @@ -315,26 +281,23 @@ class FlutterBluePlus { } } else { // failure? - if (response.success == false) { - var e = FlutterBluePlusException(_nativeError, "scan", response.errorCode, response.errorString); - _scanResults.addError(e); + final exception = response.exception("scan"); + if (exception != null) { + _scanResults.addError(exception); _stopScan(invokePlatform: false); } // iterate through advertisements - for (BmScanAdvertisement bm in response.advertisements) { + for (ScanResult sr in response.advertisements) { // cache platform name - if (bm.platformName != null) { - _platformNames[bm.remoteId] = bm.platformName!; - } + // TODO CRITICAL if (sr.advertisementData.platformName) { + // _platformNames[bm.remoteId] = bm.platformName!; + // } // cache advertised name - if (bm.advName != null) { - _advNames[bm.remoteId] = bm.advName!; - } - - // convert - ScanResult sr = ScanResult.fromProto(bm); + // TODO CRITICAL if (bm.advName != null) { + // _advNames[bm.remoteId] = bm.advName!; + // } if (oneByOne) { // push single item @@ -417,6 +380,10 @@ class FlutterBluePlus { return await _invokeMethod('getPhySupport').then((args) => PhySupport.fromMap(args)); } + static BluetoothDevice _deviceForId(DeviceIdentifier id) { + return _devices.putIfAbsent(id, () => BluetoothDevice._internal(remoteId: id)); + } + static Future _initFlutterBluePlus() async { if (_initialized) { return; @@ -425,7 +392,14 @@ class FlutterBluePlus { _initialized = true; // set platform method handler - _methodChannel.setMethodCallHandler(_methodCallHandler); + _methodChannel.setMethodCallHandler((call) async { + try { + return await _methodCallHandler(call); + } catch (e, s) { + print("[FBP] Error in methodCallHandler: $e $s"); + rethrow; + } + }); // flutter restart - wait for all devices to disconnect if ((await _methodChannel.invokeMethod('flutterRestart')) != 0) { @@ -436,6 +410,50 @@ class FlutterBluePlus { } } + static dynamic _methodCallMap(MethodCall call) { + if (call.method == OnDetachedFromEngineEvent.method) { + return OnDetachedFromEngineEvent(); + } else if (call.method == OnDiscoveredServicesEvent.method) { + BmDiscoverServicesResult r = BmDiscoverServicesResult.fromMap(call.arguments); + return OnDiscoveredServicesEvent(r); + } else if (call.method == OnAdapterStateChangedEvent.method) { + BmBluetoothAdapterState r = BmBluetoothAdapterState.fromMap(call.arguments); + return OnAdapterStateChangedEvent(r); + } else if (call.method == OnConnectionStateChangedEvent.method) { + BmConnectionStateResponse r = BmConnectionStateResponse.fromMap(call.arguments); + return OnConnectionStateChangedEvent(r); + } else if (call.method == OnBondStateChangedEvent.method) { + BmBondStateResponse r = BmBondStateResponse.fromMap(call.arguments); + return OnBondStateChangedEvent(r); + } else if (call.method == OnNameChangedEvent.method) { + BmNameChanged r = BmNameChanged.fromMap(call.arguments); + return OnNameChangedEvent(r); + } else if (call.method == OnServicesResetEvent.method) { + BmBluetoothDevice r = BmBluetoothDevice.fromMap(call.arguments); + return OnServicesResetEvent(r); + } else if (call.method == OnMtuChangedEvent.method) { + BmMtuChangedResponse r = BmMtuChangedResponse.fromMap(call.arguments); + return OnMtuChangedEvent(r); + } else if (call.method == OnCharacteristicReceivedEvent.method) { + BmCharacteristicData r = BmCharacteristicData.fromMap(call.arguments); + return OnCharacteristicReceivedEvent(r); + } else if (call.method == OnCharacteristicWrittenEvent.method) { + BmCharacteristicData r = BmCharacteristicData.fromMap(call.arguments); + return OnCharacteristicWrittenEvent(r); + } else if (call.method == OnDescriptorReadEvent.method) { + BmDescriptorData r = BmDescriptorData.fromMap(call.arguments); + return OnDescriptorReadEvent(r); + } else if (call.method == OnDescriptorWrittenEvent.method) { + BmDescriptorData r = BmDescriptorData.fromMap(call.arguments); + return OnDescriptorWrittenEvent(r); + } else if (call.method == OnScanResponseEvent.method) { + BmScanResponse r = BmScanResponse.fromMap(call.arguments); + return OnScanResponseEvent(r); + } else { + throw UnimplementedError("methodCallMap: ${call.method}"); + } + } + static Future _methodCallHandler(MethodCall call) async { // log result if (logLevel == LogLevel.verbose) { @@ -453,21 +471,22 @@ class FlutterBluePlus { print("[FBP] $func result: $result"); } + final event = _methodCallMap(call); + // android only - if (call.method == "OnDetachedFromEngine") { + if (event is OnDetachedFromEngineEvent) { _stopScan(invokePlatform: false); } // keep track of adapter states - if (call.method == "OnAdapterStateChanged") { - BmBluetoothAdapterState r = BmBluetoothAdapterState.fromMap(call.arguments); - _adapterStateNow = r.adapterState; - if (isScanningNow && r.adapterState != BmAdapterStateEnum.on) { + if (event is OnAdapterStateChangedEvent) { + _adapterStateNow = event.adapterState; + if (isScanningNow && event.adapterState != BluetoothAdapterState.on) { _stopScan(invokePlatform: false); } - if (r.adapterState == BmAdapterStateEnum.on) { + if (event.adapterState == BluetoothAdapterState.on) { for (DeviceIdentifier d in _autoConnect) { - BluetoothDevice(remoteId: d).connect(autoConnect: true, mtu: null).onError((e, s) { + FlutterBluePlus._deviceForId(d).connect(autoConnect: true, mtu: null).onError((e, s) { if (logLevel != LogLevel.none) { print("[FBP] [AutoConnect] connection failed: $e"); } @@ -477,28 +496,22 @@ class FlutterBluePlus { } // keep track of connection states - if (call.method == "OnConnectionStateChanged") { - var r = BmConnectionStateResponse.fromMap(call.arguments); - _connectionStates[r.remoteId] = r; - if (r.connectionState == BmConnectionStateEnum.disconnected) { + if (event is OnConnectionStateChangedEvent) { + event.device._connectionState = event.connectionState; + event.device._disconnectReason = event.disconnectReason; + if (event.connectionState == BluetoothConnectionState.disconnected) { // push to mtu stream, if needed - if (_mtuValues.containsKey(r.remoteId)) { - var resp = BmMtuChangedResponse(remoteId: r.remoteId, mtu: 23); - _methodStream.add(MethodCall("OnMtuChanged", resp.toMap())); + if (event.device._mtu != null) { + var resp = BmMtuChangedResponse(remoteId: event.device.remoteId, mtu: 23); + _methodStream.add(OnMtuChangedEvent(resp)); } // clear mtu - _mtuValues.remove(r.remoteId); - - // clear lastDescs (resets 'isNotifying') - _lastDescs.remove(r.remoteId); - - // clear lastChrs (api consistency) - _lastChrs.remove(r.remoteId); + event.device._mtu = null; // cancel & delete subscriptions - _deviceSubscriptions[r.remoteId]?.forEach((s) => s.cancel()); - _deviceSubscriptions.remove(r.remoteId); + event.device._subscriptions.forEach((s) => s.cancel()); + event.device._subscriptions.clear(); // Note: to make FBP easier to use, we do not clear `knownServices`, // otherwise `servicesList` would be more annoying to use. We also @@ -506,10 +519,9 @@ class FlutterBluePlus { // autoconnect if (Platform.isAndroid == false) { - if (_autoConnect.contains(r.remoteId)) { + if (_autoConnect.contains(event.device.remoteId)) { if (_adapterStateNow == BmAdapterStateEnum.on) { - var d = BluetoothDevice(remoteId: r.remoteId); - d.connect(autoConnect: true, mtu: null).onError((e, s) { + event.device.connect(autoConnect: true, mtu: null).onError((e, s) { if (logLevel != LogLevel.none) { print("[FBP] [AutoConnect] connection failed: $e"); } @@ -521,75 +533,51 @@ class FlutterBluePlus { } // keep track of device name - if (call.method == "OnNameChanged") { - var device = BmNameChanged.fromMap(call.arguments); + if (event is OnNameChangedEvent) { if (Platform.isMacOS || Platform.isIOS) { // iOS & macOS internally use the name changed callback for the platform name - _platformNames[device.remoteId] = device.name; + event.device._platformName = event.name; } } // keep track of services resets - if (call.method == "OnServicesReset") { - var r = BmBluetoothDevice.fromMap(call.arguments); - _knownServices.remove(r.remoteId); + if (event is OnServicesResetEvent) { + event.device._services.clear(); } // keep track of bond state - if (call.method == "OnBondStateChanged") { - var r = BmBondStateResponse.fromMap(call.arguments); - _bondStates[r.remoteId] = r; + if (event is OnBondStateChangedEvent) { + event.device._bondState = event.bondState; + event.device._prevBondState = event.prevState; } // keep track of services - if (call.method == "OnDiscoveredServices") { - var r = BmDiscoverServicesResult.fromMap(call.arguments); - if (r.success == true) { - _knownServices[r.remoteId] = r; - } + if (event is OnDiscoveredServicesEvent) { + event.device._services = event._constructServices(); } // keep track of mtu values - if (call.method == "OnMtuChanged") { - var r = BmMtuChangedResponse.fromMap(call.arguments); - if (r.success == true) { - _mtuValues[r.remoteId] = r; - } + if (event is OnMtuChangedEvent) { + event.device._mtu = event.mtu; } - // keep track of characteristic values - if (call.method == "OnCharacteristicReceived" || call.method == "OnCharacteristicWritten") { - var r = BmCharacteristicData.fromMap(call.arguments); - if (r.success == true) { - _lastChrs[r.remoteId] ??= {}; - _lastChrs[r.remoteId]!["${r.serviceUuid}:${r.characteristicUuid}"] = r.value; - } + // keep track of characteristic & descriptor values + if (event is OnCharacteristicReceivedEvent || + event is OnCharacteristicWrittenEvent || + event is OnDescriptorReadEvent || + event is OnDescriptorWrittenEvent) { + (event as GetAttributeValueMixin).attribute._lastValue = event.value; } - // keep track of descriptor values - if (call.method == "OnDescriptorRead" || call.method == "OnDescriptorWritten") { - var r = BmDescriptorData.fromMap(call.arguments); - if (r.success == true) { - _lastDescs[r.remoteId] ??= {}; - _lastDescs[r.remoteId]!["${r.serviceUuid}:${r.characteristicUuid}:${r.descriptorUuid}"] = r.value; - } - } - - _methodStream.add(call); + _methodStream.add(event); // cancel delayed subscriptions - if (call.method == "OnConnectionStateChanged") { - if (_delayedSubscriptions.isNotEmpty) { - var r = BmConnectionStateResponse.fromMap(call.arguments); - if (r.connectionState == BmConnectionStateEnum.disconnected) { - var remoteId = r.remoteId; - // use delayed to update the stream before we cancel it - Future.delayed(Duration.zero).then((_) { - _delayedSubscriptions[remoteId]?.forEach((s) => s.cancel()); // cancel - _delayedSubscriptions.remove(remoteId); // delete - }); - } - } + if (event is OnConnectionStateChangedEvent && event.connectionState == BluetoothConnectionState.disconnected) { + // use delayed to update the stream before we cancel it + Future.delayed(Duration.zero).then((_) { + event.device._delayedSubscriptions.forEach((s) => s.cancel()); // cancel + event.device._delayedSubscriptions.clear(); // delete + }); } } @@ -638,6 +626,18 @@ class FlutterBluePlus { return out; } + static Future _invokeMethodAndWaitForEvent(String method, dynamic arguments, bool test(T event)) async { + Stream responseStream = _extractEventStream(test); + Future futureResponse = responseStream.first; + await _invokeMethod(method, arguments); + return futureResponse; + } + + /// Extract stream event + static Stream _extractEventStream([bool test(T event)?]) { + return _methodStream.stream.where((m) => m is T).map((m) => m as T).where(test ?? (_) => true); + } + /// Turn off Bluetooth (Android only), @Deprecated('Deprecated in Android SDK 33 with no replacement') static Future turnOff({int timeout = 10}) async { @@ -762,35 +762,37 @@ class DeviceIdentifier { } class ScanResult { - final BluetoothDevice device; + final DeviceIdentifier remoteId; final AdvertisementData advertisementData; final int rssi; final DateTime timeStamp; ScanResult({ - required this.device, + required this.remoteId, required this.advertisementData, required this.rssi, required this.timeStamp, }); ScanResult.fromProto(BmScanAdvertisement p) - : device = BluetoothDevice(remoteId: p.remoteId), + : remoteId = p.remoteId, advertisementData = AdvertisementData.fromProto(p), rssi = p.rssi, timeStamp = DateTime.now(); + BluetoothDevice get device => FlutterBluePlus._deviceForId(remoteId); + @override bool operator ==(Object other) => - identical(this, other) || other is ScanResult && runtimeType == other.runtimeType && device == other.device; + identical(this, other) || other is ScanResult && runtimeType == other.runtimeType && remoteId == other.remoteId; @override - int get hashCode => device.hashCode; + int get hashCode => remoteId.hashCode; @override String toString() { return 'ScanResult{' - 'device: $device, ' + 'remoteId: $remoteId, ' 'advertisementData: $advertisementData, ' 'rssi: $rssi, ' 'timeStamp: $timeStamp' @@ -800,6 +802,7 @@ class ScanResult { class AdvertisementData { final String advName; + final String platformName; final int? txPowerLevel; final int? appearance; // not supported on iOS / macOS final bool connectable; @@ -822,6 +825,7 @@ class AdvertisementData { AdvertisementData({ required this.advName, + required this.platformName, required this.txPowerLevel, required this.appearance, required this.connectable, @@ -832,6 +836,7 @@ class AdvertisementData { AdvertisementData.fromProto(BmScanAdvertisement p) : advName = p.advName ?? "", + platformName = p.platformName ?? "", txPowerLevel = p.txPowerLevel, appearance = p.appearance, connectable = p.connectable, @@ -876,7 +881,9 @@ class PhySupport { enum ErrorPlatform { fbp, android, - apple, + apple + + // TODO DART 2.17 static ErrorPlatform get native => Platform.isAndroid ? android : apple; } final ErrorPlatform _nativeError = (() {