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
13 changes: 9 additions & 4 deletions +hw/TLOutput.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@
% 2018-01 NS created

properties
Name % The name of the timeline output, for easy identification
Enable = true % Will not do anything with it unless this is true
Verbose = false % Flag to output status updates. Initialization message outputs regardless of verbose.
% The name of the timeline output, for easy identification
Name
% Will not do anything with it unless this is true
Enable matlab.lang.OnOffSwitchState = 'on'
% Flag to output status updates. Initialization message outputs
% regardless of verbose.
Verbose matlab.lang.OnOffSwitchState = 'off'
end

properties (Transient, Hidden, Access = protected)
Session % Holds an NI DAQ session object
% Holds an NI DAQ session object
Session
end

methods (Abstract)
Expand Down
85 changes: 61 additions & 24 deletions +hw/Timeline.m
Original file line number Diff line number Diff line change
Expand Up @@ -67,44 +67,81 @@
% 2017-10 MW updated

properties
DaqVendor = 'ni' % 'ni' is using National Instruments USB-6211 DAQ
DaqIds = 'Dev1' % Device ID can be found with daq.getDevices()
DaqSampleRate = 1000 % rate at which daq aquires data in Hz, see Rate
DaqSamplesPerNotify % determines the number of data samples to be processed each time, see Timeline.process(), constructor and NotifyWhenDataAvailableExceeds
Outputs = hw.TLOutputChrono % array of output classes, defining any signals you desire to be sent from the daq. See Also HW.TLOUTPUT, HW.TLOUTPUTCLOCK
% 'ni' is using National Instruments USB-6211 DAQ
DaqVendor = 'ni'
% Device ID can be found with daq.getDevices()
DaqIds = 'Dev1'
% rate at which daq aquires data in Hz, see Rate
DaqSampleRate = 1000
% determines the number of data samples to be processed each time,
% see Timeline.process(), constructor and
% NotifyWhenDataAvailableExceeds
DaqSamplesPerNotify
% array of output classes, defining any signals you desire to be
% sent from the daq. See Also HW.TLOUTPUT, HW.TLOUTPUTCLOCK
Outputs = hw.TLOutputChrono
% All configured inputs.
Inputs = struct('name', 'chrono',...
'arrayColumn', -1,... % -1 is default indicating unused, this is update when the channels are added during tl.start()
'arrayColumn', -1,... -1 is default indicating unused, this is update when the channels are added during tl.start()
'daqChannelID', 'ai0',...
'measurement', 'Voltage',...
'terminalConfig', 'SingleEnded',...
'axesScale', 1) % multiplicative vertical scaling for when live plotting the input
UseInputs = {'chrono'} % array of inputs to record while tl is running
StopDelay = 2 % currently pauses for at least 2 secs as 'hack' before stopping main DAQ session to allow
MaxExpectedDuration = 2*60*60 % expected experiment time so data structure is initialised to sensible size (in secs)
AquiredDataType = 'double' % default data type for the acquired data array (i.e. Data.rawDAQData)
UseTimeline = false % used by expServer. If true, timeline is started by default (otherwise can be toggled with the t key)
LivePlot = false % if true the data are plotted as the data are aquired
FigureScale = [0 0 1 1]; % figure position in normalized units, default is full screen
WriteBufferToDisk = false % if true the data buffer is written to disk as they're aquired NB: in the future this will happen by default
% array of inputs to record while tl is running
UseInputs = {'chrono'}
% currently pauses for at least 2 secs as 'hack' before stopping
% main DAQ session to allow
StopDelay = 2
% expected experiment time so data structure is initialised to
% sensible size (in secs)
MaxExpectedDuration = 2*60*60
% default data type for the acquired data array (i.e.
% Data.rawDAQData)
AquiredDataType = 'double'
% If true, timeline is started by default (otherwise can be toggled
% with the t key in expServer)
UseTimeline matlab.lang.OnOffSwitchState = 'off'
% if true the data are plotted as the data are aquired
LivePlot matlab.lang.OnOffSwitchState = 'off'
% figure position in normalized units, default is full screen
FigureScale = [0 0 1 1]
% if true the data buffer is written to disk as they're aquired NB:
% in the future this will happen by default
WriteBufferToDisk matlab.lang.OnOffSwitchState = 'off'
end

properties (Dependent)
SamplingInterval % defined as 1/DaqSampleRate
IsRunning = false % flag is set to true when the first chrono pulse is aquired and set to false when tl is stopped (and everything saved), see tl.process and tl.stop
% Sampling interval defined as 1/DaqSampleRate
SamplingInterval
% Switch set to true when the first chrono pulse is aquired and
% set to false when tl is stopped (and everything saved), see
% tl.process and tl.stop
IsRunning matlab.lang.OnOffSwitchState = 'off'
end

