Skip to content

Use Advanced Logger for MATLAB #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion buildUtilities/releaseTask.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
% toolboxOptions.ToolboxGettingStartedGuide = fullfile(projObj.RootFolder,"toolbox/gettingStarted.mlx");

if ~exist(fullfile(projObj.RootFolder,"releases"), 'dir')
mkdir(fullfile(projObj.RootFolder,"releases"))
mkdir(fullfile(projObj.RootFolder,"releases"))
end
toolboxOptions.OutputFile = fullfile(projObj.RootFolder,"releases/bossdevice-api-installer.mltbx");

Expand All @@ -46,6 +46,14 @@
fwrite(fid, toolboxContent);
fclose(fid);

% Required MATLAB Add-Ons
toolboxOptions.RequiredAddons = ...
struct("Name","Advanced Logger for MATLAB", ...
"Identifier","fd9733c5-082a-4325-a5e5-e7490cdb8fb1", ...
"EarliestVersion","2.0.0", ...
"LatestVersion","2.100.0", ...
"DownloadURL","");

% Required Additional Software
% TODO: Automate download and installation of bossdevice firmware. DownloadURL must point to a ZIP file (MLDATX firmware fiel) in the downloads section of sync2brain
% toolboxOptions.RequiredAdditionalSoftware = ...
Expand Down
10 changes: 3 additions & 7 deletions tests/commonSetupTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,10 @@ function setupBossdevice(testCase)

testCase.applyFixture(SuppressedWarningsFixture("MATLAB:pfileOlderThanMfile"));

[testCase.isSGinstalled, testCase.sgPath] = bossapi.sg.isSpeedgoatBlocksetInstalled;
if testCase.isSGinstalled
% If local installation of Speedgoat blockset is present, update toolbox dependencies and work with them
bossapi.sg.removeSpeedgoatBlocksetFromPath(testCase.sgPath);
end
% Initialize bossdevice object
testCase.bd = bossdevice;

% Update target and wait until it has rebooted
testCase.bd = bossdevice;
testCase.bd.targetObject.update;

