.. BLE characteristics and services of Byteflies nodes
.. Author: Jonathan Dan

.. STATUS: production


####################################
BLE Specification of Byteflies Nodes
####################################

BLE characteristics and services of Byteflies nodes

.. rubric:: Quick Navigation
.. contents::
   :local:
   :depth: 2

----


***********************
General Characteristics
***********************

Device information
==================

Service
    - 0x180A
Characteristics
    - 0x2A24 (R- -): model number
    - 0x2A25 (R- -): serial number
    - 0x2A26 (R- -): firmware revision
    - 0x2A27 (R- -): hardware revision
    - 0x2A28 (R- -): software revision
    - 0x2A29 (R- -): manufacturer

The device information characteristics are only partly implemented. All these characteristics are **read** only. The characteristics all contain an ASCII string.

.. todo::

  better description of each characteristic and example value

The different characteristics in this service implement the BLE specification described in `this document <https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.device_information.xml>`_.

Battery
=======

Service
    - 0x180F
Characteristic
    - 0x2A19 (R-N): battery level

The current charge level of a battery. This characteristic has both **read** and **notification**. The battery level is coded in 1 unsigned byte (``uint8``). 100% represents fully charged while 0% represents fully discharged.


Real Time Clock
===============

*(new in firmware version 0.7)*

Service
    - 0xBFC0
Characteristic
    - 0xBFC1 (RW-): current unix timestamp

This characteristic controls the time of the clock on the node. The time is coded as a unix timestamp. This characteristic has both **read** and **write** enabled. The timestamp is coded in 4 bytes (``uint32``) in little endian.

.. note:: Since the drift of the µC clock has not yet been measured, it is advised to set the time before a recording.

Memory
======

Service
    - 0xBFA0
Characteristics
    - 0xBFA1 (RW-): memory status
    - 0xBFA2 (RW-): logged channels
    - 0xBFA3 (R- -): memory usage
    - 0xBFA4 (R- -): total memory

Memory status
--------------

Characteristic
    - 0xBFA1 (RW-): memory status

Indicates the status of the memory. The characteristic is **read** and **write** enabled. The memory status is coded in 1 byte. The 3 highest bits indicate an action for the memory. The 3 actions are **log data** (highest bit), **send logged data over serial** and **erase memory content**. ::

    log_send-serial_erase_0_0_0_0_0

To enable an action, the corresponding bit must be ``1``. When the memory status is **idle** all bits are ``0``. The **erase** and **send logged data over serial** automatically return to zero when they are completed. Starting and stopping a measurement must be done manually. If the memory content becomes full during a measurement, the **log data** bit is automatically set to ``0``.


*Several bits can be set to 1 at the same time. When this is the case, the microcontroller executes the action from the lowest active bit to the highest active bit (first erasing, then sending over serial and finally logging).*

Logged channels
---------------

Characteristic
    - 0xBFA2 (RW-): memory channels

Two bytes indicating which channels the node is logging (or should start logging). Each bit in these 2 bytes indicates if a channel is being logged if the bit is set to ``1``. The ordering of the bits corresponding to each channel is shown below: ::

    CH8_CH7_CH6_CH5_CH4_CH3_CH2_CH1 CH16_CH15_CH14_CH13_CH12_CH11_CH10_CH9

For the ECG node, CH1 and CH2 can be enabled and correspond to the two bipolar ECG channels.

For the PPG node, CH1, CH2 and CH3 can be enabled and correspond respectively to LED1 (green), LED2 (red) and LED3 (infrared).

The 3 axis accelerometer channels are always enabled.

Sample start measure Python code:

.. code-block:: python

  selected_devices = [True, False, False]  # bool array [LED1, LED2, LED3]
  def start_measure(self, selected_devices):
      value = 0x00
      for device in reversed(selected_devices):
          value = value << 1
          value += device
      self.device.char_write(0xBFA2, bytearray([value, 0x00]))
      self.device.char_write(0xBFA1, bytearray([0x80]))

Memory usage
------------

Characteristic
    - 0xBFA3 (R- -): memory usage

The memory usage in bytes. This characteristic is **read** only. The memory usage is coded as a 4 byte unsigned integer (``uint32``). The bytes are in little endian.

