Bluejay is a simple Swift framework for building reliable Bluetooth LE apps.

Overview

Bluejay

CocoaPods Compatible Carthage compatible Platform license

Bluejay is a simple Swift framework for building reliable Bluetooth LE apps.

Bluejay's primary goals are:

  • Simplify talking to a single Bluetooth LE peripheral
  • Make it easier to handle Bluetooth operations reliably
  • Take advantage of Swift features and conventions

Index

Features

  • A callback-based API
  • A FIFO operation queue for more synchronous and predictable behaviour
  • A background task mode for batch operations that avoids the "callback pyramid of death"
  • Simple protocols for data serialization and deserialization
  • An easy and safe way to observe connection states
  • Powerful background restoration support
  • Extended error handling and logging support

Requirements

  • iOS 11 or later recommended
  • Xcode 11.3.1 or later recommended
  • Swift 5 or later recommended

Installation

CocoaPods

pod 'Bluejay', '~> 0.8'

Or to try the latest master:

pod 'Bluejay', :git => 'https://github.com/steamclock/bluejay.git', :branch => 'master'

Carthage

0.8 github "DaveWoodCom/XCGLogger" ~> 6.1.0 ">
github "steamclock/bluejay" ~> 0.8
github "DaveWoodCom/XCGLogger" ~> 6.1.0

Refer to official Carthage documentation for the rest of the instructions.

Note: Bluejay.framework, ObjcExceptionBridging.framework, and XCGLogger.framework are all required.

Import

import Bluejay

Demo

The iOS Simulator does not simulate Bluetooth, and you may not have a debuggable Bluetooth LE peripheral handy, so we have prepared you a pair of demo apps to test with.

  1. BluejayHeartSensorDemo: an app that can connect to a Bluetooth LE heart sensor.
  2. DittojayHeartSensorDemo: a virtual Bluetooth LE heart sensor.

To try out Bluejay:

  1. Get two iOS devices – one to run Bluejay Demo, and the other to run Dittojay Demo.
  2. Grant permission for notifications on Bluejay Demo.
  3. Grant permission for background mode on Dittojay Demo.
  4. Connect using Bluejay Demo.

To try out background restoration (after connecting):

  1. In Bluejay Demo, tap on "End listen to heart rate".
  • This is to prevent the continuous heart rate notification from triggering state restoration right after we terminate the app, as it's much clearer and easier to verify state restoration when we can manually trigger a Bluetooth event at our own leisure and timing.
  1. Tap on "Terminate app".
  • This will crash the app, but also simulate app termination due to memory pressure, and allow CoreBluetooth to cache the current session and wait for Bluetooth events to begin state restoration.
  1. In Dittojay Demo, tap on "Chirp" to revive Bluejay Demo
  • This will send a Bluetooth event to the device with the terminated Bluejay Demo, and its CoreBluetooth stack will wake up the app in the background and execute a few quick tasks, such as scheduling a few local notifications for verification and debugging purposes in this case.

Usage

Initialization

To create an instance of Bluejay:

let bluejay = Bluejay()

While it is convenient to create one Bluejay instance and use it everywhere, you can also create instances in specific portions of your app and tear them down after use. It's worth noting, however, that each instance of Bluejay has its own CBCentralManager, which makes the multi-instance approach somewhat more complex.

Once you've created an instance, you can start running Bluejay, which will then initialize the CoreBluetooth session. Note that instantiating a Bluejay instance and running a Bluejay instance are two separate operations.

You must always start Bluejay in your AppDelegate's application(_:didFinishLaunchingWithOptions:) if you want to support background restoration, otherwise you are free to start Bluejay anywhere appropriate in your app. For example, apps that don't require background restoration often initialize and start their Bluejay instance from the initial view controller.

bluejay.start()

If your app needs Bluetooth to work in the background, then you have to support background restoration in your app. While Bluejay has already simplified much of background restoration for you, it will still take some extra work, and we also recommend reviewing the relevant Apple docs. Background restoration is tricky and difficult to get right.

Bluejay also supports CoreBluetooth migration for working with other Bluetooth libraries or with your own Bluetooth code.

Bluetooth Events

The ConnectionObserver protocol allows a class to monitor and to respond to major Bluetooth and connection-related events:

public protocol ConnectionObserver: class {
    func bluetoothAvailable(_ available: Bool)
    func connected(to peripheral: PeripheralIdentifier)
    func disconnected(from peripheral: PeripheralIdentifier)
}

You can register a connection observer using:

bluejay.register(connectionObserver: batteryLabel)

Unregistering a connection observer is not necessary, because Bluejay only holds weak references to registered observers, so Bluejay will clear nil observers from its list when they are found at the next event's firing. But if you need to do so before that happens, you can use:

bluejay.unregister(connectionObserver: rssiLabel)

Services and Characteristics

In Bluetooth parlance, a Service is a group of attributes, and a Characteristic is an attribute belonging to a group. For example, BLE peripherals that can detect heart rates typically have a Service named "Heart Rate" with a UUID of "180D". Inside that Service are Characteristics such as "Body Sensor Location" with a UUID of "2A38", as well as "Heart Rate Measurement" with a UUID of "2A37".

Many of these Services and Characteristics are standards specified by the Bluetooth SIG organization, and most hardware adopt their specifications. For example, most BLE peripherals implement the Service "Device Information" which has a UUID of "180A", which is where Characteristics such as firmware version, serial number, and other hardware details can be found. Of course, there are many BLE uses not covered by the Bluetooth Core Spec, and custom hardware often have their own unique Services and Characteristics.

Here is how you can specify Services and Characteristics for use in Bluejay:

let heartRateService = ServiceIdentifier(uuid: "180D")
let bodySensorLocation = CharacteristicIdentifier(uuid: "2A38", service: heartRateService)
let heartRate = CharacteristicIdentifier(uuid: "2A37", service: heartRateService)

Bluejay uses the ServiceIdentifier and CharacteristicIdentifier structs to avoid problems like accidentally specifying a Service when a Characteristic is expected.

Scanning

Bluejay has a powerful scanning API that can be be used simply or customized to satisfy many use cases.

CoreBluetooth scans for devices using services. In other words, CoreBluetooth, and therefore Bluejay, expects you to know beforehand one or several public services the peripherals you want to scan for contains.

Basic Scanning

This simple call will just notify you when there is a new discovery, and when the scan has finished:

bluejay.scan(
    serviceIdentifiers: [heartRateService],
    discovery: { [weak self] (discovery, discoveries) -> ScanAction in
        guard let weakSelf = self else {
            return .stop
        }

        weakSelf.discoveries = discoveries
        weakSelf.tableView.reloadData()

        return .continue
    },
    stopped: { (discoveries, error) in
        if let error = error {
            debugPrint("Scan stopped with error: \(error.localizedDescription)")
        }
        else {
            debugPrint("Scan stopped without error.")
        }
})

A scan result (ScanDiscovery, [ScanDiscovery]) contains the current discovery followed by an array of all the discoveries made so far.

The stopped result contains a final list of discoveries available just before stopping, and an error if there is one. If there isn't an error, that means that the scan was stopped intentionally or expectedly.

Scan Action

A ScanAction is returned at the end of a discovery callback to tell Bluejay whether to keep scanning or to stop.

public enum ScanAction {
    case `continue`
    case blacklist
    case stop
    case connect(ScanDiscovery, (ConnectionResult) -> Void)
}

