H6, H7, H10 and OH1 Heart rate sensors

From Polar Developers
Jump to: navigation, search

General

On this page you will find the operational flow diagrams for the H6, H7, H10 and OH1 Heart rate sensors.
You can also find links to the used Bluetooth specifications and example codes.

Services in use

Bluetooth GATT services are listed here https://www.bluetooth.com/specifications/gatt/services

Services needed for these Heart Rate sensor examples:
Heart rate: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml
Device information: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.device_information.xml
Battery: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.battery_service.xml

Flow diagrams

BLE HR state machine: Heart Rate Sensor and Bluetooth Low energy operate according to the following state chart. When the plastic electrode areas on the reverse side of the strap detect skin contact, Bluetooth Low Energy starts Peripheral role (as a Sensor) and advertises to be connectable with a Collector (phone or wrist unit). When the Collector sends a Connection Request after a detected advertising message, the Sensor accepts it and a short pairing phase will start if needed. After pairing is finished, the Collector enables heart rate notifications from the Sensor by writing value 0x01 to the Client Characteristic Configuration descriptor. The Sensor will measure heart rate once in a second and sends it as a Heart Rate Measurement Notification. Polar HR Sensors all support RR Intervals. Notice that the unit for RR interval is 1/1024 seconds. After the strap is removed, absence of skin contact is possible to detect from the Sensor Contact Status bits of Flags at Heart Rate Measurement Notification. The Sensor will start termination of BLE connection if contact is absent for 20-30 seconds. When the Sensor is in standby mode its current consumption is very low (< 1 uA).

BLE HR state machine.jpg


BLE Advertising: Bluetooth Low energy advertising starts with a fast interval for better discovery time. This phase lasts 30 seconds. Polar sensors all advertise with a data packet which includes the name of the Sensor preceded by "Polar " and the sensor model name. The Service UUID value indicates which sensor profile the sensor in question supports. Polar has implemented these features in accordance with the Hear Rate Profile specification.

ADVERTISING state machine.jpg


BLE Connected: This state diagram describes the logic of how the Sensor checks connection parameters after 30 sec. to ensure low energy state has been or will be entered.

CONNECTED state machine.jpg




Back to top

HR example code for iOS

// HR example code

#define HEART_RATE_SERVICE @"180D"
#define HEART_RATE_MEASUREMENT @"2A37"

struct hrflags
{
	uint8_t _hr_format_bit:1;
	uint8_t _sensor_contact_bit:2;
	uint8_t _energy_expended_bit:1;
	uint8_t _rr_interval_bit:1;
	uint8_t _reserved:3;
};

- (void) centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral
{
	[aPeripheral setDelegate:self];
	// NOTE you might only discover HR service, but on this example we discover all services 
        [aPeripheral discoverServices:nil];
}

- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error
{
	if(!error)
	{
		for (CBService *aService in aPeripheral.services)
		{
			if( [aService.UUID isEqual:[CBUUID UUIDWithString:HEART_RATE_SERVICE]] )
			{
				[aPeripheral discoverCharacteristics:nil forService:aService];
			}
		}
	}
}

- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
	if(!error)
	{
		if( [aService.UUID isEqual:[CBUUID UUIDWithString:HEART_RATE_SERVICE]] )
		{
			for (CBCharacteristic *aChar in service.characteristics)
                }
                if( [aChar.UUID isEqual:[CBUUID UUIDWithString:HEART_RATE_MEASUREMENT]] )
                {
                    [aPeripheral setNotifyValue:YES forCharacteristic:aChar];
                }
	}
}

- (void) peripheral:(CBPeripheral *)aPeripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
	if(!error)
	{
		if( [characteristic.UUID isEqual:[CBUUID UUIDWithString:HEART_RATE_MEASUREMENT]] )
		{
			const uint8_t *report_data( (uint8_t*)[characteristic.value bytes] );
			int data_size( [characteristic.value length] );
			hrflags flags={0};
			memcpy(&flags,report_data,sizeof(flags));

			int hr_value(0);
			int offset(sizeof(flags));

			// get hr
			memcpy(&hr_value,report_data+offset,flags._hr_format_bit+1);
			offset += flags._hr_format_bit+1;
			
			// get energy if present
			if ( flags._energy_expended_bit )
			{
				uint16_t energy_expended(0);
				memcpy(&energy_expended,report_data+offset,sizeof(energy_expended));
				offset += 2; 
			}        

			// get rr's if present
			if( flags._rr_interval_bit )
			{
				while( offset < data_size )
				{
					uint16_t rr_value(0);
					memcpy(&rr_value,report_data+offset,sizeof(rr_value));
					rr_value=((double)rr_value/1024.0)*1000.0;
                                        offset += 2;
				}
			}
		}
       }
}

