Skip to content

Commit 58947b1

Browse files
committed
Framer optimization (apart from RTU).
1 parent ae415ad commit 58947b1

File tree

9 files changed

+371
-313
lines changed

9 files changed

+371
-313
lines changed

pymodbus/framer/ascii.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,29 @@ class FramerAscii(FramerBase):
3434

3535
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
3636
"""Decode ADU."""
37-
if (used_len := len(data)) < self.MIN_SIZE:
38-
Log.debug("Short frame: {} wait for more data", data, ":hex")
39-
return 0, 0, 0, self.EMPTY
40-
if data[0:1] != self.START:
41-
if (start := data.find(self.START)) != -1:
42-
used_len = start
43-
Log.debug("Garble data before frame: {}, skip until start of frame", data, ":hex")
44-
return used_len, 0, 0, self.EMPTY
45-
if (used_len := data.find(self.END)) == -1:
46-
Log.debug("Incomplete frame: {} wait for more data", data, ":hex")
47-
return 0, 0, 0, self.EMPTY
48-
49-
dev_id = int(data[1:3], 16)
50-
lrc = int(data[used_len - 2: used_len], 16)
51-
msg = a2b_hex(data[1 : used_len - 2])
52-
if not self.check_LRC(msg, lrc):
53-
Log.debug("LRC wrong in frame: {} skipping", data, ":hex")
54-
return used_len+2, 0, 0, self.EMPTY
55-
return used_len+2, 0, dev_id, msg[1:]
37+
buf_len = len(data)
38+
used_len = 0
39+
while True:
40+
if buf_len - used_len < self.MIN_SIZE:
41+
return used_len, 0, 0, self.EMPTY
42+
buffer = data[used_len:]
43+
if buffer[0:1] != self.START:
44+
if (i := buffer.find(self.START)) == -1:
45+
Log.debug("No frame start in data: {}, wait for data", data, ":hex")
46+
return buf_len, 0, 0, self.EMPTY
47+
used_len += i
48+
continue
49+
if (end := buffer.find(self.END)) == -1:
50+
Log.debug("Incomplete frame: {} wait for more data", data, ":hex")
51+
return used_len, 0, 0, self.EMPTY
52+
dev_id = int(buffer[1:3], 16)
53+
lrc = int(buffer[end - 2: end], 16)
54+
msg = a2b_hex(buffer[1 : end - 2])
55+
used_len += end + 2
56+
if not self.check_LRC(msg, lrc):
57+
Log.debug("LRC wrong in frame: {} skipping", data, ":hex")
58+
continue
59+
return used_len, dev_id, dev_id, msg[1:]
5660

5761
def encode(self, data: bytes, device_id: int, _tid: int) -> bytes:
5862
"""Encode ADU."""

pymodbus/framer/base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"""
77
from __future__ import annotations
88

9+
from abc import abstractmethod
10+
911

1012
class FramerBase:
1113
"""Intern base."""
@@ -15,6 +17,13 @@ class FramerBase:
1517
def __init__(self) -> None:
1618
"""Initialize a ADU instance."""
1719

20+
def set_dev_ids(self, _dev_ids: list[int]):
21+
"""Set/update allowed device ids."""
22+
23+
def set_fc_calc(self, _fc: int, _msg_size: int, _count_pos: int):
24+
"""Set/Update function code information."""
25+
26+
@abstractmethod
1827
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
1928
"""Decode ADU.
2029
@@ -24,12 +33,11 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
2433
device_id (int) or 0
2534
modbus request/response (bytes)
2635
"""
27-
raise RuntimeError("NOT IMPLEMENTED!")
2836

37+
@abstractmethod
2938
def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes:
3039
"""Encode ADU.
3140
3241
returns:
3342
modbus ADU (bytes)
3443
"""
35-
raise RuntimeError("NOT IMPLEMENTED!")

pymodbus/framer/framer.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,37 +66,31 @@ def __init__(self,
6666
super().__init__(params, is_server)
6767
self.device_ids = device_ids
6868
self.broadcast: bool = (0 in device_ids)
69-
self.handle = {
70-
FramerType.RAW: FramerRaw,
71-
FramerType.ASCII: FramerAscii,
72-
FramerType.RTU: FramerRTU,
73-
FramerType.SOCKET: FramerSocket,
74-
FramerType.TLS: FramerTLS,
75-
}[framer_type]()
76-
7769

78-
def validate_device_id(self, dev_id: int) -> bool:
79-
"""Check if device id is expected."""
80-
return self.broadcast or (dev_id in self.device_ids)
70+
self.handle = {
71+
FramerType.RAW: FramerRaw(),
72+
FramerType.ASCII: FramerAscii(),
73+
FramerType.RTU: FramerRTU(),
74+
FramerType.SOCKET: FramerSocket(),
75+
FramerType.TLS: FramerTLS(),
76+
}[framer_type]
8177

8278

8379
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
8480
"""Handle received data."""
85-
tot_len = len(data)
86-
start = 0
81+
tot_len = 0
82+
buf_len = len(data)
8783
while True:
88-
used_len, tid, device_id, msg = self.handle.decode(data[start:])
84+
used_len, tid, device_id, msg = self.handle.decode(data[tot_len:])
85+
tot_len += used_len
8986
if msg:
90-
self.callback_request_response(msg, device_id, tid)
91-
if not used_len:
92-
return start
93-
start += used_len
94-
if start == tot_len:
87+
if self.broadcast or device_id in self.device_ids:
88+
self.callback_request_response(msg, device_id, tid)
89+
if tot_len == buf_len:
90+
return tot_len
91+
else:
9592
return tot_len
9693

97-
# --------------------- #
98-
# callbacks and helpers #
99-
# --------------------- #
10094
@abstractmethod
10195
def callback_request_response(self, data: bytes, device_id: int, tid: int) -> None:
10296
"""Handle received modbus request/response."""

pymodbus/framer/raw.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
2727
tid = int(data[1])
2828
return len(data), dev_id, tid, data[2:]
2929

30-
def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes:
30+
def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes:
3131
"""Encode ADU."""
32-
return device_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu
32+
return dev_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu

pymodbus/framer/rtu.py

Lines changed: 69 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
"""Modbus RTU frame implementation."""
22
from __future__ import annotations
33

4-
import struct
4+
from collections import namedtuple
55

6-
from pymodbus.exceptions import ModbusIOException
7-
from pymodbus.factory import ClientDecoder
86
from pymodbus.framer.base import FramerBase
97
from pymodbus.logging import Log
108

@@ -17,26 +15,53 @@ class FramerRTU(FramerBase):
1715
1816
* Note: due to the USB converter and the OS drivers, timing cannot be quaranteed
1917
neither when receiving nor when sending.
18+
19+
Decoding is a complicated process because the RTU frame does not have a fixed prefix
20+
only suffix, therefore it is necessary to decode the content (PDU) to get length etc.
21+
22+
There are some protocol restrictions that help with the detection.
23+
24+
For client:
25+
- a request causes 1 response !
26+
- Multiple requests are NOT allowed (master-slave protocol)
27+
- the server will not retransmit responses
28+
this means decoding is always exactly 1 frame (response)
29+
30+
For server (Single device)
31+
- only 1 request allowed (master-slave) protocol
32+
- the client (master) may retransmit but in larger time intervals
33+
this means decoding is always exactly 1 frame (request)
34+
35+
For server (Multidrop line --> devices in parallel)
36+
- only 1 request allowed (master-slave) protocol
37+
- other devices will send responses
38+
- the client (master) may retransmit but in larger time intervals
39+
this means decoding is always exactly 1 frame request, however some requests
40+
will be for unknown slaves, which must be ignored together with the
41+
response from the unknown slave.
42+
43+
Recovery from bad cabling and unstable USB etc is important,
44+
the following scenarios is possible:
45+
- garble data before frame
46+
- garble data in frame
47+
- garble data after frame
48+
- data in frame garbled (wrong CRC)
49+
decoding assumes the frame is sound, and if not enters a hunting mode.
50+
51+
The 3.5 byte transmission time at the slowest speed 1.200Bps is 31ms.
52+
Device drivers will typically flush buffer after 10ms of silence.
53+
If no data is received for 50ms the transmission / frame can be considered
54+
complete.
2055
"""
2156

2257
MIN_SIZE = 5
2358

59+
FC_LEN = namedtuple("FC_LEN", "req_len req_bytepos resp_len resp_bytepos")
60+
2461
def __init__(self) -> None:
2562
"""Initialize a ADU instance."""
2663
super().__init__()
27-
self.broadcast: bool = False
28-
self.dev_ids: list[int]
29-
self.fc_calc: dict[int, int]
30-
31-
def set_dev_ids(self, dev_ids: list[int]):
32-
"""Set/update allowed device ids."""
33-
if 0 in dev_ids:
34-
self.broadcast = True
35-
self.dev_ids = dev_ids
36-
37-
def set_fc_calc(self, fc: int, msg_size: int, count_pos: int):
38-
"""Set/Update function code information."""
39-
self.fc_calc[fc] = msg_size if not count_pos else -count_pos
64+
self.fc_len: dict[int, FramerRTU.FC_LEN] = {}
4065

4166

4267
@classmethod
@@ -58,120 +83,39 @@ def generate_crc16_table(cls) -> list[int]:
5883
return result
5984
crc16_table: list[int] = [0]
6085

61-
def _legacy_decode(self, callback, slave): # noqa: C901
62-
"""Process new packet pattern."""
63-
64-
def is_frame_ready(self):
65-
"""Check if we should continue decode logic."""
66-
size = self._header.get("len", 0)
67-
if not size and len(self._buffer) > self._hsize:
68-
try:
69-
self._header["uid"] = int(self._buffer[0])
70-
self._header["tid"] = int(self._buffer[0])
71-
func_code = int(self._buffer[1])
72-
pdu_class = self.decoder.lookupPduClass(func_code)
73-
size = pdu_class.calculateRtuFrameSize(self._buffer)
74-
self._header["len"] = size
75-
76-
if len(self._buffer) < size:
77-
raise IndexError
78-
self._header["crc"] = self._buffer[size - 2 : size]
79-
except IndexError:
80-
return False
81-
return len(self._buffer) >= size if size > 0 else False
82-
83-
def get_frame_start(self, slaves, broadcast, skip_cur_frame):
84-
"""Scan buffer for a relevant frame start."""
85-
start = 1 if skip_cur_frame else 0
86-
if (buf_len := len(self._buffer)) < 4:
87-
return False
88-
for i in range(start, buf_len - 3): # <slave id><function code><crc 2 bytes>
89-
if not broadcast and self._buffer[i] not in slaves:
90-
continue
91-
if (
92-
self._buffer[i + 1] not in self.function_codes
93-
and (self._buffer[i + 1] - 0x80) not in self.function_codes
94-
):
95-
continue
96-
if i:
97-
self._buffer = self._buffer[i:] # remove preceding trash.
98-
return True
99-
if buf_len > 3:
100-
self._buffer = self._buffer[-3:]
101-
return False
102-
103-
def check_frame(self):
104-
"""Check if the next frame is available."""
105-
try:
106-
self._header["uid"] = int(self._buffer[0])
107-
self._header["tid"] = int(self._buffer[0])
108-
func_code = int(self._buffer[1])
109-
pdu_class = self.decoder.lookupPduClass(func_code)
110-
size = pdu_class.calculateRtuFrameSize(self._buffer)
111-
self._header["len"] = size
112-
113-
if len(self._buffer) < size:
114-
raise IndexError
115-
self._header["crc"] = self._buffer[size - 2 : size]
116-
frame_size = self._header["len"]
117-
data = self._buffer[: frame_size - 2]
118-
crc = self._header["crc"]
119-
crc_val = (int(crc[0]) << 8) + int(crc[1])
120-
return FramerRTU.check_CRC(data, crc_val)
121-
except (IndexError, KeyError, struct.error):
122-
return False
123-
124-
self._buffer = b'' # pylint: disable=attribute-defined-outside-init
125-
broadcast = not slave[0]
126-
skip_cur_frame = False
127-
while get_frame_start(self, slave, broadcast, skip_cur_frame):
128-
self._header: dict = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"} # pylint: disable=attribute-defined-outside-init
129-
if not is_frame_ready(self):
130-
Log.debug("Frame - not ready")
131-
break
132-
if not check_frame(self):
133-
Log.debug("Frame check failed, ignoring!!")
134-
# x = self._buffer
135-
# self.resetFrame()
136-
# self._buffer = x
137-
skip_cur_frame = True
138-
continue
139-
start = 0x01 # self._hsize
140-
end = self._header["len"] - 2
141-
buffer = self._buffer[start:end]
142-
if end > 0:
143-
Log.debug("Getting Frame - {}", buffer, ":hex")
144-
data = buffer
145-
else:
146-
data = b""
147-
if (result := ClientDecoder().decode(data)) is None:
148-
raise ModbusIOException("Unable to decode request")
149-
result.slave_id = self._header["uid"]
150-
result.transaction_id = self._header["tid"]
151-
self._buffer = self._buffer[self._header["len"] :] # pylint: disable=attribute-defined-outside-init
152-
Log.debug("Frame advanced, resetting header!!")
153-
callback(result) # defer or push to a thread?
154-
155-
156-
def hunt_frame_start(self, skip_cur_frame: bool, data: bytes) -> int:
157-
"""Scan buffer for a relevant frame start."""
158-
buf_len = len(data)
159-
for i in range(1 if skip_cur_frame else 0, buf_len - self.MIN_SIZE):
160-
if not (self.broadcast or data[i] in self.dev_ids):
161-
continue
162-
if (_fc := data[i + 1]) not in self.fc_calc:
163-
continue
164-
return i
165-
return -i
86+
87+
def setup_fc_len(self, _fc: int,
88+
_req_len: int, _req_byte_pos: int,
89+
_resp_len: int, _resp_byte_pos: int
90+
):
91+
"""Define request/response lengths pr function code."""
92+
return
16693

