Skip to content
Merged
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
45 changes: 40 additions & 5 deletions octave/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ The local connection has two parts:

## Folder Structure

- `functions/`: Public functions (`cortexConnect`, `cortexSubscribe`, `cortexGetData`, `cortexDisconnect`) that hide the internal MATLAB classes.
- `functions/`: Public functions (`cortexConnect`, `cortexSession`, `cortexSubscribe`, `cortexGetData`, `cortexRecord`, `cortexMarker`, `cortexDisconnect`) that hide the internal MATLAB classes.
- `internal/`: Internal SDK classes. Not a public interface; they may change.
- `lib/`: `nv-websocket-client-2.14.jar` and `cortex-ws-client.jar`, the Java WebSocket client used for WSS.
- `java/`: Source of the small Java wrapper (`cortex-ws-client.jar`).
- `config/`: Local configuration. Copy `config/config.example.json` to `config/config.json` to store your Cortex application credentials. `config/config.json` is ignored by Git and must not be committed or shared.
- `examples/`: Beginner-friendly scripts that show complete workflows.

The public workflow uses `cortexConnect` and `cortexDisconnect`. The underlying `CortexManager` and `CortexAuth` classes are kept inside `internal/` so future examples can extend the SDK without making the user-facing scripts more complicated.
The public workflow uses procedural functions such as `cortexConnect`, `cortexSession`, `cortexSubscribe`, `cortexRecord`, `cortexMarker`, and `cortexDisconnect`. The underlying Cortex classes are kept inside `internal/` so examples can extend the SDK without making researchers manage the WebSocket or internal class instances.

## Requirements

Expand Down Expand Up @@ -136,18 +136,53 @@ The internal manager is stored in the struct only so that `cortexDisconnect(cort

```matlab
cortex = cortexConnect();
cortex = cortexSubscribe(cortex, 'INSIGHT-A1B2C3D4', {'eeg'});
cortex = cortexSession(cortex, 'INSIGHT-A1B2C3D4');
cortex = cortexSubscribe(cortex, {'eeg'});
[labels, data, time] = cortexGetData(cortex, 'eeg', 256); % newest 256 samples
cortex = cortexDisconnect(cortex); % unsubscribes and closes the socket
```

- To subscribe to other supported streams, pass their names instead, for example `{'mot','dev','eq','pow','met'}`. Call `cortexGetData(cortex, streamName)` for each stream you want to consume.
- `labels{j}` names column `j` of `data`; each row is a sample. Use the returned labels to select columns rather than hard-coding positions. For console output, loop over `labels` and print each `data(end,j)`; for plots, plot the desired numeric columns against `time`.
- `met` columns named `<metric>.isActive` are 1 when that metric is running. When it is 0 the metric value is `NaN` (Cortex sends `null`). Without a PRO license `met` arrives about every 10 seconds; with a PRO license about 2 samples per second. `pow` does not need a PRO license.
- `cortexSubscribe` scans for the headset, connects it if needed (waiting up to 5 seconds for Cortex warning 104), creates an active session and subscribes. Optional name/value pairs: `'autoRescan'` (default `true`), `'queryInterval'` (default `3` seconds), `'scanTimeout'` (default `15` seconds).
- `cortexSession` scans for the headset, connects it if needed (waiting up to 5 seconds for Cortex warning 104), and creates one active session for that headset. Calling it again for the same active headset reuses that session; a different headset requires disconnecting first.
- `cortexSubscribe(cortex, streams)` subscribes the requested streams on the active session created by `cortexSession`.
- `cortexGetData` collects the samples that have arrived and returns them from a buffer that keeps the latest 10000 samples per stream. `data` has one row per sample and one column per entry in `labels`; empty values are `NaN`.
- Call `cortexGetData` regularly. Samples are only read from the connection when it runs, and it can take up to about 1 second when no data is arriving.

## Record and Markers

`examples/recordAndMarker.m` demonstrates creating and stopping a record without subscribing to any data stream, and adding keyboard-controlled markers.

1. Set `headsetId` and `titlePrefix` near the top of `examples/recordAndMarker.m`. Add the examples folder to the MATLAB or Octave path and run the function without arguments:

```matlab
addpath('octave/examples');
recordAndMarker();
```

2. Focus the opened figure. Press `S` to start a record titled `<prefix>_<epoch-seconds>`. The example adds a description and the tags `octave` and `record-marker-demo`.
3. Press any other letter to inject an instance marker; the pressed letter is used as both its label and string value. Press that letter again to update the marker with the current epoch time in milliseconds, turning it into an interval marker. The next press injects a new instance marker.
4. Press `Esc` or close the figure to stop the active record and disconnect.

The headset-only connection and record/marker APIs are also available directly:

```matlab
cortex = cortexConnect();
cortex = cortexSession(cortex, 'INSIGHT-A1B2C3D4');
[cortex, started] = cortexRecord(cortex, 'start', 'my-study_1792692000', ...
'Example record description', {'octave', 'demo'});
[cortex, injected] = cortexMarker(cortex, 'inject', 'A', 'A');
endTime = double(javaMethod('currentTimeMillis', 'java.lang.System'));
[cortex, updated] = cortexMarker(cortex, 'update', injected.marker.uuid, endTime);
[cortex, stopped] = cortexRecord(cortex, 'stop');
cortex = cortexDisconnect(cortex);
```

`cortexSession(cortex, headsetId)` connects the headset and creates or reuses its active session without subscribing to streams. From that session, call either `cortexSubscribe(cortex, streams)` or start a record. `cortexRecord(cortex, 'start', title, description, tags)` returns the Cortex result fields `record` and `sessionId`; `cortexRecord(cortex, 'stop')` returns the stopped `record` and its `sessionId`. `cortex.isRecording` and `cortex.recordId` reflect the active record.

`cortexMarker(cortex, 'inject', label, value, ...)` returns `recordId`, `sessionId`, and `marker`. Optional name/value pairs are `time` (epoch milliseconds; defaults to the current time), `port` (defaults to `Software`), and `extras` (a scalar struct). `cortexMarker(cortex, 'update', markerId, endTime, ...)` returns the same result fields and accepts optional `extras`. `endTime` is required and is measured in milliseconds since 1970-01-01 UTC. Both marker actions require an active record.

## Troubleshooting

### Could not connect to Cortex at `wss://localhost:6868`
Expand Down Expand Up @@ -179,7 +214,7 @@ Use a recent MATLAB or GNU Octave release with native JSON support. The SDK does

