Skip to content

Commit 8e96027

Browse files
committed
Merge branch 'master' of https://github.com/cortex-lab/Rigbox
2 parents 9020166 + e849fbf commit 8e96027

14 files changed

+149
-103
lines changed

+dat/paths.m

+5-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
end
1515

1616
server1Name = '\\zubjects.cortexlab.net';
17+
server2Name = '\\zserver.cortexlab.net';
1718
basketName = '\\basket.cortexlab.net'; % for working analyses
1819
lugaroName = '\\lugaro.cortexlab.net'; % for tape backup
1920

@@ -23,6 +24,8 @@
2324
p.rigbox = fileparts(which('addRigboxPaths'));
2425
% Repository for local copy of everything generated on this rig
2526
p.localRepository = 'C:\LocalExpData';
27+
p.localAlyxQueue = 'C:\localAlyxQueue';
28+
p.databaseURL = 'https://alyx.cortexlab.net';
2629

2730
% Under the new system of having data grouped by mouse
2831
% rather than data type, all experimental data are saved here.
@@ -31,11 +34,11 @@
3134
% directory for organisation-wide configuration files, for now these should
3235
% all remain on zserver
3336
% p.globalConfig = fullfile(p.rigbox, 'config');
34-
p.globalConfig = fullfile(server1Name, 'Code', 'Rigging', 'config');
37+
p.globalConfig = fullfile(server2Name, 'Code', 'Rigging', 'config');
3538
% directory for rig-specific configuration files
3639
p.rigConfig = fullfile(p.globalConfig, rig);
3740
% repository for all experiment definitions
38-
p.expDefinitions = fullfile(server1Name, 'Code', 'Rigging', 'ExpDefinitions');
41+
p.expDefinitions = fullfile(server2Name, 'Code', 'Rigging', 'ExpDefinitions');
3942

4043
% repository for working analyses that are not meant to be stored
4144
% permanently

+eui/AlyxPanel.m

+35-6
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@
4343
NewExpSubject % Drop-down menu subject list
4444
LoginText % Text displaying whether/which user is logged in
4545
LoginButton % Button to log in to Alyx
46+
WeightButton % Button to submit weight to Alyx
4647
WaterEntry % Text box for entering the amout of water to give
4748
IsHydrogel % UI checkbox indicating whether to water to be given is in gel form
4849
WaterRequiredText % Handle to text UI element displaying the water required
4950
WaterRemainingText % Handle to text UI element displaying the water remaining
5051
LoginTimer % Timer to keep track of how long the user has been logged in, when this expires the user is automatically logged out
52+
WeightTimer % Timer to reset weight button text when scale no longer gives new readings
5153
WaterRemaining % Holds the current water required for the selected subject
5254
end
5355

