BrainBit foe developers Visit website Subscribe for updates
Home
SDK
Device

Receiving data C/C++

 

This article describes the ways data could be received from the BrainBit device. We have two options to get data from the sensor - with the use of callbacks and via channel buffers, both will be described below. The full source code could be found in the Brainbit Example repository.

 

If you have found some erroneous information on this or any other page or you have any questions about the device or SDK, don't hesitate to ask your questions via email or file a bug on our public issue tracker.

Email: support@brainbit.com

Issue tracker link: https://gitlab.com/brainbit-inc/brainbit-sdk/issues

We also have a chant on Gitter.

 

Data types

The BrainBit device could provide you with the following types of data: 

  • EEG signal from 4 channels
  • Resistance data for 4 channels
  • Bipolar EEG channels of your choice
  • Battery charge level

4 EEG channels are canonical EEG montage channels T3, T4, O1, O2 from the 10-20 EEG system. For all of these channels, you could get electrode-skin resistance value, but not simultaneously with the signal receiving. You will receive zeros instead of the signal during resistance measurement.

Up to 12 bipolar channels could be available by pairwise combining of the original EEG channels. These bipolar channels are destined for coherent noise reduction in source channels and could be used in various EEG analyses. 

 

Example program

We will use a connection to the device by serial number described in the Device Search article. First of all, create a new project and add to the main.c file the following code:

    
#include "cscanner.h"
#include "cdevice.h"
#include "sdk_error.h"
#include "stdio.h"
#include "windows.h"

int main()
{
	while (true)
	{
		printf("Enter a serial number of a device or \"q\" to exit.\n");

		uint64_t serialNumber;
		if (scanf_s("%llu", &serialNumber, (rsize_t)sizeof(uint64_t)) == 0)
		{
			if (fgetc(stdin) == 'q') 
			{
				while (fgetc(stdin) != '\n') {}
				break;
			}
			else
			{
				while (fgetc(stdin) != '\n'){}
				printf("Invalid command\n");
				continue;
			}
		}
		
		receive_data(serialNumber);
	}

	return 0;
}
    

As you can see, we just wait for a user to input a serial number of the device to connect with and call the receive_data function with that serial number.

    
void receive_data(uint64_t serial_number)
{
	DeviceEnumerator* enumerator = create_device_enumerator(DeviceTypeBrainbit);
	if (enumerator == NULL)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device enumerator is null: %s\n", errorMsg);
		return;
	}

	int attempts = 20;
	do
	{
		Device* device = find_device(enumerator, serial_number);
		if (device == NULL)
		{
			Sleep(300);
			continue;
		}

		const int resultCode = device_connect(device);
		if (resultCode != SDK_NO_ERROR)
		{
			char errorMsg[1024];
			sdk_last_error_msg(errorMsg, 1024);
			printf("Cannot connect to device: %s\n", errorMsg);
			device_delete(device);
			continue;
		}

		print_battery_charge(device);
		check_resistance(device);		
		print_signal(device);
		
		device_disconnect(device);
		device_delete(device);
		enumerator_delete(enumerator);
		return;
	}
	while (attempts-- > 0);
	
	enumerator_delete(enumerator);
	printf("Device with SN %llu not found.\n", serial_number);
}
    

This function is also very similar to its counterpart - find_by_serial - from the device search example. The main difference is that instead of printing device information we connect to the device (see the quickstart guide) and call the functions that perform reading of different types of data from the sensor.

 

Battery charge receiving

The first function to consider is print_battery_charge. Create this function with the following code.

    
void print_battery_charge(Device* device)
{
	ChannelInfoArray deviceChannels;
	int resultCode = device_available_channels(device, &deviceChannels);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Cannot get device channels info: %s\n", errorMsg);
		return;
	}
}
    

Here we use device_available_channels function to get a list of existing data channels in the devices. Before receiving data we have to be sure that required channel resides in the device. In the following code we check for BatteryChannel presence.

    
for (size_t i = 0; i < deviceChannels.info_count; ++i)
{
	if (deviceChannels.info_array[i].type == ChannelTypeBattery)
	{
		//subscribe battery charge changed event
	}
}
free_ChannelInfoArray(deviceChannels);
    

