diff --git a/pymodbus/framer/patched_rtuframer b/pymodbus/framer/patched_rtuframer new file mode 100644 index 0000000000..ff995452cd --- /dev/null +++ b/pymodbus/framer/patched_rtuframer @@ -0,0 +1,37 @@ +import logging + +from pymodbus.framer.rtu_framer import ModbusRtuFramer + + +log = logging.getLogger(__name__) + + +class PatchedModbusRtuFramer(ModbusRtuFramer): + + def processIncomingPacket(self, data, callback, unit, **kwargs): + if not isinstance(unit, (list, tuple)): + unit = [unit] + self.addToFrame(data) + single = kwargs.get("single", False) + while True: + if not self.isFrameIntendedForUs(unit): + if self.advanceToNextOccurrenceOfUnit(unit) == 0: + log.info(f"❌ Frame - [{data}] not intended for us, ignoring!!") + break + elif self.isFrameReady(): + if self.checkFrame(): + if self._validate_unit_id(unit, single): + self._process(callback) + log.info(f"✅ Frame - [{data}] responded to!!") + else: + header_txt = self._header["uid"] + log.info(f"Not a valid unit id - {header_txt}, ignoring!!") + self.resetFrame() + break + else: + log.info("Frame check failed, ignoring!!") + if self.advanceToNextOccurrenceOfUnit(unit) == 0: + break + else: + log.info(f"Frame - [{data}] not ready") + break diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index dc149a0f3d..96551dfbbc 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -2,6 +2,7 @@ # pylint: disable=missing-type-doc import struct import time +from typing import List from pymodbus.exceptions import ( InvalidMessageReceivedException, @@ -87,6 +88,16 @@ def checkFrame(self): try: self.populateHeader() frame_size = self._header["len"] + + if len(self._buffer) > frame_size: + # This means there are bytes *after* a valid frame intended to us. + # If there are bytes after that means we've probably de-sychronized + # and the master has considered us as having timed out and moved on. + # That or there is somehow a random sequence of bytes which looks like + # a real command to us and magically a crc. + # In either case, we must not respond. + return False + data = self._buffer[: frame_size - 2] crc = self._header["crc"] crc_val = (int(crc[0]) << 8) + int(crc[1]) @@ -191,6 +202,24 @@ def populateResult(self, result): result.slave_id = self._header["uid"] result.transaction_id = self._header["uid"] + def isFrameIntendedForUs(self, slaves: List[int]): + """Check slave ID of frame.""" + try: + return self._buffer[0] in slaves + except IndexError: + return True + + def advanceToNextOccurrenceOfUnit(self, slaves: List[int]): + """Skip to next slave id.""" + newbuf = b"" + for i, databyte in enumerate(self._buffer): + if databyte in slaves and i > 0: + newbuf = self._buffer[i:] + break + self._buffer = newbuf + self._header = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"} + return len(self._buffer) + # ----------------------------------------------------------------------- # # Public Member Functions # ----------------------------------------------------------------------- # @@ -331,6 +360,3 @@ def get_expected_response_length(self, data): func_code = int(data[1]) pdu_class = self.decoder.lookupPduClass(func_code) return pdu_class.calculateRtuFrameSize(data) - - -# __END__ diff --git a/test/test_client_multidrop.py b/test/test_client_multidrop.py index 946b431383..ebd112ee6b 100644 --- a/test/test_client_multidrop.py +++ b/test/test_client_multidrop.py @@ -109,7 +109,6 @@ def test_wrapped_frame(self, framer, callback): # and the master moved on and queried another device callback.assert_not_called() - @pytest.mark.skip def test_frame_with_trailing_data(self, framer, callback): """Test trailing data.""" garbage = b"\x05\x04\x03\x02\x01\x00" diff --git a/test/test_framers.py b/test/test_framers.py index de2eb34e31..bd991a35ce 100644 --- a/test/test_framers.py +++ b/test/test_framers.py @@ -258,7 +258,7 @@ def test_populate_result(rtu_framer): # pylint: disable=redefined-outer-name True, False, ), # incorrect slave id - (b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03", 17, False, True), + (b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03", 17, True, False), # good frame + part of next frame ], )