- Switch the headset on and make sure it is in range, or enable a virtual headset in EMOTIV Launcher.
- Check that `headsetId` matches the id shown in EMOTIV Launcher exactly.
- Increase `'scanTimeout'` in `cortexSubscribe` if the headset is slow to appear.
- Increase `'scanTimeout'` in `cortexSession` if the headset is slow to appear.
- If you see "warning 104 was not received", the headset did not finish connecting in 5 seconds; try again.

## Next Steps
Expand Down
167 changes: 167 additions & 0 deletions octave/examples/recordAndMarker.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
function recordAndMarker()
% recordAndMarker Start/stop a record and inject/update markers from keyboard input.
% Set headsetId and recordTitlePrefix below, then call recordAndMarker().
% Press S to start, any other letter to toggle a marker interval, and Esc to stop and exit.

% Set your headset id and the prefix used to generate each record title.
headsetId = '<YOUR_HEADSET_ID>';
recordTitlePrefix = 'my-record';

currentDir = fileparts(mfilename('fullpath'));
addpath(fullfile(currentDir, '../functions'));

% Connect: establish authorization, connect the selected headset, and create its active session.
cortex = cortexConnect();
fig = [];
connectionCleanup = onCleanup(@cleanupConnection);
try
if ~cortex.isConnected
error('recordAndMarker:connect', 'Cortex connection or authorization failed.');
end
cortex = cortexSession(cortex, headsetId);
fprintf('[recordAndMarker] Connected to %s. Key help:\n', headsetId);
fprintf(' S: start a record.\n');
fprintf(' A-Z except S: inject an instance marker; press that key again to end its interval.\n');
fprintf(' Esc: stop the record and exit.\n');

markerIds = struct();
fig = figure('Name', 'Cortex Record and Marker Example', 'NumberTitle', 'off', ...
'KeyPressFcn', @onKeyPress, 'Interruptible', 'off', 'BusyAction', 'queue');
helpLines = {'Keyboard controls', '', ...
'S: start a new record', ...
'A-Z (except S): add an instance marker while recording', ...
'Press the same letter again: end its marker interval', ...
'Esc: stop the record and exit', '', ...
'Marker ID, type, start time, and end time are printed', ...
'in the Command Window after each marker action.'};
helpText = uicontrol(fig, 'Style', 'text', 'Units', 'normalized', ...
'Position', [0.08 0.12 0.84 0.78], ...
'String', helpLines, ...
'HorizontalAlignment', 'left', 'FontSize', 12, ...
'BackgroundColor', get(fig, 'Color'), 'Enable', 'on', ...
'KeyPressFcn', @onKeyPress, 'Interruptible', 'off', 'BusyAction', 'queue');
statusText = uicontrol(fig, 'Style', 'text', 'Units', 'normalized', ...
'Position', [0.08 0.02 0.84 0.08], ...
'String', 'Ready: press S to start a record.', ...
'HorizontalAlignment', 'left', 'FontSize', 10, ...
'BackgroundColor', get(fig, 'Color'), 'Enable', 'on', ...
'KeyPressFcn', @onKeyPress, 'Interruptible', 'off', 'BusyAction', 'queue');

