Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ if (NOT DEFINED GUI_BASE_DIR)
if (DEFINED ENV{GUI_BASE_DIR})
set(GUI_BASE_DIR $ENV{GUI_BASE_DIR})
else()
set(GUI_BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../plugin-GUI)
set(GUI_BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../plugin-GUI-main)
endif()
endif()

Expand Down
169 changes: 43 additions & 126 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

![matlab-interface-screenshot](Resources/matlab-interface.png)

Streams one channel of continuous data from the Open Ephys GUI to a live Matlab session. A Matlab API allows seamless processing of the incoming data in real time.
Streams channel and spike_id information of all sorted spikes from open-ephys to matlab. The major application of the present plugin is my custom-made matlab-based close-loop BCI algorithm. It may also help with spike-oriented live processing.


## Installation

This plugin can be added via the Open Ephys GUI Plugin Installer. To access the Plugin Installer, press **ctrl-P** or **⌘P** from inside the GUI. Once the installer is loaded, browse to the "Matlab Interface" plugin and click "Install."
(Coming soon)


## Usage

Once installed, the plugin will appear as 'Matlab Interface' in the 'Filters' section of the 'Processor List' of the Open Ephys GUI. The plugin can be placed after any source, filter, or sink in the signal chain and will stream the data output by the preceding processor into your Matlab session.
Once installed, the plugin will appear as 'Matlab Interface' in the 'Filters' section of the 'Processor List' of the Open Ephys GUI. The plugin can be placed after any source, filter, or sink in the signal chain and will stream the data output by the preceding processor into your Matlab session. A 'Spike Sorter' must be upstream of 'Matlab Interface', otherwise fatal errors shall arise. Signal chain like below shall work properly:

![matlab-interface-screenshot](Resources/chain.png)

Before starting data acquisition, you will need to press the 'Connect' button in the editor to initiate a connection between the plugin and your Matlab session. The plugin will open a socket using the IP and Port address listed in the Matlab Interface editor.

Expand Down Expand Up @@ -51,149 +54,62 @@ classdef MyClass < GenericProcessor
end
```

Here's an example of a simple peak detection algorithm:

```matlab
classdef PeakDetector < GenericProcessor

properties
peaks;
end

methods
function self = PeakDetector(host, port)
self = self@GenericProcessor(host, port);
self.process();
end
end

methods (Access = protected)
function process(self)
while (true)
process@GenericProcessor(self);
yd = diff(self.dataIn.continuous)./diff(1:self.dataIn.numSamplesFetched);
self.peaks = find(~yd);
end
end
end
end
```
Once you have designed your class, you can call it from the Matlab command window using `MyClass(host,port)`, where host and port need to match the entries specified in the MatlabEngine editor in the OpenEphys GUI.

And a simple threshold detector...
However, for those who are not familiar with object-oriented programming of matlab, simple scripts are available using the API provided. Here is an example that prints information of every spike every 50ms:

```matlab
classdef ThresholdDetector < GenericProcessor

properties
thresholdValue;
end

methods
function self = ThresholdDetector(host, port)
self = self@GenericProcessor(host, port);
self.thresholdValue = 3.2; %V
self.process();
end
end

methods (Access = protected)
function process(self)
while (true)
process@GenericProcessor(self);
k = find(self.dataIn.continuous > self.thresholdValue);
host='127.0.0.1'
port=1234
processor=GenericProcessor(host, port);

cycle_duration=0.05 %50ms cycle

timeall=tic();
while 1
timet=tic();

data=[];
t=processor.process();
if ~isempty(t)
t=split(t,' ');
for i=1:length(t)-1
% For some unknown reason, there are some garbled codes in the data received for the first time
% If your system language is CHINESE, such codes are presented in the form of '屯'
% For users of other languages, you may experiment yourself
% For formal use, simply discard the data obtained the first time
if ~isempty(t{i}) && t{i}(1)~='屯'
tt=split(t{i},'+');
data=[data;str2double(tt(1)),str2double(tt(2))];
end
end
end