@@ -134,7 +136,7 @@
134136
'Enable', 'off',...
135137
'Callback', @(~,~)obj.viewAllSubjects);
136138
% Button to open a dialog for manually submitting a mouse weight
137-
uicontrol('Parent', waterbox,...
139+
obj.WeightButton = uicontrol('Parent', waterbox,...
138140
'Style', 'pushbutton', ...
139141
'String', 'Manual weighing', ...
140142
'Enable', 'off',...
@@ -206,6 +208,11 @@ function delete(obj)
206208
delete(obj.LoginTimer) % ... delete it...
207209
obj.LoginTimer = []; % ... and remove it
208210
end
211+
if ~isempty(obj.WeightTimer) % If there is a timer object
212+
stop(obj.WeightTimer) % Stop the timer...
213+
delete(obj.WeightTimer) % ... delete it...
214+
obj.WeightTimer = []; % ... and remove it
215+
end
209216
end
210217

211218
function login(obj)
@@ -225,7 +232,7 @@ function login(obj)
225232
% minutes of 'inactivity' (defined as not calling
226233
% dispWaterReq)
227234
obj.LoginTimer = timer('StartDelay', 30*60, 'TimerFcn',...
228-
@(~,~)obj.login, 'BusyMode', 'queue');
235+
@(~,~)obj.login, 'BusyMode', 'queue', 'Name', 'Login Timer');
229236
start(obj.LoginTimer)
230237
% Enable all buttons
231238
set(findall(obj.RootContainer, '-property', 'Enable'), 'Enable', 'on');
@@ -511,15 +518,15 @@ function viewSubjectHistory(obj, ax)
511518
% collect the data for the table
512519
endpnt = sprintf('water-requirement/%s?start_date=2016-01-01&end_date=%s', obj.Subject, datestr(now, 'yyyy-mm-dd'));
513520
wr = obj.AlyxInstance.getData(endpnt);
514-
iw = wr.implant_weight;
521+
iw = iff(isempty(wr.implant_weight), 0, wr.implant_weight);
515522
records = catStructs(wr.records, nan);
516-
expected = [records.weight_expected];
517-
expected(expected==0) = nan;
518523
% no weighings found
519524
if isempty(wr.records)
520525
obj.log('No weight data found for subject %s', obj.Subject);
521526
return
522527
end
528+
expected = [records.weight_expected];
529+
expected(expected==0) = nan;
523530
dates = cellfun(@(x)datenum(x), {records.date});
524531

525532
% build the figure to show it
@@ -537,10 +544,12 @@ function viewSubjectHistory(obj, ax)
537544
plot(ax, dates, ((expected-iw)*0.7)+iw, 'r', 'LineWidth', 2.0);
538545
plot(ax, dates, ((expected-iw)*0.8)+iw, 'LineWidth', 2.0, 'Color', [244, 191, 66]/255);
539546
box(ax, 'off');
547+
% Change the plot x axis limits
540548
if numel(dates) > 1; xlim(ax, [min(dates) max(dates)]); end
541549
if nargin == 1
542550
set(ax, 'XTickLabel', arrayfun(@(x)datestr(x, 'dd-mmm'), get(ax, 'XTick'), 'uni', false))
543551
else
552+
xticks(ax, 'auto')
544553
ax.XTickLabel = arrayfun(@(x)datestr(x, 'dd-mmm'), get(ax, 'XTick'), 'uni', false);
545554
end
546555
ylabel(ax, 'weight (g)');
@@ -582,7 +591,7 @@ function viewSubjectHistory(obj, ax)
582591
dat = horzcat(...
583592
arrayfun(@(x)datestr(x), dates', 'uni', false), ...
584593
weightsByDate', ...
585-
arrayfun(@(x)sprintf('%.1f', 0.8*(x-iw)), [records.weight_expected]', 'uni', false), ...
594+
arrayfun(@(x)sprintf('%.1f', 0.8*(x-iw)+iw), [records.weight_expected]', 'uni', false), ...
586595
weightPctByDate');
587596
waterDat = (...
588597
num2cell(horzcat([records.water_given]', [records.hydrogel_given]', ...
@@ -625,6 +634,26 @@ function viewAllSubjects(obj)
625634
end
626635
end
627636

637+
function updateWeightButton(obj, src, ~)
638+
% Function for changing the text on the weight button to reflect the
639+
% current weight value obtained by the scale. This function must be
640+
% a callback for the hw.WeighingScale NewReading event. If a new
641+
% reading isn't read for 10 sec the manual weighing option is made
642+
% available instead.
643+
%
644+
% Example:
645+
% aiPanel = eui.AlyxPanel;
646+
% lh = event.listener(obj.WeighingScale, 'NewReading',...
647+
% @(src,evt)aiPanel.updateWeightButton(src,evt));
648+
%
649+
% See also hw.WeighingScale, eui.MControl
650+
set(obj.WeightButton, 'String', sprintf('Record %.1fg', src.readGrams), 'Callback', @(~,~)obj.recordWeight(src.readGrams))
651+
obj.WeightTimer = timer('Name', 'Last Weight',...
652+
'TimerFcn', @(~,~)set(obj.WeightButton, 'String', 'Manual weighing', 'Callback', @(~,~)obj.recordWeight),...
653+
'StopFcn', @(src,~)delete(src), 'StartDelay', 10);
654+
start(obj.WeightTimer)
655+
end
656+
628657
function log(obj, varargin)
629658
% Function for displaying timestamped information about
630659
% occurrences. If the LoggingDisplay property is unset, the

+eui/ExpPanel.m

+4-2
Original file line numberDiff line numberDiff line change
@@ -389,10 +389,12 @@ function build(obj, parent)
389389
obj.StopButtons = [...
390390
uicontrol('Parent', buttonpanel,...
391391
'Style', 'pushbutton',...
392-
'String', 'End'),...
392+
'String', 'End',...
393+
'TooltipString', 'End experiment'),...
393394
uicontrol('Parent', buttonpanel,...
394395
'Style', 'pushbutton',...
395-
'String', 'Abort')];
396+
'String', 'Abort',...
397+
'TooltipString', 'Abort experiment without posting water to Alyx')];
396398
set(obj.StopButtons, 'Enable', 'off', 'Visible', 'off');
397399
uicontrol('Parent', buttonpanel,...
398400
'Style', 'pushbutton',...

+eui/MControl.m

+5-1
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,11 @@
9090
if isfield(rig, 'scale') && ~isempty(rig.scale)
9191
obj.WeighingScale = fieldOrDefault(rig, 'scale');
9292
init(obj.WeighingScale);
93+
% Add listners for new reading, both for the log tab and also for
94+
% the weigh button in the Alyx Panel.
9395
obj.Listeners = [obj.Listeners,...
94-
{event.listener(obj.WeighingScale, 'NewReading', @obj.newScalesReading)}];
96+
{event.listener(obj.WeighingScale, 'NewReading', @obj.newScalesReading)}...
97+
{event.listener(obj.WeighingScale, 'NewReading', @(src,evt)obj.AlyxPanel.updateWeightButton(src,evt))}];
9598
end
9699
catch
97100
obj.log('Warning: could not connect to weighing scales');
@@ -617,6 +620,7 @@ function plotWeightReading(obj)
617620
MinSignificantWeight = 5; %grams
618621
if g >= MinSignificantWeight
619622
obj.WeightReadingPlot = obj.WeightAxes.scatter(floor(now), g, 20^2, 'p', 'filled');
623+
obj.WeightAxes.XLim = [min(get(obj.WeightAxes.Handle, 'XTick')) max(now)];
620624
set(obj.RecordWeightButton, 'Enable', 'on', 'String', sprintf('Record %.1fg', g));
621625
end
622626
end

+eui/ParamEditor.m

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ function build(obj, parent) % Build parameters panel
107107
obj.SetValuesButton = uicontrol('Parent', conditionButtonBox,...
108108
'Style', 'pushbutton',...
109109
'String', 'Set values',...
110-
'TooltipString', sprintf('Set selected values to specified value, range or function'),...
110+
'TooltipString', 'Set selected values to specified value, range or function',...
111111
'Enable', 'off',...
112112
'Callback', @(~, ~) obj.setSelectedValues());
113113

+exp/SignalsExp.m

+2-2
Original file line numberDiff line numberDiff line change
@@ -904,8 +904,8 @@ function saveData(obj)
904904
contL = getOr(obj.Data.events, 'contrastLeftValues', NaN);
905905
contR = getOr(obj.Data.events, 'contrastRightValues', NaN);
906906
if ~any(isnan(contL))&&~any(isnan(contR))
907-
writeNPY(contL(:)*100, fullfile(expPath, 'cwStimOn.contrastLeft.npy'));
908-
writeNPY(contR(:)*100, fullfile(expPath, 'cwStimOn.contrastRight.npy'));
907+
writeNPY(contL(:), fullfile(expPath, 'cwStimOn.contrastLeft.npy'));
908+
writeNPY(contR(:), fullfile(expPath, 'cwStimOn.contrastRight.npy'));
909909
else
910910
warning('No ''contrastLeft'' and/or ''contrastRight'' events recorded, cannot register to Alyx')
911911
end

+hw/Timeline.m

+4-6
Original file line numberDiff line numberDiff line change
@@ -444,20 +444,18 @@ function stop(obj)
444444
obj.Data.currSysTimeTimelineOffset = CurrSysTimeTimelineOffset;
445445

446446
% saving hardware metadata for each output
447-
warning('off', 'MATLAB:structOnObject'); % sorry, don't care
448447
for outIdx = 1:numel(obj.Outputs)
449-
s = struct(obj.Outputs(outIdx));
450-
s.Class = class(obj.Outputs(outIdx));
448+
s = obj2struct(obj.Outputs(outIdx));
451449
obj.Data.hw.Outputs{outIdx} = s;
452450
end
453-
warning('on', 'MATLAB:structOnObject');
454451

455452
% save tl to all paths
456453
superSave(obj.Data.savePaths, struct('Timeline', obj.Data));
457454

458455
% write hardware info to a JSON file for compatibility with database
459-
hw = jsonencode(obj.Data.hw); %#ok<NASGU>
460-
save(fullfile(fileparts(obj.Data.savePaths{2}), 'TimelineHW.json'), 'hw', '-ascii');
456+
fid = fopen(fullfile(fileparts(obj.Data.savePaths{2}), 'TimelineHW.json'), 'w');
457+
fprintf(fid, '%s', jsonencode(obj.Data.hw));
458+
fclose(fid);
461459

462460
% save each recorded vector into the correct format in Timeline
463461
% timebase for Alyx and optionally into universal timebase if

+hw/devices.m

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
[status, hash] = system(sprintf('git -C "%s" rev-parse HEAD',...
3939
fileparts(which('addRigboxPaths'))));
4040
if status == 0
41-
rig.GitHash = hash;
41+
rig.GitHash = strtrim(hash);
4242
end
4343

4444
%% Configure common devices, if present

+srv/expServer.m

+17-6
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ function handleMessage(id, data, host)
177177
communicator.send(id, []);
178178
try
179179
communicator.send('status', {'starting', expRef});
180-
runExp(expRef, preDelay, postDelay, Alyx);
180+
aborted = runExp(expRef, preDelay, postDelay, Alyx);
181181
log('Experiment ''%s'' completed', expRef);
182-
communicator.send('status', {'completed', expRef});
182+
communicator.send('status', {'completed', expRef, aborted});
183183
catch runEx
184184
communicator.send('status', {'expException', expRef, runEx.message});
185185
log('Exception during experiment ''%s'' because ''%s''', expRef, runEx.message);
@@ -194,6 +194,7 @@ function handleMessage(id, data, host)
194194
if ~isempty(experiment)
195195
immediately = args{1};
196196
AlyxInstance = args{2};
197+
AlyxInstance.Headless = true;
197198
if immediately
198199
log('Aborting experiment');
199200
else
@@ -203,12 +204,13 @@ function handleMessage(id, data, host)
203204
experiment.AlyxInstance = AlyxInstance;
204205
end
205206
experiment.quit(immediately);
206-
send(communicator, id, immediately);
207+
send(communicator, id, []);
207208
else
208209
log('Quit message received but no experiment is running\n');
209210
end
210211
case 'updateAlyxInstance' %recieved new Alyx Instance from Stimulus Control
211212
AlyxInstance = args{1}; %get struct
213+
AlyxInstance.Headless = true;
212214
if ~isempty(AlyxInstance)
213215
experiment.AlyxInstance = AlyxInstance; %set property for current experiment
214216
end
@@ -217,7 +219,7 @@ function handleMessage(id, data, host)
217219
end
218220
end
219221

220-
function runExp(expRef, preDelay, postDelay, Alyx)
222+
function aborted = runExp(expRef, preDelay, postDelay, Alyx)
221223
% disable ptb keyboard listening
222224
KbQueueRelease();
223225

@@ -248,15 +250,24 @@ function runExp(expRef, preDelay, postDelay, Alyx)
248250
experiment.AlyxInstance = Alyx; % add Alyx Instance
249251
experiment.run(expRef); % run the experiment
250252
communicator.EventMode = false; % back to pull message mode
253+
aborted = strcmp(experiment.Data.endStatus, 'aborted');
251254
% clear the active experiment var
252255
experiment = [];
253256
rig.stimWindow.BackgroundColour = bgColour;
254257
rig.stimWindow.flip(); % clear the screen after
255258

256259
% save a copy of the hardware in JSON
257-
jsonData = obj2json(rig); %#ok<NASGU>
258260
name = dat.expFilePath(expRef, 'hw-info', 'master');
259-
save([name(1:end-3) 'json'], 'jsonData', '-ascii');
261+
fid = fopen([name(1:end-3) 'json'], 'w');
262+
fprintf(fid, '%s', obj2json(rig));
263+
fclose(fid);
264+
if ~strcmp(dat.parseExpRef(expRef), 'default')
265+
try
266+
Alyx.registerFile([name(1:end-3) 'json']);
267+
catch ex
268+
warning(ex.identifier, 'Failed to register hardware info: %s', ex.message);
269+
end
270+
end
260271

261272
if rig.timeline.UseTimeline
262273
%stop the timeline system

cb-tools/obj2json.m

-73
Original file line numberDiff line numberDiff line change
@@ -8,77 +8,4 @@
88
else
99
s = jsonencode(s, 'ConvertInfAndNaN', true);
1010
end
11-
end
12-
13-
function s = obj2struct(obj)
14-
% OBJ2STRUCT Converts input object into a struct
15-
% Returns the input but with any non-fundamental object converted to a
16-
% structure. If the input does not contain an object, the resulting
17-
% output will remain unchanged.
18-
%
19-
% NB: Does not convert National Instruments object or objects within
20-
% non-scalar structures. Cannot currently deal with Java, COM or certain
21-
% graphics objects. Function handles are converted to strings.
22-
%
23-
% 2018-05-03 MW created
24-
25-
if isobject(obj)
26-
if length(obj) > 1
27-
% If dealing with heterogeneous array of objects, recurse through array
28-
s = arrayfun(@obj2struct, obj, 'uni', 0);
29-
elseif isa(obj, 'containers.Map')
30-
% Convert to scalar struct
31-
keySet = keys(obj);
32-
valueSet = values(obj);
33-
for j = 1:length(keySet)
34-
m.(keySet{j}) = valueSet{j};
35-
end
36-
s = obj2struct(m);
37-
else % Normal object
38-
names = fieldnames(obj); % Get list of public properties
39-
for i = 1:length(names)
40-
if isobject(obj.(names{i})) % Property contains an object
41-
if startsWith(class(obj.(names{i})),'daq.ni.')
42-
% Do not attempt to save ni daq sessions of channels
43-
s.(names{i}) = [];
44-
else % Recurse
45-
s.(names{i}) = obj2struct(obj.(names{i}));
46-
end
47-
elseif iscell(obj.(names{i}))
48-
% If property contains cell array, run through each element in case
49-
% any contain an object
50-
s.(names{i}) = cellfun(@obj2struct, obj.(names{i}), 'uni', 0);
51-
elseif isstruct(obj.(names{i})) && isscalar(obj.(names{i}))
52-
% If property contains struct, run through each field in case any
53-
% contain an object
54-
s.(names{i}) = structfun(@obj2struct, obj.(names{i}), 'uni', 0);
55-
elseif isa(obj.(names{i}), 'function_handle')
56-
% Convert function to string
57-
s.(names{i}) = func2str(obj.(names{i}));
58-
elseif isa(obj.(names{i}), 'containers.Map')
59-
% Convert to scalar struct
60-
keySet = keys(obj.(names{i}));
61-
valueSet = values(obj.(names{i}));
62-
for j = 1:length(keySet)
63-
m.(keySet{j}) = valueSet{j};
64-
end
65-
s.(names{i}) = obj2struct(m);
66-
else % Property is fundamental object
67-
s.(names{i}) = obj.(names{i});
68-
end
69-
end
70-
s.ClassContructor = class(obj); % Supply class name for loading object
71-
end
72-
elseif iscell(obj)
73-
% If dealing with cell array, recurse through elements
74-
s = cellfun(@obj2struct, obj, 'uni', 0);
75-
elseif isstruct(obj) && isscalar(obj)
76-
% If dealing with structure, recurse through fields
77-
s = structfun(@obj2struct, obj, 'uni', 0);
78-
elseif isa(obj, 'function_handle')
79-
% Convert function to string
80-
s = func2str(obj);
81-
else % Fundamental object, return unchanged
82-
s = obj;
83-
end
8411
end

0 commit comments

Comments
 (0)