classdef Equipment < handle
    %EQUIPMENT Summary of this class goes here
    %   Detailed explanation goes here
    
    properties (SetAccess=private)
        name
        tcp
        channel
        locked
    end
    properties (Dependent, SetAccess=private)
        error
    end
    
    methods
        function ecq = Equipment(ipAddress,port,channel)
            %EQUIPMENT Construct an instance of this class.
            %   This functions opens the required TCP connection
            %   for this device. Channel is GPIB-channel on prologix
            %   converter if used, otherwise set channel to -1. 
            %   Name is queried from device via '*IDN?' command.
            ecq.tcp = Equipment.getTCP(ipAddress,port);
            ecq.locked = false;
            ecq.channel = channel;
            ecq.name = ecq.idn();
            if channel >= 0
                ecq.setPrologix;
            end
        end
        
        function id = idn(ecq)
            %IDN Queries identificantion from device via '*IDN?'.
            id = ecq.query_unsafe('*idn?');
        end
        
        function clear(ecq)
            %CLEAR Sends clear command to device via '*CLS'.
            ecq.write_unsafe('*cls');
        end
        
        function opc(ecq)
            %OPC executes 'operation complete query' to device.
            %Function holds untill device returns '1'. Must be 
            %used to avoid interrupting busy device.
            ecq.query_unsafe('*OPC?');
        end
        
        function unlock(ecq)
            %UNLOCK Sets the device back to 'local control'
            %This function only supports devices via the Prologix.
            if ecq.channel >= 0
                fprintf(ecq.tcp,'++loc');
                ecq.locked = false;
            end
        end
        
        function lock(ecq)
            %UNLOCK Sets the device back to 'remote control'
            %This function only supports devices via the Prologix.            
            if ecq.channel >= 0
                fprintf(ecq.tcp,'++llo');
                ecq.locked = true;
                else
                warning('Device does not support locking')
            end
        end
        
        function beep(ecq)
            %BEEP Sends the beep command to the device
            ecq.write('SYST:BEEP')
        end
        
        function write(ecq,message)
            %WRITE Sends a command to the channel.
            %The function will first check if the device is ready to 
            %recieve a command via the OPC function. Then send the command 
            %and eventually check if the device has thrown an error via 
            %the ERROR function.
            %
            %See also QUERY
            ecq.opc;
            ecq.write_unsafe(message);
            ecq.error;
        end
        
        function output = read(ecq)
            %READ Extracts recieved data from the TCP-buffer.
            %This function sends the '++read'-command to the Prologix if
            %needed. Will always read the TCP-buffer in matlab.
            if ecq.channel >= 0
                ecq.write_unsafe('++read');
            end
            output = fscanf(ecq.tcp);
            fprintf(['<<  ',output]);
        end
        
        function output = query(ecq,message)
            %QUERY Sends command to the device and read the respond.
            %The command is send via the WRITE and the respond is collected
            %from the device.
            %
            %See also WRITE, READ
            ecq.opc;
            output = ecq.query_unsafe(message);
            ecq.error;
        end
        
        function errorlist = get.error(ecq)
            %ERROR Checks for errors on device. If any, throws a warning.
            %The function will pull all errors from the device error stack.
            %This is up to a maximum of 20 errors. If errors have occured
            %the function will throw a warning with all the error messages.
            for i = 1:20
                output = ecq.query_unsafe('SYSTem:ERRor?'); %Query error message from device.
                errorlist(i,1:(length(output))) = output; %Store the error message in the errorlist array.
                %GPIB protocol states that the error code '0' means no
                %error. The for loop will break if the last recieved error
                %code is zero.
                if str2double(regexp(output,'\d*','match','once'))==0 %Check if last recieved error code is '0'
                    % If the 'no error' message is the only error then no
                    % warning will be trown. However if there is more than
                    % one error in the list. The function gives a warning
                    % with all error messages.
                    if i>1 %check for more than one error message.
                        warning('Error from device: %s %s',ecq.name,reshape(errorlist(1:i-1,:)',1,[]));
                    end
                    ecq.clear;
                    break;
                end
            end
        end
    end
    methods (Access = protected)
        
        function write_unsafe(ecq,message)
            %WRITE_UNSAFE Sends command to device.
            %This function does not check if device is ready or check if an
            %error occured after writing. If possible one should always use
            %the WRITE function.
            %
            %See also WRITE, QUERY
            if ecq.channel >= 0
                fprintf(ecq.tcp,['++addr ', num2str(ecq.channel)]);
                fprintf(['>>  ++addr ', num2str(ecq.channel),'\n']);
            end
            fprintf(ecq.tcp, message);
            fprintf(['>>  ',message,'\n']);
        end
        
        function output = query_unsafe(ecq,message)
            %QUERY_UNSAFE Sends command to device and reads device output.
            %This function does not check if device is ready or check if an
            %error occured after writing. If possible one should always use
            %the QUERY function.
            %
            %See also QUERY, WRITE, READ
            ecq.write_unsafe(message);
            output = read(ecq);
        end
    end
    methods (Access = protected, Hidden)
        
        function setPrologix(ecq)
            fprintf(ecq.tcp, '++mode 1');     %set device in controller mode
            fprintf(ecq.tcp, '++auto 0');     %disable automatic datapull. this avoids errors on equipment.
        end
            
        function delete(ecq)
            %DELETE Destructs the current object.
            ecq.unlock;
            Equipment.getTCP(ecq.tcp.RemoteHost,-ecq.tcp.RemotePort);
        end
        
    end
    
    methods (Static)
        function tcpobject = getTCP(ipAddress,port)
            %GETTCP Returns TCP-object for TCP-connection.
            %Function makes a TCP-connection for specific ipAddress and
            %port. If TCP-connection already exist no new connection is
            %made and only existing handle is returned. 
            %For existing connections the function will store the amount of
            %handles in use. Via the DELETE function the connection will be
            %closed if the connection is not in use anymore.
            %The connection will be removed if the port is negative number.
            
            persistent tcpconnection; %make variable persistent to share tcp-handles across multiple function calls.
            [ipname,ipAddress] = Equipment.ip2structname(ipAddress,abs(port)); %Get a structname and a cleaned ipaddress.
            if port > 0 %if port number is positive a connection is made.
                if isempty(tcpconnection)
                    %if the tcpconnection is empty (first time function
                    %call) make a struct in the variable.
                    tcpconnection = struct; 
                end

                if ~isfield(tcpconnection, ipname)                         %check if the handle is already made before.
                    tcpconnection.(ipname).tcp = tcpip(ipAddress,port);    %Make TCP-connection
                    tcpconnection.(ipname).nopen = 1;                      %Set number of connections in use to 1
                    fopen(tcpconnection.(ipname).tcp);                     %Open the TCP-connection
                else %If connection already exist. Increase number of connections in use by 1.
                    tcpconnection.(ipname).nopen = tcpconnection.(ipname).nopen + 1;
                end
                tcpobject = tcpconnection.(ipname).tcp; %return the TCP-connection handle.
            elseif port < 0 %If the portnumber is negative the connection is removed.
                if tcpconnection.(ipname).nopen > 1 
                    %If more than one object uses this tcp-handle. Decrease
                    %the number of connections in use by 1.
                    tcpconnection.(ipname).nopen = tcpconnection.(ipname).nopen - 1; %Decrease the number by 1.
                else
                    %If only one handle uses this connection. The
                    %connection is closed and the field is removed from the
                    %tcpconnection struct.
                    fclose(tcpconnection.(ipname).tcp);
                    tcpconnection = rmfield(tcpconnection,ipname);
                end
            else
                error([num2str(port),' is not a valid port number try a value between 1 and 65535.']);
            end
        end
        
        function bool = isnum(input)
            %ISNUM Tests if the input is numeric scalar
            bool = isnumeric(input)&&isscalar(input);
        end
		
		function num = forceNum(input)
            %FORCENUM Throws an error if the input is not numeric.
			if ~Equipment.isnum(input)
				error('Input should be a (single) number.');
			end
			num = input;
		end
        
        function iptest(ipAddress)
            %IPTEST Checks if the given IPv4-address is valid.
            validip = regexp(ipAddress,'^(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9]))$', 'once');
            if isempty(validip)
                error('Invalid IP-address');
            end
        end
        
        function [structname,cleanip] = ip2structname(ipAddress,port)
            %IP2STRUCTNAME Returns a structname and ip w/out leading zeros.
            %structname to store stuff in struct (especially for GETTCP). 
            %cleanip is a shortened ip without leading zeros.
            cleanip = regexprep(ipAddress,'(?:(?<=\.)|^)(?:0+)(?=\d)','');
            Equipment.iptest(cleanip);
            structname = matlab.lang.makeValidName(['ip',cleanip,'_',num2str(port)]);
        end
    end
end