Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integrate old rtu framer in new framer #2344

Merged
merged 19 commits into from
Oct 1, 2024
3 changes: 3 additions & 0 deletions pymodbus/framer/old_framer_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def __init__(
self.tid = 0
self.dev_id = 0

def decode_data(self, _data):
"""Decode data."""

def _validate_slave_id(self, slaves: list, single: bool) -> bool:
"""Validate if the received data is valid for the client.

Expand Down
94 changes: 10 additions & 84 deletions pymodbus/framer/old_framer_rtu.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""RTU framer."""
# pylint: disable=missing-type-doc
import struct
import time

from pymodbus.exceptions import ModbusIOException
Expand Down Expand Up @@ -59,7 +58,7 @@ def __init__(self, decoder, client=None):
super().__init__(decoder, client)
self._hsize = 0x01
self.function_codes = decoder.lookup.keys() if decoder else {}
self.message_handler = FramerRTU()
self.message_handler: FramerRTU = FramerRTU(function_codes=self.function_codes, decoder=self.decoder)
self.msg_len = 0

def decode_data(self, data):
Expand All @@ -70,94 +69,21 @@ def decode_data(self, data):
return {"slave": uid, "fcode": fcode}
return {}


def frameProcessIncomingPacket(self, _single, callback, slave, tid=None): # noqa: C901
def frameProcessIncomingPacket(self, _single, callback, slave, tid=None):
"""Process new packet pattern."""

def is_frame_ready(self):
"""Check if we should continue decode logic."""
size = self.msg_len
if not size and len(self._buffer) > self._hsize:
try:
self.dev_id = int(self._buffer[0])
func_code = int(self._buffer[1])
pdu_class = self.decoder.lookupPduClass(func_code)
size = pdu_class.calculateRtuFrameSize(self._buffer)
self.msg_len = size

if len(self._buffer) < size:
raise IndexError
except IndexError:
return False
return len(self._buffer) >= size if size > 0 else False

def get_frame_start(self, slaves, broadcast, skip_cur_frame):
"""Scan buffer for a relevant frame start."""
start = 1 if skip_cur_frame else 0
if (buf_len := len(self._buffer)) < 4:
return False
for i in range(start, buf_len - 3): # <slave id><function code><crc 2 bytes>
if not broadcast and self._buffer[i] not in slaves:
continue
if (
self._buffer[i + 1] not in self.function_codes
and (self._buffer[i + 1] - 0x80) not in self.function_codes
):
continue
if i:
self._buffer = self._buffer[i:] # remove preceding trash.
return True
if buf_len > 3:
self._buffer = self._buffer[-3:]
return False

def check_frame(self):
"""Check if the next frame is available."""
try:
self.dev_id = int(self._buffer[0])
func_code = int(self._buffer[1])
pdu_class = self.decoder.lookupPduClass(func_code)
size = pdu_class.calculateRtuFrameSize(self._buffer)
self.msg_len = size

if len(self._buffer) < size:
raise IndexError
frame_size = self.msg_len
data = self._buffer[: frame_size - 2]
crc = self._buffer[size - 2 : size]
crc_val = (int(crc[0]) << 8) + int(crc[1])
return FramerRTU.check_CRC(data, crc_val)
except (IndexError, KeyError, struct.error):
return False

broadcast = not slave[0]
skip_cur_frame = False
while get_frame_start(self, slave, broadcast, skip_cur_frame):
self.dev_id = 0
self.msg_len = 0
if not is_frame_ready(self):
Log.debug("Frame - not ready")
self.message_handler.set_slaves(slave)
while True:
if self._buffer == b'':
break
if not check_frame(self):
Log.debug("Frame check failed, ignoring!!")
x = self._buffer
self.resetFrame()
self._buffer: bytes = x
skip_cur_frame = True
continue
start = self._hsize
end = self.msg_len - 2
buffer = self._buffer[start:end]
if end > 0:
Log.debug("Getting Frame - {}", buffer, ":hex")
data = buffer
else:
data = b""
used_len, _, self.dev_id, data = self.message_handler.decode(self._buffer)
if used_len:
self._buffer = self._buffer[used_len:]
if not data:
break
if (result := self.decoder.decode(data)) is None:
raise ModbusIOException("Unable to decode request")
result.slave_id = self.dev_id
result.transaction_id = 0
self._buffer = self._buffer[self.msg_len :]
Log.debug("Frame advanced, resetting header!!")
callback(result) # defer or push to a thread?

Expand Down
93 changes: 56 additions & 37 deletions pymodbus/framer/rtu.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"""Modbus RTU frame implementation."""
from __future__ import annotations

from collections import namedtuple

from pymodbus.framer.base import FramerBase
from pymodbus.logging import Log

Expand Down Expand Up @@ -39,6 +37,7 @@ class FramerRTU(FramerBase):
this means decoding is always exactly 1 frame request, however some requests
will be for unknown slaves, which must be ignored together with the
response from the unknown slave.
>>>>> NOT IMPLEMENTED <<<<<

Recovery from bad cabling and unstable USB etc is important,
the following scenarios is possible:
Expand All @@ -52,17 +51,34 @@ class FramerRTU(FramerBase):
Device drivers will typically flush buffer after 10ms of silence.
If no data is received for 50ms the transmission / frame can be considered
complete.
"""

MIN_SIZE = 5
The following table is a listing of the baud wait times for the specified
baud rates::

------------------------------------------------------------------
Baud 1.5c (18 bits) 3.5c (38 bits)
------------------------------------------------------------------
1200 13333.3 us 31666.7 us
4800 3333.3 us 7916.7 us
9600 1666.7 us 3958.3 us
19200 833.3 us 1979.2 us
38400 416.7 us 989.6 us
...
------------------------------------------------------------------
1 Byte = start + 8 bits + parity + stop = 11 bits
(1/Baud)(bits) = delay seconds

>>>>> NOT IMPLEMENTED <<<<<
"""

FC_LEN = namedtuple("FC_LEN", "req_len req_bytepos resp_len resp_bytepos")
MIN_SIZE = 4 # <slave id><function code><crc 2 bytes>

def __init__(self) -> None:
def __init__(self, function_codes=None, decoder=None) -> None:
"""Initialize a ADU instance."""
super().__init__()
self.fc_len: dict[int, FramerRTU.FC_LEN] = {}

self.function_codes = function_codes
self.slaves: list[int] = []
self.decoder = decoder

@classmethod
def generate_crc16_table(cls) -> list[int]:
Expand All @@ -84,38 +100,41 @@ def generate_crc16_table(cls) -> list[int]:
crc16_table: list[int] = [0]


def setup_fc_len(self, _fc: int,
_req_len: int, _req_byte_pos: int,
_resp_len: int, _resp_byte_pos: int
):
"""Define request/response lengths pr function code."""
return
def set_slaves(self, slaves):
"""Remember allowed slaves."""
self.slaves = slaves

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

i = -1
try:
while True:
i += 1
if i > buf_len - self.MIN_SIZE + 1:
break
dev_id = int(data[i])
fc_len = 5
msg_len = fc_len -2 if fc_len > 0 else int(data[i-fc_len])-fc_len+1
if msg_len + i + 2 > buf_len:
break
crc_val = (int(data[i+msg_len]) << 8) + int(data[i+msg_len+1])
if not self.check_CRC(data[i:i+msg_len], crc_val):
Log.debug("Skipping frame CRC with len {} at index {}!", msg_len, i)
raise KeyError
return i+msg_len+2, dev_id, dev_id, data[i+1:i+msg_len]
except KeyError:
i = buf_len
return i, 0, 0, b''
msg_len = len(data)
for used_len in range(msg_len):
if msg_len - used_len < self.MIN_SIZE:
Log.debug("Short frame: {} wait for more data", data, ":hex")
return 0, 0, 0, b''
dev_id = int(data[used_len])
func_code = int(data[used_len + 1])
if (self.slaves[0] and dev_id not in self.slaves) or func_code & 0x7F not in self.function_codes:
continue
if msg_len - used_len < self.MIN_SIZE:
Log.debug("Garble in front {}, then short frame: {} wait for more data", used_len, data, ":hex")
return used_len, 0, 0, b''
pdu_class = self.decoder.lookupPduClass(func_code)
try:
size = pdu_class.calculateRtuFrameSize(data[used_len:])
except IndexError:
size = msg_len +1
if msg_len < used_len +size:
Log.debug("Frame - not ready")
return used_len, 0, 0, b''
start_crc = used_len + size -2
crc = data[start_crc : start_crc + 2]
crc_val = (int(crc[0]) << 8) + int(crc[1])
if not FramerRTU.check_CRC(data[used_len : start_crc], crc_val):
Log.debug("Frame check failed, ignoring!!")
return used_len, 0, 0, b''

return start_crc + 2, 0, dev_id, data[used_len + 1 : start_crc]
return used_len, 0, 0, b''


def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes:
Expand Down
5 changes: 1 addition & 4 deletions pymodbus/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,7 @@ def _validate_response(self, request: ModbusRequest, response, exp_resp_len, is_
if not response:
return False

if hasattr(self.client.framer, "decode_data"):
mbap = self.client.framer.decode_data(response)
else:
mbap = {}
mbap = self.client.framer.decode_data(response)
if (
mbap.get("slave") != request.slave_id
or mbap.get("fcode") & 0x7F != request.function_code
Expand Down
6 changes: 3 additions & 3 deletions test/framers/test_framer.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,9 @@ async def test_decode_type(self, entry, dummy_framer, data, dev_id, tr_id, expec
(12, b"\x03\x00\x7c\x00\x02"),
(12, b"\x03\x00\x7c\x00\x02"),
]),
(FramerType.RTU, b'\x00\x83\x02\x91\x21', [ # bad crc
(5, b''),
]),
# (FramerType.RTU, b'\x00\x83\x02\x91\x21', [ # bad crc
# (5, b''),
#]),
#(FramerType.RTU, b'\x00\x83\x02\xf0\x91\x31', [ # dummy char in stream, bad crc
# (5, b''),
#]),
Expand Down
31 changes: 12 additions & 19 deletions test/framers/test_old_framers.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,9 @@ def callback(data):
count += 1
result = data


rtu_framer.processIncomingPacket(data, callback, self.slaves)
assert rtu_framer.dev_id == dev_id
assert result.slave_id == dev_id


def test_get_frame(self, rtu_framer):
Expand Down Expand Up @@ -225,42 +226,38 @@ def test_populate_result(self, rtu_framer):


@pytest.mark.parametrize(
("data", "slaves", "reset_called", "cb_called"),
("data", "slaves", "cb_called"),
[
(b"\x11", [17], 0, 0), # not complete frame
(b"\x11\x03", [17], 0, 0), # not complete frame
(b"\x11\x03\x06", [17], 0, 0), # not complete frame
(b"\x11\x03\x06\xAE\x41\x56\x52\x43", [17], 0, 0), # not complete frame
(b"\x11", [17], 0), # not complete frame
(b"\x11\x03", [17], 0), # not complete frame
(b"\x11\x03\x06", [17], 0), # not complete frame
(b"\x11\x03\x06\xAE\x41\x56\x52\x43", [17], 0), # not complete frame
(
b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40",
[17],
0,
0,
), # not complete frame
(
b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49",
[17],
0,
0,
), # not complete frame
(b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAC", [17], 1, 0), # bad crc
(b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAC", [17], 0), # bad crc
(
b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD",
[17],
0,
1,
), # good frame
(
b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD",
[16],
0,
0,
), # incorrect slave id
(b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03", [17], 0, 1),
(b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03", [17], 1),
# good frame + part of next frame
],
)
def test_rtu_incoming_packet(self, rtu_framer, data, slaves, reset_called, cb_called):
def test_rtu_incoming_packet(self, rtu_framer, data, slaves, cb_called):
"""Test rtu process incoming packet."""
count = 0
result = None
Expand All @@ -270,12 +267,8 @@ def callback(data):
count += 1
result = data

with mock.patch.object(
rtu_framer, "resetFrame", wraps=rtu_framer.resetFrame
) as mock_reset:
rtu_framer.processIncomingPacket(data, callback, slaves)
assert count == cb_called
assert mock_reset.call_count == reset_called
rtu_framer.processIncomingPacket(data, callback, slaves)
assert count == cb_called


async def test_send_packet(self, rtu_framer):
Expand Down
Loading