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

Add NSBluetoothAlwaysUsageDescription to your app’s Information Property List file.

Bluetooth Usage Description

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

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Get Bluetooth 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 ValidicBluetooth 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

Agamatrix

The Agamatrix Jazz Wireless 2 glucose meter, supported via a custom Bluetooth integration (peripheralID = 32), requires an authentication key, which is provided by Agamatrix, in order to communicate with the meter. To configure your Agamatrix authentication key for use with the Agamatrix meter in the Validic Mobile SDK:

  1. Create a file named agamatrix-authkey.json
  2. File contents should be your Agamatrix key without the hex prefix notation 0x. For example, if your key is 0x011234567890987654321 then your file contains {"key": "011234567890987654321"}.
  3. Add the agamatrix-authkey.json file to the root of your mobile project.

CVS Health

The CVS Health Advanced Bluetooth Glucose Meter, supported via a custom Bluetooth integration (peripheralID = 33), requires an authentication key, which is provided by Agamatrix, in order to communicate with the meter. To configure your Agamatrix authentication key for use with the CVS Health meter in the Validic Mobile SDK:

  1. Create a file named cvshealth-authkey.json
  2. File contents should be your Agamatrix key without the hex prefix notation 0x. For example, if your key is 0x055234567890987654987 then your file contains {"key": "055234567890987654987"}.
  3. Add the cvshealth-authkey.json file to the root of your mobile project.

The CVS Health Digital Glass Body Analysis Scale is supported in an alpha state in the Validic Mobile SDKs. Body weight is the only metric that can be retrieved from the scale and a new user profile will be created on the scale during pairing. Users cannot pair to an existing user profile and historical readings (readings captured prior to pairing) will not be synced.

Roche CoaguChek INR Meters

❗️ Approval Required

Interacting with Roche CoaguChek devices requires pre-approval from Roche. Please reach out to Validic Support at support@validic.com if you are interested in integrating with Roche CoaguChek devices.

Overview

Interaction with Roche CoaguChek INR meters, supported via a custom Bluetooth integration (peripheralID = 51), requires a Validic License file, provided by Validic Support. The license file (named validic.license) must be included in the root of the main app bundle; to do so, add it to the Xcode project and make sure validic.license is listed in the “Copy Bundle Resources” section of the app target’s “Build Phases”.

Roche CoaguChek INR meters have an uncommon reading workflow, which requires that the end user provide their meter’s unique encryption key. That encryption key is required to decrypt the reading values captured from the user’s meter. Without a valid encryption key, foreground and passive reads with the INR meter will fail. Because this workflow is specific to the Roche INR meter, any Validic clients implementing the INR meter Bluetooth integration must build a user experience (modal or screen) to capture the meter’s encryption key from the user. This key must then be passed by the client to the Validic SDK where it will be stored and used to decrypt readings. Note: the key is a long string of hexadecimal characters, so it is recommended to implement a QR code reading to capture the code displayed on the meter.

One way to do this would be to request it during the pairing process:

  1. Initiate pairing with peripheral ID 51
  2. Upon successful pair, the VLDBluetoothPeripheralDelegate callback (bluetoothPeripheralController:didPairPeripheral:metadata:) will be fired. Store the metadata object’s peripheralUUID string to a variable - it will be used to store the encryption key into the Validic SDK.
  3. Prompt the user for the encryption key, found in the device’s menu at “Other” > “About” (possibly by using a QR code reader).
  4. Store the encryption key into the SDK. To do this, pass the user’s encryption key, the constant kVLDBluetoothPeripheralDataKeyEncryptionKey, and the peripheral’s UUID (from metadata.peripheralUUID in the pairing success delegate callback) to the CoaguChek VLDPeripheral‘s storeData:forKey:ofPeripheralUUID: method.

Example code:

// This example requests the encryption key from inside the pairing success callback for simplicity. Note that the only requirement is that the `metadata.peripheralUUID` value is captured from this callback so it can be used in the `storeData:forKey:ofPeripheralUUID:` call.
// This example uses a UIAlertController text field to collect the encryption key from the user. It is recommended to use a QR code reader to reduce the likelihood of errors.
- (void)bluetoothPeripheralController:(VLDBluetoothPeripheralController *)controller didPairPeripheral:(VLDBluetoothPeripheral *)peripheral metadata:(VLDBluetoothOperationMetadata *)metadata {
    if (peripheral.peripheralID == 51) {
        // Peripheral is CoaguChek, ask for encryption key
        UIAlertController *coaguChekEncryptionKeyAlertController = [UIAlertController alertControllerWithTitle:@"Enter Encryption Key" message:@"Please enter the CoaguChek's encryption key." preferredStyle:UIAlertControllerStyleAlert];
        [coaguChekEncryptionKeyAlertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
            textField.placeholder = @"Encryption Key";
        }];
        [coaguChekEncryptionKeyAlertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            NSString *encryptionKey = [[coaguChekEncryptionKeyAlertController textFields][0] text];
            // Note: it is recommended to perform validation on the length and contents of the provided encryption key (if not scanned via QR code) to reduce the possibility of uncaught input error. The entered encryption key should be a 32-character hexadecimal string.

            // Store the encryption key into the Validic SDK
            [peripheral storeData:encryptionKey forKey:kVLDBluetoothPeripheralDataKeyEncryptionKey ofPeripheralUUID:metadata.peripheralUUID];
        }]];
        [coaguChekEncryptionKeyAlertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            NSLog(@"Encryption key entry canceled.");
        }]];
        [self presentViewController:coaguChekEncryptionKeyAlertController animated:YES completion:nil];
    }
}

Passive Read

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.