Returning blacklist will ignore any future discovery of the same peripheral within the current scan session. This is only useful when allowDuplicates is set to true. See Apple docs on CBCentralManagerScanOptionAllowDuplicatesKey for more info.

Returning connect will make Bluejay stop the scan as well as perform your connection request. This is useful if you want to connect right away when you've found the peripheral you're looking for.

Tip: You can set up the ConnectionResult block outside the scan call to reduce callback nesting.

Monitoring

Another useful way to use the scanning API is to scan continuously, i.e. to monitor, for purposes such as observing the RSSI changes of nearby peripherals to estimate their proximity:

bluejay.scan(
    duration: 15,
    allowDuplicates: true,
    serviceIdentifiers: nil,
    discovery: { [weak self] (discovery, discoveries) -> ScanAction in
        guard let weakSelf = self else {
            return .stop
        }

        weakSelf.discoveries = discoveries
        weakSelf.tableView.reloadData()

        return .continue
    },
    expired: { [weak self] (lostDiscovery, discoveries) -> ScanAction in
        guard let weakSelf = self else {
            return .stop
        }

        debugPrint("Lost discovery: \(lostDiscovery)")

        weakSelf.discoveries = discoveries
        weakSelf.tableView.reloadData()

        return .continue
}) { (discoveries, error) in
        if let error = error {
            debugPrint("Scan stopped with error: \(error.localizedDescription)")
        }
        else {
            debugPrint("Scan stopped without error.")
        }
}

Setting allowDuplicates to true will stop coalescing multiple discoveries of the same peripheral into one single discovery callback. Instead, you'll get a discovery call every time a peripheral's advertising packet is picked up. This will consume more battery, and does not work in the background.

Warning: An allow duplicates scan will stop with an error if your app is backgrounded during the scan.

The expired callback is only invoked when allowDuplicates is true. This is called when Bluejay estimates that a previously discovered peripheral is likely out of range or no longer broadcasting. Essentially, when allowDuplicates is set to true, every time a peripheral is discovered a timer associated with that peripheral starts counting down. If that peripheral is within range, and even if it has a slow broadcasting interval, it is likely that peripheral will be picked up by the scan again and cause the timer to refresh. If not and the timer expires without being refreshed, Bluejay makes an educated guess and suggests that the peripheral is no longer reachable. Be aware that this is an estimation.

Warning: Setting serviceIdentifiers to nil will result in picking up all available Bluetooth peripherals in the vicinity, but is not recommended by Apple. It may cause battery and cpu issues on prolonged scanning, and it also doesn't work in the background. It is not a private API call, but an available option where you need a quick solution when testing and prototyping.

Tip: Specifying at least one specific service identifier is the most common way to scan for Bluetooth devices in iOS. If you need to scan for all Bluetooth devices, we recommend making use of the duration parameter to stop the scan after 5 ~ 10 seconds to avoid scanning indefinitely and overloading the hardware.

Connecting

It is important to keep in mind that Bluejay is designed to work with a single BLE peripheral. Multiple connections at once is not currently supported, and a connection request will fail if Bluejay is already connected or is still connecting. Although this can be a limitation for some sophisticated apps, it is more commonly a safeguard to ensure your app does not issue connections unnecessarily or erroneously.

bluejay.connect(selectedSensor, timeout: .seconds(15)) { result in
    switch result {
    case .success:
        debugPrint("Connection attempt to: \(selectedSensor.description) is successful")
    case .failure(let error):
        debugPrint("Failed to connect with error: \(error.localizedDescription)")
    }
}

Timeouts

You can also specify a timeout for a connection request, default is no timeout:

public enum Timeout {
    case seconds(TimeInterval)
    case none
}

Tip: We recommend always setting at least a 15 seconds timeout for your connection requests.

Disconnect

To disconnect:

bluejay.disconnect()

Bluejay also supports finer controls over your disconnection:

Queued Disconnect

A queued disconnect will be queued like all other Bluejay API requests, so the disconnect attempt will wait for its turn until all the queued tasks are finished.

To perform a queued disconnect, simply call:

bluejay.disconnect()

Immediate Disconnect

An immediate disconnect will immediately fail and empty all tasks from the queue even if they are still running and then immediately disconnect.

There are two ways to perform an immediate disconnect:

bluejay.disconnect(immediate: true)
bluejay.cancelEverything()

Expected vs Unexpected Disconnection

Bluejay's log will describe in detail whether a disconnection is expected or unexpected. This is important when debugging a disconnect-related issue, as well as explaining why Bluejay is or isn't attempting to auto reconnect.

Any explicit call to disconnect or cancelEverything with disconnect will result in an expected disconnection.

All other disconnection events will be considered unexpected. For examples:

  • If a connection attempt fails due to hardware errors and not from a timeout
  • If a connected device moves out of range
  • If a connected device runs out of battery or is shut off
  • If a connected device's Bluetooth module crashes and is no longer negotiable

Cancel Everything

The reason why there is a cancelEverything API in addition to disconnect, is because sometimes we want to cancel everything in the queue but remain connected.

bluejay.cancelEverything(shouldDisconnect: false)

Auto Reconnect

By default, shouldAutoReconnect is true and Bluejay will always try to automatically reconnect after an unexpected disconnection.

Bluejay will only set shouldAutoReconnect to false under these circumstances:

  1. If you manually call disconnect and the disconnection is successful.
  2. If you manually call cancelEverything and its disconnection is successful.

Bluejay will also always reset shouldAutoReconnect to true on a successful connection to a peripheral, as we usually want to reconnect to the same device as soon as possible if a connection is lost unexpectedly during normal usage.

However, there are some cases where auto reconnect is not desirable. In those cases, use a DisconnectHandler to evaluate and to override auto reconnect.

Disconnect Handler

A disconnect handler is a single delegate that is suitable for performing major recovery, retry, or reset operations, such as restarting a scan when there is a disconnection.

The purpose of this handler is to help avoid writing and repeating major resuscitation and error handling logic inside the error callbacks of your regular connect, disconnect, read, write, and listen calls. Use the disconnect handler to perform one-time and significant operations at the very end of a disconnection.

In addition to helping you avoid redundant and conflicted logic in various callbacks when there is a disconnection, the disconnect handler also allows you to evaluate and to control Bluejay's auto-reconnect behaviour.

For example, this protocol implementation will always turn off auto reconnect whenever there is a disconnection, expected or not.

func didDisconnect(
  from peripheral: PeripheralIdentifier,
  with error: Error?,
  willReconnect autoReconnect: Bool) -> AutoReconnectMode {
    return .change(shouldAutoReconnect: false)
}

We also anticipate that for most apps, different view controllers may want to handle disconnection differently, so simply register and replace the existing disconnect handler as your user navigates to different parts of your app.

bluejay.registerDisconnectHandler(handler: self)

Similar to connection observers, you do not have to explicitly unregister unless you need to.

Connection States

Your Bluejay instance has these properties to help you make connection-related decisions:

  • isBluetoothAvailable
  • isBluetoothStateUpdateImminent
  • isConnecting
  • isConnected
  • isDisconnecting
  • shouldAutoReconnect
  • isScanning
  • hasStarted
  • defaultWarningOptions
  • isBackgroundRestorationEnabled

Deserialization and Serialization

Reading, writing, and listening to Characteristics is straightforward in Bluejay. Most of the work involved is building out the deserialization and serialization for your data. Let's have a quick look at how Bluejay helps standardize this process in your app via the Receivable and Sendable protocols.

