Skip to content

Commit 96cbb94

Browse files
lutostagOrangeTux
authored andcommitted
Fix clients being to greedy when reading response
Fixes #49
1 parent 02014a8 commit 96cbb94

File tree

6 files changed

+197
-28
lines changed

6 files changed

+197
-28
lines changed

tests/unit/client/serial/test_rtu.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import pytest
22
from serial import serial_for_url
33

4-
from umodbus.client.serial.rtu import send_message
4+
from umodbus.client.serial.rtu import send_message, read_coils
55

66

77
def test_send_message_with_timeout():
8-
""" Test if TimoutError is raised when serial port doesn't receive data."""
8+
""" Test if TimoutError is raised when serial port doesn't receive enough
9+
data.
10+
"""
911
s = serial_for_url('loop://', timeout=0)
12+
# As we are using a loop, the sent request will be read back as response.
13+
# To test timeout use a request that needs more bytes for the response.
14+
message = read_coils(slave_id=0, starting_address=1, quantity=40)
1015

1116
with pytest.raises(ValueError):
12-
send_message(b'', s)
17+
send_message(message, s)

umodbus/client/serial/rtu.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@
4545
import struct
4646

4747
from umodbus.client.serial.redundancy_check import get_crc, validate_crc
48-
from umodbus.functions import (create_function_from_response_pdu, ReadCoils,
48+
from umodbus.functions import (create_function_from_response_pdu,
49+
expected_response_pdu_size_from_request_pdu,
50+
pdu_to_function_code_or_raise_error, ReadCoils,
4951
ReadDiscreteInputs, ReadHoldingRegisters,
5052
ReadInputRegisters, WriteSingleCoil,
5153
WriteSingleRegister, WriteMultipleCoils,
5254
WriteMultipleRegisters)
55+
from umodbus.utils import recv_exactly
5356

5457

5558
def _create_request_adu(slave_id, req_pdu):
@@ -189,12 +192,34 @@ def parse_response_adu(resp_adu, req_adu=None):
189192
return function.data
190193

191194

195+
def raise_for_exception_adu(resp_adu):
196+
""" Check a response ADU for error
197+
198+
:param resp_adu: Response ADU.
199+
:raises ModbusError: When a response contains an error code.
200+
"""
201+
resp_pdu = resp_adu[1:-2]
202+
pdu_to_function_code_or_raise_error(resp_pdu)
203+
204+
192205
def send_message(adu, serial_port):
193-
""" Send Modbus message over serial port and parse response. """
206+
""" Send ADU over serial to to server and return parsed response.
207+
208+
:param adu: Request ADU.
209+
:param sock: Serial port instance.
210+
:return: Parsed response from server.
211+
"""
194212
serial_port.write(adu)
195-
response = serial_port.read(serial_port.in_waiting)
213+
serial_port.flush()
214+
215+
# Check exception ADU (which is shorter than all other responses) first.
216+
exception_adu_size = 5
217+
response_error_adu = recv_exactly(serial_port.read, exception_adu_size)
218+
raise_for_exception_adu(response_error_adu)
196219

197-
if len(response) == 0:
198-
raise ValueError
220+
expected_response_size = \
221+
expected_response_pdu_size_from_request_pdu(adu[1:-2]) + 3
222+
response_remainder = recv_exactly(
223+
serial_port.read, expected_response_size - exception_adu_size)
199224

200-
return parse_response_adu(response, adu)
225+
return parse_response_adu(response_error_adu + response_remainder, adu)

umodbus/client/tcp.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,14 @@
8585
import struct
8686
from random import randint
8787

88-
from umodbus.functions import (create_function_from_response_pdu, ReadCoils,
88+
from umodbus.functions import (create_function_from_response_pdu,
89+
expected_response_pdu_size_from_request_pdu,
90+
pdu_to_function_code_or_raise_error, ReadCoils,
8991
ReadDiscreteInputs, ReadHoldingRegisters,
9092
ReadInputRegisters, WriteSingleCoil,
9193
WriteSingleRegister, WriteMultipleCoils,
9294
WriteMultipleRegisters)
95+
from umodbus.utils import recv_exactly
9396

9497

9598
def _create_request_adu(slave_id, pdu):
@@ -234,13 +237,33 @@ def parse_response_adu(resp_adu, req_adu=None):
234237
return function.data
235238

236239

240+
def raise_for_exception_adu(resp_adu):
241+
""" Check a response ADU for error
242+
243+
:param resp_adu: Response ADU.
244+
:raises ModbusError: When a response contains an error code.
245+
"""
246+
resp_pdu = resp_adu[7:]
247+
pdu_to_function_code_or_raise_error(resp_pdu)
248+
249+
237250
def send_message(adu, sock):
238251
""" Send ADU over socket to to server and return parsed response.
239252
240253
:param adu: Request ADU.
241254
:param sock: Socket instance.
242255
:return: Parsed response from server.
243256
"""
244-
sock.send(adu)
245-
response = sock.recv(1024)
246-
return parse_response_adu(response, adu)
257+
sock.sendall(adu)
258+
259+
# Check exception ADU (which is shorter than all other responses) first.
260+
exception_adu_size = 9
261+
response_error_adu = recv_exactly(sock.recv, exception_adu_size)
262+
raise_for_exception_adu(response_error_adu)
263+
264+
expected_response_size = \
265+
expected_response_pdu_size_from_request_pdu(adu[7:]) + 7
266+
response_remainder = recv_exactly(
267+
sock.recv, expected_response_size - exception_adu_size)
268+
269+
return parse_response_adu(response_error_adu + response_remainder, adu)

umodbus/functions.py

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@
5555
'b\\x06'
5656
5757
"""
58+
from __future__ import division
5859
import struct
5960
import inspect
61+
import math
6062
try:
6163
from functools import reduce
6264
except ImportError:
@@ -94,14 +96,12 @@
9496
REPORT_SERVER_ID = 17
9597

9698

97-
def create_function_from_response_pdu(resp_pdu, req_pdu=None):
98-
""" Parse response PDU and return instance of :class:`ModbusFunction` or
99+
def pdu_to_function_code_or_raise_error(resp_pdu):
100+
""" Parse response PDU and return of :class:`ModbusFunction` or
99101
raise error.
100102
101103
:param resp_pdu: PDU of response.
102-
:param req_pdu: Request PDU, some functions require more info than in
103-
response PDU in order to create instance. Default is None.
104-
:return: Number or list with response data.
104+
:return: Subclass of :class:`ModbusFunction` matching the response.
105105
:raises ModbusError: When response contains error code.
106106
"""
107107
function_code = struct.unpack('>B', resp_pdu[0:1])[0]
@@ -110,6 +110,19 @@ def create_function_from_response_pdu(resp_pdu, req_pdu=None):
110110
error_code = struct.unpack('>B', resp_pdu[1:2])[0]
111111
raise error_code_to_exception_map[error_code]
112112

113+
return function_code
114+
115+
116+
def create_function_from_response_pdu(resp_pdu, req_pdu=None):
117+
""" Parse response PDU and return instance of :class:`ModbusFunction` or
118+
raise error.
119+
120+
:param resp_pdu: PDU of response.
121+
:param req_pdu: Request PDU, some functions require more info than in
122+
response PDU in order to create instance. Default is None.
123+
:return: Number or list with response data.
124+
"""
125+
function_code = pdu_to_function_code_or_raise_error(resp_pdu)
113126
function = function_code_to_function_map[function_code]
114127

115128
if req_pdu is not None and \
@@ -136,6 +149,15 @@ def create_function_from_request_pdu(pdu):
136149
return function_class.create_from_request_pdu(pdu)
137150

138151

152+
def expected_response_pdu_size_from_request_pdu(pdu):
153+
""" Return number of bytes expected for response PDU, based on request PDU.
154+
155+
:param pdu: Array of bytes.
156+
:return: number of bytes.
157+
"""
158+
return create_function_from_request_pdu(pdu).expected_response_pdu_size
159+
160+
139161
class ModbusFunction(object):
140162
function_code = None
141163

@@ -189,8 +211,8 @@ class ReadCoils(ModbusFunction):
189211
190212
The reponse PDU varies in length, depending on the request. Each 8 coils
191213
require 1 byte. The amount of bytes needed represent status of the coils to
192-
can be calculated with: bytes = round(quantity / 8) + 1. This response
193-
contains (3 / 8 + 1) = 1 byte to describe the status of the coils. The
214+
can be calculated with: bytes = ceil(quantity / 8). This response
215+
contains ceil(3 / 8) = 1 byte to describe the status of the coils. The
194216
structure of a compleet response PDU looks like this:
195217
196218
================ ===============
@@ -264,6 +286,14 @@ def create_from_request_pdu(pdu):
264286

265287
return instance
266288

289+
@property
290+
def expected_response_pdu_size(self):
291+
""" Return number of bytes expected for response PDU.
292+
293+
:return: number of bytes.
294+
"""
295+
return 2 + int(math.ceil(self.quantity / 8))
296+
267297
def create_response_pdu(self, data):
268298
""" Create response pdu.
269299
@@ -394,8 +424,8 @@ class ReadDiscreteInputs(ModbusFunction):
394424
395425
The reponse PDU varies in length, depending on the request. 8 inputs
396426
require 1 byte. The amount of bytes needed represent status of the inputs
397-
to can be calculated with: bytes = round(quantity / 8) + 1. This response
398-
contains (3 / 8 + 1) = 1 byte to describe the status of the inputs. The
427+
to can be calculated with: bytes = ceil(quantity / 8). This response
428+
contains ceil(3 / 8) = 1 byte to describe the status of the inputs. The
399429
structure of a compleet response PDU looks like this:
400430
401431
================ ===============
@@ -469,6 +499,14 @@ def create_from_request_pdu(pdu):
469499

470500
return instance
471501

502+
@property
503+
def expected_response_pdu_size(self):
504+
""" Return number of bytes expected for response PDU.
505+
506+
:return: number of bytes.
507+
"""
508+
return 2 + int(math.ceil(self.quantity / 8))
509+
472510
def create_response_pdu(self, data):
473511
""" Create response pdu.
474512
@@ -665,6 +703,14 @@ def create_from_request_pdu(pdu):
665703

666704
return instance
667705

706+
@property
707+
def expected_response_pdu_size(self):
708+
""" Return number of bytes expected for response PDU.
709+
710+
:return: number of bytes.
711+
"""
712+
return 2 + self.quantity * 2
713+
668714
def create_response_pdu(self, data):
669715
""" Create response pdu.
670716
@@ -837,6 +883,14 @@ def create_from_request_pdu(pdu):
837883

838884
return instance
839885

886+
@property
887+
def expected_response_pdu_size(self):
888+
""" Return number of bytes expected for response PDU.
889+
890+
:return: number of bytes.
891+
"""
892+
return 2 + self.quantity * 2
893+
840894
def create_response_pdu(self, data):
841895
""" Create response pdu.
842896
@@ -999,6 +1053,14 @@ def create_from_request_pdu(pdu):
9991053

10001054
return instance
10011055

1056+
@property
1057+
def expected_response_pdu_size(self):
1058+
""" Return number of bytes expected for response PDU.
1059+
1060+
:return: number of bytes.
1061+
"""
1062+
return 5
1063+
10021064
def create_response_pdu(self):
10031065
""" Create response pdu.
10041066
@@ -1141,6 +1203,14 @@ def create_from_request_pdu(pdu):
11411203

11421204
return instance
11431205

1206+
@property
1207+
def expected_response_pdu_size(self):
1208+
""" Return number of bytes expected for response PDU.
1209+
1210+
:return: number of bytes.
1211+
"""
1212+
return 5
1213+
11441214
def create_response_pdu(self):
11451215
fmt = '>BH' + conf.TYPE_CHAR
11461216
return struct.pack(fmt, self.function_code, self.address, self.value)
@@ -1347,6 +1417,14 @@ def create_from_request_pdu(pdu):
13471417

13481418
return instance
13491419

1420+
@property
1421+
def expected_response_pdu_size(self):
1422+
""" Return number of bytes expected for response PDU.
1423+
1424+
:return: number of bytes.
1425+
"""
1426+
return 5
1427+
13501428
def create_response_pdu(self):
13511429
""" Create response pdu.
13521430
@@ -1491,6 +1569,14 @@ def create_from_request_pdu(pdu):
14911569

14921570
return instance
14931571

1572+
@property
1573+
def expected_response_pdu_size(self):
1574+
""" Return number of bytes expected for response PDU.
1575+
1576+
:return: number of bytes.
1577+
"""
1578+
return 5
1579+
14941580
def create_response_pdu(self):
14951581
""" Create response pdu.
14961582

umodbus/server/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from umodbus.functions import create_function_from_request_pdu
99
from umodbus.exceptions import ModbusError, ServerDeviceFailureError
1010
from umodbus.utils import (get_function_code_from_request_pdu,
11-
pack_exception_pdu)
11+
pack_exception_pdu, recv_exactly)
1212

1313

1414
def route(self, slave_ids=None, function_codes=None, addresses=None):
@@ -38,13 +38,14 @@ class AbstractRequestHandler(BaseRequestHandler):
3838
def handle(self):
3939
try:
4040
while True:
41-
request_adu = self.request.recv(1024)
42-
43-
# When client terminates connection length of request_adu is 0.
44-
if len(request_adu) == 0:
41+
try:
42+
mbap_header = recv_exactly(self.request.recv, 7)
43+
remaining = self.get_meta_data(mbap_header)['length'] - 1
44+
request_pdu = recv_exactly(self.request.recv, remaining)
45+
except ValueError:
4546
return
4647

47-
response_adu = self.process(request_adu)
48+
response_adu = self.process(mbap_header + request_pdu)
4849
self.respond(response_adu)
4950
except:
5051
import traceback

umodbus/utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,32 @@ def inner(arg):
112112
cache[arg] = f(arg)
113113
return cache[arg]
114114
return inner
115+
116+
117+
def recv_exactly(recv_fn, size):
118+
""" Use the function to read and return exactly number of bytes desired.
119+
120+
https://docs.python.org/3/howto/sockets.html#socket-programming-howto for
121+
more information about why this is necessary.
122+
123+
:param recv_fn: Function that can return up to given bytes
124+
(i.e. socket.recv, file.read)
125+
:param size: Number of bytes to read.
126+
:return: Byte string with length size.
127+
:raises ValueError: Could not receive enough data (usually timeout).
128+
"""
129+
recv_bytes = 0
130+
chunks = []
131+
while recv_bytes < size:
132+
chunk = recv_fn(size - recv_bytes)
133+
if len(chunk) == 0: # when closed or empty
134+
break
135+
recv_bytes += len(chunk)
136+
chunks.append(chunk)
137+
138+
response = b''.join(chunks)
139+
140+
if len(response) != size:
141+
raise ValueError
142+
143+
return response

0 commit comments

Comments
 (0)