Bluetooth

Overview

Readings from validated Bluetooth Low Energy devices can be captured and uploaded to Validic with VLDBluetoothPeripheralController. A list of supported devices can be obtained calling the static function supportedPeripherals on VLDBluetoothPeripheral. You must implement the VLDBluetoothPeripheralControllerDelegate protocol to receive callbacks so that the user may be prompted to take the measurement at the right time, display the measurement value, or get notified of any errors that occurred.

Setup

iOS 13 and later

For apps with a deployment target of iOS 13 and later, use NSBluetoothAlwaysUsageDescription instead of NSBluetoothPeripheralUsageDescription. mentioned below.

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Get Bluetoth readings</string>

Prior Versions

If your app has a deployment target earlier than iOS 13, add both NSBluetoothAlwaysUsageDescription and NSBluetoothPeripheralUsageDescription to your app’s Information Property List file. Devices running earlier versions of iOS rely on NSBluetoothPeripheralUsageDescription, while devices running later versions rely on NSBluetoothAlwaysUsageDescription.

Bluetooth Usage Description

If viewing the raw source add the NSBluetoothPeripheralUsageDescription key as displayed below.

<key>NSBluetoothPeripheralUsageDescription</key>
<string>Get Bluetoth readings</string>

If these values are not provided, the app will crash at runtime with an error reported to the console log.

Peripherals

VLDBluetoothPeripheral represents Bluetooth peripheral models that can be discovered or read from by VLDBluetoothPeripheralController

A peripheral object contains various information to be displayed to the user:

  • name - Name of the peripheral comprised of the the manufacturer name and model number.
  • pairingInstructions - Text telling the user how to pair the peripheral, if the peripheral requires pairing.
  • instructions - Text telling the user how to initialize the reading process with the peripheral.
  • readingInstructions - Text telling the user what to do during the reading process.
  • imageURL - URL for an image of the peripheral.

Supported Peripherals

To retrieve a List of supported peripherals call:

guard let supportedPeripherals = VLDBluetoothPeripheral.supportedPeripherals() as? [VLDBluetoothPeripheral] else { return }

for peripheral in supportedPeripherals {
    print("Peripheral: \(peripheral.name())")
}

Other methods are available on VLDBluetoothPeripheral to allow retrieval of a specific peripheral, or to retrieve a list of peripherals of a specific peripheral type. See the VLDBluetoothPeripheral documentation for additional information.

Filtering

To get a subset of devices you can use the built in filter method on Arrays in Swift. Check out the Supported Peripherals list to find the ID of the devices you want to display. For example, if you only want to display the Pyle Health Therometer you would:

guard let supportedPeripherals = VLDBluetoothPeripheral.supportedPeripherals() as? [VLDBluetoothPeripheral]
let devices = peripherals.filter { $0.peripheralID == 1 }

Pairing

Certain peripherals require pairing with the mobile device before a reading can be taken. You can check the requiresPairing property on the VLDBluetoothPeripheral object to know if it must be paired.

To pair a peripheral with the mobile device, call the pairPeripheral: method on VLDBluetoothPeripheralController.

let peripheral: VLDBluetoothPeripheral 
let controller = VLDBluetoothPeripheralController()
controller.delegate = self

controller.pairPeripheral(peripheral)

To know if a peripheral was successfully paired you will need to implement the pairing methods of the VLDBluetoothPeripheralControllerDelegate protocol.

func bluetoothPeripheralController(_ controller: VLDBluetoothPeripheralController!, didPairPeripheral peripheral: VLDBluetoothPeripheral!) {
    // Peripheral paired successfully
}

func bluetoothPeripheralController(_ controller: VLDBluetoothPeripheralController!, didNotPairPeripheral peripheral: VLDBluetoothPeripheral!, error: Error!) {
    // Peripheral did not pair
}

Reading

Once you are ready to read from a peripheral, the process is fairly similar to the pairing process. You should initially show the peripheral’s instructions and then show the reading instructions once bluetoothPeripheralController:isReadyToReadFromPeripheral: is called.

let peripheral: VLDBluetoothPeripheral = // A peripheral from the supportedPeripherals list
let controller = VLDBluetoothPeripheralController()
controller.delegate = self

controller.read(from: peripheral)

You must also implement the reading methods of the VLDBluetoothPeripheralControllerDelegate protocol.

func bluetoothPeripheralController(_ controller: VLDBluetoothPeripheralController!, isReadyToReadFrom peripheral: VLDBluetoothPeripheral!) {
    // Time to present the readingInstructions to the user
}

func bluetoothPeripheralController(_ controller: VLDBluetoothPeripheralController!, shouldSubmitReadings records: [VLDRecord]!, from peripheral: VLDBluetoothPeripheral!) -> Bool {
    // To have the reading automatically added to the session and uploaded
    // return YES from this method. To first have the reading validated by the user
    // you can return NO from this method. Once the user has validated the reading
    // you must manually submit the record by calling VLDSession.sharedInstance().submitRecord(record)
    return true
}

func bluetoothPeripheralController(_ controller: VLDBluetoothPeripheralController!, didCancelReadingFor peripheral: VLDBluetoothPeripheral!) {
    // Reading was cancelled
}

func bluetoothPeripheralController(_ controller: VLDBluetoothPeripheralController!, readingFailedFor peripheral: VLDBluetoothPeripheral!, error: Error!) {
    // Reading failed
}

Passive Bluetooth

Overview

