BrainBit foe developers Visit website
Home
SDK
Device

Quickstart guide C/C++

 

This guide describes the first steps required to start working with the Brainbit device and is intended to provide one with basic knowledge about the functionality of the device. To get more detailed information about specific channels, see respective articles. In this text, we will provide you with the following: technical requirements for your system to be able to work with the Brainbit device, choice of the correct form of SDK library, downloading and installation process, configuring a project for use of SDK library, sample program for configuring and control of the device. 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

 

Platform requirements

There are several requirements for use of the Brainbit device on Windows, they are listed below. The first two, which related to the Windows version and hardware type are crucial. Since the Brainbit device is working through Bluetooth connection and uses Bluetooth Low Energy protocol there is a strong limitation on a version of the operating system, because some mandatory functionality and API functions were introduced in a specified below version. Another noticeable requirement is that Bluetooth chipset must conform to the 4.1 BT-standard and in case of laptops to be and an external USB device if the latter is possible. This rule is conceived from the fact that external devices have a better quality of BT antenna and could provide one with sound and reliable connection and consistent data flow in different circumstances (high data rate, radio-noisy environment, etc.). CMake is only used in examples to demonstrate which libraries must be linked against various types of applications, you may choose other tools to organize your project, for example, VS projects.

OS: Windows 10 1803 or higher is required.

Hardware: Bluetooth v4.1, External USB Module with Chipset CSR8510 is recommended.

Software: Any C/C++ IDE, C11 compatible compiler, CMake (Optional: only needed for supplied examples)

Language standard: C11 (or C++17 in some examples)

CMake version: 3.14 or higher.  Download link

 

Downloading and extracting libraries

To begin with, you need to download zip-archive containing all necessary files. The download page provides additional information about the archive.

Firstly, extract the content of the archive to any convenient folder. You will need two subfolders from the package: include and windows.

The first contains all needed header files and the latter - DLL libraries for x86 and x86_64 architectures. You will need paths to both of them during project configuration. You might also want to store them just inside your project directory.

 

Configuring a project

We will use CMake 3.14 and Visual Studio 2019 to demonstrate the process of building an application utilizing Brainbit SDK functions. 

On the first step you need to create at least one source code file, for example, main.cand one CMakeLists.txt file. In this guide, we place these files into the same directory as SDK folders.

Now you can open this new empty project in Visual Studio. Run VS and in the start-up window choose Continue without code option in the right bottom corner.

Go to File>>Open>>CMake and navigate in the appeared window to CMake file you have created.

You will see all the content of the project directory and two empty files.

Copy and paste into CMakeLists.txt the following code. 

    
cmake_minimum_required(VERSION 3.14)

project(MyBrainbitProj LANGUAGES C)

add_executable(MyBrainbitProj main.c)
    

This is an initial configuration of your project. These lines mean that you're creating one executable MyBrainbitProj.exe from one source file main.c and using C programming language and CMake of version no lower than 3.14.

The next step is to set a linkage with the appropriate SDK library. Default CMake configuration in Visual Studio is set to use a 64-bit MSVC compiler, so we would use a 64-bit SDK library. All we need is to show CMake where our include files and libraries are. Copy and paste into CMakeLists.txt the following code after add_executable line.

    
target_include_directories(MyBrainbitProj PRIVATE include)

find_library(SDK_LIB NAMES neurosdk-x64d PATHS windows)
target_link_libraries(MyBrainbitProj PRIVATE ${SDK_LIB})

find_file(SDK_DLL NAMES neurosdk-x64d.dll PATHS windows)
add_custom_command(TARGET MyBrainbitProj POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${SDK_DLL} $<TARGET_FILE_DIR:MyBrainbitProj>)
    

The first line shows CMake where header files are (relative path include after PRIVATE keyword; if you store header files somewhere else, provide this command with the full path to include directory). PRIVATE keyword means that we do not want to expose library headers to the output directory. The second line points out where to look for libraries. To implicitly link executable against DLL linker needs export LIB file, so on link-time, only LIB file is required. The libraries' search path is specified in find_library command after the PATHS keyword.

