Bluetooth Logo

Writing an SDK With Core Bluetooth – 19 – The Code

This entry is part 23 of 24 in the series Writing an SDK With Core Bluetooth

In this entry, we’ll do a detailed code walkthrough of the changes that were made to get to this point.

WALKTHROUGH

The Starting Repo Tag

The Ending Repo Tag

The Comparison Between the Two Tags

The ITCB_SDK_Central_internal.swift File

BEFORE:

public func sendQuestion(_ inQuestion: String) {
        self.question = inQuestion
    }

AFTER:

First, we added code to actually use Core Bluetooth to send the question to the Peripheral.

    public func sendQuestion(_ inQuestion: String) {
        if  let data = inQuestion.data(using: .utf8),
            let peripheral = _peerInstance as? CBPeripheral,
            let service = peripheral.services?[_static_ITCB_SDK_8BallServiceUUID.uuidString],
            let questionCharacteristic = service.characteristics?[_static_ITCB_SDK_8BallService_Question_UUID.uuidString],
            let answerCharacteristic = service.characteristics?[_static_ITCB_SDK_8BallService_Answer_UUID.uuidString] {
            peripheral.writeValue(data, for: questionCharacteristic, type: .withoutResponse)
            peripheral.setNotifyValue(true, for: answerCharacteristic)
            question = inQuestion
        } else {
            question = nil
        }
    }

That “self.” was not necessary, so I removed it. Just a matter of style.

Next, we added code to the CBPeripheralDelegate extension to catch it when the answer is returned.

    public func peripheral(_ inPeripheral: CBPeripheral, didUpdateValueFor inCharacteristic: CBCharacteristic, error inError: Error?) {
        if  let service = inPeripheral.services?[_static_ITCB_SDK_8BallServiceUUID.uuidString],
            let answerCharacteristic = service.characteristics?[_static_ITCB_SDK_8BallService_Answer_UUID.uuidString],
            let answerData = answerCharacteristic.value,
            let answerString = String(data: answerData, encoding: .utf8),
            !answerString.isEmpty {
            inPeripheral.setNotifyValue(false, for: answerCharacteristic)
            answer = answerString
        }
    }

I’ll walk through what I did, but first, I want to talk about a change in another file:

The ITCB_SDK_internal.swift File

I added these two methods, which are Array extensions:

extension Array where Element == CBService {
    public subscript(_ inUUIDString: String) -> Element! {
        return reduce(nil) { (current, nextItem) in
            return nil == current ? (nextItem.uuid.uuidString == inUUIDString ? nextItem : nil) : current
        }
    }
}

extension Array where Element == CBCharacteristic {
    public subscript(_ inUUIDString: String) -> Element! {
        return reduce(nil) { (current, nextItem) in
            return nil == current ? (nextItem.uuid.uuidString == inUUIDString ? nextItem : nil) : current
        }
    }
}

I used Swift’s extension mechanism to add a “lookup” subscript to the Array class, if the contents are Service or Characteristic objects.

If they are, then I allow a String to be passed in, which is the String expression of the CBUUID that we have assigned a Service or Characteristic. This allows us to very quickly get a particular Service or Characteristic (because we are only using one Service -for now- we could just get the first element of the services Array, but this is a much better way to do it).

Back to the Peripheral Device Class

In this step, I simply extract the Data representation of the question String. The Characteristic write request wants Data, so we will need to provide Data.

let data = inQuestion.data(using: .utf8),

In this step, I simply cast our typeless peer property into a CBPeripheral.

let peripheral = _peerInstance as? CBPeripheral,

Using that Array extension I mentioned above, I get our “Magic 8-Ball” Service.

let service = peripheral.services?[_static_ITCB_SDK_8BallServiceUUID.uuidString],

Again, using the Array extension, I get the Question Characteristic from the Magic 8-Ball Service.

let charcteristic = service.characteristics?[_static_ITCB_SDK_8BallService_Question_UUID.uuidString]

