Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* 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.
* readRssi commands are not queued anymore
* iOS/macOS: complete only the oldest matching pending write on didWriteValueFor


## 2.2.0
* Expose microsecond scan timestamps captured before Flutter event dispatch
* Lower minimum Dart SDK to 3.3 (Flutter 3.19+) to restore compatibility with older stable Flutter releases
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> subscribedChars = UniversalBle.getSubscribedCharacteristics(deviceId);
```

### Pairing

#### Trigger pairing
Expand Down
8 changes: 8 additions & 0 deletions lib/src/extensions/ble_characteristic_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8List> read({Duration? timeout, String? queueId}) =>
UniversalBle.read(
Expand Down Expand Up @@ -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})";
Expand Down
21 changes: 19 additions & 2 deletions lib/src/universal_ble.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -312,6 +313,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<String> getSubscribedCharacteristics(String deviceId) =>
CacheHandler.instance.getSubscribedCharacteristics(deviceId);

/// Read a characteristic value.
/// On iOS and MacOS this command will also trigger [onValueChange] listener.
static Future<Uint8List> read(
Expand Down Expand Up @@ -768,7 +780,7 @@ class UniversalBle {
Duration? timeout,
String? queueId,
}) async {
return await _bleCommandQueue.queueCommand(
await _bleCommandQueue.queueCommand(
() => _platform.setNotifiable(
deviceId,
BleUuidParser.string(service),
Expand All @@ -779,6 +791,11 @@ class UniversalBle {
timeout: timeout,
queueId: queueId,
);
CacheHandler.instance.updateSubscription(
deviceId,
characteristic,
bleInputProperty != BleInputProperty.disabled,
);
}

static Future<void> _connectAndExecuteBleCommand(
Expand Down
39 changes: 38 additions & 1 deletion lib/src/utils/cache_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,45 @@ class CacheHandler {
/// Retrieves the cached Bluetooth services for a specific device.
List<BleService>? 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<String, Set<String>> _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<String> 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));
}
}
47 changes: 47 additions & 0 deletions test/ble_characteristic_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -66,9 +88,34 @@ 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();
addTearDown(() {
final mock = platform as _UniversalBleMock;
mock.notifierTimer?.cancel();
mock.notifierTimer = null;
});
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 {
Expand Down
Loading