Jump to content

Programming Reference:SignalSharingDemoClient C++ App: Difference between revisions

From BCI2000 Wiki
Mellinger (talk | contribs)
Mellinger (talk | contribs)
 
(38 intermediate revisions by 2 users not shown)
Line 1: Line 1:
==Location==
==Location==
<tt>src/contrib/SignalProcessing/SignalSharingDemo</tt>
<tt>src/core/SignalProcessing/SignalSharingDemo</tt>


==Synopsis==
==Synopsis==
The ''SignalSharingDemo'' signal processing module demonstrates how to share signal data with an external application, using shared memory.
The ''SignalSharingDemo'' client demonstrates how to receive signal data from BCI2000, using shared memory.
SignalSharing allows to tap into BCI2000 processing by receiving any filter output signal through a combination of a TCP connection, and shared memory.


==Inheritance==
For clarity, this page describes a simplified version of the ''SignalSharingDemo'' client, receiving only signal data, and not using the back channel to send back signal and state data. The full version in SVN shows how to use the full possibilities available to a SignalSharing client.
The ''SignalSharingDemoFilter'' signal processing filter derives from ''GenericFilter''.


==Function==
==Function==
The ''SignalSharingDemoFilter'' shares its input signal through a <tt>GenericSignal</tt> object which has been linked to a shared memory block using <tt>GenericSignal::ShareAcrossModules()</tt>. A dedicated thread waits for signal updates, and sends signal data out to a separate application waiting on a TCP/IP connection.
The ''SignalSharing'' component in BCI2000 shares its input signal through a <tt>GenericSignal</tt> object which has been linked to a shared memory block using <tt>GenericSignal::ShareAcrossModules()</tt>. 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.
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.
Line 20: Line 20:
</gallery>
</gallery>


==Filter Code==
==Client application code==
===Declaration of internal variables===
The client application uses parts of the BCI2000 framework to efficiently receive and decode BCI2000 messages.
<pre>
To include it into your own project, use
#include "Thread.h"
utils_include(Frameworks/Core)
#include "WaitableEvent.h"
in your CMake file if it is part of the BCI2000 project.
#include "Sockets.h"
#include "Streambuf.h"
 
struct SignalSharingDemoFilter::Private
{
  // A socket for a connection to the client application.
  ClientTCPSocket mSocket;
  // A signal to be shared.
  GenericSignal mSharedSignal;
  // An event to notify the sender thread when new signal data is available.
  WaitableEvent mSignalAvailable;
 
  // An object to wrap a call to SenderThreadFunc().
  MemberCall<void(Private*)> mSenderThreadCall;
  // The sender thread.
  Thread mSenderThread;
 
  Private();
  void SenderThreadFunc();
};
</pre>
 
===Initialization of internal variables===
<pre>
SignalSharingDemoFilter::Private::Private()
: mSenderThreadCall(&Private::SenderThreadFunc, this),
  mSenderThread(&mSenderThreadCall, "Signal Sharing Demo Sender"),
  mSignalAvailable(true)
{
  mSocket.SetTCPNodelay(true);
  mSocket.SetFlushAfterWrite(true);
}
</pre>
 
===Sender thread===
The sender thread waits for the "signal available" event to be triggered, and then calls <tt>Serialize()</tt> on the shared signal in order to notify the client.
<pre>
void
SignalSharingDemoFilter::Private::SenderThreadFunc()
{
  if(mSocket.Connected())
  {
    UnbufferedIO buf;
    buf.SetOutput(&mSocket.Output());
    std::ostream clientConnection(&buf);
    GenericSignal().Serialize(clientConnection); // empty signal to indicate beginning of data
    while(mSignalAvailable.Wait()) // will return false when Thread::Terminate() has been called
    {
      if(!mSharedSignal.Serialize(clientConnection)) // will transmit memory reference, or signal, depending on whether ShareAcrossModules() has been called
      {
        bciwarn << "Could not send signal, giving up";
        mSenderThread.Terminate();
      }
    }
    GenericSignal().Serialize(clientConnection); // empty signal to indicate end of data
  }
}
</pre>
 
