MW2023
MW2023

Reputation: 313

iOS Swift - Question about semaphores and sequencing in Bluetooth Low Energy Manager

I'm looking at the open source Bluefruit code as an example, specifically the BleManager class that is the interface with the CBCentralManager. I've emailed the author and they didn't respond:

Bluefruit BleManager class

It looks like the class is designed in a singleton architecture ("shared" on line 23) and so is constructed lazily when the first "BleManager.connect() is called elsewhere in the code.

What confuses me is in the init(), there's a semaphore "wait" function:

override init() {
    super.init()

    centralManagerPoweredOnSemaphore.wait()
    centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.global(qos: .background), options: [:])
//        centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main, options: [:])
    }

I'm really only used to semaphores in a DispatchQueue.async() thread. Isn't this wait() function being called on a main thread? Won't that lock it up? The .signal() call that will unblock that .wait() call is on line 289:

extension BleManager: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {

        DLog("centralManagerDidUpdateState: \(central.state.rawValue)")
        // Unlock state lock if we have a known state
        if central.state == .poweredOn || central.state == .poweredOff || central.state == .unsupported || central.state == .unauthorized {
            centralManagerPoweredOnSemaphore.signal()
        }

So once the system centralManager updates the BLE utility state, this will get called, and as long as it's not unknown, .signal() will get called and the rest of the init() will run. I've used print statements to confirm this is how it works. centralManagerPoweredOnSemaphore.wait() is called, then centralManagerDidUpdateState(), and then the rest of the init(). I still don't understand this though:

In the end it works though, I just wonder how the exact sequence of things is actually running under the hood to make it run properly.

Upvotes: 1

Views: 376

Answers (1)

Paulw11
Paulw11

Reputation: 115051

They are using the semaphore to block any operations on the BleManager object until the central exits the .unknown state.

The semaphore is created with an initial value of 1. The wait in the init will decrement it to 0, and not block. The central is then initialised and at some point the powered on state is entered. At this point the signal will return the semaphore to 1.

Now looking at the other functions, such as the connect function (line 167) you will see the first thing they do is wait and then signal the semaphore. Consider what happens in two different states:

  1. The central is not yet in the .poweredOn state - the semaphore count is 0 so the wait blocks. Assuming the BLE state becomes .poweredOn at some point, the wait will end and the semaphore is immediately released and then the connect function proceeds.
  2. The central is already in the .poweredOn state - the semaphore count is 1, so the wait does not block, the semaphore is then immediately released and the connect function proceeds.

The other functions work in a similar way.

The advantage of this approach is that the calling code doesn't need to keep checking the state; it can access the sharedInstance and then immediately call startScan without needing to check that the Central is powered on.

Upvotes: 3

Related Questions