www.gusucode.com > Zaber Device Control Toolbox > +Zaber/AsciiDevice.m

    classdef AsciiDevice < Zaber.Device    
%   ASCIIDEVICE Implements the Zaber.Device interface for the ASCII protocol.
%
%   device = Zaber.ASCIIDEVICE.initialize(protocol, address);
%   protocol - An instance of Zaber.AsciiProtocol.
%   address  - The numeric address of the device on a daisy chain. 
%              Legal values are 1-99.
%   device   - Output: An initialized instance of this class.
%
%   This class represents a single Zaber device on a possible daisy chain
%   of multiple devices. An instance of this class can be used to
%   communicate with the device, and its peripherals if it has any. The
%   generic methods and properties defined by the base class can be used
%   for interaction with basic features. More advanced usage requires
%   use of protocol-specific commands; the request method is useful for
%   that as it will automatically ensure your message is addressed to the
%   device represented by the class instance.
%
%   See also Zaber.Device, Zaber.Protocol.detect, Zaber.AsciiProtocol 

%   Author: Zaber Technologies Software Team <contact@zaber.com>
    

%% Public instance properties
    properties (SetAccess = protected)
        
        % FLAGS Last status flags returned by the device. Values of empty
        % or '--' indicate no warnings and can be ignored. It is best
        % practice to check the flags after completing a move command or
        % when determining the cause of an error. Meanings are documented
        % in the Zaber ASCII Protocol Manual:
        % https://www.zaber.com/wiki/Manuals/ASCII_Protocol_Manual#Warning_Flags 
        Flags
    end
    
    
 %% Public instance methods
    methods        
        function reply = request(obj, aCommand, aData)
        % REQUEST Shortcut for transacting with a device.
        % reply = device.REQUEST(command, data);
        %
        % command - Command to send the device. This is always a
        %           string, but can be empty or have multiple words.
        % data    - Arguments for the command. Can be the empty array,
        %           a number, a string or an array of numbers or
        %           strings, depending on the command.
        % reply   - Response from the device as an AsciiMessage.
        %
        % This method is a shortcut for calling device.Protocol.request()
        % with the difference that this method will automatically attach
        % the device and axis numbers and will update the device's flags
        % from the response each time.
        %
        % This method requires knowledge of the Zaber ASCII protocol. Use
        % it when other methods do not provide the functionality you need.
        %
        % See also AsciiProtocol.request, AsciiMessage
            
            message = Zaber.AsciiMessage(...
                obj.DeviceNo, aCommand, aData, 'AxisNo', obj.AxisNo);
            
            reply = obj.Protocol.request(message);
            
            if (isa(reply, 'Zaber.AsciiMessage'))
                obj.Flags = reply.Flags;
            else
                error('Zaber:AsciiDevice:request:communicationError', ...
                      'Device %d failed to respond to request.', ...
                      obj.DeviceNo);
            end
        end
        
        
        function value = get(obj, aSetting)
        % GET Read a setting from the device.
        % value = device.GET(setting)
        %
        % setting - Name of the setting to read. See the 
        %           Zaber ASCII protocol manual for legal values:
        %           https://www.zaber.com/wiki/Manuals/ASCII_Protocol_Manual#Device_Settings
        % value   - Current value of the setting. This can be a number, an
        %           array of numbers or a string.
        %
        % In the event of a communication error, an error will be thrown.
        % If the device returns an error result, a warning will occur and
        % the method will return the empty array.
        %
        % See also set
        
            value = [];
            reply = obj.request('get', aSetting);
            
            if (isa(reply, 'Zaber.AsciiMessage'))
                if (reply.IsError)
                    warning('Zaber:AsciiDevice:get:readError', ...
                            'Attempt to read setting %s from device %d resulted in error %s.', ...
                            aSetting, obj.DeviceNo, reply.DataString);
                else
                    if (length(reply.Data) > 0)
                        value = reply.Data;
                    else
                        value = reply.DataString;
                    end
                end
            end
        end
            
        
        function result = set(obj, aSetting, aValue)
        % SET Write a value to a device setting.
        % result = device.set(setting, value)
        %
        % setting - Numeric identifier for the setting to write. For legal
        %           values, see the Zaber ASCII protocol manual:
        %           https://www.zaber.com/wiki/Manuals/ASCII_Protocol_Manual#Device_Settings
        % value   - New value of the setting, as a number or a string.
        %           Note that many settings expect integer values and will
        %           produce an error if sent a number with a decimal point.
        %           If passing in a number that is not expected to have a
        %           decimal point, it is recommended that you cast it to
        %           int32 first.
        % result  - True if the write succeeded, or the reply message if
        %           the device returned an error response.
        %
        % Errors will be thrown if there is a communication error. If the
        % setting does not exist, if the setting is read-only, or if the
        % value provided is out of range for the setting then a warning
        % will occur and the device's response message will be returned.
        %
        % See also get
        
            result = false;
            
            reply = obj.request(sprintf('set %s', aSetting), aValue);
            
            if (isa(reply, 'Zaber.AsciiMessage'))
                if (reply.IsError)
                    warning('Zaber:BinaryDevice:set:writeError', ...
                            'Attempt to read setting %s from device %d resulted in error %s.', ...
                            aSetting, obj.DeviceNo, reply.DataString);
                    result = reply.DataString;
                else
                    result = true;
                end
            end
        end
        
                
        function error = waitforidle(obj, aPingInterval)
        % WAITFORIDLE Block until the device stops moving.
        % error = device.WAITFORIDLE();
        % error = device.WAITFORIDLE(interval);
        %
        % interval - Optional; number of seconds to wait between checks
        %            of the device's state. Defaults to 0.1 seconds.
        % error    - Return value, normally empty. If the device
        %            entered an error state while this method was
        %            checking for idleness, this method will return the
        %            error message.
        %
        % This method will ping the device repeatedly until the device
        % either becomes idle or produces an error response.
        %
        % See also home, stop, moveabsolute, moverelative,
        % moveatvelocity, moveindexed
            
            interval = 0.1;
            if (nargin > 1)
                interval = aPingInterval;
            end
            
            moving = true;
            if (~obj.IsAxis)
                moving = false;
            end
            
            while (moving)
                reply = obj.request('', []);
                if (~isa(reply, 'Zaber.AsciiMessage'))
                    moving = false;
                    error = reply;
                elseif (reply.IsError)
                    moving = false;
                    error = reply.Data;
                elseif (reply.IsIdle)
                    moving = false;
                else
                    pause(interval);
                end
            end
        end
        
        
        function range = getrange(obj)
        % GETRANGE Determine the movement limits of the device.
        % range = device.GETRANGE();
        %
        % range - A 1x2 matrix with the first entry being the lower bound
        %         on legal device position and the second entry being the
        %         upper bound. Empty array if the concept doesn't apply.
        %         For a multi-axis controller the result will be an Nx2
        %         array where N is the number of axes. For rotary devices
        %         the result is the range for one full rotation. Note
        %         that the range returned may not reflect physical limits
        %         if the device has been configured to use less than its
        %         full range of travel.
        %
        % Reads device settings to determine the device's current idea of
        % its range of movement. Returns the empty array if the concept
        % does not apply to the device type.
        %
        % See also getposition, getnumindices, Units
            
            devices = obj;
            if (~isempty(obj.Axes))
                devices = obj.Axes;
            elseif (~obj.IsAxis)
                range = [];
                return;
            end
            
            range = zeros(length(devices), 2);
            for (i = 1:length(devices))
                d = devices(i);
                
                if (d.FirmwareVersion >= 6.06)
                    range(i, 1) = d.get('limit.min');
                else
                    range(i, 1) = 0;
                end
                
                if (d.MotionType == Zaber.MotionType.Rotary)
                    if (d.FirmwareVersion >= 6.20)
                        rotationSize = d.get('limit.cycle.dist');
                    else
                        rotationSize = d.get('limit.max');
                    end
                    
                    circle = d.Units.positiontonative(360.0);
                    range(i, 2) = min(circle, rotationSize);
                else
                    range(i, 2) = d.get('limit.max');
                end
            end
        end

        
        function error = home(obj)
        % HOME Move the device to its home position.
        % error = device.HOME();
        %
        % error - Error message from the device, if the command fails. See
        %         the list of error codes in the Zaber Binary protocol
        %         manual:
        %         https://www.zaber.com/wiki/Manuals/Binary_Protocol_Manual#Error_Codes 
        %
        % This command does not block. The device will likely be moving
        % after control returns to the caller. To wait for completion of
        % the move, use waitforidle().
        %
        % See also stop, waitforidle, moveabsolute, moverelative,
        % moveatvelocity, moveindexed
        
            error = [];
            
            reply = obj.request('home', []);
            
            if (isa(reply, 'Zaber.AsciiMessage') && reply.IsError)
                error = reply.DataString;
            end
        end

        
        function error = moveabsolute(obj, aPosition)
        % MOVEABSOLUTE Move the device to an absolute position.
        % error = device.moveabsolute(position);
        % 
        % position - Position to move to, in native device units. If this
        %            device has multiple axes, this must be an array of
        %            positions with the same number of entries as there are
        %            axes. To move an individual axis, use its device entry
        %            from the Axes property.
        % error    - Error message from the device, if the command fails.
        %
        % This command does not block. The device will likely be moving
        % after control returns to the caller. To wait for completion of
        % the move, use waitforidle().
        %
        % See also home, stop, waitforidle, getposition, moverelative,
        % moveatvelocity, moveindexed, Units
        
            error = obj.multiaxiscommand('move abs', int64(aPosition));
        end        
        
        
        function error = moverelative(obj, aDelta)
        % MOVERELATIVE Move the device by a relative amount.
        % error = device.MOVERELATIVE(delta);
        % 
        % delta    - Distance to move by, in native device units.
        % error    - Error information from the device, if any.
        %
        % This method returns immediately upon receiving acknowledgement
        % from the device. Movement will continue after that. Use
        % waitforidle() to block until the move finishes.
        %
        % See also home, stop, waitforidle, getposition, moveabsolute,
        % moveatvelocity, moveindexed, Units
        
            error = obj.multiaxiscommand('move rel', int64(aDelta));
        end
        
        
        function error = moveatvelocity(obj, aVelocity)
        % MOVEATVELOCITY Move the device at a specified velocity.
        % error = device.MOVEATVELOCITY(velocity);
        % 
        % velocity - Speed to move at, in native device units.
        % error    - Error information from the device, if any.
        %
        % This method returns immediately upon receiving acknowledgement
        % from the device. Movement will continue after that, until either
        % a limit is reached or a pre-empting command is send. Use
        % waitforidle() if you want to block until the end of the stage is
        % reached.
        %
        % See also home, stop, waitforidle, getposition, moveabsolute,
        % moverelative, moveindexed, Units

            error = obj.multiaxiscommand('move vel', int64(aVelocity));
        end
        
        
        function error = stop(obj)
        % STOP Stop the device if it is moving.
        % error = device.STOP();
        %
        % error    - Error information from the device(s), if any.
        %
        % If this device is a multi-axis controller, all axes will be
        % stopped. To stop an individual axis, retrieve it from the Axes
        % property and invoke its stop method instead.
        %
        % See also waitforidle, moveabsolute, moverelative, moveatvelocity,
        % moveindexed
        
            error = [];
            
            reply = obj.request('stop', []);

            if (isa(reply, 'Zaber.AsciiMessage') && reply.IsError)
                error = reply.DataString;
            end
        end
        
        
        function pos = getposition(obj)
        % GETPOSITION Get the current device position in native units.
        % pos = device.GETPOSITION();
        %
        % If there is a communication error or the device is not an axis,
        % the empty array will be returned.
        %
        % This method can be called while a device is moving, and will
        % return its position as of the time the message was received.
        %
        % See also stop, waitforidle, moveabsolute, moverelative,
        % moveatvelocity, moveindexed, getnumindices, Units
        
            pos = obj.get('pos');
        end
        
        
        function num = getnumindices(obj)
        % GETNUMINDICES Determine how many indexed positions the device has.
        % num = getvice.GETNUMINDICES();
        %
        % Return value is the maximum position that can be passed to
        % MOVEINDEXED. Returns zero on devices that do not support the
        % MOVEINDEXED command.
        %
        % This method is intended for use with devices such as the X-FWR
        % Filter Wheel holder.
        %
        % See also moveindexed, stop, waitforidle, getposition, range
            
            num = 0;
            
            reply = obj.request('get', 'limit.cycle.dist');

            maxDist = [];
            
            if (isa(reply, 'Zaber.AsciiMessage') && ~reply.IsError)
                maxDist = reply.Data;
            else
                reply = obj.request('get', 'limit.max.dist');

                if (isa(reply, 'Zaber.AsciiMessage') && ~reply.IsError)
                    maxDist = reply.Data;
                end
            end

            if (~isempty(maxDist))
                indexSize = obj.get('motion.index.dist');
                if (~isempty(indexSize))
                    num = int32(floor(maxDist / indexSize));
                end
            end
        end
        
        
        function error = moveindexed(obj, aIndex)
        % MOVEINDEXED Move to an indexed position.
        % error = device.MOVEINDEXED(index);
        %
        % index - The index of the position to move to. Minimum value is 1,
        %         and maximum value is the number returned by
        %         GETNUMINDICES. Will be rejected on devices that don't
        %         support indexed moves.
        % error - Protocol-specific error code if the command is rejected
        %         or generates an error. Empty on success.
        %
        % This command is intended for use with indexed-position devices
        % such as the Filter Wheel. Although the other movement commands
        % will often work with such devices, this method provides an easier
        % way to reach a useful position.
        %
        % This method returns immediately after communicating with the
        % device. The hardware may continue moving for some time afterward;
        % use WAITFORIDLE to wait for the move to complete.
        %
        % See also getnumindices, getposition, stop, waitforidle,
        % moveabsolute, moverelative, moveatvelocity
            error = obj.multiaxiscommand('move index', int64(aIndex));
        end
        
    end
    
    
 %% Public static methods
    methods (Static)
        function instance = initialize(aProtocol, aDeviceNumber, aDeviceId)
        % INITIALIZE Construct a representation for a single device.
        % device = Zaber.AsciiDevice.INITIALIZE(protocol, address, id)
        %
        % protocol - An AsciiProtocol instance.
        % address  - The daisy chain address of a device to represent.
        % id       - The numeric device type ID of the device.
        %            Optional; if not provided the device will be
        %            queried for it.
        % obj      - An initialized AsciiDevice instance.
        %
        % Given the daisy chain address and device type ID for a device
        % that has been found using the given protocol, queries the device
        % for its properties and constructs a new Zaber.Device subclass
        % instance to represent that device.
        %
        % See also AsciiDevice, BinaryDevice.initialize
            
            if (~isa(aProtocol, 'Zaber.AsciiProtocol'))
                error('Zaber:AsciiDevice:initialize:wrongProtocol', ...
                      'Protocol must be ASCII to use this method.');
            end
            
            if (nargin > 2)
                deviceId = aDeviceId;
            else
                reply = aProtocol.request(...
                    Zaber.AsciiMessage(aDeviceNumber, 'get', 'deviceid'));
                
                if (~isempty(reply) && ~reply.IsError)
                    deviceId = reply.Data;
                else
                    error('Zaber:AsciiDevice:initialize:idError', ...
                          'Failed to get the type ID for device %d.', ...
                          aDeviceNumber);
                end
            end
            
            instance = Zaber.AsciiDevice(aProtocol, aDeviceNumber, deviceId);
            
            % Get the firmware version.
            data = instance.get('version');
            if (isnumeric(data))
                instance.FirmwareVersion = data;
            end
            
            % Get database record for device.
            db = Zaber.DeviceDatabase.instance();
            deviceRecord = db.finddevice(instance.DeviceId);
            
            % Identify axes
            numAxes = instance.get('system.axiscount');
            hasPeripherals = false;
            if (numAxes > 0)
                hasPeripherals = true;
                peripheralIds = zeros(numAxes);
                
                for (iAxis = 1:numAxes)
                    reply = aProtocol.request(...
                        Zaber.AsciiMessage(aDeviceNumber, 'get', ...
                            'peripheralid', 'AxisNo', iAxis));
                        
                    if (~isempty(reply) && ~reply.IsError)
                        peripheralIds(iAxis) = reply.Data;
                    else
                        hasPeripherals = false;
                    end
                end
            end
            
            if (hasPeripherals)
                % Controller with peripherals
                instance.Name = db.getdevicename(deviceRecord);
                
                for (iAxis = 1:numAxes)
                    axis = Zaber.AsciiDevice(instance.Protocol, ...
                        instance.DeviceNo, instance.DeviceId);
                    
                    axis.AxisNo = iAxis;
                    axis.IsAxis = true;
                    axis.FirmwareVersion = instance.FirmwareVersion;
                    
                    % get axis peripheral ID
                    axis.PeripheralId = peripheralIds(iAxis);
                    
                    % Get database record for peripheral.
                    periRecord = db.findperipheral(deviceRecord, axis.PeripheralId);
                    
                    axis.Name = db.getdevicename(deviceRecord, periRecord);

                    % Get unit conversion properties from the database.
                    if (~isempty(periRecord))
                        [axis.MotionType, axis.Units] = ...
                            db.determinemotiontype(deviceRecord, periRecord);
                        
                        % Get resolution
                        if (axis.Units.IsScaleResolutionDependent)
                            axis.Units.Resolution = axis.get('resolution');
                        end
                    end
                    
                    instance.Axes = [instance.Axes, axis];
                end
                
            else
                % Integrated device or non-controller device.
                % Use the existence of a movable axis property to tell the
                % difference. This isn't done via the device database
                % so as to let it work for device types newer than the DB.
                dummy = instance.get('maxspeed');
                if (~isempty(dummy))
                    instance.IsAxis = true;
                end                    

                % Get database record for peripheral.
                % For consistency of representation, the device database 
                % always adds a dummy peripheral record for integrated
                % devices. 
                periRecord = db.findperipheral(deviceRecord, instance.PeripheralId);

                % Don't add the peripheral name to the device name.
                instance.Name = db.getdevicename(deviceRecord);

                % Get unit conversion properties from the database.
                if (~isempty(periRecord))
                   
                    [instance.MotionType, instance.Units] = ...
                        db.determinemotiontype(deviceRecord, periRecord);
                    
                    % Get resolution
                    if (instance.Units.IsScaleResolutionDependent)
                        instance.Units.Resolution = instance.get('resolution');
                    end
                end                
            end
            
            instance.IO = Zaber.AsciiIoPort.detect(instance);
            
        end
    end
    
    
 %% Protected instance methods
    methods (Access = protected)
        function obj = AsciiDevice(aProtocol, aDeviceNumber, aDeviceId)
        % ASCIIDEVICE Constructor. Initializes properties to their default values.
        %
        % Note there is no way for code other than a subclass to initialize
        % other device properties after constructing the object this way.
        % Application code should use Zaber.AsciiDevice.initialize instead.

            obj = obj@Zaber.Device(aProtocol, aDeviceNumber, aDeviceId);
        end
        
        
        function error = multiaxiscommand(obj, aCommand, aData)
        % Helper for the various move... methods.
        
            error = [];
            
            if (~isempty(obj.Axes))
                for (i = 1:length(obj.Axes))
                    error = obj.Axes(i).multiaxiscommand(aCommand, aData(i));
                    if (~isempty(error))
                        break;
                    end
                end
            else            
                reply = obj.request(aCommand, aData);

                if (isa(reply, 'Zaber.AsciiMessage') && reply.IsError)
                    error = reply.DataString;
                end
            end
        end
    end
end