Skip to content

Instantly share code, notes, and snippets.

@isaac-ped
Forked from anonymous/cbmex.cpp
Last active February 2, 2016 16:30
Show Gist options
  • Save isaac-ped/da6ac87f33fce66ee66e to your computer and use it in GitHub Desktop.
Save isaac-ped/da6ac87f33fce66ee66e to your computer and use it in GitHub Desktop.
// Author & Date: Tom Gradel 4 April 2015
// Purpose: Control real-time cell array data acquisition methods
void OnSnapShot(
int nlhs, // Number of left hand side (output) arguments
mxArray *plhs[ ], // Array of left hand side arguments
int nrhs, // Number of right hand side (input) arguments
const mxArray *prhs[ ] )// Array of right hand side arguments
{
UINT32 nInstance = 0;
cbSdkResult res = CBSDKRESULT_SUCCESS;
if ( nrhs < 2)
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Too few inputs provided\n" );
char cmdstr[ 128 ];
if ( mxGetString( prhs[ 1 ], cmdstr, 16 ) )
{
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "invalid command name\n" );
}
enum
{
SNAPSHOT_GET,
SNAPSHOT_SAVE,
SNAPSHOT_UNKNOWN,
} command = SNAPSHOT_UNKNOWN;
if ( 0 == _strnicmp( cmdstr, "get", ARRAYSIZE( cmdstr ) ) )
command = SNAPSHOT_GET;
else if ( 0 == _strnicmp( cmdstr, "save", ARRAYSIZE( cmdstr ) ) )
command = SNAPSHOT_SAVE;
if ( command == SNAPSHOT_UNKNOWN )
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Unknown snapshot parameter\n" );
switch (command)
{
case SNAPSHOT_GET:
{
UINT32 timestamp = 0;
UINT32 stopCell[ cbNUM_ANALOG_CHANS ];
if ( nrhs != 3 && nrhs != 4 )
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Incorrect number of input parameters to 'snapshot:get'\n" );
if ( nlhs > 3 )
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Incorrect number of output parameters to 'snapshot:get'\n" );
if (nrhs == 4 )
{
if ( !mxIsNumeric( prhs[ 3 ] ) )
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Invalid instance number" );
nInstance = ( UINT32 ) mxGetScalar( prhs[ 3 ] );
}
UINT32 bufferLength = (UINT32) mxGetScalar( prhs[ 2 ] );
res = cbSdkSnapShotGet( nInstance, &timestamp, stopCell, bufferLength);
if ( res != CBSDKRESULT_SUCCESS )
{
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Error getting snapshot\n" );
}
if ( nlhs > 0 )
{
plhs[ 0 ] = mxCreateDoubleScalar( timestamp );
if ( nlhs > 1 )
{
mxArray *mxa = mxCreateDoubleMatrix( cbNUM_ANALOG_CHANS, 1, mxREAL );
double *pDest = mxGetPr( mxa );
for ( int i = 0; i < cbNUM_ANALOG_CHANS; i++ )
pDest[ i ] = stopCell[ i ];
plhs[ 1 ] = mxa;
}
}
break;
}
case SNAPSHOT_SAVE:
{
if ( nrhs < 4 || nrhs > 6)
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Incorrect number of input parameters to 'snapshot:save'" );
if ( nlhs > 0 )
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "'snapshot:save' has no output parameters" );
UINT32 stopTimestamp = 0;
if ( nrhs > 4 )
{
if ( ! mxIsNumeric( prhs[4] ) )
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Invalid timestamp" );
stopTimestamp = ( UINT32 ) mxGetScalar( prhs[ 4 ] );
if ( nrhs == 6 )
{
if ( !mxIsNumeric( prhs[ 5 ] ) )
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Invalid instance number" );
nInstance = ( UINT32 ) mxGetScalar( prhs[ 5 ] );
}
}
if ( mxGetN( prhs[ 3 ] ) != cbNUM_ANALOG_CHANS )
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "'snapshot:save' sample_buffer must have 144 columns" );
double *pStopCell = mxGetPr( prhs[ 2 ] );
UINT32 stopCell[ cbNUM_ANALOG_CHANS ];
for ( int i = 0; i < cbNUM_ANALOG_CHANS; i++ )
stopCell[ i ] = ( UINT32 ) pStopCell[ i ];
// To support different sample frequencies, a sample_size parameter could be passed.
// Instead, assume sample_buffer rows size provides the proper length for all channels
UINT32 samples = (UINT32) mxGetM( prhs[ 3 ] );
double *pSampleBuffer = mxGetPr( prhs[ 3 ] );
res = cbSdkSnapShotSave( nInstance, stopCell, stopTimestamp, samples, pSampleBuffer );
if ( nlhs > 0 )
{
plhs[ 0 ] = mxCreateDoubleScalar( ( double ) res );
} else if (res != CBSDKRESULT_SUCCESS )
{
PrintHelp( CBMEX_FUNCTION_SNAPSHOT, true, "Snapshot save failed, likely because too much time has elapsed since snapshot get\n" );
}
break;
}
default:
PrintErrorSDK( CBSDKRESULT_UNKNOWN, "OnSnapShot()" );
}
}
#define CBMEX_USAGE_SNAPSHOT \
"Control continuous event data snapshots configured by 'trialconfig' with the ring_buffer option\n" \
"Format:\n" \
" [stop_timestamp, cell_stop_index] = cbmex('snapshot', 'get', buffer_length, [instance])\n" \
" status = cbmex('snapshot', 'save', cell_stop_index, sample_buffer, [stop_timestamp,[instance]])\n" \
"Inputs:\n" \
" 'buffer_length': size of sample, eg 2000 for 2 seconds of data at 1K samples/second\n" \
" Used to ensure write pointer doesn't overlap with read.\n" \
" 'cell_stop_index': values previously returned by cbmex('snapshot', 'get')\n" \
" 'stop_timestamp' : value previously returned by cbmex('snapshot', 'get') \n"\
" ensures no buffer overlap\n"\
" 'sample_buffer': a number-of-samples x 144 matrix of doubles, where\n" \
" ' number-of-samples is copied fromm the ring buffer starting\n" \
" and cell_stop_index - number-of-samples and wraps if necessary.\n" \
" Elements are converted to doubles when they are copied.\n" \
" 'instance' (optional), value: value is the library instance to use (default is 0)\n" \
"Outputs:\n" \
" Outputs for the 'get' case are:\n" \
" 'stop_timestamp': NeuroPort time of the sample that starts in cell(cell_stop_index)\n" \
" 'cell_stop_index': 1-based index (MATLAB-style) of the cell that contains the last available data to be read. \n" \
" Since a ring buffer is used, cell_stop_index can be < cell_start_index, indicating that new\n" \
" data is from cell_start_index:end and from 1:cell_stop_index.\n" \
" When cell_stop_index is an input, a zero value of is ignored.\n" \
" Outputs for the 'status' case are:\n" \
" 'overflow': Has a value of 0 if no overflow has occurred, and 1 if an overflow has occurred" \
// Author & Date: Tom Gradel 4 March 2015
// Purpose: Get real-time snapshot information
// Inputs:
// nInstance - the instance number
// pTimestamp - array of timestamps for the time corresponding to the start cell
// pStartCell - array of start cells for each channel of the current real-time dataset
// pStopCell - array of stop cell for each channel of the current real-time dataset
// Outputs:
// returns the error code
cbSdkResult cbSdkSnapShotGet( UINT32 nInstance, UINT32 *pTimestamp, UINT32 *pStopCell, UINT32 bufferLength )
{
if ( nInstance >= cbMAXOPEN )
return CBSDKRESULT_INVALIDPARAM;
if ( g_app[ nInstance ] == NULL )
return CBSDKRESULT_CLOSED;
return g_app[ nInstance ]->SdkSnapShotGet( pTimestamp, pStopCell, bufferLength);
}
// Author & Date: Isaac Pedisich 19 January 2015
// Purpose: Get real-time snapshot information
// Inputs:
// pTimestamp - array of timestamps for the time corresponding to the start cell
// pStopCell - array of start indicies for each channel of the current real-time dataset
// bufferLength - moves write_start_index to stopCell-bufferLength
// Outputs:
// returns the error code
cbSdkResult SdkApp::SdkSnapShotGet( UINT32 *pTimestamp, UINT32 *pStopCell, UINT32 bufferLength )
{
if ( m_instInfo == 0 )
return CBSDKRESULT_CLOSED;
if ( !m_bWithinTrial || m_CD == NULL )
return CBSDKRESULT_CLOSED;
cbGetSystemClockTime( pTimestamp, m_nInstance );
m_lockTrial.lock( );
for ( int ch = 0; ch < cbNUM_ANALOG_CHANS; ch++ )
{
if ( m_CD->write_start_index[ ch ] == m_CD->write_index[ ch ] ) // No data exists
{
pStopCell[ ch ] = 0;
}
else
{
// Return 1-based indices to MATLAB
pStopCell[ ch ] = m_CD->write_index[ ch ]; // Already 1-based since is last written + 1
INT32 startCell = (INT32) pStopCell[ ch ] - (INT32) bufferLength;
if ( startCell < 0 )
startCell = (INT32) m_CD->size + startCell;
m_CD->write_start_index[ ch ] = startCell;
}
}
m_lockTrial.unlock( );
return CBSDKRESULT_SUCCESS;
}
// Author & Date: Tom Gradel 4 March 2015
// Purpose: Determine snapshot overflow status
// Inputs:
// nInstance - the instance number
// pStopCell - array of stop cells for each to release
// nLatencyOffset - offset into ring buffer to account for latency
// samples - number of data elements preceeding (and including) 'stop' that are to be kept
// pSampleBuffer - data buffer to hold data records from stop - samples + 1 to stop
// Outputs:
// returns the error code
CBSDKAPI cbSdkResult cbSdkSnapShotSave( UINT32 nInstance, UINT32 *pStopCell, UINT32 nLatencyOffset, UINT32 samples, double *pSampleBuffer )
{
if ( nInstance >= cbMAXOPEN )
return CBSDKRESULT_INVALIDPARAM;
if ( g_app[ nInstance ] == NULL )
return CBSDKRESULT_CLOSED;
return g_app[ nInstance ]->SdkSnapShotSave( pStopCell, nLatencyOffset, samples, pSampleBuffer );
}
// Author & Date: Tom Gradel 4 March 2015
// Purpose: Send an extension command
// Inputs:
// pStopCell - MATLAB 1-based set of stop cells
// nLatencyOffset - offset into ring buffer to account for latency
// samples - number of samples (N) to fill in pSampleBuffer
// pSampleBuffer - double matrix (Nx144)
// Outputs:
// returns the error code
cbSdkResult SdkApp::SdkSnapShotSave( UINT32 *pStopCell, UINT32 stopTimestamp, UINT32 samples, double *pSampleBuffer )
{
if ( m_instInfo == 0 )
return CBSDKRESULT_CLOSED;
if (stopTimestamp != 0)
{
UINT32 nowTimestamp = 0;
cbGetSystemClockTime( &nowTimestamp, m_nInstance );
double stopTimeInSeconds = ( double ) stopTimestamp / ( double ) 30000; // clock frequency hardwired to 30000
double nowTimeInSeconds = ( double ) nowTimestamp / ( double ) 30000;
// Check to make sure that we won't be reading from portions of the buffer that have been ovewritten
// Assumes first sample rate is the one we care about!
double maxSamples = m_CD->size - m_CD->current_sample_rates[ 0 ] * ( nowTimeInSeconds - stopTimeInSeconds );
if ( samples > maxSamples )
{
return CBSDKRESULT_ERROVERLAP;
}
}
// Do not lock the ring buffer. The assumption is that data was returned to the caller previously and has not been released,
// So it cannot be overwritten
double *pBuffer = pSampleBuffer;
for ( int ch = 0; ch < cbNUM_ANALOG_CHANS; ch++ )
{
// Use 'stop' and compute backwards to find the start. Do not include data from the latency offset.
INT32 start = (INT32) pStopCell[ ch ] - samples;
if ( start < 0 )
{
start = m_CD->size + start; // Start is negative, so this subtracts
}
if ( start + samples - 1 < m_CD->size ) // No ring buffer wrap
{
std::copy( &m_CD->continuous_channel_data[ ch ][ start ], &m_CD->continuous_channel_data[ ch ][ start + samples ], &pBuffer[ 0 ] );
// for ( UINT32 i = 0; i < samples; i++ )
// pBuffer[ i ] = ( double ) m_CD->continuous_channel_data[ ch ][ j++ ];
}
else // Ring buffer wrap
{
double *p = &pBuffer[ 0 ];
p = std::copy( &m_CD->continuous_channel_data[ ch ][ start ], &m_CD->continuous_channel_data[ ch ][ m_CD->size ], p );
UINT32 partLength2 = samples - (m_CD->size - start);
std::copy( &m_CD->continuous_channel_data[ ch ][ 0 ], &m_CD->continuous_channel_data[ ch ][ partLength2 ], p );
}
pBuffer += samples; // Next column
}
return CBSDKRESULT_SUCCESS;
}
function loop(this, hObject, statusCallback, exitCallback)
% Main loop - acquire samples and call analysis functions
% Get handles used below
stimControl = StimControl.getInstance(); % Singleton instance to analyis code
% This starts data zooming through the buffer. Set an onCleanup handler in case of error
% so that data acquisition can be stopped prior to releasing MATLAB memory used by cbmex.
this.setupTrialConfigMemory();
cleaner = onCleanup(@()cleanupRingBuffer(this)); % Use onCleanup handler
% Wait until we get some data. Since I'm not sure which channels will be recording, wait for
% any channel that has acquired some data.
[~, stop0] = this.getSnapShot();
[timestamp, stopIndex] = this.getSnapShot();
initialTic = tic;
while sum(stopIndex - stop0) == 0
if toc(initialTic) < 2 % Try for two seconds to get a snapshot
[~, stopIndex] = this.getSnapShot();
else
errordlg(['No data received from the NeuroPort, most likely because the NeuroPort was run', ...
' using Central rather than this application. Close this dialog, press "STOP" then',...
' Exit this application. Load Central and use Hardware Configuration to configure', ...
' 128 Front End Amp channels and 1 Analog Channel. Then restart.']);
return;
end
end
% Find the first channel that is recording
% NOTE: Only tested when all channels recording at same rate!
%delta = stopIndex > stop0;
%index = strfind(delta', 1);
ch = 1;%;index(1);
% Wait for another two seconds so enough data gets into the buffer
pause(RAMControl.SAMPLE_SIZE./1000)
% Setup to collect RAMControl.SAMPLE_SIZE samples in RAMControl.CHUNK_SIZE chunks
states = this.getActiveStates();
oldStates = states;
sampleCount = 0;
maxWait = RAMControl.CHUNK_SIZE*2/1000; % Amount of time that can pass before a chunk is collected
stopIndices = zeros(1,144);
old_ch_stopIndex = stopIndices(1);
decision = 0;
experimentState = struct;
experimentState.sample = 0;
isError = false;
errorNP = false;
latency = RAMControl.LATENCY; % Offset within buffer to account for latency
sampleSize = RAMControl.SAMPLE_SIZE + latency; % Wait for this size sample before analyzing
keepSampleSize = sampleSize - RAMControl.CHUNK_SIZE; % Keep this amount of buffer data
% ---- The following data used for profiling only
% profile on -timer real
% m1save=zeros(1,20000);
% m2save=zeros(2000000,2);
% index1=0;
% index2=0;
% countIndex = 0;
% countSave = zeros(2000000, 2);
tic;
% ---- End of profiling data
while hObject.UserData
% We need to measure when the loop started so we know if data
% acquisition is taking too long
loopStartTic = tic;
% Allow GUI to check for events
drawnow;
% Get out if button pressed that changed hObject.UserData
if isvalid(hObject) && ~hObject.UserData
break;
end
countTic = tic;
% Wait for sampleSize worth of new data before doing the next analysis
taskComputerStatus = this.getTaskComputerStatus(); % Inactive
[~,~,~,~, list] = ramex('expinfo');
experimentState.list = list;
% Get a new snapshot
[timestamp, stopIndices] = this.getSnapShot();
while old_ch_stopIndex == stopIndices(ch) &&...
isequal(states, oldStates)
% If enough time has passed that two sets of samples should have
% been recieved, something is wrong and we should break
if toc(loopStartTic) > maxWait
isError = true;
errorNP = true;
break;
end
% RAMEX detects that the Task Computer is active via periodic heartbeats so it knows
% if the Task Computer stops responding. Since there's no easy way to call MATLAB from
% the RAMEX thread, this method polls the RAMEX interface to detect this, then displays
% a message and aborts processing.
taskComputerStatus = this.getTaskComputerStatus();
if taskComputerStatus < 0
isError = true;
break;
elseif ~taskComputerStatus % Inactive because completed experiment
break;
end
[~,~,~,~, list] = ramex('expinfo');
experimentState.list = list;
% Processing done here is 'free' since we're waiting for more data before doing additional
% analysis. Below I update the GUI.
if sampleCount > 0
statusCallback(hObject, experimentState, sampleCount, decision);
end
[timestamp, stopIndices] = this.getSnapShot();
states = this.getActiveStates();
end
% Store how many samples are acquired so we can skip over next
% loop if necessary
old_ch_stopIndex = stopIndices(ch);
oldStates = states;
% Acquire a sample into RAMControl.DataSample. Note that extraction is from the end of the buffer,
% not the start of the buffer. This means we're always taking the most current data, and discarding
% the oldest data. This is necessary because there is some 'jitter' when adding the CHUNK_SIZE samples,
% so sometimes there is CHUNK_SIZE + about 10 ms of data available.
try
this.extractSampleFromRingBuffer(stopIndices, timestamp);
catch e
exitCallback(getReport(e, 'extended', 'hyperlinks', 'off'));
statusCallback(hObject, experimentState, -1, false);
hObject.UserData = 0;
end
% this.doneSnapShot(stopIndices, keepSampleSize); % Indicate done with (most) of the first snapshot
% % ending at stopIndex
experimentState.sample = experimentState.sample + 1;
sampleCount = sampleCount + 1;
% Let the GUI know there was a problem with the Task Computer or NeuroPort and stop collecting data.
if isError || taskComputerStatus < 0
if errorNP
errordlg('NeuroPort has stopped responding. Must abort the experiment');
statusCallback(hObject, experimentState, -3, false);
else
statusCallback(hObject, experimentState, -1, false);
end
break;
elseif taskComputerStatus == 0 % Completed experiment
statusCallback(hObject, experimentState, -2, false);
break;
end
% This is where the stim decision is made. Also, once a second, StimControl will update the GUI
% but it won't necessarily be displayed unless waiting for data.
try
[decision, stopSession, stopSessionMessage] = ...
stimControl.stimChoice(experimentState, this.convertDigiToAnalog(this.DataSample));
catch e
exitCallback(getReport(e, 'extended', 'hyperlinks', 'off'))
statusCallback(hObject, experimentState, -1, false);
hObject.UserData = 0;
break
end
if stopSession
exitCallback(stopSessionMessage)
statusCallback(hObject, experimentState, -1, false);
hObject.UserData = 0;
break
end
% STIM ACTUALLY APPLIED HERE:
statusCallback(hObject, experimentState, sampleCount, decision);
% TODO: Code to set/stop stimulation here
end % while
% profile off
this.cleanupRingBuffer(); % Need to call before returning control to the test environment -- not sure why
stimControl.cleanup();
% Save the buffer sizes for latency analysis
end
function [timestamp, stopIndex] = getSnapShot(this)
% Acquire the 'read' pointers to the ring buffer, each 144x1 for each of the
% continuous and analog channels. Also return the count of the number of
% samples, and the timestamp of the first sample.
% NOTE: cbmex('snapshot', 'done', ...) must be called to release data saved
% in the ring buffer.
try
% TODO: Timestamp is currently a placeholder
[timestamp, stopIndex] = cbmex('snapshot', 'get', this.SAMPLE_SIZE);
catch err
warning([RAMControl.MSG_PREFIX 'Caught error. ' err.message]);
this.TurnOffUnusedThisWarning = false; % Put this someplace it is rarely executed
end % try/catch
end
function extractSampleFromRingBuffer(this, stopIndex, timestamp)
% Extract a reformatted data sample from the ring buffer using cbmex('snapshot', 'save')
% This function copies from the (INT16) ring buffer into the (double) DataSample matrix
% and accounts for the "edges" of the ring buffer. DataSample should be N x 144
% where N is the number of samples to copy and 144 is each of the continuous and
% analog channels.
% The value 'latency' is used as an offset into the ring buffer. Instead of extracting
% indicies stopIndex-size(RAMControl.DataSample,1)+1:stopIndex, the latency offset is
% applied: stopIndex-size(RAMControl.DataSample,1)+1-latency:stopIndex-latency.
% Surround all processing in a try/catch so that we can nicely close the cbmex interface.
try
cbmex('snapshot', 'save', stopIndex, RAMControl.DataSample, timestamp); % Acquire a sample
catch err
error([RAMControl.MSG_PREFIX, 'Cannot acquire a sample. ', err.message]);
this.TurnOffUnusedThisWarning = false; % Put this someplace it is rarely executed
end % try/catch
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment