Skip to content

Commit

Permalink
#5 Implement exceptions responses.
Browse files Browse the repository at this point in the history
  • Loading branch information
OrangeTux committed Nov 9, 2015
1 parent 51753df commit 4754c7a
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 25 deletions.
14 changes: 13 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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
23 changes: 14 additions & 9 deletions umodbus/exceptions.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -35,15 +40,15 @@ def __str__(self):
return self.__doc__


class ServerDeviceFailureError(Exception):
class ServerDeviceFailureError(ModbusError):
""" An unrecoverable error occurred. """
error_code = 4

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.
"""
Expand All @@ -53,15 +58,15 @@ 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

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.
Expand All @@ -72,15 +77,15 @@ def __repr__(self):
return self.__doc__


class GatewayPathUnavailableError(Exception):
class GatewayPathUnavailableError(ModbusError):
""" The gateway is probably misconfigured or overloaded. """
error_code = 11

def __repr__(self):
return self.__doc__


class GatewayTargetDeviceFailedToRespondError(Exception):
class GatewayTargetDeviceFailedToRespondError(ModbusError):
""" Didn't get a response from target device. """
error_code = 12

Expand Down
13 changes: 9 additions & 4 deletions umodbus/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
33 changes: 22 additions & 11 deletions umodbus/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)))
Expand Down
45 changes: 45 additions & 0 deletions umodbus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 4754c7a

Please sign in to comment.