properties (Transient, Access = protected)
Listener % holds the listener for 'DataAvailable', see DataAvailable and Timeline.process()
LastTimestamp % the last timestamp returned from the daq during the DataAvailable event. Used to check sampling continuity, see tl.process()
Ref % the expRef string. See tl.start()
AlyxInstance % a struct contraining the Alyx token, user and url for ile registration. See tl.start()
Data % A structure containing timeline data
Axes % A figure handle for plotting the aquired data as it's processed
DataFID % The data file ID for writing aquired data directly to disk
% holds the listener for 'DataAvailable', see DataAvailable and
% Timeline.process()
Listener
% the last timestamp returned from the daq during the DataAvailable
% event. Used to check sampling continuity, see tl.process()
LastTimestamp
% the expRef string. See tl.start()
Ref
% a struct contraining the Alyx token, user and url for ile
% registration. See tl.start()
AlyxInstance
% A structure containing timeline data
Data
% A figure handle for plotting the aquired data as it's processed
Axes
% The data file ID for writing aquired data directly to disk
DataFID
end

properties (Transient, SetAccess = protected, GetAccess = {?hw.Timeline, ?hw.TLOutput})
Sessions = containers.Map % map of daq sessions and their channels, created at tl.start()
% Map of daq sessions and their channels, created at tl.start()
Sessions = containers.Map
end

methods
Expand Down
47 changes: 41 additions & 6 deletions tests/ParamEditor_test.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
classdef (SharedTestFixtures={matlab.unittest.fixtures.PathFixture(...
[fileparts(mfilename('fullpath')) '\fixtures'])})... % add 'fixtures' folder as test fixture
classdef (SharedTestFixtures={ % add 'fixtures' folder as test fixture
matlab.unittest.fixtures.PathFixture('fixtures'),...
matlab.unittest.fixtures.PathFixture(['fixtures' filesep 'util'])})...
ParamEditor_test < matlab.unittest.TestCase

properties
Expand Down Expand Up @@ -259,11 +260,45 @@ function test_deleteCondition(testCase)
[PE.Parameters.numTrialConditions, numel(PE.Parameters.TrialSpecificNames)])
end

function test_setValues(testCase)
% TODO Add test for the set values button. For now let's fail this
function test_setSelectedValues(testCase)
% (1:10:100) % Sets selected rows to [1 11 21 31 41 51 61 71 81 91]
% @(~)randi(100) % Assigned random integer to each selected row
% @(a)a*50 % Multiplies each condition value by 50
% false % Sets all selected rows to false
testCase.assertTrue(~testCase.Changed, 'Changed flag incorrect')
% PE = testCase.ParamEditor;
testCase.assertTrue(false, 'Test not implemented')
mock = MockDialog.instance('char');
mock.InTest = true;
mock.UseDefaults = false;
mock.Dialogs('Set values') = fun.CellSeq.create({'(1:10:100)', '@(~)randi(100)', '@(a)a*50'});

% Select some cells to set
event.Indices = [(1:5)' ones(5,1)*4];
selection_fn = testCase.Table.CellSelectionCallback;
selection_fn([],event)

% Retrieve function handle for set condition
callback_fn = pick(findobj(testCase.Figure,...
'String', 'Set values'), 'Callback');
callback_fn()

% Verify Changed event triggered
testCase.verifyTrue(testCase.Changed, ...
'Failed to notify listeners of parameter change')

P = testCase.ParamEditor.Parameters;
% Verify values changed
expected = (1:10:100);
testCase.verifyEqual(P.Struct.numRepeats(1:5), expected(1:5), ...
'Failed to modify parameters')

callback_fn()
values = P.Struct.numRepeats(1:5);
testCase.verifyTrue(all(values < 100), ...
'Failed to modify parameters')

callback_fn()
testCase.verifyEqual(P.Struct.numRepeats(1:5)/50, values, ...
'Failed to modify parameters')
end

function test_sortByColumn(testCase)
Expand Down
17 changes: 8 additions & 9 deletions tests/fixtures/util/MockDialog.m
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ function reset(obj)
end
case 'inputdlg'
% Find key
if ~strcmp(obj.Dialogs.KeyType, 'char')
if ~strcmp(obj.Dialogs.KeyType, 'char') && ~obj.UseDefaults
key = obj.fromCount;
elseif isempty(varargin)
key = 'Input';
Expand All @@ -141,6 +141,10 @@ function reset(obj)
if isa(answer, 'fun.CellSeq')
answer = answer.first;
obj.Dialogs(key) = obj.Dialogs(key).rest;
elseif isa(answer, 'fun.EmptySeq')
warning('MockDialog:NewCall:EmptySeq', ...
'End of input sequence, using default input instead')
answer = def;
end

% inputdlg always returns a cell
Expand All @@ -152,14 +156,9 @@ function reset(obj)

methods (Access = private)

function key = fromCount()
if strcmp(obj.Dialogs.KeyType, 'char')
key = [];
return
elseif ~obj.UseDefaults
assert(obj.Dialogs.Count > 0, 'MockDialog:newCall:NoValuesSet', ...
'No values saved in Dialogs property')
end
function key = fromCount(obj)
assert(obj.Dialogs.Count > 0, 'MockDialog:newCall:NoValuesSet', ...
'No values saved in Dialogs property')
key = obj.NumCalls;
if key > obj.Dialogs.Count
key = key - obj.Dialogs.Count*floor(key/uint32(obj.Dialogs.Count));
Expand Down