The next two lines are mandatory because on Windows C-linkers links executable against LIB file, but at runtime executable needs DLL library to be available. To provide executable with DLL at runtime we must copy the latter to an output directory. Another option is to add a path to a DLL directory to the PATH environment variable.

Now the project could be generated. To do this, go to Project>>Generate Cache for...

 

Example program

After that step, we could proceed with the main.c file.

Open file in VS editor and add the following code: 

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

int main()
{
	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 -1;
	}

	enumerator_delete(enumerator);
	return 0;
}
    

The first two include directives give access to SDK functions for device search and error handling, the third one, a standard C-header is for text output. 

In the main function, you could see the line in which a DeviceEnumerator object is created. The function create_device_enumerator must be provided with enum value DeviceTypeBrainbit. In case of a successful creation of the enumerator object, the enumerator variable will store a pointer to this object. Successfully created enumerator object must be deleted with enumerator_delete function after all work is done. Pass the enumerator pointer to it to free all resources. In case of errors, the enumerator pointer will be null. A text message describing the error could be obtained with sdk_last_error_msg function. Prepare quite big char buffer, to store full error message. The error message string is NULL-terminated.

At this stage, this program could be built and run, it must start and finish without any errors, but no input will be produced either. That's because no information was requested from the device enumerator. Let's add some code to get it.

Since the device enumerator is created, it is continuously searching for new nearby devices until it is deleted. To get information about currently available devices enumerator_get_device_list function could be used. It is better to call this function after notification on changed device list. To receive these notifications enumerator_set_device_list_changed_callback function could be used. Copy and paste the following code into the main function, after enumerator pointer value check, but before enumerator deletion:

    
//store listener as long as you want to receive notifications
	DeviceListListener listListener;	
	int resultCode = enumerator_set_device_list_changed_callback(enumerator, &on_device_list_changed, &listListener, 0);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Subscription error: %s\n", errorMsg);
		enumerator_delete(enumerator);
		return -1;
	}

	//wait for any input
	getchar();

	enumerator_unsubscribe_device_list_changed(listListener);
    

In this code you could see the creation of a special object DeviceListListener, it is required to subscribe to device notifications. The last line contains enumerator_unsubscribe_device_list_changed functions called on that object. This means that you must free the device list listener object when you do not want to receive notifications. To subscribe notifications enumerator_set_device_list_changed_callback function is used. Its first parameter is an enumerator object pointer, second parameter - a callback function with signature void(DeviceEnumerator*, void*), third - pointer to previously created listener object, and the last is a pointer to any user data, that will be passed to a callback function as the second parameter. Error handling is performed similarly to error handling for enumerator creation, but now the result code must be checked instead of a pointer. The result code has type int and could have only 3 different values described in sdk_error.h file. The most common use case is presented in the if statement - result code could be checked for equality to SDK_NO_ERROR to be sure that no errors occurred. There is also getchar function call is that code, it is used to control a lifetime of a program because after subscription we must be able to wait for notifications any desireable time.

A sample code of on_device_list_changed function is provided below:

    
void on_device_list_changed(DeviceEnumerator* enumerator, void* user_data)
{
	DeviceInfoArray deviceInfoArray;
	int resultCode = enumerator_get_device_list(enumerator, &deviceInfoArray);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device list read error: %s\n", errorMsg);
		return;
	}

	free_DeviceInfoArray(deviceInfoArray);
}
    

This function is responsible for reading nearby device information each time the device list is updated. To read that information enumerator_get_device_list function should be used. It takes only two parameters - pointer to enumerator object and pointer to a DeviceInfoArray structure that will hold the result. On the first line of the callback function, you could see a creation of that structure. On the next line, the device list is acquired. Handle errors in the same way as for subscription function. After work is done and device info data is no more required, the device info array object must be deleted using function free_DeviceInfoArray.

 

Reading device parameters