===Filter <tt>Initialize()</tt> function===
<pre>
void
SignalSharingDemoFilter::Initialize(const SignalProperties& Input, const SignalProperties& Output)
{
  std::string addr = Parameter("SignalSharingDemoClientAddress");
  if(!addr.empty())
  {
    p->mSocket.Open(addr);
    p->mSharedSignal = GenericSignal(Input);
    if(!p->mSocket.Connected())
    {
      bciwarn << "Cannot connect to " << addr;
    }
    else if(p->mSocket.Connected() == Socket::local)
    { // only transmit memory reference when connected locally
      p->mSharedSignal.ShareAcrossModules();
      bciwarn << "Locally connected to " << addr << ", using shared memory";
    } // otherwise, full data will be transmitted over network (slower)
    else
    {
      bciwarn << "Remotely connected to " << addr << ", transmitting data through network";
    }
  }
}
</pre>
 
===Filter <tt>StartRun()</tt> and <tt>StopRun()</tt> functions===
<tt>StartRun()</tt> and <tt>StopRun()</tt> start resp. stop the sender thread.
<pre>
void
SignalSharingDemoFilter::StartRun()
{
  p->mSenderThread.Start();
}
 
void
SignalSharingDemoFilter::StopRun()
{
  p->mSenderThread.Terminate();
}
</pre>


===Filter <tt>Process()</tt> function===
Otherwise, you might to have to create code for dealing with BCI2000 messages yourself (message format definitions may be found
The <tt>Process()</tt> function calls <tt>GenericSignal::AssignValues()</tt> to assign the shared signal's entries without affecting its shared memory representation. Then, it sets the "signal available" event to the signaled state.
[[Technical_Reference:BCI2000_Messages#Descriptor_Supplement=3:_Signal_Properties|here]] and
<pre>
[[Technical_Reference:BCI2000_Messages#Descriptor_Supplement=1:_Signal_Data|here]]).
void
SignalSharingDemoFilter::Process(const GenericSignal& Input, GenericSignal& Output)
{
  Output = Input;
  p->mSharedSignal.AssignValues(Input);
  p->mSignalAvailable.Set();
}
</pre>


==Client application code==
===Declaration of internal variables===
===Declaration of internal variables===
<pre>
<syntaxhighlight lang="cpp">
#include "SignalSharingDemoWidget.h"
#include "SignalSharingDemoWidget.h"


Line 149: Line 39:
#include "Thread.h"
#include "Thread.h"
#include "GenericSignal.h"
#include "GenericSignal.h"
#include "ParamList.h"
#include "Synchronized.h"
#include "Synchronized.h"
#include "Runnable.h"
#include "Runnable.h"
Line 163: Line 54:
   Synchronized<bool> mConnected;
   Synchronized<bool> mConnected;
   SynchronizedObject<GenericSignal> mpSignal;
   SynchronizedObject<GenericSignal> mpSignal;
  SynchronizedObject<ParamList> mpParameters;


   Thread mThread;
   Thread mThread;
Line 170: Line 62:
   Private();
   Private();
};
};
</pre>
</syntaxhighlight>


===Initialization of internal variables===
===Initialization of internal variables===
<pre>
<syntaxhighlight lang="cpp">
SignalSharingDemoWidget::Private::Private()
SignalSharingDemoWidget::Private::Private()
: mThreadCall(&Private::ThreadFunc, this),
: mThreadCall(&Private::ThreadFunc, this),
Line 182: Line 74:
     mSignalColors[i].setHsvF(i*1.0/mSignalColors.size(), 1, 0.9);
     mSignalColors[i].setHsvF(i*1.0/mSignalColors.size(), 1, 0.9);
}
}
</pre>
</syntaxhighlight>


===Receiving thread function===
===Receiving thread function===
<pre>void
By inheriting from the <tt>MessageChannel</tt> class, the <tt>Private</tt> object will receive dispatched
SignalSharingDemoWidget::Private::ThreadFunc()
messages through overridden functions:
 
<syntaxhighlight lang="cpp">struct SignalSharingDemoWidget::Private : MessageChannel
{
    ...
 
    // MessageChannel overrides
    bool OnVisSignalProperties(std::istream&) override;
    bool OnVisSignal(std::istream&) override;
    bool OnParam(std::istream&) override;
};
 
void SignalSharingDemoWidget::Private::ThreadFunc()
{
{
  while(!mThread.Terminating())
    while (!mThread.Terminating())
  { // wait for a connection
    { // wait for a connection
    while(mListeningSocket.Input().Wait())
        while (mListeningSocket.Input().Wait())
    { // accept connection
        { // accept pending connection
      ClientTCPSocket clientSocket;
            ClientTCPSocket clientSocket;
      if(mListeningSocket.WaitForAccept(clientSocket, 0))
            if (mListeningSocket.WaitForAccept(clientSocket, 0))
      {
            {
        mConnected = true;
                mConnected = true;
        Invalidate(); // calls the widget's update() method
                Invalidate();
        UnbufferedIO buf;
                mBuffer.SetInput(&clientSocket.Input());
        buf.SetInput(&clientSocket.Input());
                MessageBuffer::ClearIOState();
        std::istream stream(&buf);
                while (clientSocket.Input().Wait()) // will be interrupted by Thread::Terminate()
        while(stream && clientSocket.Input().Wait()) // will be interrupted by Thread::Terminate()
                {
        {
                    MessageChannel::HandleMessage(); // will dispatch to our overridden functions
          mpSignal.Mutable()->Unserialize(stream); // get the current signal content
                                                    // as appropriate
          Invalidate(); // call update() on the widget
                }
                *mpSignal.Mutable() = GenericSignal();
                mConnected = false;
                Invalidate();
            }
         }
         }
        *mpSignal.Mutable() = GenericSignal();
        mConnected = false;
        Invalidate();
      }
     }
     }
  }
}
}
</pre>
 
bool SignalSharingDemoWidget::Private::OnVisSignalProperties(std::istream& is)
{
    VisSignalProperties properties; // signal properties are wrapped into VisSignalProperties
    properties.Unserialize(is);
    // The only information we need from signal properties is the sampling rate
    // because the signal's dimensions will be transported by signal messages as well.
    double sampleDuration = properties.SignalProperties().ElementUnit().RawToPhysicalValue(1);
    mSamplingRate = 1.0 / sampleDuration;
    return true; // indicate we read our data from the stream
}
 
bool SignalSharingDemoWidget::Private::OnVisSignal(std::istream& is)
{
    VisSignal visSignal; // signals are wrapped into VisSignal messages
    visSignal.Unserialize(is);
    mpSignal.Mutable()->AssignValues(visSignal.Signal()); // get current signal content
    Invalidate(); // request window update
    return true; // indicate we read our data from the stream
}
 
bool SignalSharingDemoWidget::Private::OnParam(std::istream& is)
{
    Param param;
    param.Unserialize(is);
    mpParamList.Mutable()->Add(param);
    Invalidate(); // request window update
    return true; // indicate we read our data from the stream
}
</syntaxhighlight>
 
===The widget's paintEvent()===
===The widget's paintEvent()===
<pre>
<syntaxhighlight lang="cpp">
void
void
SignalSharingDemoWidget::paintEvent(QPaintEvent* ev)
SignalSharingDemoWidget::paintEvent(QPaintEvent* ev)
Line 236: Line 169:
   }
   }
}
}
</pre>
</syntaxhighlight>


