[Help needed] Return a list of discovered BT peripherals

moved the post to the correct category
Hey,
I'm stuck for a while now and I kindly ask for some help.
I want to scan for BT-peripherals with a specific name and provide those in a list to use in SwiftUI.

My current (somehow working) approach is this:

class BT public class API: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, ObservableObject {
@Published var discoveredDevices = [CBPeripheral]()

    public func scan(timeout: Double, completion: @escaping (([CBPeripheral]) -> Void))
    {
        guard connectionState == STATE.DISCONNECTED else {
            
            print ("Can only start scan from DISCONNECTED")
            return
        }
        discoveredDevices = []

      
        let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: NSNumber(value: false)]

        centralManager.scanForPeripherals(withServices: nil, options: options)

        DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {            
            self.stopScan()
            completion(self. discoveredDevices)
        }
    }


public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        
        peripheral.delegate = self

        if (peripheral.name?.contains("testDevice") ?? false) {
            discoveredDevices.append(peripheral)
        }
    }
}

But I am not happy with that. I would like to use async/await in the scan-function and return the list, so I can use something like this in SwiftUI:

let devices = try await scan(timeout:5) 

I tried around a lot already, but I cannot manage to
"collect" devices within a specified time and return those through the scan-function.

Where I cannot wrap my head around is that the delegate-method (didDiscover) gets called with every discovered device. How can I let the scan-function know when the scanning is done (timeout ran out) and only then return the list of the discovered devices?

Thanks in advance already

I don’t know a lot about Core Bluetooth [1], so I can’t help you with that side of this, but I’m happy to tackle this in a more abstract sense.

I tried around a lot already, but I cannot manage to "collect" devices
within a specified time and return those through the scan-function.

When scanning for services it’s best not to impose a timeout. The problem is that, whatever timeout you choose, you end up with suboptimal behaviour:

  • If the timeout is too long, you needlessly delay the display of services you’ve already found.

  • If the timeout is too short, you might just miss a service.

A better approach is to consider this operation as an ongoing source of add and remove events. That way you can continually monitor that source while the user is interested in your service list.

And once you think about things in those terms, it suggests a better way to handle this in Swift, namely as an AsyncSequence. See here.

I’ve had great success implementing this using AsyncStream. See here.

The main gotcha with the latter is flow control (aka back pressure). AsyncStream doesn’t have comprehensive flow control support, so you have to be careful when dealing with potentially infinite streams. For example, if services were coming and going at a furious rate, the buffer inside the AsyncStream could grow very large.

In practice, this isn’t really an issue for your situation, where you’re going to be able to consume these events faster than Core Bluetooth can generate them. That’s not true in other situations, for example, if you were using AsyncStream to model a TCP connection is an async sequence of bytes.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] For the specifics, I recommend Apple Developer Forums, tagging your question with Core Bluetooth.

1 Like