Implement isScanning api (#188)

* Implement isScanning api

* Improve IOS scanning api

* Update Readme and Changelog
This commit is contained in:
Rohit Sangwan
2025-11-12 19:27:44 +05:30
committed by GitHub
parent c3b97e414f
commit 34b39597fd
23 changed files with 193 additions and 12 deletions
+1
View File
@@ -1,5 +1,6 @@
## 1.0.0
* Unified error codes for all platforms
* Add `isScanning` api
## 0.21.1
* Fix device name resolution on Windows
+3
View File
@@ -80,6 +80,9 @@ UniversalBle.startScan(
// Stop scanning
UniversalBle.stopScan();
// Check if scanning
UniversalBle.isScanning();
```
Before initiating a scan, ensure that Bluetooth is available:
@@ -36,6 +36,7 @@ class SafeScanner(private val bluetoothManager: BluetoothManager) {
private val handler = Handler(Looper.myLooper()!!)
private val startTimes = LinkedList<Long>()
private var awaitingScan = false
private var isScanning = false
fun startScan(filters: List<ScanFilter>, settings: ScanSettings, callback: ScanCallback) {
val now = System.currentTimeMillis()
@@ -52,7 +53,7 @@ class SafeScanner(private val bluetoothManager: BluetoothManager) {
}
awaitingScan = true
val delay = startTimes.first + EXCESSIVE_SCANNING_PERIOD_MS - now + 2_000
val delay = startTimes.first() + EXCESSIVE_SCANNING_PERIOD_MS - now + 2_000
Log.e(TAG, "startScan: too frequent, schedule auto-start after $delay ms $startTimes")
handler.postDelayed({
@@ -65,8 +66,10 @@ class SafeScanner(private val bluetoothManager: BluetoothManager) {
startTimes.addLast(now)
try {
bluetoothManager.adapter.bluetoothLeScanner?.startScan(filters, settings, callback)
isScanning = true
} catch (e: Exception) {
Log.e(TAG, "Failed to start Scan : $e")
isScanning = false
}
}
}
@@ -75,5 +78,10 @@ class SafeScanner(private val bluetoothManager: BluetoothManager) {
awaitingScan = false
handler.removeCallbacksAndMessages(null)
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(callback)
isScanning = false
}
fun isScanning(): Boolean {
return isScanning || awaitingScan
}
}
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.5), do not edit directly.
// Autogenerated from Pigeon (v26.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -447,6 +447,7 @@ interface UniversalBlePlatformChannel {
fun disableBluetooth(callback: (Result<Boolean>) -> Unit)
fun startScan(filter: UniversalScanFilter?)
fun stopScan()
fun isScanning(): Boolean
fun connect(deviceId: String)
fun disconnect(deviceId: String)
fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Long, callback: (Result<Unit>) -> Unit)
@@ -557,6 +558,21 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isScanning$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.isScanning())
} catch (exception: Throwable) {
UniversalBlePigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -187,6 +187,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
safeScanner.stopScan(scanCallback)
}
override fun isScanning(): Boolean {
return safeScanner.isScanning()
}
override fun connect(deviceId: String) {
// If already connected, send connected message,
// if connecting, do nothing
+15 -1
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.5), do not edit directly.
// Autogenerated from Pigeon (v26.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -477,6 +477,7 @@ protocol UniversalBlePlatformChannel {
func disableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void)
func startScan(filter: UniversalScanFilter?) throws
func stopScan() throws
func isScanning() throws -> Bool
func connect(deviceId: String) throws
func disconnect(deviceId: String) throws
func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64, completion: @escaping (Result<Void, Error>) -> Void)
@@ -570,6 +571,19 @@ class UniversalBlePlatformChannelSetup {
} else {
stopScanChannel.setMessageHandler(nil)
}
let isScanningChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isScanning\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
isScanningChannel.setMessageHandler { _, reply in
do {
let result = try api.isScanning()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
isScanningChannel.setMessageHandler(nil)
}
let connectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
connectChannel.setMessageHandler { message, reply in
+22
View File
@@ -39,6 +39,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
private var characteristicWriteWithoutResponseFutures = [CharacteristicWriteFuture]()
private var characteristicNotifyFutures = [CharacteristicNotifyFuture]()
private var discoverServicesFutures = [DiscoverServicesFuture]()
private var isManageScanning = false
init(callbackChannel: UniversalBleCallbackChannel) {
self.callbackChannel = callbackChannel
@@ -82,10 +83,31 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
let options = [CBCentralManagerScanOptionAllowDuplicatesKey: true]
manager.scanForPeripherals(withServices: withServices, options: options)
isManageScanning = true
}
func stopScan() throws {
manager.stopScan()
isManageScanning = false
}
func isScanning() throws -> Bool {
var hasAuthorization = true
#if os(iOS)
if #available(iOS 13.1, *) {
hasAuthorization = CBCentralManager.authorization == .allowedAlways
} else {
return isManageScanning
}
#elseif os(macOS)
hasAuthorization = CBCentralManager.authorization == .allowedAlways
#endif
if hasAuthorization {
return manager.isScanning
}
return isManageScanning
}
func connect(deviceId: String) throws {
+13 -3
View File
@@ -13,6 +13,7 @@ class MockUniversalBle extends UniversalBlePlatform {
);
Uint8List _serviceValue = utf8.encode('Result');
bool _isScanning = false;
final BleService _mockService = BleService('180', [
BleCharacteristic('180A', [
@@ -26,11 +27,20 @@ class MockUniversalBle extends UniversalBlePlatform {
Future<void> startScan({
ScanFilter? scanFilter,
PlatformConfig? platformConfig,
}) async =>
updateScanResult(_mockBleDevice);
}) async {
_isScanning = true;
updateScanResult(_mockBleDevice);
}
@override
Future<void> stopScan() async {}
Future<void> stopScan() async {
_isScanning = false;
}
@override
Future<bool> isScanning() async {
return _isScanning;
}
@override
Future<void> connect(String deviceId, {Duration? connectionTimeout}) async {
+7
View File
@@ -63,6 +63,13 @@ class _MyAppState extends State<MyApp> {
// UniversalBle.onQueueUpdate = (String id, int remainingItems) {
// debugPrint("Queue: $id RemainingItems: $remainingItems");
// };
UniversalBle.isScanning().then((value) {
debugPrint("Is Scanning: $value");
setState(() {
_isScanning = value;
});
});
}
void trackAvailabilityState() {
+2 -2
View File
@@ -20,9 +20,9 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
SPEC CHECKSUMS:
device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
universal_ble: ff19787898040d721109c6324472e5dd4bc86adc
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3
+8
View File
@@ -88,6 +88,14 @@ class UniversalBle {
);
}
/// Check if currently scanning for devices.
/// Returns `true` if scanning is active, `false` otherwise.
static Future<bool> isScanning() async {
return await _bleCommandQueue.queueCommand(
() => _platform.isScanning(),
);
}
/// Connect to a device.
/// It is advised to stop scanning before connecting.
/// It throws error if device connection fails.
@@ -125,6 +125,12 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
}
@override
Future<bool> isScanning() async {
await _ensureInitialized();
return _activeAdapter?.discovering == true;
}
@override
Future<BleConnectionState> getConnectionState(String deviceId) async {
BlueZDevice? device = _devices[deviceId] ??
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.5), do not edit directly.
// Autogenerated from Pigeon (v26.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
@@ -633,6 +633,36 @@ class UniversalBlePlatformChannel {
}
}
Future<bool> isScanning() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isScanning$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
Future<void> connect(String deviceId) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$pigeonVar_messageChannelSuffix';
@@ -57,6 +57,10 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
Future<void> stopScan() =>
_executeWithErrorHandling(() => _channel.stopScan());
@override
Future<bool> isScanning() =>
_executeWithErrorHandling(() => _channel.isScanning());
@override
Future<BleConnectionState> getConnectionState(String deviceId) async {
int state = await _executeWithErrorHandling(
@@ -43,6 +43,8 @@ abstract class UniversalBlePlatform {
Future<void> stopScan();
Future<bool> isScanning();
Future<void> connect(String deviceId, {Duration? connectionTimeout});
Future<void> disconnect(String deviceId);
@@ -20,6 +20,7 @@ class UniversalBleWeb extends UniversalBlePlatform {
final Map<String, StreamSubscription> _connectedDeviceStreamList = {};
final Map<String, StreamSubscription> _characteristicStreamList = {};
final Map<String, List<_UniversalWebBluetoothService>> _serviceCache = {};
bool _isScanning = false;
@override
Future<BleConnectionState> getConnectionState(String deviceId) async {
@@ -83,6 +84,7 @@ class UniversalBleWeb extends UniversalBlePlatform {
PlatformConfig? platformConfig,
}) async {
try {
_isScanning = true;
FlutterWebBluetooth.instance.isAvailable;
BluetoothDevice device = await FlutterWebBluetooth.instance.requestDevice(
_getRequestOptionBuilder(scanFilter, platformConfig?.web),
@@ -101,6 +103,8 @@ class UniversalBleWeb extends UniversalBlePlatform {
throw WebBluetoothGloballyDisabled(message: error);
}
rethrow;
} finally {
_isScanning = false;
}
}
@@ -146,6 +150,9 @@ class UniversalBleWeb extends UniversalBlePlatform {
_disposeAdvertisementWatcher();
}
@override
Future<bool> isScanning() async => _isScanning;
@override
Future<void> setNotifiable(
String deviceId,
+2
View File
@@ -34,6 +34,8 @@ abstract class UniversalBlePlatformChannel {
void stopScan();
bool isScanning();
void connect(String deviceId);
void disconnect(String deviceId);
+1 -1
View File
@@ -38,7 +38,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
pigeon: ^26.0.5
pigeon: ^26.1.0
flutter:
plugin:
+5
View File
@@ -95,4 +95,9 @@ abstract class UniversalBlePlatformMock extends UniversalBlePlatform {
BleOutputProperty bleOutputProperty) {
throw UnimplementedError();
}
@override
Future<bool> isScanning() {
throw UnimplementedError();
}
}
+22 -1
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.5), do not edit directly.
// Autogenerated from Pigeon (v26.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
#undef _HAS_EXCEPTIONS
@@ -623,6 +623,27 @@ void UniversalBlePlatformChannel::SetUp(
channel.SetMessageHandler(nullptr);
}
}
{
BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isScanning" + prepended_suffix, &GetCodec());
if (api != nullptr) {
channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply<EncodableValue>& reply) {
try {
ErrorOr<bool> output = api->IsScanning();
if (output.has_error()) {
reply(WrapError(output.error()));
return;
}
EncodableList wrapped;
wrapped.push_back(EncodableValue(std::move(output).TakeValue()));
reply(EncodableValue(std::move(wrapped)));
} catch (const std::exception& exception) {
reply(WrapError(exception.what()));
}
});
} else {
channel.SetMessageHandler(nullptr);
}
}
{
BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect" + prepended_suffix, &GetCodec());
if (api != nullptr) {
+2 -1
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.5), do not edit directly.
// Autogenerated from Pigeon (v26.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
#ifndef PIGEON_UNIVERSAL_BLE_G_H_
@@ -352,6 +352,7 @@ class UniversalBlePlatformChannel {
virtual void DisableBluetooth(std::function<void(ErrorOr<bool> reply)> result) = 0;
virtual std::optional<FlutterError> StartScan(const UniversalScanFilter* filter) = 0;
virtual std::optional<FlutterError> StopScan() = 0;
virtual ErrorOr<bool> IsScanning() = 0;
virtual std::optional<FlutterError> Connect(const std::string& device_id) = 0;
virtual std::optional<FlutterError> Disconnect(const std::string& device_id) = 0;
virtual void SetNotifiable(
+9
View File
@@ -212,6 +212,15 @@ std::optional<FlutterError> UniversalBlePlugin::StopScan() {
}
};
ErrorOr<bool> UniversalBlePlugin::IsScanning() {
if (bluetooth_le_watcher_ != nullptr) {
try {
return bluetooth_le_watcher_.Status() == BluetoothLEAdvertisementWatcherStatus::Started;
} catch (...) {}
}
return false;
}
ErrorOr<int64_t>
UniversalBlePlugin::GetConnectionState(const std::string &device_id) {
const auto it = connected_devices_.find(str_to_mac_address(device_id));
+1
View File
@@ -158,6 +158,7 @@ namespace universal_ble
ErrorOr<int64_t> GetConnectionState(const std::string &device_id) override;
std::optional<FlutterError> StartScan(const UniversalScanFilter *filter) override;
std::optional<FlutterError> StopScan() override;
ErrorOr<bool> IsScanning() override;
std::optional<FlutterError> Connect(const std::string &device_id) override;
std::optional<FlutterError> Disconnect(const std::string &device_id) override;
void DiscoverServices(