==Parameters==
==Parameters==
===SignalSharingDemoClientAddress===
===ShareTransmissionFilter===
IP address and port number of the client application.
IP address and port number of the client application. The client's default address is <tt>localhost:1879</tt> 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==
==See also==
[[Programming Reference:GenericSignal Class]]
[[User Reference:SignalSharing]], [[Programming Reference:GenericSignal Class]],  [[Programming Reference:SignalSharing Python Demo]], [[Programming Reference:SignalSharingClientLibDemo]]
 


[[Category:Framework API]][[Category:Howto]][[Category:Development]]
[[Category:Framework API]][[Category:Howto]][[Category:Development]]

Latest revision as of 15:36, 12 March 2026

Location

src/core/SignalProcessing/SignalSharingDemo

Synopsis

The SignalSharingDemo client demonstrates how to receive signal data from BCI2000, using shared memory. SignalSharing allows to tap into BCI2000 processing by receiving any filter output signal through a combination of a TCP connection, and shared memory.

For clarity, this page describes a simplified version of the SignalSharingDemo client, receiving only signal data, and not using the back channel to send back signal and state data. The full version in SVN shows how to use the full possibilities available to a SignalSharing client.

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 client application visualizes signal data by plotting normalized signals on a circle.

Client application code

The client application uses parts of the BCI2000 framework to efficiently receive and decode BCI2000 messages. To include it into your own project, use

utils_include(Frameworks/Core)

in your CMake file if it is part of the BCI2000 project.