// SWIFT
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber){
        if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String{
            if (localName.starts(with: "Polar ")) {
                let items=localName.characters.split(separator: " ").map(String.init)
                if items.count > 2 {
                    let polarDeviceId = items.last
                    if( polarDeviceId == "12345678" && peripheral.state == .disconnected ){
                        central.stopScan()
                        central.connect(peripheral, options: nil)
                    }
                }
            }
        }
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral){
        peripheral.delegate = self
        peripheral.discoverServices(nil)
    }

    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?){
        self.central.scanForPeripherals(withServices: [CBUUID.init(string: "180D")], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]);
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?){
        self.central.scanForPeripherals(withServices: [CBUUID.init(string: "180D")], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]);
    }
    
    let HR_SERVICE     = CBUUID(string: "180D");
    let HR_MEASUREMENT = CBUUID(string: "2a37");
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?){
        if error == nil {
            for service in peripheral.services! {
                if service.uuid.isEqual(HR_SERVICE){
                    peripheral.discoverCharacteristics(nil, for: service);
                }
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?){
        BleLogger.trace_if_error("didDiscoverCharacteristicsForService: ", error: error as NSError!)
        if error == nil {
            for chr in service.characteristics! {
                if chr.uuid.isEqual(HR_MEASUREMENT){
                    peripheral.setNotifyValue(true, for: chr)
                }
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?){
        if error == nil {
            let data = characteristic.value
            var offset=0;
            let hrFormat = data![0] & 0x01;
            let sensorContactBits = Int((data![0] & 0x06) >> 1)
            let energyExpended = (data![0] & 0x08) >> 3;
            let rrPresent = (data![0] & 0x10) >> 4;
            var sensorContact = sensorContactBits == 3;
            var contactSupported = true
            if sensorContactBits == 0 {
                contactSupported = false
                sensorContact = true
            }
            let hrValue = hrFormat == 1 ? (Int(data![1]) + (Int(data![2]) << 8)) : Int(data![1]);
            if contactSupported == false && hrValue == 0 {
                //
                sensorContact = false
            }
            offset = Int(hrFormat) + 2;
            var energy = 0
            if (energyExpended == 1) {
                energy = Int(data![offset]) + (Int(data![offset + 1]) << 8);
                offset += 2;
            }
            var rrs = [Int]()
            if( rrPresent == 1 ){
                let len = data!.count
                while (offset < len) {
                    let rrValueRaw = Int(data![offset]) | (Int(data![offset + 1]) << 8)
                    let rrValue = Int((Double(rrValueRaw) / 1024.0) * 1000.0);
                    offset += 2;
                    rrs.append(rrValue);
                }
            }
        }
    }



Back to top

HR example code for Android

// HR example code

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.os.Build;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class YourBtClass {

    // HR example code
    public enum AD_TYPE
    {
        GAP_ADTYPE_UNKNOWN(0),
        GAP_ADTYPE_FLAGS(1)                         ,
        GAP_ADTYPE_16BIT_MORE(2)                    , //!< Service: More 16-bit UUIDs available
        GAP_ADTYPE_16BIT_COMPLETE(3)                , //!< Service: Complete list of 16-bit UUIDs
        GAP_ADTYPE_32BIT_MORE(4)                    , //!< Service: More 32-bit UUIDs available
        GAP_ADTYPE_32BIT_COMPLETE(5)                , //!< Service: Complete list of 32-bit UUIDs
        GAP_ADTYPE_128BIT_MORE(6)                   , //!< Service: More 128-bit UUIDs available
        GAP_ADTYPE_128BIT_COMPLETE(7)               , //!< Service: Complete list of 128-bit UUIDs
        GAP_ADTYPE_LOCAL_NAME_SHORT(8)              , //!< Shortened local name
        GAP_ADTYPE_LOCAL_NAME_COMPLETE(9)           , //!< Complete local name
        GAP_ADTYPE_POWER_LEVEL(10)                  , //!< TX Power Level: 0xXX: -127 to +127 dBm
        GAP_ADTYPE_OOB_CLASS_OF_DEVICE(11)          , //!< Simple Pairing OOB Tag: Class of device (3 octets)
        GAP_ADTYPE_OOB_SIMPLE_PAIRING_HASHC(12)     , //!< Simple Pairing OOB Tag: Simple Pairing Hash C (16 octets)
        GAP_ADTYPE_OOB_SIMPLE_PAIRING_RANDR(13)     , //!< Simple Pairing OOB Tag: Simple Pairing Randomizer R (16 octets)
        GAP_ADTYPE_SM_TK(14)                        , //!< Security Manager TK Value
        GAP_ADTYPE_SM_OOB_FLAG(15)                  , //!< Secutiry Manager OOB Flags
        GAP_ADTYPE_SLAVE_CONN_INTERVAL_RANGE(16)    , //!< Min and Max values of the connection interval (2 octets Min, 2 octets Max) (0xFFFF indicates no conn interval min or max)
        GAP_ADTYPE_SIGNED_DATA(17)                  , //!< Signed Data field
        GAP_ADTYPE_SERVICES_LIST_16BIT(18)          , //!< Service Solicitation: list of 16-bit Service UUIDs
        GAP_ADTYPE_SERVICES_LIST_128BIT(19)         , //!< Service Solicitation: list of 128-bit Service UUIDs
        GAP_ADTYPE_SERVICE_DATA(20)                 , //!< Service Data
        GAP_ADTYPE_MANUFACTURER_SPECIFIC(0xFF);       //!< Manufacturer Specific Data: first 2 octets contain the Company Identifier Code followed by the additional manufacturer specific data

        private int numVal;

        AD_TYPE(int numVal) {
            this.numVal = numVal;
        }

        public int getNumVal() {
            return numVal;
        }

    };

    public static AD_TYPE getCode(byte type){
        try {
            return type == -1 ? AD_TYPE.GAP_ADTYPE_MANUFACTURER_SPECIFIC : AD_TYPE.values()[type];
        }catch (ArrayIndexOutOfBoundsException ex){
            return AD_TYPE.GAP_ADTYPE_UNKNOWN;
        }
    }

    public static HashMap<AD_TYPE,byte[]> advertisementBytes2Map(byte[] record){
        int offset=0;
        HashMap<AD_TYPE,byte[]> adTypeHashMap = new HashMap<>();
        try {
            while ((offset + 2) < record.length) {
                AD_TYPE type = getCode(record[offset + 1]);
                int fieldLen = record[offset];
                if (fieldLen <= 0) {
                    // skip if incorrect adv is detected
                    break;
                }
                if (adTypeHashMap.containsKey(type) && type == AD_TYPE.GAP_ADTYPE_MANUFACTURER_SPECIFIC) {
                    byte data[] = new byte[adTypeHashMap.get(type).length + fieldLen - 1];
                    System.arraycopy(record, offset + 2, data, 0, fieldLen - 1);
                    System.arraycopy(adTypeHashMap.get(type), 0, data, fieldLen - 1, adTypeHashMap.get(type).length);
                    adTypeHashMap.put(type, data);
                } else {
                    byte data[] = new byte[fieldLen - 1];
                    System.arraycopy(record, offset + 2, data, 0, fieldLen - 1);
                    adTypeHashMap.put(type, data);
                }
                offset += fieldLen + 1;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            // corrupted adv data find
        }
        return adTypeHashMap;
    }


    private BluetoothAdapter bluetoothAdapter;
    private BluetoothManager btManager;
    private Context context;

    public final UUID HR_MEASUREMENT = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb");
    public final UUID HR_SERVICE = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb");
    public static final UUID DESCRIPTOR_CCC = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    private ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            super.onScanResult(callbackType, result);
            processDeviceDiscovered(result.getDevice(),result.getRssi(),result.getScanRecord().getBytes());
        }

        @Override
        public void onScanFailed(int errorCode) {
            super.onScanFailed(errorCode);
        }
    };

    private  BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            processDeviceDiscovered(device,rssi,scanRecord);
        }
    };

    private void processDeviceDiscovered(final BluetoothDevice device, int rssi, byte[] scanRecord){
        Map<AD_TYPE,byte[]> content = advertisementBytes2Map(scanRecord);
        if( content.containsKey(AD_TYPE.GAP_ADTYPE_LOCAL_NAME_COMPLETE) ) {
            String name = new String(content.get(AD_TYPE.GAP_ADTYPE_LOCAL_NAME_COMPLETE));
            if (name.startsWith("Polar ")) {
                String names[] = name.split(" ");
                if (names.length > 2) {
                    String deviceId = names[names.length-1];
                    if( deviceId.equals("12345678") ){ // TODO NOTE REPLACE with your device id
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            bluetoothAdapter.getBluetoothLeScanner().stopScan(scanCallback);
                        } else {
                            bluetoothAdapter.stopLeScan(leScanCallback);
                        }
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                            device.connectGatt(context,false,bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE);
                        } else {
                            device.connectGatt(context,false,bluetoothGattCallback);
                        }
                    }
                }
            }
        }
    }

    private BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            if (newState == BluetoothGatt.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
                gatt.discoverServices();
            } else if(newState == BluetoothGatt.STATE_DISCONNECTED){
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    bluetoothAdapter.getBluetoothLeScanner().startScan(scanCallback);
                } else {
                    bluetoothAdapter.startLeScan(leScanCallback);
                }
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            for (BluetoothGattService gattService : gatt.getServices()) {
                if( gattService.getUuid().equals(HR_SERVICE) ){
                    for (BluetoothGattCharacteristic characteristic : gattService.getCharacteristics()) {
                        if( characteristic.getUuid().equals(HR_MEASUREMENT) ){
                            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(DESCRIPTOR_CCC);
                            gatt.setCharacteristicNotification(characteristic, true);
                            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                            gatt.writeDescriptor(descriptor);
                        }
                    }
                }
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicRead(gatt, characteristic, status);
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            if (characteristic.getUuid().equals(HR_MEASUREMENT)) {
                byte[] data = characteristic.getValue();
                int hrFormat = data[0] & 0x01;
                boolean sensorContact = true;
                final boolean contactSupported = !((data[0] & 0x06) == 0);
                if( contactSupported ) {
                    sensorContact = ((data[0] & 0x06) >> 1) == 3;
                }
                int energyExpended = (data[0] & 0x08) >> 3;
                int rrPresent = (data[0] & 0x10) >> 4;
                final int hrValue = (hrFormat == 1 ? data[1] + (data[2] << 8) : data[1]) & (hrFormat == 1 ? 0x0000FFFF : 0x000000FF);
                if( !contactSupported && hrValue == 0 ){
                    // note does this apply to all sensors, also 3rd party
                    sensorContact = false;
                }
                final boolean sensorContactFinal = sensorContact;
                int offset = hrFormat + 2;
                int energy = 0;
                if (energyExpended == 1) {
                    energy = (data[offset] & 0xFF) + ((data[offset + 1] & 0xFF) << 8);
                    offset += 2;
                }
                final ArrayList<Integer> rrs = new ArrayList<>();
                if (rrPresent == 1) {
                    int len = data.length;
                    while (offset < len) {
                        int rrValue = (int) ((data[offset] & 0xFF) + ((data[offset + 1] & 0xFF) << 8));
                        offset += 2;
                        rrs.add(rrValue);
                    }
                }
            }
        }

        @Override
        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorRead(gatt, descriptor, status);
        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
        }

        @Override
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
            super.onMtuChanged(gatt, mtu, status);
        }
    };
}



HR example code for Windows 8

Microsoft has C# example for Windows 8 implementation at Microsoft Windows Developer Network site Please check that your PC hardware supports Bluetooth 4.0 or later with LE e.g. by using Device Manager to find Bluetooth and Microsoft Bluetooth LE Enumerator. If you find it then you have compatible OS and HW with correct drivers.

Bluetooth LE API for Windows 10

Microsoft GitHub contains Windows-universal-samples repo with Bluetooth Low Energy sample how to use the Windows Bluetooth LE APIs for Windows 10. This example shows how to scan LE devices, discover servers, connect to heart rate sensor and how to start heart rate notifications. Note: The Windows universal samples require Visual Studio 2017 to build and Windows 10 to execute.

DISCLAIMER: Polar does not provide support for source code examples delivered by Microsoft.


Other sensors



Back to top