% Acquire: wait for keyboard events while Cortex keeps the active session open.
uiwait(fig);
catch failure
fprintf('[recordAndMarker] ERROR %s: %s\n', failure.identifier, failure.message);
end

% Disconnect: stop any active record and close the Cortex connection on every exit path.
clear connectionCleanup;
fprintf('[recordAndMarker] Finished.\n');

function onKeyPress(source, event)
keyName = event.Key;
showStatus(sprintf('Key %s received.', keyName));
if strcmpi(keyName, 'escape') || strcmpi(keyName, 'esc')
if cortex.isRecording
try
[cortex, ~] = cortexRecord(cortex, 'stop');
fprintf('[recordAndMarker] Record stopped.\n');
showStatus('Record stopped. Closing example.');
catch stopFailure
fprintf('[recordAndMarker] ERROR %s: %s\n', stopFailure.identifier, stopFailure.message);
showStatus(['Could not stop record: ' stopFailure.message]);
end
end
if ishandle(fig), uiresume(fig); end
return;
end

keyCharacter = event.Character;
if isempty(keyCharacter) && numel(keyName) == 1
keyCharacter = keyName;
end
if ~ischar(keyCharacter) || numel(keyCharacter) ~= 1 || ...
isempty(regexp(keyCharacter, '^[A-Za-z]$', 'once'))
showStatus(sprintf('Ignored key: %s. Use S, A-Z, or Esc.', keyName));
return;
end

% Process: reserve S for record start and use every other letter to toggle marker intervals.
if lower(keyCharacter) == 's'
if cortex.isRecording
fprintf('[recordAndMarker] A record is already active.\n');
showStatus('A record is already active.');
return;
end
try
recordTitle = sprintf('%s_%d', recordTitlePrefix, floor(currentTimeMilliseconds() / 1000));
description = 'Keyboard-controlled Cortex record and marker example';
tags = {'octave', 'record-marker-demo'};
[cortex, result] = cortexRecord(cortex, 'start', recordTitle, description, tags);
fprintf('[recordAndMarker] Started record %s.\n', result.record.uuid);
showStatus(['Recording started: ' result.record.uuid]);
catch startFailure
fprintf('[recordAndMarker] ERROR %s: %s\n', startFailure.identifier, startFailure.message);
showStatus(['Record start failed: ' startFailure.message]);
end
return;
end

if ~cortex.isRecording
fprintf('[recordAndMarker] Start a record with S before adding markers.\n');
showStatus('No active record. Press S to start one first.');
return;
end

markerKey = lower(keyCharacter);
try
if isfield(markerIds, markerKey)
markerId = markerIds.(markerKey);
[cortex, result] = cortexMarker(cortex, 'update', markerId, currentTimeMilliseconds());
markerIds = rmfield(markerIds, markerKey);
fprintf('[recordAndMarker] Updated marker %s (type: %s).\n', ...
result.marker.uuid, result.marker.type);
fprintf(' Start: %s\n End: %s\n', ...
result.marker.startDatetime, result.marker.endDatetime);
showStatus(['Marker interval updated: ' result.marker.uuid]);
else
[cortex, result] = cortexMarker(cortex, 'inject', keyCharacter, keyCharacter, ...
'time', currentTimeMilliseconds(), 'port', 'MATLAB/Octave example');
markerIds.(markerKey) = result.marker.uuid;
fprintf('[recordAndMarker] Injected marker %s (type: %s, port: %s) with value %s.\n', ...
result.marker.uuid, result.marker.type, result.marker.port, keyCharacter);
fprintf(' Start: %s\n End: %s\n', ...
result.marker.startDatetime, result.marker.endDatetime);
showStatus(['Marker injected: ' result.marker.uuid ...
' (value: ' result.marker.value ')']);
end
catch markerFailure
fprintf('[recordAndMarker] ERROR %s: %s\n', markerFailure.identifier, markerFailure.message);
showStatus(['Marker action failed: ' markerFailure.message]);
end
end

function showStatus(message)
if ~isempty(statusText) && ishandle(statusText)
set(statusText, 'String', message);
drawnow();
end
end

function cleanupConnection()
if cortex.isRecording
try
[cortex, ~] = cortexRecord(cortex, 'stop');
catch stopFailure
fprintf('[recordAndMarker] Could not stop the active record: %s\n', stopFailure.message);
end
end
if ~isempty(fig) && ishandle(fig)
delete(fig);
end
cortex = cortexDisconnect(cortex);
end
end

function time = currentTimeMilliseconds()
time = double(javaMethod('currentTimeMillis', 'java.lang.System'));
end
5 changes: 3 additions & 2 deletions octave/examples/subscribeAndPlot.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ function subscribeAndPlot()
streams = {'eeg'};
durationSeconds = 90;

