====== 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:
* [[https://www.bluetooth.com/de/specifications/gatt/|https://www.bluetooth.com/de/specifications/gatt/]]
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:
[[https://www.bluetooth.com/specifications/assigned-numbers/|https://www.bluetooth.com/specifications/assigned-numbers/]]
===== - 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 [[https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Descriptors/org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml|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. [[https://developer.android.com/reference/android/bluetooth/BluetoothGatt#setCharacteristicNotification(android.bluetooth.BluetoothGattCharacteristic,%20boolean)|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. [[https://developer.android.com/reference/android/bluetooth/BluetoothGatt#writeDescriptor(android.bluetooth.BluetoothGattDescriptor)|on Android]]):
* 0x**0100** to activate **notifications**
* 0x**0200** to activate **indications**
* 0x**0300** to activate both
**Stop Notifications/Indications**
- Write the hex command 0x**0000** to the CCCD of the server
- Disable the notifications/indications on your client
===== - Services and characteristics (used by cosinuss°) =====
Most cosinuss° sensors use the following services (of which most are standard services as defined by the Bluetooth SIG). Each comes with a UUID. The "Base UUID" for standard services/characteristics from the Bluetooth SIG is ''XXXXXXXX-0000-1000-8000-00805F9B34FB''.
In the following sections only the short 16 bit version of the UUID is shown (i.e for standard services 0x180D is an abbreviation of 0000180D-0000-1000-8000-00805F9B34FB).
^ Service Name ^ 16-bit UUID (0x) ^
^ [[#Generic Access]] | 1800 |
^ [[#Generic Attribute]] | 1801 |
^ [[#Health Thermometer]] | 1809 |
^ [[#Device Information]] | 180A |
^ [[#Heart Rate]] | 180D |
^ [[#Battery]] | 180F |
^ [[#Pulse Oximeter]] | 1822 |
^ [[#Secure DFU Service]] | FE59((proprietary service)) |
^ [[#cosinuss° Custom Service]] | A000((proprietary service)) |
==== - Generic Access ====
UUID: 0x1800
Characteristics:
* Device Name (UUID: 0x2A00) read, write
* Appearance (UUID: 0x2A01) read
* Peripheral Preferred Connection Parameters (UUID: 0x2A04) read
==== - Generic Attribute ====
UUID: 0x1801
Characteristics:
* Service Changed (UUID:0x2A05) indicate. Descriptor: Client Characteristic Configuration (UUID:0x2902)
==== - 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:
* [[https://www.bluetooth.com/specifications/specs/health-thermometer-service-1-0/|https://www.bluetooth.com/specifications/specs/health-thermometer-service-1-0/]]
* 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|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
==== - 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
==== - 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:
* [[https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/|https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/]]
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
==== - Battery ====
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 %
==== - 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:
* [[https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=304965|https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=304965]]
* 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|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|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 %
==== - Secure DFU Service ====
UUID: 0000FE59-0000-1000-8000-00805F9B34FB
A propietary service of Nordic Semiconductor used to perform a device firmware update (DFU) via Bluetooth LE.
===== - cosinuss° Custom Service ====
UUID: 0000-A000-1212-EFDE-1523-785F-EABC-D123
The custom service has a different "Base UUID" for services/characteristics: ''0000-XXXX-1212-EFDE-1523-785F-EABC-D123''. It's purpose is to receive proprietary data (e.g. [[#signal quality]], [[#device error codes]], ...) via the custom characteristics:
Characteristics:
* [[#cosinuss° Rawdata Characteristic]] (UUID:0xA001) notify, read. Descriptor: Client Characteristic Configuration (UUID:0x2902)
* [[#cosinuss° SOC Characteristic]] (UUID:0xA002) notify, read. Descriptor: Client Characteristic Configuration (UUID:0x2902)
==== - cosinuss° Rawdata Characteristic ====
UUID: 0xA001
This characteristic is used for debugging purposes and to send the extended data (accelerations, ppg, etc.). You need a key to use this data stream. This channel is not meant to be public and should be confidential.
==== - cosinuss° SOC characteristic ====
UUID: 0xA002
When notifications on 0xA002 are enabled, the sensor will send additional info like [[#signal quality]] and [[#device error codes]].
=== - Signal quality ===
Signal quality is a custom quality indicator for the **heart rate** values. Values of 30 and above can be considered as good quality. Heart rate values that come with a lower signal quality are likely to be incorrect.
You can receive these values when you enable notifications on the 0xA002 characteristic and listen for the right **PacketID** (the value of the first byte in the BLE packet). Then convert the value at the byte index **8** to uint8.
There are two variants of the signal quality depending on the firmware version of the sensor:
* **Signal quality max** (recommended):
* PacketID: **0x27**
* Firmware versions
* c-med° alpha: >= 2.0.0
* °Two: >= 4.0.0
* **Signal quality **
* PacketID: **0x06**
* Firmware versions: all
Example signal quality max:
Let's assume the following packet in hex values was received:
0x27-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 27 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 = 0x27 Quality value = 0x31
Byte[00] with value 0x27 indicates that it is the correct package
Byte[08] is the quality_max value and can be converted from hex to uint8
0x31 is then 3x16 + 1 = 49 (uint8)
=== - 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 **PacketID 0x07**, the second byte represents the error code. The meanings are listed in the table below.
**Note:** Error codes of older firmware versions may differ and are not listed here.
^ error code (hex) ^ Meaning ^
| 0A | Infrared threshold |
| 0B | Red threshold |
| 0C | Acceleration axes |
| 0D | Unknown battery curve |
| 0E | Green threshold |
| 11 | Temperature defect |
| 3C | Temperature defect |
| 3D | Temperature unrealistic |
* c-med° alpha only, °Two only
Example error code:
Let's assume the following packet in hex values was received:
0x07-0B-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] contains the error code 0x0B which means that the red LED
may be defective
**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 [[https://www.oreilly.com/library/view/getting-started-with/9781491900550/ch04.html|O'Reilly]]
* Many BLE Questions can be found here [[https://devzone.nordicsemi.com/|Nordic Semiconductor's DevZone]].
* One of the most helpful tools for working with BLE is the Nordic nRF Connect Mobile App ([[https://apps.apple.com/de/app/nrf-connect/id1054362403|iOS]], [[https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&hl=de&gl=US|Android]]). For example you can read and write every service and characteristic of any BLE device or see the logging messages of the nRF app to get a better understanding of how the app communicates.
\\