Now it's time to display some information about found devices. Copy and paste at the top of the main.c file the following function:

    
void on_device_found(DeviceEnumerator* enumerator, DeviceInfo device_info)
{
	Device* device = create_Device(enumerator, device_info);
	if (device == 0)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device is null: %s\n", errorMsg);
		return;
	}
	
	char deviceName[256];
	int resultCode = device_read_Name(device, deviceName, 256);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device read name error: %s\n", errorMsg);
		return;
	}

	char deviceAddress[256];
	resultCode = device_read_Address(device, deviceAddress, 256);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device read address error: %s\n", errorMsg);
		return;
	}

	char deviceSerialNumber[256];
	resultCode = device_read_SerialNumber(device, deviceSerialNumber, 256);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device read serial number error: %s\n", errorMsg);
		return;
	}
	
	DeviceState deviceState;
	resultCode = device_read_State(device, &deviceState);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device read state error: %s\n", errorMsg);
		return;
	}

	printf
	(
		"Name: %s, MAC: [%s], S/N: %s, State: %s \n",
		deviceName,
		deviceAddress,
		deviceSerialNumber,
		deviceState == DeviceStateConnected ? "Connected" : "Disconnected"
	);

	device_delete(device);
}
    

The first line of the function is responsible for the creation of a Device object, create_Device function takes two arguments - enumerator pointer and device information object received from the enumerator. The returned device pointer must be checked for NULL in the same way as in the enumerator creation process. The device object must be deleted via device_delete function after all work is done. After the device is created several parameters could be read. To read device parameters add cparams.h header files to include directives at the top of a source code file. Parameter read function common form is device_read_XXXX, where XXXX - is a parameter name. All parameter read functions return result code which should be checked as in case of other previously mentioned functions returning codes. All parameters available for reading are presented below:

  • Name - Name of the device (e.g. BrainBit).
  • State - Connection state of the device, could be CONNECTED or DISCONNECTED.
  • Address - 6-byte Bluetooth address of the device represented as NULL-terminated ANSI-string.
  • SerialNumber - Hexidecimal serial number of the device represented as NULL-terminated ANSI-string.
  • SamplingFrequency - Sampling frequency of the main signal module, could be changed for different purposes.
  • Gain - Input amplifier gain of the main signal module, could be changed for different purposes.
  • Offset - Data offset of ADC inputs, used to widen the dynamic range of a received signal, could be changed for different purposes.
  • FirmwareVersion - Indicates the version of device firmware.

To read any parameter device must be in the CONNECTED state, except the first four parameters (Name, State, Address, SerialNumber), that are available in any state. Not all parameters reside in every device simultaneously, device configuration depends on a type of the device purchased. 

To use on_device_found paste the following code into on_device_list_changed function before free_DeviceInfoArray call:

    
for (size_t i = 0; i < deviceInfoArray.info_count; ++i) 
{
	on_device_found(enumerator, deviceInfoArray.info_array[i]);
}
    

Here on_device_found function is called for each found device record in device information array. When the operating system sees a new or a disappeared device or some changes in signal strength it notifies our application and we see new lines with the device name, address, and state appears.

 

Connecting to the device 

Most operations od the device could be performed only in a CONNECTED state. To connect to the device use device_connect which accepts only one parameter - pointer to the device. After the device is connected it is no longer visible for enumerator unless you disconnect or delete it. After connection all device features become available.

    
void connect_to_device(Device *device)
{
	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);
		return;
	}

	DeviceState deviceState; 
	do
	{
		device_read_State(device, &deviceState);
	}
	while (deviceState != DeviceStateConnected);
	
	FirmwareVersion firmwareVersion;
	resultCode = device_read_FirmwareVersion(device, &firmwareVersion);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Cannot read firmware version: %s\n", errorMsg);
		return;
	}

	printf("Firmware version: %d build %d\n", firmwareVersion.version, firmwareVersion.build);

	device_disconnect(device);
}
    

Device connection could be unstable due to bad electromagnetic environment, e.g. in case of presence of a big amount of different radio- and BT-devices. After call to device_connect function, you must check result code and then read device state until it becomes DeviceStateConnected.

After device_read_State gave you DeviceStateConnected value, you would perform other operations with the device. In the example, we read a version of device firmware. 

To use connect_to_device function add the following line into the on_device_found function before the device deletion.

    
connect_to_device(device);
    

 When the device is connected, its LED is constantly glowing. In a standby state, it makes short blinks.

Executing commands

In a connected state device also could execute 4 commands that control signal and resistance data streams. To execute command use device_execute function, which takes two parameters - pointer to a device and a command enum value CommandXXXX, where XXXX is for a command name.

