diff --git a/tests/test_utils.py b/tests/test_utils.py index 7c196a3..b23121b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,5 @@ -from umodbus.utils import unpack_mbap, pack_mbap +from umodbus.utils import (unpack_mbap, pack_mbap, pack_exception_pdu, + get_function_code_from_request_pdu) def test_unpack_mbap(): @@ -13,3 +14,14 @@ def test_pack_mbap(): Protocol identifier, Length and Unit identifier. """ assert pack_mbap(8, 0, 6, 1) == b'\x00\x08\x00\x00\x00\x06\x01' + + +def test_pack_exception_pdu(): + """ Exception PDU should correct encoding of error code and function code. + """ + assert pack_exception_pdu(1, 1) == b'\x81\x01' + + +def test_get_function_code_from_request_pdu(): + """ Get correct function code from PDU. """ + assert get_function_code_from_request_pdu(b'\x01\x00d\x00\x03') == 1 diff --git a/umodbus/exceptions.py b/umodbus/exceptions.py index 227a39c..53922f6 100644 --- a/umodbus/exceptions.py +++ b/umodbus/exceptions.py @@ -1,4 +1,9 @@ -class IllegalFunctionError(Exception): +class ModbusError(Exception): + """ Base class for all Modbus related exception. """ + pass + + +class IllegalFunctionError(ModbusError): """ The function code received in the request is not an allowable action for the server. @@ -13,7 +18,7 @@ def __str__(self): 'server.'.format(self.function_code) -class IllegalDataAddressError(Exception): +class IllegalDataAddressError(ModbusError): """ The data address received in de request is not an allowable address for the server. @@ -24,7 +29,7 @@ def __str__(self): return self.__doc__ -class IllegalDataValueError(Exception): +class IllegalDataValueError(ModbusError): """ The value contained in the request data field is not an allowable value for the server. @@ -35,7 +40,7 @@ def __str__(self): return self.__doc__ -class ServerDeviceFailureError(Exception): +class ServerDeviceFailureError(ModbusError): """ An unrecoverable error occurred. """ error_code = 4 @@ -43,7 +48,7 @@ def __str__(self): return 'An unrecoverable error occurred.' -class AcknowledgeError(Exception): +class AcknowledgeError(ModbusError): """ The server has accepted the requests and it processing it, but a long duration of time will be required to do so. """ @@ -53,7 +58,7 @@ def __str__(self): return self.__doc__ -class ServerDeviceBusyError(Exception): +class ServerDeviceBusyError(ModbusError): """ The server is engaged in a long-duration program command. """ error_code = 6 @@ -61,7 +66,7 @@ def __str__(self): return self.__doc__ -class MemoryParityError(Exception): +class MemoryParityError(ModbusError): """ The server attempted to read record file, but detected a parity error in memory. @@ -72,7 +77,7 @@ def __repr__(self): return self.__doc__ -class GatewayPathUnavailableError(Exception): +class GatewayPathUnavailableError(ModbusError): """ The gateway is probably misconfigured or overloaded. """ error_code = 11 @@ -80,7 +85,7 @@ def __repr__(self): return self.__doc__ -class GatewayTargetDeviceFailedToRespondError(Exception): +class GatewayTargetDeviceFailedToRespondError(ModbusError): """ Didn't get a response from target device. """ error_code = 12 diff --git a/umodbus/functions.py b/umodbus/functions.py index 9fec117..ba30d72 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -3,8 +3,10 @@ from math import ceil from umodbus import log -from umodbus.utils import memoize, integer_to_binary_list -from umodbus.exceptions import IllegalDataValueError, IllegalDataAddressError +from umodbus.utils import (memoize, integer_to_binary_list, + get_function_code_from_request_pdu) +from umodbus.exceptions import (IllegalFunctionError, IllegalDataValueError, + IllegalDataAddressError) try: from functools import reduce @@ -44,8 +46,11 @@ def function_factory(pdu): :param pdu: Array of bytes. :return: Instance of a function. """ - function_code, = struct.unpack('>B', pdu[:1]) - function_class = function_code_to_function_map[function_code] + function_code = get_function_code_from_request_pdu(pdu) + try: + function_class = function_code_to_function_map[function_code] + except KeyError: + raise IllegalFunctionError(function_code) return function_class.create_from_request_pdu(pdu) diff --git a/umodbus/server.py b/umodbus/server.py index 9db4c20..e65c93e 100644 --- a/umodbus/server.py +++ b/umodbus/server.py @@ -6,8 +6,10 @@ from umodbus import log from umodbus.route import Map +from umodbus.utils import (unpack_mbap, pack_mbap, pack_exception_pdu, + get_function_code_from_request_pdu) from umodbus.functions import function_factory -from umodbus.utils import unpack_mbap, pack_mbap +from umodbus.exceptions import ModbusError def get_server(host, port): @@ -59,21 +61,30 @@ def handle(self): request_adu = self.request.recv(1024).strip() log.info('<-- {0} - {1}.'.format(self.client_address[0], hexlify(request_adu))) - transaction_id, protocol_id, _, unit_id = unpack_mbap(request_adu[:7]) - - function = function_factory(request_adu[7:]) - results = function.execute(unit_id, self.server.route_map) - try: - # ReadFunction's use results of callbacks to build response PDU... - response_pdu = function.create_response_pdu(results) - except TypeError: - # ...other functions don't. - response_pdu = function.create_response_pdu() + transaction_id, protocol_id, _, unit_id = \ + unpack_mbap(request_adu[:7]) + + function = function_factory(request_adu[7:]) + results = function.execute(unit_id, self.server.route_map) + + try: + # ReadFunction's use results of callbacks to build response + # PDU... + response_pdu = function.create_response_pdu(results) + except TypeError: + # ...other functions don't. + response_pdu = function.create_response_pdu() + except ModbusError as e: + function_code = get_function_code_from_request_pdu(request_adu[7:]) + response_pdu = pack_exception_pdu(function_code, e.error_code) response_mbap = pack_mbap(transaction_id, protocol_id, len(response_pdu) + 1, unit_id) + log.debug('Response MBAP {0}'.format(response_mbap)) + log.debug('Response PDU {0}'.format(response_pdu)) + response_adu = response_mbap + response_pdu log.info('--> {0} - {1}.'.format(self.client_address[0], hexlify(response_adu))) diff --git a/umodbus/utils.py b/umodbus/utils.py index 5528d4d..6cdca91 100644 --- a/umodbus/utils.py +++ b/umodbus/utils.py @@ -54,6 +54,51 @@ def pack_mbap(transaction_id, protocol_id, length, unit_id): return struct.pack('>HHHB', transaction_id, protocol_id, length, unit_id) +def get_function_code_from_request_pdu(pdu): + """ Return function code from request PDU. + + :return pdu: Array with bytes. + :return: Function code. + """ + return struct.unpack('>B', pdu[:1])[0] + + +def pack_exception_pdu(function_code, error_code): + """ Return exception PDU of 2 bytes. + + "The exception response message has two fields that differentiate it + from a nor mal response: Function Code Field: In a normal response, the + server echoes the function code of the original request in the function + code field of the response. All function codes have a most – significant + bit (MSB) of 0 (their values are all below 80 hexadecimal). In an + exception response, the server sets the MSB of the function code to 1. + This makes the function code value in an exception response exactly 80 + hexadecimal higher than the value would be for a normal response. + + With the function code’s MSB set, the client's application program can + recognize the exception response and can examine the data field for the + exception code. Data Field: In a normal response, the server may return + data or statistics in the data field (any information that was requested + in the request). In an exception response, the server returns an + exception code in the data field. This defines the server condition that + caused the exception." + + -- MODBUS Application Protocol Specification V1.1b3, chapter 7 + + ================ =============== + Field Length (bytes) + ================ =============== + Error code 1 + Function code 1 + ================ =============== + + :param error_code: Error code. + :param function_code: Function code. + :return: PDU of 2 bytes. + """ + return struct.pack('>BB', function_code + 0x80, error_code) + + def memoize(f): """ Decorator which caches function's return value each it is called. If called later with same arguments, the cached value is returned.