/sdk-ble-swift

Swift BLE SDK for app developers who want better bluetooth performance

Primary LanguageSwift

logo

BCH compliance Carthage compatible

Swift Bluetooth SDK

The XYO Foundation provides this source code available in our efforts to advance the understanding of the XYO Procotol and its possible uses. We continue to maintain this software in the interest of developer education. Usage of this source code is not intended for production.

Table of Contents

Description

A Bluetooth library, primarily for use with XY Finder devices but can be implemented to communicate with any Bluetooth device, with monitoring capability if the device emits an iBeacon signal. The library is designed to aleviate the delegate-based interaction with Core Bluetooth classes and presents a straightforward API, allowing the developer to write asyncronous code in a syncronous manner. The libray utlizes the Google Promises library as a dependency.

Requirements

  • iOS 11.0+
  • MacOS 10.13+
  • Xcode 10.1+
  • Swift 4.2+

Install

Swift Package Manager

    .package(url: "https://github.com/XYOracleNetwork/sdk-ble-swift.git", from: "3.1.6")

CocoaPods

Note that CocoaPods support is only for iOS currently

CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:

$ gem install cocoapods

CocoaPods 1.6.0.beta.2+ is required.

To integrate into your Xcode project using CocoaPods, specify it in your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'
use_frameworks!

target '<Your Target Name>' do
    pod 'XyBleSdk', '~> 3.0.7'
end

Then, run the following command:

$ pod install

Carthage

Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.

You can install Carthage with Homebrew using the following command:

$ brew update
$ brew install carthage

To integrate into your Xcode project using Carthage, specify it in your Cartfile:

github "XYOracleNetwork/sdk-ble-swift" ~> 3.0.7

Run carthage update --use-submodules to build the framework and drag the built XyBleSdk.framework, FBLPromises.framework and Promises.framework to the Linked Frameworks and Libraries of your Xcode project. Then switch to the Build Phases tab and add a New run script phase. Expand Run Script and add the following to the Shell text field:

/usr/local/bin/carthage copy-frameworks

Click the + button under Input Files and add:

$(SRCROOT)/Carthage/Build/<target platform>/Promises.framework
$(SRCROOT)/Carthage/Build/<target platform>/FBLPromises.framework
$(SRCROOT)/Carthage/Build/<target platform>/XyBleSdk.framework

Finally, you will need to add a New Copy Files Phase, selecting Frameworks for the Destination and adding the three frameworks, ensuring the Code Sign On Copy boxes are checked.

SDK Overview

Talking to a Bluetooth device using Core Bluetooth is a drag. The developer needs to monitor delegate methods from CBCentral and CBPeripheral, with no clear path to handling multiple connections. Tutorial code for Core Bluetooth is often a chain of use-case specific method calls from within these delegates, which can lead to frustration when trying to apply the code in a more resusable pattern. Bluetooth devices are often not predictable in their reponse times due to firmware and environmental conditions, which can make them tricky to deal with, especially if the application requires multiple, disparate devices connected to operate properly.

Code Examples

The XyBleSdk provides a simple interface to communicating with an XY Finder or other Bluetooth device. Let's take a look at an example for an XY Finder device:

let device = XYBluetoothDeviceFactory.build(from: "xy:ibeacon:a44eacf4-0104-0000-0000-5f784c9977b5.20.28772")
var batteryLevel: Int?
device.connection {
    batteryLevel = device.get(BatteryService.level, timeout: .seconds(10)).asInteger
    if let level = batteryLevel, level > 15 {
        self.batteryStatus = "Battery at \(level)"
    } else {
        self.batteryStatus = "Battery level low"
    }
}

The XYBluetoothDeviceFactory can build a device from a string, peripheral, etc. Using connection manages the wrangling of the CBCentral and associated CBPeripheral delegates, ensuring you have a connection before trying any GATT operation(s) in the block.

The get, set, and notify methods operate on the specified device and block until the result is returned. This allows the developer to write syncronous code without waiting for a callback or delegate method to be called, or deal with the underlying promises directly. Each operation can also take a timeout if so desired; the default is 30 seconds.

Once all the operations have completed, you can use then if there are post actions you wish to run:

let device = XYBluetoothDeviceFactory.build(from: "xy:ibeacon:a44eacf4-0104-0000-0000-5f784c9977b5.20.28772")
var batteryLevel: Int = 0
device.connection {
    batteryLevel = device.get(BatteryService.level, timeout: .seconds(10)).asInteger
    if let level = batteryLevel, level > 15 {
        self.batteryStatus = "Battery at \(level)"
    } else {
        self.batteryStatus = "Battery level low"
    }
}.then {
    self.showBatteryNotification(for: batteryLevel)
}

You can check for an error from your operations by using hasError in the result. The error is of type XYFinderBluetoothError.

let device = XYBluetoothDeviceFactory.build(from: "xy:ibeacon:a44eacf4-0104-0000-0000-5f784c9977b5.20.28772")
var batteryLevel: Int = 0
device.connection {
    batteryLevel = device.get(BatteryService.level, timeout: .seconds(10)).asInteger
    guard batteryLevel.hasError == false else { return }
    if let level = batteryLevel, level > 15 {
        self.batteryStatus = "Battery at \(level)"
    } else {
        self.batteryStatus = "Battery level low"
    }
}.then {
    self.showBatteryNotification(for: batteryLevel)
}

If you wish a specific action to always be run regardless of the result, you can use always:

let device = XYBluetoothDeviceFactory.build(from: "xy:ibeacon:a44eacf4-0104-0000-0000-5f784c9977b5.20.28772")
var batteryLevel: Int = 0
device.connection {
    batteryLevel = device.get(BatteryService.level, timeout: .seconds(10)).asInteger
    guard batteryLevel.hasError == false else { return }
    if let level = batteryLevel, level > 15 {
        self.batteryStatus = "Battery at \(level)"
    } else {
        self.batteryStatus = "Battery level low"
    }
}.then {
    self.showBatteryNotification(for: batteryLevel)
}.always {
    self.updateView()
}

Services

The library provides three types of communication with a Bluetooth device, get, set, and notify. These operate on the characteristic of a GATT service, which is defined with the XYServiceCharacteristicType protocol. Add a new service by creating an enumeration that implements this protocol:

public enum MyService: String, XYServiceCharacteristic {

    public var serviceUuid: CBUUID { return MyService.serviceUuid }

    case level

    public var characteristicUuid: CBUUID {
        return BatterySeMyServicervice.uuids[self]!
    }

    public var characteristicType: XYServiceCharacteristicType {
        return .integer
    }

    public var displayName: String {
        return "My Level"
    }

    private static let serviceUuid = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB")

    private static let uuids: [MyService: CBUUID] = [
        .level: CBUUID(string: "00002a19-0000-1000-8000-00805f9b34fb")
    ]

    public static var values: [XYServiceCharacteristic] = [
        level
    ]
}

Operation Results

The XYBluetoothResult class wraps the data received from a get call and allows data to be passed to a get service call. The XYBluetoothResult allows for access to the raw data, as well as the convenience methods asInteger, asString and asByteArray. Any error information is available in error as an XYFinderBluetoothError.

Device Event Notifications

When a connected device changes state or an operation is performed (such as pressing the button on an XY4+) a XYFinderEvent notification is sent out via the XYFinderDeviceEventManager. You can subscribe to these events as shown here:

self.subscriptionUuid = XYFinderDeviceEventManager.subscribe(to: [.buttonPressed]) { event in
    switch event {
    case .buttonPressed(let device, _):
        guard let currentDevice = self.selectedDevice, currentDevice == device else { return }
        self.buttonPressed(on: device)
    default:
        break
    }
}

The result of the subscribe call is a subscription UUID that you can use to unsubscribe from further notifcations:

XYFinderDeviceEventManager.unsubscribe(to: [.buttonPressed], referenceKey: self.subscriptionUuid)

Smart Scan

The XYSmartScan singleton can be used to range and monitor for XY Finder devices. When using the library in an iOS application, it will range for devices in a particular XY Finder device family when put into foreground mode using switchToForeground, and use lower power monitoring when placed into backgound mode with switchToBackground. The macOS library will locate devices using the CBCentralManager.scanForPeripherals method.

Sample Projects

The library comes with two sample projects, one for macOS and one for iOS. The macOS sample requires you to run carthage update in the project directory. The iOS sample requires either pod install or carthage update to be run.

Maintainers

  • Arie Trouw
  • Carter Harrison

License

See the LICENSE file for license details.

Credits

Made with 🔥and ❄️ by XYO