from udsoncan import Response, Request, services, DidCodec, Routine, IOMasks, Dtc, DataIdentifier, MemoryLocation
from udsoncan.exceptions import *
from udsoncan.configs import default_client_config
import struct
import logging
import math
import binascii
import traceback
import functools
import time

class Client:
    """
    __init__(self, conn, config=default_client_config, request_timeout = None)

    Object that interacts with a UDS server. 
    It builds a service request, sends it to the server, receives and parses its response, detects communication anomalies and logs what it is doing for further debugging.

    :param conn: The underlying protocol interface.
    :type conn: :ref:`Connection<Connection>`

    :param config: The :ref:`client configuration<client_config>`
    :type config: dict

    :param request_timeout: Maximum amount of time to wait for a response. This parameter exists for backward compatibility only. For detailed timeout handling, see :ref:`Client configuration<config_timeouts>`
    :type request_timeout: int
    """

    class SuppressPositiveResponse:
        def __init__(self):
            self.enabled = False

        def __enter__(self):
            self.enabled = True
            return self

        def __exit__(self, type, value, traceback):
            self.enabled = False

    class PayloadOverrider:
        def __init__(self):
            self.modifier = None
            self.enabled = False

        def __enter__(self):
            self.enabled = True
            return self

        def __exit__(self, type, value, traceback):
            self.modifier = None
            self.enabled = False

        def __call__(self, modifier):
            self.modifier = modifier
            return self

        def get_overrided_payload(self, original_payload):
            if callable(self.modifier):
                return self.modifier(original_payload)
            else:
                return self.modifier


    def __init__(self, conn, config=default_client_config, request_timeout=None):
        self.conn = conn
        self.conn.uds_client = self
        self.config = dict(config) # Makes a copy of given configuration

        #For backward compatibility
        if request_timeout is not None:
            self.config['request_timeout'] = request_timeout
        self.suppress_positive_response = Client.SuppressPositiveResponse()
        self.payload_override = Client.PayloadOverrider()
        self.last_response = None

        self.session_timing = dict(p2_server_max=None, p2_star_server_max=None)

        self.refresh_config()

    def __enter__(self):
        self.open()
        return self

    def __exit__(self, type, value, traceback):
        self.close()

    def open(self):
        if not self.conn.is_open():
            self.conn.open()

    def close(self):
        self.conn.close()

    def configure_logger(self):
        logger_name = 'UdsClient'
        if 'logger_name' in self.config:
            logger_name = "UdsClient[%s]" % self.config['logger_name']

        self.logger = logging.getLogger(logger_name)

    def set_config(self, key, value):
        self.config[key] = value
        self.refresh_config()

    def set_configs(self, dic):
        self.config.update(dic)
        self.refresh_config()

    def refresh_config(self):
        self.configure_logger()
        for k in default_client_config:
            if k not in self.config:
                self.config[k] = default_client_config[k]
        self.validate_config()

    def validate_config(self):
        if self.config['standard_version'] not in [2006,2013,2020]:
            raise ConfigError('Valid standard versions are 2006, 2013, 2020. %s is not supported' % self.config['standard_version'])




    # Decorator to apply on functions that the user will call.
    # Each function raises exceptions. This decorator handles these exceptions, logs them, 
    # then suppresses them or not depending on the client configuration.
    # if func1 and func2 are decorated and func2 calls func1, it should be done this way : self.func1._func_no_error_management(self, ...)

    def standard_error_management(func):
        @functools.wraps(func)
        def decorated(self, *args, **kwargs):
            try:
                return func(self, *args, **kwargs)

            except NegativeResponseException as e:
                e.positive = False
                if self.config['exception_on_negative_response']:
                    logline = '[%s] : %s' % (e.__class__.__name__, str(e))
                    self.logger.warning(logline)
                    raise
                else:
                    self.logger.warning(str(e))
                    return e.response

            except InvalidResponseException as e:
                e.response.valid = False
                if self.config['exception_on_invalid_response']:
                    self.logger.error('[%s] : %s' % (e.__class__.__name__, str(e)))
                    raise
                else:
                    self.logger.error(str(e))
                    return e.response

            except UnexpectedResponseException as e:
                e.response.unexpected = True
                if self.config['exception_on_unexpected_response']:
                    self.logger.error('[%s] : %s' % (e.__class__.__name__, str(e)))
                    raise
                else:
                    self.logger.error(str(e))
                    return e.response

            except Exception as e:
                self.logger.error('[%s] : %s' % (e.__class__.__name__, str(e)))
                raise

        decorated._func_no_error_management = func
        return decorated

    def service_log_prefix(self, service):
        return "%s<0x%02x>" % (service.get_name(), service.request_id())

    @standard_error_management
    def change_session(self, newsession):
        """ 
        Requests the server to change the diagnostic session with a :ref:`DiagnosticSessionControl<DiagnosticSessionControl>` service request

        :Effective configuration: ``exception_on_<type>_response``

        :param newsession: The session to try to switch. Values from :class:`DiagnosticSessionControl.Session <udsoncan.services.DiagnosticSessionControl.Session>` can be used.
        :type newsession: int 

        :return: The server response parsed by :meth:`DiagnosticSessionControl.interpret_response<udsoncan.services.DiagnosticSessionControl.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        req = services.DiagnosticSessionControl.make_request(newsession)

        named_newsession = '%s (0x%02x)' % (services.DiagnosticSessionControl.Session.get_name(newsession), newsession)
        self.logger.info('%s - Switching session to %s' % (self.service_log_prefix(services.DiagnosticSessionControl), named_newsession))

        response = self.send_request(req)
        if response is None:
            return 

        services.DiagnosticSessionControl.interpret_response(response, standard_version = self.config['standard_version'])

        if newsession != response.service_data.session_echo:
            raise UnexpectedResponseException(response, "Response subfunction received from server (0x%02x) does not match the requested subfunction (0x%02x)" % (response.service_data.session_echo, newsession))

        if self.config['standard_version'] > 2006:
            if self.config['use_server_timing']:
                self.logger.info('%s - Received new timing parameters. P2=%.3fs and P2*=%.3fs.  Using these value from now on.' % (self.service_log_prefix(services.DiagnosticSessionControl), response.service_data.p2_server_max, response.service_data.p2_star_server_max))
                self.session_timing['p2_server_max'] = response.service_data.p2_server_max
                self.session_timing['p2_star_server_max'] = response.service_data.p2_star_server_max

        return response

    @standard_error_management
    def request_seed(self, level, data=bytes()):
        """ 
        Requests a seed to unlock a security level with the :ref:`SecurityAccess<SecurityAccess>` service 

        :Effective configuration: ``exception_on_<type>_response``

        :param level: The security level to unlock. If value is even, it will be converted to the corresponding odd value
        :type level: int 

        :param data: The data to send to the server
        :type data: bytes 

        :return: The server response parsed by :meth:`SecurityAccess.interpret_response<udsoncan.services.SecurityAccess.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """		
        req = services.SecurityAccess.make_request(level, mode=services.SecurityAccess.Mode.RequestSeed, data=data)

        self.logger.info('%s - Requesting seed to unlock security access level 0x%02x' % (self.service_log_prefix(services.SecurityAccess), req.subfunction))	# level may be corrected by service.

        response = self.send_request(req)
        if response is None:
            return

        services.SecurityAccess.interpret_response(response, mode=services.SecurityAccess.Mode.RequestSeed)

        expected_level = services.SecurityAccess.normalize_level(mode=services.SecurityAccess.Mode.RequestSeed, level=level)
        received_level = response.service_data.security_level_echo
        if expected_level != received_level:
            raise UnexpectedResponseException(response, "Response subfunction received from server (0x%02x) does not match the requested subfunction (0x%02x)" % (received_level, expected_level))

        self.logger.debug('Received seed [%s]' % (binascii.hexlify(response.service_data.seed)))
        return response

    # Performs a SecurityAccess service request. Send key
    @standard_error_management
    def send_key(self, level, key):
        """ 
        Sends a key to unlock a security level with the :ref:`SecurityAccess<SecurityAccess>` service 

        :Effective configuration: ``exception_on_<type>_response``

        :param level: The security level to unlock. If value is odd, it will be converted to the corresponding even value
        :type level: int 

        :param key: The key to send to the server
        :type key: bytes 

        :return: The server response parsed by :meth:`SecurityAccess.interpret_response<udsoncan.services.SecurityAccess.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """			
        req = services.SecurityAccess.make_request(level, mode=services.SecurityAccess.Mode.SendKey, data=key)
        self.logger.info('%s - Sending key to unlock security access level 0x%02x' % (self.service_log_prefix(services.SecurityAccess), req.subfunction))
        self.logger.debug('\tKey to send [%s]' % (binascii.hexlify(key)))

        response = self.send_request(req)
        if response is None:
            return

        services.SecurityAccess.interpret_response(response, mode=services.SecurityAccess.Mode.SendKey)

        expected_level = services.SecurityAccess.normalize_level(mode=services.SecurityAccess.Mode.SendKey, level=level)
        received_level = response.service_data.security_level_echo
        if expected_level != received_level:
            raise UnexpectedResponseException(response, "Response subfunction received from server (0x%02x) does not match the requested subfunction (0x%02x)" % (received_level, expected_level))

        return response

    @standard_error_management
    def unlock_security_access(self, level):
        """
        Successively calls request_seed and send_key to unlock a security level with the :ref:`SecurityAccess<SecurityAccess>` service.
        The key computation is done by calling config['security_algo']

        :Effective configuration: ``exception_on_<type>_response`` ``security_algo`` ``security_algo_params``

        :param level: The level to unlock. Can be the odd or even variant of it.
        :type level: int

        :return: The server response parsed by :meth:`SecurityAccess.interpret_response<udsoncan.services.SecurityAccess.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """

        if 'security_algo' not in self.config or not callable(self.config['security_algo']):
            raise NotImplementedError("Client configuration does not provide a security algorithm")

        response = self.request_seed._func_no_error_management(self, level)
        seed = response.service_data.seed
        if len(seed) > 0 and seed == b'\x00' * len(seed):
            self.logger.info('%s - Security access level 0x%02x is already unlocked, no key will be sent.' % (self.service_log_prefix(services.SecurityAccess), level))
            return response

        params = self.config['security_algo_params'] if 'security_algo_params' in self.config else None

        # Starting from V1.12, level is now passed to the algorithm.
        # We now use named parameters for backward compatibility
        algo_params = {}
        try:
            algo_args =  self.config['security_algo'].__code__.co_varnames[:self.config['security_algo'].__code__.co_argcount]
            
            if 'seed' in algo_args:
                algo_params['seed'] = seed
            if 'level' in algo_args:
                algo_params['level'] = level
            if 'params' in algo_args:
                algo_params['params'] = params
        except:
            algo_params = {'seed':seed, 'params' : params, 'level' : level}

        key = self.config['security_algo'].__call__(**algo_params)
        return self.send_key._func_no_error_management(self, level, key)


    @standard_error_management
    def tester_present(self):
        """
        Sends a TesterPresent request to keep the session active.

        :Effective configuration: ``exception_on_<type>_response``

        :return: The server response parsed by :meth:`TesterPresent.interpret_response<udsoncan.services.TesterPresent.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        req = services.TesterPresent.make_request()

        self.logger.info('%s - Sending TesterPresent request' % (self.service_log_prefix(services.TesterPresent)))
        response = self.send_request(req)
        if response is None:
            return

        services.TesterPresent.interpret_response(response)

        if req.subfunction != response.service_data.subfunction_echo:
            raise UnexpectedResponseException(response, "Response subfunction received from server (0x%02x) does not match the requested subfunction (0x%02x)" % (response.service_data.subfunction_echo, req.subfunction))

        return response

    @standard_error_management
    def read_data_by_identifier_first(self, didlist):
        """
        Shortcut to extract a single DID. 
        Calls read_data_by_identifier then returns the first DID asked for. 

        :Effective configuration: ``exception_on_<type>_response`` ``data_identifiers`` ``tolerate_zero_padding``

        :param didlist: The list of DID to be read
        :type didlist: list[int]

        :return: The server response parsed by :meth:`ReadDataByIdentifier.interpret_response<udsoncan.services.ReadDataByIdentifier.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        didlist = services.ReadDataByIdentifier.validate_didlist_input(didlist)
        response = self.read_data_by_identifier(didlist)
        values = response.service_data.values
        if len(values) > 0 and len(didlist) > 0:
            return values[didlist[0]]

    @standard_error_management
    def read_data_by_identifier(self, didlist):
        """
        Requests a value associated with a data identifier (DID) through the :ref:`ReadDataByIdentifier<ReadDataByIdentifier>` service.

        :Effective configuration: ``exception_on_<type>_response`` ``data_identifiers`` ``tolerate_zero_padding``

        See :ref:`an example<reading_a_did>` about how to read a DID

        :param didlist: The list of DID to be read
        :type didlist: list[int]

        :return: The server response parsed by :meth:`ReadDataByIdentifier.interpret_response<udsoncan.services.ReadDataByIdentifier.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        didlist = services.ReadDataByIdentifier.validate_didlist_input(didlist)
        req = services.ReadDataByIdentifier.make_request(didlist=didlist, didconfig=self.config['data_identifiers'])

        if len(didlist) == 1:
            self.logger.info("%s - Reading data identifier : 0x%04x (%s)" % (self.service_log_prefix(services.ReadDataByIdentifier), didlist[0], DataIdentifier.name_from_id(didlist[0])))
        else:
            self.logger.info("%s - Reading %d data identifier : %s" % (self.service_log_prefix(services.ReadDataByIdentifier), len(didlist), list(map(hex,didlist))))

        if 'data_identifiers' not in self.config or  not isinstance(self.config['data_identifiers'], dict):
            raise AttributeError('Configuration does not contains a valid data identifier description.')

        response = self.send_request(req)
        if response is None:
            return

        params = {
                'didlist' : didlist, 
                'didconfig' : self.config['data_identifiers'],
                'tolerate_zero_padding' : self.config['tolerate_zero_padding']
                }

        try:
            services.ReadDataByIdentifier.interpret_response(response, **params)
        except ConfigError as e:
            if e.key in didlist:
                raise
            else:
                raise UnexpectedResponseException(response, "Server returned values for data identifier 0x%04x that was not requested and no Codec was defined for it. Parsing must be stopped." % (e.key))

        set_request_didlist = set(didlist)
        set_response_didlist = set(response.service_data.values.keys())
        extra_did  = set_response_didlist - set_request_didlist
        missing_did  = set_request_didlist - set_response_didlist

        if len(extra_did) > 0:
            raise UnexpectedResponseException(response, "Server returned values for %d data identifier that were not requested. Dids are : %s" % (len(extra_did), extra_did))

        if len(missing_did) > 0:
            raise UnexpectedResponseException(response, "%d data identifier values are missing from server response. Dids are : %s" % (len(missing_did), missing_did))

        return response


    # Performs a WriteDataByIdentifier request.
    @standard_error_management
    def write_data_by_identifier(self, did, value):
        """
        Requests to write a value associated with a data identifier (DID) through the :ref:`WriteDataByIdentifier<WriteDataByIdentifier>` service.

        :Effective configuration:  ``exception_on_<type>_response`` ``data_identifiers``

        :param did: The DID to write its value
        :type did: int

        :param value: Value given to the :ref:`DidCodec <DidCodec>`.encode method. The payload returned by the codec will be sent to the server.
        :type value: int

        :return: The server response parsed by :meth:`WriteDataByIdentifier.interpret_response<udsoncan.services.WriteDataByIdentifier.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """
        req = services.WriteDataByIdentifier.make_request(did, value, didconfig=self.config['data_identifiers'])
        self.logger.info("%s - Writing data identifier 0x%04x (%s)" % (self.service_log_prefix(services.WriteDataByIdentifier), did, DataIdentifier.name_from_id(did)))

        response = self.send_request(req)
        if response is None:
            return
        services.WriteDataByIdentifier.interpret_response(response)

        if response.service_data.did_echo != did:
            raise UnexpectedResponseException(response, "Server returned a response for data identifier 0x%04x while client requested for did 0x%04x" % (response.service_data.did_echo, did))

        return response

    @standard_error_management
    def ecu_reset(self, reset_type):
        """
        Requests the server to execute a reset sequence through the :ref:`ECUReset<ECUReset>` service.

        :Effective configuration: ``exception_on_<type>_response``

        :param reset_type: The type of reset to perform.  :class:`ECUReset.ResetType<udsoncan.services.ECUReset.ResetType>`
        :type reset_type: int

        :return: The server response parsed by :meth:`ECUReset.interpret_response<udsoncan.services.ECUReset.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """
        req = services.ECUReset.make_request(reset_type)
        self.logger.info("%s - Requesting reset of type 0x%02x (%s)" % (self.service_log_prefix(services.ECUReset), reset_type, services.ECUReset.ResetType.get_name(reset_type)))

        response = self.send_request(req)
        if response is None:
            return
        services.ECUReset.interpret_response(response)

        if response.service_data.reset_type_echo != reset_type:
            raise UnexpectedResponseException(response, "Response subfunction received from server (0x%02x) does not match the requested subfunction (0x%02x)" % (response.service_data.reset_type_echo, reset_type))

        if response.service_data.reset_type_echo == services.ECUReset.ResetType.enableRapidPowerShutDown and response.service_data.powerdown_time != 0xFF:
            self.logger.info('Server will shutdown in %d seconds.' % (response.service_data.powerdown_time))

        return response

    @standard_error_management 
    def clear_dtc(self, group=0xFFFFFF):
        """
        Requests the server to clear its active Diagnostic Trouble Codes with the :ref:`ClearDiagnosticInformation<ClearDiagnosticInformation>` service.

        :Effective configuration: ``exception_on_<type>_response``

        :param group: The group of DTCs to clear. It may refer to Powertrain DTCs, Chassis DTCs, etc. Values are defined by the ECU manufacturer except for two specific values

                - ``0x000000`` : Emissions-related systems
                - ``0xFFFFFF`` : All DTCs
        :type group: int

        :return: The server response parsed by :meth:`ClearDiagnosticInformation.interpret_response<udsoncan.services.ClearDiagnosticInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """

        request = services.ClearDiagnosticInformation.make_request(group)
        if group == 0xFFFFFF:
            self.logger.info('%s - Clearing all DTCs (group mask : 0xFFFFFF)' % (self.service_log_prefix(services.ClearDiagnosticInformation)))
        else:
            self.logger.info('%s - Clearing DTCs matching group mask : 0x%06x' % (self.service_log_prefix(services.ClearDiagnosticInformation), group))

        response = self.send_request(request)
        if response is None:
            return
        services.ClearDiagnosticInformation.interpret_response(response)

        return response

    # Performs a RoutineControl Service request
    def start_routine(self, routine_id, data=None):
        """
        Requests the server to start a routine through the :ref:`RoutineControl<RoutineControl>` service (subfunction = 0x01).

        :Effective configuration: ``exception_on_<type>_response``

        :param routine_id: The 16-bit numerical ID of the routine to start
        :type group: int

        :param data: Optional additional data to give to the server
        :type data: bytes

        :return: The server response parsed by :meth:`RoutineControl.interpret_response<udsoncan.services.RoutineControl.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """
        return self.routine_control(routine_id, services.RoutineControl.ControlType.startRoutine, data)

    # Performs a RoutineControl Service request
    def stop_routine(self, routine_id, data=None):
        """
        Requests the server to stop a routine through the :ref:`RoutineControl<RoutineControl>` service (subfunction = 0x02).

        :Effective configuration: ``exception_on_<type>_response``

        :param routine_id: The 16-bit numerical ID of the routine to stop
        :type group: int

        :param data: Optional additional data to give to the server
        :type data: bytes

        :return: The server response parsed by :meth:`RoutineControl.interpret_response<udsoncan.services.RoutineControl.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """
        return self.routine_control(routine_id, services.RoutineControl.ControlType.stopRoutine, data)

    # Performs a RoutineControl Service request
    def get_routine_result(self, routine_id, data=None):
        """
        Requests the server to send back the execution result of the specified routine through the :ref:`RoutineControl<RoutineControl>` service (subfunction = 0x03).

        :Effective configuration: ``exception_on_<type>_response``

        :param routine_id: The 16-bit numerical ID of the routine
        :type group: int

        :param data: Optional additional data to give to the server
        :type data: bytes

        :return: The server response parsed by :meth:`RoutineControl.interpret_response<udsoncan.services.RoutineControl.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.routine_control(routine_id, services.RoutineControl.ControlType.requestRoutineResults, data)

    # Performs a RoutineControl Service request
    @standard_error_management
    def routine_control(self, routine_id, control_type, data=None):
        """
        Sends a generic request for the :ref:`RoutineControl<RoutineControl>` service with custom subfunction (control_type).

        :Effective configuration: ``exception_on_<type>_response``

        :param control_type: The service subfunction. See :class:`RoutineControl.ControlType<udsoncan.services.RoutineControl.ControlType>`
        :type group: int

        :param routine_id: The 16-bit numerical ID of the routine
        :type group: int

        :param data: Optional additional data to give to the server
        :type data: bytes

        :return: The server response parsed by :meth:`RoutineControl.interpret_response<udsoncan.services.RoutineControl.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        request = services.RoutineControl.make_request(routine_id, control_type, data=data)
        payload_length = 0 if data is None else len(data)
        action = "ISOSAEReserved action for routine ID"
        if control_type == services.RoutineControl.ControlType.startRoutine:
            action = "Starting routine ID"
        elif control_type == services.RoutineControl.ControlType.stopRoutine:
            action = "Stoping routine ID"
        elif control_type == services.RoutineControl.ControlType.requestRoutineResults:
            action = "Requesting result for routine ID"

        self.logger.info("%s - ControlType=0x%02x - %s 0x%04x (%s) with a payload of %d bytes" % (self.service_log_prefix(services.RoutineControl), control_type, action,  routine_id, Routine.name_from_id(routine_id), payload_length ))
        if data is not None:
            self.logger.debug("\tPayload data : %s" % binascii.hexlify(data))

        response = self.send_request(request)
        if response is None:
            return
        services.RoutineControl.interpret_response(response)

        if control_type != response.service_data.control_type_echo:
            raise UnexpectedResponseException(response, "Control type of response (0x%02x) does not match request control type (0x%02x)" % (response.service_data.control_type_echo, control_type))

        if routine_id != response.service_data.routine_id_echo:
            raise UnexpectedResponseException(response, "Response received from server (ID = 0x%04x) does not match the requested routine ID (0x%04x)" % (response.service_data.routine_id_echo, routine_id))

        return response

    def read_extended_timing_parameters(self):
        """
        Reads the timing parameters from the server with :ref:`AccessTimingParameter<AccessTimingParameter>` service with subfunction ``readExtendedTimingParameterSet`` (0x01).

        :Effective configuration: ``exception_on_<type>_response``

        :return: The server response parsed by :meth:`AccessTimingParameter.interpret_response<udsoncan.services.AccessTimingParameter.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """
        return self.access_timing_parameter(access_type=services.AccessTimingParameter.AccessType.readExtendedTimingParameterSet)

    def reset_default_timing_parameters(self):
        """
        Resets the server timing parameters to their default value with :ref:`AccessTimingParameter<AccessTimingParameter>` service with subfunction ``setTimingParametersToDefaultValues`` (0x02).

        :Effective configuration: ``exception_on_<type>_response``

        :return: The server response parsed by :meth:`AccessTimingParameter.interpret_response<udsoncan.services.AccessTimingParameter.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """
        return self.access_timing_parameter(access_type=services.AccessTimingParameter.AccessType.setTimingParametersToDefaultValues)

    def read_active_timing_parameters(self):
        """
        Reads the currently active timing parameters from the server with :ref:`AccessTimingParameter<AccessTimingParameter>` service with subfunction ``readCurrentlyActiveTimingParameters`` (0x03).

        :Effective configuration: ``exception_on_<type>_response``

        :return: The server response parsed by :meth:`AccessTimingParameter.interpret_response<udsoncan.services.AccessTimingParameter.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """
        return self.access_timing_parameter(access_type=services.AccessTimingParameter.AccessType.readCurrentlyActiveTimingParameters)

    def set_timing_parameters(self, params):
        """
        Sets the timing parameters into the server with :ref:`AccessTimingParameter<AccessTimingParameter>` service with subfunction ``setTimingParametersToGivenValues`` (0x04).

        :Effective configuration: ``exception_on_<type>_response``

        :param params: The parameters data. Specific to each ECU.
        :type params: bytes

        :return: The server response parsed by :meth:`AccessTimingParameter.interpret_response<udsoncan.services.AccessTimingParameter.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """
        return self.access_timing_parameter(access_type=services.AccessTimingParameter.AccessType.setTimingParametersToGivenValues, request_record=params)


    @standard_error_management
    def access_timing_parameter(self, access_type, timing_param_record=None):
        """
        Sends a generic request for :ref:`AccessTimingParameter<AccessTimingParameter>` service with configurable subfunction (access_type).

        :Effective configuration: ``exception_on_<type>_response``

        :param access_type: The service subfunction. See :class:`AccessTimingParameter.AccessType<udsoncan.services.AccessTimingParameter.AccessType>`
        :type access_type: bytes

        :param params: The parameters data. Specific to each ECU.
        :type params: bytes

        :return: The server response parsed by :meth:`AccessTimingParameter.interpret_response<udsoncan.services.AccessTimingParameter.interpret_response>`
        :rtype: :ref:`Response<Response>`

        """

        request = services.AccessTimingParameter.make_request(access_type, timing_param_record)
        payload_length = 0 if timing_param_record is None else len(timing_param_record)

        self.logger.info('%s - AccessType=0x%02x (%s) - Sending request with record payload of %d bytes' % (self.service_log_prefix(services.AccessTimingParameter), access_type, services.AccessTimingParameter.AccessType.get_name(access_type), payload_length))
        if timing_param_record is not None:
            self.logger.debug("Payload data : %s" % binascii.hexlify(data))

        response = self.send_request(request)
        if response is None:
            return

        services.AccessTimingParameter.interpret_response(response)

        if access_type != response.service_data.access_type_echo:
            raise UnexpectedResponseException(response, "Access type of response (0x%02x) does not match request access type (0x%02x)" % (response.service_data.access_type_echo, access_type))

        allowed_response_record_access_type = [
                services.AccessTimingParameter.AccessType.readExtendedTimingParameterSet, 
                services.AccessTimingParameter.AccessType.readCurrentlyActiveTimingParameters
        ]

        if len(response.service_data.timing_param_record) > 0 and access_type not in allowed_response_record_access_type:
            self.logger.warning("Server returned data in the AccessTimingParameter response although none was asked")

        return response

    @standard_error_management
    def communication_control(self, control_type, communication_type):
        """
        Switches the transmission or reception of certain messages on/off with :ref:`CommunicationControl<CommunicationControl>` service.

        :Effective configuration: ``exception_on_<type>_response``

        :param control_type: The action to request such as enabling or disabling some messages. See :class:`CommunicationControl.ControlType<udsoncan.services.CommunicationControl.ControlType>`. This value can also be ECU manufacturer-specific
        :type control_type: int

        :param communication_type: Indicates what section of the network and the type of message that should be affected by the command. Refer to :ref:`CommunicationType<CommunicationType>` for more details. If an `integer` or a `bytes` is given, the value will be decoded to create the required :ref:`CommunicationType<CommunicationType>` object
        :type communication_type: :ref:`CommunicationType<CommunicationType>`, bytes, int

        :return: The server response parsed by :meth:`CommunicationControl.interpret_response<udsoncan.services.CommunicationControl.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """

        communication_type = services.CommunicationControl.normalize_communication_type(communication_type)

        request = services.CommunicationControl.make_request(control_type, communication_type)
        self.logger.info('%s - ControlType=0x%02x (%s) - Sending request with a CommunicationType byte of 0x%02x (%s)' % (self.service_log_prefix(services.CommunicationControl), control_type, services.CommunicationControl.ControlType.get_name(control_type), communication_type.get_byte_as_int(), str(communication_type)))

        response = self.send_request(request)
        if response is None:
            return

        services.CommunicationControl.interpret_response(response)

        if control_type != response.service_data.control_type_echo:
            raise UnexpectedResponseException(response, "Control type of response (0x%02x) does not match request control type (0x%02x)" % (response.service_data.control_type_echo, control_type))

        return response

    def request_download(self, memory_location, dfi=None):
        """
        Informs the server that the client wants to initiate a download from the client to the server by sending a :ref:`RequestDownload<RequestDownload>` service request.

        :Effective configuration: ``exception_on_<type>_response`` ``server_address_format`` ``server_memorysize_format``

        :param memory_location: The address and size of the memory block to be written.
        :type memory_location: :ref:`MemoryLocation <MemoryLocation>`

        :param dfi: Optional :ref:`DataFormatIdentifier <DataFormatIdentifier>` defining the compression and encryption scheme of the data. 
                If not specified, the default value of 00 will be used, specifying no encryption and no compression
        :type dfi: :ref:`DataFormatIdentifier <DataFormatIdentifier>`

        :return: The server response parsed by :meth:`RequestDownload.interpret_response<udsoncan.services.RequestDownload.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.request_upload_download(services.RequestDownload, memory_location, dfi)

    def request_upload(self, memory_location, dfi=None):
        """
        Informs the server that the client wants to initiate an upload from the server to the client by sending a :ref:`RequestUpload<RequestUpload>` service request.

        :Effective configuration: ``exception_on_<type>_response`` ``server_address_format`` ``server_memorysize_format``

        :param memory_location: The address and size of the memory block to be written.
        :type memory_location: :ref:`MemoryLocation <MemoryLocation>`

        :param dfi: Optional :ref:`DataFormatIdentifier <DataFormatIdentifier>` defining the compression and encryption scheme of the data. 
                If not specified, the default value of 00 will be used, specifying no encryption and no compression
        :type dfi: :ref:`DataFormatIdentifier <DataFormatIdentifier>`

        :return: The server response parsed by :meth:`RequestUpload.interpret_response<udsoncan.services.RequestUpload.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.request_upload_download(services.RequestUpload, memory_location, dfi)

    # Common code for both RequestDownload and RequestUpload services
    @standard_error_management
    def request_upload_download(self, service_cls, memory_location, dfi=None):
        dfi = service_cls.normalize_data_format_identifier(dfi)

        if service_cls not in [services.RequestDownload, services.RequestUpload]:
            raise ValueError('Service must either be RequestDownload or RequestUpload')

        if not isinstance(memory_location, MemoryLocation):
            raise ValueError('memory_location must be an instance of MemoryLocation')

        # If user does not specify a byte format, we apply the one in client configuration.
        if 'server_address_format' in self.config:
            memory_location.set_format_if_none(address_format=self.config['server_address_format'])

        if 'server_memorysize_format' in self.config:
            memory_location.set_format_if_none(memorysize_format=self.config['server_memorysize_format'])

        request = service_cls.make_request(memory_location=memory_location, dfi=dfi)

        action = ""
        if service_cls == services.RequestDownload:
            action = "Requesting a download (client to server)"
        elif service_cls == services.RequestUpload:
            action = "Requesting an upload (server to client)"

        self.logger.info('%s - %s for memory location [%s] and DataFormatIdentifier 0x%02x (%s)' % (self.service_log_prefix(service_cls), action, str(memory_location), dfi.get_byte_as_int(), str(dfi)))

        response = self.send_request(request)
        if response is None:
            return
        service_cls.interpret_response(response)

        return response

    @standard_error_management
    def transfer_data(self, sequence_number, data=None):
        """
        Transfer a block of data to/from the client to/from the server by sending a :ref:`TransferData<TransferData>` service request and returning the server response.

        :Effective configuration: ``exception_on_<type>_response``

        :param sequence_number: Corresponds to an 8bit counter that should increment for each new block transferred.
                Allowed values are from 0 to 0xFF
        :type sequence_number: int

        :param data: Optional additional data to send to the server
        :type data: bytes

        :return: The server response parsed by :meth:`TransferData.interpret_response<udsoncan.services.TransferData.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        request = services.TransferData.make_request(sequence_number, data)

        data_len = 0 if data is None else len(data)
        self.logger.info('%s - Sending a block of data with SequenceNumber=%d that is %d bytes long .' % (self.service_log_prefix(services.TransferData), sequence_number, data_len))
        if data is not None:
            self.logger.debug('Data to transfer : %s' % binascii.hexlify(data))

        response = self.send_request(request)
        if response is None:
            return
        services.TransferData.interpret_response(response)

        if sequence_number != response.service_data.sequence_number_echo:
            raise UnexpectedResponseException(response, "Block sequence number of response (0x%02x) does not match request block sequence number (0x%02x)" % (response.service_data.sequence_number_echo, sequence_number))

        return response

    @standard_error_management
    def request_transfer_exit(self, data=None):
        """
        Informs the server that the client wants to stop the data transfer by sending a :ref:`RequestTransferExit<RequestTransferExit>` service request.

        :Effective configuration: ``exception_on_<type>_response``

        :param data: Optional additional data to send to the server
        :type data: bytes

        :return: The server response parsed by :meth:`RequestTransferExit.interpret_response<udsoncan.services.RequestTransferExit.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        request = services.RequestTransferExit.make_request(data)
        self.logger.info('%s - Sending exit request' % (self.service_log_prefix(services.RequestTransferExit)))

        response = self.send_request(request)
        if response is None:
            return
        services.RequestTransferExit.interpret_response(response)

        return response

    @standard_error_management
    def link_control(self, control_type, baudrate=None):
        """
        Controls the communication baudrate by sending a :ref:`LinkControl<LinkControl>` service request.

        :Effective configuration: ``exception_on_<type>_response``

        :param control_type: Allowed values are from 0 to 0xFF. See :class:`LinkControl.ControlType<udsoncan.services.LinkControl.ControlType>`
        :type control_type: int

        :param baudrate: Required baudrate value when ``control_type`` is either ``verifyBaudrateTransitionWithFixedBaudrate`` (1) or ``verifyBaudrateTransitionWithSpecificBaudrate`` (2)
        :type baudrate: :ref:`Baudrate <Baudrate>`

        :return: The server response parsed by :meth:`LinkControl.interpret_response<udsoncan.services.LinkControl.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        request = services.LinkControl.make_request(control_type, baudrate)
        baudrate_str = 'No baudrate specified' if baudrate is None else 'Baudrate : ' + str(baudrate)

        action = "Performing LinkControl request"
        if control_type in [services.LinkControl.ControlType.verifyBaudrateTransitionWithFixedBaudrate, services.LinkControl.ControlType.verifyBaudrateTransitionWithSpecificBaudrate]:
            action = "Verifiying support"
        elif   control_type == services.LinkControl.ControlType.transitionBaudrate:
            action = "Switching"

        self.logger.info('%s - ControlType=0x%02x (%s) - %s for baudrate %s ' % (self.service_log_prefix(services.LinkControl), control_type, services.LinkControl.ControlType.get_name(control_type), action, baudrate_str))

        response = self.send_request(request)
        if response is None:
            return
        services.LinkControl.interpret_response(response)

        if control_type != response.service_data.control_type_echo:
            raise UnexpectedResponseException(response, "Control type of response (0x%02x) does not match request control type (0x%02x)" % (response.service_data.control_type_echo, control_type))

        return response

    @standard_error_management
    def io_control(self, did, control_param=None, values=None, masks=None):
        """
        Substitutes the value of an input signal or overrides the state of an output by sending a :ref:`InputOutputControlByIdentifier<InputOutputControlByIdentifier>` service request.

        :Effective configuration: ``exception_on_<type>_response`` ``input_output`` ``tolerate_zero_padding``

        :param did: Data identifier to represent the IO
        :type did: int

        :param control_param: Optional parameter that can be a value from :class:`InputOutputControlByIdentifier.ControlParam<udsoncan.services.InputOutputControlByIdentifier.ControlParam>`
        :type control_param: int

        :param values: Optional values to send to the server. This parameter will be given to :ref:`DidCodec<DidCodec>`.encode() method. 
                It can be:

                        - A list for positional arguments
                        - A dict for named arguments
                        - An instance of :ref:`IOValues<IOValues>` for mixed arguments

        :type values: list, dict, :ref:`IOValues<IOValues>`

        :param masks: Optional mask record for composite values. The mask definition must be included in ``config['input_output']``
                It can be:

                        - A list naming the bit mask to set
                        - A dict with the mask name as a key and a boolean setting or clearing the mask as the value
                        - An instance of :ref:`IOMask<IOMask>`
                        - A boolean value to set all masks to the same value.
        :type masks: list, dict, :ref:`IOMask<IOMask>`, bool

        :return: The server response parsed by :meth:`InputOutputControlByIdentifier.interpret_response<udsoncan.services.InputOutputControlByIdentifier.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        if 'input_output' not in self.config:
            raise ConfigError('input_output', msg='input_output must be defined in client configuration in order to use InputOutputControlByIdentifier service')

        request = services.InputOutputControlByIdentifier.make_request( did, control_param=control_param, values=values, masks=masks, ioconfig=self.config['input_output'])

        control_param_str = 'no control parameter' if control_param is None else 'control parameter 0x%02x (%s)' % (control_param, services.InputOutputControlByIdentifier.ControlParam.get_name(control_param))		
        self.logger.info('%s - Sending request for DID=0x%04x, %s.' % (self.service_log_prefix(services.InputOutputControlByIdentifier), did, control_param_str))

        response = self.send_request(request)
        if response is None:
            return
        services.InputOutputControlByIdentifier.interpret_response(response, control_param=control_param, tolerate_zero_padding=self.config['tolerate_zero_padding'], ioconfig=self.config['input_output'])

        if response.service_data.did_echo != did:
            raise UnexpectedResponseException(response, "Echo of the DID number (0x%04x) does not match the value in the request (0x%04x)" % (response.service_data.did_echo, did))

        if control_param != response.service_data.control_param_echo:
            raise UnexpectedResponseException(response, 'Echo of the InputOutputControlParameter (0x%02x) does not match the value in the request (0x%02x)' % (response.service_data.control_param_echo, control_param))	

        return response

    @standard_error_management
    def control_dtc_setting(self, setting_type, data=None):
        """
        Controls some settings related to the Diagnostic Trouble Codes by sending a :ref:`ControlDTCSetting<ControlDTCSetting>` service request. 
        It can enable/disable some DTCs or perform some ECU specific configuration.

        :Effective configuration: ``exception_on_<type>_response``

        :param setting_type: Allowed values are from 0 to 0x7F. See :class:`ControlDTCSetting.SettingType<udsoncan.services.ControlDTCSetting.SettingType>`
        :type setting_type: int

        :param data: Optional additional data sent with the request called `DTCSettingControlOptionRecord`
        :type data: bytes

        :return: The server response parsed by :meth:`ControlDTCSetting.interpret_response<udsoncan.services.ControlDTCSetting.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        request = services.ControlDTCSetting.make_request(setting_type, data)
        data_len = 0 if data is None else len(data)
        action = "Performing ControlDTCSetting request"
        if setting_type == services.ControlDTCSetting.SettingType.on:
            action = "Turning DTC On"
        elif setting_type == services.ControlDTCSetting.SettingType.off:
            action =  "Turning DTC Off"

        self.logger.info('%s - SettingType=0x%02x (%s) - %s with a payload of %d bytes' % (self.service_log_prefix(services.ControlDTCSetting), setting_type, services.ControlDTCSetting.SettingType.get_name(setting_type), action, data_len))
        if data is not None:
            self.logger.debug("Payload of data : %s" % binascii.hexlify(data))

        response = self.send_request(request)
        if response is None:
            return

        services.ControlDTCSetting.interpret_response(response)

        if response.service_data.setting_type_echo != setting_type:
            raise UnexpectedResponseException(response, "Setting type of response (0x%02x) does not match request control type (0x%02x)" % (response.service_data.setting_type_echo, setting_type))

        return response

    @standard_error_management
    def read_memory_by_address(self, memory_location):
        """
        Reads a block of memory from the server by sending a :ref:`ReadMemoryByAddress<ReadMemoryByAddress>` service request. 

        :Effective configuration: ``exception_on_<type>_response`` ``server_address_format`` ``server_memorysize_format``

        :param memory_location: The address and the size of the memory block to read.
        :type memory_location: :ref:`MemoryLocation <MemoryLocation>`

        :return: The server response parsed by :meth:`ReadMemoryByAddress.interpret_response<udsoncan.services.ReadMemoryByAddress.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        if not isinstance(memory_location, MemoryLocation):
            raise ValueError('memory_location must be an instance of MemoryLocation')

        if 'server_address_format' in self.config:
            memory_location.set_format_if_none(address_format=self.config['server_address_format'])

        if 'server_memorysize_format' in self.config:
            memory_location.set_format_if_none(memorysize_format=self.config['server_memorysize_format'])

        request = services.ReadMemoryByAddress.make_request(memory_location)
        self.logger.info('%s - Reading memory address at %s' % (self.service_log_prefix(services.ReadMemoryByAddress), str(memory_location)))

        response = self.send_request(request)
        if response is None:
            return
        services.ReadMemoryByAddress.interpret_response(response)
        memdata = response.service_data.memory_block

        if len(memdata) < memory_location.memorysize:
            raise UnexpectedResponseException(response, 'Data block given by the server is too short. Client requested for %d bytes but only received %d bytes' % (memory_location.memorysize, len(response.data)))

        if len(memdata) > memory_location.memorysize:
            extra_bytes = len(memdata) - memory_location.memorysize
            if memdata[memory_location.memorysize:] == b'\x00' * extra_bytes and self.config['tolerate_zero_padding']:
                response.service_data.memory_block = memdata[0:memory_location.memorysize]	# trim exceeding zeros
            else:
                raise UnexpectedResponseException(response, 'Data block given by the server is too long. Client requested for %d bytes but received %d bytes' % (memory_location.memorysize, len(response.data)))

        return response

    @standard_error_management
    def write_memory_by_address(self, memory_location, data):
        """
        Writes a block of memory in the server by sending a :ref:`WriteMemoryByAddress<WriteMemoryByAddress>` service request. 

        :Effective configuration: ``exception_on_<type>_response`` ``server_address_format`` ``server_memorysize_format``

        :param memory_location: The address and the size of the memory block to read. 
        :type memory_location: :ref:`MemoryLocation <MemoryLocation>`

        :param data: The data to write into memory.
        :type data: bytes

        :return: The server response parsed by :meth:`WriteMemoryByAddress.interpret_response<udsoncan.services.WriteMemoryByAddress.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """

        if not isinstance(memory_location, MemoryLocation):
            raise ValueError('memory_location must be an instance of MemoryLocation')

        if 'server_address_format' in self.config:
            memory_location.set_format_if_none(address_format=self.config['server_address_format'])

        if 'server_memorysize_format' in self.config:
            memory_location.set_format_if_none(memorysize_format=self.config['server_memorysize_format'])

        request = services.WriteMemoryByAddress.make_request(memory_location, data)
        self.logger.info('%s - Writing %d bytes to memory address at %s' % (self.service_log_prefix(services.WriteMemoryByAddress), len(data), str(memory_location)))

        if len(data) != memory_location.memorysize:
            self.logger.warning('%s: Given data block length (%d bytes) does not match MemoryLocation size (%d bytes)' % (self.service_log_prefix(services.WriteMemoryByAddress), len(data), memory_location.memorysize))

        response = self.send_request(request)
        if response is None:
            return
        services.WriteMemoryByAddress.interpret_response(response, memory_location)

        alfid_byte 			= memory_location.alfid.get_byte_as_int()   # AddressAndLengthFormatIdentifier
        address_bytes 		= memory_location.get_address_bytes()
        memorysize_bytes 	= memory_location.get_memorysize_bytes()

        # We make sure that the echo from the server matches the request we sent.
        if response.service_data.alfid_echo != alfid_byte:
            raise UnexpectedResponseException(response, 'AddressAndLengthFormatIdentifier echoed back by the server (0x%02X) does not match the one requested by the client (0x%02X)' % (response.service_data.alfid_echo, int(alfid_byte)) )

        if response.service_data.memory_location_echo.address != memory_location.address:
            raise UnexpectedResponseException(response, 'Address echoed back by the server (0x%X) does not match the one requested by the client (0x%X)' % (response.service_data.memory_location_echo.address, memory_location.address) )

        if response.service_data.memory_location_echo.memorysize != memory_location.memorysize:
            raise UnexpectedResponseException(response, 'Memory size echoed back by the server (0x%X) does not match the one requested by the client (0x%X)' % (response.service_data.memory_location_echo.memorysize, memory_location.memorysize) )

        return response

# ====  ReadDTCInformation
    def get_dtc_by_status_mask(self, status_mask):
        """
        Reads all the Diagnostic Trouble Codes that have a status matching the given mask. 
        The server will check all of its DTCs and if (Dtc.status & status_mask) != 0, then the DTCs match the filter and are sent back to the client.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :param status_mask: The status mask against which the DTCs are tested. 
        :type status_mask: int or :ref:`Dtc.Status<DTC_Status>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCByStatusMask, status_mask=status_mask)

    def get_emission_dtc_by_status_mask(self, status_mask):
        """
        Reads the emission-related Diagnostic Trouble Codes that have a status matching the given mask.
        The server will check its emission-related DTCs and if (Dtc.status & status_mask) != 0, then the DTCs match the filter and are sent back to the client.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :param status_mask: The status mask against which the DTCs are tested. 
        :type status_mask: int or :ref:`Dtc.Status<DTC_Status>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportEmissionsRelatedOBDDTCByStatusMask, status_mask=status_mask)

    def get_mirrormemory_dtc_by_status_mask(self, status_mask):
        """
        Reads all the Diagnostic Trouble Codes stored in mirror memory that have a status matching the given mask. 
        The server will check all of its DTCs and if (Dtc.status & status_mask) != 0, then the DTCs match the filter and are sent back to the client.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :param status_mask: The status mask against which the DTCs are tested. 
        :type status_mask: int or :ref:`Dtc.Status<DTC_Status>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportMirrorMemoryDTCByStatusMask, status_mask=status_mask)

    def get_dtc_by_status_severity_mask(self, status_mask, severity_mask):
        """
        Reads all the Diagnostic Trouble Codes that have a status and a severity matching the given masks. 
        The server will check all of its DTCs and if ( (Dtc.status & status_mask) != 0 && (Dtc.severity & severity) !=0), then the DTCs match the filter and are sent back to the client.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :param status_mask: The status mask against which the DTCs are tested. 
        :type status_mask: int or :ref:`Dtc.Status<DTC_Status>`

        :param severity_mask: The severity mask against which the DTCs are tested. 
        :type severity_mask: int or :ref:`Dtc.Severity<DTC_Severity>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCBySeverityMaskRecord, status_mask=status_mask, severity_mask=severity_mask)

    def get_number_of_dtc_by_status_mask(self, status_mask):
        """
        Gets the number of DTCs that match the specified status mask.

        :Effective configuration: ``exception_on_<type>_response``

        :param status_mask: The status mask against which the DTCs are tested. 
        :type status_mask: int or :ref:`Dtc.Status<DTC_Status>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportNumberOfDTCByStatusMask, status_mask=status_mask)

    def get_mirrormemory_number_of_dtc_by_status_mask(self, status_mask):
        """
        Gets the number of DTCs that match the specified status mask in mirror memory.

        :Effective configuration: ``exception_on_<type>_response``

        :param status_mask: The status mask against which the DTCs are tested. 
        :type status_mask: int or :ref:`Dtc.Status<DTC_Status>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportNumberOfMirrorMemoryDTCByStatusMask, status_mask=status_mask)

    def get_number_of_emission_dtc_by_status_mask(self, status_mask):
        """
        Gets the number of emission-related DTCs that match the specified status mask.

        :Effective configuration: ``exception_on_<type>_response``

        :param status_mask: The status mask against which the DTCs are tested. 
        :type status_mask: int or :ref:`Dtc.Status<DTC_Status>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportNumberOfEmissionsRelatedOBDDTCByStatusMask, status_mask=status_mask)

    def get_number_of_dtc_by_status_severity_mask(self, status_mask, severity_mask):
        """
        Gets the number of DTCs that match the specified status mask and severity mask.

        :Effective configuration: ``exception_on_<type>_response``

        :param status_mask: The status mask against which the DTCs are tested. 
        :type status_mask: int or :ref:`Dtc.Status<DTC_Status>`

        :param severity_mask: The severity mask against which the DTCs are tested. 
        :type severity_mask: int or :ref:`Dtc.Severity<DTC_Severity>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportNumberOfDTCBySeverityMaskRecord, status_mask=status_mask, severity_mask=severity_mask)

    def get_dtc_severity(self, dtc):
        """
        Requests the server for a specific DTC severity level.

        :Effective configuration: ``exception_on_<type>_response``

        :param dtc: The DTC ID for which we request the severity. It can be a 3-byte integer or a DTC instance with an ID set.
        :type dtc: int or :ref:`Dtc<DTC>`

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportSeverityInformationOfDTC, dtc=dtc)

    def get_supported_dtc(self):
        """
        Requests the list of supported DTCs by the server.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportSupportedDTCs)

    def get_first_test_failed_dtc(self):
        """
        Reads a single DTC. Requests the server for the first DTC that set its ``Dtc.Status.test_failed`` bit.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportFirstTestFailedDTC)

    def get_first_confirmed_dtc(self):
        """
        Reads a single DTC. Requests the server for the first DTC that set its ``Dtc.Status.confirmed`` bit.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportFirstConfirmedDTC)

    def get_most_recent_test_failed_dtc(self):
        """
        Reads a single DTC. Requests the server for the last DTC that set its ``Dtc.Status.test_failed`` bit.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportMostRecentTestFailedDTC)

    def get_most_recent_confirmed_dtc(self):
        """
        Reads a single DTC. Requests the server for the last DTC that set its ``Dtc.Status.confirmed`` bit.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportMostRecentConfirmedDTC)

    def get_dtc_with_permanent_status(self):
        """
        Returns all DTCs that the server marked as `permanent`. 

        A permanent DTC is a DTC stored in Non-Volatile memory and that cannot be erased by test equipment or by power-cycling the ECU.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCWithPermanentStatus)

    def get_dtc_fault_counter(self):
        """
        Requests the server for all DTCs that are `prefailed` along with their fault detection counter. 

        A prefailed DTC is a DTC for which the detection condition is met, but has not been identified as `pending` or `confirmed` yet. 

        If the ECU follows the UDS guidelines, it will wait to detect a fault many times before setting a status bit for this fault DTC. Each time the fault is detected, a fault counter is incremented, when it is not detected, the counter is decremented.
        Once the fault counter reaches a threshold, a status bit is set and the DTC is not `prefailed` anymore. A `prefailed` DTC is any DTC that has fault detection counter greater than 0, but less than the detection threshold.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCFaultDetectionCounter)

    def get_dtc_snapshot_identification(self):
        """
        Requests the server to return an index of all the DTC snapshots available. The server will respond with a list of DTCs and a list of snapshot record numbers for each DTC.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc``

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCSnapshotIdentification)

    def get_dtc_snapshot_by_dtc_number(self, dtc, record_number=0xFF):
        """
        Requests the server for one or many specific DTC snapshots associated with a single DTC.
        Each snapshot has a data identifier associated with it. The data will be decoded using the associated :ref:`DidCodec<DidCodec>` defined in ``config['data_identifiers']``.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``ignore_all_zero_dtc`` ``dtc_snapshot_did_size``

        :param dtc: The DTC ID for which we request the snapshot data. It can be a 3-byte integer or a DTC instance with an ID set.
        :type dtc: int or :ref:`Dtc<DTC>`

        :param record_number: The record number of the snapshot data to read. If 0xFF is given, then all snapshots will be read, otherwise, a single snapshot will be read.
        :type record_number: int

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCSnapshotRecordByDTCNumber, dtc=dtc, snapshot_record_number=record_number)

    def get_dtc_snapshot_by_record_number(self, record_number=0xFF):
        """
        Requests the server for one or many DTC snapshots by specifying a record number. This functionality can exist only if the server assigns globally unique record_numbers to DTC snapshots, regardless of the DTC ID.

        Each snapshot has a data identifier associated with it. The data will be decoded using the associated :ref:`DidCodec<DidCodec>` defined in ``config['data_identifiers']``.

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding``  ``dtc_snapshot_did_size``

        :param record_number: The record number of the snapshot data to read. If 0xFF is given, then all snapshots will be read, otherwise, a single snapshot will be read.
        :type record_number: int

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCSnapshotRecordByRecordNumber, snapshot_record_number=record_number)

    def get_dtc_extended_data_by_dtc_number(self, dtc, record_number=0xFF, data_size = None):
        """
        Requests the server for one or many DTC **extended data** by specifying a record number.

        The DTC extended data is an ECU specific set of data that is not associated with a data identifier. Given as ``bytes``

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``extended_data_size``

        :param dtc: The DTC ID for which we request the extended data. It can be a 3-byte integer or a DTC instance with an ID set.
        :type dtc: int or :ref:`Dtc<DTC>`

        :param record_number: The record number of the extended data to read. If 0xFF is given, then all extended data entries will be read, otherwise, a single entry will be read.
        :type record_number: int

        :param data_size: The number of bytes of an extended data record. If not specified ``config['extended_data_size'][dtc]`` will be used.
        :type data_size: int or None

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportDTCExtendedDataRecordByDTCNumber, dtc=dtc, extended_data_record_number=record_number,extended_data_size=data_size)

    def get_mirrormemory_dtc_extended_data_by_dtc_number(self, dtc, record_number=0xFF, data_size = None):
        """
        Requests the server for one or many DTC **extended data** stored in mirror memory by specifying a record number.

        The DTC extended data is an ECU specific set of data that is not associated with a data identifier. Given as ``bytes``

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding`` ``extended_data_size``

        :param dtc: The DTC ID for which we request the extended data. It can be a 3-byte integer or a DTC instance with an ID set.
        :type dtc: int or :ref:`Dtc<DTC>`

        :param record_number: The record number of the extended data to read. If 0xFF is given, then all extended data entries will be read, otherwise, a single entry will be read.
        :type record_number: int

        :param data_size: The number of bytes of an extended data record. If not specified ``config['extended_data_size'][dtc]`` wil be used.
        :type data_size: int or None

        :return: The server response parsed by :meth:`ReadDTCInformation.interpret_response<udsoncan.services.ReadDTCInformation.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.read_dtc_information(services.ReadDTCInformation.Subfunction.reportMirrorMemoryDTCExtendedDataRecordByDTCNumber, dtc=dtc, extended_data_record_number=record_number, extended_data_size=data_size)

    # Performs a ReadDiagnsticInformation service request.
    # Many requests are encoded the same way and many responses are encoded the same way. Request grouping and response grouping are independent.
    @standard_error_management
    def read_dtc_information(self, subfunction, status_mask=None, severity_mask=None,  dtc=None, snapshot_record_number=None, extended_data_record_number=None, extended_data_size=None):
        if dtc is not None and isinstance(dtc, Dtc):
            dtc = dtc.id

        request_params = {
                'subfunction' : subfunction,
                'status_mask' : status_mask,
                'severity_mask' : severity_mask,
                'dtc' : dtc,
                'snapshot_record_number' : snapshot_record_number,
                'extended_data_record_number' : extended_data_record_number
        }

        request = services.ReadDTCInformation.make_request(**request_params)

        self.logger.info('%s - Sending request with subfunction "%s" (0x%02X).' % (self.service_log_prefix(services.ReadDTCInformation), services.ReadDTCInformation.Subfunction.get_name(subfunction), subfunction))
        self.logger.debug('\tParams are : %s' % str(request_params))
        response = self.send_request(request)
        if response is None:
            return

        response_params = {
                'subfunction' : subfunction,
                'tolerate_zero_padding' : self.config['tolerate_zero_padding'],
                'ignore_all_zero_dtc' : self.config['ignore_all_zero_dtc'],
                'dtc_snapshot_did_size' : self.config['dtc_snapshot_did_size'],
                'didconfig' : self.config['data_identifiers'] if 'data_identifiers' in self.config else None,
                'extended_data_size' : extended_data_size
        }

        if extended_data_size is None:
            if 'extended_data_size' in self.config:
                if dtc is not None and dtc in self.config['extended_data_size']:
                    response_params['extended_data_size'] = self.config['extended_data_size'][dtc]

        services.ReadDTCInformation.interpret_response(response, **response_params)

        if response.service_data.subfunction_echo != subfunction:
            raise UnexpectedResponseException(response, 'Echo of ReadDTCInformation subfunction gotten from server(0x%02x) does not match the value in the request subfunction (0x%02x)' % (response.service_data.subfunction_echo, subfunction))	

        if subfunction == services.ReadDTCInformation.Subfunction.reportDTCSnapshotRecordByDTCNumber:
            if len(response.service_data.dtcs) == 1:
                if dtc != response.service_data.dtcs[0].id:
                    raise UnexpectedResponseException(response, 'Server returned snapshot with DTC ID 0x%06x while client requested for 0x%06x' % (response.service_data.dtcs[0].id, dtc))

        if subfunction in [services.ReadDTCInformation.Subfunction.reportDTCSnapshotRecordByRecordNumber, services.ReadDTCInformation.Subfunction.reportDTCSnapshotRecordByDTCNumber]:
            if len(response.service_data.dtcs) == 1 and snapshot_record_number != 0xFF:
                for snapshot in response.service_data.dtcs[0].snapshots:
                    if snapshot.record_number != snapshot_record_number:
                        raise UnexpectedResponseException(response, 'Server returned snapshot with record number 0x%02x while client requested for 0x%02x' % (snapshot.record_number, snapshot_record_number)) 

        if subfunction in [services.ReadDTCInformation.Subfunction.reportDTCExtendedDataRecordByDTCNumber, services.ReadDTCInformation.Subfunction.reportMirrorMemoryDTCExtendedDataRecordByDTCNumber]:
            if len(response.service_data.dtcs) == 1 and extended_data_record_number < 0xF0: # Standard specifies that values between 0xF0 and 0xFF are for reporting groups (more than one record)
                for extended_data in response.service_data.dtcs[0].extended_data:
                    if extended_data.record_number != extended_data_record_number :	
                        raise UnexpectedResponseException(response, 'Extended data record number given by the server (0x%02x) does not match the record number requested by the client (0x%02x)' % (extended_data.record_number, extended_data_record_number))

        for dtc in response.service_data.dtcs:
            if dtc.fault_counter is not None and (dtc.fault_counter >= 0x7F or dtc.fault_counter < 0x01):
                self.logger.warning('Server returned a fault counter value of 0x%02x for DTC id 0x%06x while value should be between 0x01 and 0x7E.' % (dtc.fault_counter, dtc.id))

        if Dtc.Format.get_name(response.service_data.dtc_format) is None:
            self.logger.warning('Unknown DTC Format Identifier 0x%02x. Value should be between 0 and 3' % response.service_data.dtc_format)

        return response


    def add_file(self, filename, dfi=None, filesize=None):
        """
        Sends a RequestFileTransfer request with ModeOfOperation=AddFile(1).

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding``

        :param filename: The name of the file to create, limited to ASCII characters.
        :type filename: str

        :param dfi: DataFormatIdentifier defining the compression and encryption scheme of the data. 
                If not specified, the default value of 00 will be used, specifying no encryption and no compression. 
        :type dfi: :ref:`DataFormatIdentifier<DataFormatIdentifier>`

        :param filesize: The filesize of the file to write. 
            If filesize is an object of type :ref:`Filesize<Filesize>`, the uncompressed size and compressed size will be encoded on
            the minimum amount of bytes necessary, unless a ``width`` is explicitly defined. If no compressed size is given or filesize is an ``int``,
            then the compressed size will be set equal to the uncompressed size or the integer value given as specified by ISO-14229
        :type filesize: :ref:`Filesize<Filesize>` or int

        :return: The server response parsed by :meth:`RequestFileTransfer.interpret_response<udsoncan.services.RequestFileTransfer.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.request_file_transfer(moop=services.RequestFileTransfer.ModeOfOperation.AddFile, path=filename, dfi=dfi, filesize=filesize)

    def delete_file(self, filename):
        """
        Sends a RequestFileTransfer request with ModeOfOperation=DeleteFile(2).

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding``

        :param filename: The name of the file to delete, limited to ASCII characters.
        :type filename: str

        :return: The server response parsed by :meth:`RequestFileTransfer.interpret_response<udsoncan.services.RequestFileTransfer.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.request_file_transfer(moop=services.RequestFileTransfer.ModeOfOperation.DeleteFile, path=filename)
    
    def replace_file(self, filename, dfi=None, filesize=None):
        """
        Sends a RequestFileTransfer request with ModeOfOperation=ReplaceFile(3).

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding``

        :param filename: The name of the file to replace, limited to ASCII characters.
        :type filename: str

        :param dfi: DataFormatIdentifier defining the compression and encryption scheme of the data. 
                If not specified, the default value of 00 will be used, specifying no encryption and no compression. 
        :type dfi: :ref:`DataFormatIdentifier<DataFormatIdentifier>`

        :param filesize: The filesize of the file to write. 
            If filesize is an object of type :ref:`Filesize<Filesize>`, the uncompressed size and compressed size will be encoded on
            the minimum amount of bytes necessary, unless a ``width`` is explicitly defined. If no compressed size is given or filesize is an ``int``,
            then the compressed size will be set equal to the uncompressed size or the integer value given as specified by ISO-14229
        :type filesize: :ref:`Filesize<Filesize>` or int

        :return: The server response parsed by :meth:`RequestFileTransfer.interpret_response<udsoncan.services.RequestFileTransfer.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.request_file_transfer(moop=services.RequestFileTransfer.ModeOfOperation.ReplaceFile, path=filename, dfi=dfi, filesize=filesize)
   
    def read_file(self, filename, dfi=None):
        """
        Sends a RequestFileTransfer request with ModeOfOperation=ReadFile(4).

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding``

        :param filename: The name of the file to read, limited to ASCII characters.
        :type filename: str

        :param dfi: DataFormatIdentifier defining the compression and encryption scheme of the data. 
                If not specified, the default value of 00 will be used, specifying no encryption and no compression. 
        :type dfi: :ref:`DataFormatIdentifier<DataFormatIdentifier>`

        :return: The server response parsed by :meth:`RequestFileTransfer.interpret_response<udsoncan.services.RequestFileTransfer.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.request_file_transfer(moop=services.RequestFileTransfer.ModeOfOperation.ReadFile, path=filename, dfi=dfi)
   
    def read_dir(self, path):
        """
        Sends a RequestFileTransfer request with ModeOfOperation=ReadDir(5).

        :Effective configuration: ``exception_on_<type>_response`` ``tolerate_zero_padding``

        :param path: The name of the directory to read, limited to ASCII characters.
        :type path: str

        :return: The server response parsed by :meth:`RequestFileTransfer.interpret_response<udsoncan.services.RequestFileTransfer.interpret_response>`
        :rtype: :ref:`Response<Response>`
        """
        return self.request_file_transfer(moop=services.RequestFileTransfer.ModeOfOperation.ReadDir, path=path)

    @standard_error_management
    def request_file_transfer(self, moop, path, dfi=None, filesize=None):
        request = services.RequestFileTransfer.make_request(moop=moop, path=path, dfi=dfi, filesize=filesize)
        self.logger.info('%s - Sending a "%s"(0x%02x) request on "%s". ' % (self.service_log_prefix(services.RequestFileTransfer), services.RequestFileTransfer.ModeOfOperation.get_name(moop), moop, path))
        self.logger.debug("%s - DataFormatIdentifier = %s. Filesize = %s" % (self.service_log_prefix(services.RequestFileTransfer), dfi, filesize))
       
        response = self.send_request(request)
        if response is None:
            return
        try:
            services.RequestFileTransfer.interpret_response(response, tolerate_zero_padding = self.config['tolerate_zero_padding'])
        except InvalidResponseException as e:
            if e.response.service_data.moop_echo is not None and e.response.service_data.moop_echo != moop:
                raise UnexpectedResponseException(e.response, 'ModeOfOperation echo does not match request and caused the service to failed decoding the payload correctly. Received 0x%02x, Requested=0x%02x' % (e.response.service_data.moop_echo, moop))    
            else:
                raise e

        if response.service_data.moop_echo != moop:
            raise UnexpectedResponseException(response, 'ModeOfOperation echo does not match request. Received 0x%02x, Requested=0x%02x' % (response.service_data.moop_echo, moop))

        if response.service_data.dfi is not None and dfi is not None:
            received = response.service_data.dfi.get_byte_as_int()
            expected = dfi.get_byte_as_int()
            if received != expected:
                raise UnexpectedResponseException(response, 'DataFormatIdentifier echo does not match request. Received 0x%02x, Requested=0x%02x' % (received, expected))

        return response

    # Basic transmission of requests. This will need to be improved
    def send_request(self, request, timeout=-1):
        if timeout < 0:
            # Timeout not provided by user: defaults to Client request_timeout value
            overall_timeout = self.config['request_timeout']
            p2 = self.config['p2_timeout'] if self.session_timing['p2_server_max'] is None else self.session_timing['p2_server_max']
            single_request_timeout = min(overall_timeout, p2)
        else:
            overall_timeout = timeout
            single_request_timeout = timeout

        using_p2_star = False	# Will switch to true when Nrc 0x78 will be received the first time.

        self.conn.empty_rxqueue()
        self.logger.debug("Sending request to server")
        override_suppress_positive_response = False
        if self.suppress_positive_response.enabled == True and request.service.use_subfunction():
            payload = request.get_payload(suppress_positive_response=True)
            override_suppress_positive_response = True
        else:
            payload = request.get_payload()

        if self.payload_override.enabled:
            payload = self.payload_override.get_overrided_payload(payload)

        if self.suppress_positive_response.enabled and not request.service.use_subfunction():
            self.logger.warning('SuppressPositiveResponse cannot be used for service %s. Ignoring' % (request.service.get_name()))

        self.conn.send(payload)

        if request.suppress_positive_response  or override_suppress_positive_response:
            return

        done_receiving = False
        overall_timeout_time = time.time() + overall_timeout
        while not done_receiving:
            done_receiving = True
            self.logger.debug("Waiting for server response")

            try:
                if time.time() + single_request_timeout < overall_timeout_time:	
                    timeout_type_used 	= 'single_request'
                    timeout_value 		= single_request_timeout
                else:	
                    timeout_type_used 	= 'overall'
                    timeout_value 		= max(overall_timeout_time - time.time(), 0)

                payload = self.conn.wait_frame(timeout=timeout_value, exception=True)	
            except TimeoutException:
                if timeout_type_used == 'single_request':
                    timeout_name_to_report = 'P2* timeout' if using_p2_star else 'P2 timeout'
                elif timeout_type_used == 'overall':
                    timeout_name_to_report = 'Global request timeout'
                else:	# Shouldn't go here.
                    timeout_name_to_report = 'Timeout'
                raise TimeoutException('Did not receive response in time. %s time has expired (timeout=%.3f sec)' % (timeout_name_to_report, timeout_value))
            except Exception as e:
                raise e

            response = Response.from_payload(payload)
            self.last_response = response
            self.logger.debug("Received response from server")

            if not response.valid:
                raise InvalidResponseException(response)

            if response.service.response_id() != request.service.response_id():
                msg = "Response gotten from server has a service ID different than the request service ID. Received=0x%02x, Expected=0x%02x" % (response.service.response_id() , request.service.response_id() )
                raise UnexpectedResponseException(response, msg)

            if not response.positive:
                if not request.service.is_supported_negative_response(response.code):
                    self.logger.warning('Given response code "%s" (0x%02x) is not a supported negative response code according to UDS standard.' % (response.code_name, response.code))

                if response.code == Response.Code.RequestCorrectlyReceived_ResponsePending:
                    done_receiving = False
                    if not using_p2_star:
                        # Received a 0x78 NRC: timeout is now set to P2*
                        p2_star = self.config['p2_star_timeout'] if self.session_timing['p2_star_server_max'] is None else self.session_timing['p2_star_server_max']
                        single_request_timeout = p2_star
                        using_p2_star = True
                        self.logger.debug("Server requested to wait with response code %s (0x%02x), single request timeout is now set to P2* (%.3f seconds)" % (response.code_name, response.code, single_request_timeout))
                else:
                    raise NegativeResponseException(response)

        self.logger.info('Received positive response for service %s (0x%02x) from server.' % (response.service.get_name(), response.service.request_id()))
        return response