The full list of commands is provided below:

  • StartSignal - starts EEG signal data flow.
  • StopSignal - stops EEG signal data flow.
  • StartResistance - starts resistance data flow.
  • StopResistance - stops resistance data flow.

To execute simple StartSignal command add the following code to connect_to_device function before a call to device_disconnect.

    
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;
}
    

The device_execute command also returns result code, so you are able to check whether the command was successfully executed. 

When the device is steaming data, its LED makes long blinks.

 

Project files content

Here you could find the whole content of main.c and CMakeLists.txt files.

Main.c

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

void connect_to_device(Device *device)
{
	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);
		return;
	}

	DeviceState deviceState; 
	do
	{
		device_read_State(device, &deviceState);
	}
	while (deviceState != DeviceStateConnected);
	
	FirmwareVersion firmwareVersion;
	resultCode = device_read_FirmwareVersion(device, &firmwareVersion);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Cannot read firmware version: %s\n", errorMsg);
		return;
	}

	printf("Firmware version: %d build %d\n", firmwareVersion.version, firmwareVersion.build);

	device_execute(device, CommandStartSignal);
		
	device_disconnect(device);
}

void on_device_found(DeviceEnumerator* enumerator, DeviceInfo device_info)
{
	Device* device = create_Device(enumerator, device_info);
	if (device == 0)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device is null: %s\n", errorMsg);
		return;
	}
	
	char deviceName[256];
	int resultCode = device_read_Name(device, deviceName, 256);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device read name error: %s\n", errorMsg);
		return;
	}

	char deviceAddress[256];
	resultCode = device_read_Address(device, deviceAddress, 256);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device read address error: %s\n", errorMsg);
		return;
	}

	char deviceSerialNumber[256];
	resultCode = device_read_SerialNumber(device, deviceSerialNumber, 256);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device read serial number error: %s\n", errorMsg);
		return;
	}
	
	DeviceState deviceState;
	resultCode = device_read_State(device, &deviceState);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device read state error: %s\n", errorMsg);
		return;
	}

	printf
	(
		"Name: %s, MAC: [%s], S/N: %s, State: %s \n",
		deviceName,
		deviceAddress,
		deviceSerialNumber,
		deviceState == DeviceStateConnected ? "Connected" : "Disconnected"
	);

	connect_to_device(device);

	device_delete(device);
}

void on_device_list_changed(DeviceEnumerator* enumerator, void* user_data)
{
	DeviceInfoArray deviceInfoArray;
	int resultCode = enumerator_get_device_list(enumerator, &deviceInfoArray);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Device list read error: %s\n", errorMsg);
		return;
	}

	for (size_t i = 0; i < deviceInfoArray.info_count; ++i)
	{
		on_device_found(enumerator, deviceInfoArray.info_array[i]);
	}

	free_DeviceInfoArray(deviceInfoArray);
}

int main()
{
	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 -1;
	}
	
	//store listener as long as you want to receive notifications
	DeviceListListener listListener;	
	int resultCode = enumerator_set_device_list_changed_callback(enumerator, &on_device_list_changed, &listListener, 0);
	if (resultCode != SDK_NO_ERROR)
	{
		char errorMsg[1024];
		sdk_last_error_msg(errorMsg, 1024);
		printf("Subscription error: %s\n", errorMsg);
		enumerator_delete(enumerator);
		return -1;
	}

	//wait for any input
	getchar();

	enumerator_unsubscribe_device_list_changed(listListener);
	enumerator_delete(enumerator);
	return 0;
}
    

 

CMakeLists.txt

    
cmake_minimum_required(VERSION 3.14)

project(MyBrainbitProj LANGUAGES C)

add_executable(MyBrainbitProj main.c)

target_include_directories(MyBrainbitProj PRIVATE include)

find_library(SDK_LIB NAMES neurosdk-x64d PATHS windows)
target_link_libraries(MyBrainbitProj PRIVATE ${SDK_LIB})

find_file(SDK_DLL NAMES neurosdk-x64d.dll PATHS windows)
add_custom_command(TARGET MyBrainbitProj POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${SDK_DLL} $<TARGET_FILE_DIR:MyBrainbitProj>)