Manage 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
KTKDevicesManager
classAn 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 Install 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 toKTKDevicesManagerDiscoveryModeAuto
startDevicesDiscoveryWithInterval:
will set the discovery mode toKTKDevicesManagerDiscoveryModeInterval
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
classKTKNearbyDevice
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
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 inif 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 = 123newConfiguration.transmissionPower = .power5}}}
Getting a pending configuration from Kontakt.io Cloud
A new configuration for a beacon can also be created from Kio Cloud or the Device Management 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 = 123newConfiguration.transmissionPower = .power5connection.write(newConfiguration) { synchronized, configuration, error inif 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 UIKitimport KontaktSDKclass 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) inif 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 inif let fw = firmware?.first {self.connection.update(with: fw, progress: { print("Firmware upgrade progress: \($0)%")}) { synchronized, error inif 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")}}}}}}}