www.gusucode.com > Zaber Device Control Toolbox > +Zaber/AsciiMessage.m
classdef AsciiMessage < handle % ASCIIMESSAGE Helper class for Zaber ASCII protocol messages. % This class helps serialize and deserialize string messages % used by the Zaber ASCII protocol into their constituent parts. % % See also AsciiProtocol, Device, AsciiDevice % Author: Zaber Technologies Software Team <contact@zaber.com> %% Public instance properties properties % DEVICENO The address of a device on a serial daisy chain. % For commands this will be the device the command is addressed % to, in the range of 1-99, or 0 to address all devices. % In replies this will be the address of the device responding. DeviceNo % AXISNO The index of the peripheral addressed, if relevant. % If the device addressed by DeviceNo has multiple peripherals % and a message is addressed to or from the peripheral, this % property stores the 1-based index of the peripheral. Zero is % treated as addressing the parent controller. AxisNo % COMMAND The main key word or phrase of the command, without data. % For example, 'move abs'. % % See also Data Command % MESSAGEID Optional message ID for correlating messages. % If message ID mode is enabled on a device, this value will % be echoed back by the device in its replies. Use a negative value % to disable sending a message ID (default). MessageId % DATA Numeric data payload for commands and replies. % An array of numbers. This will contain any numeric % values successfully parsed from the payload section of the % message. Note that some messages have numbers interspersed with % non-numeric values; see the DataString property to retrieve % those. Numeric types will typically be 64-bit integers, but % may be doubles if a parsed value contains decimal places. % % See also DataString Data % DATASTRING Payload component of a message. % This includes all the data associated with a command or response, % without the device address components, device ID, flags or message % checksum. % % See also Data DataString % FLAGS Device warning flags string. % Only applicable to device replies - indicates error or warning % conditions in the device. See % https://www.zaber.com/wiki/Manuals/ASCII_Protocol_Manual#Warning_Flags % for a list of their meanings. Flags % ISERROR True if this message represents a device error state. IsError % ISIDLE True if the device was idle when the reply was sent. IsIdle % MESSAGETYPE Identifies whether this message is a request (a % command for a device), a response (a reply from the device), an % informational message or an alert. % % See also Zaber.MessageType MessageType end %% Public instance methods methods function obj = AsciiMessage(aDeviceNo, aCommand, aData, varargin) % ASCIIMESSAGE Construct an AsciiMessage object from values. % message = Zaber.ASCIIMESSAGE(address, command, data) % message = Zaber.ASCIIMESSAGE(address, command, data, % 'MessageId' = id, 'AxisNo' = index) % % address - The numeric address of a device, or 0 for all. % command - The command to send to the device. % data - Arguments to the command. Can be an array of % numbers if the command only has numeric % arguments, or a string otherwise. Use an empty % array if there are no arguments. % 'MessageId' - Optional message ID. If included, the device % will include the same message ID in its % response. % 'AxisNo' - Optional axis number for peripheral-specific % commands and responses. Defaults to 0. % message - Return value: An initialized instance of the % AsciiMessage class. % % This function constructs an instance of the AsciiMessage % class with its properties initialized. % % Note construction and serialization from the MATLAB end are % intended only for sending requests. Only the deserialize % method can create instances that represent responses, alerts % or info messages. obj.DeviceNo = 0; obj.AxisNo = 0; obj.Command = ''; obj.MessageId = -1; obj.Data = []; obj.DataString = ''; obj.Flags = ''; obj.IsError = false; obj.IsIdle = true; obj.MessageType = Zaber.MessageType.Request; p = inputParser; addParameter(p, 'MessageId', -1); addParameter(p, 'AxisNo', 0); parse(p, varargin{:}); obj.MessageId = p.Results.MessageId; obj.AxisNo = p.Results.AxisNo; if ((aDeviceNo < 0) || (aDeviceNo > 99)) error('Zaber:AsciiMessage:badAddress', ... 'Zaber device addresses must range from 0 to 99.'); end obj.DeviceNo = uint8(aDeviceNo); if (isnumeric(aData) && ~isempty(aData)) % Data is a numeric array - generate string equivalent. obj.Data = aData; dataAsStrings = ... Zaber.AsciiMessage.numberarraytostringarray(aData); obj.DataString = Zaber.AsciiMessage.joinstrings(... dataAsStrings, ' '); elseif (isa(aData, 'char')) % Data is a string or string array. sz = size(aData); if (sz(1) < 2) obj.DataString = aData; else s = aData(1,:); for (i = 2:sz(1)) s = sprintf('%s %s', s, aData(i,:)); end obj.DataString = s; end % Extract numeric data from strings for reference. parts = Zaber.AsciiMessage.splitstrings(obj.DataString); for (i = 1:length(parts)) num = str2double(parts{i}); if (isnumeric(num)) obj.Data = [obj.Data num]; end end end if (~isa(aCommand, 'char')) error('Zaber:AsciiMessage:missingCommand', ... 'Zaber ASCII commands require a command string.'); end obj.Command = aCommand; end function byteArray = serialize(obj, aUseChecksum) % SERIALIZE Convert to an array of bytes suitable for transmission. % byteArray = message.SERIALIZE() % byteArray = message.SERIALIZE(useChecksum) % % useChecksum - Optional. Set to true to include a checksum in % the serialized message. The receiving device % will verify the checksum before honoring the % command. % byteArray - Return value. An array of bytes to send. % % Returns an array of bytes ready to be transmitted to a % Zaber device using the ASCII protocol. % % See also deserialize paddedCmd = obj.Command; if (~isempty(paddedCmd)) paddedCmd = sprintf(' %s', paddedCmd); end if (isnumeric(obj.MessageId) && (obj.MessageId >= 0)) s = sprintf('%d %d %d%s', ... obj.DeviceNo, obj.AxisNo, obj.MessageId, ... paddedCmd); else s = sprintf('%d %d%s', ... obj.DeviceNo, obj.AxisNo, ... paddedCmd); end if (~isempty(obj.DataString)) s = sprintf('%s %s', s, obj.DataString); end if ((nargin > 1) && aUseChecksum) checksum = int32(0); temp = unicode2native(s, 'US-ASCII'); for (i = 1:length(temp)) checksum = checksum + int32(temp(i)); end checksum = bitand(checksum, 255); checksum = bitxor(checksum, 255) + 1; checksum = bitand(checksum, 255); s = sprintf('%s:%02X', s, checksum); end s = sprintf('/%s\r\n', s); byteArray = unicode2native(s, 'US-ASCII'); end end %% Public static methods methods (Static) function obj = deserialize(aBytes) % DESERIALIZE Convert an array of bytes or a string to an AsciiMessage. % message = Zaber.AsciiMessage.DESERIALIZE(line) % % line - A string or array of bytes containing a line of ASCII % text. Leading and trailing whitespace will be removed % automatically. % message - Return value. An AsciiMessage object parsed from % the input line. % % Given a string or an array of bytes representing an ASCII string, % this method will construct a corresponding AsciiMessage class % with the properties filled in accordingly. Use this to convert % reply data from a device into a more convenient form. % % Message IDs and checksums are automatically detected and % checksums verified. % % If the message is not properly formatted, an error will be % thrown. % % See also serialize obj = Zaber.AsciiMessage(0, ' ', ''); obj.Command = ''; s = aBytes; if (isnumeric(s)) sz = size(s); if ((sz(1) == 1) && (sz(2) >= 1)) s = native2unicode(s); end end if (~isa(s, 'char')) error('Zaber:AsciiMessage:deserialize:badType', ... 'AsciiMessage.deserialize() expects a string or byte array.'); end originalString = s; s = strtrim(s); % Consume the message type identifier. obj.MessageType = Zaber.MessageType.Invalid; switch(s(1)) case '!' obj.MessageType = Zaber.MessageType.Alert; case '#' obj.MessageType = Zaber.MessageType.Info; case '@' obj.MessageType = Zaber.MessageType.Response; case '/' obj.MessageType = Zaber.MessageType.Request; otherwise error('Zaber:AsciiMessage:deserialize:parseFailure', ... 'Unrecognized message type: %s', originalString); end s = s(2:end); % Check checksum if present i = strfind(s, ':'); if (length(i) > 1) error('Zaber:AsciiMessage:deserialize:multipleChecksums', ... 'Message contains multiple checksum markers: %s', ... originalString); elseif (length(i) == 1) if (i(1) ~= (length(s) - 2)) error('Zaber:AsciiMessage:deserialize:malformedChecksum', ... 'Malformed checksum in message: %s', originalString); end checksum = hex2dec(s(i(1) + 1 : end)); s = s(1:i - 1); verif = int32(0); checkBytes = unicode2native(s, 'US-ASCII'); for (i = 1:length(checkBytes)) verif = verif + int32(checkBytes(i)); end verif = bitand(verif, 255); verif = bitxor(verif, 255) + 1; verif = bitand(verif, 255); if (verif ~= checksum) error('Zaber:AsciiMessage:deserialize:badChecksum', ... 'Message checksum is incorrect (expected %02X): %s\r\n', ... verif, originalString); end end % Extract address and message ID (if present) tokens = Zaber.AsciiMessage.splitstrings(s); if (length(tokens) > 1) obj.DeviceNo = str2double(tokens{1}); tokens = tokens(2:end); end if (~isnumeric(obj.DeviceNo) || (length(obj.DeviceNo) ~= 1) ... || isnan(obj.DeviceNo) || (obj.DeviceNo < 1) || (obj.DeviceNo > 99)) error('Zaber:AsciiMessage:deserialize:invalidDeviceNo', ... 'Invalid device number in message: %s', originalString); end if (length(tokens) > 1) obj.AxisNo = str2double(tokens{1}); tokens = tokens(2:end); end if (~isnumeric(obj.AxisNo) || (length(obj.AxisNo) ~= 1) || isnan(obj.AxisNo)) error('Zaber:AsciiMessage:deserialize:invalidAxisNo', ... 'Invalid axis number in message: %s', originalString); end if (length(tokens) > 1) possibleId = str2double(tokens{1}); if (isnumeric(possibleId) && (length(possibleId) == 1) && ~isnan(possibleId)) obj.MessageId = possibleId; tokens = tokens(2:end); end end switch (obj.MessageType) case Zaber.MessageType.Response if (length(tokens) < 4) error('Zaber:AsciiMessage:deserialize:messageTruncated', ... 'Not enough content in response: %s', originalString); end obj.IsError = ~strcmp(tokens{1}, 'OK'); obj.IsIdle = strcmp(tokens{2}, 'IDLE'); obj.Flags = tokens{3}; tokens = tokens(4:end); obj.DataString = Zaber.AsciiMessage.joinstrings(tokens, ' '); obj.Data = Zaber.AsciiMessage.findnumbers(tokens); case Zaber.MessageType.Alert if (length(tokens) >= 2) obj.IsIdle = strcmp(tokens{1}, 'IDLE'); obj.Flags = tokens{2}; tokens = tokens(3:end); end obj.DataString = Zaber.AsciiMessage.joinstrings(tokens, ' '); obj.Data = Zaber.AsciiMessage.findnumbers(tokens); case Zaber.MessageType.Request if (length(tokens) >= 1) obj.Command = tokens{1}; obj.DataString = Zaber.AsciiMessage.joinstrings(tokens(2:end), ' '); obj.Data = Zaber.AsciiMessage.findnumbers(tokens(2:end)); end case Zaber.MessageType.Info obj.DataString = Zaber.AsciiMessage.joinstrings(tokens, ' '); obj.Data = Zaber.AsciiMessage.findnumbers(tokens); otherwise error('Zaber:AsciiMessage:deserialize:invalidType', ... 'Invalid message type detected.'); end obj.DataString = Zaber.AsciiMessage.defaultstring(obj.DataString, ''); end end %% Private static methods methods (Static, Access = private) function numArray = findnumbers(aStringArray) % Extracts all numeric values from an array of strings. % NOTE this currently doesn't attempt to differentiate between % potential int64s and doubles, as MATLAB doesn't support mixed- % type numeric arrays without going to cell arrays. nums = str2double(aStringArray); mask = arrayfun(... @(x) isnumeric(x) && ~isnan(x) && (length(x) == 1), nums); numArray = nums(mask)'; end function result = joinstrings(aStringArray, aDelimiter) % Version-safe method to join an array of strings into one, % with a space delimiter between. persistent JoinFunc; if isempty(JoinFunc) if (verLessThan('matlab', '9.1')) JoinFunc = @strjoin; else % join is recommended for R2016b and later. JoinFunc = @join; end end if (isempty(aStringArray)) result = ''; else result = JoinFunc(aStringArray, aDelimiter); end % In some versions of MATLAB the join function returns the % string in a cell array instead of as a string. Unbox it. while (iscell(result)) result = result{1}; end end function result = splitstrings(aString) % Version-safe function to split a string into a string array % by whitespace. persistent SplitFunc; if isempty(SplitFunc) if (verLessThan('matlab', '9.1')) % split doesn't work on strings before R2016b, and % strsplit returns a column vector instead of a row. SplitFunc = @(s) strsplit(s)'; else % split is recommended for R2016b and later. SplitFunc = @split; end end result = SplitFunc(aString); end function result = defaultstring(aString, aDefault) % Version-safe function to replace a missing string with % a default, in order to ensure a string is always present. persistent FixStringFunc; if isempty(FixStringFunc) if (verLessThan('matlab', '9.1')) % split doesn't work on strings before R2016b, and % strsplit returns a column vector instead of a row. FixStringFunc = @Zaber.AsciiMessage.defaultstringold; else % split is recommended for R2016b and later. FixStringFunc = @Zaber.AsciiMessage.defaultstringnew; end end % Unbox the string so the type checks will work. if isa(aString, 'cell') aString = aString{:}; end result = FixStringFunc(aString, aDefault); end function result = defaultstringold(aString, aDefault) % ismissing doesn't exist in older matlab versions. if (isempty(aString) || (~isa(aString, 'string') && ~isa(aString, 'char'))) result = aDefault; else result = aString; end end function result = defaultstringnew(aString, aDefault) if (ismissing(aString)) result = aDefault; else result = aString; end end function result = numberarraytostringarray(aNumbers) % Helper to convert arrays of numbers to arrays of strings in a % firmware-compatible way. result = cell(1, length(aNumbers)); for i = 1:length(aNumbers) result(i) = ... { Zaber.AsciiMessage.numbertostring(aNumbers(i)) }; end end function result = numbertostring(aNumber) % Helper like num2str to convert numeric values to strings in % a firmware-compatible way. Integer values are converted without % decimal places. Float types are converted without using % scientific notation, and with the minimal number of decimal % places needed (ie no trailing zeroes). if (isinteger(aNumber)) result = sprintf('%d', aNumber); else temp = sprintf('%f', aNumber); % Strip trailing zeroes. temp = regexprep(temp, '(\.\d+?)0+$', '$1'); % If result has only a zero after the decimal, strip that. result = regexprep(temp, '\.0$', ''); end end end end