Skip to content

4. Managing beacons

When beacon-enabled apps detect beacons, they usually don't connect to them, but only register the fact of their presence. If your app is only monitoring and ranging iBeacons or discovering Eddystone beacons, then it never actually connects to them, just register signals coming from them and perform actions depending on the information transmitted in those signals as well as their strength.

But when it comes co changing configuration and upgrading firmware, we have to stop thinking of beacons as beacons and instead treat them as a generic Bluetooth device (peripheral). Reading and writing configurations to beacons, same as upgrading their firmware, requires establishing a connection.

Bluetooth devices expose information about themselves through characteristics. Changing a configuration is just a matter of writing data to the right place. You can read more about the technical background of this process in Core Bluetooth documentation.

Kontakt.io iOS SDK makes beacon configuration much easier and less error-prone by completely abstracting away beacon services and characteristics.

KTKDevicesManager class

An object in your app responsible for detecting and connecting to nearby beacons needs to be an instance of a KTKDevicesManager class. Under the hood KTKDevicesManager depends on Core Bluetooth to find Bluetooth devices and establish connection with your beacons when needed.

First step

Make sure that Kontakt.io iOS SDK has been added to your project. Instructions on how to do this are available in the Installation section.

Please also don't forget that you need to add a reference to Kontakt.io iOS SDK in every file that you want to use it through an import statement:

import KontaktSDK

Initializing a manager

Your KTKDevicesManager should be initialized as a view controller's instance variable.

class ViewController: UIViewController {

    var devicesManager: KTKDevicesManager!

    override func viewDidLoad() {
        super.viewDidLoad()

        devicesManager = KTKDevicesManager(delegate: self)
    }
}

Please remember that the delegate that you specify when instantiating your beacon manager must conform to KTKDevicesManagerDelegate protocol:

extension ViewController: KTKDevicesManagerDelegate {
    // Implement delegate methods here
}

Starting discovery of nearby devices

Before you start detecting nearby devices, it's good to check whether a device that runs your app has Bluetooth turned on. You can do that by looking at manager's centralState property.

Once you make sure Bluetooth is available, you can start discovery using one of two methods:

  • startDevicesDiscovery
  • startDevicesDiscoveryWithInterval:
if devicesManager.centralState == CBCentralManagerState.poweredOn {
    devicesManager.startDevicesDiscovery(withInterval: 2.0)
//  devicesManager.startDevicesDiscovery()
}

Discovery mode

The choice of the method you use to start a nearby devices discovery will affect in what mode this discovery will happen:

  • startDevicesDiscovery will set the discovery mode to KTKDevicesManagerDiscoveryModeAuto
  • startDevicesDiscoveryWithInterval: will set the discovery mode to KTKDevicesManagerDiscoveryModeInterval

KTKDevicesManagerDiscoveryModeAuto delivers notification about newly discovered devices right away. This approach is more responsive, but also more resource-intensive. If you expect users of your app might encounter large quantities of beacons, it's better to make sure your devices manager uses KTKDevicesManagerDiscoveryModeInterval, which delivers notifications about new nearby devices in batches. For more information please check KTKDevicesManagerDiscoveryMode constants reference.

Handling device discovery events

KTKDevicesManagerDelegate protocol has one required method that you need to implement: devicesManager:didDiscoverDevices:. It handles newly discovered devices that are in range of your iPhone/iPad. It will represent them as an optional array of KTKNearbyDevice objects.

extension ViewController: KTKDevicesManagerDelegate {
    func devicesManager(_ manager: KTKDevicesManager, didDiscover devices: [KTKNearbyDevice]?) {
        guard let nearbyDevices = devices else {
            return
        }

        for device in nearbyDevices {
            if let uniqueId = device.uniqueID {
                print("Detected a beacon \(uniqueId)")
            } else {
                print("Detected a beacon with an unknown Unique ID")
            }
        }
    }
}

KTKNearbyDevice class

KTKNearbyDevice object is a representation of a beacon device that was discovered via your devices manager.

Since iBeacon advertising packets are filtered out on the system level and forwarded to be handled by Core Location-based solution, freshly detected KTKNearbyDevice objects do not have information about what Proximity UUID, Major and Minor this particular beacon is broadcasting. However, you can get this information later by either connecting to that beacon reading its configuration or by calling a proper API endpoint. All those solutions will be described later in their respective sections of this handbook.

Connecting to a beacon

For typical end-user tasks your app needs to only monitor and/or range beacons. That means it should detect them, "be aware" of them, but it's usually not necessary to connect to them. And since establishing a connection comes with much higher energy requirements, it's actually not advisable to make every user of your app connect to your beacons without a specific reason.

However, if you need to change some configuration of your beacons, then making a Bluetooth connection is the only way to do this.

Setting up a KTKDeviceConnection

Connection to a nearby device is handled by an KTKDeviceConnection object. Your connection can be instantiated only through initWithNearbyDevice: method. That means you need to provide a KTKNearbyDevice object representing a beacon you want to connect to.

class ViewController: UIViewController {

    var devicesManager: KTKDevicesManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        devicesManager = KTKDevicesManager(delegate: self)

        if devicesManager.centralState == CBCentralManagerState.poweredOn {
            devicesManager.startDevicesDiscovery(withInterval: 2.0)
        }
    }
}

extension ViewController: KTKDevicesManagerDelegate {
    func devicesManager(_ manager: KTKDevicesManager, didDiscover devices: [KTKNearbyDevice]?) {
        if let device = devices?.filter({$0.uniqueID == "abcd"}).first {
            let connection = KTKDeviceConnection(nearbyDevice: device)
        }
    }
}

An actual Bluetooth link between your iOS device and a beacon won't happen until you you request e.g. writing a new configuration to the beacon.

Credentials

You can try connecting to any beacon, but if that beacon does not belong to an account from which you've provided an API key, that connection will be dropped. Your KTKDeviceConnection object can handles this for you automatically. The only thing thing you need to provide is your API key, as described in the SDK setup section.

Your iPhone/iPad won't connect to a beacon until you actually call a read configuration or write configuration command.

Password for beacons with newer firmwares is embedded in the encrypted configuration payload generated on our backend. This way you can store configs safely on 3rd party devices without worry of compromising security of your beacons and use it e.g. to configure beacons in places with no Internet access. More about this in next sections.

For beacons with firmware 3.1 or earlier you need to provide a password for each beacon before you can read or write a new configuration. Normally, your KTKDeviceConnection object would do that for you automatically, you only need to provide the right API key in the setup stage, but if you want to configure beacons while offline, you can set a credentials property of your KTKDeviceConnection object to an instance of KTKDeviceCredentials that has a password for your particular beacon you want to configure.

Reading a configuration from beacons

Some beacon configuration details are available publicly, either in advertising packet or in scan response packet, but if you don't want make a separate call to our API, the only reason to learn all things about a particular beacon is to connect to it and read the whole configuration. This is what KTKDeviceConnection's method readConfigurationWithCompletion: is for.

Once you have a connection set up with the desired KTKNearbyDevice object that represents your beacon, you can call that method. It requires and a KTKDeviceConnectionReadCompletion completion closure/block that takes two arguments configuration (KTKDeviceConfiguration?) and error (NSError?):

extension ViewController: KTKDevicesManagerDelegate {
    func devicesManager(_ manager: KTKDevicesManager, didDiscover devices: [KTKNearbyDevice]?) {
        if let device = devices?.filter({$0.uniqueID == "abcd"}).first {
            connection = KTKDeviceConnection(nearbyDevice: device)
            connection.readConfiguration() { configuration, error in
                if error == nil, let config = configuration {
                    print("Advertising interval for beacon \(config.uniqueID) is \(config.advertisingInterval!)ms")
                }
            }
        }
    }
}

Writing a new configuration to a beacon

If you want to change settings of your beacons, you need to use the connection to a beacon to write a new configuration, which is an instance of KTKDeviceConfiguration class.

Making a new configuration inside an app

A new configuration can be made on the go, directly in your own app. It's just a matter of setting up a KTKDeviceConfiguration object that object's properties you want to change with new values:

extension ViewController: KTKDevicesManagerDelegate {
    func devicesManager(_ manager: KTKDevicesManager, didDiscover devices: [KTKNearbyDevice]?) {
        if let device = devices?.filter({$0.uniqueID == "abcd"}).first {
            let newConfiguration = KTKDeviceConfiguration(uniqueID: device.uniqueID!)
            newConfiguration.major = 123
            newConfiguration.transmissionPower = .power5
        }
    }
}

Getting a pending configuration from Kontakt.io Cloud

A new configuration for a beacon can also be created through a Web Panel or API. This way you can manage you whole fleet of beacons from one place and then task users of your app to deliver new configs to beacons. Please check examples from API Client section to learn how to get pending configurations from Kontakt.io Cloud.

Applying a new configuration

Once you have a new configuration for your beacon, you can write it to a nearby device. To do that you need to call KTKDeviceConnection's method writeConfiguration:completion:. As arguments you need to provide a new configuration and a KTKDeviceConnectionWriteCompletion block/closure, that has three parameters: synchronized (Bool), configuration (KTKDeviceConfiguration?) and error (NSError?).

func devicesManager(_ manager: KTKDevicesManager, didDiscover devices: [KTKNearbyDevice]?) {
    if let device = devices?.filter({$0.uniqueID == "abcd"}).first {
        let connection = KTKDeviceConnection(nearbyDevice: device)

        let newConfiguration = KTKDeviceConfiguration(uniqueID: device.uniqueID!)
        newConfiguration.major = 123
        newConfiguration.transmissionPower = .power5

        connection.write(newConfiguration) { synchronized, configuration, error in
            if error == nil {
                print("Configuration applied")
                if !synchronized, let config = configuration {
                    saveConfigForLater(config)
                }
            }
        }
    }
}

The synchronized parameter tells you whether the Kontakt.io Cloud API has been notified about the changes on your beacon. If it's false (this usually happens when you don't have an Internet access when connecting to a beacon) you should save configuration (if available) or your original KTKDeviceConfiguration object and attempt synchronization once again.

The configuration parameter will be nil for beacons with firmware 3.1 or earlier. On beacons with firmware 4.0, since they use Kontakt.io Secure Communication protocol it will also be an instance of KTKDeviceConfiguration class, but it will have only two parameters populated: secureResponse and secureResponseTime.

KTKDeviceConfiguration class conforms to KTKCloudModel protocol, which in turn conforms to NSSecureCoding. It means you won't have any problems with storing your config objects.

Example: bulk configuring with Packets Interleaving

Starting with firmware 4.1 Kontakt.io beacons can simultaneously broadcast packets from both iBeacon and Eddystone standards. In this example we assume that you already have a number of beacons with firmware 4.1, that are currently set up to broadcast only the iBeacon packet, but you want them to start broadcasting the Eddystone-URL packet with your link as well. Let's also add a randomly generated Minor value to the mix:

let config = KTKDeviceConfiguration()
config.packets = [.iBeacon, .eddystoneURL]
config.url = URL(string: "https://kontakt.io")
config.minor = NSNumber(value: arc4random_uniform(65536))

We will also need a way to reference beacons that we want to configure. For the purpose of this example we will use a simple array of strings representing beacons' Unique IDs:

var myBeacons = ["abcd", "efgh", "ijkl"]

Once we have this we can start applying new configurations. The whole implementation can look for example like this:

import UIKit
import KontaktSDK

class ViewController: UIViewController {

    var devicesManager: KTKDevicesManager!
    var myBeacons = ["abcd", "efgh", "ijkl"]

    override func viewDidLoad() {
        super.viewDidLoad()

        devicesManager = KTKDevicesManager(delegate: self)
        devicesManager.startDevicesDiscovery(withInterval: 2.0)
    }

}

extension ViewController: KTKDevicesManagerDelegate {

    func devicesManager(_ manager: KTKDevicesManager, didDiscover devices: [KTKNearbyDevice]?) {
        guard let nearbyDevices = devices else {
            return
        }

        for device in nearbyDevices {
            if let index = myBeacons.index(of: device.uniqueID!) {
                let config = KTKDeviceConfiguration()
                config.packets = [.iBeacon, .eddystoneURL]
                config.url = URL(string: "https://kontakt.io")
                config.minor = NSNumber(value: arc4random_uniform(65536))

                let connection = KTKDeviceConnection(nearbyDevice: device)
                connection.write(config, completion: { (synchronized, configuration, error) in
                    if error == nil {
                        print("Configuration for \(configuration?.uniqueID!) applied successfully")
                    }
                })

                myBeacons.remove(at: index)
            }

            if myBeacons.isEmpty {
                manager.stopDevicesDiscovery()
                print("Scanning has stopped")
                break
            } else {
                print("Still scanning...")
            }
        }
    }
}

Upgrading firmware

Most of Kontakt.io beacons have an upgradeable firmware. This way we can enable new features on your existing beacons.

In a way upgrading firmware is similar to writing a new configuration. First you need to set up a connection to a beacon and then call updateWithFirmware:progress:completion: method on that connection. But before you do that, you will need to get a firmware binary appropriate for beacons you want to upgrade. That's what KTKFirmware class method getFirmwaresForUniqueIDs:completion: is for:

extension ViewController: KTKDevicesManagerDelegate {
    func devicesManager(_ manager: KTKDevicesManager, didDiscover devices: [KTKNearbyDevice]?) {
        if let device = devices?.filter({$0.uniqueID == "abcd"}).first {
            manager.stopDevicesDiscovery()
            connection = KTKDeviceConnection(nearbyDevice: device)

            KTKFirmware.getFirmwaresForUniqueIDs(["abcd"]) { firmware, error in
                if let fw = firmware?.first {
                    self.connection.update(with: fw, progress: { print("Firmware upgrade progress: \($0)%")}) { synchronized, error in
                        if let _ = error {
                            print("Firmware upgrade failed")
                        } else if !synchronized {
                            // Kontakt.io Cloud has not been notified about a firmware upgrade.
                            // Save this information and try later.
                        } else {
                            print("Successful firmware upgrade")
                        }
                    }
                }
            }
        }
    }
}