Programming Reference:SignalSharing Python Demo

Location
src/core/SignalProcessing/SignalSharingDemo/PythonClientApp
Synopsis
The SignalSharing Python Demo demonstrates how to make a complex real-time visualization in Python using data parallelly collected in BCI2000. It makes use of the SignalSharing feature in BCI2000.
Function
The SignalSharing Python Demo creates a basic visualization in Python using the data collected in BCI2000. Since this is done outside of the BCI2000 processing loop, rendering visualizations can take as long as needed. Using Python also allows for the use of the numerous packages that are available to create complex figures.
This Demo has two parts, a source and a client. The source is the BCI2000 SignalProcessing Filter, and the client is the Python app that visualizes the data:
- SignalSharing Signal Processing Filter: This is the same filter as what is used for the SignalSharingDemo with the C++ application. Please refer to that page for details on the BCI2000 Filter aspect.
- Python visualization app: A simple Python script that connects with the port that is being used to synchronize the data transfer, and updates the visualization as data is being streamed.
Source vs Client
The BCI2000 data is sent according to the format specified in BCI2000 Messages Wiki page, as Descriptor=4: Visualization and Brain Signal Data Format, then Descriptor Supplement=1: Signal Data, then data type 2.
With this demo, BCI2000 and the Python script must be run on the same computer. The script expects the data stream to be sending the name of the shared memory, which will only happen if they are both on the same computer. It is possible to grab BCI2000 data from another computer, and is implemented in the C++ SignalSharing Demo. If on separate computers, the actual data points are being shared instead of the memory name. Implementing this would require a simple extension of this demo.
How to run
- Build BCI2000 as you would, make sure to check BUILD_DEMOS
- Make sure the SignalSharingDemo works first
- Navigate to src\core\SignalProcessing\SignalSharingDemo\PythonClientApp, where there is a batch file and a Python file. Copy the batch file to your BCI2000 batch folder
- Run the Python file. For example from the command line, navigate to the folder and run
python SignalSharingPythonDemo.py - Run the batch file, and press "Start Run" (it already sets the configuration, it won't work it you press it multiple times)
- If you change the Parameter SignalSharingDemoClientAddress, make sure to also change it in the Python script
Python Script
Here is the same code that is on the SVN (r7926)
#import libraries
import socket
from multiprocessing import shared_memory
import matplotlib.pyplot as plt
import numpy as np
#user input
HOST, PORT = "localhost", 1879
print("Waiting for BCI2000 on %s at port %i" %(HOST, PORT))
#initialize variables
CHANNELS = -1
ELEMENTS = -1
SPACE = 32 #ascii code for space
EMPTY = b''
lastEl = SPACE
setProps = False
gotMemoryName = False
chNames = []
myBuffer = [] #add stream to this buffer, items separated by space
figure, ax = plt.subplots(figsize=(10, 8))
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)
figure.set_facecolor((0,0,0,1))
ax.set_axis_off()
ax.set_frame_on(0)
figure.canvas.draw()
chEndIndex = 1
#attempt connection to specified port
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen(1)
conn, addr = s.accept()
with conn:
print('Connected by', addr)
try:
while True: #go until we manually stop program
while setProps is False: #get properties
#get stream
stream = conn.recv(128)
#split into names
names = stream.split(b' ')
#remove any empty characters
while(EMPTY in names):
names.remove(EMPTY)
#combine name if it is split up
if lastEl != SPACE and stream[0] != SPACE:
#last channel name was split apart
namePart = names.pop(0)
myBuffer[-1] += namePart
#save in case it was a space
lastEl = stream[-1]
myBuffer = np.append(myBuffer, names)
if len(myBuffer) > chEndIndex+1 and CHANNELS == -1:
# we have at least part of ch information
chID = myBuffer[1] # either start of channel names or number of channels
if chID==b'{': #start of channel names
while chEndIndex < len(myBuffer):
if myBuffer[chEndIndex] != b'}': #end of channel names
chEndIndex += 1
else:
chNames = myBuffer[2:chEndIndex]
CHANNELS = len(chNames) #we have all the channels
break
else:
CHANNELS = int(str(chID, encoding='utf-8'))
chNames = range(1,CHANNELS+1)
#chEndIndex -= 1
if CHANNELS == -1:
continue
#-----DONE WITH CHANNELS-----#
#element size is next
elIndex = chEndIndex+1
if elIndex+1 < len(myBuffer): #wait for extra element in case element number is split up
#got element number
el = myBuffer[elIndex]
elementString = str(el, encoding='utf-8')
ELEMENTS = int(elementString)
if CHANNELS == -1 or ELEMENTS == -1:
continue
#-----DONE WITH CHANNELS AND ELEMENTS-----#
#initialize variables once we have channels and elements
phi = np.zeros((CHANNELS, ELEMENTS))
bla = np.zeros((ELEMENTS,1))
lineArr = list(range(CHANNELS))
for i in range(0,CHANNELS):
lineArr[i], = ax.plot(bla, bla)
print("Properties: Channels: %i, Elements: %i" %(CHANNELS, ELEMENTS))
setProps= True
if not gotMemoryName:
#print(stream)
stream = conn.recv(1)
for b in stream:
if b==66: #signal type 2, 6th bit flipped (+64) for shared memory
#next 8 bytes are number of channels
chs = conn.recv(2)
if (int.from_bytes(chs, byteorder='little', signed=False) != CHANNELS):
conn.recv(1) #to make sure we don't get on a cycle
break
#next 8 bytes are number of samples
els = conn.recv(2)
if (int.from_bytes(els, byteorder='little', signed=False) != ELEMENTS):
conn.recv(1) #to make sure we don't get on a cycle
break
#WE ARE CERTAIN WE HAVE MEMORY NAME
mem = conn.recv(128)
streamName = mem.split(b'/')[1] #mem name right after
#print(streamName)
byteName = streamName.split(b'\x00')[0]
mName = str(byteName, encoding='utf-8')
mem = shared_memory.SharedMemory(mName)
gotMemoryName = True
print("Connected to shared memory")
print("Visualizing data...")
break
else:
#block until we get data
stream = conn.recv(128)
#update visualization with new data
data = np.ndarray((CHANNELS,ELEMENTS),dtype=np.double, buffer=mem.buf)
for ch in range(0,CHANNELS):
for el in range(0, ELEMENTS):
phi[ch, el] = el*2*np.pi/(ELEMENTS-1)
xdata = np.multiply(1+0.003*data[ch,:],np.cos(phi[ch,:]))
ydata = np.multiply(1+0.003*data[ch,:],np.sin(phi[ch,:]))
#update plots
lineArr[ch].set_xdata(xdata)
lineArr[ch].set_ydata(ydata)
#update figure
figure.canvas.draw()
plt.pause(0.01) #render update
except:
print('exception')
conn.close() #close connection to client
finally:
print('disconnected')
conn.close() #close connection to client
Conclusion
This demo shows how to grab data from BCI2000 and plot it in Python! The main advantage of this demo shows how to access the data in Python. Once this is done, Python's extensive library can be used to create complex, real-time visualizations and calculations!
See also
Programming Reference:SignalSharingDemo Signal Processing, Technical Reference:BCI2000 Messages, User Tutorial:BCI2000Remote