diff --git a/.github/workflows/runAllTests.yml b/.github/workflows/runAllTests.yml
index 2df473a0..ef3d7cd2 100644
--- a/.github/workflows/runAllTests.yml
+++ b/.github/workflows/runAllTests.yml
@@ -25,14 +25,14 @@ jobs:
- name: Setup environment
uses: matlab-actions/run-command@v2
with:
- command: addpath('Tests'); setupGithubEnvironment;
+ command: addpath('Tests/'); setupGithubEnvironment;
# Runs a set of commands using the runners shell
- name: Run all tests
uses: matlab-actions/run-tests@v2
with:
- source-folder: Functions/
- select-by-folder: Tests/BpodLib/
+ source-folder: Functions/; Tests/
+ select-by-folder: Tests/BpodLib/; Tests/Integrations/
test-windows:
runs-on: windows-latest
@@ -48,11 +48,11 @@ jobs:
- name: Setup environment
uses: matlab-actions/run-command@v2
with:
- command: addpath('Tests'); setupGithubEnvironment;
+ command: addpath('Tests/'); setupGithubEnvironment;
# Runs a set of commands using the runners shell
- name: Run all tests
uses: matlab-actions/run-tests@v2
with:
- source-folder: Functions/
- select-by-folder: Tests/BpodLib/
+ source-folder: Functions/; Tests/
+ select-by-folder: Tests/BpodLib/; Tests/Integrations/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..46ae116b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.asv
+.vscode
diff --git a/Bpod.m b/Bpod.m
index 26f950dc..4c8609ba 100644
--- a/Bpod.m
+++ b/Bpod.m
@@ -101,8 +101,7 @@ function Bpod(varargin)
end
end
end
-BpodLib.calibration.liquid.io.report(BpodSystem.CalibrationTables.LiquidCal)
-BpodLib.path.verifyPathing(BpodSystem);
+
function emulator_setup(varargin)
% Runs setup with emulator mode flag set to 'true'.
@@ -119,10 +118,12 @@ function emulator_setup(varargin)
BpodSystem.SetupHardware();
BpodSystem.InitializeGUI();
BpodSystem.Status.Initialized = true;
+BpodLib.calibration.liquid.io.report(BpodSystem.CalibrationTables.LiquidCal)
+BpodLib.path.verifyPathing(BpodSystem);
evalin('base', 'global BpodSystem')
function emulator_dialog
-% Launches a GUI indicating that hardware connection has failed.
+% Launches a non-modal GUI indicating that hardware connection has failed.
% Prompts the user to start emulator mode or close the program.
global BpodSystem
BpodErrorSound;
diff --git a/Functions/+BpodLib/+BpodObject/+setup/updatePathAndSettings.m b/Functions/+BpodLib/+BpodObject/+setup/updatePathAndSettings.m
index ad626348..525acc6f 100644
--- a/Functions/+BpodLib/+BpodObject/+setup/updatePathAndSettings.m
+++ b/Functions/+BpodLib/+BpodObject/+setup/updatePathAndSettings.m
@@ -120,10 +120,10 @@ function updatePathAndSettings(BpodSystem, varargin)
if strcmp(BpodLib.calibration.liquid.utils.checkCOM(BpodSystem), 'no')
if p.Results.verbose
warning('BpodLib:Calibration:Liquid:CheckCOM', 'Calibration file does not match the detected state machine''s USB serial port.');
- msg = msgbox(sprintf("Detected state machine USB serial port (%s) does not match the port used to create the current liquid calibration: (%s).\nPlease either initialize a multi-machine setup or re-run calibration.", ...
- BpodLib.utils.getCurrentCOM(BpodSystem), BpodSystem.CalibrationTables.LiquidCal.metadata.COM), ...
- 'Calibration USB Port mismatch', 'warn', 'modal');
- uiwait(msg)
+ BpodSystem.GUIHandles.LiquidCalWarningMessageBox = ...
+ msgbox(sprintf("Detected state machine USB serial port (%s) does not match the port used to create the current liquid calibration: (%s).\nPlease either initialize a multi-machine setup or re-run calibration.", ...
+ BpodLib.utils.getCurrentCOM(BpodSystem), BpodSystem.CalibrationTables.LiquidCal.metadata.COM), ...
+ 'Calibration USB Port mismatch', 'warn', 'non-modal');
end
end
catch err
diff --git a/Functions/+BpodLib/+launcher/+ui/loadProtocols.m b/Functions/+BpodLib/+launcher/+ui/loadProtocols.m
new file mode 100644
index 00000000..de678350
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/+ui/loadProtocols.m
@@ -0,0 +1,6 @@
+function loadProtocols(BpodSystem)
+% Update the list of available protocols in the GUI
+
+ProtocolNames = BpodLib.launcher.findProtocols(BpodSystem);
+set(BpodSystem.GUIHandles.ProtocolSelector, 'String', ProtocolNames);
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/+ui/loadSettings.m b/Functions/+BpodLib/+launcher/+ui/loadSettings.m
new file mode 100644
index 00000000..324a86e9
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/+ui/loadSettings.m
@@ -0,0 +1,9 @@
+function loadSettings(BpodSystem, protocolName, subjectName)
+% Populate UI with settings files for the selected subject and protocol
+
+settingsFileNames = BpodLib.launcher.findSettings(BpodSystem.Path.DataFolder, protocolName, subjectName);
+
+set(BpodSystem.GUIHandles.SettingsSelector, 'String', settingsFileNames);
+set(BpodSystem.GUIHandles.SettingsSelector,'Value',1);
+
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/+ui/loadSubjects.m b/Functions/+BpodLib/+launcher/+ui/loadSubjects.m
new file mode 100644
index 00000000..ae4dc10c
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/+ui/loadSubjects.m
@@ -0,0 +1,9 @@
+function loadSubjects(BpodSystem, protocolName)
+% Load all subjects in the data folder that have a folder for the given protocol
+
+subjectNames = BpodLib.launcher.findSubjects(BpodSystem.Path.DataFolder, protocolName, BpodSystem.GUIData.DummySubjectString);
+
+set(BpodSystem.GUIHandles.SubjectSelector,'String',subjectNames);
+set(BpodSystem.GUIHandles.SubjectSelector,'Value',1);
+
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/+ui/setDataFilePath.m b/Functions/+BpodLib/+launcher/+ui/setDataFilePath.m
new file mode 100644
index 00000000..cff40fba
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/+ui/setDataFilePath.m
@@ -0,0 +1,14 @@
+function setDataFilePath(BpodSystem, protocolName, subjectName)
+
+dataFolder = BpodSystem.Path.DataFolder;
+dataFilePath = BpodLib.launcher.createDataFilePath(dataFolder, protocolName, subjectName);
+localDir = dataFolder(max(find(dataFolder(1:end-1) == filesep)+1):end);
+[~, fileName, ext] = fileparts(dataFilePath);
+fileName = [fileName ext];
+
+BpodSystem.Path.CurrentDataFile = dataFilePath;
+
+set(BpodSystem.GUIHandles.DataFilePathDisplay, 'String',...
+ [filesep fullfile(localDir, subjectName, protocolName, 'Session Data') filesep],'interpreter','none');
+set(BpodSystem.GUIHandles.DataFileDisplay, 'String', fileName);
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/createDataFilePath.m b/Functions/+BpodLib/+launcher/createDataFilePath.m
new file mode 100644
index 00000000..f779713b
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/createDataFilePath.m
@@ -0,0 +1,20 @@
+function dataFilePath = createDataFilePath(dataFolder, protocolName, subjectName)
+% Create a file path for the data file to be saved
+%
+% Inputs
+% ------
+% dataFolder : char
+% The path to the data folder (e.g. Local Data/Data/)
+% protocolName : char
+% The name of the protocol
+% subjectName : char
+% The name of the subject
+
+dateInfo = datestr(now, 30);
+dateInfo(dateInfo == 'T') = '_';
+fileName = [subjectName '_' protocolName '_' dateInfo '.mat'];
+
+
+dataFilePath = fullfile(dataFolder, subjectName, protocolName, 'Session Data', fileName);
+
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/createDefaultSettingsFile.m b/Functions/+BpodLib/+launcher/createDefaultSettingsFile.m
new file mode 100644
index 00000000..e2962d55
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/createDefaultSettingsFile.m
@@ -0,0 +1,11 @@
+function createDefaultSettingsFile(dataFolder, subjectName, protocolName)
+% Create a default (empty) settings file for the given subject and protocol
+% Only creates the file if it does not already exist
+
+defaultSettingsFilePath = fullfile(dataFolder, subjectName, protocolName, 'Session Settings', 'DefaultSettings.mat');
+if ~exist(defaultSettingsFilePath)
+ ProtocolSettings = struct;
+ save(defaultSettingsFilePath, 'ProtocolSettings')
+end
+
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/findProtocols.m b/Functions/+BpodLib/+launcher/findProtocols.m
new file mode 100644
index 00000000..44775384
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/findProtocols.m
@@ -0,0 +1,50 @@
+function ProtocolNames = findProtocols(BpodSystem)
+% ProtocolNames = findProtocols(BpodSystem)
+% Returns a cell array of protocol names found in the ProtocolFolder
+
+
+if strcmp(BpodSystem.Path.ProtocolFolder, BpodSystem.SystemSettings.ProtocolFolder) % todo: make this less janky?
+ startPos = 3;
+else
+ startPos = 2;
+end
+Candidates = dir(BpodSystem.Path.ProtocolFolder);
+ProtocolNames = cell(0);
+nProtocols = 0;
+for x = startPos:length(Candidates)
+ if Candidates(x).isdir
+ ProtocolFolder = fullfile(BpodSystem.Path.ProtocolFolder, Candidates(x).name);
+ Contents = dir(ProtocolFolder);
+ nItems = length(Contents);
+ Found = 0;
+ for y = 3:nItems
+ if strcmp(Contents(y).name, [Candidates(x).name '.m'])
+ Found = 1;
+ end
+ end
+ if Found
+ ProtocolName = Candidates(x).name;
+ else
+ ProtocolName = ['<' Candidates(x).name '>'];
+ end
+ nProtocols = nProtocols + 1;
+ ProtocolNames{nProtocols} = ProtocolName;
+ end
+end
+
+if isempty(ProtocolNames)
+ ProtocolNames = {'No Protocols Found'};
+else
+ % Sort to put organizing directories first
+ Types = ones(1,nProtocols);
+ for i = 1:nProtocols
+ ProtocolName = ProtocolNames{i};
+ if ProtocolName(1) == '<'
+ Types(i) = 0;
+ end
+ end
+ [a, Order] = sort(Types);
+ ProtocolNames = ProtocolNames(Order);
+end
+
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/findSettings.m b/Functions/+BpodLib/+launcher/findSettings.m
new file mode 100644
index 00000000..0150a7ca
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/findSettings.m
@@ -0,0 +1,18 @@
+function settingsFileNames = findSettings(dataFolder, protocolName, subjectName)
+% Find all settings files in the Settings folder for a given protocol and subject
+
+settingsPath = fullfile(dataFolder, subjectName, protocolName, 'Session Settings');
+candidates = dir(settingsPath);
+nSettingsFiles = 0;
+settingsFileNames = cell(1);
+for x = 3:length(candidates)
+ extension = candidates(x).name;
+ extension = extension(end-2:end);
+ if strcmp(extension, 'mat')
+ nSettingsFiles = nSettingsFiles + 1;
+ name = candidates(x).name;
+ settingsFileNames{nSettingsFiles} = name(1:end-4);
+ end
+end
+
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/findSubjects.m b/Functions/+BpodLib/+launcher/findSubjects.m
new file mode 100644
index 00000000..520b2393
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/findSubjects.m
@@ -0,0 +1,21 @@
+function subjectNames = findSubjects(dataFolder, protocolName, dummySubjectString)
+% Find all subjects in the data folder that have a folder for the given protocol
+
+candidateSubjects = dir(dataFolder);
+subjectNames = cell(1);
+nSubjects = 1;
+subjectNames{1} = dummySubjectString;
+for x = 1:length(candidateSubjects)
+ if x > 2
+ if candidateSubjects(x).isdir
+ if ~strcmp(candidateSubjects(x).name, dummySubjectString)
+ testpath = fullfile(dataFolder,candidateSubjects(x).name,protocolName);
+ if exist(testpath) == 7
+ nSubjects = nSubjects + 1;
+ subjectNames{nSubjects} = candidateSubjects(x).name;
+ end
+ end
+ end
+ end
+end
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/launchProtocol.m b/Functions/+BpodLib/+launcher/launchProtocol.m
new file mode 100644
index 00000000..10bcbbd2
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/launchProtocol.m
@@ -0,0 +1,182 @@
+function launchProtocol(BpodSystem, protocolPointer, subjectName, varargin)
+% Launches a protocol for a given subject and settings file
+% launchProtocol(BpodSystem, protocolPointer, subjectName, settingsName, varargin)
+%
+% Arguments
+% ---------
+% BpodSystem : BpodObject
+% The Bpod system
+% protocolPointer : char
+% The name of the protocol to run
+% Because of ambiguity in names, can be a absolute/relative file path (e.g. 'Protocols/Group1/MyProtocol' or 'Group1/MyProtocol')
+% subjectName : char
+% The name of the subject
+%
+% Keyword Arguments
+% -----------------
+% settingsName : char, optional (default = 'DefaultSettings')
+% The name of the settings file
+% runProtocol : char, optional (default = true)
+% If false, the protocol will not be run, only the setup will be done
+% protocolvarargin : cell, optional (default = no additional arguments passed)
+% Additional arguments to pass to the protocol (i.e. MyProtocol(varargin{:}))
+
+p = inputParser();
+p.addParameter('settingsName', 'DefaultSettings', @ischar);
+p.addParameter('runProtocol', true, @islogical);
+p.addParameter('protocolvarargin', [], @iscell);
+p.parse(varargin{:});
+settingsName = p.Results.settingsName;
+
+% Generate path to protocol file
+% ? allow absolute paths to define protocols outside of protocol folder?
+CurrentProtocol = BpodLib.path.findProtocolFile(BpodSystem.SystemSettings.ProtocolFolder, protocolPointer);
+[protocolRunFolder, protocolName] = fileparts(CurrentProtocol);
+
+% Verify data path
+dataFilePath = BpodLib.launcher.createDataFilePath(BpodSystem.Path.DataFolder, protocolName, subjectName);
+protocolDataFolder = fileparts(dataFilePath);
+if ~exist(protocolDataFolder)
+ error(['Error starting protocol: Test subject "' subjectName...
+ '" must be added first, from the launch manager.'])
+end
+
+% Ensure that the settings file exists
+BpodLib.launcher.createDefaultSettingsFile(BpodSystem.Path.DataFolder, subjectName, protocolName);
+settingsFolderPath = fullfile(BpodSystem.Path.DataFolder, subjectName, protocolName, 'Session Settings');
+candidateSettingsFiles = dir(fullfile(settingsFolderPath, '*.mat'));
+settingsFileNames = {candidateSettingsFiles.name};
+if ~ismember([settingsName '.mat'], settingsFileNames)
+ error(['Error: Settings file: ' settingsName '.mat does not exist for test subject: '...
+ subjectName ' in protocol: ' protocolName '.'])
+end
+settingsFilePath = fullfile(BpodSystem.Path.DataFolder, subjectName, protocolName,...
+ 'Session Settings', [settingsName '.mat']);
+
+% Set BpodSystem status, protocol, and path fields for new session
+BpodSystem.Status.Live = 1;
+BpodSystem.Status.LastEvent = 0;
+BpodSystem.Path.Settings = settingsFilePath;
+BpodSystem.Path.CurrentDataFile = dataFilePath;
+BpodSystem.Status.CurrentProtocolName = protocolName;
+BpodSystem.Status.CurrentSubjectName = subjectName;
+settingStruct = load(BpodSystem.Path.Settings);
+F = fieldnames(settingStruct);
+fieldName = F{1};
+BpodSystem.ProtocolSettings = settingStruct.(fieldName);
+
+% Send metadata to Bpod Phone Home program (disabled pending a more stable server)
+% isOnline = BpodSystem.check4Internet();
+% if (isOnline == 1) && (BpodSystem.SystemSettings.PhoneHome == 1)
+ % BpodSystem.BpodPhoneHome(1); % Disabled until server migration. -JS July 2018
+% end
+
+% Set BpodSystem status flags
+BpodSystem.Status.BeingUsed = 1;
+BpodSystem.Status.SessionStartFlag = 1;
+
+% Record session start time
+BpodSystem.ProtocolStartTime = now*100000;
+BpodSystem.resetSessionClock();
+
+% Clear BpodSystem.Data
+BpodSystem.Data = struct;
+
+%% Setup Flex I/O Analog Input data
+% On Bpod r2+, if FlexIO channels are configured as analog, setup data file
+if BpodSystem.MachineType > 3
+ nAnalogChannels = sum(BpodSystem.HW.FlexIO_ChannelTypes == 2);
+ if nAnalogChannels > 0
+ % Setup binary data file
+ analogFilename = [BpodSystem.Path.CurrentDataFile(1:end-4) '_ANLG.dat'];
+ if BpodSystem.Status.RecordAnalog == 1
+ BpodSystem.AnalogDataFile = fopen(analogFilename,'w');
+ if BpodSystem.AnalogDataFile == -1
+ error(['Error: Could not open the analog data file: ' analogFilename])
+ end
+ end
+ BpodSystem.Status.nAnalogSamples = 0;
+
+ % Setup analog data struct
+ BpodSystem.Data.Analog = struct;
+ BpodSystem.Data.Analog.info = struct;
+ BpodSystem.Data.Analog.FileName = analogFilename;
+ BpodSystem.Data.Analog.nChannels = nAnalogChannels;
+ BpodSystem.Data.Analog.channelNumbers = find(BpodSystem.HW.FlexIO_ChannelTypes == 2);
+ BpodSystem.Data.Analog.SamplingRate = BpodSystem.HW.FlexIO_SamplingRate;
+ BpodSystem.Data.Analog.nSamples = 0;
+
+ % Add human-readable info about data fields to 'info struct
+ BpodSystem.Data.Analog.info.FileName = 'Complete path and filename of the binary file to which the raw data was logged';
+ BpodSystem.Data.Analog.info.nChannels = 'The number of Flex I/O channels configured as analog input';
+ BpodSystem.Data.Analog.info.channelNumbers = 'The indexes of Flex I/O channels configured as analog input';
+ BpodSystem.Data.Analog.info.SamplingRate = 'The sampling rate of the analog data. Units = Hz';
+ BpodSystem.Data.Analog.info.nSamples = 'The total number of analog samples captured during the behavior session';
+ BpodSystem.Data.Analog.info.Samples = 'Analog measurements captured. Rows are separate analog input channels. Units = Volts';
+ BpodSystem.Data.Analog.info.Timestamps = 'Time of each sample (computed from sample index and sampling rate)';
+ BpodSystem.Data.Analog.info.TrialNumber = 'Experimental trial during which each analog sample was captured';
+ BpodSystem.Data.Analog.info.TrialData = 'A cell array of Samples. Each cell contains samples captured during a single trial.';
+ end
+end
+
+%% Prepare GUI
+if ~isempty(BpodSystem.GUIHandles)
+ % ? these are not referenced anywhere
+ BpodSystem.GUIData.ProtocolName = protocolName;
+ BpodSystem.GUIData.SubjectName = subjectName;
+ BpodSystem.GUIData.SettingsFileName = settingsFilePath;
+
+ % Clear console GUI fields
+ set(BpodSystem.GUIHandles.CurrentStateDisplay, 'String', '---');
+ set(BpodSystem.GUIHandles.PreviousStateDisplay, 'String', '---');
+ set(BpodSystem.GUIHandles.LastEventDisplay, 'String', '---');
+ set(BpodSystem.GUIHandles.TimeDisplay, 'String', '0:00:00');
+ if sum(BpodSystem.InputsEnabled(BpodSystem.HW.Inputs == 'P')) == 0
+ warning(['All Bpod behavior ports are currently disabled.'...
+ 'If your protocol requires behavior ports, enable them from the settings menu.'])
+ end
+
+ % Disable analog viewer record button (fixed for session)
+ if BpodSystem.Status.AnalogViewer
+ set(BpodSystem.GUIHandles.RecordButton, 'Enable', 'off')
+ end
+
+ % Set console GUI run button
+ set(BpodSystem.GUIHandles.RunButton, 'cdata', BpodSystem.GUIData.PauseButton, 'TooltipString', 'Press to pause session');
+end
+
+%%
+% Add protocol folder to the path
+onPath = contains([pathsep, path, pathsep], [pathsep, BpodSystem.Path.CurrentProtocol, pathsep], 'IgnoreCase', ispc);
+if onPath
+ rmpath(fileparts(BpodSystem.Path.CurrentProtocol)) % this is here because errors might prevent any shutdown procedures (from previous run) from running
+end
+addpath(protocolRunFolder);
+BpodSystem.Path.CurrentProtocol = CurrentProtocol; % for removal from path on next run
+% ? could cd into protocolRunFolder instead of adding to path to resolve pathing issues
+
+%% Run the protocol!
+if ~p.Results.runProtocol
+ return
+end
+if BpodSystem.Status.Verbose
+ fprintf('%s Launched protocol: %s\n', datestr(now, 13), CurrentProtocol)
+end
+if isempty(p.Results.protocolvarargin)
+ % Cleanest easiest behaviour
+ run(CurrentProtocol);
+else
+ % If the user requested to pass additional arguments to the protocol
+ protocolFuncHandle = str2func(protocolName);
+ funcInfo = functions(protocolFuncHandle);
+ if ~strcmp(funcInfo.file, CurrentProtocol)
+ % In this situation the pathing to the protocol is clear within Protocols/ but
+ % there may be Path clashes from elsewhere. If the user is savvy enough to
+ % pass additional arguments, hopefully they can resolve this issue.
+ fprintf('Requested protocol: %s\n', CurrentProtocol)
+ fprintf('Found protocol: %s\n', funcInfo.file)
+ error('The function handle does not point to the correct file.');
+ end
+ protocolFuncHandle(p.Results.protocolvarargin{:});
+end
+
diff --git a/Functions/+BpodLib/+launcher/prepareDataFolders.m b/Functions/+BpodLib/+launcher/prepareDataFolders.m
new file mode 100644
index 00000000..fa0bf2a7
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/prepareDataFolders.m
@@ -0,0 +1,22 @@
+function prepareDataFolders(subjectDataFolder, protocolName)
+% prepareDataFolders(subjectDataFolder, protocolName)
+% Make standard folders for this protocol.
+% This will fail silently if the folders exist
+% Inputs
+% ------
+% subjectDataFolder : str
+% Path to the subject data folder (e.g. Bpod_Local/Data/FakeSubject/)
+% protocolName : str
+% Name of the protocol
+
+warning off % Suppress warning that directory already exists
+mkdir(subjectDataFolder, protocolName);
+mkdir(fullfile(subjectDataFolder, protocolName, 'Session Data'))
+mkdir(fullfile(subjectDataFolder, protocolName, 'Session Settings'))
+warning on
+
+% Ensure that a default settings file exists
+[dataFolder, subjectName] = fileparts(subjectDataFolder);
+BpodLib.launcher.createDefaultSettingsFile(dataFolder, subjectName, protocolName);
+
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+launcher/stopProtocol.m b/Functions/+BpodLib/+launcher/stopProtocol.m
new file mode 100644
index 00000000..1945410f
--- /dev/null
+++ b/Functions/+BpodLib/+launcher/stopProtocol.m
@@ -0,0 +1,67 @@
+function stopProtocol(BpodSystem)
+% End the current protocol session.
+% stopProtocol(BpodSystem)
+
+if ~isempty(BpodSystem.Status.CurrentProtocolName) & BpodSystem.Status.Verbose
+ disp(' ')
+ fprintf('%s Stopping protocol: %s\n', BpodLib.utils.isotime('time'), BpodSystem.Status.CurrentProtocolName)
+end
+warning off % Suppress warning, in case protocol folder has already been removed
+% This folder is added in launchProtocol.m
+rmpath(fileparts(BpodSystem.Path.CurrentProtocol));
+warning on
+BpodSystem.Status.BeingUsed = 0;
+BpodSystem.Status.CurrentProtocolName = '';
+BpodSystem.Path.Settings = '';
+BpodSystem.Status.Live = 0;
+if BpodSystem.EmulatorMode == 0
+ if BpodSystem.MachineType > 3
+ stop(BpodSystem.Timers.AnalogTimer);
+ try
+ fclose(BpodSystem.AnalogDataFile);
+ catch
+ end
+ end
+ BpodSystem.SerialPort.write('X', 'uint8');
+ pause(.1);
+ BpodSystem.SerialPort.flush;
+ if BpodSystem.MachineType > 3
+ BpodSystem.AnalogSerialPort.flush;
+ end
+ if isfield(BpodSystem.PluginSerialPorts, 'TeensySoundServer')
+ TeensySoundServer('end');
+ end
+end
+BpodSystem.Status.RecordAnalog = 1;
+BpodSystem.Status.InStateMatrix = 0;
+
+% Close protocol and plugin figures
+try
+ Figs = fields(BpodSystem.ProtocolFigures);
+ nFigs = length(Figs);
+ for x = 1:nFigs
+ try
+ close(eval(['BpodSystem.ProtocolFigures.' Figs{x}]));
+ catch
+
+ end
+ end
+ try
+ close(BpodNotebook)
+ catch
+ end
+ try
+ BpodSystem.analogViewer('end', []);
+ catch
+ end
+catch
+end
+if ~isempty(BpodSystem.GUIHandles)
+ set(BpodSystem.GUIHandles.RunButton, 'cdata', BpodSystem.GUIData.GoButton,...
+ 'TooltipString', 'Launch behavior session');
+end
+if BpodSystem.Status.Pause == 1
+ BpodSystem.Status.Pause = 0;
+end
+% ---- end Shut down Plugins
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+multi/isMultiSetup.m b/Functions/+BpodLib/+multi/isMultiSetup.m
index ecaee492..5255aa95 100644
--- a/Functions/+BpodLib/+multi/isMultiSetup.m
+++ b/Functions/+BpodLib/+multi/isMultiSetup.m
@@ -38,7 +38,7 @@
%}
p = inputParser();
-p.addOptional('BpodSystem', [], @(x) isempty(x) | isstruct(x) | isa(x, 'BpodObject') | isa(x, 'BpodLib.BpodObject.MockBpodObject'))
+p.addOptional('BpodSystem', [], @(x) isempty(x) | isstruct(x) | isa(x, 'BpodObject') | isa(x, 'BpodTest.MockBpodObject'))
p.addParameter('LocalDir', '')
p.parse(varargin{:})
if ~isempty(p.Results.LocalDir)
diff --git a/Functions/+BpodLib/+path/findAllProtocols.m b/Functions/+BpodLib/+path/findAllProtocols.m
new file mode 100644
index 00000000..2296fb67
--- /dev/null
+++ b/Functions/+BpodLib/+path/findAllProtocols.m
@@ -0,0 +1,48 @@
+function protocolStruct = findAllProtocols(topProtocolFolderPath, depth)
+% Find all protocols in a folder of protocols
+% protocolStruct = findAllProtocols()
+%
+% Arguments
+% ---------
+% topProtocolFolderPath : char
+% Path to the top level protocol folder
+% depth : int
+% Depth of recursion, default 100 folders down
+%
+% Returns
+% -------
+% protocolStruct is a struct array with fields:
+% - Name: Name of the protocol
+% - Path: Path to the protocol
+
+currentFolder = topProtocolFolderPath; % Top level protocol folder
+if nargin < 2
+ depth = 100;
+end
+
+folderInfo = dir(currentFolder); % Get info about entries in the folder
+folderInfo = folderInfo([folderInfo.isdir]); % Keep only directories
+folderNames = {folderInfo.name}; % Get names of the directories
+folderNames = folderNames(~cellfun(@(x) x(1) == '.', folderNames)); % Remove hidden folders
+
+
+protocolStruct = struct('Name', [], 'Path', []); % Create structure with valid fields
+protocolStruct = protocolStruct([]); % Make it 0x0 structure
+iProtocol = 0;
+for i = 1:length(folderNames)
+ subfolderPath = fullfile(currentFolder, folderNames{i});
+
+ % Check if there is an M-file with the same name as the folder
+ if exist(fullfile(subfolderPath, [folderNames{i} '.m']), 'file')
+ % Congrats its a protocol folder
+ iProtocol = iProtocol + 1;
+ protocolStruct(iProtocol).Name = folderNames{i}; % Add to list of protocol names
+ protocolStruct(iProtocol).Path = subfolderPath; % Add to list of protocol folders
+ elseif depth > 1
+ % Recursively search for folders meeting criteria with reduced depth
+ returnStruct = BpodLib.path.findAllProtocols(subfolderPath, depth - 1);
+ if ~isempty(fieldnames(returnStruct))
+ protocolStruct = [protocolStruct, returnStruct];
+ end
+ end
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+path/findCurrentProtocolDatafiles.m b/Functions/+BpodLib/+path/findCurrentProtocolDatafiles.m
new file mode 100644
index 00000000..b7f7ed76
--- /dev/null
+++ b/Functions/+BpodLib/+path/findCurrentProtocolDatafiles.m
@@ -0,0 +1,18 @@
+function fileStructure = findCurrentProtocolDatafiles(BpodSystem)
+% fileStructure = findCurrentProtocolDatafiles(BpodSystem)
+% Returns the structure of the files in the folder where the data is being saved
+%
+% Inputs
+% ------
+% BpodSystem : BpodObject
+%
+% Outputs
+% -------
+% fileStructure : struct
+% The structure of the files (from dir()) in the folder where the data is being saved
+
+[savefolder, ~, ~] = fileparts(BpodSystem.Path.CurrentDataFile); % name of the .mat file being saved to
+
+fileStructure = dir(fullfile(savefolder, '*.mat'));
+
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+path/findProtocolFile.m b/Functions/+BpodLib/+path/findProtocolFile.m
new file mode 100644
index 00000000..84bc1f95
--- /dev/null
+++ b/Functions/+BpodLib/+path/findProtocolFile.m
@@ -0,0 +1,57 @@
+function protocolFilePath = findProtocolFile(protocolRootFolder, protocolPointer)
+% Find the file path for the given protocol name in the given folder
+% protocolFilePath = findProtocolFile(protocolRootFolder, protocolPointer)
+%
+% Arguments
+% ---------
+% protocolRootFolder : char
+% Path to the folder containing the protocols
+% protocolPointer : char
+% Name of the protocol, which can have folder names to handle ambiguity
+% e.g. 'Group1/MyProtocol' or 'Protocols/Group1/MyProtocol'
+%
+% Returns
+% -------
+% protocolFilePath : char
+% Absolute path to the protocol file
+
+% Normalize paths to avoid issues with different OS path separators
+protocolPointer = strrep(protocolPointer, '/', filesep);
+protocolPointer = strrep(protocolPointer, '\', filesep);
+
+% Split the protocolPointer into parts based on file separators
+pathParts = strsplit(protocolPointer, filesep);
+
+% Extract the actual protocolName (last element)
+protocolName = pathParts{end};
+
+% Retrieve and match protocols
+protocolStruct = BpodLib.path.findAllProtocols(protocolRootFolder);
+matchedProtocols = protocolStruct(strcmp({protocolStruct.Name}, protocolName));
+
+
+% Check the number of matches
+numMatches = numel(matchedProtocols);
+if numMatches == 1
+ % If exactly one match, return the file path
+ protocolFilePath = fullfile(matchedProtocols(1).Path, [protocolName, '.m']);
+elseif numMatches > 1
+
+ for depth = 1:length(pathParts)
+ % Construct the trailing path from the last 'depth' elements of pathParts
+ trailingPath = fullfile(pathParts{end-depth+1:end});
+
+ % Filter matchedProtocols by checking if their paths end with trailingPath
+ filteredProtocols = matchedProtocols(...
+ cellfun(@(x) endsWith(x, trailingPath, 'IgnoreCase', true), {matchedProtocols.Path}));
+
+ if numel(filteredProtocols) == 1
+ protocolFilePath = fullfile(filteredProtocols(1).Path, [pathParts{end}, '.m']);
+ return
+ end
+ end
+
+ error('BpodLib:path:AmbiguousProtocolMatch', 'Multiple matches found for protocol %s', protocolName);
+else
+ error('BpodLib:path:ProtocolNotFound', 'Protocol file %s not found', protocolName);
+end
diff --git a/Functions/+BpodLib/+path/getPath.m b/Functions/+BpodLib/+path/getPath.m
index 3b24a359..b2283e92 100644
--- a/Functions/+BpodLib/+path/getPath.m
+++ b/Functions/+BpodLib/+path/getPath.m
@@ -54,7 +54,7 @@
%}
p = inputParser();
-p.addOptional('BpodSystem', [], @(x) isempty(x) || isstruct(x) || isa(x, 'BpodObject') || isa(x, 'BpodLib.BpodObject.MockBpodObject'))
+p.addOptional('BpodSystem', [], @(x) isempty(x) || isstruct(x) || isa(x, 'BpodObject') || isa(x, 'BpodTest.MockBpodObject'))
p.addParameter('setuptype', [], @(x) isempty(x) || ischar(x) || isstring(x))
p.addParameter('LocalDir', [], @(x) isempty(x) || ischar(x) || isstring(x))
p.addParameter('com', [], @(x) isempty(x) || ischar(x) || isstring(x))
diff --git a/Functions/@BpodObject/BpodObject.m b/Functions/@BpodObject/BpodObject.m
index 1742e530..4c902245 100644
--- a/Functions/@BpodObject/BpodObject.m
+++ b/Functions/@BpodObject/BpodObject.m
@@ -98,7 +98,7 @@
end
% Add Bpod code to MATLAB path
- bpodPath = fileparts(which('Bpod'));
+ bpodPath = BpodLib.path.getPath('root');
addpath(genpath(fullfile(bpodPath, 'Assets')));
rmpath(genpath(fullfile(bpodPath, 'Assets', 'BControlPatch', 'ExperPort')));
addpath(genpath(fullfile(bpodPath, 'Examples', 'State Machines')));
diff --git a/Functions/@BpodObject/InitializeGUI.m b/Functions/@BpodObject/InitializeGUI.m
index 5187ca90..2edc9443 100644
--- a/Functions/@BpodObject/InitializeGUI.m
+++ b/Functions/@BpodObject/InitializeGUI.m
@@ -58,13 +58,13 @@
obj.GUIHandles.RunButton = uicontrol('Style', 'pushbutton',...
'String', '', ...
'Position', [742 100 60 60],...
- 'Callback', 'RunProtocol(''StartPause'')',...
+ 'Callback', @(~, ~) RunProtocol('StartPause'),...
'CData', obj.GUIData.GoButton,...
'TooltipString', 'Launch behavior session');
obj.GUIHandles.EndButton = uicontrol('Style', 'pushbutton',...
'String', '',...
'Position', [742 20 60 60],...
- 'Callback', 'RunProtocol(''Stop'')',...
+ 'Callback', @(~, ~) RunProtocol('Stop'),...
'CData', obj.GUIData.StopButton,...
'TooltipString', 'End session');
@@ -82,7 +82,7 @@
obj.GUIHandles.SettingsButton = uicontrol('Style', 'pushbutton',...
'String', '',...
'Position', [778 275 29 29],...
- 'Callback', 'BpodSettingsMenu',...
+ 'Callback', @(~, ~) BpodSettingsMenu,...
'CData', obj.GUIData.SettingsButton,...
'TooltipString', 'Settings and calibration');
obj.GUIHandles.RefreshButton = uicontrol('Style', 'pushbutton',...
@@ -94,13 +94,13 @@
obj.GUIHandles.SystemInfoButton = uicontrol('Style', 'pushbutton',...
'String', '',...
'Position', [778 227 29 29],...
- 'Callback', 'BpodSystemInfo',...
+ 'Callback', @(~, ~) BpodSystemInfo,...
'CData', obj.GUIData.SystemInfoButton,...
'TooltipString', 'View system info');
obj.GUIHandles.USBButton = uicontrol('Style', 'pushbutton',...
'String', '',...
'Position', [733 227 29 29],...
- 'Callback', 'ConfigureModuleUSB',...
+ 'Callback', @(~, ~) ConfigureModuleUSB,...
'CData', obj.GUIData.USBButton,...
'TooltipString', 'Configure module USB ports');
obj.GUIHandles.DocButton = uicontrol('Style', 'pushbutton',...
@@ -184,7 +184,7 @@
case 1
StateMachinePanel_0_5; % This is a file in /Bpod/Functions/OverridePanels/
case 2
- StateMachinePanel_0_7;
+ StateMachinePanel_0_7(obj);
case 3
StateMachinePanel_2_0_0;
case 4
@@ -200,11 +200,11 @@
eval([moduleFunctionName '(obj.GUIHandles.OverridePanel(' num2str(i) '), ''' thisModuleName ''');']);
obj.GUIData.DefaultPanel(i) = 0;
else % No override panel function exists for module
- DefaultBpodModule_Panel(obj.GUIHandles.OverridePanel(i), obj.Modules.Name{i-1});
+ DefaultBpodModule_Panel(obj, obj.GUIHandles.OverridePanel(i), obj.Modules.Name{i-1});
end
else % Module did not respond
- DefaultBpodModule_Panel(obj.GUIHandles.OverridePanel(i), obj.Modules.Name{i-1});
+ DefaultBpodModule_Panel(obj, obj.GUIHandles.OverridePanel(i), obj.Modules.Name{i-1});
end
end
drawnow;
diff --git a/Functions/@BpodObject/RefreshGUI.m b/Functions/@BpodObject/RefreshGUI.m
index 47c44d47..c25677e9 100644
--- a/Functions/@BpodObject/RefreshGUI.m
+++ b/Functions/@BpodObject/RefreshGUI.m
@@ -25,6 +25,10 @@
% Calling functions are RunStateMachine() and BpodTrialManager()
function obj = RefreshGUI(obj)
+ if isempty(obj.GUIHandles)
+ % TODO: a better check for headless mode
+ return % No GUI to update (running headless)
+ end
% Update most recent state and event names
if ~isempty(obj.StateMatrix)
set(obj.GUIHandles.PreviousStateDisplay, 'String', obj.Status.LastStateName);
diff --git a/Functions/@BpodObject/SetupHardware.m b/Functions/@BpodObject/SetupHardware.m
index 5cdcdc77..5ec87df2 100644
--- a/Functions/@BpodObject/SetupHardware.m
+++ b/Functions/@BpodObject/SetupHardware.m
@@ -43,7 +43,9 @@
close(obj.GUIHandles.LaunchEmuFig);
catch
end
- disp('Bpod started in Emulator mode: State Machine v1.0')
+ if obj.Status.Verbose
+ disp('Bpod started in Emulator mode: State Machine v1.0')
+ end
obj.FirmwareVersion = obj.CurrentFirmware.StateMachine;
obj.MachineType = 2;
nModules = sum(obj.HW.Outputs=='U');
@@ -91,7 +93,9 @@
obj.HW.StateMachineModel = smName;
% Confirm connection via message in command window
- disp(['State Machine ' smName ' connected on port ' obj.SerialPort.PortName newline])
+ if obj.Status.Verbose
+ disp(['State Machine ' smName ' connected on port ' obj.SerialPort.PortName newline])
+ end
% Firmware mismatch notices
if obj.FirmwareVersion ~= obj.CurrentFirmware.StateMachine
diff --git a/Functions/@BpodObject/refreshGUIPanels.m b/Functions/@BpodObject/refreshGUIPanels.m
index 844dd434..8bcbdc74 100644
--- a/Functions/@BpodObject/refreshGUIPanels.m
+++ b/Functions/@BpodObject/refreshGUIPanels.m
@@ -76,11 +76,11 @@
eval([moduleFunctionName '(obj.GUIHandles.OverridePanel(' num2str(i) '), ''' thisModuleName ''');']);
obj.GUIData.DefaultPanel(i) = 0;
else % No override panel function exists for module
- DefaultBpodModule_Panel(obj.GUIHandles.OverridePanel(i), obj.Modules.Name{i-1});
+ DefaultBpodModule_Panel(obj, obj.GUIHandles.OverridePanel(i), obj.Modules.Name{i-1});
end
else % Module did not respond
- DefaultBpodModule_Panel(obj.GUIHandles.OverridePanel(i), obj.Modules.Name{i-1});
+ DefaultBpodModule_Panel(obj, obj.GUIHandles.OverridePanel(i), obj.Modules.Name{i-1});
end
set(obj.GUIHandles.OverridePanel(i), 'Visible', 'off');
end
diff --git a/Functions/AddTrialEvents.m b/Functions/AddTrialEvents.m
index 0b5cce0b..663cfd52 100644
--- a/Functions/AddTrialEvents.m
+++ b/Functions/AddTrialEvents.m
@@ -1,3 +1,23 @@
+%{
+----------------------------------------------------------------------------
+
+This file is part of the Sanworks Bpod repository
+Copyright (C) Sanworks LLC, Rochester, New York, USA
+
+----------------------------------------------------------------------------
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3.
+
+This program is distributed WITHOUT ANY WARRANTY and without even the
+implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+%}
+
% AddTrialEvents formats trial events returned by RunStateMachine() or BpodTrialManager()
% and adds them to a human-readable session data struct.
%
@@ -27,26 +47,6 @@
% Note: If BpodSystem.Data is passed as sd, it can be saved to the current data file with SaveBpodSessionData();
% BpodSystem.Data = AddTrialEvents(BpodSystem.Data,RawEvents);
-%{
-----------------------------------------------------------------------------
-
-This file is part of the Sanworks Bpod repository
-Copyright (C) Sanworks LLC, Rochester, New York, USA
-
-----------------------------------------------------------------------------
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, version 3.
-
-This program is distributed WITHOUT ANY WARRANTY and without even the
-implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-%}
-
function updatedSD = AddTrialEvents(sd, rawTrialEvents)
global BpodSystem % Import the global BpodSystem object
@@ -113,6 +113,7 @@
end
sd.Info.SessionStartTime_UTC = datestr(theTime, 13);
sd.Info.SessionStartTime_MATLAB = theTime;
+ sd.Info.ProtocolFilePath = BpodSystem.Path.CurrentProtocol;
% Add settings struct selected for the session in the launch manager
sd.SettingsFile = BpodSystem.ProtocolSettings;
diff --git a/Functions/EndBpod.m b/Functions/EndBpod.m
index 8adf35af..349997f2 100644
--- a/Functions/EndBpod.m
+++ b/Functions/EndBpod.m
@@ -74,11 +74,12 @@
% Close legacy Bonsai TCP server if open
BpodSocketServer('close');
-
- if BpodSystem.EmulatorMode == 0
- disp('Bpod successfully disconnected.')
- else
- disp('Bpod emulator successfully closed.')
+ if BpodSystem.Status.Verbose
+ if BpodSystem.EmulatorMode == 0
+ disp('Bpod successfully disconnected.')
+ else
+ disp('Bpod emulator successfully closed.')
+ end
end
% Close remaining figures
diff --git a/Functions/Launch manager/LaunchManager.m b/Functions/Launch manager/LaunchManager.m
index 96647004..57bfa2c7 100644
--- a/Functions/Launch manager/LaunchManager.m
+++ b/Functions/Launch manager/LaunchManager.m
@@ -158,7 +158,7 @@
BpodSystem.setupFolders;
close(BpodSystem.GUIHandles.LaunchManagerFig);
else
- loadProtocols;
+ BpodLib.launcher.ui.loadProtocols(BpodSystem);
BpodSystem.GUIData.DummySubjectString = 'FakeSubject';
% Set selected protocol to first non-folder item
protocolNames = get(BpodSystem.GUIHandles.ProtocolSelector, 'String');
@@ -184,21 +184,12 @@
BpodSystem.Status.CurrentProtocolName = selectedProtocolName;
dataPath = fullfile(BpodSystem.Path.DataFolder,BpodSystem.GUIData.DummySubjectString);
protocolName = BpodSystem.Status.CurrentProtocolName;
- %Make standard folders for this protocol. This will fail silently if the folders exist
- warning off % Suppress warning that directory already exists
- mkdir(dataPath, protocolName);
- mkdir(fullfile(dataPath,protocolName,'Session Data'))
- mkdir(fullfile(dataPath,protocolName,'Session Settings'))
- warning on
- % Ensure that a default settings file exists
- defaultSettingsFilePath = fullfile(dataPath,protocolName,'Session Settings', 'DefaultSettings.mat');
- if ~exist(defaultSettingsFilePath)
- ProtocolSettings = struct;
- save(defaultSettingsFilePath, 'ProtocolSettings')
- end
- loadSubjects(protocolName);
- loadSettings(protocolName, BpodSystem.GUIData.DummySubjectString);
- update_datafile(protocolName, BpodSystem.GUIData.DummySubjectString);
+
+ BpodLib.launcher.prepareDataFolders(dataPath, protocolName)
+
+ BpodLib.launcher.ui.loadSubjects(BpodSystem, protocolName);
+ BpodLib.launcher.ui.loadSettings(BpodSystem, protocolName, BpodSystem.GUIData.DummySubjectString);
+ BpodLib.launcher.ui.setDataFilePath(BpodSystem, protocolName, BpodSystem.GUIData.DummySubjectString);
BpodSystem.GUIData.ProtocolSelectorLastValue = 1;
end
@@ -218,7 +209,7 @@ function ProtocolSelectorNavigate (a,b)
BpodSystem.Path.ProtocolFolder = fullfile(BpodSystem.Path.ProtocolFolder, folderName);
end
isNewFolder = true;
- loadProtocols;
+ BpodLib.launcher.ui.loadProtocols(BpodSystem);
end
else
protocolName = String{currentValue};
@@ -235,9 +226,9 @@ function ProtocolSelectorNavigate (a,b)
save(defaultSettingsPath, 'ProtocolSettings')
end
- loadSubjects(protocolName);
- loadSettings(protocolName, BpodSystem.GUIData.DummySubjectString);
- update_datafile(protocolName, BpodSystem.GUIData.DummySubjectString);
+ BpodLib.launcher.ui.loadSubjects(BpodSystem, protocolName);
+ BpodLib.launcher.ui.loadSettings(BpodSystem, protocolName, BpodSystem.GUIData.DummySubjectString);
+ BpodLib.launcher.ui.setDataFilePath(BpodSystem, protocolName, BpodSystem.GUIData.DummySubjectString);
BpodSystem.Status.CurrentProtocolName = protocolName;
end
end
@@ -276,107 +267,8 @@ function subject_selector_navigate(a,b)
set(BpodSystem.GUIHandles.SettingsSelector, 'String', settingsFileNames);
set(BpodSystem.GUIHandles.SettingsSelector, 'Value', 1);
BpodSystem.Status.CurrentSubjectName = selectedName;
-update_datafile(protocolName, selectedName);
-
-function loadProtocols
-global BpodSystem % Import the global BpodSystem object
-if strcmp(BpodSystem.Path.ProtocolFolder, BpodSystem.SystemSettings.ProtocolFolder)
- startPos = 3;
-else
- startPos = 2;
-end
-candidates = dir(BpodSystem.Path.ProtocolFolder);
-protocolNames = cell(0);
-nProtocols = 0;
-for x = startPos:length(candidates)
- if candidates(x).isdir
- protocolFolder = fullfile(BpodSystem.Path.ProtocolFolder, candidates(x).name);
- contents = dir(protocolFolder);
- nItems = length(contents);
- found = 0;
- for y = 3:nItems
- if strcmp(contents(y).name, [candidates(x).name '.m'])
- found = 1;
- end
- end
- if found
- protocolName = candidates(x).name;
- else
- protocolName = ['<' candidates(x).name '>'];
- end
- nProtocols = nProtocols + 1;
- protocolNames{nProtocols} = protocolName;
- end
-end
-
-if isempty(protocolNames)
- protocolNames = {'No Protocols Found'};
-else
- % Sort to put organizing directories first
- Types = ones(1,nProtocols);
- for i = 1:nProtocols
- protocolName = protocolNames{i};
- if protocolName(1) == '<'
- Types(i) = 0;
- end
- end
- [a, Order] = sort(Types);
- protocolNames = protocolNames(Order);
-end
-set(BpodSystem.GUIHandles.ProtocolSelector, 'String', protocolNames);
-
-function loadSubjects(ProtocolName)
-global BpodSystem % Import the global BpodSystem object
-% Make a list of the names of all subjects who already have a folder for this
-% protocol.
-candidateSubjects = dir(BpodSystem.Path.DataFolder);
-subjectNames = cell(1);
-nSubjects = 1;
-subjectNames{1} = BpodSystem.GUIData.DummySubjectString;
-for x = 1:length(candidateSubjects)
- if x > 2
- if candidateSubjects(x).isdir
- if ~strcmp(candidateSubjects(x).name, BpodSystem.GUIData.DummySubjectString)
- testpath = fullfile(BpodSystem.Path.DataFolder,candidateSubjects(x).name,ProtocolName);
- if exist(testpath) == 7
- nSubjects = nSubjects + 1;
- subjectNames{nSubjects} = candidateSubjects(x).name;
- end
- end
- end
- end
-end
-set(BpodSystem.GUIHandles.SubjectSelector,'String',subjectNames);
-set(BpodSystem.GUIHandles.SubjectSelector,'Value',1);
+BpodLib.launcher.ui.setDataFilePath(BpodSystem, protocolName, selectedName);
-function loadSettings(ProtocolName, SubjectName)
-global BpodSystem % Import the global BpodSystem object
-settingsPath = fullfile(BpodSystem.Path.DataFolder, SubjectName, ProtocolName, 'Session Settings');
-candidates = dir(settingsPath);
-nSettingsFiles = 0;
-settingsFileNames = cell(1);
-for x = 3:length(candidates)
- extension = candidates(x).name;
- extension = extension(end-2:end);
- if strcmp(extension, 'mat')
- nSettingsFiles = nSettingsFiles + 1;
- name = candidates(x).name;
- settingsFileNames{nSettingsFiles} = name(1:end-4);
- end
-end
-set(BpodSystem.GUIHandles.SettingsSelector, 'String', settingsFileNames);
-set(BpodSystem.GUIHandles.SettingsSelector,'Value',1);
-
-function update_datafile(protocolName, subjectName)
-global BpodSystem % Import the global BpodSystem object
-dateInfo = datestr(now, 30);
-dateInfo(dateInfo == 'T') = '_';
-localDir = BpodSystem.Path.DataFolder(max(find(BpodSystem.Path.DataFolder(1:end-1) == filesep)+1):end);
-set(BpodSystem.GUIHandles.DataFilePathDisplay, 'String',...
- [filesep fullfile(localDir, subjectName, protocolName, 'Session Data') filesep],'interpreter','none');
-fileName = [subjectName '_' protocolName '_' dateInfo '.mat'];
-set(BpodSystem.GUIHandles.DataFileDisplay, 'String', fileName);
-BpodSystem.Path.CurrentDataFile = fullfile(BpodSystem.Path.DataFolder, subjectName, protocolName, 'Session Data', fileName);
function add_subject(a,b)
global BpodSystem % Import the global BpodSystem object
@@ -493,7 +385,7 @@ function create_protocol(a,b)
fclose(file1);
edit(newProtocolFile);
set(BpodSystem.GUIHandles.ProtocolSelector,'Value',1);
- loadProtocols
+ BpodLib.launcher.ui.loadProtocols(BpodSystem)
end
end
@@ -627,7 +519,7 @@ function delete_settings(a,b)
close(deleteFig);
delete(settingsFile);
set(BpodSystem.GUIHandles.SettingsSelector,'Value',1);
- loadSettings(selectedProtocolName, selectedSubjectName);
+ BpodLib.launcher.ui.loadSettings(BpodSystem, selectedProtocolName, selectedSubjectName);
end
end
@@ -660,7 +552,7 @@ function delete_protocol(a,b)
if ((okToDelete == 1) && (~isempty(selectedProtocolName)))
rmdir(protocolPath, 's');
set(BpodSystem.GUIHandles.ProtocolSelector,'Value',1);
- loadProtocols
+ BpodLib.launcher.ui.loadProtocols(BpodSystem)
end
end
@@ -751,7 +643,7 @@ function import_settings(a,b)
copyfile(targetSettingsPath, destinationSettingsPath);
% Update UI with new settings
-loadSettings(selectedProtocolName, selectedSubjectName);
+BpodLib.launcher.ui.loadSettings(BpodSystem, selectedProtocolName, selectedSubjectName);
function launch_protocol(a,b)
global BpodSystem % Import the global BpodSystem object
@@ -764,91 +656,10 @@ function launch_protocol(a,b)
settingsList = get(BpodSystem.GUIHandles.SettingsSelector, 'String');
settingsIndex = get(BpodSystem.GUIHandles.SettingsSelector,'Value');
settingsName = settingsList{settingsIndex};
-settingsFileName = fullfile(BpodSystem.Path.DataFolder, subjectName, protocolName, 'Session Settings', [settingsName '.mat']);
-dataFolder = fullfile(BpodSystem.Path.DataFolder,subjectName,protocolName,'Session Data');
-if ~exist(dataFolder)
- mkdir(dataFolder);
-end
-
-% On Bpod r2+, if FlexIO channels are configured as analog,
-% setup binary data file
-if BpodSystem.MachineType > 3
- nAnalogChannels = sum(BpodSystem.HW.FlexIO_ChannelTypes == 2);
- if nAnalogChannels > 0
- analogFilename = [BpodSystem.Path.CurrentDataFile(1:end-4) '_ANLG.dat'];
- if BpodSystem.Status.RecordAnalog == 1
- BpodSystem.AnalogDataFile = fopen(analogFilename,'w');
- if BpodSystem.AnalogDataFile == -1
- error(['Error: Could not open the analog data file: ' analogFilename])
- end
- end
- BpodSystem.Status.nAnalogSamples = 0;
- end
-end
-BpodSystem.Status.Live = 1;
-BpodSystem.Status.LastEvent = 0;
-BpodSystem.GUIData.ProtocolName = protocolName;
-BpodSystem.GUIData.SubjectName = subjectName;
-BpodSystem.GUIData.SettingsFileName = settingsFileName;
-BpodSystem.Path.Settings = settingsFileName;
-settingStruct = load(BpodSystem.Path.Settings);
-F = fieldnames(settingStruct);
-fieldName = F{1};
-BpodSystem.ProtocolSettings = eval(['settingStruct.' fieldName]);
-BpodSystem.Data = struct;
-if BpodSystem.MachineType > 3
- if nAnalogChannels > 0
- BpodSystem.Data.Analog = struct;
- BpodSystem.Data.Analog.info = struct;
- BpodSystem.Data.Analog.FileName = analogFilename;
- BpodSystem.Data.Analog.nChannels = nAnalogChannels;
- BpodSystem.Data.Analog.channelNumbers = find(BpodSystem.HW.FlexIO_ChannelTypes == 2);
- BpodSystem.Data.Analog.SamplingRate = BpodSystem.HW.FlexIO_SamplingRate;
- BpodSystem.Data.Analog.nSamples = 0;
- % Add human-readable info about data fields to 'info struct
- BpodSystem.Data.Analog.info.FileName = 'Complete path and filename of the binary file to which the raw data was logged';
- BpodSystem.Data.Analog.info.nChannels = 'The number of Flex I/O channels configured as analog input';
- BpodSystem.Data.Analog.info.channelNumbers = 'The indexes of Flex I/O channels configured as analog input';
- BpodSystem.Data.Analog.info.SamplingRate = 'The sampling rate of the analog data. Units = Hz';
- BpodSystem.Data.Analog.info.nSamples = 'The total number of analog samples captured during the behavior session';
- BpodSystem.Data.Analog.info.Samples = 'Analog measurements captured. Rows are separate analog input channels. Units = Volts';
- BpodSystem.Data.Analog.info.Timestamps = 'Time of each sample (computed from sample index and sampling rate)';
- BpodSystem.Data.Analog.info.TrialNumber = 'Experimental trial during which each analog sample was captured';
- BpodSystem.Data.Analog.info.TrialData = 'A cell array of Samples. Each cell contains samples captured during a single trial.';
- end
-end
-protocolFolderPath = fullfile(BpodSystem.Path.ProtocolFolder,protocolName);
-protocolPath = fullfile(BpodSystem.Path.ProtocolFolder,protocolName,[protocolName '.m']);
-addpath(protocolFolderPath);
-set(BpodSystem.GUIHandles.RunButton, 'cdata', BpodSystem.GUIData.PauseButton, 'TooltipString', 'Press to pause session');
-
-% % Send metadata to Bpod Phone Home program (disabled pending a more stable server)
-% isOnline = BpodSystem.check4Internet();
-% if (isOnline == 1) && (BpodSystem.SystemSettings.PhoneHome == 1)
-% BpodSystem.BpodPhoneHome(1);
-% end
-
-if BpodSystem.Status.AnalogViewer
- set(BpodSystem.GUIHandles.RecordButton, 'Enable', 'off')
-end
-
-BpodSystem.Status.BeingUsed = 1;
-BpodSystem.Status.SessionStartFlag = 1;
-BpodSystem.ProtocolStartTime = now*100000;
-BpodSystem.resetSessionClock();
close(BpodSystem.GUIHandles.LaunchManagerFig);
-disp(' ');
-disp(['Starting ' protocolName]);
-set(BpodSystem.GUIHandles.CurrentStateDisplay, 'String', '---');
-set(BpodSystem.GUIHandles.PreviousStateDisplay, 'String', '---');
-set(BpodSystem.GUIHandles.LastEventDisplay, 'String', '---');
-set(BpodSystem.GUIHandles.TimeDisplay, 'String', '0:00:00');
-if sum(BpodSystem.InputsEnabled(BpodSystem.HW.Inputs == 'P')) == 0
- warning(['All Bpod behavior ports are currently disabled.'...
- 'If your protocol requires behavior ports, enable them from the settings menu.'])
-end
-run(protocolPath);
+protocolFolderPath = fullfile(BpodSystem.Path.ProtocolFolder, protocolName);
+BpodLib.launcher.launchProtocol(BpodSystem, protocolFolderPath, subjectName, 'settingsName', settingsName);
function outputString = spaces2underscores(inputString)
spaceIndexes = inputString == ' ';
diff --git a/Functions/Launch manager/RunProtocol.m b/Functions/Launch manager/RunProtocol.m
index cfbfb2b9..f9f7ef70 100644
--- a/Functions/Launch manager/RunProtocol.m
+++ b/Functions/Launch manager/RunProtocol.m
@@ -1,3 +1,22 @@
+function RunProtocol(Opstring, varargin)
+% RunProtocol() is the starting point for running a Bpod experimental session.
+%
+% Usage:
+% RunProtocol('Start') - Loads the launch manager
+% RunProtocol('Start', 'protocolName', 'subjectName', ['settingsName']) - Runs
+% the protocol "protocolName". subjectName is required. settingsName is
+% optional. All 3 are names as they would appear in the launch manager
+% (i.e. do not include full path or file extension).
+% RunProtocol('StartStop') - Loads the launch manager if no protocol is
+% running, pauses the protocol if one is running
+% RunProtocol('Stop') - Stops the currently running protocol. Data from the
+% partially completed trial is discarded.
+%
+% Arguments
+% ---------
+% Opstring : char
+% The operation to perform. Can be: 'Start', 'StartPause', 'Stop'
+
%{
----------------------------------------------------------------------------
@@ -18,21 +37,6 @@
along with this program. If not, see .
%}
-% RunProtocol() is the starting point for running a Bpod experimental session.
-
-% Usage:
-% RunProtocol('Start') - Loads the launch manager
-% RunProtocol('Start', 'protocolName', 'subjectName', ['settingsName']) - Runs
-% the protocol "protocolName". subjectName is required. settingsName is
-% optional. All 3 are names as they would appear in the launch manager
-% (i.e. do not include full path or file extension).
-% RunProtocol('StartStop') - Loads the launch manager if no protocol is
-% running, pauses the protocol if one is running
-% RunProtocol('Stop') - Stops the currently running protocol. Data from the
-% partially completed trial is discarded.
-
-function RunProtocol(Opstring, varargin)
-
global BpodSystem % Import the global BpodSystem object
% Verify that Bpod is running
@@ -44,7 +48,7 @@ function RunProtocol(Opstring, varargin)
case 'Start'
% Starts a new behavior session
if nargin == 1
- NewLaunchManager;
+ LaunchManager;
else
% Read user variables
protocolName = varargin{1};
@@ -55,156 +59,9 @@ function RunProtocol(Opstring, varargin)
settingsName = 'DefaultSettings';
end
- % Resolve target protocol file
- BpodSystem.Path.ProtocolFolder = BpodSystem.SystemSettings.ProtocolFolder;
- protocolPath = fullfile(BpodSystem.Path.ProtocolFolder, protocolName);
- if ~exist(protocolPath)
- % Look 1 level deeper
- rootContents = dir(BpodSystem.Path.ProtocolFolder);
- nItems = length(rootContents);
- found = 0;
- iFolder = 3;
- while found == 0 && iFolder <= nItems
- if rootContents(iFolder).isdir
- protocolPath = fullfile(BpodSystem.Path.ProtocolFolder,...
- rootContents(iFolder).name, protocolName);
- if exist(protocolPath)
- found = 1;
- end
- end
- iFolder = iFolder + 1;
- end
- end
-
- % Throw an error if not found
- if ~exist(protocolPath)
- error(['Error: Protocol "' protocolName '" not found.'])
- end
-
- % Generate path to protocol file
- protocolRunFile = fullfile(protocolPath, [protocolName '.m']);
-
- % Verify data path
- dataPath = fullfile(BpodSystem.Path.DataFolder,subjectName);
- if ~exist(dataPath)
- error(['Error starting protocol: Test subject "' subjectName...
- '" must be added first, from the launch manager.'])
- end
-
- %Make standard folders for this protocol. This will fail silently if the folders exist
- mkdir(dataPath, protocolName);
- mkdir(fullfile(dataPath,protocolName,'Session Data'))
- mkdir(fullfile(dataPath,protocolName,'Session Settings'))
- dateInfo = datestr(now, 30);
- dateInfo(dateInfo == 'T') = '_';
- fileName = [subjectName '_' protocolName '_' dateInfo '.mat'];
- dataFolder = fullfile(BpodSystem.Path.DataFolder,subjectName,protocolName,'Session Data');
- if ~exist(dataFolder)
- mkdir(dataFolder);
- end
-
- % Ensure that a default settings file exists
- defaultSettingsFilePath = fullfile(dataPath,protocolName,'Session Settings', 'DefaultSettings.mat');
- if ~exist(defaultSettingsFilePath)
- protocolSettings = struct;
- save(defaultSettingsFilePath, 'protocolSettings')
- end
- settingsFileName = fullfile(BpodSystem.Path.DataFolder, subjectName, protocolName,...
- 'Session Settings', [settingsName '.mat']);
- if ~exist(settingsFileName)
- error(['Error: Settings file: ' settingsName '.mat does not exist for test subject: '...
- subjectName ' in protocol: ' protocolName '.'])
- end
-
- % On Bpod r2+, if FlexIO channels are configured as analog, setup data file
- nAnalogChannels = sum(BpodSystem.HW.FlexIO_ChannelTypes == 2);
- if nAnalogChannels > 0
- analogFilename = [subjectName '_' protocolName '_' dateInfo '_ANLG.dat'];
- if BpodSystem.Status.RecordAnalog == 1
- BpodSystem.AnalogDataFile = fopen(analogFilename,'w');
- if BpodSystem.AnalogDataFile == -1
- error(['Error: Could not open the analog data file: ' analogFilename])
- end
- end
- BpodSystem.Status.nAnalogSamples = 0;
- end
-
- % Set BpodSystem status, protocol and path fields for new session
- BpodSystem.Status.Live = 1;
- BpodSystem.Status.LastEvent = 0;
- BpodSystem.GUIData.ProtocolName = protocolName;
- BpodSystem.GUIData.SubjectName = subjectName;
- BpodSystem.GUIData.SettingsFileName = settingsFileName;
- BpodSystem.Path.Settings = settingsFileName;
- BpodSystem.Path.CurrentDataFile = fullfile(dataFolder, fileName);
- BpodSystem.Status.CurrentProtocolName = protocolName;
- BpodSystem.Status.CurrentSubjectName = subjectName;
- SettingStruct = load(BpodSystem.Path.Settings);
- f = fieldnames(SettingStruct);
- fieldName = f{1};
- BpodSystem.ProtocolSettings = eval(['SettingStruct.' fieldName]);
-
- % Clear BpodSystem.Data
- BpodSystem.Data = struct;
-
- % Setup Flex I/O Analog Input data fields
- if BpodSystem.MachineType > 3
- if nAnalogChannels > 0
- % Setup analog data struct
- BpodSystem.Data.Analog = struct;
- BpodSystem.Data.Analog.info = struct;
- BpodSystem.Data.Analog.FileName = analogFilename;
- BpodSystem.Data.Analog.nChannels = nAnalogChannels;
- BpodSystem.Data.Analog.channelNumbers = find(BpodSystem.HW.FlexIO_ChannelTypes == 2);
- BpodSystem.Data.Analog.SamplingRate = BpodSystem.HW.FlexIO_SamplingRate;
- BpodSystem.Data.Analog.nSamples = 0;
-
- % Add human-readable info about data fields to 'info struct
- BpodSystem.Data.Analog.info.FileName = 'Complete path and filename of the binary file to which the raw data was logged';
- BpodSystem.Data.Analog.info.nChannels = 'The number of Flex I/O channels configured as analog input';
- BpodSystem.Data.Analog.info.channelNumbers = 'The indexes of Flex I/O channels configured as analog input';
- BpodSystem.Data.Analog.info.SamplingRate = 'The sampling rate of the analog data. Units = Hz';
- BpodSystem.Data.Analog.info.nSamples = 'The total number of analog samples captured during the behavior session';
- BpodSystem.Data.Analog.info.Samples = 'Analog measurements captured. Rows are separate analog input channels. Units = Volts';
- BpodSystem.Data.Analog.info.Timestamps = 'Time of each sample (computed from sample index and sampling rate)';
- BpodSystem.Data.Analog.info.TrialNumber = 'Experimental trial during which each analog sample was captured';
- BpodSystem.Data.Analog.info.TrialData = 'A cell array of Samples. Each cell contains samples captured during a single trial.';
- end
- end
-
- % Add protocol folder to the path
- addpath(protocolRunFile);
-
- % Set console GUI run button
- set(BpodSystem.GUIHandles.RunButton, 'cdata', BpodSystem.GUIData.PauseButton, 'TooltipString', 'Press to pause session');
-
- % Send metadata to Bpod Phone Home program (disabled pending a more stable server)
- % isOnline = BpodSystem.check4Internet();
- % if (isOnline == 1) && (BpodSystem.SystemSettings.PhoneHome == 1)
- %BpodSystem.BpodPhoneHome(1); % Disabled until server migration. -JS July 2018
- % end
-
- % Disable analog viewer record button (fixed for session)
- if BpodSystem.Status.AnalogViewer
- set(BpodSystem.GUIHandles.RecordButton, 'Enable', 'off')
- end
-
- % Clear console GUI fields
- set(BpodSystem.GUIHandles.CurrentStateDisplay, 'String', '---');
- set(BpodSystem.GUIHandles.PreviousStateDisplay, 'String', '---');
- set(BpodSystem.GUIHandles.LastEventDisplay, 'String', '---');
- set(BpodSystem.GUIHandles.TimeDisplay, 'String', '0:00:00');
-
- % Set BpodSystem status flags
- BpodSystem.Status.BeingUsed = 1;
- BpodSystem.Status.SessionStartFlag = 1;
-
- % Record session start time
- BpodSystem.ProtocolStartTime = now*100000;
-
% Push console GUI to top and run protocol file
figure(BpodSystem.GUIHandles.MainFig);
- run(protocolRunFile);
+ BpodLib.launcher.launchProtocol(BpodSystem, protocolName, subjectName, 'settingsName', settingsName)
end
case 'StartPause'
% Toggles to start or pause the session
@@ -228,63 +85,5 @@ function RunProtocol(Opstring, varargin)
end
case 'Stop'
% Manually ends the session. The partially completed trial is not saved with the data.
- if ~isempty(BpodSystem.Status.CurrentProtocolName)
- disp(' ')
- disp([BpodSystem.Status.CurrentProtocolName ' ended'])
- end
- warning off % Suppress warning, in case protocol folder has already been removed
- rmpath(fullfile(BpodSystem.Path.ProtocolFolder, BpodSystem.Status.CurrentProtocolName));
- warning on
- BpodSystem.Status.BeingUsed = 0;
- BpodSystem.Status.CurrentProtocolName = '';
- BpodSystem.Path.Settings = '';
- BpodSystem.Status.Live = 0;
- if BpodSystem.EmulatorMode == 0
- if BpodSystem.MachineType > 3
- stop(BpodSystem.Timers.AnalogTimer);
- try
- fclose(BpodSystem.AnalogDataFile);
- catch
- end
- end
- BpodSystem.SerialPort.write('X', 'uint8');
- pause(.1);
- BpodSystem.SerialPort.flush;
- if BpodSystem.MachineType > 3
- BpodSystem.AnalogSerialPort.flush;
- end
- if isfield(BpodSystem.PluginSerialPorts, 'TeensySoundServer')
- TeensySoundServer('end');
- end
- end
- BpodSystem.Status.RecordAnalog = 1;
- BpodSystem.Status.InStateMatrix = 0;
-
- % Close protocol and plugin figures
- try
- Figs = fields(BpodSystem.ProtocolFigures);
- nFigs = length(Figs);
- for x = 1:nFigs
- try
- close(eval(['BpodSystem.ProtocolFigures.' Figs{x}]));
- catch
-
- end
- end
- try
- close(BpodNotebook)
- catch
- end
- try
- BpodSystem.analogViewer('end', []);
- catch
- end
- catch
- end
- set(BpodSystem.GUIHandles.RunButton, 'cdata', BpodSystem.GUIData.GoButton,...
- 'TooltipString', 'Launch behavior session');
- if BpodSystem.Status.Pause == 1
- BpodSystem.Status.Pause = 0;
- end
- % ---- end Shut down Plugins
+ BpodLib.launcher.stopProtocol(BpodSystem);
end
\ No newline at end of file
diff --git a/Functions/Override Panels/DefaultBpodModule_Panel.m b/Functions/Override Panels/DefaultBpodModule_Panel.m
index a9940f4f..83aa2fb5 100644
--- a/Functions/Override Panels/DefaultBpodModule_Panel.m
+++ b/Functions/Override Panels/DefaultBpodModule_Panel.m
@@ -27,9 +27,7 @@
% This file includes bug fixes and/or feature updates contributed by:
% - Florian Rau, Poulet Lab, Max Delbruck Center, Berlin Germany
-function DefaultBpodModule_Panel(panelHandle, moduleName)
-
-global BpodSystem % Import the global BpodSystem object
+function DefaultBpodModule_Panel(BpodSystem, panelHandle, moduleName)
fontName = 'Courier New';
if ~ismac && ~ispc
diff --git a/Functions/Override Panels/StateMachinePanel_0_7.m b/Functions/Override Panels/StateMachinePanel_0_7.m
index 966e225d..d2577157 100644
--- a/Functions/Override Panels/StateMachinePanel_0_7.m
+++ b/Functions/Override Panels/StateMachinePanel_0_7.m
@@ -18,12 +18,10 @@
along with this program. If not, see .
%}
-% StateMachinePanel_0_7() sets up the state machine panel on the
+% StateMachinePanel_0_7(BpodSystem) sets up the state machine panel on the
% console GUI for State Machine r0.7-r1.0
-function StateMachinePanel_0_7
-
-global BpodSystem % Import the global BpodSystem object
+function StateMachinePanel_0_7(BpodSystem)
fontName = 'Courier New';
if ~ismac && ~ispc
diff --git a/Functions/RunStateMachine.m b/Functions/RunStateMachine.m
index 396380b5..55d1daaa 100644
--- a/Functions/RunStateMachine.m
+++ b/Functions/RunStateMachine.m
@@ -99,8 +99,10 @@
% Update console GUI time display
timeElapsed = ceil((now*100000) - BpodSystem.ProtocolStartTime);
-set(BpodSystem.GUIHandles.TimeDisplay, 'String', secs2hms(timeElapsed));
-set(BpodSystem.GUIHandles.RunButton, 'cdata', BpodSystem.GUIData.PauseButton);
+if ~isempty(BpodSystem.GUIHandles)
+ set(BpodSystem.GUIHandles.TimeDisplay, 'String', secs2hms(timeElapsed));
+ set(BpodSystem.GUIHandles.RunButton, 'cdata', BpodSystem.GUIData.PauseButton);
+end
% Start trial
BpodSystem.Status.BeingUsed = 1; BpodSystem.Status.InStateMatrix = 1;
diff --git a/Tests/+BpodTest/+dir/createDataFolder.m b/Tests/+BpodTest/+dir/createDataFolder.m
new file mode 100644
index 00000000..efbba8b8
--- /dev/null
+++ b/Tests/+BpodTest/+dir/createDataFolder.m
@@ -0,0 +1,35 @@
+function createDataFolder(dataFolder, subjectName, protocolName)
+% Create a data folder structure for a subject and protocol
+% createDataFolder(dataFolder, subjectName, protocolName)
+%
+% Arguments
+% ---------
+% dataFolder : char
+% Path to the data folder
+% subjectName : char
+% Name of the subject
+% protocolName : char
+% Name of the protocol
+
+%{
+----------------------------------------------------------------------------
+
+This file is part of the Sanworks Bpod repository
+Copyright (C) Sanworks LLC, Rochester, New York, USA
+
+----------------------------------------------------------------------------
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3.
+
+This program is distributed WITHOUT ANY WARRANTY and without even the
+implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+%}
+
+mkdir(fullfile(dataFolder, subjectName, protocolName, 'Session Data'));
+mkdir(fullfile(dataFolder, subjectName, protocolName, 'Session Settings'));
\ No newline at end of file
diff --git a/Tests/+BpodTest/+dir/createDummyProtocolFolders.m b/Tests/+BpodTest/+dir/createDummyProtocolFolders.m
new file mode 100644
index 00000000..cff13178
--- /dev/null
+++ b/Tests/+BpodTest/+dir/createDummyProtocolFolders.m
@@ -0,0 +1,94 @@
+function testEnvironment = createDummyProtocolFolders(rootPath)
+% Create a directory structure for testing the findProtocolFile function
+%
+% Inputs
+% ------
+% rootPath : str
+% Path to the root directory where the test environment will be created
+%
+% Outputs
+% -------
+% testEnvironment : struct
+% Various properties specific to environment (e.g. .protocolFolder)
+% The structure will be as follows:
+% rootPath/
+% Bpod Local/
+% Data/
+% FakeSubject/
+% Protocol_matching1/
+% Session Data/
+% FakeSubject_Protocol_matching1_20160315_021512.mat
+% Session Settings/
+% DefaultSettings.mat
+% settings1.mat
+% Protocol_unique1/
+% Protocol_unique2/
+% Subject1/
+% Protocol_unique1/
+% Protocol_matching1/
+% Subject2/
+% Protocol_matching1/
+% Protocol_unique2/
+% Protocols/
+% Procotol_unique1/Protocol_unique1.m
+% Protocol_unique2
+% Protocol_matching1
+% subfolderA/
+% Protocol_matching1
+% Protocol_matching2
+% subfolderB/
+% subfolderC/
+% Protocol_unique4
+% Protocol_unique3
+% Protocol_matching2
+
+protocolFolder = fullfile(rootPath, 'Bpod Local/Protocols');
+dataFolder = fullfile(rootPath, 'Bpod Local/Data');
+
+% Create testEnvironment
+testEnvironment = struct;
+testEnvironment.protocolFolder = protocolFolder;
+testEnvironment.dataFolder = dataFolder;
+
+% Create a protocol folder structure
+protocolStructure = {...
+ {fullfile(protocolFolder), 'Protocol_unique1'}, ...
+ {fullfile(protocolFolder), 'Protocol_unique2'}, ...
+ {fullfile(protocolFolder), 'Protocol_matching1'}, ...
+ {fullfile(protocolFolder, 'subfolderA'), 'Protocol_matching1'}, ...
+ {fullfile(protocolFolder, 'subfolderA'), 'Protocol_matching2'}, ...
+ {fullfile(protocolFolder, 'subfolderB'), 'Protocol_unique3'}, ...
+ {fullfile(protocolFolder, 'subfolderB'), 'Protocol_matching2'}, ...
+ {fullfile(protocolFolder, 'subfolderB/subfolderC'), 'Protocol_unique4'}, ...
+ };
+for i = 1:length(protocolStructure)
+ BpodTest.dir.createProtocolMaterials(protocolStructure{i}{1}, protocolStructure{i}{2}, 'dataFolder', false);
+end
+
+dataStructure = {...
+ {'FakeSubject', 'Protocol_matching1'}, ...
+ {'FakeSubject', 'Protocol_unique1'}, ...
+ {'FakeSubject', 'Protocol_unique2'}, ...
+ {'Subject1', 'Protocol_unique1'}, ...
+ {'Subject1', 'Protocol_matching1'}, ...
+ {'Subject2', 'Protocol_matching1'}, ...
+ {'Subject2', 'Protocol_unique2'}, ...
+ };
+
+for i = 1:length(dataStructure)
+ BpodTest.dir.createDataFolder(dataFolder, dataStructure{i}{1}, dataStructure{i}{2});
+end
+
+% Create settings files
+filepath = fullfile(dataFolder, 'FakeSubject', 'Protocol_matching1', 'Session Settings', 'DefaultSettings.mat');
+emptystruct = struct;
+save(filepath, 'emptystruct');
+dummysettings = struct('dummy', 'dummy');
+filepath = fullfile(dataFolder, 'FakeSubject', 'Protocol_matching1', 'Session Settings', 'settings1.mat');
+save(filepath, 'dummysettings');
+
+% Create dummy data files
+filepath = fullfile(dataFolder, 'FakeSubject', 'Protocol_matching1', 'Session Data', 'FakeSubject_Protocol_matching1_20160315_021512.mat');
+save(filepath, 'emptystruct');
+
+end
\ No newline at end of file
diff --git a/Tests/+BpodTest/+dir/createProtocolMaterials.m b/Tests/+BpodTest/+dir/createProtocolMaterials.m
new file mode 100644
index 00000000..2210f515
--- /dev/null
+++ b/Tests/+BpodTest/+dir/createProtocolMaterials.m
@@ -0,0 +1,80 @@
+function createProtocolMaterials(varargin)
+% Create dummy materials for protocol testing
+% createProtocolMaterials(folderPath, protocolName, 'subjectName', _, 'dataFolder', _)
+%
+% Arguments
+% ---------
+% folderPath : char
+% Path to the folder where the protocol materials will be created
+% protocolName : char
+% Name of the protocol
+%
+% Keyword Arguments
+% -----------------
+% subjectName : char (optional, default: 'FakeSubject')
+% Name of the subject
+% dataFolder : char | logical (optional, default: true)
+% Path to the data folder or a logical indicating whether to create a data folder for the subject.
+
+%{
+----------------------------------------------------------------------------
+
+This file is part of the Sanworks Bpod repository
+Copyright (C) Sanworks LLC, Rochester, New York, USA
+
+----------------------------------------------------------------------------
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3.
+
+This program is distributed WITHOUT ANY WARRANTY and without even the
+implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+%}
+
+p = inputParser();
+p.addRequired('folderPath')
+p.addRequired('protocolName')
+p.addParameter('subjectName', 'FakeSubject')
+p.addParameter('dataFolder', true, @(x) ischar(x) || islogical(x)); % str or logical
+p.parse(varargin{:})
+
+folderPath = p.Results.folderPath;
+protocolName = p.Results.protocolName;
+subjectName = p.Results.subjectName;
+dataFolder = p.Results.dataFolder;
+
+if islogical(dataFolder) && dataFolder
+ makeData = true;
+ % go up until folder ends with folderPath
+ while true
+ [parentFolder, folderName] = fileparts(folderPath);
+ if strcmp(folderName, 'Bpod Local')
+ break
+ end
+ dataFolder = fullfile(parentFolder, 'Data');
+ end
+elseif islogical(dataFolder) && ~dataFolder
+ makeData = false;
+elseif ischar(dataFolder)
+ makeData = true;
+else
+ error('dataFolder must be a logical or a string')
+end
+
+%% Create a protocol file
+mkdir(fullfile(folderPath, protocolName));
+fileID = fopen(fullfile(folderPath, protocolName, [protocolName '.m']), 'w');
+filestr = ['function ' protocolName '\n disp("This is a protocol file.");'];
+fprintf(fileID, filestr);
+fclose(fileID);
+
+%% Make the data
+if ~makeData
+ return
+end
+BpodTest.dir.createDataFolder(dataFolder, subjectName, protocolName);
\ No newline at end of file
diff --git a/Tests/+BpodTest/+ui/click.m b/Tests/+BpodTest/+ui/click.m
new file mode 100644
index 00000000..8ff33342
--- /dev/null
+++ b/Tests/+BpodTest/+ui/click.m
@@ -0,0 +1,21 @@
+function click(handle)
+% Simulate a user click on an object
+% click(handle)
+
+% tc = matlab.uitest.TestCase.forInteractiveUse;
+% pressed = false;
+% try
+% tc.press(handle)
+% pressed = true;
+% catch ME
+% % If we move to uifigure framework for the UI elements then this would work.
+% if strcmp(ME.identifier, 'MATLAB:uiautomation:Driver:MustBelongToUIFigure')
+% elseif strcmp(ME.identifier, 'MATLAB:uiautomation:Driver:GestureNotSupportedForClass')
+% else
+% rethrow(ME)
+% end
+% end
+% if pressed
+% return
+% end
+feval(get(handle, 'Callback'))
\ No newline at end of file
diff --git a/Tests/+BpodTest/+ui/closeMessageBoxes.m b/Tests/+BpodTest/+ui/closeMessageBoxes.m
new file mode 100644
index 00000000..3166cfc9
--- /dev/null
+++ b/Tests/+BpodTest/+ui/closeMessageBoxes.m
@@ -0,0 +1,17 @@
+function closeMessageBoxes(BpodSystem)
+% Close message boxes opened during Bpod's verbose/GUI startup
+% closeMessageBoxes(BpodSystem)
+
+if ~isprop(BpodSystem, 'GUIHandles')
+ return
+end
+
+messageBoxFields = {'BpodPhoneHomeFig', 'LiquidCalWarningMessageBox'};
+
+for idx = 1:numel(messageBoxFields)
+ fieldName = messageBoxFields{idx};
+ if isfield(BpodSystem.GUIHandles, fieldName) && isvalid(BpodSystem.GUIHandles.(fieldName))
+ close(BpodSystem.GUIHandles.(fieldName));
+ BpodSystem.GUIHandles = rmfield(BpodSystem.GUIHandles, fieldName);
+ end
+end
diff --git a/Tests/+BpodTest/+ui/closePhoneHome.m b/Tests/+BpodTest/+ui/closePhoneHome.m
new file mode 100644
index 00000000..09041947
--- /dev/null
+++ b/Tests/+BpodTest/+ui/closePhoneHome.m
@@ -0,0 +1,8 @@
+function closePhoneHome(BpodSystem)
+if isprop(BpodSystem, 'GUIHandles')
+ if isfield(BpodSystem.GUIHandles, 'BpodPhoneHomeFig') && isvalid(BpodSystem.GUIHandles.BpodPhoneHomeFig)
+ close(BpodSystem.GUIHandles.BpodPhoneHomeFig)
+ % BpodSystem.phoneHomeRegister(0) % this is a private method
+ end
+end
+end
\ No newline at end of file
diff --git a/Functions/+BpodLib/+BpodObject/MockBpodObject.m b/Tests/+BpodTest/MockBpodObject.m
similarity index 97%
rename from Functions/+BpodLib/+BpodObject/MockBpodObject.m
rename to Tests/+BpodTest/MockBpodObject.m
index 4f8a6bf1..417869b7 100644
--- a/Functions/+BpodLib/+BpodObject/MockBpodObject.m
+++ b/Tests/+BpodTest/MockBpodObject.m
@@ -1,4 +1,4 @@
-% Mock BpodObject for use in unit testing
+% A lightweight mock BpodObject for use in unit testing
%{
----------------------------------------------------------------------------
diff --git a/Tests/+BpodTest/getPath.m b/Tests/+BpodTest/getPath.m
new file mode 100644
index 00000000..b63b340d
--- /dev/null
+++ b/Tests/+BpodTest/getPath.m
@@ -0,0 +1,11 @@
+function path = getPath(target)
+switch lower(target)
+ case 'assets'
+ % Return path to the assets directory
+ path = fullfile(fileparts(BpodTest.getPath('self')), 'assets');
+ case 'self'
+ % Return path to the current script
+ path = fileparts(mfilename('fullpath'));
+ otherwise
+ error("Target '%s' not recognized.", target)
+end
\ No newline at end of file
diff --git a/Tests/+BpodTest/setupBpodSystemFixture.m b/Tests/+BpodTest/setupBpodSystemFixture.m
new file mode 100644
index 00000000..a510a6b3
--- /dev/null
+++ b/Tests/+BpodTest/setupBpodSystemFixture.m
@@ -0,0 +1,43 @@
+function setupBpodSystemFixture(testCase, varargin)
+% Prepare a BpodSystem for testing, using an existing BpodSystem or creating a new one.
+% setupBpodSystemFixture(testCase, _)
+%
+% Expected to be called in setupOnce()
+% Works in tandem with teardownBpodSystemFixture.m
+%
+% Keyword Arguments
+% -----------------
+% gui : bool (default = false)
+% Startup a GUI component
+
+p = inputParser();
+p.addParameter('gui', false)
+p.parse(varargin{:})
+
+global BpodSystem
+wasRunning = ~isempty(BpodSystem);
+testCase.TestData.Original = struct();
+testCase.TestData.Original.wasRunning = wasRunning;
+if wasRunning
+ assert(isa(BpodSystem, 'BpodObject'), 'BpodSystem should be a BpodObject')
+ testCase.TestData.Original.BpodSystem = BpodSystem;
+ testCase.TestData.Original.LocalDir = BpodSystem.Path.LocalDir; % store for restoration later
+ testCase.TestData.Original.Verbose = BpodSystem.Status.Verbose;
+ BpodSystem.Status.Verbose = false;
+else
+ % Create a new BpodSystem for testing
+ % A fresh startup requires a local dir (to not contaminate existing local dir)
+ localDir = fullfile(tempname, 'Bpod Local');
+ mkdir(localDir)
+ testCase.TestData.Original.rootDir = fileparts(localDir);
+ BpodSystem = BpodTest.setupEmulator(localDir, 'gui', p.Results.gui);
+end
+
+if p.Results.gui
+ assert(~isempty(BpodSystem.GUIHandles))
+ BpodTest.ui.closePhoneHome(BpodSystem)
+end
+
+testCase.TestData.BpodSystem = BpodSystem;
+
+end
\ No newline at end of file
diff --git a/Tests/+BpodTest/setupEmulator.m b/Tests/+BpodTest/setupEmulator.m
new file mode 100644
index 00000000..18adc416
--- /dev/null
+++ b/Tests/+BpodTest/setupEmulator.m
@@ -0,0 +1,34 @@
+function BpodSystem = setupEmulator(varargin)
+% Create a no-GUI Bpod Emulator
+% BpodSystem = setupEmulator(_)
+%
+% Arguments
+% ---------
+% LocalDir : char
+% Location of the Bpod Local/, should be a tempname
+%
+% Keyword Arguments
+% -----------------
+% gui : bool (default = false)
+% Use .InitializeGUI() method
+%
+% Returns
+% -------
+% BpodSystem : BpodObject
+% A BpodSystem (not assigned global) in Emulator Mode without a GUI
+
+p = inputParser();
+p.addRequired('LocalDir')
+p.addParameter('gui', false)
+p.parse(varargin{:})
+
+BpodSystem = BpodObject('verbose', false);
+BpodSystem.EmulatorMode = true;
+BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystem, 'verbose', false, 'LocalDir', p.Results.LocalDir)
+BpodSystem.SetupHardware();
+if p.Results.gui
+ BpodSystem.InitializeGUI();
+end
+BpodSystem.Status.Initialized = true;
+
+end
\ No newline at end of file
diff --git a/Tests/+BpodTest/teardownBpodSystemFixture.m b/Tests/+BpodTest/teardownBpodSystemFixture.m
new file mode 100644
index 00000000..1ccdf117
--- /dev/null
+++ b/Tests/+BpodTest/teardownBpodSystemFixture.m
@@ -0,0 +1,22 @@
+function teardownBpodSystemFixture(testCase)
+% Reset the BpodSystem fixture to its original state
+% teardownBpodSystemFixture(testCase)
+%
+% Expected to be called in teardownOnce()
+% Works in tandem with setupBpodSystemFixture.m
+
+% Shut down the emulated Bpod setup if it was running
+if ~testCase.TestData.Original.wasRunning
+ EndBpod % clears global BpodSystem
+ rmdir(testCase.TestData.Original.rootDir, 's')
+end
+
+% Restore the original BpodSystem to its original state
+if testCase.TestData.Original.wasRunning
+ global BpodSystem
+ BpodSystem = testCase.TestData.Original.BpodSystem;
+ BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystem, 'LocalDir', testCase.TestData.Original.LocalDir)
+ BpodSystem.Status.Verbose = testCase.TestData.Original.Verbose;
+end
+
+end
\ No newline at end of file
diff --git a/Tests/BpodLib/BpodObject/setup/test_SettingsAndFiles.m b/Tests/BpodLib/BpodObject/setup/test_SettingsAndFiles.m
index dd035847..86557d41 100644
--- a/Tests/BpodLib/BpodObject/setup/test_SettingsAndFiles.m
+++ b/Tests/BpodLib/BpodObject/setup/test_SettingsAndFiles.m
@@ -5,7 +5,7 @@
end
function setup(testCase)
- BpodSystem = BpodLib.BpodObject.MockBpodObject('COM13');
+ BpodSystem = BpodTest.MockBpodObject('COM13');
testCase.TestData.BpodSystem = BpodSystem;
% Setup test data that will be used for all tests
@@ -64,7 +64,7 @@ function test_newMultiSetup(testCase)
testCase.verifyTrue(BpodLib.multi.isMultiSetup(BpodSystem), 'Multi setup should be recognised to trigger multi mode.');
% On initialisation this should create a new multi
- NewBpodSystem = BpodLib.BpodObject.MockBpodObject('COM5');
+ NewBpodSystem = BpodTest.MockBpodObject('COM5');
BpodLib.BpodObject.setup.updatePathAndSettings(NewBpodSystem, 'LocalDir', testCase.TestData.LocalDir);
testCase.verifyTrue(isfolder(fullfile(testCase.TestData.LocalDir, 'Config/Machine-COM5')), 'Folder for new BpodSystem should exist.');
testCase.verifyTrue(isfolder(fullfile(testCase.TestData.LocalDir, 'Config/Machine-COM13')), 'Folder for existing BpodSystem should still exist.');
@@ -90,7 +90,7 @@ function test_emulatorBehaviour(testCase)
BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystem, 'LocalDir', testCase.TestData.LocalDir);
BpodLib.multi.createMultiSetup(BpodSystem);
- EMUBpodSystem = BpodLib.BpodObject.MockBpodObject('EMU');
+ EMUBpodSystem = BpodTest.MockBpodObject('EMU');
BpodLib.BpodObject.setup.updatePathAndSettings(EMUBpodSystem, 'LocalDir', testCase.TestData.LocalDir);
% Check if the emulator is recognised as a multi setup
testCase.verifyTrue(BpodLib.multi.isMultiSetup(BpodSystem), 'EMU should be treated as a multi setup.');
diff --git a/Tests/BpodLib/BpodObject/setup/test_legacySetup.m b/Tests/BpodLib/BpodObject/setup/test_legacySetup.m
index f401d49f..77ab5494 100644
--- a/Tests/BpodLib/BpodObject/setup/test_legacySetup.m
+++ b/Tests/BpodLib/BpodObject/setup/test_legacySetup.m
@@ -4,7 +4,7 @@
end
function setup(testCase)
- BpodSystem = BpodLib.BpodObject.MockBpodObject('COM13');
+ BpodSystem = BpodTest.MockBpodObject('COM13');
testCase.TestData.BpodSystem = BpodSystem;
% Setup test data that will be used for all tests
@@ -81,7 +81,7 @@ function test_conversionCongruence(testCase)
rootPath = testCase.TestData.rootPath;
newLocalDir = fullfile(rootPath, 'Bpod Local New');
- BpodSystemNew = BpodLib.BpodObject.MockBpodObject('COM13');
+ BpodSystemNew = BpodTest.MockBpodObject('COM13');
BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystemNew, 'LocalDir', newLocalDir);
% Now test to see if Bpod Local/ and Bpod Local New/ are the same
diff --git a/Tests/BpodLib/calibration/liquid/test_liquidutils.m b/Tests/BpodLib/calibration/liquid/test_liquidutils.m
index 1e632ee1..9258d257 100644
--- a/Tests/BpodLib/calibration/liquid/test_liquidutils.m
+++ b/Tests/BpodLib/calibration/liquid/test_liquidutils.m
@@ -8,7 +8,7 @@ function setup(testCase)
localdir = fullfile(testCase.TestData.rootPath, 'Bpod Local');
mkdir(localdir)
- mockBpod = BpodLib.BpodObject.MockBpodObject('COM13');
+ mockBpod = BpodTest.MockBpodObject('COM13');
mockBpod.Path.LocalDir = localdir;
testCase.TestData.mockBpod = mockBpod;
diff --git a/Tests/BpodLib/launcher/test_launcherPathing.m b/Tests/BpodLib/launcher/test_launcherPathing.m
new file mode 100644
index 00000000..2504e56e
--- /dev/null
+++ b/Tests/BpodLib/launcher/test_launcherPathing.m
@@ -0,0 +1,92 @@
+function tests = test_launcherPathing()
+ tests = functiontests(localfunctions);
+end
+
+function setupOnce(testCase)
+ % Setup - Create a mock directory structure
+ rootPath = tempname; % Generate a unique temporary directory
+ testCase.TestData.rootPath = rootPath;
+
+ testEnvironment = BpodTest.dir.createDummyProtocolFolders(rootPath);
+ testCase.TestData.protocolFolder = testEnvironment.protocolFolder;
+ testCase.TestData.dataFolder = testEnvironment.dataFolder;
+end
+
+function teardownOnce(testCase)
+ % Cleanup - Remove the directory structure after testing
+ rmdir(testCase.TestData.rootPath, 's');
+end
+
+function test_createDataFilePath(testCase)
+ % Test the createDataFilePath function
+ dataFilePath = BpodLib.launcher.createDataFilePath(testCase.TestData.dataFolder, 'Protocol_matching1', 'FakeSubject');
+ [folder, fileName, ext] = fileparts(dataFilePath);
+
+ testCase.verifyEqual(folder, fullfile(testCase.TestData.dataFolder, 'FakeSubject', 'Protocol_matching1', 'Session Data'))
+ testCase.verifyTrue(strcmp(ext, '.mat'))
+
+ % todo: test order of subject, protocol, and date format
+
+end
+
+function test_prepareDataFolders(testCase)
+ % Test the prepareDataFolders function
+ subjectName = 'Subject1';
+ protocolName = 'Protocol_unique_generated';
+ subjectDataFolder = fullfile(testCase.TestData.dataFolder, subjectName);
+
+
+ BpodLib.launcher.prepareDataFolders(subjectDataFolder, protocolName)
+
+ % Test all of its parts
+ testCase.verifyTrue(exist(fullfile(subjectDataFolder, protocolName), 'dir') == 7)
+ testCase.verifyTrue(exist(fullfile(subjectDataFolder, protocolName, 'Session Data'), 'dir') == 7)
+ testCase.verifyTrue(exist(fullfile(subjectDataFolder, protocolName, 'Session Settings'), 'dir') == 7)
+ testCase.verifyTrue(exist(fullfile(subjectDataFolder, protocolName, 'Session Settings', 'DefaultSettings.mat'), 'file') == 2)
+end
+
+function test_findProtocols(testCase)
+ % Test situation where they're the same
+ BpodSystem = struct;
+ BpodSystem.Path.ProtocolFolder = testCase.TestData.protocolFolder;
+ BpodSystem.SystemSettings.ProtocolFolder = testCase.TestData.protocolFolder;
+
+ protocolNames = BpodLib.launcher.findProtocols(BpodSystem);
+ testCase.verifyEqual(protocolNames, {'', '', 'Protocol_matching1', 'Protocol_unique1', 'Protocol_unique2'})
+end
+
+function test_findProtocols_different(testCase)
+ % Test situation where they're different
+ BpodSystem = struct;
+ BpodSystem.Path.ProtocolFolder = fullfile(testCase.TestData.protocolFolder, 'subfolderA');
+ BpodSystem.SystemSettings.ProtocolFolder = testCase.TestData.protocolFolder;
+
+ protocolNames = BpodLib.launcher.findProtocols(BpodSystem);
+ testCase.verifyEqual(protocolNames, {'<..>', 'Protocol_matching1', 'Protocol_matching2'})
+end
+
+function test_findProtocols_different_nested(testCase)
+ % Test situation where they're different, and further in
+ BpodSystem = struct;
+ BpodSystem.Path.ProtocolFolder = fullfile(testCase.TestData.protocolFolder, 'subfolderB/subfolderC');
+ BpodSystem.SystemSettings.ProtocolFolder = testCase.TestData.protocolFolder;
+
+ protocolNames = BpodLib.launcher.findProtocols(BpodSystem);
+ testCase.verifyEqual(protocolNames, {'<..>', 'Protocol_unique4'})
+end
+
+function test_findSubjects(testCase)
+ subjectNames = BpodLib.launcher.findSubjects(testCase.TestData.dataFolder, 'Protocol_matching1', 'FakeSubject');
+ testCase.verifyEqual(subjectNames, {'FakeSubject', 'Subject1', 'Subject2'})
+end
+
+function test_findSubjects_nosubjects(testCase)
+ % Even if protocol hasn't been run before it'll autopopulate with the dummy subject
+ subjectNames = BpodLib.launcher.findSubjects(testCase.TestData.dataFolder, 'Protocol_nonexistent', 'FakeSubject');
+ testCase.verifyEqual(subjectNames, {'FakeSubject'})
+end
+
+function test_findSettings(testCase)
+ settingsFileNames = BpodLib.launcher.findSettings(testCase.TestData.dataFolder, 'Protocol_matching1', 'FakeSubject');
+ testCase.verifyEqual(settingsFileNames, {'DefaultSettings', 'settings1'})
+end
\ No newline at end of file
diff --git a/Tests/BpodLib/multi/test_isMultiSetup.m b/Tests/BpodLib/multi/test_isMultiSetup.m
index 6bec3591..25d47f65 100644
--- a/Tests/BpodLib/multi/test_isMultiSetup.m
+++ b/Tests/BpodLib/multi/test_isMultiSetup.m
@@ -8,7 +8,7 @@ function setup(testCase)
testCase.TestData.rootPath = rootPath;
mkdir(testCase.TestData.rootPath)
- mockBpod = BpodLib.BpodObject.MockBpodObject('COM13');
+ mockBpod = BpodTest.MockBpodObject('COM13');
LocalDir = fullfile(rootPath, 'Bpod Local');
mkdir(LocalDir)
testCase.TestData.LocalDir = LocalDir;
@@ -35,7 +35,7 @@ function test_isMultiSetupCOM(testCase)
function test_isMultiSetupEMU(testCase)
% Test if the EMU setup is recognised as a multi setup
- mockBpod = BpodLib.BpodObject.MockBpodObject('EMU');
+ mockBpod = BpodTest.MockBpodObject('EMU');
BpodLib.BpodObject.setup.updatePathAndSettings(mockBpod, 'LocalDir', testCase.TestData.LocalDir);
testCase.verifyTrue(~BpodLib.multi.isMultiSetup(mockBpod), 'The setup should not be recognised as a multi setup before creation.');
% Create a multi setup
diff --git a/Tests/BpodLib/path/test_findProtocolFile.m b/Tests/BpodLib/path/test_findProtocolFile.m
new file mode 100644
index 00000000..0dc422f7
--- /dev/null
+++ b/Tests/BpodLib/path/test_findProtocolFile.m
@@ -0,0 +1,102 @@
+function tests = test_findProtocolFile
+ tests = functiontests(localfunctions);
+end
+
+function setupOnce(testCase)
+ % Setup - Create a mock directory structure
+ rootPath = tempname; % Generate a unique temporary directory
+ testCase.TestData.rootPath = rootPath;
+
+ testEnvironment = BpodTest.dir.createDummyProtocolFolders(rootPath);
+ testCase.TestData.protocolFolder = testEnvironment.protocolFolder;
+end
+
+function teardownOnce(testCase)
+ % Cleanup - Remove the directory structure after testing
+ rmdir(testCase.TestData.rootPath, 's');
+end
+
+function test_nProtocolsFound(testCase)
+ % findAllProtocols is used by findProtocolFile
+ protocolFolder = testCase.TestData.protocolFolder;
+ protocolStruct = BpodLib.path.findAllProtocols(protocolFolder);
+ testCase.verifyEqual(length(protocolStruct), 8, 'The number of protocols found does not match the expected number.');
+end
+
+function test_basicFind(testCase)
+ protocolFolder = testCase.TestData.protocolFolder;
+ expectedPath = fullfile(protocolFolder, 'Protocol_unique1', 'Protocol_unique1.m');
+ resultPath = BpodLib.path.findProtocolFile(protocolFolder, 'Protocol_unique1');
+ testCase.verifyEqual(resultPath, expectedPath, 'The path returned by findProtocolFile does not match the expected path.');
+end
+
+function test_noMatchFail(testCase)
+ protocolFolder = testCase.TestData.protocolFolder;
+ testCase.verifyError(@()BpodLib.path.findProtocolFile(protocolFolder, 'Protocol_noMatch'), 'BpodLib:path:ProtocolNotFound', 'Should not be able to find protocol')
+end
+
+function test_subfolderFind(testCase)
+ protocolFolder = testCase.TestData.protocolFolder;
+ expectedPath = fullfile(protocolFolder, 'subfolderB', 'Protocol_unique3', 'Protocol_unique3.m');
+ resultPath = BpodLib.path.findProtocolFile(protocolFolder, 'Protocol_unique3');
+ testCase.verifyEqual(resultPath, expectedPath, 'The path returned by findProtocolFile does not match the expected path.');
+end
+
+function test_ambiguousMatchUNIX(testCase)
+ % Test with a UNIX-style path
+ protocolFolder = testCase.TestData.protocolFolder;
+ expectedPath = fullfile(protocolFolder, 'subfolderA', 'Protocol_matching1', 'Protocol_matching1.m');
+ resultPath = BpodLib.path.findProtocolFile(protocolFolder, 'subfolderA/Protocol_matching1');
+ testCase.verifyEqual(resultPath, expectedPath, 'The path returned by findProtocolFile does not match the expected path.');
+end
+
+function test_ambiguousMatchWIN(testCase)
+ % Test with a Windows-style path
+ protocolFolder = testCase.TestData.protocolFolder;
+ expectedPath = fullfile(protocolFolder, 'subfolderA', 'Protocol_matching1', 'Protocol_matching1.m');
+ resultPath = BpodLib.path.findProtocolFile(protocolFolder, 'subfolderA\Protocol_matching1');
+ testCase.verifyEqual(resultPath, expectedPath, 'The path returned by findProtocolFile does not match the expected path.');
+end
+
+function test_ambiguousMatchSubfolder(testCase)
+ % Test with a subfolder that has multiple matches
+ protocolFolder = testCase.TestData.protocolFolder;
+ expectedPath = fullfile(protocolFolder, 'subfolderA', 'Protocol_matching2', 'Protocol_matching2.m');
+ resultPath = BpodLib.path.findProtocolFile(protocolFolder, 'subfolderA/Protocol_matching2');
+ testCase.verifyEqual(resultPath, expectedPath, 'The path returned by findProtocolFile does not match the expected path.');
+
+end
+
+function test_ambiguousMatchFail(testCase)
+ % Ensure that an error is thrown when there are multiple matches
+ protocolFolder = testCase.TestData.protocolFolder;
+ testCase.verifyError(@()BpodLib.path.findProtocolFile(protocolFolder, 'Protocol_matching1'), 'BpodLib:path:AmbiguousProtocolMatch', 'Should be unable to return protocol path')
+end
+
+function test_subfolderAmbiguousMatchFail(testCase)
+ % Ensure that an error is thrown when there are multiple matches, even across subfolders
+ protocolFolder = testCase.TestData.protocolFolder;
+ try
+ BpodLib.path.findProtocolFile(protocolFolder, 'Protocol_matching2');
+ fail('Expected findProtocolFile to throw an error for ambiguous matches, but it did not.');
+ catch ME
+ assert(strcmp(ME.identifier, 'BpodLib:path:AmbiguousProtocolMatch'), 'Unexpected error for ambiguous match.');
+ end
+end
+
+function test_emptyFolders(testCase)
+ % Top level being empty should pose no problem.
+ protocolFolder = testCase.TestData.protocolFolder;
+ rmdir(fullfile(protocolFolder,'Protocol_unique1'), 's')
+ rmdir(fullfile(protocolFolder,'Protocol_unique2'), 's')
+ rmdir(fullfile(protocolFolder,'Protocol_matching1'), 's')
+ protocols = BpodLib.path.findAllProtocols(protocolFolder);
+ testCase.verifyEqual(numel(protocols), 5, 'Failed to find expected number of protocols')
+
+ % Empty intermediate folder
+ bfolder = fullfile(protocolFolder, 'subfolderB');
+ rmdir(fullfile(bfolder, 'Protocol_unique3'), 's')
+ rmdir(fullfile(bfolder, 'Protocol_matching2'), 's')
+ protocols = BpodLib.path.findAllProtocols(protocolFolder);
+ testCase.verifyEqual(numel(protocols), 3, 'Failed to find expected number of protocols')
+end
\ No newline at end of file
diff --git a/Tests/BpodLib/path/test_getPath.m b/Tests/BpodLib/path/test_getPath.m
index bfe43c7c..b94fdbef 100644
--- a/Tests/BpodLib/path/test_getPath.m
+++ b/Tests/BpodLib/path/test_getPath.m
@@ -9,7 +9,7 @@ function setup(testCase)
mkdir(localdir)
testCase.TestData.localDir = localdir;
- mockBpod = BpodLib.BpodObject.MockBpodObject('COM13');
+ mockBpod = BpodTest.MockBpodObject('COM13');
BpodLib.BpodObject.setup.updatePathAndSettings(mockBpod, 'LocalDir', localdir)
testCase.TestData.mockBpod = mockBpod;
diff --git a/Tests/BpodSystemTests/test_BpodStartup.m b/Tests/BpodSystemTests/test_BpodStartup.m
deleted file mode 100644
index bd9c0c8c..00000000
--- a/Tests/BpodSystemTests/test_BpodStartup.m
+++ /dev/null
@@ -1,38 +0,0 @@
-function tests = test_pathing()
-% Test the user interactions of liquid calibration.
- tests = functiontests(localfunctions);
-end
-
-function setupOnce(testCase)
- global BpodSystem
- wasRunning = ~isempty(BpodSystem);
- if ~wasRunning
- Bpod('EMU')
- global BpodSystem
- end
- testCase.TestData.BpodSystem = BpodSystem;
- testCase.TestData.Original.wasRunning = wasRunning;
-end
-
-function teardownOnce(testCase)
- global BpodSystem
- if ~testCase.TestData.Original.wasRunning
- EndBpod
- end
-end
-
-function test_fullStartup(testCase)
- global BpodSystem
- BpodSystem = [];
- % verify that function Bpod() runs without error
- try
- Bpod('EMU')
- catch ME
- testCase.verifyFail(sprintf('Bpod startup failed: %s', ME.message));
- end
-
- % Return BpodSystem to the stored one
- EndBpod
- global BpodSystem
- BpodSystem = testCase.TestData.BpodSystem;
-end
\ No newline at end of file
diff --git a/Tests/GUI_Tests/test_BpodStartup.m b/Tests/GUI_Tests/test_BpodStartup.m
new file mode 100644
index 00000000..3f8c2b3f
--- /dev/null
+++ b/Tests/GUI_Tests/test_BpodStartup.m
@@ -0,0 +1,27 @@
+function tests = test_BpodStartup()
+% Test that Bpod starts up without errors.
+ tests = functiontests(localfunctions);
+end
+
+function setupOnce(testCase)
+ BpodTest.setupBpodSystemFixture(testCase, 'gui', false);
+end
+
+function teardownOnce(testCase)
+ BpodTest.teardownBpodSystemFixture(testCase);
+end
+
+function test_fullStartup(testCase)
+ global BpodSystem
+ BpodSystem = [];
+ % verify that function Bpod() runs without error
+ try
+ Bpod('EMU')
+ BpodTest.ui.closeMessageBoxes(BpodSystem)
+ catch ME
+ testCase.verifyFail(sprintf('Bpod startup failed: %s', ME.message));
+ end
+
+ % Return BpodSystem to the stored one
+ EndBpod
+end
\ No newline at end of file
diff --git a/Tests/GUI_Tests/test_ProtocolLaunchManager.m b/Tests/GUI_Tests/test_ProtocolLaunchManager.m
new file mode 100644
index 00000000..191c5205
--- /dev/null
+++ b/Tests/GUI_Tests/test_ProtocolLaunchManager.m
@@ -0,0 +1,75 @@
+function tests = test_ProtocolLaunchManager()
+ % Test user launching of protocol
+ tests = functiontests(localfunctions);
+end
+
+function setupOnce(testCase)
+ BpodTest.setupBpodSystemFixture(testCase, 'gui', true)
+end
+
+function teardownOnce(testCase)
+ BpodTest.teardownBpodSystemFixture(testCase)
+end
+
+function setup(testCase)
+ % Setup local folder
+ testCase.TestData.root = tempname;
+ localDir = fullfile(testCase.TestData.root, 'Bpod Local');
+ mkdir(localDir)
+ testCase.TestData.LocalDir = localDir;
+ protocolFolder = fullfile(localDir, 'Protocols');
+ dataFolder = fullfile(localDir, 'Data');
+ mkdir(protocolFolder)
+
+ % Configure BpodSystem to use the test directory
+ BpodSystem = testCase.TestData.BpodSystem;
+ BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystem, 'LocalDir', localDir)
+ BpodSystem.SystemSettings.ProtocolFolder = protocolFolder;
+ BpodSystem.Path.DataFolder = dataFolder;
+
+ % Put test protocols into the folder
+ assetPath = BpodTest.getPath('assets');
+ copyfile(fullfile(assetPath, 'CoreTestProtocol'), fullfile(protocolFolder, 'CoreTestProtocol'))
+ copyfile(fullfile(assetPath, 'GUITestProtocol'), fullfile(protocolFolder, 'GUITestProtocol'))
+ mkdir(fullfile(localDir, 'Data/FakeSubject/GUITestProtocol/Session Data'))
+ mkdir(fullfile(localDir, 'Data/FakeSubject/CoreTestProtocol/Session Data'))
+ mkdir(fullfile(localDir, 'Data/FakeSubject/CoreTestProtocol/Session Settings/'))
+end
+
+function teardown(testCase)
+ rmdir(testCase.TestData.root, 's');
+end
+
+function test_runLaunchManager(testCase)
+ % Test that the UI can run a protocol
+ % Ensure that the empty protocol is there
+
+
+
+ %
+ BpodSystem = testCase.TestData.BpodSystem;
+ BpodTest.ui.click(BpodSystem.GUIHandles.RunButton)
+
+ BpodSystem.GUIHandles.ProtocolSelector.Value = 1;
+ protocolName = BpodSystem.GUIHandles.ProtocolSelector.String{BpodSystem.GUIHandles.ProtocolSelector.Value};
+ testCase.verifyTrue(strcmp(protocolName, 'CoreTestProtocol'), ...
+ 'Should selector correct protocol.')
+
+ BpodSystem.GUIHandles.SubjectSelector.Value = 1;
+ subjectName = BpodSystem.GUIHandles.SubjectSelector.String{BpodSystem.GUIHandles.SubjectSelector.Value};
+ testCase.verifyTrue(strcmp(subjectName, 'FakeSubject'), ...
+ 'Should select correct subject.')
+
+ BpodSystem.GUIHandles.SettingsSelector.Value = 1;
+ settingsName = BpodSystem.GUIHandles.SettingsSelector.String{BpodSystem.GUIHandles.SettingsSelector.Value};
+ testCase.verifyTrue(strcmp(settingsName, 'DefaultSettings'),...
+ 'Should select correct settings.')
+
+ BpodTest.ui.click(BpodSystem.GUIHandles.LaunchButton)
+ pause(0.5)
+ BpodTest.ui.click(BpodSystem.GUIHandles.EndButton)
+end
+
+% function test_runWithMachine(testCase)
+% BpodSystem = testCase.TestData.MachineBpod;
+% end
\ No newline at end of file
diff --git a/Tests/BpodSystemTests/test_liquidcalibrator.m b/Tests/GUI_Tests/test_liquidcalibrator.m
similarity index 85%
rename from Tests/BpodSystemTests/test_liquidcalibrator.m
rename to Tests/GUI_Tests/test_liquidcalibrator.m
index 1308ae5a..0e946c8b 100644
--- a/Tests/BpodSystemTests/test_liquidcalibrator.m
+++ b/Tests/GUI_Tests/test_liquidcalibrator.m
@@ -4,26 +4,11 @@
end
function setupOnce(testCase)
- global BpodSystem
- wasRunning = ~isempty(BpodSystem);
- if ~wasRunning
- Bpod('EMU')
- global BpodSystem
- end
- testCase.TestData.BpodSystem = BpodSystem;
- testCase.TestData.Original = struct();
- testCase.TestData.Original.wasRunning = wasRunning;
- testCase.TestData.Original.LocalDir = BpodSystem.Path.LocalDir;
- testCase.TestData.Original.LiquidCal = BpodSystem.CalibrationTables.LiquidCal;
+ BpodTest.setupBpodSystemFixture(testCase, 'gui', true)
end
function teardownOnce(testCase)
- global BpodSystem
- BpodSystem.Path.LocalDir = testCase.TestData.Original.LocalDir;
- BpodSystem.CalibrationTables.LiquidCal = testCase.TestData.Original.LiquidCal;
- if ~testCase.TestData.Original.wasRunning
- EndBpod
- end
+ BpodTest.teardownBpodSystemFixture(testCase)
end
function setup(testCase)
@@ -79,13 +64,17 @@ function test_RunCalibration(testCase)
lc.GUIHandles.AmountEntry.String = '40';
feval(lc.GUIHandles.OkButton.Callback, [], [])
+ % Decrease n pulses to two
+ nPulses = '2';
+ lc.GUIHandles.nPulsesEdit.String = nPulses;
+
% Request running of measurement
feval(lc.GUIHandles.MeasurePendingButton.Callback, [], [])
feval(lc.GUIHandles.OkButton.Callback, [], [])
% Measurement runs
-
- lc.GUIHandles.ValueEntryGUI.GUIHandles.Valve3.String = '.8'; % enter value weighed
+ weight = num2str(.8 * str2double(nPulses) / 100);
+ lc.GUIHandles.ValueEntryGUI.GUIHandles.Valve3.String = weight; % enter value weighed
feval(lc.GUIHandles.ValueEntryGUI.GUIHandles.EnterMeasurementButton2.Callback, [], []) % click OK button
close(lc.GUIHandles.msgbox) % close box that confirms saving
@@ -94,7 +83,7 @@ function test_RunCalibration(testCase)
testCase.verifyEqual(BpodSystem.CalibrationTables.LiquidCal.getValve('Valve3').getValveTime(5), 42.507, 'AbsTol', 1e-2)
% Test GetValveTimes returns expected value
- testCase.verifyEqual(GetValveTimes(5, 3), 42.507 / 1000, 'AbsTol', 1e-2)
+ testCase.verifyEqual(GetValveTimes(5, 3) * 1000, 42.507, 'AbsTol', 1e-2)
% Test file is saved appropriately
savedliquid = BpodLib.calibration.liquid.io.load('BpodSystem', BpodSystem, 'type', 'statemachine');
diff --git a/Tests/BpodSystemTests/test_GetValveTimes.m b/Tests/Integrations/test_GetValveTimes.m
similarity index 57%
rename from Tests/BpodSystemTests/test_GetValveTimes.m
rename to Tests/Integrations/test_GetValveTimes.m
index 5a83203f..ba451922 100644
--- a/Tests/BpodSystemTests/test_GetValveTimes.m
+++ b/Tests/Integrations/test_GetValveTimes.m
@@ -4,42 +4,22 @@
end
function setupOnce(testCase)
- global BpodSystem
- wasRunning = ~isempty(BpodSystem);
- if ~wasRunning
- Bpod('EMU')
- global BpodSystem
- end
- testCase.TestData.BpodSystem = BpodSystem;
- testCase.TestData.Original.wasRunning = wasRunning;
- testCase.TestData.Original.LocalDir = BpodSystem.Path.LocalDir;
- testCase.TestData.Original.LiquidCal = BpodSystem.CalibrationTables.LiquidCal;
- testCase.TestData.Original.PortArrays = BpodSystem.CalibrationTables.PortArrays;
+ BpodTest.setupBpodSystemFixture(testCase)
end
function teardownOnce(testCase)
- global BpodSystem
- BpodSystem.Path.LocalDir = testCase.TestData.Original.LocalDir;
- BpodSystem.CalibrationTables.LiquidCal = testCase.TestData.Original.LiquidCal;
- BpodSystem.CalibrationTables.PortArrays = testCase.TestData.Original.PortArrays;
- if ~testCase.TestData.Original.wasRunning
- EndBpod
- end
+ BpodTest.teardownBpodSystemFixture(testCase)
end
function setup(testCase)
- BpodSystem = testCase.TestData.BpodSystem;
root = tempname;
testCase.TestData.root = root;
localdir = fullfile(root, 'Bpod Local');
- BpodSystem.Path.LocalDir = localdir;
mkdir(localdir)
- mkdir(fullfile(localdir, 'Calibration Files'))
- mkdir(fullfile(localdir, 'Settings'))
- mkdir(fullfile(localdir, 'Config'))
- copyfile('../BpodLib/calibration/liquid/testData/ExpectedLiquidCalibration.json', fullfile(localdir, 'Config/LiquidCalibration.json'))
- BpodSystem.CalibrationTables.LiquidCal = BpodLib.calibration.liquid.io.load('BpodSystem', BpodSystem, 'type', 'statemachine');
-
+
+ BpodSystem = testCase.TestData.BpodSystem;
+ BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystem, 'LocalDir', localdir)
+;
% -- Create port array data
PAM = BpodLib.calibration.liquid.portarray.createValveManager(BpodSystem);
valve = PAM.getValve('PA2_3');
@@ -47,7 +27,6 @@ function setup(testCase)
valve.addMeasurement(10, 10);
valve.addMeasurement(7, 7);
BpodSystem.CalibrationTables.PortArrays = PAM;
-
end
function teardown(testCase)
diff --git a/Tests/Integrations/test_headlessProtocolLaunch.m b/Tests/Integrations/test_headlessProtocolLaunch.m
new file mode 100644
index 00000000..d6b79daa
--- /dev/null
+++ b/Tests/Integrations/test_headlessProtocolLaunch.m
@@ -0,0 +1,70 @@
+function tests = test_headlessProtocolLaunch()
+% Test launching a protocol without a GUI (compatile with GitHub Actions)
+ tests = functiontests(localfunctions);
+end
+
+function setupOnce(testCase)
+ BpodTest.setupBpodSystemFixture(testCase, 'gui', false);
+end
+
+function teardownOnce(testCase)
+ BpodTest.teardownBpodSystemFixture(testCase);
+end
+
+function setup(testCase)
+ % Setup local folder
+ testCase.TestData.root = tempname;
+ localDir = fullfile(testCase.TestData.root, 'Bpod Local');
+ mkdir(localDir)
+ testCase.TestData.LocalDir = localDir;
+ protocolFolder = fullfile(localDir, 'Protocols');
+ dataFolder = fullfile(localDir, 'Data');
+ mkdir(protocolFolder)
+
+ % Configure BpodSystem to use the test directory
+ BpodSystem = testCase.TestData.BpodSystem;
+ BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystem, 'LocalDir', localDir)
+ BpodSystem.SystemSettings.ProtocolFolder = protocolFolder;
+ BpodSystem.Path.DataFolder = dataFolder;
+
+ % Put test protocols into the folder
+ assetPath = BpodTest.getPath('assets');
+ copyfile(fullfile(assetPath, 'CoreTestProtocol'), fullfile(protocolFolder, 'CoreTestProtocol'))
+ copyfile(fullfile(assetPath, 'GUITestProtocol'), fullfile(protocolFolder, 'GUITestProtocol'))
+ mkdir(fullfile(localDir, 'Data/FakeSubject/GUITestProtocol/Session Data'))
+ mkdir(fullfile(localDir, 'Data/FakeSubject/CoreTestProtocol/Session Data'))
+ mkdir(fullfile(localDir, 'Data/FakeSubject/CoreTestProtocol/Session Settings/'))
+end
+
+function teardown(testCase)
+ rmdir(testCase.TestData.root, 's')
+end
+
+function test_headlessLaunch(testCase)
+% Test that we can launch into a protocol without a GUI
+BpodSystem = testCase.TestData.BpodSystem;
+BpodLib.launcher.launchProtocol(BpodSystem, 'CoreTestProtocol', 'FakeSubject')
+BpodLib.launcher.stopProtocol(BpodSystem)
+
+% When protocol runs the data should update
+testCase.verifyTrue(isfield(BpodSystem.Data, 'TrialOutcomes'), ...
+ 'BpodSystem.Data should have been created by the protocol launch');
+
+% Verify that the data was saved into the expected location
+dataFile = fullfile(BpodSystem.Path.DataFolder, 'FakeSubject/CoreTestProtocol/Session Data', ...
+ 'FakeSubject_CoreTestProtocol_*.mat');
+files = dir(dataFile);
+testCase.verifyTrue(~isempty(dir(dataFile)), ...
+ 'Data file should have been created in the expected location');
+dataFilepath = fullfile(files(1).folder, files(1).name);
+testCase.verifyTrue(isfile(dataFilepath), ...
+ 'Data file should exist at the expected path');
+
+% Verify that the saved data contains the expected fields
+SessionData = load(dataFilepath, 'SessionData').SessionData;
+testCase.verifyTrue(isfield(SessionData, 'TrialOutcomes'),...
+ 'Data defined within protocol should be given.')
+
+testCase.verifyTrue(isfield(SessionData.RawEvents.Trial{1}.States, 'Start'),...
+ "The state 'Start' should have been entered into.")
+end
\ No newline at end of file
diff --git a/Tests/README.md b/Tests/README.md
index a1721b2a..13494f9e 100644
--- a/Tests/README.md
+++ b/Tests/README.md
@@ -1,13 +1,13 @@
# Bpod testing
-This folder contains the testing of Bpod, which can be run like so:
-1. `Bpod`
-2. navigate into `Tests/` and `runAllTests`
+This folder contains the testing of Bpod. `runBpodTests` requires two things:
+1. `Bpod` has been run (to add the necessary items into Path)
+2. The folder Tests/ is accessible (either `addpath('Tests')` or Current Directory is in it)
-Users can run all of the tests on their setup when, after having run `Bpod` to add necessary files to the Path, the user runs `runAllTests` from within this folder.
+- `BpodLib/` is unit-tests for `+BpodLib`.
+- `Integrations/` uses a `BpodSystem` without its GUI elements to run non-GUI integration tests.
+- `GUI_Tests/` includes tests for anything involving GUIs.
-The tests contained in `BpodLib` also run on GitHub Actions in pull requests made to `develop` and `master`.
-However, because of `BpodSystem`'s integration with GUI elements, GitHub cannot run tests that use `BpodSystem`.
-For this reason, any tests that require the actual `BpodSystem` should go in `BpodSystemTests/` to be run locally.
+The BpodLib and Integrations tests run on GitHub Actions in pull requests made to `develop` and `master`. However, GitHub Actions doesn't support GUI elements in its testing and so GUI_Tests can only be run locally.
-If `BpodSystem` does not exist in the workspace, the tests in `BpodSystemTests/` will call `Bpod EMU`.
-Otherwise the user can call `runAllTests` with `BpodSystem` in the workspace and the tests will use that `BpodSystem` (i.e. to run tests with a non-emulated `BpodSystem`).
\ No newline at end of file
+## Running with emulator vs real machine
+Both the tests in `Integrations/` and `GUI_Tests/` can optionally be run with an existing `BpodSystem`. If there is no active `BpodSystem` they initialize an emulator. To run tests with real hardware, calling `Bpod` and then `runBpodTests` will make tests use the `BpodSystem` that the user initialised.
diff --git a/Tests/assets/CoreTestProtocol/CoreTestProtocol.m b/Tests/assets/CoreTestProtocol/CoreTestProtocol.m
new file mode 100644
index 00000000..a6ae8d4f
--- /dev/null
+++ b/Tests/assets/CoreTestProtocol/CoreTestProtocol.m
@@ -0,0 +1,30 @@
+function CoreTestProtocol()
+% Protocol to verify that BpodSystem can run a protocol.
+% Doesn't include any GUI components, so it can run headless.
+
+global BpodSystem
+
+BpodSystem.SoftCodeHandlerFunction = 'TestSoftCodeHandler';
+valvetime = GetValveTimes(5, 1);
+
+sma = NewStateMachine();
+sma = AddState(sma, 'Name', 'Start', ...
+ 'Timer', 0,...
+ 'StateChangeConditions', {'Tup', 'End'},...
+ 'OutputActions', {'ValveState', 1, 'BNCState', 1}); % Open valve and set BNC
+sma = AddState(sma, 'Name', 'SoftCode', ...
+ 'Timer', 0,...
+ 'StateChangeConditions', {'Tup', 'End'},...
+ 'OutputActions', {'SoftCode', 1});
+sma = AddState(sma, 'Name', 'End', ...
+ 'Timer', 0,...
+ 'StateChangeConditions', {'Tup', 'exit'},...
+ 'OutputActions', {});
+
+SendStateMachine(sma);
+RawEvents = RunStateMachine;
+BpodSystem.Data = AddTrialEvents(BpodSystem.Data, RawEvents);
+BpodSystem.Data.TrialOutcomes = 1; % For verification that it ran
+SaveBpodSessionData
+
+end
\ No newline at end of file
diff --git a/Tests/assets/CoreTestProtocol/TestSoftCodeHandler.m b/Tests/assets/CoreTestProtocol/TestSoftCodeHandler.m
new file mode 100644
index 00000000..99f4a240
--- /dev/null
+++ b/Tests/assets/CoreTestProtocol/TestSoftCodeHandler.m
@@ -0,0 +1,6 @@
+function TestSoftCodeHandler(softCode)
+% Tests whether the soft code handler function is called correctly.
+
+global BpodSystem
+
+assert(isinstance(softCode, 'numeric'), 'Soft code must be numeric');
\ No newline at end of file
diff --git a/Tests/assets/GUITestProtocol/GUITestProtocol.m b/Tests/assets/GUITestProtocol/GUITestProtocol.m
new file mode 100644
index 00000000..a7549221
--- /dev/null
+++ b/Tests/assets/GUITestProtocol/GUITestProtocol.m
@@ -0,0 +1,73 @@
+function GUITestProtocol(varargin)
+% Protocol that includes GUI components.
+
+global BpodSystem
+
+p = inputParser();
+p.addParameter('verbose', true)
+p.addParameter('gui', true)
+p.addParameter('maxTrials', 3)
+p.parse(varargin);
+
+S = BpodSystem.ProtocolSettings;
+
+if isempty(fieldnames(S)) % If settings file was an empty struct, populate struct with default settings
+ S.GUI.CurrentBlock = 1; % Training level % 1 = Direct Delivery at both ports 2 = Poke for delivery
+ S.GUI.RewardAmount = 5; %ul
+ S.GUI.PortOutRegDelay = 0.5; % How long the mouse must remain out before poking back in
+end
+
+if p.Results.gui
+ BpodParameterGUI('init', S);
+end
+
+maxTrials = p.Results.maxTrials;
+
+for currentTrial = 1:maxTrials
+ if p.Results.gui
+ S = BpodParameterGUI('sync', S);
+ end
+
+ % Test getting valve values
+ valveAction = {'ValveState', };
+
+ sma = NewStateMachine();
+ sma = AddState(sma, 'Name', 'Start',...
+ 'Timer', 0,...
+ 'StateChangeConditions', {'Tup', 'OpenValve'},...
+ 'OutputActions', {});
+ sma = AddState(sma, 'Name', 'ValveCheck',...
+ 'Timer', valveDur,...
+ 'StateChangeConditions', {'Tup', 'End'},...
+ 'OutputActions', valveAction);
+ sma = AddState(sma, 'Name', 'End',...
+ 'Timer', 0,...
+ 'StateChangeConditions', {'Tup', 'exit'},...
+ 'OutputActions', {});
+
+
+ SendStateMachine(sma)
+
+ RawEvents = RunStateMachine;
+ if isempty(fieldnames(RawEvents))
+ break
+ end
+
+ BpodSystem.Data = AddTrialEvents(BpodSystem.Data,RawEvents); % Computes trial events from raw data
+ if p.Results.gui
+ BpodSystem.Data = BpodNotebook('sync', BpodSystem.Data); % Sync with Bpod notebook plugin
+ end
+ BpodSystem.Data.TrialSettings(currentTrial) = S; % Adds the settings used for the current trial to the Data struct
+ BpodSystem.Data.TrialTypes(currentTrial) = trialTypes(currentTrial); % Adds the trial type of the current trial to data
+ outcomePlot.update(trialTypes, BpodSystem.Data); % Update the outcome plot
+ SaveBpodSessionData; % Saves the field BpodSystem.Data to the current data file
+
+ HandlePauseCondition; % Checks to see if the protocol is paused. If so, waits until user resumes.
+
+ % Exit the session if the user has pressed the end button
+ if BpodSystem.Status.BeingUsed == 0
+ break
+ end
+end
+
+end
\ No newline at end of file
diff --git a/Tests/runBpodTests.m b/Tests/runBpodTests.m
index 07656f76..d55aec94 100644
--- a/Tests/runBpodTests.m
+++ b/Tests/runBpodTests.m
@@ -1,10 +1,12 @@
-function results = runbpodtests(varargin)
+function results = runBpodTests(varargin)
% RUNBPODTESTS Run all unit tests in the Tests directory and display results
import matlab.unittest.TestSuite
import matlab.unittest.TestRunner
import matlab.unittest.plugins.CodeCoveragePlugin
import matlab.unittest.plugins.codecoverage.CoverageReport
+addpath(fileparts(mfilename('fullpath')))
+
p = inputParser();
p.addOptional('test', 'all', @ischar)
p.parse(varargin{:});
@@ -15,8 +17,10 @@
testFolders = '.';
case 'bpodlib'
testFolders = 'BpodLib';
- case 'bpodsystem'
- testFolders = 'BpodSystemTests';
+ case 'gui'
+ testFolders = 'GUI_Tests';
+ case 'integrations'
+ testFolders = 'Integrations';
otherwise
error('Invalid test type specified. Use "all" or "unit".');
end
diff --git a/Tests/setupGithubEnvironment.m b/Tests/setupGithubEnvironment.m
index 9c8acf62..5a606cc4 100644
--- a/Tests/setupGithubEnvironment.m
+++ b/Tests/setupGithubEnvironment.m
@@ -1,13 +1,6 @@
%% Prepare GitHub environment
% Create Bpod_Local folder
-parentDir = fileparts(pwd);
-localDir = fullfile(parentDir, 'Bpod Local');
-calFolder = fullfile(localDir, 'Calibration Files');
-mkdir(localDir)
-mkdir(calFolder)
-copyfile(fullfile(parentDir, 'Bpod_Gen2/Examples/Example Calibration Files'), calFolder)
+% parentDir = fileparts(pwd);
-% Bpod EMU
-% EndBpod
%% Run all of the tests
% the workflow will run the tests
\ No newline at end of file