When we successfully found the BatteryChannel information in the device channels list we can subscribe for that channel notifications. To do it we need to store somewhere notification listener handler. We also need a variable to store a battery charge. The listener handle and the variable for the charge value have to be global in our example because channel data could be received only asynchronously. Add the following lines to the beginning of the main.c file after include directives.  

    
ListenerHandle BatteryListener = NULL;
int BatteryCharge = 0;
    

Now replace the //subscribe battery charge changed event the following code.

    
resultCode = device_subscribe_int_channel_data_received(device, deviceChannels.info_array[i], &on_battery_charge_received, &BatteryListener, NULL);
if (resultCode != SDK_NO_ERROR)
{
	char errorMsg[1024];
	sdk_last_error_msg(errorMsg, 1024);
	printf("Cannot subscribe battery channel notifications: %s\n", errorMsg);
	return;
}
    

This code sets callback function on_battery_charge_received which will be receiving notifications until the BatteryListener listener handle is deleted with free_listener_handle function. You have to pass to device_subscribe_int_channel_data_received function ChannelInfo structure to identify the channel which you want to subscribe to. The _int_ part of the name of the function means that it can set a callback for any channel with integer data. BatteryChannel provides us with a charge value in percent represented as integer values. 

Add a definition of the callback function to the main.c file

    
void on_battery_charge_received(Device* device, ChannelInfo channelInfo, IntDataArray batteryData, void* user_data)
{
	if (batteryData.samples_count > 0)
	{
		BatteryCharge = batteryData.data_array[batteryData.samples_count - 1];
	}
}
    

In this function, we set the BatteryCharge variable with the last value in the received buffer to get the latest charge value. Now we need some mechanism to track an update of the charge variable.

Add this line to the print_battery_charge function before channel search for-loop

    
BatteryCharge = -1;
    

 and these lines after

    
while (BatteryCharge < 0)
{
	//waiting for battery charge
}
    

Thus, we are able to check whether the BatteryCharge variable was set or not. After we leave the while loop, we know that charge data were received. 

Now we could free the listener and print battery data.

    
free_listener_handle(BatteryListener);
BatteryListener = NULL;

printf("Battery charge: %d\n", BatteryCharge);
    

 

 

Electrodes resistance

The next function to consider is check_resistance. It is more complicated because we must subscribe for 4 channels data events, though the main idea is the same as for BatteryChannel. 

    
void check_resistance(Device* device)
{
	printf("Measuring electrodes resistance...\n");

	ChannelInfoArray deviceChannels;
	int resultCode = device_available_channels(device, &deviceChannels);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Cannot get device channels info: %s\n", errorMsg);
		return;
	}

	for (size_t i = 0; i < deviceChannels.info_count; ++i)
	{
		if (deviceChannels.info_array[i].type == ChannelTypeResistance)
		{
			if (strcmp(deviceChannels.info_array[i].name, "T3") == 0)
			{
				device_subscribe_double_channel_data_received(device, deviceChannels.info_array[i], &on_resistance_received, &T3ResistanceListener, NULL);
			}
			if (strcmp(deviceChannels.info_array[i].name, "T4") == 0)
			{
				device_subscribe_double_channel_data_received(device, deviceChannels.info_array[i], &on_resistance_received, &T4ResistanceListener, NULL);
			}
			if (strcmp(deviceChannels.info_array[i].name, "O1") == 0)
			{
				device_subscribe_double_channel_data_received(device, deviceChannels.info_array[i], &on_resistance_received, &O1ResistanceListener, NULL);
			}
			if (strcmp(deviceChannels.info_array[i].name, "O2") == 0)
			{
				device_subscribe_double_channel_data_received(device, deviceChannels.info_array[i], &on_resistance_received, &O2ResistanceListener, NULL);
			}
		}
	}
	free_ChannelInfoArray(deviceChannels);
}
    