The ValidicMobile library supports passively reading from a given set of Bluetooth peripherals. Passive Bluetooth reading is a long lived capability. Once enabled, the library will listen for and read from the specified peripherals. Reading will automatically occur once the peripheral is discovered regardless of app state. This includes if the application is currently in the foreground, in the background, or has been terminated due to memory pressure. Apps using this feature must have been started by the user at least once since the iOS device has booted otherwise the feature will not be active even if enabled.

Setup

Passive Bluetooth reading relies upon iOS Bluetooth state restoration and background processing support. Apps using passive reading need to set Bluetooth as a required background mode in the Info.plist. In Xcode, in the Info.plist add a new key, Required background modes, add an item with value App communicates using CoreBluetooth.

Background Bluetooth

If editing the XML source file, add Bluetooth-central background mode as follows:

    <key>UIBackgroundModes</key>
    <array>
        <string>Bluetooth-central</string>
    </array>

Passive Readings

VLDBluetoothPassiveManager is a singleton which coordinates the passive reading process. Passive reading is enabled by passing it an array of peripheral IDs:

VLDBluetoothPassiveManager.sharedInstance().peripheralIDs = [1, 2]

Or by passing an array of peripherals:

if let peripheral = VLDBluetoothPeripheral(forID: 1) {
    VLDBluetoothPassiveManager.sharedInstance().setPeripherals([peripheral])
}

To disable passive reading, set the peripheralIDs property to an empty array or nil.

VLDBluetoothPassiveManager.sharedInstance().peripheralIDs = nil

To support state restoration, a method on the passive manager must be called on app launch. This should be done in the application:didFinishLaunchingWithOptions: method of the AppDelegate as follows:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
        VLDBluetoothPassiveManager.sharedInstance().restoreState()
        return true
    }

State restoration will automatically start the app in the background when a requested peripheral is discovered. Within application:didFinishLaunchingWithOptions:, apps can determine if they were launched in background and avoid expensive operations.

Passive Readings Notifications

Passive readings are automatically sent to the Validic server. Session notifications are sent on upload success or failure. Passive Bluetooth specific notifications are sent when a device is discovered and becomes ready to read. Notifications are also sent for reading success and failure. These notifications specify the associated peripheral ID in the userInfo dictionary with a key, peripheralID.

  • kVLDBluetoothPassiveDidReadNotification is sent when records are read. The notification object is an array of VLDRecord read from the peripheral.
  • kVLDBluetoothPassiveDidFailNotification is sent when failing to read from a discovered peripheral.
  • kVLDBluetoothPassiveIsReadyToReadNotification is sent when the peripheral becomes ready to read. This notification is only sent out for devices that are discoverable before a measurement is taken. It is important to handle this notification in an app and provide feedback to the user that the device has been discovered and is ready to take a measurement. Since Passive Bluetooth reading is usually performed in the background, a standard UI is not available. A local notification can be sent with sound to provide feedback to the user that the app is ready to read from the peripheral. If the user doesn’t hear the sound, it indicates that the device has not been discovered.

The following code fragment registers an observer for the kVLDBluetoothPassiveIsReadyToReadNotification notification and sends a local notification including the name of the peripheral. A similar process could be done for the kVLDBluetoothPassiveDidReadNotification to confirm to the user that a reading was successfully taken.

NotificationCenter.default.addObserver(forName: NSNotification.Name.vldBluetoothPassiveIsReadyToRead, object: nil, queue: nil) { (notification) in
    if UIApplication.shared.applicationState == .background {
        if let peripheralID = notification.userInfo?["peripheralID"] as? UInt,
            let peripheralName = VLDBluetoothPeripheral.init(forID: peripheralID)?.name()
        {
            let localNotification = UILocalNotification()
            localNotification.alertBody = "Ready to read \(peripheralName)"
            UIApplication.shared.presentLocalNotificationNow(localNotification)
        }
    }
}

Considerations

There are situations where passive Bluetooth reading is stopped and requires the app to be relaunched. These situations include:

  • When the user explicitly kills the app by swiping up on the app switcher view, the app will not be restarted by iOS when a peripheral is discovered.
  • If the iOS device is rebooted, state restoration is no longer in effect and iOS will not automatically restart the app when a peripheral is discovered. The app needs to be run at least once after reboot. When the app is launched the passive manager remembers what peripherals if any where being passively read and restarts the capability.

Passive Bluetooth reading has some additional considerations due to the behavior of the iOS Bluetooth stack and background support.

  • Interactive, ie non-passive, Bluetooth operations will suspend passive Bluetooth processing until the interative operation completes.
  • When scanning for peripherals in the background, iOS reduces the frequency and duration of time the phone listens for devices. This may cause some peripherals to not be discovered in the background. This varies by phone model and peripheral. Generally older phone models are more likely to miss discovery of a peripheral.
    • Nonin pulse oximeters, ChoiceMMed pulse oximeter and scale are not usually detectable when in the background.
    • The Pyle thermometer is sometimes not discovered on old phone when the app is in the background.
  • iOS has additional heuristics to determine scanning frequency which may not be documented. If multiple apps on the phone are performing background scanning, scanning may become more infrequent.
  • To prevent repeated attempted reads from a device for one actual reading, the passive manager waits after a successful read until the device stops broadcasting, then waits an additional few seconds before acknowledging the device again. If attempting multiple readings in quick succession, the second reading may not be read. Instead the user should wait until the device powers off and then wait an additional 15 seconds or longer before attempting another reading.