Otherwise, you might to have to create code for dealing with BCI2000 messages yourself (message format definitions may be found here and here).

Declaration of internal variables

#include "SignalSharingDemoWidget.h"

#include "Sockets.h"
#include "StringUtils.h"
#include "Streambuf.h"
#include "Thread.h"
#include "GenericSignal.h"
#include "ParamList.h"
#include "Synchronized.h"
#include "Runnable.h"

#include <QPaintEvent>
#include <QPainter>

struct SignalSharingDemoWidget::Private
{
  SignalSharingDemoWidget* mpSelf;
  std::vector<QColor> mSignalColors;

  ServerTCPSocket mListeningSocket;
  Synchronized<bool> mConnected;
  SynchronizedObject<GenericSignal> mpSignal;
  SynchronizedObject<ParamList> mpParameters;

  Thread mThread;
  void ThreadFunc();
  MemberCall<void(Private*)> mThreadCall;
  void Invalidate();
  Private();
};

Initialization of internal variables

SignalSharingDemoWidget::Private::Private()
: mThreadCall(&Private::ThreadFunc, this),
  mThread(&mThreadCall, "SignalSharingDemoWidget listening/receiving thread")
{
  mSignalColors.resize(8);
  for(int i = 0; i < mSignalColors.size(); ++i)
    mSignalColors[i].setHsvF(i*1.0/mSignalColors.size(), 1, 0.9);
}

Receiving thread function

By inheriting from the MessageChannel class, the Private object will receive dispatched messages through overridden functions:

struct SignalSharingDemoWidget::Private : MessageChannel
{
    ...

    // MessageChannel overrides
    bool OnVisSignalProperties(std::istream&) override;
    bool OnVisSignal(std::istream&) override;
    bool OnParam(std::istream&) override;
};

void SignalSharingDemoWidget::Private::ThreadFunc()
{
    while (!mThread.Terminating())
    { // wait for a connection
        while (mListeningSocket.Input().Wait())
        { // accept pending connection
            ClientTCPSocket clientSocket;
            if (mListeningSocket.WaitForAccept(clientSocket, 0))
            {
                mConnected = true;
                Invalidate();
                mBuffer.SetInput(&clientSocket.Input());
                MessageBuffer::ClearIOState();
                while (clientSocket.Input().Wait()) // will be interrupted by Thread::Terminate()
                {
                    MessageChannel::HandleMessage(); // will dispatch to our overridden functions
                                                     // as appropriate
                }
                *mpSignal.Mutable() = GenericSignal();
                mConnected = false;
                Invalidate();
            }
        }
    }
}

bool SignalSharingDemoWidget::Private::OnVisSignalProperties(std::istream& is)
{
    VisSignalProperties properties; // signal properties are wrapped into VisSignalProperties
    properties.Unserialize(is);
    // The only information we need from signal properties is the sampling rate
    // because the signal's dimensions will be transported by signal messages as well.
    double sampleDuration = properties.SignalProperties().ElementUnit().RawToPhysicalValue(1);
    mSamplingRate = 1.0 / sampleDuration;
    return true; // indicate we read our data from the stream
}

bool SignalSharingDemoWidget::Private::OnVisSignal(std::istream& is)
{
    VisSignal visSignal; // signals are wrapped into VisSignal messages
    visSignal.Unserialize(is);
    mpSignal.Mutable()->AssignValues(visSignal.Signal()); // get current signal content
    Invalidate(); // request window update
    return true; // indicate we read our data from the stream
}

bool SignalSharingDemoWidget::Private::OnParam(std::istream& is)
{
    Param param;
    param.Unserialize(is);
    mpParamList.Mutable()->Add(param);
    Invalidate(); // request window update
    return true; // indicate we read our data from the stream
}

The widget's paintEvent()

void
SignalSharingDemoWidget::paintEvent(QPaintEvent* ev)
{
  ev->accept();
  WithLocked(pSignal = p->mpSignal.Const()) // lock the signal while reading from it
  {
    if(pSignal->Empty())
    {
      QPainter painter(this);
      painter.fillRect(ev->rect(), Qt::gray);
      painter.setPen(Qt::white);
      painter.drawText(geometry(), Qt::AlignCenter, 
        p->mConnected ? "Waiting for signal ..." : "Waiting for connection ...");
    }
    else
    {
       // draw some visualization into the widget
       ...
    }
  }
}

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