You may notice that the subscription function has _double_ instead of _int_ int its name, that's because resistance values are in Ohms represented as double-precision floating-point numbers. We now also are checking the names of channels to distinguish them. Every channel has its own listener handle. Add the following handle definitions to the beginning of the file after the include section.

    
ListenerHandle T3ResistanceListener = NULL;
ListenerHandle T4ResistanceListener = NULL;
ListenerHandle O1ResistanceListener = NULL;
ListenerHandle O2ResistanceListener = NULL;
    

 We will also use a helper function to delete handles.

    
void free_resistance_listeners()
{
	free_listener_handle(T3ResistanceListener);
	free_listener_handle(T4ResistanceListener);
	free_listener_handle(O1ResistanceListener);
	free_listener_handle(O2ResistanceListener);

	T3ResistanceListener = NULL;
	T4ResistanceListener = NULL;
	O1ResistanceListener = NULL;
	O2ResistanceListener = NULL;
}
    

And the callback function definition.

    
void on_resistance_received(Device *device, ChannelInfo channel_info, DoubleDataArray data_array, void* user_data)
{
	if (channel_info.type == ChannelTypeResistance)
	{
		//accumulating RESISTANCE_AVERAGE_COUNT resistance values for each channel
		if (strcmp(channel_info.name, "T3") == 0)
		{
			append_resistance_value(&T3AverageResistance, data_array);
		}
		if (strcmp(channel_info.name, "T4") == 0)
		{
			append_resistance_value(&T4AverageResistance, data_array);
		}
		if (strcmp(channel_info.name, "O1") == 0)
		{
			append_resistance_value(&O1AverageResistance, data_array);
		}
		if (strcmp(channel_info.name, "O2") == 0)
		{
			append_resistance_value(&O2AverageResistance, data_array);
		}
	}

	free_DoubleDataArray(data_array);
}
    

As you can see we use one callback function for all 4 channels and check the channel name each time we receive a notification. We also can use separate callback functions to eliminate name checks. To store resistance data we will use simple struct which has one field for an average resistance value and one counter to control the number of samples in average value. The number of resistance samples to accumulate is declared as a macro definition RESISTANCE_AVERAGE_COUNT.

    
#define RESISTANCE_AVERAGE_COUNT 10

typedef struct _AverageResistance
{
	double Value;
	size_t AverageCount;
} AverageResistance;

AverageResistance T3AverageResistance;
AverageResistance T4AverageResistance;
AverageResistance O1AverageResistance;
AverageResistance O2AverageResistance;
    

Our program will accumulate 10 values of resistance fore each channel and print average values. To append new values to Average structures append_resistance_value function is used.

    
void append_resistance_value(AverageResistance *resistance, DoubleDataArray resistance_array)
{
	for (size_t i = 0; 
		i < resistance_array.samples_count && resistance->AverageCount < RESISTANCE_AVERAGE_COUNT; 
		++i, ++resistance->AverageCount)
	{
		resistance->Value += resistance_array.data_array[i] / RESISTANCE_AVERAGE_COUNT;
	}
}
    

It accumulates resistance values divided by predefined count for averaging until the counter reaches RESISTANCE_AVERAGE_COUNT. If the averaging structure is full we ignore new data. Now we need to implement a wait-for-data mechanism. Before do it add these lines to check_resistance function before the channel iterating for-loop.

    
T3AverageResistance.AverageCount = 0;
T3AverageResistance.Value = 0.0;
T4AverageResistance.AverageCount = 0;
T4AverageResistance.Value = 0.0;
O1AverageResistance.AverageCount = 0;
O1AverageResistance.Value = 0.0;
O2AverageResistance.AverageCount = 0;
O2AverageResistance.Value = 0.0;
    

We must reset values before use, to be sure, that our algorithm will give us the right values.

