Synchronising Android and PLUX sensors
Difficulty Level:
Tags other☁android☁opensignals mobile☁bitalino

The OpenSignals mobile application allows to acquire data from internal Android phone sensors and PLUX sensors at the same time. In order to put these into a proper context, the signals acquired from all these sensors need to be synchronised.

In this Jupyter notebook we will show you the essential steps in order to achieve this. First, we will be doing some minor data manipulation. This is then followed by having a look at the data we want to synchronise. Finally, all signals will be synchronised and written into one file.

The Android data used in this notebook was recorded using a Samsung Galaxy A40 with a set sampling rate of 100 Hz . We recorded data from the accelerometer , GPS , linear accelerometer , and rotation vector sensors. Prior to starting this notebook, we already did a synchronisation of the Android sensors. All sensors were resampled to 100 Hz using a padding of type "same" and an interpolation using the "previous" type. If you want to find out how this can be done, please have a look at our Synchronising data from multiple Android sensor files into one file notebook.

The other data was acquired using a new, fresh out of the box, BITalino with a sampling rate of 100 Hz . We recorded data from the accelerometer sensor. Since the BITalino was not altered before acquiring data, only the z-axis of the accelerometer was available. In case you choose to make an acquisition with your PLUX device with a sampling rate of 1000 Hz, you should resample your Android signals to the same sampling rate.

To synchronise both device we took the same approach presented in the notebook on synchronisation of two PLUX devices using the accelerometers. At the beginning of our recording we produced a clearly distinctive event on both accelerometers by moving the devices along their z-axes. The z-axis of the Android phone is perpendicular to the screen.

In case this is your first time working with Android sensors, we highly recommend reading the Introduction to Android sensors notebook with general information on Android sensors.


1 - Package imports

First, let"s import biosignalsnotebooks package and some other packages. All functionalities that we need are within these packages.

In [1]:
# biosignalsnotebooks package
import biosignalsnotebooks as bsnb

# numpy packaged
from numpy import loadtxt

# package for using operating system dependent functionality
import os

# needed in order to extract information from the header in a convenient way
import json

2 - Unit conversion of the BITalino signals

If you want to have a file in which all values are physical quantities, then it makes sense to convert the RAW values acquired with the BITalino . We are also going to change the axis indicating the number of samples to a real time axis. This is done, so that in the final synchronised file, both the BITalino and the Android sensors use time axis. However, this step can also be skipped if you are not in need of that. We will convert the values and subsequently save them to a new file. For value conversion we can make use of the raw_to_phy(...) function of the biosignalsnotebooks package.

