-
-
Save isaac-ped/da6ac87f33fce66ee66e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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, ×tamp, 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()" ); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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" \ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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