16794
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
16895
"""Decode ADU."""
169-
if len(data) < self.MIN_SIZE:
96+
if (buf_len := len(data)) < self.MIN_SIZE:
97+
Log.debug("Short frame: {} wait for more data", data, ":hex")
17098
return 0, 0, 0, b''
17199

172-
while (i := self.hunt_frame_start(False, data)) > 0:
173-
pass
174-
return -i, 0, 0, b''
100+
i = -1
101+
try:
102+
while True:
103+
i += 1
104+
if i > buf_len - self.MIN_SIZE + 1:
105+
break
106+
dev_id = int(data[i])
107+
fc_len = 5
108+
msg_len = fc_len -2 if fc_len > 0 else int(data[i-fc_len])-fc_len+1
109+
if msg_len + i + 2 > buf_len:
110+
break
111+
crc_val = (int(data[i+msg_len]) << 8) + int(data[i+msg_len+1])
112+
if not self.check_CRC(data[i:i+msg_len], crc_val):
113+
Log.debug("Skipping frame CRC with len {} at index {}!", msg_len, i)
114+
raise KeyError
115+
return i+msg_len+2, dev_id, dev_id, data[i+1:i+msg_len]
116+
except KeyError:
117+
i = buf_len
118+
return i, 0, 0, b''
175119

176120

177121
def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes:

0 commit comments

Comments
 (0)