The function takes the following inputs:

  • sensor (string): The name of the sensor as string. Supported sensors are "TEMP", "EMG", "ECG", "BVP", "SpO2.ARM", "SpO2.HEAD", "SpO2.FING", "ACC", "EEG", and "EDA".

  • device (string): The name of the device as string.

  • raw_signal (array): The raw signal data to be converted.

  • resolution (int): The resolution of the sensor.

  • option (string):
    A abbreviation for the physical value to which the raw signal should be converted to. Depending on the type of signals, the following options are available: "Ohm", "K", "C" (for "TEMP") | "mV", "V" (for "EMG" and "ECG") | "uA", "A" (for "BVP", "SpO2.ARM", "SpO2.HEAD", and "SpO2.FING") | "g" (for "ACC") | "uV", "V" (for "EEG) | "uS", "S" (for "EDA")

But first, we need to load the raw signal file, open it, and extract all needed information from the header.

2.1 - Get metadata through the read of file header

In [2]:
# set file path
path = '../../images/other/android_bitalino_sync/opensignals_98D351FD8E78_2020-07-29_19-51-58.txt'
     
# get the header of the file
with open(path, encoding='latin-1') as opened_file:
    # read the information from the header lines (omitting the begin and end tags of the header)
    header = opened_file.readlines()[1][2:]  # omit "# " at the beginning of the sensor information
    
    # convert the header to a dictionary (for easier access of the information that we need)
    header = json.loads(header)

2.2 - Access/store relevant metadata about the sensor, device and acquisition

In [3]:
# get the key first key of the dictionary (all information can be accessed using that key)
dict_key = list(header.keys())[0]
    
# get the values for unit conversion
device = header[dict_key]['device']
resolution = header[dict_key]['resolution'][0]

# get the sampling rate of the device
sampling_rate = header[dict_key]['sampling rate']

# set the sensor name and the option
sensor_name = 'ACC'
option = 'm/s^2'

2.3 - Load BITalino data from file

In [4]:
# get the data from the file (as an numpy array) 
data = loadtxt(path)

2.4 - Generate a time-axis and convert RAW values to acceleration physical units

In [5]:
# convert the sampling axis to a time axis
data[:, 0] = bsnb.generate_time(data[:, -1], sampling_rate)

# convert the data of the ACC channel (in our case the ACC data is the last column in the file. This can be accessed using -1)
# in order to make things simple we are directly overwriting that channel in the data array
data[:, -1] = bsnb.raw_to_phy(sensor_name, device, data[:, -1], resolution, option)

2.5 - Creation of a new file with the converted time and signal values

In [6]:
# set file name for the file in which the converted data is going to be saved
file_name = 'bitalino_converted.txt'

# get the current directory
curr_path = os.path.abspath(os.getcwd())

# crete full file path
save_path = os.path.join(curr_path, file_name)

# convert header to string
header_string = "# OpenSignals Text File Format\n# " + json.dumps(header) + '\n# EndOfHeader\n'

# open a new file at the path location
sync_file = open(save_path, 'w')

# write the header to the file
sync_file.write(header_string)

# write the data to the file. The values in each line are written tab separated
for row in data:
    sync_file.write('\t'.join(str(value) for value in row) + '\t\n')

# close the file
sync_file.close()

3 - Taking a look at the channels used for synchronisation

Before we start synchronising both files, we will take a look at both accelerometer channels that we are going to use for synchronisation. The z-axis of the accelerometer are the fifth ("CH5") and the second channel ("CH2") for the BITalino and the Android sensor files, respectively.

As we can see in the plot below, both channels are only slightly shifted in relation to each other. This shift may however change from recording to recording, because of the Android system"s sampling procedure. In case you are wondering: The Android accelerometer sensor measures the acceleration including the force of gravity, thus the data is shifted along the y-axis of the plot.

In [7]:
# set file path
path = '../../images/other/android_bitalino_sync/'

# set full file paths
# for the BITalino we reuse the path where we saved the file
bitalino_file = save_path
android_file =  path + 'android_sensor_sync.txt'

# load the data
bitalino_data = bsnb.load(bitalino_file, get_header=False)
android_data = bsnb.load(android_file, get_header=False)

# plot data
bitalino_time = loadtxt(bitalino_file)[:, 0]
android_time = loadtxt(android_file)[:, 0]


# plot the data of both files
bsnb.plot([bitalino_time, android_time], [bitalino_data['CH5'], android_data['CH2']], legend_label=["bitalino CH5", "android CH2"], y_axis_label=["Accelerometer", "Accelerometer"], x_axis_label="Time (s)")

4 - Create the synchronised file

Now, that we have everything set up, we can perform the synchronisation of the two files. This can be achieved by calling the generate_sync_txt_file(...) function of the biosignalsnotebooks package.

The function takes the following inputs:

  • in_path (string or list of strings): A path or a list of paths, as string, pointing to the files that are supposed to be synchronised. If the input is a string, it is assumed that the two signals are in the same file, else, if the input is a list, it is assumed that the two signals are in different file.

  • channels (list): List with the strings identifying the channels of each signal. (default: ("CH1", "CH1")).

  • new_path (string): The path to create the new file. (default: "sync_file.txt")
In [8]:
# put list in file
input_paths = [bitalino_file, android_file]

# set path, including new file name
new_path = os.path.join(curr_path, 'android_bitalino_sync.txt')

# synchronize
bsnb.generate_sync_txt_file(input_paths, channels=('CH5', 'CH2'), new_path=new_path)

In this Jupyter notebook we learned how to synchronise data acquired from PLUX and Android sensor at he same time using the biosignalsnotebooks package.

We hope that you have enjoyed this guide . biosignalsnotebooks is an environment in continuous expansion, so don"t stop your journey and learn more with the remaining Notebooks .

In [9]:
from biosignalsnotebooks.__notebook_support__ import css_style_apply
css_style_apply()
.................... CSS Style Applied to Jupyter Notebook .........................
Out[9]: