Realterm RS232 comms - serial()-like version

by

 

19 Jun 2013 (Updated )

Use Realterm the way you can using the MATLAB "serial" commands and instrument control toolbox.

(CaseInsensitiveProperties, TruncatedProperties) rt
classdef (CaseInsensitiveProperties, TruncatedProperties) rt < hgsetget
    
    % creates rt as a subclass of hgsetget (in turn a subclass of handle) -
    % this is essential, because we require the ability to delete the
    % object in a managed way, i.e. delete the activeX server before
    % cleaning up.  This also gives correct use cf serial because then we
    % get passing by reference.  hgsetget allows the use of set() and get()
    % notation, while setting CaseInsensitiveProperties allows the use of
    % e.g. get(rt, 'BAUDRATE'), cf serial().
    
    % see:
    % http://undocumentedmatlab.com/blog/undocumented-classdef-attributes/
    
    % this is a partial implementation of serial, using Realterm and
    % ActiveX to establish communication.
    
    % Due to the use of hgsetget, this is only compatible with 2008a (7.6)
    % onwards.
    % http://www.cs.ubc.ca/~murphyk/Software/matlabTutorial/html/objectOrientedOldStyle.html
    
    
    %% public properties which need to be retrieved when requested
    properties (Dependent)
        
        % these properties are dynamically set and get, so need to have
        % this property set so that it is evaluated at runtime
        BaudRate;
        BytesAvailable;
        DataBits;
        FlowControl;
        Parity;
        Port;
        RecordStatus;
        Status;
        StopBits;
        ValuesReceived;
        ValuesSent;
    end
    
    %% public properties which don't get set at runtime
    properties (GetAccess = public, SetAccess = public)
        
        % these properties are set to be available to everything - the
        % below listed values are the defaults
        InputBufferSize = 512;
        Name = '';
        Terminator = 'CR';
        TimeOut = 1;
        UserData;
    end

    %% hidden properties for this class - temporarily allowed for development
    properties %(Hidden, GetAccess = private, SetAccess = private)
        
        % properties which are hidden won't be set on bootup, but are kept
        % available for debugging and development - these can only be
        % overriden by within class methods
        rtHandle;
        
        % this could be set to be a dependent property, retrieved every
        % time using the CaptureFile field in the rtHandle, but the problem
        % is that in MATLAB the same file can be opened in several
        % different ways with different permissions - so there could be
        % multiple valid fileID's, but we only want the one with the
        % specific permissions we set in the beginning - we could check
        % these as well, but there can be multiple instances of this too
        RecordID;
    end
    
    %% hidden dependent properties for this class
    properties (Hidden, Dependent)
        
        % the capture filename
        RecordName;
        
        % whether it matches up properly with MATLAB's fileID or not
        captureFileStatus;
        
        % returns the terminators
        readTerminator;
        writeTerminator;
    end
    
    %% constructor and destructor - called on instantiation
    methods
        
        % define the constructor
        function object = rt(comPort, varargin)
            
            % required to allow the use of rt as a subclass of hgsetget
            % (not required for handle), which is required to allow the use
            % of set() and get() cf serial objects
            object = object@hgsetget;
            
            % if this errors during creation, then the destructor gets
            % called
            
            % issues SERIAL-like error if no comPort specified
            if ~nargin
                
                % complain
                error('The PORT must be specified.')
            end
            
            % checks the property-value arguments for syntax - this is just
            % a superficial check
            if nargin >= 2 && ~object.isPV(varargin{:})
                
                % complain
                error('Invalid property-value syntax.')
            end
            
            % now check that the property names are valid - properties
            % which we don't want overridden have already been marked as
            % hidden
            if nargin >= 2 && any(~ismember(varargin(1:2:end), properties(object)))
                
                % then complain
                error('Invalid properties specified.')
            end
            
            % create the realterm instance via ActiveX - this will error if
            % it is not installed
            object.rtHandle = actxserver('realterm.realtermintf');
            
            % sets some default settings - these will get overridden later
            % if user property-value settings are provided
            object.rtHandle.Caption = 'MATLAB RealTerm Server';
            object.rtHandle.WindowState = 'wsMinimized';
            object.rtHandle.CaptureDirect = 1;
            object.rtHandle.Visible = 0;
            object.Port = comPort;
            object.Name = ['Realterm-', comPort];
            
            % for each property value...
            for m = 1:2:numel(varargin)
                
                % apply - this will error if a specific property/value is
                % invalid
                object.(varargin{m}) = varargin{m + 1};
            end
        end
        
        % and the destructor
        function delete(object)
            
            % this is the destructor part - what happens when you try and
            % delete it - it should not error on the way through (but is
            % tolerated by MATLAB)
            
            % tries
            try
                
                % if this is connected, then we need to close the connection
                % first (as with serial() behaviour)
                if strcmp(object.Status, 'open')
                    
                    % then disconnect first
                    fclose(object)
                end
                
            catch
                
                % issue a warning - turned off for now
                %warning('Did not disconnect from device.')
            end
            
            % tries
            try
                
                % close the ActiveX connection - this will error if it is
                % already closed - no other way to establish if the server
                % is live or not prior to closing it
                object.rtHandle.Close
                
            catch
                
                % this might leave an open instance of Realterm which might
                % cause issues later
                %warning('Did not properly close Realterm instance.')
            end
            
            % delete the actxserver object - not too worried about this
            % failing
            try
                
                % deletes - if this has already been deleted then it will
                % error
                delete(object.rtHandle);
                
            catch
               
                % do nothing (mimics behaviour of serial objects)
            end
            
            % don't need to delete the rt object itself, as this is taken
            % care of by MATLAB
        end
    end
    
    %% hidden methods for this class only, that require the object in the input arguments
    methods (Hidden, Access = private)
        
        function setRT(object, propertyName, newValue, errorMessage)
            
            % helper function for setting properties because of no erroring
            % if the value was not set
            object.rtHandle.(propertyName) = newValue;
            
            % the new value wasn't set...
            if ~isequal(newValue, object.rtHandle.(propertyName))
                
                % if the data is something that can be readily displayed...
                if ischar(newValue)
                    
                    % set the format
                    errorFormat = 'to %s';
                    
                elseif isnumeric(newValue)
                    
                    % need to convert
                    errorFormat = 'to %d';
                    
                else
                    
                    % no conversion
                    errorFormat = '';
                end
                
                % different messages...
                if nargin >= 4
                    
                    % display the supplier message
                    error(errorMessage)
                    
                elseif ~isempty(errorFormat)
                                    
                    % issue an error
                    error(['Unable to set %s to ', errorFormat, '.'], propertyName, newValue)
                    
                else
                    
                    % simpler
                    error('Unable to set %s.')
                end
            end            
        end
        
        function readCheck(object)
            
            % will error if the object is not correctly set up for reading
            
            % need to check if its connected first
            if ~strcmp(object.Status, 'open')
                
                % error
                error('Unsuccessful read: OBJ must be connected to the hardware with FOPEN.')
            end
            
            % additional checks for capture being on
            if ~strcmp(object.RecordStatus, 'on')
                
                % error
                error('Unsuccessful read: Realterm is not capturing data to file.')
            end
            
            % and for the files matching up
            if ~object.captureFileStatus
                
                % error
                error('Unsuccessful read: MATLAB is not correctly associated with the capture file in Realterm.')
            end
        end
        
        function writeCheck(object)
           
            % will error if the object is not correctly set up for writing            
            
            % need to check if its connected
            if ~strcmp(object.Status, 'open')
                
                % error
                error('Unsuccessful write: OBJ must be connected to the hardware with FOPEN.')
            end
        end
        
    end
    
    %% hidden static methods for this class only - helpers for the constructors and destructors
    methods (Hidden, Static, Access = private)
        
        function comPorts = getavailablecom
            
            % GETAVAILABLECOM fetches the available com ports on the system
            % comPorts = getavailablecom returns comPorts as a cell array
            % of available COM port names on this system.  NOTE it does not
            % consider those COM ports which have been opened by other
            % programs, after the MATLAB session has been started.
            
            % if the instrument tool box is there...
            if ~isempty(ver('instrument'))
                
                % gets the serial info
                info = instrhwinfo('serial');
                
                % gets the available ports
                comPorts = info.AvailableSerialPorts;
                
            else
                
                % if that didn't work, do it an awkward cowboy way
                
                % fetches the standard error message featuring the list of available COM
                % ports
                try
                    
                    % generates an impossible serial object
                    s = serial(num2str(rand));
                    
                    % attempts to connect to object - forcing an error
                    fopen(s)
                    
                catch ME
                    
                    % extracts out the message part of the last error - it contains a list
                    % of COM ports in a long string
                    serialError = ME.message;
                end
                
                % cleans up dummy serial object
                delete(s)
                
                % finds the indices of each mention of the string 'COM' (the index actually
                % refers to the 'C')
                comStartPosition = strfind(serialError, 'COM');
                
                % finds the index of the first mention of the string 'Use' (3 is subtracted
                % from this to get the index of last digit of the COM name e.g. 'COM9')
                comEndPosition = strfind(serialError, 'Use') - 3;
                
                % extracts out the relevent part of the string
                comString = serialError(comStartPosition:comEndPosition);
                
                % finds the indices of the all of the commas in comString - this marks the
                % END of the COM[X] part of the string
                commaPositions = strfind(comString, ',');
                
                % different cases need different evaluations
                if isempty(comStartPosition)
                    
                    % if no COM ports are available, doesn't return anything
                    comPorts{1} = '';
                    
                elseif isempty(commaPositions)
                    
                    % if only one COM port is available, don't need to parse string
                    comPorts{1} = comString;
                    
                else
                    
                    % extracts first COM port
                    comPorts{1} = comString(1:commaPositions(1) - 1);
                    
                    % finds the number of comma positions
                    numberCommaPositions = numel(commaPositions);
                    
                    % extracts the rest
                    for m = 2:numberCommaPositions
                        
                        % extracts the COM[X] string by finding the next COM AFTER the
                        % previous commaPosition, moving forward 2 to ignore the comma
                        % and the space, and capturing it up to the next comma, then
                        % subtracting 1 to remove the comma part
                        comPorts{m, 1} = comString(commaPositions(m - 1) + 2:commaPositions(m) - 1);
                    end
                    
                    % extracts last COM port (doesn't have a comma at the end)
                    comPorts{numberCommaPositions + 1, 1} = comString(commaPositions(numel(commaPositions)) + 2:end);
                end
            end
        end
        
        function response = isPV(varargin)
            % ISPV checks if all the arguments supplied conform to valid property-value
            % syntax.
            
            % shortcut for if there are no arguments
            if ~nargin
                % not valid
                response = false;
                
            else
                % defines the indices
                indices = 1:nargin;
                
                % checks that there is an even number of arguments, and that the odd
                % arguments (the field names) are all strings
                response = ~rem(nargin, 2) && iscellstr(varargin(indices(isOdd(indices))));
            end
            
            function response = isOdd(input)
                
                % in as a subfunction because for some reason it doesn't
                % seem to like being a separate one
                
                % says if something is odd or not - taken from Peter Acklams
                % work:
                % http://home.online.no/~pjacklam/matlab/software/util/matutil/isodd.m
                response = mod(input, 2) == 1;
            end
        end
        
        function response = parseTerminator(input)
            
            % takes the input and parses it into a format that Realterm can
            % deal with
            % decides what to do
            switch input
                
                case 'CR'
                    
                    % simple
                    response = char(13);
                    
                case 'LF'
                    
                    % simple again
                    response = char(10);
                    
                case 'CR/LF'
                    
                    % straight forward
                    response = char([13, 10]);
                    
                case 'LF/CR'
                    
                    % simple
                    response = char([10, 13]);
                    
                otherwise
                    
                    % if its a number - kept to double for consistency with
                    % serial()
                    if isa(input, 'double')
                        
                        % modify it
                        response = char(input);
                        
                    elseif ischar(input)
                        
                        % leave it as it is
                        response = input;
                        
                    else
                        
                        % errors
                        error('Unknown terminator.')
                    end
            end
        end
    end
    
    %% set and get methods for properties - these do not have access to private or protected functions
    methods
        
        function response = get.Status(object)
            
            % checks if its connected or not
            switch object.rtHandle.PortOpen
                
                case 1
                    
                    % its connected
                    response = 'open';
                    
                case 0
                    
                    % its not
                    response = 'closed';
                    
                otherwise
                    
                    % error
                    error('Unknown connection status.')
            end
            
            % an additional check might involve checking that capturing was
            % on, but this is not entirely necessary, e.g. for devices that
            % don't send data back
        end
                
        function set.Status(~, ~)
            
            % errors
            error('Cannot change the ''Status'' property of realterm objects - use fopen and fclose instead.')
        end
        
        function response = get.BaudRate(object)
            
            % fetches the baud rate as set
            response = object.rtHandle.baud;
        end
        
        function set.BaudRate(object, value)
            
            % sets the new baud rate
            setRT(object, 'baud', value)
        end
        
        function response = get.DataBits(object)
            
            % fetches the data bits
            response = object.rtHandle.DataBits;
        end
        
        function set.DataBits(object, value)
            
            % set the data bits
            setRT(object, 'DataBits', value)
        end
        
        function response = get.StopBits(object)
            
            % fetches
            response = object.rtHandle.StopBits;
        end
        
        function set.StopBits(object, value)
            
            % set the stop bits
            setRT(object, 'StopBits', value)
        end
        
        function response = get.Parity(object)
            
            % fetches the parity - this is all uppercase so need to convert
            response = lower(object.rtHandle.Parity);
        end
        
        function set.Parity(object, value)
            
            % sets the parity - this is all uppercase so need to convert
            setRT(object, 'Parity', upper(value));
        end
        
        function response = get.FlowControl(object)
            
            % fetches
            flowControlMode = object.rtHandle.FlowControl;
            
            % depends on what its set to
            switch flowControlMode
                
                case 0
                    
                    % none
                    response = 'none';
                    
                case 1
                    
                    % hardware
                    response = 'hardware';
                    
                otherwise
                    
                    % shouldn't fall through, but included for completeness
                    error('Unknown flow control.')
            end
        end
        
        function set.FlowControl(object, value)
            
            % deal with inputs in the same way serial() does
            if ~ischar(value)
                
                % complain
                error('Parameter must be a non-empty string.')
            end
            
            % serial() has some unhelpful error messages for invalid string
            % inputs - not implemented
            %error('There is no enumerated value named ''%s''.', value)
            
            % defines what to set
            switch value
                
                case 'none'
                    
                    % turn it off
                    newValue = 0;
                    
                case 'hardware'
                    
                    % set it to 1
                    newValue = 1;
                    
                otherwise
                    
                    % complain
                    error('Invalid flow control.')
            end
            
            % fetches
            setRT(object, 'FlowControl', newValue);
        end
        
        function response = get.Port(object)
            
            % gets the port setting
            response = ['COM', object.rtHandle.Port];
        end
        
        function set.Port(object, value)
            
            % only allows if not connected
            if ~strcmp(object.Status, 'closed')
                
                % error
                error('Must be disconnected to change port.')
            end
            
            % try to set the port - this gets properly error-checked
            % because Realterm allows some rather silly values of COMx
            % (e.g. 1000)
            
            % check the comPort is valid - it must be a 'COMx' string,
            % where x is 1 to 255.
            if ischar(value) && any(size(value, 2) == 4:6) && strcmp(value(1:3), 'COM') && all(isstrprop(value(4:end), 'digit'));
                
                % then it fits the pattern - pull out the comPortNumber
                comPortNumber = str2double(value(4:end));
                
                % is this OK?
                if comPortNumber < 1 || comPortNumber > 255 || comPortNumber ~= round(comPortNumber)
                    
                    % complain
                    error('COM port number must be an integer from 1 to 255.')
                end
                
            else
                
                % its not a valid com port
                error('COM port string must be of the format ''COMx'' where x is a number from 1 to 255.')
            end
            
            % now check that the port is available in MATLAB
            if all(~strcmp(value, getavailablecom))
                
                % its not a valid COM port
                error('COM port is not available in MATLAB.')
            end
            
            % set the port
            setRT(object, 'Port', value(4:end));
        end
        
        function response = get.BytesAvailable(object)
            
            % if its not connected, return 0 by default
            if strcmp(object.Status, 'closed')
                
                % return false
                response = 0;
                
            else
                
                % evaluate from file position
                
                % get the current position of the file (with respect to the
                % beginning of the file)
                filePosition = ftell(object.RecordID);
                
                % seek to the end
                fread(object.RecordID);
                
                % gets the new position
                endPosition = ftell(object.RecordID);
                
                % seek back to the original position
                fseek(object.RecordID, filePosition, 'bof');
                
                % calculates the bytes left
                response = endPosition - filePosition;
            end
        end
              
        function set.BytesAvailable(~, ~)
            
            % error
            error('Cannot set ''BytesAvailable'' for realterm objects.')
        end
        
        function response = get.RecordName(object)
            
            % fetches
            response = object.rtHandle.CaptureFile;
        end
        
        function set.RecordName(object, value)
            
            % sets it
            setRT(object, 'CaptureFile', value)
        end
        
        function response = get.RecordStatus(object)
            
            % need to determine that Realterm is capturing
            switch object.rtHandle.Capture
                
                % its on
                case 'cmOn'
                    
                    % its true
                    response = 'on';
                    
                case 'cmOff'
                    
                    % its not
                    response = 'off';
                
                otherwise
                    
                    % error
                    error('Unknown capture status.')
            end            
        end
        
        function set.RecordStatus(object, value)
            
            % value must be on or off
            if ~ischar(value) || (~strcmpi(value, 'on') && ~strcmpi(value, 'off'))
                
                % error
                error('RecordStatus must be a string of ''on'' or ''off''.')
                
            else
                
                % define the error message
                errorMessage = 'Unable to change Realterm capture status.';
                
                % for case-insensitivity
                switch lower(value)
                    
                    case 'on'
                        
                        % if its not connected, we can't allow this change
                        if ~strcmp(object.Status, 'Open')
                            
                            % error
                            error('Cannot change RecordStatus while disconnected.')
                        end
                        
                        % turn it on
                        setRT(object, 'Capture', 'cmOn', errorMessage)
                        
                    case 'off'
                        
                        % turn it off
                        setRT(object, 'Capture', 'cmOff', errorMessage)
                        
                        % issue a warning (with an ID which can be switched
                        % off
                        warning('rt:RecordStatus:off:cannotReceiveData', 'Since the capture mode is off, data cannot be received by Realterm.')
                end
            end            
        end
        
        function response = get.captureFileStatus(object)
            
            % returns true if and only if the capture file in realterm is
            % open in matlab with the correct fileID
            response = ~isempty(object.RecordID) && strcmp(fopen(object.RecordID), object.RecordName);
        end
        
        function set.captureFileStatus(~, ~)
            
            % error
            error('Cannot set ''captureFileStatus'' for realterm objects.')
        end
        
        function set.Terminator(object, value)
            
            % if its a cell...
            if iscell(value)
                
                % it must be a valid size
                if any(size(value) ~= [1, 2])
                    
                    % then error
                    error('The Terminator cell array must be a 1-by-2 array. The first element is the read terminator and the second element is the write terminator.')
            
                else
                    
                    % recursively check
                    cellfun(@(x) isValidTerminator(x), value)
                end
                
            else
                
                % check the terminator in the subfunction
                isValidTerminator(value)
            end
            
            % if it got this far its fine
            object.Terminator = value;
            
            % subfunction for checking the terminator            
            function isValidTerminator(input)
            
                % define the error message
                errorMessage = 'Terminator must be an integer between 0 and 127, the ASCII equivalent, ''CR/LF'' or ''LF/CR''.';
                
                % slightly different if a char
                if ischar(input)
                    
                    % depends - this will also deal properly with arrays etc
                    switch input
                        
                        case {'CR', 'LF', 'CR/LF', 'LF/CF'}
                            
                            % these are OK
                            
                        case num2cell(char(0:127))
                            
                            % numbers valid too
                            
                        otherwise
                            
                            % its not
                            error(errorMessage)
                    end
                    
                % must be a scalar of 0-127
                elseif isa(input, 'double') && (~isscalar(input) || ~ismember(input, 0:127))
                    
                    % its not
                    error(errorMessage)                    
                end
            end
        end
        
        function response = get.readTerminator(object)
            
            % if its a cell array we only want the first one
            if iscell(object.Terminator)
                
                % then get that
                response = object.parseTerminator(object.Terminator{1});
                
            else
                
                % pass on directly
                response = object.parseTerminator(object.Terminator);
            end
        end
        
        function set.readTerminator(~, ~)
            
            % can't set this
            error('Cannot set readTerminator for Realterm objects directly - set using ''Terminator'' instead.')
        end
        
        function response = get.writeTerminator(object)
            
            % if its a cell array we only want the second one
            if iscell(object.Terminator)
                
                % then get that
                response = object.parseTerminator(object.Terminator{2});
                
            else
                
                % pass on directly
                response = object.parseTerminator(object.Terminator);
            end
        end
        
        function set.writeTerminator(~, ~)
            
            % can't set this
            error('Cannot set writeTerminator for Realterm objects directly - set using ''Terminator'' instead.')
        end
        
        function set.InputBufferSize(object, value)
            
            % establishes reasonable values for the buffer size
            
            % must be scalar
            if ~isscalar(value)
                
                % complain
                error('Parameter must be scalar.')
            end
            
            % must be real
            if ~isreal(value)
                
                % error
                error('Parameter must be real.')
            end
            
            % can't have NaN's
            if isnan(value)
                
                % errors
                error('Parameter must be a number.')
            end
            
            % must be numeric or a logical
            if ~isnumeric(value) && ~islogical(value)
                
                % don't allow it
                error('Array must be numeric or logical.')
            end
            
            % check for sizing
            if value < 1
                
                % complain
                error('InputBufferSize must be greater than or equal to 1.')
            end
            
            % set it, converting to double
            object.InputBufferSize = double(value);            
        end
        
        function set.TimeOut(object, value)
           
            % sets the timeout - we want to only allow scalar numeric
            % numbers that are greater than 0, but are real and not NaN's
            if ~isnumeric(value) && ~islogical(value)
                
                % error
                error('Array must be numeric or logical.')
            end
            
            % scalar
            if ~isscalar(value)
                
                % error
                error('Parameter must be scalar.')
            end
            
            % real
            if ~isreal(value)
                
                % error
                error('Parameter must be real.')
            end
            
            % NaN
            if isnan(value)
                
                % error
                error('Timeout cannot be set to NaN. Timeout must be greater than or equal to 0.')
            end
            
            % 0 or larger
            if value < 0
                
                % error
                error('Timeout must be greater than or equal to 0.')
            end
            
            % set it if it got this far
            object.TimeOut = value;
        end
        
        function response = get.ValuesSent(object)
            
            % fetches the values from Realterm
            response = object.rtHandle.CharCount;
        end
        
        function set.ValuesSent(~, ~)
            
            % errors
            error('Changing the ''ValuesSent'' property of Realterm serial port objects is not allowed.')
        end
        
        function response = get.ValuesReceived(object)
            
            % if its closed, return 0
            if strcmp(object.Status, 'closed')
                
                % return
                response = 0;
                
            % its open, but capturing is not enabled
            elseif ~strcmp(object.RecordStatus, 'on')
                
                % invalid, so return NaN
                response = NaN;
                
            else
            
                % finds out how many bytes are in the capture file very simply
                dirInfo = dir(object.RecordName);
                
                % returns the bytes field
                response = dirInfo.bytes;
            end
        end
        
        function set.ValuesReceived(~, ~)
            
            % errors
            error('Changing the ''ValuesReceived'' property of Realterm serial port objects is not allowed.')
        end
    end
    
    %% overloaded functions for reading and writing data via Realterm
    methods (Access = public)
        
        function disp(object)
            
            % defines the indent
            shortIndent = repmat(' ', 1, 3);
            
            % need to do some preparsing for the terminator or it'll come
            % out wrong
            if iscell(object.Terminator)
                
                % then convert it to a prettier looking string
                parsedTerminator = ['{''', object.Terminator{1}, ''',''', object.Terminator{2}, '''}'];
                
            else
                
                % just use it as is, adding quote marks either side
                parsedTerminator = ['''', object.Terminator, ''''];
            end
            
            % print out the information we want, line by line
            fprintf('%sRealterm Serial Port Object : %s\n', shortIndent, object.Name)
            fprintf('\n')
            fprintf('%sCommunication Settings\n', shortIndent)
            printLineSimple(object, 'Port')
            printLineSimple(object, 'BaudRate')
            printLine('Terminator', parsedTerminator)
            fprintf('\n')
            fprintf('%sCommunication State\n', shortIndent)
            printLineSimple(object, 'Status')
            printLineSimple(object, 'RecordStatus')
            fprintf('\n')
            fprintf('%sRead\\Write State\n', shortIndent)
            printLineSimple(object, 'BytesAvailable')
            printLineSimple(object, 'ValuesReceived')
            printLineSimple(object, 'ValuesSent')
            fprintf('\n')
            
            function printLineSimple(object, fieldName)
               
                % passes it on
                printLine(fieldName, object.(fieldName))
            end
            
            function printLine(fieldName, fieldValue)
                
                % defines the widest field
                longestField = numel('BytesAvailable');
                
                % define the indentations
                longIndent = repmat(' ', 1, 6);

                % defines the padding between the end of the field
                fieldPadding = 6;
                
                % prints out a line given a suitable set of values
                if ischar(fieldValue)
                    
                    % conversion is for a string
                    valueFormatSpec = '%s';
                    
                elseif isnumeric(fieldValue)
                    
                    % it'll be a number
                    valueFormatSpec = '%d';
                end
                
                % calculates the padding - generated this way because I
                % couldn't get the '% *s' working the way I wanted it to
                padding = longestField + fieldPadding - numel(fieldName) - 1;
                
                % print out the line
                fprintf(['%s%s:%s', valueFormatSpec, '\n'], longIndent, fieldName, repmat(' ', 1, padding), fieldValue)
            end
        end
        
        function fopen(object)
            
            % connect to the device
            
            % if already connected, complain
            if strcmp(object.Status, 'open')
                
                % error cf serial
                error('Open failed: OBJECT has already been opened.')
            end
            
            % tries to connect
            setRT(object, 'PortOpen', 1, 'Unable to connect.')
            
            % generates a meaningful unique filename in the same directory
            % as this function
            object.rtHandle.CaptureFile = [fileparts(mfilename('fullpath')), filesep, 'temp', char(java.util.UUID.randomUUID), '.dat'];
            
            % start capture - this produces an annoying modal dialog in
            % Realterm which MATLAB will hang on until it is cleared, if
            % the Port isn't already open
            object.rtHandle.StartCapture
            
            % try-catched for tidiness
            try
                
                % open the file in MATLAB (read-only access)
                object.RecordID = fopen(object.RecordName);
                
                % if it didn't open...
                if object.RecordID < 3
                    
                    % error
                    error('Error opening capture file in MATLAB.')
                end
                
            catch ME
                
                % try to clean up
                try
                    
                    % close
                    fclose(object)
                    
                catch
                    
                    % do nothing - just clean up as much as possible
                end
                
                % rethrow the original error
                rethrow(ME)
            end
        end
        
        function fclose(object)
            
            % disconnects the object
            
            % does this only if connected, otherwise do nothing
            if strcmp(object.Status, 'open')
                
                % tries to stop the capture
                try
                    
                    % tries to stop the capture - no way to monitor the
                    % outcome of this
                    object.rtHandle.StopCapture
                    
                    % tries to close the port
                    setRT(object, 'PortOpen', 0, 'Unable to disconnect from the serial object.')
                    
                    % clears the capture file name
                    setRT(object, 'CaptureFile', '')
                    
                    % defines the capture file name before we close and
                    % delete it (deliberately different to normal - we want
                    % to clean up as best as possible at the MATLAB end
                    RecordName = fopen(object.RecordID);                    
                    
                    % closes the capture file in MATLAB
                    closeOutcome = fclose(object.RecordID);
                    
                    % checks it closed
                    if closeOutcome == -1
                        
                        % complain
                        warning('Unable to close Capture File in MATLAB - %s not deleted.')
                        
                    end
                    
                    % if the file exists, tidy up
                    if exist(RecordName, 'file')
                        
                        % deletes the capture file
                        delete(RecordName)
                    end
                    
                catch ME
                    
                    % displays a warning
                    warning('Could not correctly stop the data capture.')
                    
                    % rethrow the error
                    rethrow(ME)
                end
            end
        end
        
        function [output, byteCount, warningMessage] = fread(object, varargin)
            
            % idea is to let the fread function do the heavy lifting with
            % respect to error checking of number of bytes and the
            % precision.
            
            % varargin{1} is the number of bytes to read
            % varargin{2} is the precision (not the same as the format
            % associated with fscanf/fgetl)
            
            % check its ready for reading data - this will error with an
            % appropriate message if its not
            readCheck(object)
            
            % need to check the precision first, because that will affect
            % the default number of bytes to read if none were specified
            
            % same for the precision
            if nargin >= 3
                
                % use the supplied
                precision = varargin{2};
                
            else
                
                % define a default - ideally you'd just use the defaults
                % from fread and allow fread to handle it, but this is a
                % limitation due to the differences between serial/fread
                % and the file-reading fread
                precision = 'uchar';
            end
            
            % define the words per byte
            switch precision
                
                case {'uchar', 'schar', 'char', 'int8', 'uint8'}
                    
                    % its 1 bytes per word (8 bits)
                    bytesPerWord = 1;
                    
                case {'int16', 'uint16', 'ushort'}
                    
                    % two bytes are a word (16 bits)
                    bytesPerWord = 2;
                    
                case {'int32', 'uint32', 'int', 'uint', 'single', 'float32', 'float', 'long', 'ulong'}
                    
                    % four bytes per word (32 bits) - note that 'long' and
                    % 'ulong' can be 64-bit integers on other platforms,
                    % but since Realterm is a Windows application, this
                    % isn't an issue here
                    bytesPerWord = 4;
                    
                case {'double', 'float64'}
                    
                    % 8 bytes (64 bits)
                    bytesPerWord = 8;
                    
                otherwise
                    
                    % invalid precision
                    error('Invalid precision.')
            end
            
            % define a default words to read if none was specified
            if nargin >= 2
                
                % must be a valid format - can be either a scalar or an m
                % x n array
                if ndims(varargin{1}) > 2
                    
                    % complain
                    error('Invalid SIZE specified.')
                end
                
                % find how many to read - we'll reshape this matrix at
                % the end if necessary
                wordsToRead = prod(varargin{1});
                
            else
                
                % define as the input buffer size divided by the bytes per
                % word, rounded down
                wordsToRead = floor(object.InputBufferSize / bytesPerWord);
            end
            
            % define the number of bytes to read
            bytesToRead = wordsToRead * bytesPerWord;
            
            % error if this is larger than the buffer size
            if bytesToRead > object.InputBufferSize
                
                % complain
                error('Unsuccessful read: SIZE * PRECISION must be less than or equal to InputBufferSize.')
            end
            
            % specify a start time (in seconds)
            startTime = now * 24 * 60 * 60;
            
            % defines an output using zeros with this size, so that the
            % vertcat behaviour works properly
            output = zeros(0, 1);

            % loops round to read out up the number of characters, or the
            % timeout - whichever is reached first - this can be several
            % thousand loops within a few seconds, but this at least allows
            % for well-behaved interuptibility
            while numel(output) < bytesToRead && (now * 24 * 60 * 60 - startTime) < object.TimeOut
                
                % tries to read it out - the concatenation behaviour may be
                % a bit odd if [m, n] arguments are used for the bytes to
                % read
                output = vertcat(output, fread(object.RecordID, bytesToRead - numel(output), varargin{2:end}));
                
                % a short pause to allow other threads to work:
                % http://undocumentedmatlab.com/blog/waiting-for-asynchronous-events/
                pause(0.01)
            end

            % defines the bytes collected
            byteCount = numel(output);
            
            % defines the warningMessage
            warningMessage = 'The specified amount of data was not returned within the Timeout period.';
            
            % if at the end it was a timeout and the max characters was not reached...
            if ((now * 24 * 60 * 60) - startTime) > object.TimeOut && isfinite(bytesToRead) && byteCount < bytesToRead
                
                % display a warning
                warning(warningMessage)
                
            % if the size was specified, and it has two dimension, we need
            % to reshape the matrix (we already errored to exclude the
            % possibility of higher-dimensional arrays
            elseif nargin >= 2 && ~isscalar(varargin{1})
                
                % then we need to reshape the matrix to the desired shape -
                % done in column order
                output = reshape(output, varargin{1});
            end
        end
        
        function [output, byteCount, warningMessage] = fscanf(object, varargin)
            
            % fscanf is coded using the fread file-reading operatings
            
            % define the precision for fread - this shouldn't need to be
            % different - so this is 1 byte per word, which simplifies the
            % code compared to fread
            precision = 'uchar';
            
            % check the input arguments
            if nargin >= 2
                
                % then check it - this is the best we can do!
                if ~ischar(varargin{1})
                    
                    % complain
                    error('formatSpec must be a valid string - see sprintf documentation.')
                end
                
                % define
                formatSpec = varargin{1};
                
            else
                
                % define a default
                formatSpec = '%s';
            end
            
            % if the maximum characters wasn't supplied, set it to the
            % buffer size
            if nargin >= 3
                
                % must be a valid format - can be either a scalar or an m
                % x n array
                if ndims(varargin{2}) > 2
                    
                    % complain
                    error('Invalid SIZE specified.')
                end
                
                % if it was zero also not acceptable
                if isscalar(varargin{2}) && ~varargin{2}
                    
                    % complain
                    error('SIZE must be greater than 0.')
                end
                
                % find how many to read - we'll reshape this matrix at
                % the end if necessary
                maxBytes = prod(varargin{2});
                
                % error if this is larger than the buffer size
                if maxBytes > object.InputBufferSize
                    
                    % complain - the first message is as displayed by
                    % serial/fscanf, but the second is more accurate, since
                    % the precision is fixed for fscanf.
                    %error('Unsuccessful read: SIZE * PRECISION must be less than or equal to InputBufferSize.')
                    error('Unsuccessful read: SIZE must be less than or equal to InputBufferSize.')
                end
                
            else
                
                % set it to the buffer size
                maxBytes = object.InputBufferSize;
            end
            
            % specify a start time (in seconds)
            startTime = now * 24 * 60 * 60;
            
            % if no terminator is defined, then just call fread
            if isempty(object.readTerminator)
                
                % call it
                [output, byteCount, warningMessage] = fread(object, maxBytes, precision);
                
                % transposes the output from column vector to row vector
                % format for consistency with fscanf
                output = output';

            else
                
                % initialises output
                output = '';
                
                % can loop ca. 500-1000 times for a couple of bytes - if
                % the overhead becomes significant it may be worth while
                % slowing this down a little, but if it is done this way,
                % then other functions can interrupt and not be an issue
                
                % keeps on going until it matches or hits the end of the
                % file, or the timeout is breached (doesn't use for loop
                % since you can't do 1:Inf as a counter)
                while numel(output) < maxBytes
                    
                    % defines the logical responses - this code also works
                    % for multiple terminators, but this won't happen -
                    % this will fail for empty terminators which is why
                    % this possibility was dealt with earlier
                    reachedTerminator = numel(output) >= numel(object.readTerminator) && all(output(end - numel(object.readTerminator) + 1:end) == object.readTerminator);
                    
                    % and whther its timed out or not
                    timedOut = (now * 24 * 60 * 60 - startTime) > object.TimeOut;
                    
                    % if any of those are reached, then leave the loop
                    if reachedTerminator || timedOut
                        
                        % leave
                        break
                        
                    else
                        
                        % read another byte out, appending it on to the row
                        % vector - unfortunately this does not appear to be
                        % readily pre-allocatable.  When fread is already
                        % at the end of the capture file, any attempts to
                        % read return empty responses, and would also not
                        % contribute to the number of characters that we
                        % want to read out
                        output = [output, fread(object.RecordID, 1, precision)];
                        
                        % a short pause to allow other threads to work:
                        % http://undocumentedmatlab.com/blog/waiting-for-asynchronous-events/
                        pause(0.01)
                    end
                end
                
                % defines the other output arguments
                byteCount = numel(output);
                
                % and the warning message
                if timedOut
                    
                    % define
                    warningMessage = 'A timeout occurred before the Terminator was reached.';
                    
                else
                    
                    % its blank
                    warningMessage = '';
                end
                
                % apply the format
                output = sscanf(output, formatSpec);
                
                % if the size was specified, and it has two dimension, we
                % need to reshape the matrix (we already errored to exclude
                % the possibility of higher-dimensional arrays
                if nargin >= 3 && ~isscalar(varargin{2})
                    
                    % then we need to reshape the matrix to the desired shape -
                    % done in column order
                    output = reshape(output, varargin{2});
                end
            end
        end
        
        function [output, byteCount, warningMessage] = fgets(object)
            
            % currently just a wrapper for fscanf, but without the options
            % to apply different formatting
            
            % reads out a line
            [output, byteCount, warningMessage] = fscanf(object);
        end
        
        function [output, byteCount, warningMessage] = fgetl(object)
            
            % uses fgets, then removes the terminators
            
            % tries to read out a line - note that the byteCount includes
            % terminators, so does not need to be altered
            [output, byteCount, warningMessage] = fgets(object);
            
            % define the range where the terminator might be (because
            % fscanf might have timed out, and not reached a terminator) -
            % this is coded for multiple terminators should this ever be
            % required
            terminatorRange = (numel(output) - numel(object.readTerminator) + 1):numel(output);
            
            % if the end of the line matches the terminator, then remove
            % those
            if strcmp(output(terminatorRange), object.readTerminator)
                
                % then remove those parts (coded for multiple terminators)
                output = output(1:end - numel(object.readTerminator));
            end
        end
        
        function fwrite(object, data, varargin)
            
            % note that 'mode' is ignored because it has no meaning here,
            % but is allowed through for the sake of compatibility
            
            % check the object is OK to write stuff
            writeCheck(object)
            
            % checks the data is acceptable - note that this will
            % deliberately not allow logicals through
            if ~ischar(data) && ~isnumeric(data)
                
                % then its not
                error('The input argument A must be numeric or a string.')
            end
            
            % check for sizing - can be empty, or a vector (includes
            % scalars in that definition) - standard error message is
            % unhelpful ('Unscussessful write: An error occurred during
            % writing.') so has been changed.
            if ~isempty(data) && ~isvector(data)
                
                % error
                error('Data must be a valid numeric or string empty matrix, scalar, or vector - arrays are not supported.')
            end
            
            % converts the input if it needs to be
            if isnumeric(data)
                
                % converts it
                data = char(data);
            end
            
            % depending on the size of the input, and if it contains any 0s
            % - UPDATE - this has been deprecated because realterm appears
            % to be modifying the last byte in the string if it is larger
            % than 129 (seen for 132 but OK for 129) - unknown why! - for
            % security, done for larger than 127 (suspect its an issue with
            % post-ASCII codes)
            if numel(data) == 1 || any(~data) || any(double(data) > 127)
                
                % sends it one character at a time
                for m = 1:numel(data)
                    
                    % sends it
                    invoke(object.rtHandle, 'PutChar', data(m))
                end
                
            else
                
                % send it as a string
                invoke(object.rtHandle, 'PutString', data)
            end
        end
        
        function fprintf(object, data, varargin)
            
            % this is essentially a wrapper for fread, but to allow arrays
            % through we have to adjust the approach slightly
            
            % if its an array, allow it through, but send the terminator
            % through as a separate thing at the end (it's done this way
            % internally anyway)
            if isempty(data) || isvector(data)
                
                % append on the write terminator - coded for multiple
                % terminators, although not implemented for consistency
                % with serial()
                data(end + 1:end + numel(object.writeTerminator)) = object.writeTerminator;
                
                % send the command
                fwrite(object, data, varargin)
                
            else
                
                % send it through byte by byte (fread does not work like
                % this)
                for m = 1:numel(data)
                    
                    % send
                    fwrite(object, data(m), varargin)
                end
                
                % and send through the terminator at the end
                fwrite(object, object.writeTerminator, varargin)
            end
        end
        
        function flushinput(object)
            
            % flushes the buffer - we don't care about what it returns, or
            % the precision
            fread(object, object.BytesAvailable);
        end
    end
end

Contact us