rebuilding packets (pb with guard let)

Hello,


I am trying to send 20-byte packets from an Bluetooth LE module to an app. It's kinda working... but I noticed that sometimes my packets are split or incomplete (when I print the size of the buffer I sometime get 16 then 4 or 5 then 15 instead of 20). I tried to write a couple of functions that would handle that but something is wrong with my code:I get error messages on line 120 :

Expected ';' joining multiclause condition

Initializer for conditional binding must have optional type not 'Data'


Basically it does not like the syntax of my guard let statement but I don't know why (is it because data is the result of a function ?)


Q1: what's wrong with line 120 ?

Q2: is there a better way to handle split packets than my attempt in func rebuiltPackets ?

Q3: is there a better way to convert bytes from a buffer into separated values than my attempt in func parsePackets ?


Thanks !



//  ViewController.swift
import UIKit
import CoreBluetooth

let rxCharacteristicUUID = CBUUID(string: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
let UARTserviceUUID = CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")

var dataBuf: Data! = nil

class ViewController: UIViewController {

    // Variables
    var myCentralManager: CBCentralManager!
    var mySelectedDevices = [Int]()
    var myDevice: CBPeripheral!
    var myPeripherals =  Array()
    
    // IBOutlets
    @IBOutlet weak var myTableView: UITableView!
    
    @IBAction func didPressConnect(_ sender: Any) {
        if (mySelectedDevices.count)<2 {
            for d in mySelectedDevices {
                myDevice = myPeripherals[d]
                myDevice.delegate = self
                myCentralManager.stopScan()
                myCentralManager.connect(myDevice)
            }
        }
    }
    
    
    // Functions
    override func viewDidLoad() {
        super.viewDidLoad()
        myCentralManager = CBCentralManager(delegate: self, queue: nil)
        myTableView.dataSource = self
        myTableView.delegate = self
    }
}


// Class extensions

//**********************************************************************************************
extension ViewController: CBCentralManagerDelegate{
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            print("powered on")
            myCentralManager.scanForPeripherals(withServices: nil, options: nil)  // withServices: nil or [UARTserviceUUID]
        }
    }
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        if peripheral.name != nil {
            myPeripherals.append(peripheral)
            myPeripherals = myPeripherals.removingDuplicates()
            myTableView.reloadData()
        }
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("connected")
        print(peripheral.name!)
        peripheral.discoverServices([UARTserviceUUID]) //nil or [UARTserviceUUID]
        myCentralManager.stopScan()
    }
    
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        print(" not connecting")
        print(peripheral.name!)    }
}

// from https://www.hackingwithswift.com/example-code/language/how-to-remove-duplicate-items-from-an-array
extension Array where Element: Hashable {
    func removingDuplicates() -> [Element] {
        var addedDict = [Element: Bool]()

        return filter {
            addedDict.updateValue(true, forKey: $0) == nil
        }
    }
    mutating func removeDuplicates() {
        self = self.removingDuplicates()
    }
}

//**********************************************************************************************
extension ViewController:CBPeripheralDelegate {
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }

        for service in services {
            print(service)
            peripheral.discoverCharacteristics([rxCharacteristicUUID], for: service) //nil or [rxCharacteristicUUID]
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let characteristics = service.characteristics else { return }
        print("characteritics")

        for characteristic in characteristics {
            print(characteristic)
            if characteristic.properties.contains(.notify) {
                peripheral.setNotifyValue(true, for: characteristic)
            }
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        guard let read = characteristic.value else {return}

        if read.count == 20 {
            let data = read
            parsePacket(data: data)
        } else {
            guard let data = rebuiltPacket(data: read) where (data.count == 20) else {return}
             parsePacket(data: data)
        }
    }
    
    
    
    func parsePacket(data:Data) {
        let myFirstByte:UInt8 = UInt8(data[0])
        print("\(myFirstByte)")
        //let mySecondByte:UInt16 = UInt16(data![1]) * 256 + UInt16(data![2])
        //let myThirdByte:UInt16 = UInt16(data![3]) * 256 + UInt16(data![4])
        
    }
    

    func rebuiltPacket(data:Data)-> Data {
        var fullPacket: Data! = nil
        if dataBuf == nil {
            dataBuf=data
        } else if (dataBuf.count + data.count) == 20 {
            fullPacket = dataBuf + data
        }
        dataBuf = nil
        return fullPacket
    }
    
}

//**********************************************************************************************
extension ViewController:UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let list = tableView.indexPathsForSelectedRows {
            for l in list {
                mySelectedDevices.append(l[1])
                mySelectedDevices = mySelectedDevices.removingDuplicates()
            }
        }
    }
    
    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        if let cell = tableView.cellForRow(at: indexPath) {
            if !cell.isSelected {
                cell.accessoryType = .none
                mySelectedDevices.removeAll()
            }
        }
    }
    
    func updateCount(_ tableView:UITableView) -> Int {
        var count = 0
        if let list = tableView.indexPathsForSelectedRows {
            count = list.count
        }
        return count
    }
}

//**********************************************************************************************
extension ViewController:UITableViewDataSource{
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myPeripherals.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let myCell = myTableView.dequeueReusableCell(withIdentifier: "myReusableIdentifier")!
        let myPeripheral = myPeripherals[indexPath.row]
        myCell.textLabel?.text = myPeripheral.name
        return myCell
    }
}



     return myCell
    }
}

Accepted Reply

I’m not going to comment on your specific issues — others have done that well already — but I do have some general suggestions:

  • Separate this code out into a separate data type. That will make it easier to test in isolation, rather than having it tied up with all the Core Bluetooth goo.

  • When parsing and formatting data, learn to love the high-level features provided by the API. That is, rather than treat data as an array of bytes, exploit the fact that it has a bunch of nice high-level functions.

    For example, data is a sequence of bytes, so you can enumerate the bytes like this:

    let data = Data(bytes: [1, 2, 3, 4])
    for byte in data {
        print(byte)
    }

    And that means you can do all sorts of sequence operations on it, like calling

    reduce(…)
    to calculate your checksum:
    let sum = data.reduce(0) { (soFar, byte) in
        soFar &+ byte
    }

    There are many other helper routines you can use, for example:

    • You can get the first N bytes using

      prefix(_:)
    • You can data a new data with the first N bytes removed using

      dropFirst(_:)
    • You can search using

      contains(_:)
      , or find using
      firstIndex(of:)
      or
      firstIndex(where:)
    Data
    is an incredibly powerful type and the more you learn about it the easier time you’ll have.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

Q1: what's wrong with line 120 ?


if/guard-let-where was removed from Swift long time ago. You use `,` in place of `where`:

guard let data = rebuiltPacket(data: read), data.count == 20 else {return}


Oh, the above code still shows Initializer for conditional binding must have optional type not 'Data'

Your `rebuildPacket(data:)` returns non-Optional `Data`, so the line should be something like this:

guard case let data = rebuiltPacket(data: read), data.count == 20 else {return}



Sorry, I have not enough time to discuss Q2 & Q3 now...

For Q3: why declare new var for each data ?

func parsePacket(data:Data) {

let myFirstByte:UInt8 = UInt8(data[0])

print("\(myFirstByte)")

//let mySecondByte:UInt16 = UInt16(data![1]) * 256 + UInt16(data![2])

//let myThirdByte:UInt16 = UInt16(data![3]) * 256 + UInt16(data![4])

}


Could use an array and append the right content (I did not analyze the logic for getting the content) to myBytes

var myBytes: [UInt8] = []

True, I could use an array but those variables correspond to very different things and won't be processed the same way later on. I just found clearer to give then individual names (obviously "myFirstByte" will be replaced by something way mor explicit).

I’m not going to comment on your specific issues — others have done that well already — but I do have some general suggestions:

  • Separate this code out into a separate data type. That will make it easier to test in isolation, rather than having it tied up with all the Core Bluetooth goo.

  • When parsing and formatting data, learn to love the high-level features provided by the API. That is, rather than treat data as an array of bytes, exploit the fact that it has a bunch of nice high-level functions.

    For example, data is a sequence of bytes, so you can enumerate the bytes like this:

    let data = Data(bytes: [1, 2, 3, 4])
    for byte in data {
        print(byte)
    }

    And that means you can do all sorts of sequence operations on it, like calling

    reduce(…)
    to calculate your checksum:
    let sum = data.reduce(0) { (soFar, byte) in
        soFar &+ byte
    }

    There are many other helper routines you can use, for example:

    • You can get the first N bytes using

      prefix(_:)
    • You can data a new data with the first N bytes removed using

      dropFirst(_:)
    • You can search using

      contains(_:)
      , or find using
      firstIndex(of:)
      or
      firstIndex(where:)
    Data
    is an incredibly powerful type and the more you learn about it the easier time you’ll have.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks, your solution did remove the alert messages but the whole thing is not working properly, which brings back to Q2...

Great ! suggestions about my code in genreal are always welcome, that's how I learn 😉


I shall look more into those high-level functions. I did have a go with reduce(), unsuccessfuly. Having said that I noticed your syntax is different from what I did...