Receivable

Models that represent data you wish to read and receive from your peripheral should all conform to the Receivable protocol.

Here is a partial example for the Heart Rate Measurement Characteristic:

import Bluejay
import Foundation

struct HeartRateMeasurement: Receivable {

    private var flags: UInt8 = 0
    private var measurement8bits: UInt8 = 0
    private var measurement16bits: UInt16 = 0
    private var energyExpended: UInt16 = 0
    private var rrInterval: UInt16 = 0

    private var isMeasurementIn8bits = true

    var measurement: Int {
        return isMeasurementIn8bits ? Int(measurement8bits) : Int(measurement16bits)
    }

    init(bluetoothData: Data) throws {
        flags = try bluetoothData.extract(start: 0, length: 1)

        isMeasurementIn8bits = (flags & 0b00000001) == 0b00000000

        if isMeasurementIn8bits {
            measurement8bits = try bluetoothData.extract(start: 1, length: 1)
        } else {
            measurement16bits = try bluetoothData.extract(start: 1, length: 2)
        }
    }

}

Note how you can use the extract function that Bluejay adds to Data to easily parse the bytes you need. We have plans to build more protection and error handling for this in the future.

Finally, while it is not essential and it will depend on the context, we suggest only exposing the needed and computed properties of your models.

Sendable

Models representing data you wish to send to your peripheral should all conform to the Sendable protocol. In a nutshell, this is how you help Bluejay determine how to convert your models into Data:

import Foundation
import Bluejay

struct Coffee: Sendable {

    let data: UInt8

    init(coffee: CoffeeEnum) {
        data = UInt8(coffee.rawValue)
    }

    func toBluetoothData() -> Data {
        return Bluejay.combine(sendables: [data])
    }

}

The combine helper function makes it easier to group and to sequence the outgoing data.

Sending and Receiving Primitives

In some cases, you may want to send or receive data simple enough that creating a custom struct which implements Sendable or Receivable to hold it is unnecessarily complicated. For those cases, Bluejay also retroactively conforms several built-in Swift types to Sendable and Receivable. Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Data are all conformed to both protocols and so they can all be sent or received directly.

Int and UInt are intentionally not conformed. Bluetooth values are always sent and/or received at a specific bit width. The intended bit width for an Int is ambiguous, and trying to use one often indicates a programmer error, in the form of not considering the bit width the Bluetooth device is expecting on a characteristic.

Interactions

Once you have your data modelled using either the Receivable or Sendable protocol, the read, write, and listen APIs in Bluejay should handle the deserialization and serialization seamlessly for you. All you need to do is to specify the type for the generic result wrappers: ReadResult or WriteResult.

Reading

Here is an example showing how to read the sensor body location characteristic, and converting its value to its corresponding string and display it in the UI.

) in guard let weakSelf = self else { return } switch result { case .success(let location): debugPrint("Read from sensor location is successful: \(location)") var locationString = "Unknown" switch location { case 0: locationString = "Other" case 1: locationString = "Chest" case 2: locationString = "Wrist" case 3: locationString = "Finger" case 4: locationString = "Hand" case 5: locationString = "Ear Lobe" case 6: locationString = "Foot" default: locationString = "Unknown" } weakSelf.sensorLocationCell.detailTextLabel?.text = locationString case .failure(let error): debugPrint("Failed to read sensor location with error: \(error.localizedDescription)") } } ">
let heartRateService = ServiceIdentifier(uuid: "180D")
let sensorLocation = CharacteristicIdentifier(uuid: "2A38", service: heartRateService)

bluejay.read(from: sensorLocation) { [weak self] (result: ReadResult<UInt8>) in
    guard let weakSelf = self else {
	     return
    }

    switch result {
    case .success(let location):
        debugPrint("Read from sensor location is successful: \(location)")

        var locationString = "Unknown"

        switch location {
        case 0:
            locationString = "Other"
        case 1:
            locationString = "Chest"
        case 2:
            locationString = "Wrist"
        case 3:
            locationString = "Finger"
        case 4:
            locationString = "Hand"
        case 5:
            locationString = "Ear Lobe"
        case 6:
            locationString = "Foot"
        default:
            locationString = "Unknown"
        }

        weakSelf.sensorLocationCell.detailTextLabel?.text = locationString
    case .failure(let error):
        debugPrint("Failed to read sensor location with error: \(error.localizedDescription)")
    }
}

Writing

Writing to a characteristic is very similar to reading:

let heartRateService = ServiceIdentifier(uuid: "180D")
let sensorLocation = CharacteristicIdentifier(uuid: "2A38", service: heartRateService)

bluejay.write(to: sensorLocation, value: UInt8(2)) { result in
    switch result {
    case .success:
        debugPrint("Write to sensor location is successful.")
    case .failure(let error):
        debugPrint("Failed to write sensor location with error: \(error.localizedDescription)")
    }
}

Listening

Listening turns on broadcasting on a characteristic and allows you to receive its notifications.

Unlike read and write where the completion block is only called once, listen callbacks are persistent. It could be minutes (or never) before the receive block is called, and the block can be called multiple times.

Some Bluetooth devices will turn off notifications when it is disconnected, some don't. That said, when you don't need to listen anymore, it is generally good practice to always explicitly turn off broadcasting on that characteristic using the endListen function.

Not all characteristics support listening, it is a feature that must be enabled for a characteristic on the Bluetooth device itself.

) in guard let weakSelf = self else { return } switch result { case .success(let heartRate): weakSelf.heartRate = heartRate weakSelf.tableView.reloadData() case .failure(let error): debugPrint("Failed to listen with error: \(error.localizedDescription)") } } ">
let heartRateService = ServiceIdentifier(uuid: "180D")
let heartRateCharacteristic = CharacteristicIdentifier(uuid: "2A37", service: heartRateService)

bluejay.listen(to: heartRateCharacteristic, multipleListenOption: .replaceable)
{ [weak self] (result: ReadResult<HeartRateMeasurement>) in
        guard let weakSelf = self else {
            return
        }

        switch result {
        case .success(let heartRate):
            weakSelf.heartRate = heartRate
            weakSelf.tableView.reloadData()
        case .failure(let error):
            debugPrint("Failed to listen with error: \(error.localizedDescription)")
        }
}

Multiple Listen Options

You can only have one listener callback installed per characteristic. If you need multiple observers on the same characteristic, you can still do so yourself using just one Bluejay listener and within it create your own app-specific notifications.

Pass in the appropriate MultipleListenOption in your listen call to either protect against multiple listen attempts on the same characteristic, or to intentionally allow overwriting an existing listen.

/// Ways to handle calling listen on the same characteristic multiple times.
public enum MultipleListenOption: Int {
    /// New listen on the same characteristic will not overwrite an existing listen.
    case trap
    /// New listens on the same characteristic will replace the existing listen.
    case replaceable
}

Background Task

Bluejay also supports performing a longer series of reads, writes, and listens in a background thread. Each operation in a background task is blocking and will not return until completed.

This is useful when you need to complete a specific and large task such as syncing or upgrading to a new firmware. This is also useful when working with a notification-based Bluetooth module where you need to pause and wait for Bluetooth execution, primarily the listen operation, but without blocking the main thread.

Bluejay will call your completion block on the main thread when everything finishes without an error, or if any one of the operations in the background task has failed.

Here's a made-up example in trying get both user and admin access to a Bluetooth device using the same password:

