diff --git a/Modules/+Drivers/+Kinesis/KinesisBSC203.m b/Modules/+Drivers/+Kinesis/KinesisBSC203.m new file mode 100644 index 000000000..56f02aec8 --- /dev/null +++ b/Modules/+Drivers/+Kinesis/KinesisBSC203.m @@ -0,0 +1,344 @@ +classdef KinesisBSC203 < Drivers.Kinesis.Kinesis_invisible & Modules.Driver + % Driver for kinesis stages. Currently only works with motor_channels = [1 2 3] (i.e. x y z motors in ascending order). Situations other than this will result in wrong motors moving, to be fixed. + properties + name + motor_channels + end + properties(Constant, Hidden) + GENERICMOTORDLL='Thorlabs.MotionControl.GenericMotorCLI.dll'; + GENERICMOTORCLASSNAME='Thorlabs.MotionControl.GenericMotorCLI.GenericMotorCLI'; + STEPPERMOTORDLL='Thorlabs.MotionControl.Benchtop.StepperMotorCLI.dll'; + STEPPERMOTORCLASSNAME='Thorlabs.MotionControl.Benchtop.StepperMotorCLI.BenchtopStepperMotor'; + end + + properties(SetAccess = private, SetObservable, AbortSet) + isconnected = false; % Flag set if device connected + serialnumbers; % Device serial numbers + controllername; % Controller Name + controllerdescription % Controller Description + stagename; % Stage Name + acceleration; % Acceleration + maxvelocity; % Maximum velocity limit + minvelocity; % Minimum velocity limit + positions = [NaN, NaN, NaN]; % Motor position (1 * 3 array) + + Homed; + isMoving = false; + + Travel; + factor; + end + + properties(Hidden) + deviceNET; % Device object within .NET + channelsNET; % Channel object within .NET (1 * 3 cell) + motorSettingsNET; % motorSettings within .NET (1 * 3 cell) + currentDeviceSettingsNET; % currentDeviceSetings within .NET (1 * 3 cell) + deviceInfoNET; % deviceInfo within .NET (1 * 3 cell) + end + + methods(Access=private) + % Constructor + function obj = KinesisBSC203(serialNo, travel, name, motor_channels, factor) % Instantiate the KinesisBSC203 motor object + Drivers.Kinesis.KinesisBSC203.loaddlls; % Load DLLs if not already loaded + obj.motor_channels = motor_channels; + obj.factor = factor; + obj.connect(serialNo); % Connect device + obj.Travel = travel; + obj.name = name; + end + end + + methods(Static) + % Use this to create/retrieve instance associated with serialNo + function obj = instance(serialNo, travel, name, motor_channels, factor) + mlock; + if nargin < 2 + name = serialNo; + end + persistent Objects + if isempty(Objects) + Objects = Drivers.Kinesis.KinesisBSC203.empty(1,0); % Create an empty class + end + for i = 1:length(Objects) + if isvalid(Objects(i)) && isequal(serialNo,Objects(i).singleton_id) % Find instance with the same singleton ID + obj = Objects(i); + return + end + end + obj = Drivers.Kinesis.KinesisBSC203(serialNo, travel, name, motor_channels, factor); % Create an instance + obj.singleton_id = serialNo; % Define singleton ID + Objects(end+1) = obj; % Add the instance to the object list + end + end + + methods + % Connect to the device with a specified serial number and initialize the device + function connect(obj, serialNo) % serialNo := str + obj.GetDevices; % Call this to build device list if not already done + if ~obj.isconnected() % Connect and initialize device if not connected + obj.deviceNET = Thorlabs.MotionControl.Benchtop.StepperMotorCLI.BenchtopStepperMotor.CreateBenchtopStepperMotor(serialNo); % Create an instance of .NET BenchtopStepperMotor + obj.deviceNET.Connect(serialNo); % Connect to device via .NET interface + for i = obj.motor_channels + if ~isnan(i) + obj.channelsNET{i} = obj.deviceNET.GetChannel(i); % Get channel objects of the device + obj.channelsNET{i}.ClearDeviceExceptions(); % Clear device exceptions via .NET interface + else + disp('Motor channel is empty') + end + end + + obj.initialize(serialNo) % Initialize the device + else % Device already connected + error('Device is already connected') + end + obj.updatestatus % Update status variables from device + end + + function initialize(obj, serialNo) % Initialize all three channels of the device, serialNo := str + for i = obj.motor_channels + if ~isnan(i) + try + if ~obj.channelsNET{i}.IsSettingsInitialized() + obj.channelsNET{i}.WaitForSettingsInitialized(obj.TIMEOUTSETTINGS); % Initialize the ith channel + else + disp('Device Already Initialized.') + end + if ~obj.channelsNET{i}.IsSettingsInitialized() % Device not successfully initialized + error('Unable to initialize device') + end + obj.channelsNET{i}.StartPolling(obj.TPOLLING); % Start polling device via .NET interface + obj.channelsNET{i}.EnableDevice(); % Enable device via .NET interface + + % Initialize motor configuration + deviceID = obj.channelsNET{i}.DeviceID; + settingsLoadOption = Drivers.Kinesis.Kinesis_invisible.GetSettingsLoadOption(serialNo, deviceID); + obj.motorSettingsNET{i} = obj.channelsNET{i}.GetMotorConfiguration(serialNo, settingsLoadOption); + + % Initialize current motor settings + obj.currentDeviceSettingsNET{i}=obj.channelsNET{i}.MotorDeviceSettings; + obj.deviceInfoNET{i} = obj.channelsNET{i}.GetDeviceInfo(); % Get deviceInfo via .NET interface + catch err + disp(['Unable to initialize channel ', num2str(i)]); + rethrow err + end + end + end + + end + + function updatestatus(obj) + obj.isconnected = obj.deviceNET.IsConnected(); % connection status + homed = true(1, 3); + moving = false(1, 3); + for i = 1:3 + + if ~isnan(obj.motor_channels(i)) + obj.serialnumbers{obj.motor_channels(i)}=char(obj.channelsNET{obj.motor_channels(i)}.DeviceID); % update serial number + obj.controllername{obj.motor_channels(i)}=char(obj.deviceInfoNET{obj.motor_channels(i)}.Name); % update controleller name + obj.controllerdescription{obj.motor_channels(i)}=char(obj.deviceInfoNET{obj.motor_channels(i)}.Description); % update controller description + obj.stagename{obj.motor_channels(i)}=char(obj.motorSettingsNET{obj.motor_channels(i)}.DeviceSettingsName); % update stagename + velocityparams{obj.motor_channels(i)}=obj.channelsNET{obj.motor_channels(i)}.GetVelocityParams(); % update velocity parameter + obj.acceleration{obj.motor_channels(i)}=System.Decimal.ToDouble(velocityparams{obj.motor_channels(i)}.Acceleration); % update acceleration parameter + obj.maxvelocity{obj.motor_channels(i)}=System.Decimal.ToDouble(velocityparams{obj.motor_channels(i)}.MaxVelocity); % update max velocit parameter + obj.minvelocity{obj.motor_channels(i)}=System.Decimal.ToDouble(velocityparams{obj.motor_channels(i)}.MinVelocity); % update Min velocity parameter + obj.positions(i) = System.Decimal.ToDouble(obj.channelsNET{obj.motor_channels(i)}.Position) * obj.factor; % motor positions + homed(i) = ~obj.channelsNET{obj.motor_channels(i)}.NeedsHoming; + if obj.channelsNET{obj.motor_channels(i)}.State == Thorlabs.MotionControl.GenericMotorCLI.MotorStates.Idle + moving(i) = false; + else + moving(i) = true; + end + end + end + obj.Homed = all(homed); + obj.isMoving = any(moving); + end + + function disconnect(obj) + obj.isconnected = obj.deviceNET.IsConnected(); % Read connection status + if obj.isconnected % Disconnect device if connected + for i = 1:3 + if ~isnan(obj.motor_channels(i)) + try + obj.channelsNET{obj.motor_channels(i)}.StopPolling(); % Stop polling device via .NET interface + obj.channelsNET{obj.motor_channels(i)}.DisableDevice(); % Disable device via .NET interface + catch + error(['Unable to disconnect device',obj.serialnumbers{obj.motor_channels(i)}]); + end + + end + end + try + obj.deviceNET.Disconnect(true) + catch + error(['Unable to disconnect device',obj.serialnumbers{obj.motor_channels(i)}]); + end + obj.isconnected = obj.deviceNET.IsConnected(); + else % Cannot disconnect because device not connected + error('Device not connected.') + end + end + + function home(obj) + for i = 1:3 + if ~isnan(obj.motor_channels(i)) + workDone=obj.channelsNET{obj.motor_channels(i)}.InitializeWaitHandler(); % Initialise Waithandler for timeout + obj.channelsNET{obj.motor_channels(i)}.Home(workDone); % Home device via .NET interface + obj.channelsNET{obj.motor_channels(i)}.Wait(obj.TIMEOUTMOVE); % Wait for move to finish + end + end + obj.updatestatus; % Update status variables from device + end + + function tf = checkMove(obj, target_pos) + % Check to make sure target_pos is ok to execute + % Error if it is outside limits + % Error if the channel needs to be homed + % Otherwise returns true + tf = true; + for i = 1:3 + if ~isnan(obj.motor_channels(i)) + assert(~obj.channelsNET{obj.motor_channels(i)}.NeedsHoming,'Motor %f is not homed!', i) + assert(target_pos(i) <= max(obj.Travel) && target_pos(i) >= min(obj.Travel),... + 'Attempted to move motor %f to %f, but it is limited to %f, %f', obj.motor_channels(i), target_pos, min(obj.Travel), max(obj.Travel)) + end + end + end + + function moveto(obj, target_pos) + % Move to target position, target_pos := 1 * 3 array of double + assert(obj.checkMove(target_pos),'Target position is out of range') + target_pos_calib = target_pos / obj.factor; + obj.isMoving = true; + for i = 1:3 + if ~isnan(obj.motor_channels(i)) + try + workDone=obj.channelsNET{obj.motor_channels(i)}.InitializeWaitHandler(); % Initialise Waithandler for timeout + obj.channelsNET{obj.motor_channels(i)}.MoveTo(target_pos_calib(i), workDone); % Move device to position via .NET interface + obj.channelsNET{obj.motor_channels(i)}.Wait(obj.TIMEOUTMOVE); % Wait for move to finish + catch + error(['Unable to Move channel ',obj.serialnumbers{obj.motor_channels(i)},' to ',num2str(target_pos(i))]); + end + end + end + obj.updatestatus + end + + function step(obj, channelNo, distance) + % Method to move the motor by a jog + % channelNo := int, distance : double, + if distance < 0 % Set jog direction to backwards + motordirection=Thorlabs.MotionControl.GenericMotorCLI.MotorDirection.Backward; + elseif distance > 0 % Set jog direction to forwards + motordirection=Thorlabs.MotionControl.GenericMotorCLI.MotorDirection.Forward; + else + error('Step size cannot be zero') + end + + % Calculate the position after the step + step_pos = [0 0 0]; + distance_calib = distance / obj.factor; + obj.channelsNET{channelNo}.SetJogStepSize(abs(distance_calib)) % Set the step size for jog + step_pos(channelNo) = obj.channelsNET{channelNo}.GetJogStepSize(); + target_pos = obj.positions + step_pos; + + % Check whether the position after the step exceeds the travel + tf = obj.checkMove(target_pos); + + if tf + try + workDone = obj.channelsNET{channelNo}.InitializeWaitHandler(); + obj.channelsNET{channelNo}.MoveJog(motordirection, workDone); % Execute jog + obj.channelsNET{channelNo}.Wait(obj.TIMEOUTMOVE); + catch + error('Unable to execute jog') + end + else + error('Target position is out of range') + end + obj.updatestatus + end + + function movecont(h, channelNo, varargin) % Set motor to move continuously + if (nargin>2) && (varargin{1}) % if parameter given (e.g. 1) move backwards + motordirection=Thorlabs.MotionControl.GenericMotorCLI.MotorDirection.Backward; + else % if no parametr given move forwards + motordirection=Thorlabs.MotionControl.GenericMotorCLI.MotorDirection.Forward; + end + h.channelsNET{channelNo}.MoveContinuous(motordirection); % Set motor into continous move via .NET interface + obj.updatestatus; % Update status variables from device + end + + function stop(h, immediate) % Stop the motor moving (needed if set motor to continous) + for i = 1:3 + if ~isnan(obj.motor_channels(i)) + if nargin > 1 && immediate + h.channelsNET{obj.motor_channels(i)}.StopImmediate(); + else + h.channelsNET{obj.motor_channels(i)}.Stop(h.TIMEOUTMOVE); % Stop motor movement via.NET interface + end + end + end + obj.updatestatus % Update status variables from device + end + + function pos = get.positions(obj) + pos = [NaN NaN NaN]; + for i = 1:3 + if ~isnan(obj.motor_channels(i)) + pos(i) = System.Decimal.ToDouble(obj.channelsNET{obj.motor_channels(i)}.Position) * obj.factor; + end + end + end + + function enable(obj) + for i = 1:3 + if ~isnan(obj.motor_channels(i)) + obj.channelsNET{obj.motor_channels(i)}.EnableDevice(); % Enable device via .NET interface + end + end + obj.updatestatus + end + + function disable(obj) + for i = 1:3 + if ~isnan(obj.motor_channels(i)) + obj.channelsNET{obj.motor_channels(i)}.DisableDevice(); % Enable device via .NET interface + end + end + obj.updatestatus + end + + end + + methods (Static) + function loaddlls() % Load DLLs (Load all relevant dlls in case the GetDevices function was not called) + if ~exist(Drivers.Kinesis.KinesisBSC203.DEVICEMANAGERCLASSNAME,'class') + try % Load DeviceManagerCLI dll if not already loaded + NET.addAssembly([Drivers.Kinesis.KinesisBSC203.MOTORPATHDEFAULT,Drivers.Kinesis.KinesisBSC203.DEVICEMANAGERDLL]); + catch + error('Unable to load .NET assemblies') + end + end + if ~exist(Drivers.Kinesis.KinesisBSC203.GENERICMOTORCLASSNAME,'class') + try % Load in DLLs if not already loaded + NET.addAssembly([Drivers.Kinesis.KinesisBSC203.MOTORPATHDEFAULT,Drivers.Kinesis.KinesisBSC203.GENERICMOTORDLL]); + NET.addAssembly([Drivers.Kinesis.KinesisBSC203.MOTORPATHDEFAULT,Drivers.Kinesis.KinesisBSC203.STEPPERMOTORDLL]); + catch % DLLs did not load + error('Unable to load .NET assemblies') + end + end + end + + function motorSerialNumbers = getAvailMotors() + motorSerialNumbers = {}; + serialNumbers = Drivers.Kinesis.Kinesis_invisible.GetDevices; + for i = 1 : length(serialNumbers) + if strcmp(serialNumbers{i}(1:2), '70') + motorSerialNumbers{end + 1} = serialNumbers{i}; + end + end + end + end +end diff --git a/Modules/+Drivers/+Kinesis/Kinesis_invisible.m b/Modules/+Drivers/+Kinesis/Kinesis_invisible.m new file mode 100644 index 000000000..4502f7770 --- /dev/null +++ b/Modules/+Drivers/+Kinesis/Kinesis_invisible.m @@ -0,0 +1,58 @@ +classdef Kinesis_invisible < handle + + properties(Constant, Hidden) + MOTORPATHDEFAULT='C:\Program Files\Thorlabs\Kinesis\'; + DEVICEMANAGERDLL='Thorlabs.MotionControl.DeviceManagerCLI.dll'; + DEVICEMANAGERCLASSNAME='Thorlabs.MotionControl.DeviceManagerCLI.DeviceManagerCLI'; + + TPOLLING=250; % Default polling time + TIMEOUTSETTINGS=7000; % Default timeout time for settings change + TIMEOUTMOVE=100000; % Default time out time for motor move + end + + methods(Abstract) + + loaddlls() % Load dlls + + connect(obj, serialNum) % Connect devices with a specified serial number + + disconnect(obj) % add comments + + end + + methods + function obj = Kinesis_invisible() + Drivers.Kinesis.Kinesis_invisible.loadDeviceManagerdll(); + end + end + + methods(Static) + function loadDeviceManagerdll() % Load DeviceManagerCLI dll + if ~exist(Drivers.Kinesis.Kinesis_invisible.DEVICEMANAGERCLASSNAME,'class') + try + NET.addAssembly([Drivers.Kinesis.Kinesis_invisible.MOTORPATHDEFAULT,Drivers.Kinesis.Kinesis_invisible.DEVICEMANAGERDLL]); + catch + error('Unable to load .NET assemblies') + end + end + end + + function serialNumbers = GetDevices() % Returns a cell array of serial numbers of connected devices + Drivers.Kinesis.Kinesis_invisible.loadDeviceManagerdll(); % Load DeviceMnagerCLI dll if not already loaded + + Thorlabs.MotionControl.DeviceManagerCLI.DeviceManagerCLI.BuildDeviceList(); % Build device list + serialNumbersNet = Thorlabs.MotionControl.DeviceManagerCLI.DeviceManagerCLI.GetDeviceList(); % Get device list + serialNumbers=cell(ToArray(serialNumbersNet)); % Convert serial numbers to cell array + end + + function settingsLoadOption = GetSettingsLoadOption(serialNo, deviceID) + Drivers.Kinesis.Kinesis_invisible.loadDeviceManagerdll(); % Load DeviceMnagerCLI dll if not already loaded + + deviceConfigMag = Thorlabs.MotionControl.DeviceManagerCLI.DeviceConfigurationManager; + deviceConfigMagInstance = deviceConfigMag.Instance(); + deviceConfigMagInstance.CreateDeviceConfiguration(serialNo, uint32(str2double(serialNo(1:2))), true); + deviceConfig = deviceConfigMag.Instance().GetDeviceConfiguration(deviceID); + settingsLoadOption = deviceConfig.ApplicationSettingsLoadOption; + end + end +end \ No newline at end of file diff --git a/Modules/+Stages/BSC203.m b/Modules/+Stages/BSC203.m new file mode 100644 index 000000000..34ab4f477 --- /dev/null +++ b/Modules/+Stages/BSC203.m @@ -0,0 +1,208 @@ +classdef BSC203 < Modules.Stage + %MAX302 Control the motors of this stage. + % Uses 3 instances of APTMotor for x,y,z. + % + % All values are in microns. It is good practice to home after + % construction. + % + % Home is -2,-2,-2 + + properties + prefs = {'availMotors','x_motor','y_motor','z_motor', 'factor'}; + end + + properties(GetObservable, SetObservable, AbortSet) + availMotors = Prefs.MultipleChoice('', 'choices', Drivers.Kinesis.KinesisBSC203.getAvailMotors, 'allow_empty', true, 'set', 'set_controller'); + x_motor = Prefs.MultipleChoice(1,'choices',{1,2,3},'allow_empty',true,'set','set_x_motor','help_text','Which channel in the controller controls the x direction motor','readonly',true); % Show only for now; need to fix driver to be able to change properly + y_motor = Prefs.MultipleChoice(2,'choices',{1,2,3},'allow_empty',true,'set','set_y_motor','help_text','Which channel in the controller controls the y direction motor','readonly',true); + z_motor = Prefs.MultipleChoice(3,'choices',{1,2,3},'allow_empty',true,'set','set_z_motor','help_text','Which channel in the controller controls the z direction motor','readonly',true); + factor = Prefs.Double(0.5, 'help_text', 'The factor between the actual distance moved and the distance read from Kinesis') + end + properties(SetAccess=private,SetObservable,AbortSet) + % Default here will only matter if motors aren't set + Homed = false; + Moving = false; % Track this to update position + end + properties(SetAccess=private) + position + motors; + motor_channels = [1 2 3]; + end + + properties (Constant) + % Currently used as placeholders to avoid the error as Abstract properties + xRange = [-2 2] * 1000; + yRange = [-2 2] * 1000; + zRange = [-2 2] * 1000; + end + + methods(Static) + function obj = instance() + mlock; + persistent Object + if isempty(Object) || ~isvalid(Object) + Object = Stages.BSC203(); + end + obj = Object; + end + + end + methods(Access=private) + function obj = BSC203() + obj.loadPrefs; + end + + function val = set_motor_generic(obj, channelNo, motor_index) + % Generic function that checks to make sure that no other motor + % is using this channel before setting motor connection + + not_motor_index = [1 2 3]; + not_motor_index = not_motor_index(not_motor_index ~= motor_index); + + if isempty(channelNo) + obj.motor_channels(motor_index) = NaN; + obj.motors.disconnect; + delete(obj.motors); + elseif channelNo ~= obj.motor_channels(motor_index) % Need to check to prevent initialising motor multiple times at startup + if any(obj.motor_channels(not_motor_index) == channelNo) % Check that not equal to an existing channel + error(strcat('Channel ', num2str(channelNo), ' was already used for other motors.')) + else + if ~isempty(obj.motors) && isobject(obj.motors) && isvalid(obj.motors) && obj.motors.isconnected + obj.motors.disconnect; + delete(obj.motors); + end + + obj.motor_channels(motor_index) = channelNo; + end + end + + obj.set_controller(obj.availMotors); + + val = channelNo; + end + end + % Callback functions for BSC203 Motor + methods(Access=?KinesisBSC203) + function homedCallback(obj,varargin) + if ~isempty(obj.motors) && isobject(obj.motors) && isvalid(obj.motors) + obj.Homed = obj.motors.Homed; + else + obj.Homed = false; + end + end + function movingCallback(obj,varargin) + if ~isempty(obj.motors) && isobject(obj.motors) && isvalid(obj.motors) + obj.Moving = obj.motors.isMoving; + else + obj.Moving = false; + end + end + end + methods + function delete(obj) + if isobject(obj.motors) + obj.motors.disconnect; + delete(obj.motors); + end + end + function pos = get.position(obj) + % this function reads the current motor position + pos = [NaN NaN NaN]; + if ~isempty(obj.motors) && isobject(obj.motors) && isvalid(obj.motors) + n = 1; + for channelNo = obj.motor_channels + positions = obj.motors.positions; + pos = positions; + if isnan(channelNo) + pos(n) = NaN; + else + pos(n) = positions(channelNo); + n = n + 1; + end + end + end + end + function move(obj,x,y,z) + % Method to move the motor to a given position [x, y, z] + pos = obj.position; % reads the current position + + new_pos = [x,y,z]; + + for i = 1:length(new_pos) + % Check whether the input target position for a certain axis is empty, + if isempty(new_pos(i)) || new_pos(i)==pos(i) || isnan(new_pos(i)) + new_pos(i) = pos(i); % if empty, set the target position to the original position of that axis + end + end + + if ~isempty(obj.motors)&&isobject(obj.motors) && isvalid(obj.motors) + obj.motors.moveto(new_pos) % move to the new position + end + end + + function enable(obj) + %Method to enable motors drive + if ~isempty(obj.motors)&&isobject(obj.motors) && isvalid(obj.motors) + obj.motors.enable(); + end + drawnow; % Flush callback queue + end + function disable(obj) + %Method to disable motors drive + if ~isempty(obj.motors)&&isobject(obj.motors) && isvalid(obj.motors) + obj.motors.disable(); + end + drawnow; % Flush callback queue + end + function home(obj) + %Method to home/zero the motors + if ~isempty(obj.motors)&&isobject(obj.motors) && isvalid(obj.motors) + obj.motors.home() + end + drawnow; % Flush callback queue + end + function abort(obj,varargin) + %Method to try and stop the motors + if ~isempty(obj.motors)&&isobject(obj.motors) && isvalid(obj.motors) + obj.motors.stop(varargin{:}); + end + drawnow; % Flush callback queue + end + + function val = set_x_motor(obj, channelNo, ~) + % Use generic function with appropriate index to check to make sure that no other motor is using this channel before setting x motor + val = obj.set_motor_generic(channelNo, 1); + end + + function val = set_y_motor(obj, channelNo, ~) + % check to make sure that no other motor is using this channel before setting y motor + val = obj.set_motor_generic(channelNo, 2); + end + + function val = set_z_motor(obj, channelNo, ~) + % check to make sure that no other motor is using this channel before setting z motor + val = obj.set_motor_generic(channelNo, 3); + end + + function val = set_controller(obj, SerialNo, ~) + if ~isempty(SerialNo) + % take val, and instantiate the driver for the BSC203 + val = SerialNo; + obj.motors = Drivers.Kinesis.KinesisBSC203.instance(SerialNo, [0 8], SerialNo, obj.motor_channels, obj.factor); + % Listeners will follow lifecycle of their motor + addlistener(obj.motors,'isMoving','PostSet',@obj.movingCallback); + addlistener(obj.motors,'Homed','PostSet',@obj.homedCallback); + % Intialize values + obj.homedCallback; + obj.movingCallback; + else + if ~isempty(obj.motors) && isobject(obj.motors) && isvalid(obj.motors) && obj.motors.isconnected + obj.motors.disconnect; + delete(obj.motors); + end + val = []; + end + end + end +end + diff --git a/Modules/+Stages/NanoMax300.m b/Modules/+Stages/NanoMax300.m new file mode 100644 index 000000000..4d8e80ce7 --- /dev/null +++ b/Modules/+Stages/NanoMax300.m @@ -0,0 +1,2 @@ +classdef NanoMax300 < Modules.Stage +end \ No newline at end of file