Bluetooth Low Energy (BLE)
The cosinuss° sensors comply to the official BLE specification as published by the Bluetooth SIG (www.bluetooth.com).
Advertising
BLE sensors that advertise, also called the server, (e.g. cosinuss° One, cosinuss° Two, …) are detected through a procedure based on broadcasting advertising packets on specified advertising channels (2.402 GHz, 2.426 GHz, 2.480 GHz). Different information, like device name or the mac address are included in the advertising packets. The advertising device sends a packet on at least one of these three channels, with a repetition period called the advertising interval. The scanner, also called the client, is a receiving device (e.g. Gateway, Lab App) that listens to each channel for a duration called the scan window. After finishing the scan on one channel, it listens to the next advertising channel. This is periodically repeated with an interval called scan interval. After the discovery of the sensor with the correct mac address the receiver can connect to the BLE sensor.
Services & Characteristics
All Bluetooth Low Energy devices use the Generic Attribute Profile (GATT) specified by Bluetooth SIG:
GATT has the following terminology:
- client: A device that initiates GATT commands and requests, and accepts responses, for example, a computer or smartphone.
- server: A device that receives GATT commands and requests, and returns responses, for example, a temperature sensor (cosinuss° One, °Two, …).
- characteristic: A data value transferred between client and server, for example, the current battery voltage.
- service: A collection of related characteristics, which operate together to perform a particular function. For instance, the Health Thermometer service includes characteristics for a temperature measurement value and a time interval between measurements.
Some service and characteristic values are used for administrative purposes – for instance, the model name and serial number can be read as standard characteristics within the Generic Access service.
Services, characteristics are collectively referred to as attributes, and identified by UUIDs (128 bit). The Bluetooth SIG has reserved a range of UUIDs (of the form xxxxxxxx-0000-1000-8000-00805F9B34FB) for standard services/characteristics. For efficiency, these identifiers are represented as 16-bit or 32-bit values in the protocol, rather than the 128 bits required for a full UUID. For example, the Device Information service has the short 16-bit code 0x180A, rather than 0000180A-0000-1000-8000-00805F9B34FB. Note that the short UUIDs are only allowed for the services/characteristics of the SIG but not for custom ones. The full list of UUIDs is kept in the Bluetooth Assigned Numbers document online:
GATT operations
The GATT protocol provides a number of commands for the client to discover information about the server. These include:
- Discover UUIDs for all primary services
- Find a service with a given UUID
- Discover all characteristics for a given service
- Find characteristics matching a given UUID
Commands are also provided to read (data transfer from server to client) and write (from client to server) the values of characteristics:
- A value may be read either by specifying the characteristic's UUID, or by a handle value (which is returned by the information discovery commands above).
- Write operations always identify the characteristic by handle, but have a choice of whether or not a response from the server is required.
- 'Long read' and 'Long write' operations can be used when the length of the characteristic's data exceeds the Maximum Transmission Unit (MTU) of the radio link.
- Finally, GATT offers notifications and indications. The client may request a notification for a particular characteristic from the server. The server can then send the value/values to the client whenever it becomes available. For instance, a temperature sensor server may notify its client every time it takes a measurement. This avoids the need for the client to poll the server, which would require the server's radio circuitry to be constantly operational.
- An indication is similar to a notification, except that it requires a response from the client, as confirmation that it has received the message.
How to enable and disable notifications and indications
Note Most Librarys for working with BLE have methods for subscribing to characteristics (and by this enabling notifications/indications), so you won't have to write the CCCD manually.
In order to start and stop notifications and indications of a certain characteristic you have to write the two value bits of the Client Characteristic Configuration Descriptor (CCCD) that belongs to this characteristic. E.g. Battery Level characteristic (UUID 0x2A19) has notifications whereas the Temperature Measurement Characteristic (UUID 0x2A1C) has indications. The CCCDs have the UUID 0x2902. Their first bit represents the notifications (0=off, 1=on) and the second bit the indications (0=off, 1=on). If the appropriate bits are set, the server will send (heart rate, temperature, …) values to the client whenever it has new values.
Start Notifications/Indications
- Enable notifications/indications on the characteristic you are interested in on your local client. There is probably a method like this, e.g. for Android developers. Or you may have to define a callback function, that deals with the incoming data. (May be handled differently in different Bluetooth libraries and plattforms)
- Write the hex command on the CCCD of the characteristic you are interested in to your server (e.g. on Android):
- 0x0100 to activate notifications
- 0x0200 to activate indications
- 0x0300 to activate both
Stop Notifications/Indications
- Write the hex command 0x0000 to the CCCD of the server
- Disable the notifications/indications on your client
BLE standard services and characteristics (used by cosinuss°)
“Base UUID” for standard services/characteristics from the Bluetooth SIG: XXXXXXXX-0000-1000-8000-00805F9B34FB
The only exception is the custom service with a base UUID format: 0000-XXXX-1212-EFDE-1523-785F-EABC-D123
In the following only the short 16 bit version of the UUID is shown:
Service: Heart Rate
UUID: 0x180D see also org.bluetooth.service.heart_rate
The cosinuss° sensors use this service to provide the heart rate (bpm) and the rr-intervals (ms). The data is sent with 1 Hz by notification.
Characteristics:
- Heart Rate Measurement (UUID:0x2A37) notify. Descriptor: Client Characteristic Configuration (UUID:0x2902) (heart rate and rr-intervals)
- Body Sensor Location (UUID:0x2A38) read (not used at cosinuss°)
Bluetooth Specifications:
Example:
# Heart Rate Measurement and rr-interval Conversion - Python Example # Incoming byte input (hex) from the Bluetooth device: # 0x 10 44 33 03 29 03 byte_data = bytearray(b'\x10\x44\x33\x03\x29\x03') # These are the components of the input bytes: # byte 0 0x10: Flag byte where each bit has a meaning (LSB first); # 0x10 = 00010000 as bits # [0] heart rate value data format is UINT8 # [1&2] contact feature status # [3] energy expended not present # [4] rr-intervals present # See Bluetooth specs for details # byte 1 0x44: heart rate value # bytes 2..end 0x33, 0x03, ... : in this case rr-intervals # heart rate value is just the uint8 representation of byte 1 # heart_rate = 0x44 (hex) # = 01000100 (bin) # = 68 (uint8) heart_rate = byte_data[1] # (python converts automatically to uint) # (note that e.g. Java converts bytes automatically to signed int!) print('heart rate =', heart_rate, 'bpm') # heart rate = 68 bpm # the remaining bytes are rr-intervals # each rr interval comprises 2 bytes as uint16 # the rr-interval in milliseconds is then calculated with: # rr_int = uint16_value / 1024 * 1000 # Note that the data comes LSB first, so the bytes have to be flipped rr_int_1 = (byte_data[3] << 8 | byte_data[2]) / 1024 * 1000 print('rr-interval 1 =', rr_int_1, 'ms') # rr interval 1 = 799.8046875 ms rr_int_2 = (byte_data[5] << 8 | byte_data[4]) / 1024 * 1000 print('rr-interval 2 =', rr_int_2, 'ms') # rr-interval 2 = 790.0390625 ms
Service: Health Thermometer
UUID: 0x1809 see also org.bluetooth.service.health_thermometer
The cosinuss° sensors use this service to provide the body core temperature. The data is sent with 1 Hz by indication.
Characteristics:
- Temperature Measurement (UUID:0x2A1C) indicate. Descriptor: Client Characteristic Configuration (UUID:0x2902)
Bluetooth Specifications:
- The temperature bytes are converted to a “medical” 32 bit float as defined by IEEE Std 11073-20601™- 2008 Health Informatics. See chapter 2.2.1 in https://www.bluetooth.com/wp-content/uploads/2019/03/PHD_Transcoding_WP_v16.pdf.
Example:
# Health Thermometer Float Conversion - Python Example # Incoming byte input (hex) from the Bluetooth device: # 0x 04 6A 08 00 FE 03 byte_data = bytearray(b'\x04\x6A\x08\x00\xFE\x03') # These are the components of the input: # byte 0 0x04: Flag byte where each bit has a meaning (LSB first); # 0x04 = 00000100 # [0] data is in °C # [1] time stamp field not present # [2] temperature type field is included # See Bluetooth specs for details # byte 1...4 0x6A 0x08 0x00 0xFE: the data (in LSB first order) # byte 5 0x03: Temperature type field; # Measuring position is the ear # The IEEE float calculation goes according to this formula: # temperature_float = mantissa * 10 ^ exponent # where # mantissa is an int24 from bytes 0x6A, 0x08, 0x00 # exponent is an int8 from byte 0xFE # convert exponent to int8 with two's complement # exponent = 0xFE (hex) # = 1111 1110 (bin) # = -2 (int8 two's complement representation) exponent = 0x00 - ((byte_data[4] ^ 0xFF) + 1) # note that this conversion works only with sign bit set # but we are not expecting positive exponents (high temperatures) # convert mantissa to int24 # mantissa = 0x00386A (hex) # = 00000000 111000 01101010 (bin) # = 2154 (int24) mantissa = ((byte_data[3] << 16) | (byte_data[2] << 8) | byte_data[1]) & 0xFFFFFF # note that this conversion works only with sign bit not set # but we are not expecting negative temperatures temperature_float = mantissa * (10 ** exponent) print('temperature =', temperature_float, '°C') # temperature = 2154 * 10^(-2) °C # temperature = 21.54 °C
Service: Pulse Oximeter
UUID: 0x1822 see also org.bluetooth.service.pulse_oximeter
The cosinuss° sensors use this service to provide the peripheral oxygen saturation SpO2. The data is sent by notification.
Characteristics:
- PLX Continuous Measurement (UUID:0x2A5F) indicate. Descriptor: Client Characteristic Configuration (UUID:0x2902)
Bluetooth Specifications:
- SpO2 and perfusion values are converted to a “medical” 16 bit sfloat as defined by IEEE Std 11073-20601™- 2008 Health Informatics. See chapter 2.2.2 in https://www.bluetooth.com/wp-content/uploads/2019/03/PHD_Transcoding_WP_v16.pdf.
- Note: In firmware versions of the Two up to (and including) 2.0.2, the values are instead coded as “normal” half precision floating point values: https://en.wikipedia.org/wiki/Half-precision_floating-point_format
Example:
# SpO2 and Perfusion Conversion - Python Example # Incoming byte input (hex) from the Bluetooth device: # 0x 10 60 00 FF 07 23 E0 byte_data = bytearray(b'\x10\x60\x00\xFF\x07\x23\xE0') # These are the components of the input bytes: # byte 0 0x10: Flag byte where each bit has a meaning (LSB first); # 0x10 = 00010000 as bits # [4] Pulse Amplitude ("perfusion") present # See Bluetooth specs for details # byte 1..2 0x60, 0x00: SpO2 value # bytes 3..4 0xFF, 0x07: Pulse Rate but it's not used. # 0x07FF == NaN, see Bluetooth specs. # (heart rate is sent separately) # bytes 5..6 0x20, 0x34: perfusion index # ("Pulse Amplitude Index" in Bluetooth Docs) # Both, SpO2 and perfusion are SFLOAT values and are calculated as follows: # float_value = mantissa * 10 ^ exponent # where # exponent high 4 bit signed integer of the 2 data bytes # mantissa low 12 bit signed integer of the 2 data bytes # SpO2 # exponent = 0x0 (hex) - taken from 0x0060 # = 0000 (bin) # = 0 (int4, two's complement) exponent_spo2 = byte_data[2]>> 4 # use only 4 high bits # note that this conversion only works for positive exponents # but the SpO2 precision is typically not in such small magnitudes print('spo2 exponent =', exponent_spo2) # mantissa # mantissa = 0x060 (hex) - taken from 0x0060 # = 0000 01010 0000 (bin) # = 96 (int12) mantissa_spo2 = ((byte_data[2] & 0xF) << 8 | byte_data[1]) # note that this conversion only works for a positive mantissa # but it would be unhealthy for a patient to have negative SpO2 print('spo2 mantissa =', mantissa_spo2) spo2 = mantissa_spo2 * 10**exponent_spo2 print('spo2 =', spo2, '%') # spo2 = 96 % # perfusion # exponent = 0xE (hex) from 0xE023 # = 1110 (bin) # = -2 (int4, two's complement) # sign bit is set => two's complement on high 4 bits if byte_data[6]>> 7 == 1: exponent_perf = 0x0 - (((byte_data[6]>>4) ^ 0xF) + 1) # high 4 bits as unsigned integer else: exponent_perf = byte_data[6]>> 4 print('perfusion exponent =', exponent_perf) mantissa_perf = ((byte_data[6] & 0xF) << 8 | byte_data[5]) print('perfusion mantissa =', mantissa_perf) # note that this works only for a positive mantissa # but the perfusion should be positive anyway. perfusion = mantissa_perf * 10**exponent_perf print('perfusion =', perfusion, '%') # perfusion = 0.35 %
Service: Battery Service
UUID: 0x180F see also org.bluetooth.service.battery_service
The cosinuss° sensors indicate the current battery level with this service. The data is sent by notification.
Characteristics:
- Battery Level (UUID:0x2A19) notify, read. Descriptor: Client Characteristic Configuration (UUID:0x2902)
Example:
# Battery Conversion - Python Example # Incoming byte input (hex) from the bluetooth device: # 0x60 byte_data = bytearray(b'\x60') # The battery percentage is simply the uint8 value of the first byte # battery = 0x60 (hex) # = 01100000 (bin) # = 96 (uint8) battery = byte_data[0] # (Python converts automatically to uint) print('battery =', battery, '%') # battery = 96 %
Service: Device Information
UUID: 0x180A see also org.bluetooth.service.device_information.
The cosinuss° sensors use this service to provide information about the current software version.
Characteristics:
- Manufacturer Name String (UUID:0x2A29) read, e.g. cosinuss
- Model Number String (UUID:0x2A24) read, e.g. one3
- Hardware Revision String (UUID:0x2A27) read, e.g. 3
- Firmware Revision String (UUID:0x2A26) read, e.g. 83f45dae4e0a (hg commit hash)
- Software Revision String (UUID:0x2A28) read, e.g. 6-1
- System ID (UUID:2A23) read
Service: Generic Access
UUID: 0x1800
Characteristics:
- Device Name (UUID: 0x2A00) read, write
- Appearance (UUID: 0x2A01) read
- Peripheral Preferred Connection Parameters (UUID: 0x2A04) read
Service: Generic Attribute
UUID: 0x1801
Characteristics:
- Service Changed (UUID:0x2A05) indicate. Descriptor: Client Characteristic Configuration (UUID:0x2902)
Service: Device Firmware Update Service
UUID: 0x1530
This service is used to perform a device firmware update (DFU) via Bluetooth LE.
Characteristics:
- DFU Packet (UUID:0x1532) write no response
- DFU Control Point (UUID:0x1531) notify, write. Descriptor: Client Characteristic Configuration (UUID:0x2902)
- DFU Version (UUID:0x1534) read
Service: Custom Service
The custom service has a different “Base UUID” for services/characteristics: 0000-XXXX-1212-EFDE-1523-785F-EABC-D123. It's purpose is to send/receive sent proprietary data/commands.
UUID: 0xA000 so full length 0000-A000-1212-EFDE-1523-785F-EABC-D123
Characteristics:
The characteristics may be specified differently from sensor to sensor.
- rawdata (UUID:0xA001) notify, read. Descriptor: Client Characteristic Configuration (UUID:0x2902)
- signal quality and error codes (UUID:0xA002) notify, read. Descriptor: Client Characteristic Configuration (UUID:0x2902)
Signal quality: This is a custom quality indicator for the heart rate values. Values of 30 and above can be considered as good quality. Heart rate values with lower quality are likely to be incorrect. You can receive these values when you enable notifications on 0xA002. The first byte (at index 0) must be the identifier 06 (hex value) to indicate that quality values are in this packet. From this packet the 9th byte (at index 8) needs to be converted to uint8 and then yields the quality value. All other bytes in the packet can be ignored.
Example signal quality:
Let's assume the following packet in hex values was received: 0x06-00-00-85-00-59-5B-2E-31-FF-EF-86-23-EF-F6-DB-FE-9d-23-BE Assign an index to all of the 20 bytes: Bytes(hex): 0x 06 00 00 85 00 59 5B 2E 31 FF EF 86 23 EF F6 DB FE 9d 23 BE Indices: [00][01][02][03][04][05][06][07][08][09][10][11][12][13][14][15][16][17][18][19] ^ ^ Paket ID = 0x06 Quality value = 0x31 Byte[00] with value 06 indicates that it is the correct package Byte[08] is the quality value and can be converted from hex to uint8 0x31 is then 3x16 + 1 = 49 (uint8)
rawdata: This is a data channel used for debugging purposes and to send the raw data (accelerations, ppg, etc.). And to send coommands from client to server. You need a key to use this data stream. This channel is not meant to be public and should be confidential.
Device Error Codes
The sensor may send error codes on 0xA002 when it notices unexpected behavior. The first byte of the package must contain the indicator 0x07, the second byte represents the error code which must be converted to uint8.
Two error codes
Value of byte[1] (uint8) | Meaning |
---|---|
0 | Infrared or red PPG signal too low (only firmware 3.0.0 and lower) |
Errors below are only for firmware versions 3.1.0 and higher | |
10 | Infrared PPG signal too low |
11 | Red PPG singal too low |
12 | Accelerometer error |
13 | Battery curve missing (may lead to wrong battery life estimation) |
60 | Temperature measurement defect |
61 | Temperature measurement unrealistic. Sensor might be out of the ear. |
Example error code:
Let's assume the following packet in hex values was received: 0x07-3C-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 Byte[0] with value 07 indicates that it is a package with an error code Byte[1] is the error code and can be converted from hex to uint8 0x3C is then 3x16 + 12 = 60 (uint8) Which means that the temperature measurement is defect
<font 20px/inherit;;#ff0000;;inherit>!</font> Please note, that the sensor may send an error code on the first sign of a specific error, for example when the PPG signal was below a certain threshold for only a few samples but then returns to normal behavior. It is therefore necessary to implement a threshold for certain errors to avoid a false alarm.
Further reading and useful tools
- A detailed description on GATT can be found on O'Reilly
- Many BLE Questions can be found here Nordic Semiconductor's DevZone.