Skip to content

Commit f84d374

Browse files
author
Siarhei Farbotka
committed
Properly process incomplete RTU frames, improve test coverage
1 parent 7e8e7cf commit f84d374

File tree

2 files changed

+89
-45
lines changed

2 files changed

+89
-45
lines changed

pymodbus/framer/rtu_framer.py

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def __init__(self, decoder, client=None):
6060
:param decoder: The decoder factory implementation to use
6161
"""
6262
self._buffer = b''
63-
self._header = {'uid': 0x00, 'len': 0, 'crc': '0000'}
63+
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
6464
self._hsize = 0x01
6565
self._end = b'\x0d\x0a'
6666
self._min_frame_size = 4
@@ -89,14 +89,9 @@ def checkFrame(self):
8989
self.populateHeader()
9090
frame_size = self._header['len']
9191
data = self._buffer[:frame_size - 2]
92-
crc = self._buffer[frame_size - 2:frame_size]
92+
crc = self._header['crc']
9393
crc_val = (byte2int(crc[0]) << 8) + byte2int(crc[1])
94-
if checkCRC(data, crc_val):
95-
return True
96-
else:
97-
_logger.debug("CRC invalid, discarding header!!")
98-
self.resetFrame()
99-
return False
94+
return checkCRC(data, crc_val)
10095
except (IndexError, KeyError, struct.error):
10196
return False
10297

@@ -107,13 +102,10 @@ def advanceFrame(self):
107102
it or determined that it contains an error. It also has to reset the
108103
current frame header handle
109104
"""
110-
try:
111-
self._buffer = self._buffer[self._header['len']:]
112-
except KeyError:
113-
# Error response, no header len found
114-
self.resetFrame()
105+
106+
self._buffer = self._buffer[self._header['len']:]
115107
_logger.debug("Frame advanced, resetting header!!")
116-
self._header = {}
108+
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
117109

118110
def resetFrame(self):
119111
"""
@@ -127,7 +119,7 @@ def resetFrame(self):
127119
_logger.debug("Resetting frame - Current Frame in "
128120
"buffer - {}".format(hexlify_packets(self._buffer)))
129121
self._buffer = b''
130-
self._header = {}
122+
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
131123

132124
def isFrameReady(self):
133125
"""
@@ -137,31 +129,38 @@ def isFrameReady(self):
137129
138130
:returns: True if ready, False otherwise
139131
"""
140-
if len(self._buffer) > self._hsize:
141-
if not self._header:
142-
self.populateHeader()
132+
if len(self._buffer) <= self._hsize:
133+
return False
143134

144-
return self._header and len(self._buffer) >= self._header['len']
145-
else:
135+
try:
136+
# Frame is ready only if populateHeader() successfully populates crc field which finishes RTU frame
137+
# Otherwise, if buffer is not yet long enough, populateHeader() raises IndexError
138+
self.populateHeader()
139+
except IndexError:
146140
return False
147141

142+
return True
143+
148144
def populateHeader(self, data=None):
149145
"""
150146
Try to set the headers `uid`, `len` and `crc`.
151147
152148
This method examines `self._buffer` and writes meta
153-
information into `self._header`. It calculates only the
154-
values for headers that are not already in the dictionary.
149+
information into `self._header`.
155150
156151
Beware that this method will raise an IndexError if
157152
`self._buffer` is not yet long enough.
158153
"""
159-
data = data if data else self._buffer
154+
data = data if data is not None else self._buffer
160155
self._header['uid'] = byte2int(data[0])
161156
func_code = byte2int(data[1])
162157
pdu_class = self.decoder.lookupPduClass(func_code)
163158
size = pdu_class.calculateRtuFrameSize(data)
164159
self._header['len'] = size
160+
161+
if len(data) < size:
162+
# crc yet not available
163+
raise IndexError
165164
self._header['crc'] = data[size - 2:size]
166165

167166
def addToFrame(self, message):

test/test_framers.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pymodbus.exceptions import ModbusIOException
99
from pymodbus.compat import IS_PYTHON3
1010
if IS_PYTHON3:
11-
from unittest.mock import Mock
11+
from unittest.mock import Mock, patch
1212
else: # Python 2
1313
from mock import Mock
1414

@@ -44,7 +44,7 @@ def test_framer_initialization(framer):
4444
assert framer._start == b':'
4545
assert framer._end == b"\r\n"
4646
elif isinstance(framer, ModbusRtuFramer):
47-
assert framer._header == {'uid': 0x00, 'len': 0, 'crc': '0000'}
47+
assert framer._header == {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
4848
assert framer._hsize == 0x01
4949
assert framer._end == b'\x0d\x0a'
5050
assert framer._min_frame_size == 4
@@ -64,47 +64,78 @@ def test_decode_data(rtu_framer, data):
6464
assert decoded == expected
6565

6666

67-
@pytest.mark.parametrize("data", [(b'', False),
68-
(b'\x02\x01\x01\x00Q\xcc', True)])
67+
@pytest.mark.parametrize("data", [
68+
(b'', False),
69+
(b'\x02\x01\x01\x00Q\xcc', True),
70+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD', True), # valid frame
71+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAC', False), # invalid frame CRC
72+
])
6973
def test_check_frame(rtu_framer, data):
7074
data, expected = data
7175
rtu_framer._buffer = data
7276
assert expected == rtu_framer.checkFrame()
7377

7478

75-
@pytest.mark.parametrize("data", [b'', b'abcd'])
76-
def test_advance_framer(rtu_framer, data):
77-
rtu_framer._buffer = data
79+
@pytest.mark.parametrize("data", [
80+
(b'', {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}, b''),
81+
(b'abcd', {'uid': 0x00, 'len': 2, 'crc': b'\x00\x00'}, b'cd'),
82+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x12\x03', # real case, frame size is 11
83+
{'uid': 0x00, 'len': 11, 'crc': b'\x00\x00'}, b'\x12\x03'),
84+
])
85+
def test_rtu_advance_framer(rtu_framer, data):
86+
before_buf, before_header, after_buf = data
87+
88+
rtu_framer._buffer = before_buf
89+
rtu_framer._header = before_header
7890
rtu_framer.advanceFrame()
79-
assert rtu_framer._header == {}
80-
assert rtu_framer._buffer == data
91+
assert rtu_framer._header == {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
92+
assert rtu_framer._buffer == after_buf
8193

8294

8395
@pytest.mark.parametrize("data", [b'', b'abcd'])
84-
def test_reset_framer(rtu_framer, data):
96+
def test_rtu_reset_framer(rtu_framer, data):
8597
rtu_framer._buffer = data
8698
rtu_framer.resetFrame()
87-
assert rtu_framer._header == {}
99+
assert rtu_framer._header == {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
88100
assert rtu_framer._buffer == b''
89101

90102

91103
@pytest.mark.parametrize("data", [
92104
(b'', False),
105+
(b'\x11', False),
106+
(b'\x11\x03', False),
93107
(b'\x11\x03\x06', False),
94108
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49', False),
95109
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD', True),
96-
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\xAB\xCD', True)
110+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\xAB\xCD', True),
97111
])
98112
def test_is_frame_ready(rtu_framer, data):
99113
data, expected = data
100114
rtu_framer._buffer = data
101-
rtu_framer.advanceFrame()
115+
# rtu_framer.advanceFrame()
102116
assert rtu_framer.isFrameReady() == expected
103117

104118

105-
def test_populate_header(rtu_framer):
106-
rtu_framer.populateHeader(b'abcd')
107-
assert rtu_framer._header == {'crc': b'd', 'uid': 97, 'len': 5}
119+
@pytest.mark.parametrize("data", [
120+
b'',
121+
b'\x11',
122+
b'\x11\x03',
123+
b'\x11\x03\x06',
124+
b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x43',
125+
])
126+
def test_rtu_populate_header_fail(rtu_framer, data):
127+
with pytest.raises(IndexError):
128+
rtu_framer.populateHeader(data)
129+
130+
131+
@pytest.mark.parametrize("data", [
132+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD', {'crc': b'\x49\xAD', 'uid': 17, 'len': 11}),
133+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03', {'crc': b'\x49\xAD', 'uid': 17, 'len': 11})
134+
])
135+
def test_rtu_populate_header(rtu_framer, data):
136+
buffer, expected = data
137+
rtu_framer.populateHeader(buffer)
138+
assert rtu_framer._header == expected
108139

109140

110141
def test_add_to_frame(rtu_framer):
@@ -126,12 +157,26 @@ def test_populate_result(rtu_framer):
126157
assert result.unit_id == 255
127158

128159

129-
@pytest.mark.parametrize('framer', [ascii_framer, rtu_framer, binary_framer])
130-
def test_process_incoming_packet(framer):
131-
def cb(res):
132-
return res
133-
# data = b''
134-
# framer.processIncomingPacket(data, cb, unit=1, single=False)
160+
@pytest.mark.parametrize("data", [
161+
(b'\x11', 17, False, False), # not complete frame
162+
(b'\x11\x03', 17, False, False), # not complete frame
163+
(b'\x11\x03\x06', 17, False, False), # not complete frame
164+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43', 17, False, False), # not complete frame
165+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40', 17, False, False), # not complete frame
166+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49', 17, False, False), # not complete frame
167+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAC', 17, True, False), # bad crc
168+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD', 17, False, True), # good frame
169+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD', 16, True, False), # incorrect unit id
170+
(b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03', 17, False, True), # good frame + part of next frame
171+
])
172+
def test_rtu_process_incoming_packet(rtu_framer, data):
173+
buffer, units, reset_called, process_called = data
174+
175+
with patch.object(rtu_framer, '_process') as mock_process, \
176+
patch.object(rtu_framer, 'resetFrame') as mock_reset:
177+
rtu_framer.processIncomingPacket(buffer, Mock(), units)
178+
assert mock_process.call_count == (1 if process_called else 0)
179+
assert mock_reset.call_count == (1 if reset_called else 0)
135180

136181

137182
def test_build_packet(rtu_framer):

0 commit comments

Comments
 (0)