Now add the waiting code to the same function after the loop. It also contains device commands code to run and stop resistance measurement.

    
resultCode = device_execute(device, CommandStartResist);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Cannot execute StartResist command: %s\n", errorMsg);
		free_resistance_listeners();
		return;
	}
	
	while (T3AverageResistance.AverageCount < RESISTANCE_AVERAGE_COUNT 
		|| T4AverageResistance.AverageCount < RESISTANCE_AVERAGE_COUNT
		|| O1AverageResistance.AverageCount < RESISTANCE_AVERAGE_COUNT
		|| O2AverageResistance.AverageCount < RESISTANCE_AVERAGE_COUNT)
	{
		//waiting for resistance
	}

	resultCode = device_execute(device, CommandStopResist);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Cannot execute StartResist command: %s\n", errorMsg);
		free_resistance_listeners();
		return;
	}

	free_resistance_listeners();
    

Here we run the resistance measurement and start witing for all channels to have enough data for averaging. After that, we stop resistance receiving because we don't need that data anymore. And eventually, we delete resistance listeners.

And finally, we can print the resulting resistance in kOhms.

    
//printing resistance in kOhms
	printf("Average resistance T3: %.2fk T4: %.2fk O1: %.2fk O2: %.2fk\n", 
		T3AverageResistance.Value / 1e3, 
		T4AverageResistance.Value / 1e3,
		O1AverageResistance.Value / 1e3,
		O2AverageResistance.Value / 1e3);
    

 

 EEG signal data

The process of acquiring signal data significantly differs from that for Battery and resistance channels. The signal data rate is about 125 pairs of samples per second for each channel so the SDK library provides internal buffers to store that data for users' convenience. These buffers are also called channels and could be used in a vast variety of algorithms for EEG analysis. 

Nonetheless, the first step of data receiving is similar to that for Resistance and Battery channels.

    
void print_signal(Device* device)
{
	printf("Receiving signal...\n");

	ChannelInfoArray deviceChannels;
	int resultCode = device_available_channels(device, &deviceChannels);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Cannot get device channels info: %s\n", errorMsg);
		return;
	}

	for (size_t i = 0; i < deviceChannels.info_count; ++i)
	{
		if (deviceChannels.info_array[i].type == ChannelTypeSignal)
		{
			if (strcmp(deviceChannels.info_array[i].name, "T3") == 0)
			{
				T3Signal = create_EegDoubleChannel_info(device, deviceChannels.info_array[i]);
			}
			if (strcmp(deviceChannels.info_array[i].name, "T4") == 0)
			{
				T4Signal = create_EegDoubleChannel_info(device, deviceChannels.info_array[i]);
			}
			if (strcmp(deviceChannels.info_array[i].name, "O1") == 0)
			{
				O1Signal = create_EegDoubleChannel_info(device, deviceChannels.info_array[i]);
			}
			if (strcmp(deviceChannels.info_array[i].name, "O2") == 0)
			{
				O2Signal = create_EegDoubleChannel_info(device, deviceChannels.info_array[i]);
			}
		}
	}
	free_ChannelInfoArray(deviceChannels);
}
    

As you can see, searching for channel info is mandatory for any channel. The difference is in the way we store received data. Here we don't need callbacks for data receiving (though we could set channel buffer length callback), we just create EEG channel buffers for specified channel info structures. Now we need to declare that channel buffers. Add the following include directive to your main.c file.

    
#include "c_eeg_channels.h"
    

(WARNING: The current version 1.6.5 has an error in the latter header file. The compiler would argue about line 29, in this case, remove ": unsigned int" from that line after the "typedef enum _SourceChannel" and before "{")

Declare 4 channel buffers at the beginning of the file and also add a macro definition for maximum samples count to display.

    
#define SIGNAL_SAMPLES_COUNT 20

EegDoubleChannel* T3Signal = NULL;
EegDoubleChannel* T4Signal = NULL;
EegDoubleChannel* O1Signal = NULL;
EegDoubleChannel* O2Signal = NULL;
    

Now we are ready to receive EEG data, so we need to control data length in buffers. Add the following code after channel-info loop.

    
resultCode = device_execute(device, CommandStartSignal);
if (resultCode != SDK_NO_ERROR)
{
	char errorMsg[1024];
	sdk_last_error_msg(errorMsg, 1024);
	printf("Cannot execute StartSignal command: %s\n", errorMsg);
	return;
}

size_t t3Length = 0;
size_t t4Length = 0;
size_t o1Length = 0;
size_t o2Length = 0;

do
{
	AnyChannel_get_total_length(T3Signal, &t3Length);
	AnyChannel_get_total_length(T4Signal, &t4Length);
	AnyChannel_get_total_length(O1Signal, &o1Length);
	AnyChannel_get_total_length(O2Signal, &o2Length);
} while (t3Length < SIGNAL_SAMPLES_COUNT
		|| t4Length < SIGNAL_SAMPLES_COUNT
		|| o1Length < SIGNAL_SAMPLES_COUNT
		|| o2Length < SIGNAL_SAMPLES_COUNT);

resultCode = device_execute(device, CommandStopSignal);
if (resultCode != SDK_NO_ERROR)
{
	char errorMsg[1024];
	sdk_last_error_msg(errorMsg, 1024);
	printf("Cannot execute StartResist command: %s\n", errorMsg);
	return;
}
    

As you can see we start the signal before the length check and stop it after as it was done previously for the battery and the resistance. The difference is in the way we check the length. We use AnyChannel_get_total_length function to get the length of the buffer. This function could be used for any channel buffer from the SDK collection. Thus, we get lengths of all 4 channels and compare them to our defined threshold. After the amount of data in buffers is sufficient we stop signal receiving.

Now we need to retrieve data from channel buffers to display them. For that purpose the SDK provides the DoubleChannel_read_data function.

    
double t3SignalBuffer[SIGNAL_SAMPLES_COUNT];
double t4SignalBuffer[SIGNAL_SAMPLES_COUNT];
double o1SignalBuffer[SIGNAL_SAMPLES_COUNT];
double o2SignalBuffer[SIGNAL_SAMPLES_COUNT];

DoubleChannel_read_data(T3Signal, 0, SIGNAL_SAMPLES_COUNT, t3SignalBuffer, SIGNAL_SAMPLES_COUNT, &t3Length);
DoubleChannel_read_data(T4Signal, 0, SIGNAL_SAMPLES_COUNT, t4SignalBuffer, SIGNAL_SAMPLES_COUNT, &t4Length);
DoubleChannel_read_data(O1Signal, 0, SIGNAL_SAMPLES_COUNT, o1SignalBuffer, SIGNAL_SAMPLES_COUNT, &o1Length);
DoubleChannel_read_data(O2Signal, 0, SIGNAL_SAMPLES_COUNT, o2SignalBuffer, SIGNAL_SAMPLES_COUNT, &o2Length);
    

We use temporary buffers to get EEG data from channel buffers. We must pass the lengths of that temporary buffers to read function for safety. We also must provide pointer to a size_t variable as the last parameter. This variable will contain actual data length copied to the temporary buffer. In our case, we pass previously used length variables. It would be a good practice to check a result code of the read function.

Now we could print signal data. After that, channel buffers must be deleted.

    
printf("%d signal samples are received.\n", SIGNAL_SAMPLES_COUNT);
printf("     T3:\t     T4:\t     O1:\t     O2:\n");
for (size_t i = 0; i < SIGNAL_SAMPLES_COUNT; ++i)
{
	printf("%.2fuV\t%.2fuV\t%.2fuV\t%.2fuV\n", 
		t3SignalBuffer[i]*1e6,
		t4SignalBuffer[i]*1e6,
		o1SignalBuffer[i]*1e6,
		o2SignalBuffer[i]*1e6);
}

AnyChannel_delete(T3Signal);
T3Signal = NULL;
AnyChannel_delete(T4Signal);
T4Signal = NULL;
AnyChannel_delete(O1Signal);
O1Signal = NULL;
AnyChannel_delete(O2Signal);
O2Signal = NULL;