iOS

Configure Beacons with CoreBluetooth

Introduction#

Hot to read and write data to a bluetooth low energy device.

Remarks#

Some important points

  • No capabilities are needed.
  • iPhone store bytes in Little Endian format, so check if bluetooth accessory use Little Endian too. Example:
    • intel CPU usually use little endian.
    • The ARM architecture was little-endian before version 3 when it became big-endian.
  • After a single or batch operation the connection will be lost, so you have to reconnect before continue.

Scan for SERVICE UUID

func SearchBLE(){
    cb_manager.scanForPeripherals(withServices:[service_uuid], options: nil)
    StopSearchBLE()
}

How to discover SERVICE UUID without documentation

func centralManager(_ central: CBCentralManager, didConnect peripheral:             
CBPeripheral) {
        peripheral.delegate = self
        peripheral.discoverServices(nil)
}

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    for service in peripheral.services! {
        print("Service: \(service)\n error: \(error)")
    }
}
  • discoverServices(nil) - NIL means that all services will be returned, which is not a good option.( READ Remarks 3)
  • If you haven’t found the SERVICE UUID run your code and looking for in console

enter image description here

  • I found have 3 services: Battery, Device information (Firmware) and FFF0
  • This uuid service isn’t a standard one, a list with standards can find here
  • FFF0 is the SERVICE UUID in this case

Convert data to UInt16 and contrary

Add this extensions to your class

protocol DataConvertible {
    init?(data: Data)
    var data: Data { get }
}

extension DataConvertible {

    init?(data: Data) {
        guard data.count == MemoryLayout<Self>.size else { return nil }
        self = data.withUnsafeBytes { $0.pointee }
    }

    var data: Data {
        var value = self
        return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }
}
extension UInt16 : DataConvertible {
    init?(data: Data) {
        guard data.count == MemoryLayout<UInt16>.size else { return nil }
        self = data.withUnsafeBytes { $0.pointee }
    }
    var data: Data {
        var value = CFSwapInt16HostToBig(self)
        return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }
}

Showing names of all Bluetooth Low Energy (BLE)

  • For this example I have a controlled room with a single BLE device enable.

  • Your class should extend CBCentralManagerDelegate.

  • Implement the method: centralManagerDidUpdateState(_ central: CBCentralManager).

  • Use global queue to not freeze the screen while searching for a device.

  • Instantiate CBCentralManager and wait for callback centralManagerDidUpdateState response.

    class BLEController: CBCentralManagerDelegate{

    var cb_manager: CBCentralManager! var bles : [CBPeripheral] = []

      override func viewDidLoad() {
          super.viewDidLoad()
          cb_manager = CBCentralManager(delegate: self, queue: DispatchQueue.global())
      }
    
      func centralManagerDidUpdateState(_ central: CBCentralManager) {
          print("UPDATE STATE - \(central)")
      }

    }

Callback to centralManagerDidUpdateState indicates that CoreBluetooth is ready, so you can search for BLE now. Update centralManagerDidUpdateState code to search for all BLE device when it is ready.

func centralManagerDidUpdateState(_ central: CBCentralManager) {
    print("UPDATE STATE - \(central)")
    SearchBLE()
}

func SearchBLE(){
    cb_manager.scanForPeripherals(withServices: nil, options: nil)
    StopSearchBLE()
}

func StopSearchBLE() {
    let when = DispatchTime.now() + 5 // change 5 to desired number of seconds
    DispatchQueue.main.asyncAfter(deadline: when) {
        self.cb_manager.stopScan()
    }
}
  • SearchBLE() search for BLE devices and stop searching after 5s

  • cb_manager.scanForPeripherals(withServices: nil, options: nil) looks for every BLE in range with you.

  • StopSearchBLE() will stop the search after 5s.

  • Each BLE found will callback func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)

    func centralManager(_ central: CBCentralManager, didDiscover peripheral:
    CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { guard let name = peripheral.name else { return } print(name) bles.append(peripheral) }

Connect and read major value

  • I’m in a controlled room with a single minew beacon that use IBEACON protocol.

  • BLEController needs to extend CBPeripheralDelegate

  • I’ll use the first BLE to connect after the search has stop.

  • Modify the method StopSearchBLE()

    class BLEController: CBCentralManagerDelegate, CBPeripheralDelegate{ //… func StopSearchMiniewBeacon() { let when = DispatchTime.now() + 5 // change 2 to desired number of seconds DispatchQueue.main.asyncAfter(deadline: when) { self.cb_manager.stopScan() self.cb_manager.connect(bles.first) } } /… }

  • In the documention of your BLE device, you should look for the SERVICE UUID and MAJOR UUID CHARACTERISTIC

    var service_uuid = CBUUID(string: “0000fff0-0000-1000-8000-00805f9b34fb”) var major_uuid = CBUUID(string: “0000fff2-0000-1000-8000-00805f9b34fb”) func centralManager(_ central: CBCentralManager, didConnect peripheral:
    CBPeripheral) { peripheral.delegate = self peripheral.discoverServices([service_uuid]) }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { print(“Service: (service)\n error: (error)”) peripheral.discoverCharacteristics([major_uuid], for: (peripheral.services?[0])!) }

  • Create a variable ‘service_uuid’ and ‘major_uuid’ like code above. ‘-0000-1000-8000-00805f9b34fb’ is part of the standard. ‘fff0’ is my SERVICE UUID, ‘fff2’ is my MAJOR UUID characteristic and ‘0000’ are required to fill the 4 bytes uuid 1º block.

  • discoverCharacteristics([major_uuid], for: (peripheral.services?[0])!) will get major characteristic from my device gatt server and it will have NIL as value for now.

  • (peripheral.services?[0])! - 0 beacuse will return a single value once I did peripheral.discoverServices([service_uuid])

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { for characteristic in service.characteristics! { print(“Characteristic: (characteristic)\n error: (error)”) if(characteristic.uuid.uuidString == “FFF2”){ peripheral.readValue(for: characteristic) } } }

    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { print(“Characteristic read: (characteristic)\n error: (error)”) let major = UInt16.init(bigEndian: UInt16(data: characteristic.value!)!) print(“major: (major)”) }

  • Characteristic value will only be readable after call peripheral.readValue(for: characteristic)

  • readValue will result in func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) with value in Data type.

Write major value

  • You need discover the services and characteristic

  • You don’t need read value from the characteristic before writing over it.

  • will continue for, for this example, after read value. Modify func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)

  • Add a variable new_major and reset_characteristic

    var reset_characteristic : CBCharacteristic! func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { for characteristic in service.characteristics! { print(“Characteristic: (characteristic)\n error: (error)”) if(characteristic.uuid.uuidString == “FFF2”){ peripheral.readValue(for: characteristic) } if(characteristic.uuid.uuidString == “FFFF”){ reset_characteristic = characteristic } } } let new_major : UInt16 = 100 func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { print(“Characteristic read: (characteristic)\n error: (error)”) let major = UInt16.init(bigEndian: UInt16(data: characteristic.value!)!) print(“major: (major)”) peripheral.writeValue(new_major.data, for: characteristic, type: CBCharacteristicWriteType.withResponse) }

  • iPhone by deafult will send and receive bytes in Little Endian format, but my device MINEW witch chipset NRF51822 have ARM archteture and need bytes in Big Endian format, so I have to swap it.

  • BLE Device documentation will say what type of input and output each characteristic will have and if you can read it like above (CBCharacteristicWriteType.withResponse).

    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { print(“Characteristic write: (characteristic)\n error: (error)”) if(characteristic.uuid.uuidString == “FFF2”){ print(“Resetting”) peripheral.writeValue(“minew123”.data(using: String.Encoding.utf8)!, for: reset_characteristic, type: CBCharacteristicWriteType.withResponse) } if(characteristic.uuid.uuidString == “FFFF”){ print(“Reboot finish”) cb_manager.cancelPeripheralConnection(peripheral) } }

  • To update a gatt server information you have to reboot it programmatically or save data to it and turn off and turn on manually.

  • FFFF is characteristic that do it in this device.

  • ‘minew123’ is the default password for reboot o save information in this case.

  • run your app and watch you console for any error, I hope none, but you will not see the new value yet.

    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { print(“Characteristic read: (characteristic)\n error: (error)”) let major = UInt16.init(bigEndian: UInt16(data: characteristic.value!)!) print(“major: (major)”) //peripheral.writeValue(new_major.data, for: characteristic, type: CBCharacteristicWriteType.withResponse) }

  • Last step is to comment last line in method didUpdateValueFor and rerun the app, now you will the new value.


This modified text is an extract of the original Stack Overflow Documentation created by the contributors and released under CC BY-SA 3.0 This website is not affiliated with Stack Overflow