Total memory
------------

Characteristic
    - 0xBFA4 (R- -): total memory

The total memory in bytes. This characteristic is **read** only. The total memory is coded as a 4 byte unsigned integer (``uint32``). The bytes are in little endian.


Acceleration
============

Service
    - 0xBFB0
Characteristics
    - 0xBFB1 (R-N): X-axis
    - 0xBFB2 (R-N): Y-axis
    - 0xBFB3 (R-N): Z-axis

Three characteristics with both **read** and **notification**. Each channel emits data at 25Hz [1]_. A data package is composed of 20 bytes (10 data points). Each data point is coded in 2 bytes. The data points are little endian and can be converted to signed values by taking twos complement.

Sample decoding Python code:

.. code-block:: python

  def read_acceleration(raw_data):
  data = list()
  for i in range(10):
      value = int.from_bytes(raw_data[2 * i:2 * (i + 1)], byteorder='little')
      data.append(twos_comp(value, 16))
  return data

.. [1] The logging frequency is higher.

**********
ECG Sensor
**********

Service
    - 0xBF10

Data
====

Service
    - 0xBF10
Characteristics
    - 0xBF11 (R-N): channel 1
    - 0xBF12 (R-N): channel 2

Two characteristics with both **read** and **notification**. Each channel emits data at 125Hz. A data package is composed of 12 bytes (4 data points). Each data point is coded in 3 bytes. The data points are big endian and can be converted to signed values by taking twos complement.

Sample decoding Python code:

.. code-block:: python

  def read_ecg(self, raw_data):
      data = list()
      for i in range(4):
          value = int.from_bytes(raw_data[3 * i:3 * (i + 1)], byteorder='big')
          data.append(twos_comp(value, 24))
      return data

Configuration
=============

Service
    - 0xBF00
Characteristic
    - 0xBF13 (RW-): configuration

The configuration of the ECG sensor. The characteristic is **read** and **write** enabled. The configuration is 1 byte. It configures the sampling frequency of the ECG sensor. This sampling frequency determines the frequency of the logged samples, not of the samples sent over Bluetooth.

It can hold values from 0 to 6 (``uint8``). The sampling frequency is given by:

.. math::

  125*2^n

where *n* is the value contained in the configuration characteristic as a ``uint8``.

+--------+--------------------+
| 0xBF13 | Sampling Frequency |
+========+====================+
| 0      | 125 Hz             |
+--------+--------------------+
| 1      | 250 Hz             |
+--------+--------------------+
| 2      | 500 Hz             |
+--------+--------------------+
| 3      | 1 kHz              |
+--------+--------------------+
| 4      | 2 kHz              |
+--------+--------------------+
| 5      | 4 kHz              |
+--------+--------------------+
| 6      | 8 kHz              |
+--------+--------------------+

**********
PPG Sensor
**********

Service
    - 0xBF00

Data
====

Service
    - 0xBF00
Characteristics
    - 0xBF01 (R-N): green LED
    - 0xBF02 (R-N): red LED
    - 0xBF03 (R-N): infrared LED
    - 0xBF04 (R-N): ambient light

Four characteristics with both **read** and **notification**. Each channel emits data at 25Hz. A data package is composed of 12 bytes (4 data points). Each data point is coded in 3 bytes. The data points are little endian and can be converted to signed values by taking twos complement.

Sample decoding Python code:

.. code-block:: python

  def read_ppg(self, raw_data):
      data = list()
      for i in range(4):
          value = int.from_bytes(raw_data[3 * i:3 * (i + 1)], byteorder='little')
          data.append(twos_comp(value, 24))
      return data

Configuration
=============

Service
    - 0xBF00
Characteristic
    - 0xBF05 (RW-): configuration

The configuration of the PPG sensor. The characteristic is **read** and **write** enabled. The configuration is 7 bytes. It configures :

* The LED light intensities (green, red, infrared)
* The LED offset current: cancellation current (green, red, infrared)
* The amplifier gain (R\ :sub:`f`)
* The amplifier filter (C\ :sub:`f`)

All these configurations are contained in 7 bytes. When referring to the individual bits, the python indexing notation will be used:

[n\ :sub:`0`, n\ :sub:`1`\ [, where n\ :sub:`0` is first bit to read (bit 0 is the :abbr:`MSB (most significant bit)`) and n\ :sub:`1` is the stop bit (i.e. the first bit that should not be read).

PPG config summary
------------------

+-----------+-------+-------+-------+-------+-------+-------+-------+----------+
|           | Bit 0 | Bit 1 | Bit 2 | Bit 3 | Bit 4 | Bit 5 | Bit 6 | Bit 7    |
+===========+=======+=======+=======+=======+=======+=======+=======+==========+
| **Byte 0**| 0     | 0     | Green LED                                        |
+-----------+-------+-------+--------------------------------------------------+
| **Byte 1**| 0     | 0     | Red LED                                          |
+-----------+-------+-------+--------------------------------------------------+
| **Byte 2**| 0     | 0     | Infrared LED                                     |
+-----------+-------+-------+-------+-------------------------------+----------+
| **Byte 3**| 0     | 0     | 0     | Green offset current value    | polarity |
+-----------+-------+-------+-------+-------------------------------+----------+
| **Byte 4**| 0     | 0     | 0     | Red offset current value      | polarity |
+-----------+-------+-------+-------+-------------------------------+----------+
| **Byte 5**| 0     | 0     | 0     | Infrared offset current value | polarity |
+-----------+-------+-------+-------+-------+-------+---------------+----------+
| **Byte 6**| Gain (R\ :sub:`f`)    | 0     | 0     | Filter (C\ :sub:`f`)     |
+-----------+-----------------------+-------+-------+--------------------------+


LED intensity
-------------

* Green LED: bits [2, 8[
* Red LED: bits [10, 16[
* Infrared LED: bits [18, 24[

The LED intensity is coded as a 6 bit unsigned integer (``uint6``). The encoding is big endian. The intensity can take values between 0 and 63. This value can be converted to the current thought the LED using this formula

.. math::

  \text{LED current} = 50*\frac{n}{63} \text{mA}

where *n* is the intensity of the LED.

LED offset
----------

Value
  * Green LED: bits [27, 31[
  * Red LED: bits [35, 39[
  * Infrared LED: bits [43, 47[
Sign
  * Green LED: bit [31]
  * Red LED: bit [39]
  * Infrared LED: bit [47]

The LED offset value is coded as a 4 bit unsigned integer (``uint4``). The encoding is big endian. The intensity can take values between 0 and 15. The offset can approximately (with a 20% variation across nodes) be converted to an offset current using the formula

.. math::
  \text{offset current} = 0.47*n ~ \mu\text{A}

where *n* is the offset value.

The sign of the offset is coded on one bit. ``1`` means a negative offset and ``0`` a positive offset.

Gain
----

* Gain bits [48, 51[

The transfer function of the amplifier is controlled by a resistance and a capacitor. Doubling the value of the resistance doubles the amplification factor. A set of resistance values are available:

+-------+-----------+
|  code |  Resistor |
+=======+===========+
| 000   |  500 kΩ   |
+-------+-----------+
| 001   |  250 kΩ   |
+-------+-----------+
| 010   |  100 kΩ   |
+-------+-----------+
| 011   |  50 kΩ    |
+-------+-----------+
| 100   |  25 kΩ    |
+-------+-----------+
| 101   |  10 kΩ    |
+-------+-----------+
| 110   |  1 MΩ     |
+-------+-----------+
| 111   |  2 MΩ     |
+-------+-----------+

Filter capacitor
----------------

* Filter capacitor bits [53, 56[

The capacitor value can be used as a low pass filter for the amplifier. This setting is implemented but not used. It should be set to 25 pF (``110``). A set of capacitor values are available:

+------+------------+
| code |  Capacitor |
+======+============+
| 000  |  5 pF      |
+------+------------+
| 001  |  2.5 pF    |
+------+------------+
| 011  |  7.5 pF    |
+------+------------+
| 010  |  10 pF     |
+------+------------+
| 101  |  17.5 pF   |
+------+------------+
| 100  |  20 pF     |
+------+------------+
| 111  |  22.5 pF   |
+------+------------+
| 110  |  25 pF     |
+------+------------+