ListenAction in if let responseCode = AuthResponse(rawValue: response) { isUserAuthenticated = responseCode == .success } return .done }) try peripheral.writeAndListen( writeTo: adminAuth, value: passwordData, listenTo: adminAuth, timeoutInSeconds: .seconds(15), completion: { (response: UInt8) -> ListenAction in if let responseCode = AuthResponse(rawValue: response) { isAdminAuthenticated = responseCode == .success } return .done }) } // 5. Return results of authentication. return (isUserAuthenticated, isAdminAuthenticated) }, completionOnMainThread: { (result) in switch result { case .success(let authResults): debugPrint("Is user authenticated: \(authResults.0)") debugPrint("Is admin authenticated: \(authResults.1)") case .failure(let error): debugPrint("Background task failed with error: \(error.localizedDescription)") } }) ">
var isUserAuthenticated = false
var isAdminAuthenticated = false

bluejay.run(backgroundTask: { (peripheral) -> (Bool, Bool) in
    // 1. No need to perform any Bluetooth tasks if there's no password to try.
    guard let password = enteredPassword else {
      return (false, false)
    }

    // 2. Flush auth characteristics in case they are still broadcasting unwanted data.
    try peripheral.flushListen(to: userAuth, nonZeroTimeout: .seconds(3), completion: {
        debugPrint("Flushed buffered data on the user auth characteristic.")
    })

    try peripheral.flushListen(to: adminAuth, nonZeroTimeout: .seconds(3), completion: {
        debugPrint("Flushed buffered data on the admin auth characteristic.")
    })

    // 3. Sanity checks, making sure the characteristics are not broadcasting anymore.
    try peripheral.endListen(to: userAuth)
    try peripheral.endListen(to: adminAuth)

    // 4. Attempt authentication.
    if let passwordData = password.data(using: .utf8) {
        debugPrint("Begin authentication...")

        try peripheral.writeAndListen(
            writeTo: userAuth,
            value: passwordData,
            listenTo: userAuth,
            timeoutInSeconds: .seconds(15),
            completion: { (response: UInt8) -> ListenAction in
                if let responseCode = AuthResponse(rawValue: response) {
                    isUserAuthenticated = responseCode == .success
                }

                return .done
        })

        try peripheral.writeAndListen(
            writeTo: adminAuth,
            value: passwordData,
            listenTo: adminAuth,
            timeoutInSeconds: .seconds(15),
            completion: { (response: UInt8) -> ListenAction in
                if let responseCode = AuthResponse(rawValue: response) {
                    isAdminAuthenticated = responseCode == .success
                }

                return .done
        })
    }

    // 5. Return results of authentication.
    return (isUserAuthenticated, isAdminAuthenticated)
}, completionOnMainThread: { (result) in
    switch result {
    case .success(let authResults):
        debugPrint("Is user authenticated: \(authResults.0)")
        debugPrint("Is admin authenticated: \(authResults.1)")
    case .failure(let error):
        debugPrint("Background task failed with error: \(error.localizedDescription)")
    }
})

Important:

While Bluejay will not crash because it has built in error handling that will inform you of the following violations, these rules are are still worth calling out:

  1. Do not call any regular read/write/listen functions inside the backgroundTask block. Use the SynchronizedPeripheral provided to you and its read/write/listen API instead.
  2. Regular read/write/listen calls outside of the backgroundTask block will also not work when a background task is still running.

Note that because the backgroundTask block is running on a background thread, you need to be careful about accessing any global or captured data inside that block for thread safety reasons, like you would with any GCD or OperationQueue task. To help with this, use run(userData:backgroundTask:completionOnMainThread:) to pass an object you wish to have thread-safe access to while working inside the background task.

Background Restoration

CoreBluetooth allows apps to continue processing active Bluetooth operations when it is backgrounded or even when it is evicted from memory. In Bluejay, we refer to this feature and behaviour as "background restoration". For examples, a pending connect request that finishes, or a subscribed characteristic that fires a notification, can cause the system to wake or restart the app in the background. This can, for example, allow syncing data from a device without requiring the user to launch the app.

In order to support background Bluetooth, there are two steps to take:

  1. Give your app permission to use Bluetooth in the background
  2. Implement and handle state restoration

Background Permission

This is the easy step. Just turn on the Background Modes capability in your Xcode project with Uses Bluetooth LE accessories enabled.

State Restoration

Bluejay already handles much of the gnarly state restoration implementation for you. However, there are still a few things you need to do to help Bluejay help you:

  1. Create a background restoration configuration with a restore identifier
  2. Always start your Bluejay instance in your AppDelegate's application(_:didFinishLaunchingWithOptions:)
  3. Always pass Bluejay the launchOptions
  4. Setup a BackgroundRestorer and a ListenRestorer to handle restoration results
BackgroundRestoreCompletion { // Opportunity to perform syncing related logic here. return .continue } func didFailToRestoreConnection( to peripheral: PeripheralIdentifier, error: Error) -> BackgroundRestoreCompletion { // Opportunity to perform cleanup or error handling logic here. return .continue } } extension AppDelegate: ListenRestorer { func didReceiveUnhandledListen( from peripheral: PeripheralIdentifier, on characteristic: CharacteristicIdentifier, with value: Data?) -> ListenRestoreAction { // Re-install or defer installing a callback to a notifying characteristic. return .promiseRestoration } } ">
import Bluejay
import UIKit

let bluejay = Bluejay()

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        let backgroundRestoreConfig = BackgroundRestoreConfig(
            restoreIdentifier: "com.steamclock.bluejayHeartSensorDemo",
            backgroundRestorer: self,
            listenRestorer: self,
            launchOptions: launchOptions)

        let backgroundRestoreMode = BackgroundRestoreMode.enable(backgroundRestoreConfig)

        let options = StartOptions(
          enableBluetoothAlert: true,
          backgroundRestore: backgroundRestoreMode)

        bluejay.start(mode: .new(options))

        return true
    }

}

extension AppDelegate: BackgroundRestorer {
    func didRestoreConnection(
      to peripheral: PeripheralIdentifier) -> BackgroundRestoreCompletion {
        // Opportunity to perform syncing related logic here.
        return .continue
    }

    func didFailToRestoreConnection(
      to peripheral: PeripheralIdentifier, error: Error) -> BackgroundRestoreCompletion {
        // Opportunity to perform cleanup or error handling logic here.
        return .continue
    }
}

extension AppDelegate: ListenRestorer {
    func didReceiveUnhandledListen(
      from peripheral: PeripheralIdentifier,
      on characteristic: CharacteristicIdentifier,
      with value: Data?) -> ListenRestoreAction {
        // Re-install or defer installing a callback to a notifying characteristic.
        return .promiseRestoration
    }
}

While Bluejay has simplified background restoration to just a few initialization rules and two protocols, it can still be difficult to get right. Please contact us if you have any questions

Listen Restoration

If you app is evicted from memory, you lose all your listen callbacks as well. Yet, the Bluetooth device can still be broadcasting on the characteristics you were listening to. Listen restoration gives you an opportunity to restore and to respond to that notification when your app is restored in the background.

If you need to re-install a listen, simply call listen again as you normally would when setting up a new listen inside didReceiveUnhandledListen(from:on:with:) before returning .promiseRestoration. Otherwise, return .stopListen to ask Bluejay to turn off notification on that characteristic.

/**
 * Available actions to take on an unhandled listen event from background restoration.
 */
public enum ListenRestoreAction {
    /// Bluejay will continue to receive but do nothing with the incoming listen events until a new listener is installed.
    case promiseRestoration
    /// Bluejay will attempt to turn off notifications on the peripheral.
    case stopListen
}
extension AppDelegate: ListenRestorer {
    func didReceiveUnhandledListen(
      from peripheral: PeripheralIdentifier,
      on characteristic: CharacteristicIdentifier,
      with value: Data?) -> ListenRestoreAction {
        // Re-install or defer installing a callback to a notifying characteristic.
        return .promiseRestoration
    }
}

Advanced Usage

The following section will demonstrate a few advanced usage of Bluejay.

Write and Assemble

One of the Bluetooth modules we've worked with doesn't always send back the entire data in one packet, even if the data is smaller than either the software's or hardware's maximum packet size. To handle incoming data that can be broken up into an unknown number of packets, we've added the writeAndAssemble function that is very similar to writeAndListen on the SynchronizedPeripheral. Therefore, at least for now, this is currently only supported when using the background task.

When using writeAndAssemble, we still expect you to know the total size of the data you are receiving, but Bluejay will keep listening and receiving packets until the expected size is reached before trying to deserialize the data into the object you need.

You can also specify a timeout in case something hangs or takes abnormally long.

Here is an example writing a request for a value to a Bluetooth module, so that it can return the value we want via a notification on a characteristic. And of course, we're not sure and have no control over how many packets the module will send back.

try peripheral.writeAndAssemble(
    writeTo: Characteristics.rigadoTX,
    value: ReadRequest(handle: Registers.system.firmwareVersion),
    listenTo: Characteristics.rigadoRX,
    expectedLength: FirmwareVersion.length,
    completion: { (firmwareVersion: FirmwareVersion) -> ListenAction in
        settings.firmware = firmwareVersion.string
        return .done
})

Flush Listen

Some Bluetooth modules will pause sending data when it loses connection to your app, then resume sending the same set of data from where it left off when the connection is re-established. This isn't an issue most of the time, except for Bluetooth modules that do overload one characteristic with multiple purposes and values.

For example, you might have to re-authenticate the user when the app is re-opened. But if authentication requires listening to the same characteristic where an incomplete data set from a previous request is still being sent, then you will be getting back unexpected values and most likely crash when trying to deserialize authentication related objects.

To handle this, it is often a good idea to flush a notifiable characteristic before starting a critical operation. This is also only available on the SynchronizedPeripheral when working within the background task

try peripheral.flushListen(to: auth, nonZeroTimeout: .seconds(3), completion: {
    debugPrint("Flushed buffered data on the auth characteristic.")
})

The nonZeroTimeout specifies the duration of the absence of incoming data needed to predict that the flush is most likely completed. In the above example, it is not that the flush will come to a hard stop after 3 seconds, but rather will only stop if Bluejay doesn't have any data to flush after waiting for 3 seconds. It will continue to flush for as long as there is incoming data.

CoreBluetooth Migration

If you want to start Bluejay with a pre-existing CoreBluetooth stack, you can do so by specifying .use in the start mode instead of .new when calling the start function.

bluejay.start(mode: .use(manager: anotherManager, peripheral: alreadyConnectedPeripheral))

You can also transfer Bluejay's CoreBluetooth stack to another Bluetooth library or your own using this function:

public func stopAndExtractBluetoothState() ->
    (manager: CBCentralManager, peripheral: CBPeripheral?)

Finally, you can check whether Bluejay has been started or stopped using the hasStarted property.

Monitor Peripheral Services

Some peripherals can add or remove services while it's being used, and Bluejay provides a basic way to react to this. See BluejayHeartSensorDemo and DittojayHeartSensorDemo in the project for more examples.

bluejay.register(serviceObserver: self)
func didModifyServices(
  from peripheral: PeripheralIdentifier,
  invalidatedServices: [ServiceIdentifier]) {
    if invalidatedServices.contains(where: { invalidatedServiceIdentifier -> Bool in
        invalidatedServiceIdentifier == chirpCharacteristic.service
    }) {
        endListen(to: chirpCharacteristic)
    } else if invalidatedServices.isEmpty {
        listen(to: chirpCharacteristic)
    }
}

Notes from Apple:

If you previously discovered any of the services that have changed, they are provided in the invalidatedServices parameter and can no longer be used. You can use the discoverServices: method to discover any new services that have been added to the peripheral’s database or to find out whether any of the invalidated services that you were using (and want to continue using) have been added back to a different location in the peripheral’s database.

API Documentation

We have more in-depth API documentation for Bluejay using inline documentation and Jazzy.