% 2. Connection / Authentication, then headset connection and subscription.
% 2. Connection / Authentication, then create a headset session and subscribe to streams.
% Credentials are loaded from config/config.json.
cortex = cortexConnect();
connectionCleanup = onCleanup(@() cortexDisconnect(cortex));
if cortex.isConnected
cortex = cortexSubscribe(cortex, headsetId, streams);
cortex = cortexSession(cortex, headsetId);
cortex = cortexSubscribe(cortex, streams);
end

% 3. Acquisition Loop: each cortexGetData call collects new samples into the buffer.
Expand Down
6 changes: 4 additions & 2 deletions octave/functions/cortexConnect.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
% userInfo user details returned by Cortex ([] on failure)
% licenseInfo license details returned by Cortex ([] on failure)
% manager internal handle used by cortexDisconnect; do not modify
% isSubscribed, headsetId, sessionId, streams filled in by cortexSubscribe
% isSubscribed, headsetId, sessionId, streams filled in by headset connection/subscription
% isRecording, recordId filled in by cortexRecord

functionsDir = fileparts(mfilename('fullpath'));
addpath(fullfile(functionsDir, '..', 'internal'));

[clientId, clientSecret, port] = cortexCredentials();

cortex = struct('isConnected', false, 'token', '', 'userInfo', [], 'licenseInfo', [], 'manager', [], ...
'isSubscribed', false, 'headsetId', '', 'sessionId', '', 'streams', {{}});
'isSubscribed', false, 'isRecording', false, 'recordId', '', ...
'headsetId', '', 'sessionId', '', 'streams', {{}});
manager = CortexManager(clientId, clientSecret, 'localhost', port);
cortex.manager = manager;

Expand Down
8 changes: 7 additions & 1 deletion octave/functions/cortexDisconnect.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@

if isstruct(cortex) && isfield(cortex, 'manager') && ~isempty(cortex.manager)
fprintf('[cortexDisconnect] Closing Cortex connection...\n');
cortex.manager.close();
try
cortex.manager.close();
catch failure
fprintf('[cortexDisconnect] Could not close the Cortex connection cleanly: %s\n', failure.message);
end
cortex.manager = [];
end

cortex.isConnected = false;
cortex.isSubscribed = false;
cortex.isRecording = false;
cortex.recordId = '';
cortex.headsetId = '';
cortex.sessionId = '';
cortex.streams = {};
Expand Down
58 changes: 58 additions & 0 deletions octave/functions/cortexMarker.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
function [cortex, result] = cortexMarker(cortex, action, varargin)
% cortexMarker Inject an instance marker or update it to an interval marker.
% [cortex, result] = cortexMarker(cortex, 'inject', label, value, ...)
% Optional inject name/value pairs: time (epoch milliseconds), port, extras.
% [cortex, result] = cortexMarker(cortex, 'update', markerId, endTime, ...)
% Optional update name/value pair: extras. Markers require an active record.

result = [];
try
if ~isstruct(cortex) || ~isfield(cortex, 'isConnected') || ~cortex.isConnected || ...
~isfield(cortex, 'isRecording') || ~cortex.isRecording || ...
~isfield(cortex, 'manager') || isempty(cortex.manager)
error('cortex:markerFailed', 'Start a record before using marker operations.');
end
if ~ischar(action)
error('cortex:markerFailed', 'action must be ''inject'' or ''update''.');
end

marker = cortex.manager.getMarker();
switch lower(action)
case 'inject'
if numel(varargin) < 2
error('cortex:markerFailed', 'Inject requires a label and value.');
end
label = varargin{1};
value = varargin{2};
options = parseOptions(varargin(3:end), struct('time', [], 'port', 'Software', 'extras', struct()));
result = marker.inject(label, value, options.time, options.port, options.extras);
case 'update'
if numel(varargin) < 2
error('cortex:markerFailed', 'Update requires a markerId and endTime.');
end
markerId = varargin{1};
endTime = varargin{2};
options = parseOptions(varargin(3:end), struct('extras', struct()));
result = marker.update(markerId, endTime, options.extras);
otherwise
error('cortex:markerFailed', 'action must be ''inject'' or ''update''.');
end
catch failure
if strcmp(failure.identifier, 'cortex:markerFailed'), rethrow(failure); end
error('cortex:markerFailed', 'Marker operation failed: %s', failure.message);
end
end

function options = parseOptions(arguments, options)
if mod(numel(arguments), 2) ~= 0
error('cortex:markerFailed', 'Marker options must be supplied as name/value pairs.');
end
for index = 1:2:numel(arguments)
name = arguments{index};
if ~ischar(name) || ~isfield(options, name)
error('cortex:markerFailed', 'Unknown marker option.');
end
options.(name) = arguments{index + 1};
end
end

Loading
Loading