diff --git a/tests/unit/client/serial/test_rtu.py b/tests/unit/client/serial/test_rtu.py index d30c5f1..6ddd8e3 100644 --- a/tests/unit/client/serial/test_rtu.py +++ b/tests/unit/client/serial/test_rtu.py @@ -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) diff --git a/umodbus/client/serial/rtu.py b/umodbus/client/serial/rtu.py index 0b0780b..99581ea 100644 --- a/umodbus/client/serial/rtu.py +++ b/umodbus/client/serial/rtu.py @@ -42,14 +42,16 @@ 8 """ +import io 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) def _create_request_adu(slave_id, req_pdu): @@ -191,10 +193,14 @@ 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) + serial_port.flush() + bio = io.BufferedReader(serial_port, buffer_size=expected_response_size) + response = bio.read(expected_response_size) - if len(response) == 0: + if len(response) < expected_response_size: raise ValueError return parse_response_adu(response, adu) diff --git a/umodbus/client/tcp.py b/umodbus/client/tcp.py index 4298e35..f8fc625 100644 --- a/umodbus/client/tcp.py +++ b/umodbus/client/tcp.py @@ -82,14 +82,16 @@ byte) + PDU (5 bytes). """ +import io 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) def _create_request_adu(slave_id, pdu): @@ -241,6 +243,15 @@ def send_message(adu, sock): :param sock: Socket instance. :return: Parsed response from server. """ - sock.send(adu) - response = sock.recv(1024) + expected_response_size = \ + expected_response_pdu_size_from_request_pdu(adu[7:]) + 7 + fd = sock.makefile('rwb') + fd.write(adu) + fd.flush() + bio = io.BufferedReader(fd, buffer_size=expected_response_size) + response = bio.read(expected_response_size) + + if len(response) < expected_response_size: + raise ValueError + return parse_response_adu(response, adu) diff --git a/umodbus/functions.py b/umodbus/functions.py index ed77dc4..1ad6e06 100644 --- a/umodbus/functions.py +++ b/umodbus/functions.py @@ -57,6 +57,7 @@ """ import struct import inspect +import math try: from functools import reduce except ImportError: @@ -136,6 +137,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 @@ -189,8 +199,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: ================ =============== @@ -264,6 +274,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 + math.ceil(self.quantity / 8) + def create_response_pdu(self, data): """ Create response pdu. @@ -394,8 +412,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: ================ =============== @@ -469,6 +487,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 + math.ceil(self.quantity / 8) + def create_response_pdu(self, data): """ Create response pdu. @@ -665,6 +691,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. @@ -837,6 +871,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. @@ -999,6 +1041,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. @@ -1141,6 +1191,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) @@ -1347,6 +1405,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. @@ -1491,6 +1557,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.