Skip to content

Commit

Permalink
Merge 67b9a4f into 02014a8
Browse files Browse the repository at this point in the history
  • Loading branch information
lutostag committed May 19, 2018
2 parents 02014a8 + 67b9a4f commit c610a11
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 29 deletions.
11 changes: 8 additions & 3 deletions tests/unit/client/serial/test_rtu.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import pytest
from serial import serial_for_url

from umodbus.client.serial.rtu import send_message
from umodbus.client.serial.rtu import send_message, read_coils


def test_send_message_with_timeout():
""" Test if TimoutError is raised when serial port doesn't receive data."""
""" Test if TimoutError is raised when serial port doesn't receive enough
data.
"""
s = serial_for_url('loop://', timeout=0)
# as we are using a loop, we need the request will be read back as response
# to test timeout we need a request with a response that needs more bytes
message = read_coils(slave_id=0, starting_address=1, quantity=40)

with pytest.raises(ValueError):
send_message(b'', s)
send_message(message, s)
20 changes: 11 additions & 9 deletions umodbus/client/serial/rtu.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@
import struct

from umodbus.client.serial.redundancy_check import get_crc, validate_crc
from umodbus.functions import (create_function_from_response_pdu, ReadCoils,
ReadDiscreteInputs, ReadHoldingRegisters,
ReadInputRegisters, WriteSingleCoil,
WriteSingleRegister, WriteMultipleCoils,
WriteMultipleRegisters)
from umodbus.functions import (create_function_from_response_pdu,
expected_response_pdu_size_from_request_pdu,
ReadCoils, ReadDiscreteInputs,
ReadHoldingRegisters, ReadInputRegisters,
WriteSingleCoil, WriteSingleRegister,
WriteMultipleCoils, WriteMultipleRegisters)
from umodbus.utils import recv_exactly


def _create_request_adu(slave_id, req_pdu):
Expand Down Expand Up @@ -191,10 +193,10 @@ def parse_response_adu(resp_adu, req_adu=None):

def send_message(adu, serial_port):
""" Send Modbus message over serial port and parse response. """
expected_response_size = \
expected_response_pdu_size_from_request_pdu(adu[1:-2]) + 3
serial_port.write(adu)
response = serial_port.read(serial_port.in_waiting)

if len(response) == 0:
raise ValueError
serial_port.flush()
response = recv_exactly(serial_port.read, expected_response_size)

return parse_response_adu(response, adu)
19 changes: 12 additions & 7 deletions umodbus/client/tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,13 @@
import struct
from random import randint

from umodbus.functions import (create_function_from_response_pdu, ReadCoils,
ReadDiscreteInputs, ReadHoldingRegisters,
ReadInputRegisters, WriteSingleCoil,
WriteSingleRegister, WriteMultipleCoils,
WriteMultipleRegisters)
from umodbus.functions import (create_function_from_response_pdu,
expected_response_pdu_size_from_request_pdu,
ReadCoils, ReadDiscreteInputs,
ReadHoldingRegisters, ReadInputRegisters,
WriteSingleCoil, WriteSingleRegister,
WriteMultipleCoils, WriteMultipleRegisters)
from umodbus.utils import recv_exactly


def _create_request_adu(slave_id, pdu):
Expand Down Expand Up @@ -241,6 +243,9 @@ def send_message(adu, sock):
:param sock: Socket instance.
:return: Parsed response from server.
"""
sock.send(adu)
response = sock.recv(1024)
sock.sendall(adu)
expected_response_size = \
expected_response_pdu_size_from_request_pdu(adu[7:]) + 7
response = recv_exactly(sock.recv, expected_response_size)

return parse_response_adu(response, adu)
83 changes: 79 additions & 4 deletions umodbus/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@
'b\\x06'
"""
from __future__ import division
import struct
import inspect
import math
try:
from functools import reduce
except ImportError:
Expand Down Expand Up @@ -136,6 +138,15 @@ def create_function_from_request_pdu(pdu):
return function_class.create_from_request_pdu(pdu)


def expected_response_pdu_size_from_request_pdu(pdu):
""" Return number of bytes expected for response PDU, based on request PDU.
:param pdu: Array of bytes.
:return: number of bytes.
"""
return create_function_from_request_pdu(pdu).expected_response_pdu_size


class ModbusFunction(object):
function_code = None

Expand Down Expand Up @@ -189,8 +200,8 @@ class ReadCoils(ModbusFunction):
The reponse PDU varies in length, depending on the request. Each 8 coils
require 1 byte. The amount of bytes needed represent status of the coils to
can be calculated with: bytes = round(quantity / 8) + 1. This response
contains (3 / 8 + 1) = 1 byte to describe the status of the coils. The
can be calculated with: bytes = ceil(quantity / 8). This response
contains ceil(3 / 8) = 1 byte to describe the status of the coils. The
structure of a compleet response PDU looks like this:
================ ===============
Expand Down Expand Up @@ -264,6 +275,14 @@ def create_from_request_pdu(pdu):

return instance

@property
def expected_response_pdu_size(self):
""" Return number of bytes expected for response PDU.
:return: number of bytes.
"""
return 2 + int(math.ceil(self.quantity / 8))

def create_response_pdu(self, data):
""" Create response pdu.
Expand Down Expand Up @@ -394,8 +413,8 @@ class ReadDiscreteInputs(ModbusFunction):
The reponse PDU varies in length, depending on the request. 8 inputs
require 1 byte. The amount of bytes needed represent status of the inputs
to can be calculated with: bytes = round(quantity / 8) + 1. This response
contains (3 / 8 + 1) = 1 byte to describe the status of the inputs. The
to can be calculated with: bytes = ceil(quantity / 8). This response
contains ceil(3 / 8) = 1 byte to describe the status of the inputs. The
structure of a compleet response PDU looks like this:
================ ===============
Expand Down Expand Up @@ -469,6 +488,14 @@ def create_from_request_pdu(pdu):

return instance

@property
def expected_response_pdu_size(self):
""" Return number of bytes expected for response PDU.
:return: number of bytes.
"""
return 2 + int(math.ceil(self.quantity / 8))

def create_response_pdu(self, data):
""" Create response pdu.
Expand Down Expand Up @@ -665,6 +692,14 @@ def create_from_request_pdu(pdu):

return instance

@property
def expected_response_pdu_size(self):
""" Return number of bytes expected for response PDU.
:return: number of bytes.
"""
return 2 + self.quantity * 2

def create_response_pdu(self, data):
""" Create response pdu.
Expand Down Expand Up @@ -837,6 +872,14 @@ def create_from_request_pdu(pdu):

return instance

@property
def expected_response_pdu_size(self):
""" Return number of bytes expected for response PDU.
:return: number of bytes.
"""
return 2 + self.quantity * 2

def create_response_pdu(self, data):
""" Create response pdu.
Expand Down Expand Up @@ -999,6 +1042,14 @@ def create_from_request_pdu(pdu):

return instance

@property
def expected_response_pdu_size(self):
""" Return number of bytes expected for response PDU.
:return: number of bytes.
"""
return 5

def create_response_pdu(self):
""" Create response pdu.
Expand Down Expand Up @@ -1141,6 +1192,14 @@ def create_from_request_pdu(pdu):

return instance

@property
def expected_response_pdu_size(self):
""" Return number of bytes expected for response PDU.
:return: number of bytes.
"""
return 5

def create_response_pdu(self):
fmt = '>BH' + conf.TYPE_CHAR
return struct.pack(fmt, self.function_code, self.address, self.value)
Expand Down Expand Up @@ -1347,6 +1406,14 @@ def create_from_request_pdu(pdu):

return instance

@property
def expected_response_pdu_size(self):
""" Return number of bytes expected for response PDU.
:return: number of bytes.
"""
return 5

def create_response_pdu(self):
""" Create response pdu.
Expand Down Expand Up @@ -1491,6 +1558,14 @@ def create_from_request_pdu(pdu):

return instance

@property
def expected_response_pdu_size(self):
""" Return number of bytes expected for response PDU.
:return: number of bytes.
"""
return 5

def create_response_pdu(self):
""" Create response pdu.
Expand Down
13 changes: 7 additions & 6 deletions umodbus/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from umodbus.functions import create_function_from_request_pdu
from umodbus.exceptions import ModbusError, ServerDeviceFailureError
from umodbus.utils import (get_function_code_from_request_pdu,
pack_exception_pdu)
pack_exception_pdu, recv_exactly)


def route(self, slave_ids=None, function_codes=None, addresses=None):
Expand Down Expand Up @@ -38,13 +38,14 @@ class AbstractRequestHandler(BaseRequestHandler):
def handle(self):
try:
while True:
request_adu = self.request.recv(1024)

# When client terminates connection length of request_adu is 0.
if len(request_adu) == 0:
try:
mbap_header = recv_exactly(self.request.recv, 7)
remaining = self.get_meta_data(mbap_header)['length'] - 1
request_pdu = recv_exactly(self.request.recv, remaining)
except ValueError:
return

response_adu = self.process(request_adu)
response_adu = self.process(mbap_header + request_pdu)
self.respond(response_adu)
except:
import traceback
Expand Down
29 changes: 29 additions & 0 deletions umodbus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,32 @@ def inner(arg):
cache[arg] = f(arg)
return cache[arg]
return inner


def recv_exactly(recv_fn, size):
""" Use the function to read and return exactly number of bytes desired.
https://docs.python.org/3/howto/sockets.html#socket-programming-howto for
more information about why this is necessary.
:param recv_fn: Function that can return up to given bytes
(i.e. socket.recv, file.read)
:param size: Number of bytes to read.
:return: Byte string with length size.
:raises ValueError: Could not receive enough data (usually timeout).
"""
recv_bytes = 0
chunks = []
while recv_bytes < size:
chunk = recv_fn(size - recv_bytes)
if len(chunk) == 0: # when closed or empty
break
recv_bytes += len(chunk)
chunks.append(chunk)

response = b''.join(chunks)

if len(response) != size:
raise ValueError

return response

0 comments on commit c610a11

Please sign in to comment.