Comments
  • It's impossible using Bluejay with some other libraries

    It's impossible using Bluejay with some other libraries

    Bluejay does the right thing and incapsulates CoreBluetooth classes. But in my case I have to use DFU library, https://github.com/NordicSemiconductor/IOS-Pods-DFU-Library that requires direct access to CBCentralManager and CBPeripheral. As result I had to get rid of all Bluejay super-features and implement manually a part of work that Bluejay does perfectly.

    Maybe it would be reasonable to expose some CoreBluetooth classes at developers risk? So it would be some-kind of Force Unwrap in Swift?

    opened by larryonoff 17
  • -Fix for crashes when listening on multiple characteristics

    -Fix for crashes when listening on multiple characteristics

    Fixes #:

    This fixes the situation where a didReadCharacteristic, event comes in on a characteristic that is already being "listened" on.

    Summary of Problem:

    I am not sure why this was happening as I am not reading after setting up the listener, but I suspect it may be a situation related to having multiple listeners. Also, it could be that iOS is querying the battery service, of the connected peripheral. In any case, the preconditionFailure causes the app to crash, which is not desirable.

    Proposed Solution:

    If anything perhaps a custom error should be raised within Bluejay, but not a crash.

    Testing Completed and Required:

    Screenshots:

    opened by beachcitiessoftware 14
  • Bluejay can crash in DiscoverService.swift:51, possible missing safety check or queuing bug

    Bluejay can crash in DiscoverService.swift:51, possible missing safety check or queuing bug

    Many users have this problem. This bug is present in every version of ios (from ios 9 to ios 12).

    I still do not understand what cause this crash but it happens when the disconnected(from peripheral: Peripheral) and connected(to peripheral: Peripheral) functions are called.

    This is my log before the crash

    ...
    23 | \| | 09:38:29:183 (UTC) | \| | disconnected(from peripheral: Peripheral) 
    24 | \| | 09:38:36:286 (UTC) | \| | disconnected(from peripheral: Peripheral)
    25 | \| | 09:38:58:320 (UTC) | \| | connected(to peripheral: Peripheral)
    --CRASH--
    

    issue.txt

    opened by FIndustries 14
  • Scan called after disconnect causes crash

    Scan called after disconnect causes crash

    Summary: If startScan is called immediately after disconnect, an exception is thrown which causes a crash. I believe the issue arises because the Scan instance is never assigned a queue because of the isDisconnectionQueued short circuit. See https://github.com/steamclock/bluejay/blob/320dc606b7a8b61b06e5df651c3014e73a8334a7/Bluejay/Bluejay/Queue.swift#L53

    This results in Scan.Fail() called, followed by Scan.stopScan(), then finally Queueable.updateQueue(), which has a guard which checks if Scan.queue is not nil. See https://github.com/steamclock/bluejay/blob/320dc606b7a8b61b06e5df651c3014e73a8334a7/Bluejay/Bluejay/Queueable.swift#L35

    I think the easy fix would be to assign the queue to the Queueable before the short circuit.

    I can submit a pull request for this if you would like.

    However, there is an additional issue that the startScan will fail because a disconnect is queued. It seems like this logic is misplaced as the Queue does not know wether the Queueable should fail when a disconnect is queued.

    Thank you for providing such a useful library around CoreBluetooth. It simplifies many of the interactions for BLE app development.

    opened by jeffbk 11
  • Make Receivable init throw

    Make Receivable init throw

    I like the idea of this library. I haven't tried it yet. But it looks that it really simplify CoreBluetooth API.

    I'm curious why Receivable init(bluetoothData: Data) doesn't throw or can return nil? What to do if there's invalid data from Peripheral?

    opened by larryonoff 11
  • v0.8.0 - Improve background restoration, multiple listens, drop iOS 9, migrate to Swift 4.2, and more...

    v0.8.0 - Improve background restoration, multiple listens, drop iOS 9, migrate to Swift 4.2, and more...

    Fixes:

    • Background and listen restoration callbacks #120, #156, #181
    • Allow trapping and replacing multiple listens, #141
    • Fix queueing bug related to discover service and characteristic, #161
    • Drop support for iOS 9 #88
    • Update to Swift 4.2, #182
    • Fixes bugs in demo, #110, #111
    • Add main thread safety check, #179

    Changes:

    • Removed outdated or soon-to-be-replaced demo projects
    • Redo, clean up, and improve Heart Sensor Demo project
    • Restrict public access to Peripheral
      • The user should never interface with the Peripheral directly
        • All interactions should be completed through the Bluejay instance for consistency and stability
      • The user should not be allowed to hold a strong reference to a Peripheral object
        • The Peripheral objects contain CBPeripheral references, and when Bluejay set them to nil they are required to deinit and be freed from memory in some cases such as after a disconnection to maintain proper states in the CoreBluetooth stack as well
      • Public APIs in Bluejay as well as the PeripheralIdentifier should be sufficient for most usage and design, so Peripheral is really not needed anyway (until more compelling feedback)

    Added:

    • Add XCGLogger with log to file and monitor log file features
      • Showcased in the updated Heart Sensor demo
      • Will help debug future potential state restoration issues that are difficult to investigate
    • Dittojay demo app as a virtual Bluetooth LE heart sensor
      • Can also help test background state restoration

    Summary of Problems:

    • Implementation for background and listen restoration was not as comprehensive and effective
    • It's easy for newcomers to install multiple listens accidentally and crashing
    • Support for iOS 9 is no longer beneficial
    • Not yet updated to Swift 4.2
    • Bugs in demo apps
    • Lack of additional thread safety checks
    • Lack of better logging features
    • Lack of a way to test background state restoration

    Proposed Solution:

    • For specifics, see commit history and discussions in linked issues

    Testing Completed and Required:

    • Our existing projects work as expected
    • Happy flows for Bluejay and Dittojay demo work as expected

    Screenshots:

    • N/A
    opened by sakuraehikaru 9
  • Allow setting CBCentralManager initialization flags

    Allow setting CBCentralManager initialization flags

    If an app does not want to use the bluetooth background mode, the CBCentralManager should not have the flags CBConnectPeripheralOptionNotifyOnDisconnectionKey and CBConnectPeripheralOptionNotifyOnConnectionKey set to true, since the notifications are not providing any value to the user. So I added a setter to be able to change the global variable called standardConnectOptions in the Connection.swift file.

    opened by JustusvonBrandt 9
  • Drop support for iOS 9 when iOS 12 is released

    Drop support for iOS 9 when iOS 12 is released

    I would like to use the library in iOS 9.0 and later. How would you recommend to replace the "Dispatch.dispatchPrecondition(condition:" in order to be compatible with iOS 9.0 ?

    opened by Menesidis 8
  • Crash reading a characteristic:

    Crash reading a characteristic: "Expecting read from \(characteristicIdentifier.description), but actually read from \(readFrom.uuid)"

    Summary:

    We haven't been able to reproduce it, because we're not sure when/how it occurs, but our logs show several crashes on ReadCharacteristic.swift line 57, which says "Expecting read from (characteristicIdentifier.description), but actually read from (readFrom.uuid)".

    Steps to Reproduce:

    Not sure.

    Actual Result:

    Crash.

    Expected Result:

    Doesn't crash?

    Device, Build, OS:

    iPhone 6s, iOS 12.3.1, Bluejay 0.8.5.

    opened by SaintNicholas 7
  • Add support for iOS 15 / Xcode 13

    Add support for iOS 15 / Xcode 13

    Summary:

    When using Xcode 13 to build an iOS 15 app with the Bluejay dependency, there are compiler errors:

    Value of optional type 'CBService?' must be unwrapped to refer to member 'uuid' of wrapped base type 'CBService'
    

    Steps to Reproduce:

    1. Include pod 'Bluejay' as a Cocoapods dependency.
    2. Open workspace in Xcode 13.0.
    3. Try build or run the project.

    Actual Result:

    Screenshot 2021-09-30 at 13 54 15

    Expected Result:

    Bluejay supports the newer CoreBluetooth API which has an optional CBService property on CBCharacteristic and the build succeeds.

        /**
         * @property service
         *
         *  @discussion
         *      A back-pointer to the service this characteristic belongs to.
         *
         */
        weak open var service: CBService? { get }
    

    Device, Build, OS:

    Any simulator or device building against iOS 15.

    opened by LordParsley 6
  • Create Bluejay logo

    Create Bluejay logo

    We should have a logo for Bluejay. Typically we'd bring in a specialist for logo work, but @rachteo here at Steamclock is keen to do some initial experimentation, which is a great way to start. We may come up with something great, or worst case we'd have a starting point for a logo and illustration expert to refine.

    Two potential starting points for a logo would be the Bluetooth logo, with its stylized B that if you squint hard enough could evoke a bird:

    image

    and obviously the image of a blue jay. Obviously we need to be distinctive and clearly different from the various trademarked blue jay logos out there, but they can serve as some inspiration.

    image image image

    Ideally the logo will look professionally executed but have an element of warmth and personality. One open source logo that I like for this aspect that also happens to depict a bird is the Bower logo:

    image

    opened by apike 6
  • Make WriteCharacteristic fail if types mismatch

    Make WriteCharacteristic fail if types mismatch

    Fixes #263:

    Summary of Problem:

    There's no way to know if a characteristic has the specified CBCharacteristicWriteType when calling write because if it doesn't, success is returned instead of fail.

    Proposed Solution:

    In WriteCharacteristic we check that the CBCharacteristic has the passed type as a property, if it doesn't we fail the write.

    Testing Completed and Required:

    Calling write with a mismatched CBCharacteristicWriteType executes the fail callback.

    opened by DantePuglisi 2
  • Write doesn't fail if the wrong type is passed

    Write doesn't fail if the wrong type is passed

    Summary: There's no way to know if a characteristic has the specified CBCharacteristicWriteType when calling write because if it doesn't, success is returned instead of fail.

    Steps to Reproduce:

    1. Call bluejay.write passing a CharacteristicIdentifier and either .withResponse or .withoutResponse as its type (pass the one that doesn't correspond to what that characteristic has as its writeProperty).

    Actual Result: Callback from write is success even if the wrong CBCharacteristicWriteType is passed.

    Expected Result: Callback from write should be fail if the wrong CBCharacteristicWriteType is passed.

    Device, Build, OS: iPhone 13 Pro - iOS 15.4.1

    opened by DantePuglisi 1
  • I have crash in willRestoreState

    I have crash in willRestoreState

    Summary: I used backgroundRestoreMode at bluejay.start mode. But I have a crash in this code when my app start.

    public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
        
        debugLog("Central manager will restore state.")
    
        guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral], let cbPeripheral = peripherals.first else {
            debugLog("No peripherals found during state restoration.")
            endStartupBackgroundTask()
            return
        }
    
        let peripheral = Peripheral(delegate: self, cbPeripheral: cbPeripheral, bluejay: self)
        precondition(peripherals.count == 1, "Invalid number of peripheral to restore.")
        debugLog("Peripheral state to restore: \(cbPeripheral.state.string())")
    

    I have problem this line " precondition(peripherals.count == 1, "Invalid number of peripheral to restore.") " in bluejay.swif code

    Don't other people have this problem?

    opened by bluendev 0
  • Build Failed on XCode13

    Build Failed on XCode13

    Summary: When we build project, i found some errors.CBCharacteristic's service is optional type. In CharacteristicIdentifier file, there are two functions using CBCharacteristic.service but not appending ! or ??. The two functions are 'init' and '==' which in line 22 and 49

    Steps to Reproduce: 1. 1 When we build on XCode 13,We can find it

    Actual Result: When we build on XCode 13,We can find it

    Expected Result: self.service = ServiceIdentifier(uuid: cbCharacteristic.service!.uuid) (lhs.uuid == rhs.uuid) && (lhs.service.uuid == rhs.service!.uuid)

    Device, Build, OS: iphone12, 15.3.1

    opened by csxfno21 0
  • Unable to detect my device

    Unable to detect my device

    Summary: The peripheral I am trying to find with bluetooth scan is not getting listed after addition of few service.

    Steps to Reproduce:

    1. Set Expected services
    2. Start scanning
    3. Wait for returning devices in callback

    Actual Result:

    No Devices Expected Result: Device to be listed in Peripheral found.

    Device, Build, OS: iPhone X, iOS 14

    Log :

    2021-04-26 18:26:06.053600+0800 Kyla Mercury[931:309467] Lost discovery: ScanDiscovery(peripheralIdentifier: Bluejay.PeripheralIdentifier(uuid: 0629A72F-D08B-9C64-DA54-183D0111B8C8, name: "KYLA MECURY"), advertisementPacket: ["kCBAdvDataSolicitedServiceUUIDs": <__NSArrayM 0x2833b7e40>(
    Current Time
    )
    , "kCBAdvDataTimestamp": 641125551.046602, "kCBAdvDataIsConnectable": 1, "kCBAdvDataRxPrimaryPHY": 0, "kCBAdvDataServiceUUIDs": <__NSArrayM 0x2833b7d20>(
    Heart Rate,
    Battery,
    Device Information,
    1814,
    Blood Pressure,
    Health Thermometer,
    Bond Management
    )
    , "kCBAdvDataLocalName": KY, "kCBAdvDataRxSecondaryPHY": 0], rssi: 127)
    
    opened by abhi-aztech 0
