Skip to content

Commit

Permalink
integrate old rtu framer in new framer (#2344)
Browse files Browse the repository at this point in the history
  • Loading branch information
janiversen authored Oct 1, 2024
1 parent 2109989 commit 577ffa6
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 147 deletions.
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

0 comments on commit 577ffa6

Please sign in to comment.