testCase.assertThat(@() bossapi.tg.pingTarget(testCase.bd.targetObject),...
Expand All @@ -46,7 +42,7 @@ function restoreInstrument(testCase)
function resetSgPath(testCase)
if testCase.isSGinstalled
% If local installation of Speedgoat blockset is present, restore default paths
bossapi.sg.addSpeedgoatBlocksetToPath(testCase.sgPath);
bossapi.sg.addSpeedgoatBlocksetToPath(testCase.bd.logObj, testCase.sgPath);
end
end

Expand Down
2 changes: 1 addition & 1 deletion toolbox/dependencies/+bossapi
6 changes: 3 additions & 3 deletions toolbox/examples/demo_sdi.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
% Get signal objects and expand to convert multidimensional signals to scalar channels
eegSig = runObj.getSignalsByName("biosignal.eeg");
eegSig.expand;
conSig = runObj.getSignalsByName("con");
conSig.expand;
spfSig = runObj.getSignalsByName("spf");
spfSig.expand;

% Plot signals in subplots
arrayfun(@(sigObj) plotOnSubPlot(sigObj,1,1,true), eegSig.Children(1:6));
plotOnSubPlot(conSig.Children(1),2,1,true);
plotOnSubPlot(spfSig.Children(1),2,1,true);
67 changes: 39 additions & 28 deletions toolbox/src/bossdevice.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
firmwareDepsPath
end

properties (SetAccess = immutable)
logObj bossapi.Logger = bossapi.Logger('bossdevice API')
end

properties (SetAccess = protected)
firmwareFilepath
end
Expand Down Expand Up @@ -64,19 +68,26 @@ function exportRecording()

methods (Access=protected)
function initOscillationProps(obj)
obj.theta = bossdevice_oscillation(obj.targetObject, 'theta');
obj.alpha = bossdevice_oscillation(obj.targetObject, 'alpha');
obj.beta = bossdevice_oscillation(obj.targetObject, 'beta');
obj.theta = bossdevice_oscillation(obj.targetObject, 'theta', obj.logObj);
obj.alpha = bossdevice_oscillation(obj.targetObject, 'alpha', obj.logObj);
obj.beta = bossdevice_oscillation(obj.targetObject, 'beta', obj.logObj);
end
end

methods
function obj = bossdevice(targetName, ipAddress)
function obj = bossdevice(targetName, ipAddress, opts)
%BOSSDEVICE Construct an instance of this class
arguments
targetName {mustBeTextScalar} = '';
ipAddress {mustBeTextScalar} = '';
opts.VerboseLevel mlog.Level = mlog.Level.INFO;
end

% Setup command window verbose level
if batchStartupOptionUsed
opts.VerboseLevel = mlog.Level.TRACE;
end
obj.logObj.CommandWindowThreshold = opts.VerboseLevel;

% Verify if SLRT Target Support Package is installed
if ~batchStartupOptionUsed && isMATLABReleaseOlderThan('R2024b') % Should run always but using if due to two MATLAB bugs
Expand Down Expand Up @@ -121,7 +132,7 @@ function initOscillationProps(obj)

if isSGinstalled
% Using own full installation of Speedgoat I/O Blockset (for development or debugging purposes)
fprintf('[Debug] Using own full installation of Speedgoat I/O Blockset v%s.\n',speedgoat.version);
obj.logObj.debug('Using own full installation of Speedgoat I/O Blockset v%s.',speedgoat.version');

elseif isfolder(fullfile(obj.sgDepsPath,matlabRelease.Release))
% MATLAB Toolbox installer adds everything to the path. We must remove first everything and
Expand All @@ -132,7 +143,7 @@ function initOscillationProps(obj)
sprintf('Speedgoat files not found in "%s". Please reach out to <a href="matlab:open(''bossdevice_api_support.html'')">sync2brain technical support</a>.',fullfile(obj.sgDepsPath,matlabRelease.Release)));

else
error('Speedgoat dependencies not found. Please reach out to <a href="matlab:open(''bossdevice_api_support.html'')">sync2brain technical support</a>.');
obj.logObj.error('Speedgoat dependencies not found. Please reach out to <a href="matlab:open(''bossdevice_api_support.html'')">sync2brain technical support</a>.');

end

Expand All @@ -149,7 +160,7 @@ function initOscillationProps(obj)
obj.targetObject = slrealtime(targetName);
if isTargetNew || ~strcmp(obj.targetObject.TargetSettings.address,ipAddress)
obj.targetObject.TargetSettings.address = ipAddress;
fprintf('Added new target configuration for "%s" with IP address "%s".\n',targetName,ipAddress);
obj.logObj.info('Added new target configuration for "%s" with IP address "%s".',targetName,ipAddress);
end

% Search firmware binary and prompt user if not found in MATLAB path
Expand All @@ -162,17 +173,17 @@ function initOscillationProps(obj)
obj.firmwareFilepath = which([obj.appName,'.mldatx']);
elseif ~batchStartupOptionUsed
obj.selectFirmware;
disp('Please run installFirmwareOnToolbox to permanently copy the firmware file into the toolbox and skip this step.');
obj.logObj.info('Please run installFirmwareOnToolbox to permanently copy the firmware file into the toolbox and skip this step.');
else
error('bossapi:noMLDATX',[obj.appName,'.mldatx could not be found in the MATLAB path.']);
obj.logObj.error('bossapi:noMLDATX',[obj.appName,'.mldatx could not be found in the MATLAB path.']);
end

% Initialize bossdevice if it is connected
if obj.isConnected
obj.initialize;
else
if ~batchStartupOptionUsed
disp('Connect the bossdevice and initialize your bossdevice object to start. For example, if you are using "bd = bossdevice", run "bd.initialize".');
obj.logObj.info('Connect the bossdevice and initialize your bossdevice object to start. For example, if you are using "bd = bossdevice", run "bd.initialize".');
end
end
end
Expand All @@ -181,7 +192,7 @@ function initOscillationProps(obj)
[filename, filepath] = uigetfile([obj.appName,'.mldatx'],...
'Select the firmware binary to load on the bossdevice');
if isequal(filename,0)
error('User selected Cancel. Please download the latest firmware version from <a href="https://sync2brain.com/downloads-bossdevice-research">sync2brain downloads portal</a> and select the firmware mldatx file to complete bossdevice dependencies.');
obj.logObj.error('User selected Cancel. Please download the latest firmware version from <a href="https://sync2brain.com/downloads-bossdevice-research">sync2brain downloads portal</a> and select the firmware mldatx file to complete bossdevice dependencies.');
else
obj.firmwareFilepath = fullfile(filepath,filename);
end
Expand All @@ -195,7 +206,7 @@ function initOscillationProps(obj)
copyfile(obj.firmwareFilepath,fileparts(obj.firmwareDepsPath),'f');
obj.firmwareFilepath = obj.firmwareDepsPath;
else
error('Firmware file is not located. Run method selectFirmware first.');
obj.logObj.error('Firmware file is not located. Run method selectFirmware first.');
end
end

Expand All @@ -209,7 +220,7 @@ function initOscillationProps(obj)
% Change IP address on remote target
res = bossapi.tg.changeRemoteTargetIP(obj.targetObject, targetIP, targetNetmask);
if res.ExitCode~=0
error(res.ErrorOutput);
obj.logObj.error(res.obj.logObj.errorOutput);
end

% Reboot target to apply new settings
Expand All @@ -219,7 +230,7 @@ function initOscillationProps(obj)
obj = bossdevice(obj.targetObject.TargetSettings.name, targetIP);

% Output message
fprintf('The IP address "%s" has been applied to the bossdevice "%s". The device is now rebooting, please wait 30 seconds before reinitializing.\n',...
obj.logObj.info('The IP address "%s" has been applied to the bossdevice "%s". The device is now rebooting, please wait 30 seconds before reinitializing.',...
targetIP,obj.targetObject.TargetSettings.name);
end

Expand All @@ -232,9 +243,9 @@ function initialize(obj)
% Set Ethernet IP in secondary interface
bossapi.tg.setEthernetInterface(obj.targetObject,'wm1','192.168.200.5/24');

fprintf('Loading application "%s" on "%s"...\n',obj.appName,obj.targetObject.TargetSettings.name);
obj.logObj.info('Loading application "%s" on "%s"...',obj.appName,obj.targetObject.TargetSettings.name);
obj.targetObject.load(obj.firmwareFilepath);
fprintf('Application loaded. Ready to start.\n');
obj.logObj.info('Application loaded. Ready to start.');
end

% Figure out some oscillation values
Expand All @@ -250,9 +261,9 @@ function start(obj)
% Start application on target if not running yet
if ~obj.targetObject.isRunning
obj.targetObject.start("ReloadOnStop",true,"StopTime",Inf);
disp('Application started!');
obj.logObj.info('Application started!');
else
disp('Application is already running.');
obj.logObj.info('Application is already running.');
end
end

Expand Down Expand Up @@ -320,7 +331,7 @@ function stop(obj)
obj.setparam('TRG', 'countdown_reset', 1);
obj.setparam('TRG', 'countdown_reset', 0);
else
error('Remaining triggers cannot be set unless application is running. Start the bossdevice before calling this method.');
obj.logObj.error('Remaining triggers cannot be set unless application is running. Start the bossdevice before calling this method.');
end
end

Expand Down Expand Up @@ -357,7 +368,7 @@ function stop(obj)
obj bossdevice
sequence {mustBeNumeric}
end
bossapi.boss.setGenSequenceOnTarget(obj.targetObject,sequence);
bossapi.boss.setGenSequenceOnTarget(obj.targetObject, sequence, obj.logObj);
end

function tiledObj = plot_generator_sequence(obj, sequence, figParent)
Expand Down Expand Up @@ -395,7 +406,7 @@ function stop(obj)
setparam(obj, 'GEN', 'enabled', true);
setparam(obj, 'TRG', 'enabled', 1);
if ~obj.isRunning
disp('bossdevice is armed and ready to start.');
obj.logObj.info('bossdevice is armed and ready to start.');
end
else
setparam(obj, 'GEN', 'enabled', false);
Expand Down Expand Up @@ -452,7 +463,7 @@ function sendPulse(obj, port, width, marker)

obj.manualTrigger;
else
disp('No pulse sent because app is not running yet. Start app first.');
obj.logObj.info('No pulse sent because app is not running yet. Start app first.');
end
end

Expand All @@ -463,14 +474,14 @@ function manualTrigger(obj)
setparam(obj, 'GEN', 'manualtrigger', true);
setparam(obj, 'GEN', 'manualtrigger', false);

disp('Triggering sequence...');
obj.logObj.info('Triggering sequence...');

% Block execution of manualTrigger while generator is running
while obj.isGeneratorRunning
pause(0.1);
end

disp('Sequence completed.');
obj.logObj.info('Sequence completed.');
end

function openDocumentation(obj)
Expand Down Expand Up @@ -571,23 +582,23 @@ function setparam(obj, path, varargin)

function stopRecording(obj)
obj.targetObject.stopRecording;
disp('Recording stopped.');
obj.logObj.info('Recording stopped.');
end

function startRecording(obj)
if ~obj.targetObject.isRunning
error('Target "%s" is not running yet. Start it before recording.',...
obj.logObj.error('Target "%s" is not running yet. Start it before recording.',...
obj.targetObject.TargetSettings.name);
end

try
obj.targetObject.stopRecording;
catch ME
disp(ME.message);
obj.logObj.info(ME.message);
end

obj.targetObject.startRecording;
disp('Recording started.');
obj.logObj.info('Recording started.');
end
end

Expand Down
16 changes: 11 additions & 5 deletions toolbox/src/bossdevice_oscillation.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
% Detailed explanation goes here

properties (Access = protected)
targetObj
targetObj slrealtime.Target
name
end

properties (SetAccess = immutable)
logObj bossapi.Logger
end

properties (Dependent)
phase_target
phase_plusminus
Expand All @@ -18,23 +22,25 @@
end

methods
function obj = bossdevice_oscillation(targetObj, name)
function obj = bossdevice_oscillation(targetObj, name, logObj)
%UNTITLED Construct an instance of this class
arguments
targetObj slrealtime.Target
name {mustBeMember(name,{'alpha','beta','theta'})}
logObj bossapi.Logger
end

obj.targetObj = targetObj;
obj.name = name;
obj.logObj = logObj;

if obj.targetObj.isConnected && obj.targetObj.isLoaded
obj.phase_target = getparam(obj.targetObj, ['mainmodel/bosslogic/EVD/' obj.name], 'phase_target');
obj.phase_plusminus = getparam(obj.targetObj, ['mainmodel/bosslogic/EVD/' obj.name], 'phase_plusminus');
obj.amplitude_min = getparam(obj.targetObj, ['mainmodel/bosslogic/EVD/' obj.name], 'amplitude_min');
obj.amplitude_max = getparam(obj.targetObj, ['mainmodel/bosslogic/EVD/' obj.name], 'amplitude_max');
else
error('bossdevice is not ready. Initialize your bossdevice object before further processing. For example, if you are using "bd = bossdevice", run "bd.initialize".');
obj.logObj.obj.logObj.error('bossdevice is not ready. Initialize your bossdevice object before further processing. For example, if you are using "bd = bossdevice", run "bd.initialize".');
end
end

Expand All @@ -53,7 +59,7 @@
else
newValue = phi;
if ~all(size(previousValue) == size(newValue))
warning('unable to set phase target, dimension mismatch')
obj.logObj.warning('Wnable to set phase target, dimension mismatch');
return
end
end
Expand Down Expand Up @@ -139,7 +145,7 @@
obj.amplitude_max = 1e6 * ones(size(obj.amplitude_max));
end
else
error('bossdevice is not ready. Initialize your bossdevice object before further processing. For example, if you are using "bd = bossdevice", run "bd.initialize".');
obj.logObj.obj.logObj.error('bossdevice is not ready. Initialize your bossdevice object before further processing. For example, if you are using "bd = bossdevice", run "bd.initialize".');
end
end

Expand Down
Loading