First I ask the Peripheral object to request a write, using the Data from the String, and for the Question Characteristic. I am not looking for a response.

peripheral.writeValue(data, for: charcteristic, type: .withoutResponse)

Finally, I “subscribe” to the answer Characteristic, which tells Core Bluetooth to get back to me, when the answer Characteristic changes value.

peripheral.setNotifyValue(true, for: answerCharacteristic)

At this point, Core Bluetooth takes over, and will send the question to the Peripheral.

After that, I’ll set the “question” property to the String.

NOTE: The Central does not change the Characteristic. What it does, is send a new value that it is proposing for the Characteristic to the Peripheral. The Peripheral will then decide whether or not to accept the proposed change.

The ITCB_SDK_Peripheral_internal.swift File (the Peripheral End of Things)

I added the CBPeripheralManagerDelegate callback for receiving a write request.

     public func peripheralManager(_ inPeripheralManager: CBPeripheralManager, didReceiveWrite inWriteRequests: [CBATTRequest]) {
        guard   1 == inWriteRequests.count,
                let mutableChar = inWriteRequests[0].characteristic as? CBMutableCharacteristic,
                let data = inWriteRequests[0].value,
                let stringVal = String(data: data, encoding: .utf8) else {
            return
        }

        mutableChar.value = data
        
        if nil == central {
            central = ITCB_SDK_Device_Central(inWriteRequests[0].central, owner: self)
        }
        
        _sendQuestionAskedToAllObservers(device: central, question: stringVal)
    }

Let’s walk through what happens:

  1. We use a guard statement to:
    • Make sure that we only have one write request (We could, theoretically, get more than one)
    • fetch the Characteristic that the Central wants to change, but as a mutable one (the one we created, earlier)
    • Extract the proposed change, as Data
    • Convert that data to a String.
  2. We assume the data is good, and simply change the Characteristic value to the new value.
  3. If the Central has not already been assigned, We create a new ITCB_SDK_Device_Central instance, with the CBCentral that was passed in via the write request, and set it to our instance stored property.
  4. We inform observers that the question was asked.

In order to accomplish step #3, I created a new initializer for the ITCB_SDK_Device_Central class, with CBCentral and “owner” arguments:

    init(_ inCentral: CBCentral, owner inOwner: ITCB_SDK_Peripheral) {
        super.init()
        owner = inOwner
        _peerInstance = inCentral
        subscribedChar = nil
    }

I added a CBPeripheralManager delegate callback to react to the Central subscription. I do the check for the central property being nil, because sometimes, the subscription may arrive before the write (remember what I said about “chaos”?), and we need to make sure the central is in place. We did the same check in the write request:

    public func peripheralManager(_ inPeripheralManager: CBPeripheralManager, central inCentral: CBCentral, didSubscribeTo inCharacteristic: CBCharacteristic) {
        if nil == central {
            central = ITCB_SDK_Device_Central(inCentral, owner: self)
        }
        
        if  let central = central as? ITCB_SDK_Device_Central {
            central.subscribedChar = inCharacteristic
        }
    }

WHERE WE ARE NOW

The “Magic 8-Ball” app is now completely functional. We can run a Mac or iPhone as a Peripheral (even multiple Peripherals), and any of the apps as a Central.

The Central will discover and list the Peripherals. It should be noted that a Central “captures” the Peripheral when it lists it, so no other Central will be able to find that Peripheral.

The Central can “ask a question” of a Peripheral.

The Peripheral will be notified that a “question has been asked,” and will allow the user to either send a random answer, or one selected from a list.

The Central will then display the answered question.

The Central Has Found A peripheral
Peripheral Is Waiting for A Question
Central Asks A Question
Peripheral Displays the Question
Central Displays the Answer
Peripheral Answers the Question
The Watch App
The TV App

Now that we have that out of the way, let’s wrap up…