From 03d938edf2a8dfbed0e29194cb9c99ad5567f714 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:36:53 +1000 Subject: [PATCH 01/43] Fix launch manager go --- Functions/Launch manager/RunProtocol.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Functions/Launch manager/RunProtocol.m b/Functions/Launch manager/RunProtocol.m index cfbfb2b9..c90329e0 100644 --- a/Functions/Launch manager/RunProtocol.m +++ b/Functions/Launch manager/RunProtocol.m @@ -44,7 +44,7 @@ function RunProtocol(Opstring, varargin) case 'Start' % Starts a new behavior session if nargin == 1 - NewLaunchManager; + LaunchManager; else % Read user variables protocolName = varargin{1}; From 4119aff23d84c5c9e69bdba5e21c5250af83984d Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:55:42 +1000 Subject: [PATCH 02/43] create stopProtocol --- Functions/+BpodLib/+launcher/stopProtocol.m | 65 +++++++++++++++++++++ Functions/Launch manager/RunProtocol.m | 60 +------------------ 2 files changed, 66 insertions(+), 59 deletions(-) create mode 100644 Functions/+BpodLib/+launcher/stopProtocol.m diff --git a/Functions/+BpodLib/+launcher/stopProtocol.m b/Functions/+BpodLib/+launcher/stopProtocol.m new file mode 100644 index 00000000..7b83ec19 --- /dev/null +++ b/Functions/+BpodLib/+launcher/stopProtocol.m @@ -0,0 +1,65 @@ +function stopProtocol(BpodSystem) +% End the current protocol session. +% stopProtocol(BpodSystem) + + +if ~isempty(BpodSystem.Status.CurrentProtocolName) & BpodSystem.Status.Verbose + 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 +end \ No newline at end of file diff --git a/Functions/Launch manager/RunProtocol.m b/Functions/Launch manager/RunProtocol.m index c90329e0..4e07e49c 100644 --- a/Functions/Launch manager/RunProtocol.m +++ b/Functions/Launch manager/RunProtocol.m @@ -228,63 +228,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 From 2df00588b5a5a37839b30000f5e32dd4edf59ba3 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:57:00 +1000 Subject: [PATCH 03/43] Create setupEmulator.m --- Tests/+BpodTest/setupEmulator.m | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Tests/+BpodTest/setupEmulator.m diff --git a/Tests/+BpodTest/setupEmulator.m b/Tests/+BpodTest/setupEmulator.m new file mode 100644 index 00000000..63589ddf --- /dev/null +++ b/Tests/+BpodTest/setupEmulator.m @@ -0,0 +1,31 @@ +function BpodSystem = setupEmulator(varargin) +% Create a no-GUI Bpod Emulator +% BpodSystem = setupEmulator(_) +% +% Keyword Arguments +% ----------------- +% LocalDir : char +% Location of the Bpod Local/, should be a tempname +% 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 = 1; +BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystem, 'verbose', false, 'LocalDir', p.Results.LocalDir) +if p.Results.gui + BpodSystem.InitializeGUI(); +end +BpodSystem.SetupHardware(); +BpodSystem.Status.Initialized = true; + +end \ No newline at end of file From 7387a49209af5bb899c201e18d7c2b2ee47f90fe Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:58:23 +1000 Subject: [PATCH 04/43] Move MockBpodObject into +BpodTest --- Functions/+BpodLib/+multi/isMultiSetup.m | 2 +- Functions/+BpodLib/+path/getPath.m | 2 +- .../+BpodObject => Tests/+BpodTest}/MockBpodObject.m | 2 +- Tests/BpodLib/BpodObject/setup/test_SettingsAndFiles.m | 6 +++--- Tests/BpodLib/BpodObject/setup/test_legacySetup.m | 4 ++-- Tests/BpodLib/calibration/liquid/test_liquidutils.m | 2 +- Tests/BpodLib/multi/test_isMultiSetup.m | 4 ++-- Tests/BpodLib/path/test_getPath.m | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) rename {Functions/+BpodLib/+BpodObject => Tests/+BpodTest}/MockBpodObject.m (97%) 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/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/+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/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/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_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; From 1d21d7ec95dfad6c1ec666a46e28cbbb54dd7772 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:59:00 +1000 Subject: [PATCH 05/43] Use Status.Verbose for disp() --- Functions/@BpodObject/SetupHardware.m | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 From 8646fe3e91fb5c4af7384c469e2d93bd22333e68 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:01:06 +1000 Subject: [PATCH 06/43] Major refactor of protocol logic --- .../+BpodLib/+launcher/+ui/loadProtocols.m | 6 + .../+BpodLib/+launcher/+ui/loadSettings.m | 9 + .../+BpodLib/+launcher/+ui/loadSubjects.m | 9 + .../+BpodLib/+launcher/+ui/setDataFilePath.m | 14 ++ .../+BpodLib/+launcher/createDataFilePath.m | 20 ++ .../+launcher/createDefaultSettingsFile.m | 11 + Functions/+BpodLib/+launcher/findProtocols.m | 50 ++++ Functions/+BpodLib/+launcher/findSettings.m | 18 ++ Functions/+BpodLib/+launcher/findSubjects.m | 21 ++ Functions/+BpodLib/+launcher/launchProtocol.m | 180 ++++++++++++++ .../+BpodLib/+launcher/prepareDataFolders.m | 22 ++ Functions/+BpodLib/+path/findAllProtocols.m | 48 ++++ .../+path/findCurrentProtocolDatafiles.m | 18 ++ Functions/+BpodLib/+path/findProtocolFile.m | 57 +++++ Functions/Launch manager/LaunchManager.m | 224 ++---------------- Functions/Launch manager/RunProtocol.m | 183 ++------------ 16 files changed, 520 insertions(+), 370 deletions(-) create mode 100644 Functions/+BpodLib/+launcher/+ui/loadProtocols.m create mode 100644 Functions/+BpodLib/+launcher/+ui/loadSettings.m create mode 100644 Functions/+BpodLib/+launcher/+ui/loadSubjects.m create mode 100644 Functions/+BpodLib/+launcher/+ui/setDataFilePath.m create mode 100644 Functions/+BpodLib/+launcher/createDataFilePath.m create mode 100644 Functions/+BpodLib/+launcher/createDefaultSettingsFile.m create mode 100644 Functions/+BpodLib/+launcher/findProtocols.m create mode 100644 Functions/+BpodLib/+launcher/findSettings.m create mode 100644 Functions/+BpodLib/+launcher/findSubjects.m create mode 100644 Functions/+BpodLib/+launcher/launchProtocol.m create mode 100644 Functions/+BpodLib/+launcher/prepareDataFolders.m create mode 100644 Functions/+BpodLib/+path/findAllProtocols.m create mode 100644 Functions/+BpodLib/+path/findCurrentProtocolDatafiles.m create mode 100644 Functions/+BpodLib/+path/findProtocolFile.m 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..959692ef --- /dev/null +++ b/Functions/+BpodLib/+launcher/launchProtocol.m @@ -0,0 +1,180 @@ +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? +protocolRunFilepath = BpodLib.path.findProtocolFile(BpodSystem.SystemSettings.ProtocolFolder, protocolPointer); +[protocolRunFolder, protocolName] = fileparts(protocolRunFilepath); + +% 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 = protocolRunFilepath; % 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 +fprintf('%s Launched protocol: %s\n', datestr(now, 13), protocolRunFilepath) +if isempty(p.Results.protocolvarargin) + % Cleanest easiest behaviour + run(protocolRunFilepath); +else + % If the user requested to pass additional arguments to the protocol + protocolFuncHandle = str2func(protocolName); + funcInfo = functions(protocolFuncHandle); + if ~strcmp(funcInfo.file, protocolRunFilepath) + % 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', protocolRunFilepath) + 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/+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/Launch manager/LaunchManager.m b/Functions/Launch manager/LaunchManager.m index 96647004..aa8c9864 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,9 @@ 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); +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 4e07e49c..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 @@ -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 From 50df5851cb8fabb826d33cb1db17762abd63bee1 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:01:24 +1000 Subject: [PATCH 07/43] Use anonymous funcs for callbacks --- Functions/@BpodObject/InitializeGUI.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Functions/@BpodObject/InitializeGUI.m b/Functions/@BpodObject/InitializeGUI.m index 5187ca90..1c076a3b 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',... From 7f4011b8294fc7261e036d8e66c3fd2d27faab5f Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:01:40 +1000 Subject: [PATCH 08/43] Save current protocol path into session data --- Functions/AddTrialEvents.m | 41 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 20 deletions(-) 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; From bb49aa462b03b356f9f81992572d184603c8e43d Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:01:53 +1000 Subject: [PATCH 09/43] Missing item --- Functions/Launch manager/LaunchManager.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Functions/Launch manager/LaunchManager.m b/Functions/Launch manager/LaunchManager.m index aa8c9864..57bfa2c7 100644 --- a/Functions/Launch manager/LaunchManager.m +++ b/Functions/Launch manager/LaunchManager.m @@ -658,6 +658,7 @@ function launch_protocol(a,b) settingsName = settingsList{settingsIndex}; close(BpodSystem.GUIHandles.LaunchManagerFig); +protocolFolderPath = fullfile(BpodSystem.Path.ProtocolFolder, protocolName); BpodLib.launcher.launchProtocol(BpodSystem, protocolFolderPath, subjectName, 'settingsName', settingsName); function outputString = spaces2underscores(inputString) From 3150f8bf8071f811fc8149c4d6c2100e4353165c Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:03:07 +1000 Subject: [PATCH 10/43] Create tests --- Tests/+BpodTest/createDummyProtocolFolders.m | 98 +++++++++++++++++ Tests/BpodLib/launcher/test_launcherPathing.m | 92 ++++++++++++++++ Tests/BpodLib/path/test_findProtocolFile.m | 102 ++++++++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 Tests/+BpodTest/createDummyProtocolFolders.m create mode 100644 Tests/BpodLib/launcher/test_launcherPathing.m create mode 100644 Tests/BpodLib/path/test_findProtocolFile.m diff --git a/Tests/+BpodTest/createDummyProtocolFolders.m b/Tests/+BpodTest/createDummyProtocolFolders.m new file mode 100644 index 00000000..e8131213 --- /dev/null +++ b/Tests/+BpodTest/createDummyProtocolFolders.m @@ -0,0 +1,98 @@ +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 +create_protocol(fullfile(protocolFolder), 'Protocol_unique1'); +create_protocol(fullfile(protocolFolder), 'Protocol_unique2'); +create_protocol(fullfile(protocolFolder), 'Protocol_matching1'); +create_protocol(fullfile(protocolFolder, 'subfolderA'), 'Protocol_matching1'); +create_protocol(fullfile(protocolFolder, 'subfolderA'), 'Protocol_matching2'); +create_protocol(fullfile(protocolFolder, 'subfolderB'), 'Protocol_unique3'); +create_protocol(fullfile(protocolFolder, 'subfolderB'), 'Protocol_matching2'); +create_protocol(fullfile(protocolFolder, 'subfolderB/subfolderC'), 'Protocol_unique4'); + +% Create a data folder structure +create_datafolder(dataFolder, 'FakeSubject', 'Protocol_matching1'); +create_datafolder(dataFolder, 'FakeSubject', 'Protocol_unique1'); +create_datafolder(dataFolder, 'FakeSubject', 'Protocol_unique2'); +create_datafolder(dataFolder, 'Subject1', 'Protocol_unique1'); +create_datafolder(dataFolder, 'Subject1', 'Protocol_matching1'); +create_datafolder(dataFolder, 'Subject2', 'Protocol_matching1'); +create_datafolder(dataFolder, 'Subject2', 'Protocol_unique2'); + +% 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 + +function create_protocol(folderPath, protocolName) + % Create a folder with the protocol name and an empty protocol file + mkdir(fullfile(folderPath, protocolName)); + fileID = fopen(fullfile(folderPath, protocolName, [protocolName '.m']), 'w'); + fprintf(fileID, 'function %s\n disp("This is a protocol file.");', protocolName); + fclose(fileID); +end + +function create_datafolder(dataFolder, subjectName, protocolName) + % Create a data folder structure for a subject and protocol + mkdir(fullfile(dataFolder, subjectName, protocolName, 'Session Data')); + mkdir(fullfile(dataFolder, subjectName, protocolName, 'Session Settings')); +end \ No newline at end of file diff --git a/Tests/BpodLib/launcher/test_launcherPathing.m b/Tests/BpodLib/launcher/test_launcherPathing.m new file mode 100644 index 00000000..73b6daea --- /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.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/path/test_findProtocolFile.m b/Tests/BpodLib/path/test_findProtocolFile.m new file mode 100644 index 00000000..ba4e47a9 --- /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.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 From 76b543bd5b81072ac36742e1908a1f41e57b1255 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:17:56 +1000 Subject: [PATCH 11/43] Improve addpath/rmpath for protocols Prior to this the code assumed the protocol folder was in the top-level of the Protocols folder, meaning that the folders for the protocol would not be removed from the Path if it was a nested protocol. --- Functions/+BpodLib/+launcher/launchProtocol.m | 16 +++++++++------- Functions/+BpodLib/+launcher/stopProtocol.m | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Functions/+BpodLib/+launcher/launchProtocol.m b/Functions/+BpodLib/+launcher/launchProtocol.m index 959692ef..10bcbbd2 100644 --- a/Functions/+BpodLib/+launcher/launchProtocol.m +++ b/Functions/+BpodLib/+launcher/launchProtocol.m @@ -30,8 +30,8 @@ function launchProtocol(BpodSystem, protocolPointer, subjectName, varargin) % Generate path to protocol file % ? allow absolute paths to define protocols outside of protocol folder? -protocolRunFilepath = BpodLib.path.findProtocolFile(BpodSystem.SystemSettings.ProtocolFolder, protocolPointer); -[protocolRunFolder, protocolName] = fileparts(protocolRunFilepath); +CurrentProtocol = BpodLib.path.findProtocolFile(BpodSystem.SystemSettings.ProtocolFolder, protocolPointer); +[protocolRunFolder, protocolName] = fileparts(CurrentProtocol); % Verify data path dataFilePath = BpodLib.launcher.createDataFilePath(BpodSystem.Path.DataFolder, protocolName, subjectName); @@ -152,26 +152,28 @@ function launchProtocol(BpodSystem, protocolPointer, subjectName, varargin) 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 = protocolRunFilepath; % for removal from path on next run +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 -fprintf('%s Launched protocol: %s\n', datestr(now, 13), protocolRunFilepath) +if BpodSystem.Status.Verbose + fprintf('%s Launched protocol: %s\n', datestr(now, 13), CurrentProtocol) +end if isempty(p.Results.protocolvarargin) % Cleanest easiest behaviour - run(protocolRunFilepath); + run(CurrentProtocol); else % If the user requested to pass additional arguments to the protocol protocolFuncHandle = str2func(protocolName); funcInfo = functions(protocolFuncHandle); - if ~strcmp(funcInfo.file, protocolRunFilepath) + 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', protocolRunFilepath) + 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 diff --git a/Functions/+BpodLib/+launcher/stopProtocol.m b/Functions/+BpodLib/+launcher/stopProtocol.m index 7b83ec19..41997da9 100644 --- a/Functions/+BpodLib/+launcher/stopProtocol.m +++ b/Functions/+BpodLib/+launcher/stopProtocol.m @@ -8,7 +8,8 @@ function stopProtocol(BpodSystem) 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)); +% This folder is added in launchProtocol.m +rmpath(fileparts(BpodSystem.Path.CurrentProtocol)); warning on BpodSystem.Status.BeingUsed = 0; BpodSystem.Status.CurrentProtocolName = ''; From 01585b53cd7f22816817d9133bdb7fb17b08f8cd Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:18:06 +1000 Subject: [PATCH 12/43] Improve verbosity handling --- Functions/+BpodLib/+launcher/stopProtocol.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Functions/+BpodLib/+launcher/stopProtocol.m b/Functions/+BpodLib/+launcher/stopProtocol.m index 41997da9..1945410f 100644 --- a/Functions/+BpodLib/+launcher/stopProtocol.m +++ b/Functions/+BpodLib/+launcher/stopProtocol.m @@ -2,10 +2,9 @@ function stopProtocol(BpodSystem) % End the current protocol session. % stopProtocol(BpodSystem) - if ~isempty(BpodSystem.Status.CurrentProtocolName) & BpodSystem.Status.Verbose disp(' ') - disp([BpodSystem.Status.CurrentProtocolName ' ended']) + 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 @@ -57,8 +56,10 @@ function stopProtocol(BpodSystem) end catch end -set(BpodSystem.GUIHandles.RunButton, 'cdata', BpodSystem.GUIData.GoButton,... - 'TooltipString', 'Launch behavior session'); +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 From 708ee139ce26342ae8117d4f58b492b0b5e8c2c6 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:32:21 +1000 Subject: [PATCH 13/43] Move integration tests into GUI/non-GUI folders --- Tests/{BpodSystemTests => GUI_Tests}/test_BpodStartup.m | 0 Tests/{BpodSystemTests => GUI_Tests}/test_liquidcalibrator.m | 0 Tests/{BpodSystemTests => Integrations}/test_GetValveTimes.m | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename Tests/{BpodSystemTests => GUI_Tests}/test_BpodStartup.m (100%) rename Tests/{BpodSystemTests => GUI_Tests}/test_liquidcalibrator.m (100%) rename Tests/{BpodSystemTests => Integrations}/test_GetValveTimes.m (100%) diff --git a/Tests/BpodSystemTests/test_BpodStartup.m b/Tests/GUI_Tests/test_BpodStartup.m similarity index 100% rename from Tests/BpodSystemTests/test_BpodStartup.m rename to Tests/GUI_Tests/test_BpodStartup.m diff --git a/Tests/BpodSystemTests/test_liquidcalibrator.m b/Tests/GUI_Tests/test_liquidcalibrator.m similarity index 100% rename from Tests/BpodSystemTests/test_liquidcalibrator.m rename to Tests/GUI_Tests/test_liquidcalibrator.m diff --git a/Tests/BpodSystemTests/test_GetValveTimes.m b/Tests/Integrations/test_GetValveTimes.m similarity index 100% rename from Tests/BpodSystemTests/test_GetValveTimes.m rename to Tests/Integrations/test_GetValveTimes.m From e1d48361fe884a56af99cb46e8965951e5048646 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:41:12 +1000 Subject: [PATCH 14/43] Use verbosity for EndBpod --- Functions/EndBpod.m | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 From fecb398740fa70d7c1acdbcb797a9f5145bda1c5 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:39:09 +1000 Subject: [PATCH 15/43] Fix error with emulator dialog box If no COM is found the emulator dialog box is launched, and then the rest of the function runs. In that situation no BpodSystem is created and so the attempt to report on the calibration table fails. --- Bpod.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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; From 2973d26af3f072a66816af66253174043ed9fc3b Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:24:55 +1000 Subject: [PATCH 16/43] Add GUI refresh skip if running without GUI --- Functions/@BpodObject/RefreshGUI.m | 4 ++++ Functions/RunStateMachine.m | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) 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/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; From 2ef0b7754d672bc391994f48ce733a299b8f934e Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:25:10 +1000 Subject: [PATCH 17/43] Create BpodSystem setup and teardown --- Tests/+BpodTest/setupBpodSystemFixture.m | 40 +++++++++++++++++++++ Tests/+BpodTest/teardownBpodSystemFixture.m | 21 +++++++++++ 2 files changed, 61 insertions(+) create mode 100644 Tests/+BpodTest/setupBpodSystemFixture.m create mode 100644 Tests/+BpodTest/teardownBpodSystemFixture.m diff --git a/Tests/+BpodTest/setupBpodSystemFixture.m b/Tests/+BpodTest/setupBpodSystemFixture.m new file mode 100644 index 00000000..67b3bbdb --- /dev/null +++ b/Tests/+BpodTest/setupBpodSystemFixture.m @@ -0,0 +1,40 @@ +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 +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)) +end + +testCase.TestData.BpodSystem = BpodSystem; + +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..ea1ef13b --- /dev/null +++ b/Tests/+BpodTest/teardownBpodSystemFixture.m @@ -0,0 +1,21 @@ +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) +end + +end \ No newline at end of file From 2c23ea93af4080a8c686c93c72f7e728f5f6d3d0 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:25:20 +1000 Subject: [PATCH 18/43] Create getPath.m --- Tests/+BpodTest/getPath.m | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Tests/+BpodTest/getPath.m 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 From 6a2f5f969d8e8a71ea4c1329450bbaa8f3fe22df Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:25:28 +1000 Subject: [PATCH 19/43] Create test protocols --- .../CoreTestProtocol/CoreTestProtocol.m | 24 +++++++ .../assets/GUITestProtocol/GUITestProtocol.m | 71 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 Tests/assets/CoreTestProtocol/CoreTestProtocol.m create mode 100644 Tests/assets/GUITestProtocol/GUITestProtocol.m diff --git a/Tests/assets/CoreTestProtocol/CoreTestProtocol.m b/Tests/assets/CoreTestProtocol/CoreTestProtocol.m new file mode 100644 index 00000000..be4471b6 --- /dev/null +++ b/Tests/assets/CoreTestProtocol/CoreTestProtocol.m @@ -0,0 +1,24 @@ +function CoreTestProtocol() +% Protocol to verify that it can run + +global BpodSystem + +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', '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/GUITestProtocol/GUITestProtocol.m b/Tests/assets/GUITestProtocol/GUITestProtocol.m new file mode 100644 index 00000000..780a81f9 --- /dev/null +++ b/Tests/assets/GUITestProtocol/GUITestProtocol.m @@ -0,0 +1,71 @@ +function TestProtocol(varargin) +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', 'EnterState',... + 'Timer', 0,... + 'StateChangeConditions', {'Tup', 'OpenValve'},... + 'OutputActions', {}); + sma = AddState(sma, 'Name', 'ValveCheck',... + 'Timer', valveDur,... + 'StateChangeConditions', {'Tup', 'ExitState'},... + 'OutputActions', valveAction); + sma = AddState(sma, 'Name', 'ExitState',... + '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 + return + end +end + +end \ No newline at end of file From 7349c42d6d67f8d6771a69fa897ee4f48a0acc46 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:25:43 +1000 Subject: [PATCH 20/43] Use setup fixture funcs --- Tests/Integrations/test_GetValveTimes.m | 33 +++++-------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/Tests/Integrations/test_GetValveTimes.m b/Tests/Integrations/test_GetValveTimes.m index 5a83203f..ba451922 100644 --- a/Tests/Integrations/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) From b1205ff8ec88c2d098896c7a854bac71672f9bfd Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:25:50 +1000 Subject: [PATCH 21/43] Create test_protocolLaunch.m --- Tests/Integrations/test_protocolLaunch.m | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Tests/Integrations/test_protocolLaunch.m diff --git a/Tests/Integrations/test_protocolLaunch.m b/Tests/Integrations/test_protocolLaunch.m new file mode 100644 index 00000000..b9b738e3 --- /dev/null +++ b/Tests/Integrations/test_protocolLaunch.m @@ -0,0 +1,71 @@ +function tests = test_protocolLaunch() +% Test launching a protocol without a GUI + 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 From 4ffb9c92d72624c562b6eb1c400534da22f3b017 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:31:04 +1000 Subject: [PATCH 22/43] Refactor environment setup into func --- .github/workflows/runAllTests.yml | 2 +- Tests/setupGithubEnvironment.m | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/runAllTests.yml b/.github/workflows/runAllTests.yml index 2df473a0..0d5e1767 100644 --- a/.github/workflows/runAllTests.yml +++ b/.github/workflows/runAllTests.yml @@ -25,7 +25,7 @@ jobs: - name: Setup environment uses: matlab-actions/run-command@v2 with: - command: addpath('Tests'); setupGithubEnvironment; + command: setupGithubEnvironment; # Runs a set of commands using the runners shell - name: Run all tests diff --git a/Tests/setupGithubEnvironment.m b/Tests/setupGithubEnvironment.m index 9c8acf62..98be39ea 100644 --- a/Tests/setupGithubEnvironment.m +++ b/Tests/setupGithubEnvironment.m @@ -1,11 +1,7 @@ %% 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) +addpath('Tests') % Bpod EMU % EndBpod From b6f0f1a6e1035858ba70b565349a8acf25ddb56c Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:31:10 +1000 Subject: [PATCH 23/43] Add integrations --- .github/workflows/runAllTests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/runAllTests.yml b/.github/workflows/runAllTests.yml index 0d5e1767..5ae3fd9c 100644 --- a/.github/workflows/runAllTests.yml +++ b/.github/workflows/runAllTests.yml @@ -32,7 +32,7 @@ jobs: uses: matlab-actions/run-tests@v2 with: source-folder: Functions/ - select-by-folder: Tests/BpodLib/ + select-by-folder: Tests/BpodLib/; Tests/Integrations test-windows: runs-on: windows-latest From 6620d3c0dff0adc794679099b955ddc192657d95 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:37:37 +1000 Subject: [PATCH 24/43] Fix adding of Tests to Path --- .github/workflows/runAllTests.yml | 2 +- Tests/setupGithubEnvironment.m | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/runAllTests.yml b/.github/workflows/runAllTests.yml index 5ae3fd9c..0e6aab92 100644 --- a/.github/workflows/runAllTests.yml +++ b/.github/workflows/runAllTests.yml @@ -25,7 +25,7 @@ jobs: - name: Setup environment uses: matlab-actions/run-command@v2 with: - command: setupGithubEnvironment; + command: addpath('Tests'); setupGithubEnvironment; # Runs a set of commands using the runners shell - name: Run all tests diff --git a/Tests/setupGithubEnvironment.m b/Tests/setupGithubEnvironment.m index 98be39ea..5a606cc4 100644 --- a/Tests/setupGithubEnvironment.m +++ b/Tests/setupGithubEnvironment.m @@ -1,9 +1,6 @@ %% Prepare GitHub environment % Create Bpod_Local folder -parentDir = fileparts(pwd); -addpath('Tests') +% parentDir = fileparts(pwd); -% Bpod EMU -% EndBpod %% Run all of the tests % the workflow will run the tests \ No newline at end of file From 9fe90a1b228d2c25b7282f7cba5b0933beb10468 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:02:38 +1000 Subject: [PATCH 25/43] Refactor setup/teardown --- Tests/GUI_Tests/test_liquidcalibrator.m | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Tests/GUI_Tests/test_liquidcalibrator.m b/Tests/GUI_Tests/test_liquidcalibrator.m index 1308ae5a..2c9db80a 100644 --- a/Tests/GUI_Tests/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) From 9ba43da99e6d85682fd0ee7930ef15fff3dbc9c0 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:02:59 +1000 Subject: [PATCH 26/43] Improve ordering --- Tests/+BpodTest/setupEmulator.m | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Tests/+BpodTest/setupEmulator.m b/Tests/+BpodTest/setupEmulator.m index 63589ddf..18adc416 100644 --- a/Tests/+BpodTest/setupEmulator.m +++ b/Tests/+BpodTest/setupEmulator.m @@ -2,10 +2,13 @@ % Create a no-GUI Bpod Emulator % BpodSystem = setupEmulator(_) % -% Keyword Arguments -% ----------------- +% Arguments +% --------- % LocalDir : char % Location of the Bpod Local/, should be a tempname +% +% Keyword Arguments +% ----------------- % gui : bool (default = false) % Use .InitializeGUI() method % @@ -20,12 +23,12 @@ p.parse(varargin{:}) BpodSystem = BpodObject('verbose', false); -BpodSystem.EmulatorMode = 1; +BpodSystem.EmulatorMode = true; BpodLib.BpodObject.setup.updatePathAndSettings(BpodSystem, 'verbose', false, 'LocalDir', p.Results.LocalDir) +BpodSystem.SetupHardware(); if p.Results.gui BpodSystem.InitializeGUI(); end -BpodSystem.SetupHardware(); BpodSystem.Status.Initialized = true; end \ No newline at end of file From dc9b35c3fc138d9f0cf52b1521c0e645ad90db69 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:03:17 +1000 Subject: [PATCH 27/43] Remove global var for EMU setup --- Functions/@BpodObject/InitializeGUI.m | 2 +- Functions/Override Panels/StateMachinePanel_0_7.m | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Functions/@BpodObject/InitializeGUI.m b/Functions/@BpodObject/InitializeGUI.m index 1c076a3b..1ce58aac 100644 --- a/Functions/@BpodObject/InitializeGUI.m +++ b/Functions/@BpodObject/InitializeGUI.m @@ -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 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 From 1679cb216efadbe91d33c9e0021716a6ef778693 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:03:50 +1000 Subject: [PATCH 28/43] Add Tests to source-folder The environment setup doesn't retain the path? --- .github/workflows/runAllTests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/runAllTests.yml b/.github/workflows/runAllTests.yml index 0e6aab92..d20ab346 100644 --- a/.github/workflows/runAllTests.yml +++ b/.github/workflows/runAllTests.yml @@ -25,13 +25,13 @@ 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/ + source-folder: Functions/; Tests/ select-by-folder: Tests/BpodLib/; Tests/Integrations test-windows: From 3b8b5ec291198c0874794bcc911c888cdb83b87c Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:12:50 +1000 Subject: [PATCH 29/43] Use getPath GH Actions says fileparts(which('Bpod')) is '/home/runner/work/Bpod_Gen2/Bpod_Gen2/Functions/Internal Functions/', so let's use the self-fixing function instead. --- Functions/@BpodObject/BpodObject.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'))); From e3b9fad3cd19fb7bf553d2a277fc0c3d10b0d804 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:18:35 +1000 Subject: [PATCH 30/43] Add Tests/ to Windows testing source --- .github/workflows/runAllTests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/runAllTests.yml b/.github/workflows/runAllTests.yml index d20ab346..ef3d7cd2 100644 --- a/.github/workflows/runAllTests.yml +++ b/.github/workflows/runAllTests.yml @@ -32,7 +32,7 @@ jobs: uses: matlab-actions/run-tests@v2 with: source-folder: Functions/; Tests/ - select-by-folder: Tests/BpodLib/; Tests/Integrations + 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/ From 9dd8d555afb908a870b950d42da791bbae6f9c04 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 18:02:37 +1000 Subject: [PATCH 31/43] Create test_RunProtocol.m --- Tests/GUI_Tests/test_RunProtocol.m | 73 ++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 Tests/GUI_Tests/test_RunProtocol.m diff --git a/Tests/GUI_Tests/test_RunProtocol.m b/Tests/GUI_Tests/test_RunProtocol.m new file mode 100644 index 00000000..1ca62463 --- /dev/null +++ b/Tests/GUI_Tests/test_RunProtocol.m @@ -0,0 +1,73 @@ +function tests = test_RunProtocol() + tests = functiontests(localfunctions); +end + +function setupOnce(testCase) + BpodTest.setupBpodSystemFixture(testCase) +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.Original.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 From e090157b81320db0c123dd43480821081c751567 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sat, 2 Aug 2025 18:02:45 +1000 Subject: [PATCH 32/43] Create click.m --- Tests/+BpodTest/+ui/click.m | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Tests/+BpodTest/+ui/click.m 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 From db016f1b93969e981e64cd9062ce6e3d3f3c831d Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:09:06 +1000 Subject: [PATCH 33/43] Refactor dummy protocol construction Weirder and more wonderful tests will require better construction patterns --- Tests/+BpodTest/+dir/createDataFolder.m | 35 ++++++++ .../{ => +dir}/createDummyProtocolFolders.m | 56 ++++++------- .../+BpodTest/+dir/createProtocolMaterials.m | 80 +++++++++++++++++++ Tests/BpodLib/launcher/test_launcherPathing.m | 2 +- Tests/BpodLib/path/test_findProtocolFile.m | 2 +- 5 files changed, 143 insertions(+), 32 deletions(-) create mode 100644 Tests/+BpodTest/+dir/createDataFolder.m rename Tests/+BpodTest/{ => +dir}/createDummyProtocolFolders.m (57%) create mode 100644 Tests/+BpodTest/+dir/createProtocolMaterials.m 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/createDummyProtocolFolders.m b/Tests/+BpodTest/+dir/createDummyProtocolFolders.m similarity index 57% rename from Tests/+BpodTest/createDummyProtocolFolders.m rename to Tests/+BpodTest/+dir/createDummyProtocolFolders.m index e8131213..cff13178 100644 --- a/Tests/+BpodTest/createDummyProtocolFolders.m +++ b/Tests/+BpodTest/+dir/createDummyProtocolFolders.m @@ -51,23 +51,33 @@ testEnvironment.dataFolder = dataFolder; % Create a protocol folder structure -create_protocol(fullfile(protocolFolder), 'Protocol_unique1'); -create_protocol(fullfile(protocolFolder), 'Protocol_unique2'); -create_protocol(fullfile(protocolFolder), 'Protocol_matching1'); -create_protocol(fullfile(protocolFolder, 'subfolderA'), 'Protocol_matching1'); -create_protocol(fullfile(protocolFolder, 'subfolderA'), 'Protocol_matching2'); -create_protocol(fullfile(protocolFolder, 'subfolderB'), 'Protocol_unique3'); -create_protocol(fullfile(protocolFolder, 'subfolderB'), 'Protocol_matching2'); -create_protocol(fullfile(protocolFolder, 'subfolderB/subfolderC'), 'Protocol_unique4'); +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'}, ... + }; -% Create a data folder structure -create_datafolder(dataFolder, 'FakeSubject', 'Protocol_matching1'); -create_datafolder(dataFolder, 'FakeSubject', 'Protocol_unique1'); -create_datafolder(dataFolder, 'FakeSubject', 'Protocol_unique2'); -create_datafolder(dataFolder, 'Subject1', 'Protocol_unique1'); -create_datafolder(dataFolder, 'Subject1', 'Protocol_matching1'); -create_datafolder(dataFolder, 'Subject2', 'Protocol_matching1'); -create_datafolder(dataFolder, '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'); @@ -81,18 +91,4 @@ filepath = fullfile(dataFolder, 'FakeSubject', 'Protocol_matching1', 'Session Data', 'FakeSubject_Protocol_matching1_20160315_021512.mat'); save(filepath, 'emptystruct'); -end - -function create_protocol(folderPath, protocolName) - % Create a folder with the protocol name and an empty protocol file - mkdir(fullfile(folderPath, protocolName)); - fileID = fopen(fullfile(folderPath, protocolName, [protocolName '.m']), 'w'); - fprintf(fileID, 'function %s\n disp("This is a protocol file.");', protocolName); - fclose(fileID); -end - -function create_datafolder(dataFolder, subjectName, protocolName) - % Create a data folder structure for a subject and protocol - mkdir(fullfile(dataFolder, subjectName, protocolName, 'Session Data')); - mkdir(fullfile(dataFolder, subjectName, protocolName, 'Session Settings')); 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/BpodLib/launcher/test_launcherPathing.m b/Tests/BpodLib/launcher/test_launcherPathing.m index 73b6daea..2504e56e 100644 --- a/Tests/BpodLib/launcher/test_launcherPathing.m +++ b/Tests/BpodLib/launcher/test_launcherPathing.m @@ -7,7 +7,7 @@ function setupOnce(testCase) rootPath = tempname; % Generate a unique temporary directory testCase.TestData.rootPath = rootPath; - testEnvironment = BpodTest.createDummyProtocolFolders(rootPath); + testEnvironment = BpodTest.dir.createDummyProtocolFolders(rootPath); testCase.TestData.protocolFolder = testEnvironment.protocolFolder; testCase.TestData.dataFolder = testEnvironment.dataFolder; end diff --git a/Tests/BpodLib/path/test_findProtocolFile.m b/Tests/BpodLib/path/test_findProtocolFile.m index ba4e47a9..0dc422f7 100644 --- a/Tests/BpodLib/path/test_findProtocolFile.m +++ b/Tests/BpodLib/path/test_findProtocolFile.m @@ -7,7 +7,7 @@ function setupOnce(testCase) rootPath = tempname; % Generate a unique temporary directory testCase.TestData.rootPath = rootPath; - testEnvironment = BpodTest.createDummyProtocolFolders(rootPath); + testEnvironment = BpodTest.dir.createDummyProtocolFolders(rootPath); testCase.TestData.protocolFolder = testEnvironment.protocolFolder; end From 9910210eceff5247f284dea5747ce6040387ca98 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:09:59 +1000 Subject: [PATCH 34/43] Rename test protocols --- .../{test_RunProtocol.m => test_ProtocolLaunchManager.m} | 6 ++++-- ...{test_protocolLaunch.m => test_headlessProtocolLaunch.m} | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) rename Tests/GUI_Tests/{test_RunProtocol.m => test_ProtocolLaunchManager.m} (96%) rename Tests/Integrations/{test_protocolLaunch.m => test_headlessProtocolLaunch.m} (95%) diff --git a/Tests/GUI_Tests/test_RunProtocol.m b/Tests/GUI_Tests/test_ProtocolLaunchManager.m similarity index 96% rename from Tests/GUI_Tests/test_RunProtocol.m rename to Tests/GUI_Tests/test_ProtocolLaunchManager.m index 1ca62463..3fc5dc07 100644 --- a/Tests/GUI_Tests/test_RunProtocol.m +++ b/Tests/GUI_Tests/test_ProtocolLaunchManager.m @@ -1,4 +1,5 @@ -function tests = test_RunProtocol() +function tests = test_ProtocolLaunchManager() + % Test user launching of protocol tests = functiontests(localfunctions); end @@ -11,7 +12,7 @@ function teardownOnce(testCase) end function setup(testCase) - % Setup local folder + % Setup local folder testCase.TestData.root = tempname; localDir = fullfile(testCase.TestData.root, 'Bpod Local'); mkdir(localDir) @@ -34,6 +35,7 @@ function setup(testCase) 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 diff --git a/Tests/Integrations/test_protocolLaunch.m b/Tests/Integrations/test_headlessProtocolLaunch.m similarity index 95% rename from Tests/Integrations/test_protocolLaunch.m rename to Tests/Integrations/test_headlessProtocolLaunch.m index b9b738e3..d6b79daa 100644 --- a/Tests/Integrations/test_protocolLaunch.m +++ b/Tests/Integrations/test_headlessProtocolLaunch.m @@ -1,5 +1,5 @@ -function tests = test_protocolLaunch() -% Test launching a protocol without a GUI +function tests = test_headlessProtocolLaunch() +% Test launching a protocol without a GUI (compatile with GitHub Actions) tests = functiontests(localfunctions); end @@ -67,5 +67,4 @@ function test_headlessLaunch(testCase) testCase.verifyTrue(isfield(SessionData.RawEvents.Trial{1}.States, 'Start'),... "The state 'Start' should have been entered into.") - end \ No newline at end of file From 4a6d12440e52a61da2718ad2739a32c19dfd2767 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:27:57 +1000 Subject: [PATCH 35/43] Set existing BpodSystem verbosity 0 --- Tests/+BpodTest/setupBpodSystemFixture.m | 2 ++ Tests/+BpodTest/teardownBpodSystemFixture.m | 1 + 2 files changed, 3 insertions(+) diff --git a/Tests/+BpodTest/setupBpodSystemFixture.m b/Tests/+BpodTest/setupBpodSystemFixture.m index 67b3bbdb..2075e9d2 100644 --- a/Tests/+BpodTest/setupBpodSystemFixture.m +++ b/Tests/+BpodTest/setupBpodSystemFixture.m @@ -22,6 +22,8 @@ function setupBpodSystemFixture(testCase, varargin) 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) diff --git a/Tests/+BpodTest/teardownBpodSystemFixture.m b/Tests/+BpodTest/teardownBpodSystemFixture.m index ea1ef13b..1ccdf117 100644 --- a/Tests/+BpodTest/teardownBpodSystemFixture.m +++ b/Tests/+BpodTest/teardownBpodSystemFixture.m @@ -16,6 +16,7 @@ function teardownBpodSystemFixture(testCase) 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 From b882cef594f5e5b1833ed6590902c234302ab697 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:35:05 +1000 Subject: [PATCH 36/43] Add softcode handler test --- Tests/assets/CoreTestProtocol/CoreTestProtocol.m | 8 +++++++- Tests/assets/CoreTestProtocol/TestSoftCodeHandler.m | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 Tests/assets/CoreTestProtocol/TestSoftCodeHandler.m diff --git a/Tests/assets/CoreTestProtocol/CoreTestProtocol.m b/Tests/assets/CoreTestProtocol/CoreTestProtocol.m index be4471b6..a6ae8d4f 100644 --- a/Tests/assets/CoreTestProtocol/CoreTestProtocol.m +++ b/Tests/assets/CoreTestProtocol/CoreTestProtocol.m @@ -1,8 +1,10 @@ function CoreTestProtocol() -% Protocol to verify that it can run +% 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(); @@ -10,6 +12,10 @@ function CoreTestProtocol() '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'},... 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 From 0b3a1ee43c43815094d9bc674c3e11888ff837c2 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:40:16 +1000 Subject: [PATCH 37/43] Close phone home box --- Tests/+BpodTest/+ui/closePhoneHome.m | 8 ++++++++ Tests/+BpodTest/setupBpodSystemFixture.m | 1 + Tests/GUI_Tests/test_BpodStartup.m | 21 +++++--------------- Tests/GUI_Tests/test_ProtocolLaunchManager.m | 4 ++-- 4 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 Tests/+BpodTest/+ui/closePhoneHome.m 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/Tests/+BpodTest/setupBpodSystemFixture.m b/Tests/+BpodTest/setupBpodSystemFixture.m index 2075e9d2..e885fdec 100644 --- a/Tests/+BpodTest/setupBpodSystemFixture.m +++ b/Tests/+BpodTest/setupBpodSystemFixture.m @@ -35,6 +35,7 @@ function setupBpodSystemFixture(testCase, varargin) if p.Results.gui assert(~isempty(BpodSystem.GUIHandles)) + BpodTest.ui.closePhoneHome(BpodSystem) end testCase.TestData.BpodSystem = BpodSystem; diff --git a/Tests/GUI_Tests/test_BpodStartup.m b/Tests/GUI_Tests/test_BpodStartup.m index bd9c0c8c..f3ff71f7 100644 --- a/Tests/GUI_Tests/test_BpodStartup.m +++ b/Tests/GUI_Tests/test_BpodStartup.m @@ -1,24 +1,14 @@ -function tests = test_pathing() -% Test the user interactions of liquid calibration. +function tests = test_BpodStartup() +% Test that Bpod starts up without errors. 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; + BpodTest.setupBpodSystemFixture(testCase, 'gui', false); end function teardownOnce(testCase) - global BpodSystem - if ~testCase.TestData.Original.wasRunning - EndBpod - end + BpodTest.teardownBpodSystemFixture(testCase); end function test_fullStartup(testCase) @@ -27,12 +17,11 @@ function test_fullStartup(testCase) % verify that function Bpod() runs without error try Bpod('EMU') + BpodTest.ui.closePhoneHome(BpodSystem) 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_ProtocolLaunchManager.m b/Tests/GUI_Tests/test_ProtocolLaunchManager.m index 3fc5dc07..191c5205 100644 --- a/Tests/GUI_Tests/test_ProtocolLaunchManager.m +++ b/Tests/GUI_Tests/test_ProtocolLaunchManager.m @@ -4,7 +4,7 @@ end function setupOnce(testCase) - BpodTest.setupBpodSystemFixture(testCase) + BpodTest.setupBpodSystemFixture(testCase, 'gui', true) end function teardownOnce(testCase) @@ -47,7 +47,7 @@ function test_runLaunchManager(testCase) % - BpodSystem = testCase.TestData.Original.BpodSystem; + BpodSystem = testCase.TestData.BpodSystem; BpodTest.ui.click(BpodSystem.GUIHandles.RunButton) BpodSystem.GUIHandles.ProtocolSelector.Value = 1; From e282da663358c06973557d8547808b58fd6dfca4 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:40:45 +1000 Subject: [PATCH 38/43] Remove global --- Functions/@BpodObject/InitializeGUI.m | 4 ++-- Functions/@BpodObject/refreshGUIPanels.m | 4 ++-- Functions/Override Panels/DefaultBpodModule_Panel.m | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Functions/@BpodObject/InitializeGUI.m b/Functions/@BpodObject/InitializeGUI.m index 1ce58aac..2edc9443 100644 --- a/Functions/@BpodObject/InitializeGUI.m +++ b/Functions/@BpodObject/InitializeGUI.m @@ -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/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/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 From 4d6af3836a58fa21c3c50687060c6a01d64cd344 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:41:05 +1000 Subject: [PATCH 39/43] Fix name --- Tests/assets/GUITestProtocol/GUITestProtocol.m | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/assets/GUITestProtocol/GUITestProtocol.m b/Tests/assets/GUITestProtocol/GUITestProtocol.m index 780a81f9..a7549221 100644 --- a/Tests/assets/GUITestProtocol/GUITestProtocol.m +++ b/Tests/assets/GUITestProtocol/GUITestProtocol.m @@ -1,4 +1,6 @@ -function TestProtocol(varargin) +function GUITestProtocol(varargin) +% Protocol that includes GUI components. + global BpodSystem p = inputParser(); @@ -30,15 +32,15 @@ function TestProtocol(varargin) valveAction = {'ValveState', }; sma = NewStateMachine(); - sma = AddState(sma, 'Name', 'EnterState',... + sma = AddState(sma, 'Name', 'Start',... 'Timer', 0,... 'StateChangeConditions', {'Tup', 'OpenValve'},... 'OutputActions', {}); sma = AddState(sma, 'Name', 'ValveCheck',... 'Timer', valveDur,... - 'StateChangeConditions', {'Tup', 'ExitState'},... + 'StateChangeConditions', {'Tup', 'End'},... 'OutputActions', valveAction); - sma = AddState(sma, 'Name', 'ExitState',... + sma = AddState(sma, 'Name', 'End',... 'Timer', 0,... 'StateChangeConditions', {'Tup', 'exit'},... 'OutputActions', {}); @@ -64,7 +66,7 @@ function TestProtocol(varargin) % Exit the session if the user has pressed the end button if BpodSystem.Status.BeingUsed == 0 - return + break end end From 22ff665650062d5644a09d6ed6a0f8a33cc732ae Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:54:53 +1000 Subject: [PATCH 40/43] Update cases --- Tests/runBpodTests.m | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 From d40425defada414b491e9489affa232ad67387f2 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:02:46 +1000 Subject: [PATCH 41/43] Improve documentation --- Tests/+BpodTest/setupBpodSystemFixture.m | 2 +- Tests/README.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/+BpodTest/setupBpodSystemFixture.m b/Tests/+BpodTest/setupBpodSystemFixture.m index e885fdec..a510a6b3 100644 --- a/Tests/+BpodTest/setupBpodSystemFixture.m +++ b/Tests/+BpodTest/setupBpodSystemFixture.m @@ -1,6 +1,6 @@ function setupBpodSystemFixture(testCase, varargin) % Prepare a BpodSystem for testing, using an existing BpodSystem or creating a new one. -% setupBpodSystemFixture(testCase) +% setupBpodSystemFixture(testCase, _) % % Expected to be called in setupOnce() % Works in tandem with teardownBpodSystemFixture.m 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. From 964856dc815495da0b1802b270e9a372fdecf371 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:25:23 +1000 Subject: [PATCH 42/43] Reduce real machine time requirement --- Tests/GUI_Tests/test_liquidcalibrator.m | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Tests/GUI_Tests/test_liquidcalibrator.m b/Tests/GUI_Tests/test_liquidcalibrator.m index 2c9db80a..0e946c8b 100644 --- a/Tests/GUI_Tests/test_liquidcalibrator.m +++ b/Tests/GUI_Tests/test_liquidcalibrator.m @@ -64,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 @@ -79,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'); From 2de419dd6f9ea99faba4d9bf59d7f1c516cc05d8 Mon Sep 17 00:00:00 2001 From: George Stuyt <15712782+ogeesan@users.noreply.github.com> Date: Mon, 11 Aug 2025 13:11:52 +1000 Subject: [PATCH 43/43] Make liquid warning non-modal Required to make tests run through without manual intervention. --- .gitignore | 2 ++ .../+BpodObject/+setup/updatePathAndSettings.m | 8 ++++---- Tests/+BpodTest/+ui/closeMessageBoxes.m | 17 +++++++++++++++++ Tests/GUI_Tests/test_BpodStartup.m | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 Tests/+BpodTest/+ui/closeMessageBoxes.m 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/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/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/GUI_Tests/test_BpodStartup.m b/Tests/GUI_Tests/test_BpodStartup.m index f3ff71f7..3f8c2b3f 100644 --- a/Tests/GUI_Tests/test_BpodStartup.m +++ b/Tests/GUI_Tests/test_BpodStartup.m @@ -17,7 +17,7 @@ function test_fullStartup(testCase) % verify that function Bpod() runs without error try Bpod('EMU') - BpodTest.ui.closePhoneHome(BpodSystem) + BpodTest.ui.closeMessageBoxes(BpodSystem) catch ME testCase.verifyFail(sprintf('Bpod startup failed: %s', ME.message)); end