if ~isempty(data)
data
end

while toc(timet)<cycle_duration; end

toc(timeall)
end
```

And here's an example of a Plotter that plots the incoming data to a figure in real-time.

```matlab
classdef Plotter < GenericProcessor

properties
hPlot;
end

properties
xAxisRange;
yAxisRange;
end

methods

function self = Plotter(host, port)

self = self@GenericProcessor(host, port);

self.xAxisRange = [0,80000];
self.yAxisRange = [-1000 1000];

plotTitle = 'Open Ephys Data Stream';
xLabel = 'Sample Count';
yLabel = 'Voltage [uV]';

clf; cla;
self.hPlot = plot(0,0); drawnow; hold on;
title(plotTitle);
xlabel(xLabel); ylabel(yLabel);
xlim(self.xAxisRange); ylim(self.yAxisRange);

self.process();
end
end

methods (Access = protected)

function process(self)

lastSample = 0;
xAxisSize = self.xAxisRange(2);

while ishandle(self.hPlot)

process@GenericProcessor(self);

numSamples = self.dataIn.numSamplesFetched;

if lastSample + numSamples > xAxisSize
xData = (lastSample+1):xAxisSize;
yData = self.dataIn.continuous(1:(xAxisSize-lastSample));
else
xData = (lastSample+1):(lastSample+numSamples);
yData = self.dataIn.continuous;
end

self.hPlot = plot(xData,yData); drawnow;
set(self.hPlot, 'XData', xData, 'YData', yData);

