Skip to content

Commit dfecd8c

Browse files
authored
add _legacy_decoder to message rtu (#2119)
1 parent 6c192f6 commit dfecd8c

File tree

5 files changed

+129
-12
lines changed

5 files changed

+129
-12
lines changed

pymodbus/framer/rtu_framer.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import struct
44
import time
55

6-
from pymodbus.exceptions import (
7-
ModbusIOException,
8-
)
6+
from pymodbus.exceptions import ModbusIOException
97
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
108
from pymodbus.logging import Log
119
from pymodbus.message.rtu import MessageRTU

pymodbus/message/base.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class MessageBase:
1616

1717
def __init__(
1818
self,
19-
device_ids: list[int] | None,
19+
device_ids: list[int],
2020
is_server: bool,
2121
) -> None:
2222
"""Initialize a message instance.
@@ -25,10 +25,12 @@ def __init__(
2525
"""
2626
self.device_ids = device_ids
2727
self.is_server = is_server
28+
self.broadcast: bool = (0 in device_ids)
29+
2830

2931
def validate_device_id(self, dev_id: int) -> bool:
3032
"""Check if device id is expected."""
31-
return not (self.device_ids and dev_id and dev_id not in self.device_ids)
33+
return self.broadcast or (dev_id in self.device_ids)
3234

3335
@abstractmethod
3436
def decode(self, _data: bytes) -> tuple[int, int, int, bytes]:

pymodbus/message/message.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ def __init__(self,
5454
message_type: MessageType,
5555
params: CommParams,
5656
is_server: bool,
57-
device_ids: list[int] | None,
57+
device_ids: list[int],
5858
):
5959
"""Initialize a message instance.
6060
6161
:param message_type: Modbus message type
6262
:param params: parameter dataclass
6363
:param is_server: true if object act as a server (listen/connect)
64-
:param device_ids: list of device id to accept (server only), None for all.
64+
:param device_ids: list of device id to accept, 0 in list means broadcast.
6565
"""
6666
super().__init__(params, is_server)
6767
self.device_ids = device_ids

pymodbus/message/rtu.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
"""
77
from __future__ import annotations
88

9+
import struct
10+
11+
from pymodbus.exceptions import ModbusIOException
12+
from pymodbus.factory import ClientDecoder
13+
from pymodbus.logging import Log
914
from pymodbus.message.base import MessageBase
1015

1116

@@ -40,6 +45,13 @@ class MessageRTU(MessageBase):
4045
neither when receiving nor when sending.
4146
"""
4247

48+
function_codes: list[int] = []
49+
50+
@classmethod
51+
def set_legal_function_codes(cls, function_codes: list[int]):
52+
"""Set legal function codes."""
53+
cls.function_codes = function_codes
54+
4355
@classmethod
4456
def generate_crc16_table(cls) -> list[int]:
4557
"""Generate a crc16 lookup table.
@@ -59,10 +71,116 @@ def generate_crc16_table(cls) -> list[int]:
5971
return result
6072
crc16_table: list[int] = [0]
6173

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

183+
66184
def encode(self, data: bytes, device_id: int, _tid: int) -> bytes:
67185
"""Decode message."""
68186
packet = device_id.to_bytes(1,'big') + data

test/message/test_message.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ async def test_message_build_send(self, msg):
6868

6969
@pytest.mark.parametrize(
7070
("dev_id", "res"), [
71-
(None, True),
72-
(0, True),
71+
(0, False),
7372
(1, True),
7473
(2, False),
7574
])
@@ -220,7 +219,7 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3
220219
pytest.skip("Not supported")
221220
if frame == MessageTLS and (tid or dev_id):
222221
pytest.skip("Not supported")
223-
frame_obj = frame(None, True)
222+
frame_obj = frame([0], True)
224223
expected = frame_expected[inx1 + inx2 + inx3]
225224
encoded_data = frame_obj.encode(data, dev_id, tid)
226225
assert encoded_data == expected
@@ -324,7 +323,7 @@ async def test_decode_bad_crc(self, frame, data, exp_len):
324323
"""Test encode method."""
325324
if frame == MessageRTU:
326325
pytest.skip("Waiting for implementation.")
327-
frame_obj = frame(None, True)
326+
frame_obj = frame([0], True)
328327
used_len, _, _, data = frame_obj.decode(data)
329328
assert used_len == exp_len
330329
assert not data

0 commit comments

Comments
 (0)