Releases(v0.8.9)
  • v0.8.9(Nov 24, 2021)

    Fixes it so it can compile correctly for the latest version of Xcode / iOS SDK.

    Code that directly creates a Bluejay.CharacteristicIdentifier from a CBCharacteristic needs to change to accommodate the fact that initializer is now fallible due to changes in the optionality of CBCharacteristic.service

    Also includes a few smaller changes:

    • Add setting to broadcast errors sent to cancelEverything to all listeners.
    • Fix problem with timeouts in writeAndAssemble not working as expected if partial data is received
    • Fix problem with the queue not restarting after transferring bluetooth state back to Bluejay
    Source code(tar.gz)
    Source code(zip)
  • v0.8.7(May 11, 2020)

  • v0.8.6(Feb 3, 2020)

  • v0.8.5(May 6, 2019)

    Updated Bluejay, demos, and XCGLogger for Swift 5 and Xcode 10.2.1. Only hiccup was that jazzy doc seems broken with Xcode 10.2.1, so I couldn't update the jazzy documentation, but the README on GitHub is fine and updated.

    Source code(tar.gz)
    Source code(zip)
  • v0.8.4(Apr 5, 2019)

    Failing a queueable requires updating its queue, and therefore requires the queueable to have a reference to its queue. The disconnection-is-queued failure block is relatively new, so I didn't catch the new requirement - that is to shuffle the queue assignment to the new starting point of the add call.

    Source code(tar.gz)
    Source code(zip)
  • v0.8.3(Apr 3, 2019)

  • v0.8.2(Mar 21, 2019)

  • v0.8.1(Jan 25, 2019)

  • v0.8.0(Jan 11, 2019)

    Added

    • XCGLogger, and APIs for logging to a file and monitoring log file changes
    • Dittojay demo app as a virtual Bluetooth LE heart rate sensor
      • Also allows testing background state restoration

    Changed

    • Migrate to Swift 4.2
    • Dropped support for iOS 9
    • Removed outdated or soon-to-be-replaced demo projects and documentation
    • Redo, clean up, and improve Bluejay demo app to work with Dittojay demo.
    • Restrict public access to Peripheral

    Fixed

    • Background and listen restoration callbacks
    • Multiple listen crash by allowing trapping or replacing an existing listen
    • Order of queueing when discovering services and characteristics
    • Thread-related crashes by adding main thread safety checks to important Bluejay API calls
    Source code(tar.gz)
    Source code(zip)
  • v0.7.1(Dec 6, 2018)

  • v0.7.0(Dec 6, 2018)

    Version 0.7.0 introduces several API-breaking changes and improvements:

    • Add StartMode to better encapsulate CBCentralManager initialization options
    • Add WarningOptions to better encapsulate connect warning options
    • Fix handling of expected and unexpected disconnections
    • Add disconnect handler to handle auto-reconnect
    • Remove all cancellation callbacks; use existing failure blocks and look for BluejayError.cancelled instead if you need to differentiate between a failure caused by cancellation versus a failure caused by other errors
    • Return Bluejay Peripheral instead of CBPeripheral for connect, disconnect, and RSSI observer callbacks; so that you don't have to import CoreBluetooth in many of your files anymore, and you can also get a "better/more suitable" peripheral object to work with
    • Fix a scan crash caused by not clearing the scan request's global timer
    • Remove set privileges on all public Bluejay instance variables; they are all supposed to be read only from the get go, but a few were settable and that was dangerous
    Source code(tar.gz)
    Source code(zip)
  • v0.6.5(Oct 22, 2018)

  • v0.6.4(Aug 2, 2018)

  • v0.6.3(Jul 26, 2018)

    • Add API to check whether a peripheral is listening to a characteristic
    • Allow disabling auto-reconnect when using cancelEverything
    • Expose auto-reconnect variable
    • Update readme, changelog, and documentation
    Source code(tar.gz)
    Source code(zip)
  • v0.6.0(May 1, 2018)

  • v0.5.1(Apr 9, 2018)

  • v0.5.0(Apr 9, 2018)

    There was a subtle true/false reversal mistake that we didn't catch when bringing in the new error enums. This was causing the second connection request to cancel the first ongoing connection request.

    Source code(tar.gz)
    Source code(zip)
  • v0.4.9(Apr 9, 2018)

    • Prevent indefinite flush
    • Add missing cancellation handling
    • Dedup listen semaphore signals
    • Add missing semaphore signal for end listen
    • Use timeout enum
    Source code(tar.gz)
    Source code(zip)
  • v0.4.8(Mar 16, 2018)

  • v0.4.7(Mar 16, 2018)

  • v0.4.6(Feb 27, 2018)

    Connecting immediately after a disconnect wasn't possible due to a strict and slightly incorrect double connect protection. This has been fixed now.

    Source code(tar.gz)
    Source code(zip)
  • v0.4.5(Feb 27, 2018)

    Apply the same recent fixes made to writeAndListen for writeAndAssemble as well:

    • Better management of semaphore locks and releases
    • Better usage of end listen
    • Allow failing the operation if bluetooth becomes unavailable or if there's a disconnection after both the read and listen have been setup correctly
    Source code(tar.gz)
    Source code(zip)
  • v0.4.4(Feb 9, 2018)

  • v0.4.3(Feb 6, 2018)

  • v0.4.2(Feb 5, 2018)

    • If there is a disconnect while a background task is running, defer the disconnection clean up it to the end of the background task to allow proper tear down of the Bluejay states
    • Fix auto reconnect states to allow proper reconnection when expected
    Source code(tar.gz)
    Source code(zip)
  • v0.4.1(Feb 2, 2018)

    • Improved handling and exiting the locks in write and listen for synchronized peripheral
    • Improved handling of end listen completion for synchronized peripheral
    • Improved handling of state restoration for the connecting and disconnecting states
    • Fix connection timeout not working as expected
    Source code(tar.gz)
    Source code(zip)
  • v0.4.0(Jan 24, 2018)

    We have changed the connection API slightly.

    We should not assume all connection requests need a timeout. There are in fact many cases where an indefinite connection request, the default of CoreBluetooth's connection, is necessary and useful.

    This change now requires you to explicitly specify whether your connection has a timeout or not. This will give you greater control and transparency over how your connection requests will behave.

    The connection API now has an additional timeout parameter, and looks like this now: public func connect(_ peripheralIdentifier: PeripheralIdentifier, timeout: Timeout, completion: @escaping (ConnectionResult) -> Void)

    Source code(tar.gz)
    Source code(zip)
Owner
Steamclock Software
A mobile product studio focused on building quality apps.
Steamclock Software
Bluetooth mesh messaging SDK for apps

Berkanan SDK Berkanan SDK enables Bluetooth mesh messaging between nearby apps. It's the framework used by Berkanan Messenger (Product Hunt, TechCrunc

Zsombor Szabo 189 Jan 1, 2023
An open-source Swift framework for building event-driven, zero-config Multipeer Connectivity apps

PeerKit An open-source Swift framework for building event-driven, zero-config Multipeer Connectivity apps Usage // Automatically detect and attach to

JP Simard 861 Dec 23, 2022
iOS Bluetooth LE framework

Features A futures interface replacing protocol implementations. Timeout for Peripheral connection, Service scan, Service + Characteristic discovery a

Troy Stribling 696 Dec 25, 2022
Build your own 'AirTags' 🏷 today! Framework for tracking personal Bluetooth devices via Apple's massive Find My network.

OpenHaystack is a framework for tracking personal Bluetooth devices via Apple's massive Find My network.

Secure Mobile Networking Lab 5.8k Jan 9, 2023
Blocks Based Bluetooth LE Connectivity framework for iOS/watchOS/tvOS/OSX. Quickly configure centrals & peripherals, perform read/write operations, and respond characteristic updates.

ExtendaBLE Introduction ExtendaBLE provides a very flexible syntax for defining centrals and peripherals with ease. Following a blocks based builder a

Anton 94 Nov 29, 2022
Simple, block-based, lightweight library over CoreBluetooth. Will clean up your Core Bluetooth related code.

LGBluetooth Simple, block-based, lightweight library over CoreBluetooth. Steps to start using Drag and Drop it into your project Import "LGBluetooth.h

null 170 Sep 19, 2022
Bluetooth mapping in Swift

Bluetonium is part of the E-sites iOS Suite. Bluetonium is a Swift Library that makes it easy to communicate with Bluetooth devices. Features ?? Servi

E-sites 165 Nov 20, 2022
The Bluetooth LE library for iOS and Mac. 100% Swift.

iOS-BLE-Library An in-development Bluetooth Low Energy Library by Nordic Semiconductor to interact with the , which is not complicated, but requires w

Nordic Semiconductor 6 Dec 19, 2022
RxBluetoothKit is a Bluetooth library that makes interaction with BLE devices much more pleasant.

RxBluetoothKit is a Bluetooth library that makes interaction with BLE devices much more pleasant. It's backed by RxSwift and CoreBluetooth and it prov

Polidea 1.3k Jan 6, 2023
Omnipod Bluetooth PumpManager For Loop

OmniBLE Omnipod Bluetooth PumpManager For Loop Status This module is at the very beginning stages of development and does not even compile yet. DO NOT

Randall Knutson 20 Apr 21, 2022
Diabetes: test the FreeStyle Libre glucose sensor as a Bluetooth Low Energy device, even directly from an Apple Watch.

Since the FreeStyle Libre 2 / 3 glucose sensors are Bluetooth Low Energy devices, I am trying to leverage their capabilities to implement something ne

Guido Soranzio 6 Jan 2, 2023
The easiest way to use Bluetooth (BLE )in ios,even bady can use.

The easiest way to use Bluetooth (BLE )in ios,even bady can use.

刘彦玮 4.6k Dec 27, 2022
Fluetooth - Flutter library for sending bytes to Bluetooth devices on Android/iOS

A Flutter library for sending bytes to Bluetooth devices. Available on Android a

Iandi Santulus 1 Jan 2, 2022
MiniVendingMachine - SwiftUI demo Apple Watch app to open a mini vending machine via bluetooth

Mini Vending Machine Use Apple Watch to open vending machine cells. Note: This a

CGH 3 Apr 8, 2022
CombineCoreBluetooth is a library that bridges Apple's CoreBluetooth framework and Apple's Combine framework

CombineCoreBluetooth is a library that bridges Apple's CoreBluetooth framework and Apple's Combine framework, making it possible to subscribe to perform bluetooth operations while subscribing to a publisher of the results of those operations, instead of relying on implementing delegates and manually filtering for the results you need.

Starry 74 Dec 29, 2022
A simple framework that brings Apple devices together - like a family

Apple Family A simple framework that brings Apple devices together - like a family. It will automatically use bluetooth, wifi, or USB to connect and c

Kiran 62 Aug 21, 2022
Functional wrapper for Apple's MultipeerConnectivity framework.

A functional wrapper for the MultipeerConnectivity framework. PeerConnectivity is meant to have a lightweight easy to use syntax, be extensible and fl

Reid Chatham 48 Jun 28, 2021
AZPeerToPeerConnectivity is a wrapper on top of Apple iOS Multipeer Connectivity framework. It provides an easier way to create and manage sessions. Easy to integrate

AZPeerToPeerConnection Controller Features Multipeer Connectivity Connection via Bluetooth or Wifi No need write all session, browser, services delega

Afroz Zaheer 66 Dec 19, 2022
📱📲 A wrapper for the MultipeerConnectivity framework for automatic offline data transmission between devices

A wrapper for Apple's MultipeerConnectivity framework for offline data transmission between Apple devices. This framework makes it easy to automatical

Wilson Ding 197 Nov 2, 2022