I also tried to put all the bluetooth stuff into a separate swift file (outside ViewController) but I couldn't update the TableView with the content of my peripheral array. I am probably not using the delegate properly...

BTW, how would you even create this class ? This:


class BLEManager: CBCentralManager {

}


does not work

BTW, how would you even create this class ?

The code you posted is trying to subclass

CBCentralManager
, which is not a good way to approach it. Normally you would have a property that refers to your
CBCentralManager
instance and then set yourself as the
CBCentralManager
delegate.

Doing this from Swift is a bit tricky because of Swift’s strict rules about initialisation order. Probably the best option is to start with no delegate and then set it afterwards. For example

import CoreBluetooth

class BLEManager: NSObject, CBCentralManagerDelegate {

    let manager: CBCentralManager

    override init() {
        self.manager = CBCentralManager(delegate: nil, queue: nil)
        super.init()
        self.manager.delegate = self
    }

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
    }
}

Line 3 declares the

BLEManager
as:
  • A subclass of

    NSObject
  • Implementing the

    CBCentralManagerDelegate
    protocol

You need the former because

CBCentralManagerDelegate
is an Objective-C protocol.

Line 5 is the reference to the central manager.

Line 8 initialises that reference with a

nil
delegate. You can’t use
self
here because you can’t reference
self
until all of its fields have been initialised.

Line 9 calls

super
, which is the point where all the fields of
self
have been initialised.

Line 10 can now set the delegate, because

self
is fully initialised.

Lines 13…14 implement the only required method in the

CBCentralManagerDelegate
protocol.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks a lot ! I separated the BLE stuff from the ViewController like you said but I'm having trouble getting the two class talk to each other:


for instance my UITableViewDataSource class has a function that requires reading a property of myCentralManager:


extension ViewController:UITableViewDataSource{

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myCentralManager.myPeripherals.count
    }


similarly my centralManagerDelegate class has a function that calls a TableView function


func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        if peripheral.name != nil {
            myPeripherals.append(peripheral)
            myPeripherals = myPeripherals.removingDuplicates()
            myTableView.reloadData()
        }
    }

I have already defined delegates for both the centralManager and the TableView but I am not quite sure how make use of them here. Do I need to create special functions inside the delagates protocols (other than the built-in ones ?)


Cheers

There’s two parts to this:

  • Down references, that is, giving your view controller a reference to your model layer

  • Up notifications, that is, having your view controller hear about changes in the model layer

I’ll tackle each in turn.

IMPORTANT I want to stress that I’m giving a very high-level overview of one straightforward way to solve problems like this. The best way to structure an app is the subject of ongoing discussion in the Apple developer community. If you search on the ’net you’ll find lots of resources explaining lots of different approaches.

What I’m describing here is a simple version of the classic model-view-controller architecture (MVC) architecture. I think MVC is a great place to start because it remains the ‘default’ architecture used in Apple documentation. However, as you get more experience you may find an architecture that better suits your needs. For example, in a recent app I wrote for my own personal use I used an Elm-inspired architecture based on read-only model types and centralised change tracking.

For down references there are two common strategies:

  • Dependency injection

  • Singletons

I prefer the former because it promotes reuse and eases testing. The idea here is that whoever is responsible for putting the view controller on screen should also give that view controller a reference to the model layer.

It’s relatively obvious how to do this if the view controller is transitory. For example, if you have a segue that leads to your view controller then the parent view controller can set this up in its

prepare(for:sender:)
method.

Doing this in your main view controller is a bit trickier:

  • If the main view controller is explicitly set up by the app delegate, it can take care of this.

  • If you’re using storyboards for your main view controller, you need a bit of an ugly hack )-:

With regards the last point, the basic idea is for your app delegate to find the main view controller and give it a reference to the model layer. Here’s an example from a simple network app that I’m working on.

let networkManager = NetworkManager()

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    let nav = (self.window!.rootViewController! as! UINavigationController)
    self.mainViewController = (nav.viewControllers[0] as! MainViewController)
    self.mainViewController.networkManager = self.networkManager
}

With regards data flowing up the stack, you have to choose between two options:

  • Delegation

  • Notification

Delegate makes sense if your model layer needs information and only one thing can provide that information. For example, a networking model layer might use a delegate to help with authentication (that is, present an authentication UI) because it’s clear that there can only be one delegate for that.

For view controllers, however, you typically want to use notification, because it’s note uncommon for multiple view controllers to display the same state. In that case you just have the model layer post a notification and the view controller subscribe to that notification. You can use whatever notification system you want, although most folks just use the standard

Notification
API.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"