Programming Reference:SignalSharingClientLibDemo
Location
src/core/SignalProcessing/SignalSharingDemo/CppLibraryClient
Synopsis
The SignalSharingClientLibDemo client demonstrates how to receive signal data from BCI2000, using the SignalSharingClient Library. SignalSharing allows to tap into BCI2000 processing by receiving any filter output signal through a combination of a TCP connection, and shared memory.
Function
The SignalSharing component in BCI2000 shares its input signal through a GenericSignal object which has been linked to a shared memory block using GenericSignal::ShareAcrossModules(). A dedicated thread waits for signal updates, and sends signal data out to a separate application waiting on a TCP/IP connection.
When the client application is running on a separate machine, full signal data are sent over the network. When the client is running on the same machine, only a reference to a shared memory block is sent. On the application side, unserializing the signal will transparently bind it to the shared memory block if available.
The simple client application receives BCI2000 parameters and data blocks, and writes them into a parameter file, and a signal file.
Client application code
The client application uses the SignalSharingClient Library to receive data from BCI2000. This way, it does not have to deal with BCI2000 messages but will see the raw data directly. The SignalSharingClient Library is contained in the file prog/SignalSharingClientLib.dll for Windows, and carrying the appropriate file extension for shared libraries on other platforms. For convenience, header file and .lib file (if any) are also located at prog.
The simple example consists of a single source file.
Note how the callback functions call the appropriate SSC_Lock*() function, followed with processing of the data, and calling SSC_Release*() as soon as possible. Also note that care is taken to call SSC_Release*() only if the call to SSC_Lock*() was successful.
Similarly, note how main() calls SSC_DeleteConnector() no matter whether an exception occurred or not.
To avoid clutter, and throw an exception when an unexpected error has occurred, we define a SSC_SucceedOrThrow class that can be used in place of a plain numeric variable to hold a function's return value. Note how exceptions are caught in the main() function before calling the SSC_DeleteConnector() function. This is possible because SSC_CreateConnector() guarantees to null the resulting handle if any error occurs. This implies that we can call SSC_DeleteConnector() without using a dangling or otherwise invalid pointer.
#include "SignalSharingClientLib.h"
#include <iostream>
#include <fstream>
#include <string>
#include <mutex>
static std::string SSC_ResultToString(SSC_RESULT result)
{
switch (result) {
case SSC_SUCCESS:
return "Success";
case SSC_INVALID_ARG:
return "Invalid argument";
case SSC_CANNOT_LISTEN:
return "Cannot listen on given address";
case SSC_CANNOT_FULFILL_REQUEST:
return "Cannot fulfill request at this time";
}
return "Unknown error";
}
// Translate an error code into an exception
struct SSC_SucceedOrThrow
{
SSC_SucceedOrThrow(SSC_RESULT result)
{
if (result != SSC_SUCCESS) {
throw std::runtime_error(SSC_ResultToString(result));
}
}
};
// The data we need inside the callback functions
struct ConnectorData
{
SSC_HANDLE h = nullptr;
std::ofstream paramfile, datafile;
std::mutex mutex;
};
// Callback function for handling parameters
static void HandleParameters(void* pData)
{
std::cout << "Received parameters ..." << std::endl;
auto pConnectorData = static_cast<ConnectorData*>(pData);
const char* const* parameterLines = nullptr;
int count = 0;
if (SSC_SUCCESS == ::SSC_LockParameters(pConnectorData->h, ¶meterLines, &count)) {
std::unique_lock lock(pConnectorData->mutex);
for (int i = 0; i < count; ++i) {
pConnectorData->paramfile << parameterLines[i] << "\n";
}
lock.unlock();
::SSC_ReleaseParameters(pConnectorData->h);
}
}
// Callback function for handling signals
static void HandleSignal(void* pData)
{
std::cout << "Received signal ..." << std::endl;
auto pConnectorData = static_cast<ConnectorData*>(pData);
union { const double* d; const char* c; } pSignal = { nullptr };
int channels = 0, samples = 0;
if (SSC_SUCCESS == ::SSC_LockSignal(pConnectorData->h, &pSignal.d, &channels, &samples, nullptr)) {
std::unique_lock lock(pConnectorData->mutex);
// Transpose data so we have a continuous stream of samples across channels
for (int sample = 0; sample < samples; ++sample) {
for (int ch = 0; ch < channels; ++ch) {
// Data comes in channel-major format
int idx = ch * samples + sample;
pConnectorData->datafile.write(pSignal.c + idx * sizeof(double), sizeof(double));
}
}
lock.unlock();
::SSC_ReleaseSignal(pConnectorData->h);
}
}
int main(int argc, char **argv)
{
std::string address = "localhost:1879";
if (argc > 1)
address = argv[1];
std::string paramfile = "sscparams.prm";
if (argc > 2)
paramfile = argv[2];
std::string datafile = "sscdata.raw";
if (argc > 3)
datafile = argv[3];
// The main function's return value
int result = 0;
ConnectorData data;
// Callback functions are called from a separate thread, so let's lock the mutex
// (not really necessary here because the thread will first start inside SSC_CreateConnector() below)
std::unique_lock lock(data.mutex);
data.paramfile.open(paramfile);
data.datafile.open(datafile, std::ios::binary | std::ios::out);
lock.unlock();
try {
SSC_SucceedOrThrow result = ::SSC_CreateConnector(address.c_str(), &data.h);
result = ::SSC_RegisterParametersCallback(data.h, &HandleParameters, &data);
result = ::SSC_RegisterSignalCallback(data.h, &HandleSignal, &data);
// Callback functions are called from a separate thread, so just wait for user input
std::cout << "Press <Enter> to quit" << std::endl;
std::string ignored;
std::getline(std::cin, ignored);
}
catch (const std::exception& exc) {
std::cerr << "Error: " << exc.what() << std::endl;
result = -1;
}
::SSC_DeleteConnector(data.h);
return result;
}
Parameters
IP address and port number of the client application. The client's default address is localhost:1879 but may be changed on the client's command line.
The example uses the ShareTransmissionFilter parameter but any other filter's Share<FilterName> parameter under the SignalSharing tab will work as well to visualize the chosen filter's output signal.
See also
User Reference:SignalSharing, Programming Reference:GenericSignal Class, Programming Reference:SignalSharing Python Demo, Programming Reference:SignalSharingClientLibDemo