From a92fe8d51245a8385034ceed92e0ffdf4ff7b4e4 Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Sun, 6 Sep 2026 21:32:50 +0200 Subject: [PATCH 1/3] Add function to check if a characteristic is subscribed --- .../ble_characteristic_extension.dart | 8 ++++ lib/src/universal_ble.dart | 21 +++++++++- lib/src/utils/cache_handler.dart | 39 ++++++++++++++++- test/ble_characteristic_test.dart | 42 +++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/lib/src/extensions/ble_characteristic_extension.dart b/lib/src/extensions/ble_characteristic_extension.dart index 04c198d7..9addbecf 100644 --- a/lib/src/extensions/ble_characteristic_extension.dart +++ b/lib/src/extensions/ble_characteristic_extension.dart @@ -37,6 +37,11 @@ extension BleCharacteristicExtension on BleCharacteristic { queueId: queueId, ); + /// Returns whether this characteristic is currently subscribed to. + bool get isSubscribed => + metaData?.deviceId != null && + UniversalBle.isSubscribed(metaData!.deviceId, uuid); + /// Reads the current value of the characteristic. Future read({Duration? timeout, String? queueId}) => UniversalBle.read( @@ -187,6 +192,9 @@ class CharacteristicSubscription { ); } + /// Returns whether this characteristic is currently subscribed to. + bool get isSubscribed => _characteristic.isSubscribed; + @override String toString() => "CharacteristicSubscription(property: ${_property.name}, isSupported: $isSupported, characteristic: ${_characteristic.uuid})"; diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index ac419410..092fbb1f 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:universal_ble/src/utils/ble_command_queue.dart'; import 'package:universal_ble/src/universal_ble_linux/universal_ble_linux_instance.dart'; import 'package:universal_ble/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart'; import 'package:universal_ble/src/universal_ble_web/universal_ble_web.dart'; +import 'package:universal_ble/src/utils/ble_command_queue.dart'; +import 'package:universal_ble/src/utils/cache_handler.dart'; import 'package:universal_ble/src/utils/universal_logger.dart'; import 'package:universal_ble/universal_ble.dart'; @@ -311,6 +312,17 @@ class UniversalBle { ); } + /// Returns whether this app is currently subscribed to notifications/indications for [characteristic]. + /// + /// Subscription state is updated when [subscribeNotifications], [subscribeIndications], + /// or [unsubscribe] completes, and is automatically cleared when the device disconnects. + static bool isSubscribed(String deviceId, String characteristic) => + CacheHandler.instance.isSubscribed(deviceId, characteristic); + + /// Returns the list of characteristic UUIDs currently subscribed to on [deviceId]. + static List getSubscribedCharacteristics(String deviceId) => + CacheHandler.instance.getSubscribedCharacteristics(deviceId); + /// Read a characteristic value. /// On iOS and MacOS this command will also trigger [onValueChange] listener. static Future read( @@ -776,7 +788,7 @@ class UniversalBle { Duration? timeout, String? queueId, }) async { - return await _bleCommandQueue.queueCommand( + await _bleCommandQueue.queueCommand( () => _platform.setNotifiable( deviceId, BleUuidParser.string(service), @@ -787,6 +799,11 @@ class UniversalBle { timeout: timeout, queueId: queueId, ); + CacheHandler.instance.updateSubscription( + deviceId, + characteristic, + bleInputProperty != BleInputProperty.disabled, + ); } static Future _connectAndExecuteBleCommand( diff --git a/lib/src/utils/cache_handler.dart b/lib/src/utils/cache_handler.dart index 25bbc3d1..df19ac6a 100644 --- a/lib/src/utils/cache_handler.dart +++ b/lib/src/utils/cache_handler.dart @@ -27,8 +27,45 @@ class CacheHandler { /// Retrieves the cached Bluetooth services for a specific device. List? getServices(String deviceId) => _servicesCache[_key(deviceId)]; - /// Resets the cache for a specific device, removing all stored services. + /// Internal cache to store subscribed characteristic UUIDs for each device. + final Map> _subscriptionsCache = {}; + + /// Updates the subscription state of a characteristic for a specific device. + void updateSubscription( + String deviceId, + String characteristicId, + bool isSubscribed, + ) { + final key = _key(deviceId); + final normalizedCharId = BleUuidParser.string(characteristicId); + if (isSubscribed) { + (_subscriptionsCache[key] ??= {}).add(normalizedCharId); + } else { + _subscriptionsCache[key]?.remove(normalizedCharId); + if (_subscriptionsCache[key]?.isEmpty ?? false) { + _subscriptionsCache.remove(key); + } + } + } + + /// Checks if a characteristic is subscribed to on a specific device. + bool isSubscribed(String deviceId, String characteristicId) { + try { + final normalizedCharId = BleUuidParser.string(characteristicId); + return _subscriptionsCache[_key(deviceId)]?.contains(normalizedCharId) ?? + false; + } catch (_) { + return false; + } + } + + /// Retrieves the list of subscribed characteristic UUIDs for a specific device. + List getSubscribedCharacteristics(String deviceId) => + _subscriptionsCache[_key(deviceId)]?.toList() ?? []; + + /// Resets the cache for a specific device, removing all stored services and subscriptions. void resetDeviceCache(String deviceId) { _servicesCache.remove(_key(deviceId)); + _subscriptionsCache.remove(_key(deviceId)); } } diff --git a/test/ble_characteristic_test.dart b/test/ble_characteristic_test.dart index 353ca6f7..af372952 100644 --- a/test/ble_characteristic_test.dart +++ b/test/ble_characteristic_test.dart @@ -53,9 +53,31 @@ void main() { test("Subscription Test", () async { BleCharacteristic characteristic = mockBleCharacteristic; + expect(characteristic.isSubscribed, false); + expect(characteristic.notifications.isSubscribed, false); + expect(UniversalBle.isSubscribed(mockDeviceId, characteristicId), false); + expect(UniversalBle.getSubscribedCharacteristics(mockDeviceId), isEmpty); + debugPrint("Subscribing to char"); await characteristic.notifications.subscribe(); + expect(characteristic.isSubscribed, true); + expect(characteristic.notifications.isSubscribed, true); + expect(UniversalBle.isSubscribed(mockDeviceId, characteristicId), true); + // Case-insensitivity check for deviceId and normalized UUID check + expect( + UniversalBle.isSubscribed(mockDeviceId.toUpperCase(), characteristicId), + true, + ); + expect( + UniversalBle.isSubscribed(mockDeviceId, characteristic.uuid), + true, + ); + expect( + UniversalBle.getSubscribedCharacteristics(mockDeviceId), + contains(characteristic.uuid), + ); + bool gotEvent = false; var subscription = characteristic.notifications.listen((data) { debugPrint("Received CharValue: $data"); @@ -66,9 +88,29 @@ void main() { await characteristic.notifications.unsubscribe(); debugPrint("Unsubscribed from char"); + expect(characteristic.isSubscribed, false); + expect(characteristic.notifications.isSubscribed, false); + expect(UniversalBle.isSubscribed(mockDeviceId, characteristicId), false); + expect(UniversalBle.getSubscribedCharacteristics(mockDeviceId), isEmpty); + subscription.cancel(); expect(gotEvent, true); }); + + test("isSubscribed resets on device disconnect", () async { + BleCharacteristic characteristic = mockBleCharacteristic; + await characteristic.notifications.subscribe(); + expect(characteristic.isSubscribed, true); + expect(UniversalBle.isSubscribed(mockDeviceId, characteristicId), true); + + // Simulate device disconnect + platform.updateConnection(mockDeviceId, false); + + expect(characteristic.isSubscribed, false); + expect(characteristic.notifications.isSubscribed, false); + expect(UniversalBle.isSubscribed(mockDeviceId, characteristicId), false); + expect(UniversalBle.getSubscribedCharacteristics(mockDeviceId), isEmpty); + }); }); test("Write/Read Value Test", () async { From 9c07e0af14e97c34878aa125807b026d3fbab27d Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Sun, 6 Sep 2026 21:47:20 +0200 Subject: [PATCH 2/3] feat: add characteristic subscription status methods to Central mode --- CHANGELOG.md | 1 + README.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f77adcb6..e9cf7e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Android: add `closeGattOnDetach` connection option to release GATT clients when the app is killed * Android: close the GATT client once the disconnect completes instead of right after `disconnect()`, and report the real disconnect status * Android: Fix peripheral `getReadinessState()` to check permissions and adapter power before advertising support, and throttle `startAdvertising` Bluetooth enable prompts to at most one dialog. +* Add `isSubscribed` and `getSubscribedCharacteristics` to check characteristic notification/indication subscription status in Central mode. ## 2.2.0 diff --git a/README.md b/README.md index 2b5ffa6c..214e5d35 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,23 @@ Unsubscribe from notifications and indications of this characteristic. await characteristic.unsubscribe(); ``` +### Check Subscription Status + +Check if a characteristic is currently subscribed to: + +```dart +// Via BleCharacteristic or CharacteristicSubscription +bool isSubscribed = characteristic.isSubscribed; +// or +bool isSubscribed = characteristic.notifications.isSubscribed; + +// Or globally via UniversalBle +bool isSubscribed = UniversalBle.isSubscribed(deviceId, characteristicId); + +// Get all subscribed characteristic UUIDs for a device +List subscribedChars = UniversalBle.getSubscribedCharacteristics(deviceId); +``` + ### Pairing #### Trigger pairing From 7ce034ae3fa0c3bc1f69aa3e712f8178a5fc8191 Mon Sep 17 00:00:00 2001 From: Navideck Labs <130186950+navidecklabs@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:02:47 +0200 Subject: [PATCH 3/3] Add teardown for notifierTimer in subscription test Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/ble_characteristic_test.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/ble_characteristic_test.dart b/test/ble_characteristic_test.dart index af372952..f8088ffa 100644 --- a/test/ble_characteristic_test.dart +++ b/test/ble_characteristic_test.dart @@ -100,6 +100,11 @@ void main() { test("isSubscribed resets on device disconnect", () async { BleCharacteristic characteristic = mockBleCharacteristic; await characteristic.notifications.subscribe(); + addTearDown(() { + final mock = platform as _UniversalBleMock; + mock.notifierTimer?.cancel(); + mock.notifierTimer = null; + }); expect(characteristic.isSubscribed, true); expect(UniversalBle.isSubscribed(mockDeviceId, characteristicId), true);