%TODO: Currently ignores (doesn't plot) samples that overshoot x-axis range
lastSample = lastSample + numSamples;
if lastSample > xAxisSize
lastSample = 0;
cla; self.hPlot = plot(0,0); drawnow;
end
end
end
end
end
```

Once you have designed your class, you can call it from the Matlab command window using `MyClass(host,port)`, where host and port need to match the entries specified in the MatlabEngine editor in the OpenEphys GUI.


## Building from source

First, follow the instructions on [this page](https://open-ephys.github.io/gui-docs/Developer-Guide/Compiling-the-GUI.html) to build the Open Ephys GUI.

**Important:** This plugin is intended for use with the latest version of the GUI (0.6.0 and higher). The GUI should be compiled from the [`main`](https://github.com/open-ephys/plugin-gui/tree/main) branch, rather than the former `master` branch.
**Important:** This plugin is intended for the latest version of open-ephys GUI (**0.6.0**).

Then, clone this repository into a directory at the same level as the `plugin-GUI`, e.g.:

```
Code
├── plugin-GUI
├── plugin-GUI-master
│ ├── Build
│ ├── Source
│ └── ...
Expand All @@ -218,6 +134,7 @@ Next, launch Visual Studio and open the `OE_PLUGIN_matlab-interface.sln` file th

Selecting the `INSTALL` project and manually building it will copy the `.dll` and any other required files into the GUI's `plugins` directory. The next time you launch the GUI from Visual Studio, the Matlab Interface plugin should be available.

**Warning:** Linux and macOS are not tested yet

### Linux

Expand All @@ -232,7 +149,7 @@ make -j
make install
```

This will build the plugin and copy the `.so` file into the GUI's `plugins` directory. The next time you launch the compiled version of the GUI, the Matlab Interface plugin should be available.
This will build the plugin and copy the `.so` file into the GUI's `plugins` directory. The next time you launch the GUI compiled version of the GUI, the Matlab Interface plugin should be available.


### macOS
Expand All @@ -251,4 +168,4 @@ Running the `ALL_BUILD` scheme will compile the plugin; running the `INSTALL` sc

### Questions and Troubleshooting

If you have any questions and/or issues regarding this plugin, please open an Issue in this repository or reach out to [email protected].
If you have any questions and/or issues regarding this plugin, please open an Issue in this repository.
25 changes: 16 additions & 9 deletions Resources/API/GenericProcessor.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

self.client = OEClient(host, port);
self.dataIn = DataBuffer(self.client);
self.dataOut = ['Test!' blanks(self.client.WRITE_MSG_SIZE_IN_BYTES/2-5)];
self.dataOut = ['Fetch' blanks(self.client.WRITE_MSG_SIZE_IN_BYTES/2-5)];
self.timer = tic;
self.dataRate = 0;
self.packetsFetched = 0;
Expand All @@ -24,16 +24,23 @@

end

methods (Access = protected)
methods (Access = public)

function process(self)
function data=process(self)
self.client.write(self.dataOut);
self.dataIn.continuous = str2num(self.client.read()); %#ok<*ST2NM>
self.packetsFetched = self.packetsFetched + 1;
self.dataIn.numSamplesFetched = length(self.dataIn.continuous);
lastFetch = self.dataIn.numSamplesFetched/toc(self.timer);
self.dataRate = (lastFetch + self.packetsFetched * self.dataRate ) / (self.packetsFetched + 1);
fprintf("Data rate: %1.2f\n", self.dataRate);
t=tic();
while toc(t)<0.002
end
data=self.client.read();
data=char(data);
% fprintf("%s;\n",t);

% self.dataIn.continuous = t; %#ok<*ST2NM>
% self.packetsFetched = self.packetsFetched + 1;
% self.dataIn.numSamplesFetched = length(self.dataIn.continuous);
% lastFetch = self.dataIn.numSamplesFetched/toc(self.timer);
% self.dataRate = (lastFetch + self.packetsFetched * self.dataRate ) / (self.packetsFetched + 1);
% fprintf("Data rate: %1.2f\n", self.dataRate);
self.timer = tic;
end

Expand Down
49 changes: 29 additions & 20 deletions Resources/API/OEClient.m
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
classdef OEClient < handle

%OECLIENT A client socket for receiving data streams from the Open-Ephys GUI.
%OECLIENT A client socket for receiving data streams from the Open-Ephys GUI.
% This class encapsulates a socket on a dedicated, local port in order to
% receive continous data stream from Open Ephys.

% receive continous data stream from Open Ephys.
properties (Access = public)
header;
end
Expand All @@ -19,13 +19,13 @@
stream_reader;
buffered_reader;
end

properties (Constant)
END_OF_MESSAGE = '~';
MAX_CONNECT_ATTEMPTS = 5;
MAX_CONNECT_ATTEMPTS = 50;
WRITE_MSG_SIZE_IN_BYTES = 1024;
end

methods

function self = OEClient(host, port)
Expand All @@ -41,23 +41,32 @@
attempts = attempts + 1;
end

if ~self.connected
if ~self.connected
fprintf("Failed to connect to Open Ephys!\n");
end

end

function line = read(self)

line = self.buffered_reader.readLine;

% line=[];
% t=self.buffered_reader.read()
% while t~=-1
% line=[line t];
% t=self.buffered_reader.read()
% end
% line=[line t];
if self.buffered_reader.ready()
line = self.buffered_reader.readLine();
else
line=[];
end
end

function self = write(self, message)

fprintf("Size of msg: %d\n", length(message));
fprintf("Message: %s\n", message);

% fprintf("Size of msg: %d\n", length(message));
% fprintf("Message: %s\n", message);
if (length(message)/2 == self.WRITE_MSG_SIZE_IN_BYTES)
self.stream_writer.writeBytes(message);
self.stream_writer.flush;
Expand All @@ -66,7 +75,7 @@
self.stream_writer.writeBytes([message msg_pad]);
self.stream_writer.flush;
end

end

function self = connect(self, host, port)
Expand All @@ -87,12 +96,12 @@
fprintf("Created output stream writer...\n");
self.stream_reader = InputStreamReader(self.in_stream);
fprintf("Created input stream reader...\n");
self.buffered_reader = BufferedReader(self.stream_reader);
self.buffered_reader = BufferedReader(self.stream_reader,65536);
fprintf("Created buffered reader...\n");
catch
fprintf("Connecting...\n");
catch
fprintf("Connecting openEphys...\n");
end

end

end
Expand Down
Binary file added Resources/chain.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading