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