Jump to content

Programming Reference:SignalSharingClientLibDemo

From BCI2000 Wiki
Revision as of 16:00, 12 March 2026 by Mellinger (talk | contribs) (Created page with "==Location== <tt>src/core/SignalProcessing/SignalSharingDemo/CppLibraryClient</tt> ==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'' com...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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.

#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, &parameterLines, &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

ShareTransmissionFilter

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