From 3c0e9edbc5753c5312825a834ee3538b140bc690 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 2 Apr 2024 21:00:47 +0200 Subject: [PATCH] RTU decoder, ready for test. --- pymodbus/framer/ascii.py | 42 ++--- pymodbus/framer/base.py | 8 +- pymodbus/framer/framer.py | 22 +-- pymodbus/framer/rtu.py | 181 ++++++-------------- test/framers/conftest.py | 43 +++-- test/framers/test_ascii.py | 2 +- test/framers/test_framer.py | 326 +++++++++++++++++++++--------------- test/framers/test_rtu.py | 23 +++ 8 files changed, 343 insertions(+), 304 deletions(-) diff --git a/pymodbus/framer/ascii.py b/pymodbus/framer/ascii.py index 03746b87e8..0229ed19f3 100644 --- a/pymodbus/framer/ascii.py +++ b/pymodbus/framer/ascii.py @@ -34,25 +34,29 @@ class FramerAscii(FramerBase): def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" - if (used_len := len(data)) < self.MIN_SIZE: - Log.debug("Short frame: {} wait for more data", data, ":hex") - return 0, 0, 0, self.EMPTY - if data[0:1] != self.START: - if (start := data.find(self.START)) != -1: - used_len = start - Log.debug("Garble data before frame: {}, skip until start of frame", data, ":hex") - return used_len, 0, 0, self.EMPTY - if (used_len := data.find(self.END)) == -1: - Log.debug("Incomplete frame: {} wait for more data", data, ":hex") - return 0, 0, 0, self.EMPTY - - dev_id = int(data[1:3], 16) - lrc = int(data[used_len - 2: used_len], 16) - msg = a2b_hex(data[1 : used_len - 2]) - if not self.check_LRC(msg, lrc): - Log.debug("LRC wrong in frame: {} skipping", data, ":hex") - return used_len+2, 0, 0, self.EMPTY - return used_len+2, 0, dev_id, msg[1:] + buf_len = len(data) + used_len = 0 + while True: + if buf_len - used_len < self.MIN_SIZE: + return used_len, 0, 0, self.EMPTY + buffer = data[used_len:] + if buffer[0:1] != self.START: + if (i := buffer.find(self.START)) == -1: + Log.debug("No frame start in data: {}, wait for data", data, ":hex") + return buf_len, 0, 0, self.EMPTY + used_len += i + continue + if (end := buffer.find(self.END)) == -1: + Log.debug("Incomplete frame: {} wait for more data", data, ":hex") + return used_len, 0, 0, self.EMPTY + dev_id = int(buffer[1:3], 16) + lrc = int(buffer[end - 2: end], 16) + msg = a2b_hex(buffer[1 : end - 2]) + used_len += end + 2 + if not self.check_LRC(msg, lrc): + Log.debug("LRC wrong in frame: {} skipping", data, ":hex") + continue + return used_len, dev_id, dev_id, msg[1:] def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: """Encode ADU.""" diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index a7e0ba1461..9fa3b083c1 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -15,6 +15,12 @@ class FramerBase: def __init__(self) -> None: """Initialize a ADU instance.""" + def set_dev_ids(self, _dev_ids: list[int]): + """Set/update allowed device ids.""" + + def set_fc_calc(self, _fc: int, _msg_size: int, _count_pos: int): + """Set/Update function code information.""" + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU. @@ -24,7 +30,6 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: device_id (int) or 0 modbus request/response (bytes) """ - raise RuntimeError("NOT IMPLEMENTED!") def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes: """Encode ADU. @@ -32,4 +37,3 @@ def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes: returns: modbus ADU (bytes) """ - raise RuntimeError("NOT IMPLEMENTED!") diff --git a/pymodbus/framer/framer.py b/pymodbus/framer/framer.py index 9dc9eabd01..f7e891e5d6 100644 --- a/pymodbus/framer/framer.py +++ b/pymodbus/framer/framer.py @@ -75,23 +75,19 @@ def __init__(self, }[framer_type]() - def validate_device_id(self, dev_id: int) -> bool: - """Check if device id is expected.""" - return self.broadcast or (dev_id in self.device_ids) - - def callback_data(self, data: bytes, addr: tuple | None = None) -> int: """Handle received data.""" - tot_len = len(data) - start = 0 + tot_len = 0 + buf_len = len(data) while True: - used_len, tid, device_id, msg = self.handle.decode(data[start:]) + used_len, tid, device_id, msg = self.handle.decode(data[tot_len:]) + tot_len += used_len if msg: - self.callback_request_response(msg, device_id, tid) - if not used_len: - return start - start += used_len - if start == tot_len: + if self.broadcast or device_id in self.device_ids: + self.callback_request_response(msg, device_id, tid) + if tot_len == buf_len: + return tot_len + else: return tot_len # --------------------- # diff --git a/pymodbus/framer/rtu.py b/pymodbus/framer/rtu.py index bfda583650..47ae9be88e 100644 --- a/pymodbus/framer/rtu.py +++ b/pymodbus/framer/rtu.py @@ -1,10 +1,6 @@ """Modbus RTU frame implementation.""" from __future__ import annotations -import struct - -from pymodbus.exceptions import ModbusIOException -from pymodbus.factory import ClientDecoder from pymodbus.framer.base import FramerBase from pymodbus.logging import Log @@ -17,6 +13,39 @@ class FramerRTU(FramerBase): * Note: due to the USB converter and the OS drivers, timing cannot be quaranteed neither when receiving nor when sending. + + Decoding is a complicated process because the RTU frame does not have a fixed prefix + only suffix, therefore it is nessecary to decode the content (PDU) to get length etc. + + There are some restraints however that help the detection. + + For client: + - a request causes 1 response ! + - Multiple requests are NOT allowed (master-slave protocol) + - the server will not retransmit responses + this means decoding is always exactly 1 frame (response) + + For server (Single device) + - only 1 request allowed (master-slave) protocol + - the client (master) may retransmit but in larger time intervals + this means decoding is always exactly 1 frame (request) + + For server (Multidrop line --> devices in parallel) + - only 1 request allowed (master-slave) protocol + - other devices will send responses + - the client (master) may retransmit but in larger time intervals + this means decoding is always exactly 1 frame (request / response) + + Recovery from bad cabling etc is important, the following scenarios is possible: + - garble data before frame + - garble data in frame + - garble data after frame + - data in frame garbled (wrong CRC) + decoding assumes the frame is sound, and if not enters a hunting mode. + + The 3.5 byte wait betwen frames is 31ms at 1.200Bps and 1ms at 38.600bps, + so the decoder will wait 50ms for more data if not the transmission is + considered complete """ MIN_SIZE = 5 @@ -24,19 +53,6 @@ class FramerRTU(FramerBase): def __init__(self) -> None: """Initialize a ADU instance.""" super().__init__() - self.broadcast: bool = False - self.dev_ids: list[int] - self.fc_calc: dict[int, int] - - def set_dev_ids(self, dev_ids: list[int]): - """Set/update allowed device ids.""" - if 0 in dev_ids: - self.broadcast = True - self.dev_ids = dev_ids - - def set_fc_calc(self, fc: int, msg_size: int, count_pos: int): - """Set/Update function code information.""" - self.fc_calc[fc] = msg_size if not count_pos else -count_pos @classmethod @@ -58,120 +74,31 @@ def generate_crc16_table(cls) -> list[int]: return result crc16_table: list[int] = [0] - def _legacy_decode(self, callback, slave): # noqa: C901 - """Process new packet pattern.""" - - def is_frame_ready(self): - """Check if we should continue decode logic.""" - size = self._header.get("len", 0) - if not size and len(self._buffer) > self._hsize: - try: - self._header["uid"] = int(self._buffer[0]) - self._header["tid"] = int(self._buffer[0]) - func_code = int(self._buffer[1]) - pdu_class = self.decoder.lookupPduClass(func_code) - size = pdu_class.calculateRtuFrameSize(self._buffer) - self._header["len"] = size - - if len(self._buffer) < size: - raise IndexError - self._header["crc"] = self._buffer[size - 2 : size] - 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): # - 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._header["uid"] = int(self._buffer[0]) - self._header["tid"] = int(self._buffer[0]) - func_code = int(self._buffer[1]) - pdu_class = self.decoder.lookupPduClass(func_code) - size = pdu_class.calculateRtuFrameSize(self._buffer) - self._header["len"] = size - - if len(self._buffer) < size: - raise IndexError - self._header["crc"] = self._buffer[size - 2 : size] - frame_size = self._header["len"] - data = self._buffer[: frame_size - 2] - crc = self._header["crc"] - crc_val = (int(crc[0]) << 8) + int(crc[1]) - return FramerRTU.check_CRC(data, crc_val) - except (IndexError, KeyError, struct.error): - return False - - self._buffer = b'' # pylint: disable=attribute-defined-outside-init - broadcast = not slave[0] - skip_cur_frame = False - while get_frame_start(self, slave, broadcast, skip_cur_frame): - self._header: dict = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"} # pylint: disable=attribute-defined-outside-init - if not is_frame_ready(self): - Log.debug("Frame - not ready") - break - if not check_frame(self): - Log.debug("Frame check failed, ignoring!!") - # x = self._buffer - # self.resetFrame() - # self._buffer = x - skip_cur_frame = True - continue - start = 0x01 # self._hsize - end = self._header["len"] - 2 - buffer = self._buffer[start:end] - if end > 0: - Log.debug("Getting Frame - {}", buffer, ":hex") - data = buffer - else: - data = b"" - if (result := ClientDecoder().decode(data)) is None: - raise ModbusIOException("Unable to decode request") - result.slave_id = self._header["uid"] - result.transaction_id = self._header["tid"] - self._buffer = self._buffer[self._header["len"] :] # pylint: disable=attribute-defined-outside-init - Log.debug("Frame advanced, resetting header!!") - callback(result) # defer or push to a thread? - - - def hunt_frame_start(self, skip_cur_frame: bool, data: bytes) -> int: - """Scan buffer for a relevant frame start.""" - buf_len = len(data) - for i in range(1 if skip_cur_frame else 0, buf_len - self.MIN_SIZE): - if not (self.broadcast or data[i] in self.dev_ids): - continue - if (_fc := data[i + 1]) not in self.fc_calc: - continue - return i - return -i - def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" - if len(data) < self.MIN_SIZE: + if (buf_len := len(data)) < self.MIN_SIZE: + Log.debug("Short frame: {} wait for more data", data, ":hex") return 0, 0, 0, b'' - while (i := self.hunt_frame_start(False, data)) > 0: - pass - return -i, 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'' def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes: diff --git a/test/framers/conftest.py b/test/framers/conftest.py index 5592ac9947..078599c998 100644 --- a/test/framers/conftest.py +++ b/test/framers/conftest.py @@ -5,27 +5,28 @@ import pytest +from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.framer import Framer, FramerType from pymodbus.transport import CommParams, ModbusProtocol -class DummyMessage(Framer): +class DummyFramer(Framer): """Implement use of ModbusProtocol.""" def __init__(self, - message_type: FramerType, + framer_type: FramerType, params: CommParams, is_server: bool, device_ids: list[int] | None, ): - """Initialize a message instance.""" - super().__init__(message_type, params, is_server, device_ids) + """Initialize a frame instance.""" + super().__init__(framer_type, params, is_server, device_ids) self.send = mock.Mock() - self.message_type = message_type + self.framer_type = framer_type def callback_new_connection(self) -> ModbusProtocol: """Call when listener receive new connection request.""" - return DummyMessage(self.message_type, self.comm_params, self.is_server, self.device_ids) # pragma: no cover + return DummyFramer(self.framer_type, self.comm_params, self.is_server, self.device_ids) # pragma: no cover def callback_connected(self) -> None: """Call when connection is succcesfull.""" @@ -37,7 +38,29 @@ def callback_request_response(self, data: bytes, device_id: int, tid: int) -> No """Handle received modbus request/response.""" -@pytest.fixture(name="dummy_message") -async def prepare_dummy_message(): - """Return message object.""" - return DummyMessage +@pytest.fixture(name="entry") +def prepare_entry(): + """Return framer_type.""" + return FramerType.RAW + +@pytest.fixture(name="is_server") +def prepare_is_server(): + """Return client/server.""" + return False + +@pytest.fixture(name="dummy_framer") +async def prepare_test_framer(entry, is_server): + """Return framer object.""" + framer = DummyFramer( + entry, + CommParams(), + is_server, + [0, 1], + ) + if entry == FramerType.RTU: + func_table = (ServerDecoder if is_server else ClientDecoder)().lookup + for key, ent in func_table.items(): + fix_len = ent._rtu_frame_size if hasattr(ent, "_rtu_frame_size") else 0 # pylint: disable=protected-access + cnt_pos = ent. _rtu_byte_count_pos if hasattr(ent, "_rtu_byte_count_pos") else 0 # pylint: disable=protected-access + framer.handle.set_fc_calc(key, fix_len, cnt_pos) + return framer diff --git a/test/framers/test_ascii.py b/test/framers/test_ascii.py index d78d2e87b4..d3b4ef74c8 100644 --- a/test/framers/test_ascii.py +++ b/test/framers/test_ascii.py @@ -34,7 +34,7 @@ def test_decode(self, frame, packet, used_len, res_id, res): res_len, tid, dev_id, data = frame.decode(packet) assert res_len == used_len assert data == res - assert not tid + assert tid == res_id assert dev_id == res_id @pytest.mark.parametrize( diff --git a/test/framers/test_framer.py b/test/framers/test_framer.py index 0236fcb342..59e6cdda1e 100644 --- a/test/framers/test_framer.py +++ b/test/framers/test_framer.py @@ -9,33 +9,15 @@ from pymodbus.framer.rtu import FramerRTU from pymodbus.framer.socket import FramerSocket from pymodbus.framer.tls import FramerTLS -from pymodbus.transport import CommParams class TestFramer: """Test module.""" - @staticmethod - @pytest.fixture(name="msg") - async def prepare_message(dummy_message): - """Return message object.""" - return dummy_message( - FramerType.RAW, - CommParams(), - False, - [1], - ) - - @pytest.mark.parametrize(("entry"), list(FramerType)) - async def test_message_init(self, entry, dummy_message): - """Test message type.""" - msg = dummy_message(entry.value, - CommParams(), - False, - [1], - ) - assert msg.handle + async def test_framer_init(self, dummy_framer): + """Test framer type.""" + assert dummy_framer.handle @pytest.mark.parametrize(("data", "res_len", "cx", "rc"), [ (b'12345', 5, 1, [(5, 0, 0, b'12345')]), # full frame @@ -43,38 +25,39 @@ async def test_message_init(self, entry, dummy_message): (b'12345', 5, 0, [(5, 0, 0, b'')]), # faulty frame, skipped (b'1234512345', 10, 2, [(5, 0, 0, b'12345'), (5, 0, 0, b'12345')]), # 2 full frames (b'12345678', 5, 1, [(5, 0, 0, b'12345'), (0, 0, 0, b'')]), # full frame, not full frame - (b'67812345', 8, 1, [(3, 0, 0, b''), (5, 0, 0, b'12345')]), # garble first, full frame next - (b'12345678', 5, 0, [(5, 0, 0, b''), (0, 0, 0, b'')]), # garble first, not full frame - (b'12345678', 8, 0, [(5, 0, 0, b''), (3, 0, 0, b'')]), # garble first, faulty frame + (b'67812345', 8, 1, [(8, 0, 0, b'12345')]), # garble first, full frame next + (b'12345678', 5, 0, [(5, 0, 0, b'')]), # garble first, not full frame + (b'12345678', 8, 0, [(8, 0, 0, b'')]), # garble first, faulty frame ]) - async def test_message_callback(self, msg, data, res_len, cx, rc): - """Test message type.""" - msg.callback_request_response = mock.Mock() - msg.handle.decode = mock.MagicMock(side_effect=iter(rc)) - assert msg.callback_data(data) == res_len - assert msg.callback_request_response.call_count == cx + async def test_framer_callback(self, dummy_framer, data, res_len, cx, rc): + """Test framer type.""" + dummy_framer.callback_request_response = mock.Mock() + dummy_framer.handle.decode = mock.MagicMock(side_effect=iter(rc)) + assert dummy_framer.callback_data(data) == res_len + assert dummy_framer.callback_request_response.call_count == cx if cx: - msg.callback_request_response.assert_called_with(b'12345', 0, 0) + dummy_framer.callback_request_response.assert_called_with(b'12345', 0, 0) else: - msg.callback_request_response.assert_not_called() + dummy_framer.callback_request_response.assert_not_called() - async def test_message_build_send(self, msg): - """Test message type.""" - msg.handle.encode = mock.MagicMock(return_value=(b'decode')) - msg.build_send(b'decode', 1, 0) - msg.handle.encode.assert_called_once() - msg.send.assert_called_once() - msg.send.assert_called_with(b'decode', None) + @pytest.mark.parametrize(("data", "res_len", "rc"), [ + (b'12345', 5, [(5, 0, 17, b'12345'), (0, 0, 0, b'')]), # full frame, wrong dev_id + ]) + async def test_framer_callback_wrong_id(self, dummy_framer, data, res_len, rc): + """Test framer type.""" + dummy_framer.callback_request_response = mock.Mock() + dummy_framer.handle.decode = mock.MagicMock(side_effect=iter(rc)) + dummy_framer.broadcast = False + assert dummy_framer.callback_data(data) == res_len + dummy_framer.callback_request_response.assert_not_called() - @pytest.mark.parametrize( - ("dev_id", "res"), [ - (0, False), - (1, True), - (2, False), - ]) - async def test_validate_id(self, msg, dev_id, res): - """Test message type.""" - assert res == msg.validate_device_id(dev_id) + async def test_framer_build_send(self, dummy_framer): + """Test framer type.""" + dummy_framer.handle.encode = mock.MagicMock(return_value=(b'decode')) + dummy_framer.build_send(b'decode', 1, 0) + dummy_framer.handle.encode.assert_called_once() + dummy_framer.send.assert_called_once() + dummy_framer.send.assert_called_with(b'decode', None) @pytest.mark.parametrize( ("data", "res_len", "res_id", "res_tid", "res_data"), [ @@ -82,9 +65,9 @@ async def test_validate_id(self, msg, dev_id, res): (b'\x01\x02\x03', 3, 1, 2, b'\x03'), (b'\x04\x05\x06\x07\x08\x09\x00\x01\x02\x03', 10, 4, 5, b'\x06\x07\x08\x09\x00\x01\x02\x03'), ]) - async def test_decode(self, msg, data, res_id, res_tid, res_len, res_data): + async def test_framer_decode(self, dummy_framer, data, res_id, res_tid, res_len, res_data): """Test decode method in all types.""" - t_len, t_id, t_tid, t_data = msg.handle.decode(data) + t_len, t_id, t_tid, t_data = dummy_framer.handle.decode(data) assert res_len == t_len assert res_id == t_id assert res_tid == t_tid @@ -95,9 +78,9 @@ async def test_decode(self, msg, data, res_id, res_tid, res_len, res_data): (b'\x01\x02', 5, 6, b'\x05\x06\x01\x02'), (b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09', 17, 25, b'\x11\x19\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09'), ]) - async def test_encode(self, msg, data, dev_id, tid, res_data): + async def test_framer_encode(self, dummy_framer, data, dev_id, tid, res_data): """Test decode method in all types.""" - t_data = msg.handle.encode(data, dev_id, tid) + t_data = dummy_framer.handle.encode(data, dev_id, tid) assert res_data == t_data @pytest.mark.parametrize( @@ -135,7 +118,7 @@ def test_roundtrip_CRC(self): -class TestFramer2: +class TestFramerType: """Test classes.""" @pytest.mark.parametrize( @@ -151,6 +134,24 @@ class TestFramer2: b':FF03007C000280\r\n', b':FF0304008D008EDF\r\n', b':FF83027C\r\n', + b':0003007C00027F\r\n', + b':000304008D008EDE\r\n', + b':0083027B\r\n', + b':1103007C00026E\r\n', + b':110304008D008ECD\r\n', + b':1183026A\r\n', + b':FF03007C000280\r\n', + b':FF0304008D008EDF\r\n', + b':FF83027C\r\n', + b':0003007C00027F\r\n', + b':000304008D008EDE\r\n', + b':0083027B\r\n', + b':1103007C00026E\r\n', + b':110304008D008ECD\r\n', + b':1183026A\r\n', + b':FF03007C000280\r\n', + b':FF0304008D008EDF\r\n', + b':FF83027C\r\n', ]), (FramerRTU, [ b'\x00\x03\x00\x7c\x00\x02\x04\x02', @@ -162,6 +163,24 @@ class TestFramer2: b'\xff\x03\x00\x7c\x00\x02\x10\x0d', b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', b'\xff\x83\x02\xa1\x01', + b'\x00\x03\x00\x7c\x00\x02\x04\x02', + b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', + b'\x00\x83\x02\x91\x31', + b'\x11\x03\x00\x7c\x00\x02\x07\x43', + b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', + b'\x11\x83\x02\xc1\x34', + b'\xff\x03\x00\x7c\x00\x02\x10\x0d', + b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', + b'\xff\x83\x02\xa1\x01', + b'\x00\x03\x00\x7c\x00\x02\x04\x02', + b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', + b'\x00\x83\x02\x91\x31', + b'\x11\x03\x00\x7c\x00\x02\x07\x43', + b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', + b'\x11\x83\x02\xc1\x34', + b'\xff\x03\x00\x7c\x00\x02\x10\x0d', + b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', + b'\xff\x83\x02\xa1\x01', ]), (FramerSocket, [ b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', @@ -213,10 +232,9 @@ class TestFramer2: (9, 3077), ] ) - def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): + def test_encode_type(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): """Test encode method.""" - if ((frame != FramerSocket and tid) or - (frame == FramerTLS and dev_id)): + if frame == FramerTLS and dev_id + tid: return frame_obj = frame() expected = frame_expected[inx1 + inx2 + inx3] @@ -224,47 +242,47 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 assert encoded_data == expected @pytest.mark.parametrize( - ("msg_type", "data", "dev_id", "tid", "expected"), + ("entry", "is_server", "data", "dev_id", "tid", "expected"), [ - (FramerType.ASCII, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.ASCII, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.ASCII, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception - (FramerType.ASCII, b':1103007C00026E\r\n', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.ASCII, b':110304008D008ECD\r\n', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.ASCII, b':1183026A\r\n', 17, 0, b'\x83\x02',), # Exception - (FramerType.ASCII, b':FF03007C000280\r\n', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.ASCII, b':FF0304008D008EDF\r\n', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.ASCII, b':FF83027C\r\n', 255, 0, b'\x83\x02',), # Exception - (FramerType.RTU, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.RTU, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception - (FramerType.RTU, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.RTU, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.RTU, b'\x11\x83\x02\xc1\x34', 17, 0, b'\x83\x02',), # Exception - (FramerType.RTU, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.RTU, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.RTU, b'\xff\x83\x02\xa1\x01', 255, 0, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception - (FramerType.TLS, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.TLS, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.TLS, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, True, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, False, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, False, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, True, b':1103007C00026E\r\n', 17, 17, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, False, b':110304008D008ECD\r\n', 17, 17, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, False, b':1183026A\r\n', 17, 17, b'\x83\x02',), # Exception + (FramerType.ASCII, True, b':FF03007C000280\r\n', 255, 255, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, False, b':FF0304008D008EDF\r\n', 255, 255, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, False, b':FF83027C\r\n', 255, 255, b'\x83\x02',), # Exception + (FramerType.RTU, True, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, False, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, False, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception + (FramerType.RTU, True, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 17, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, False, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 17, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, False, b'\x11\x83\x02\xc1\x34', 17, 17, b'\x83\x02',), # Exception + (FramerType.RTU, True, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 255, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, False, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 255, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, False, b'\xff\x83\x02\xa1\x01', 255, 255, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception + (FramerType.TLS, True, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.TLS, False, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.TLS, False, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception ] ) @pytest.mark.parametrize( @@ -275,54 +293,98 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 "single", ] ) - async def test_decode2(self, dummy_message, msg_type, data, dev_id, tid, expected, split): + async def test_decode_type(self, entry, dummy_framer, data, dev_id, tid, expected, split): """Test encode method.""" - if msg_type == FramerType.RTU: - pytest.skip("Waiting on implementation!") - if msg_type == FramerType.TLS and split != "no": + if entry == FramerType.TLS and split != "no": + return + if entry == FramerType.RTU: return - frame = dummy_message( - msg_type, - CommParams(), - False, - [1], - ) - frame.callback_request_response = mock.Mock() + dummy_framer.callback_request_response = mock.MagicMock() if split == "no": - used_len = frame.callback_data(data) - + used_len = dummy_framer.callback_data(data) elif split == "half": split_len = int(len(data) / 2) - assert not frame.callback_data(data[0:split_len]) - frame.callback_request_response.assert_not_called() - used_len = frame.callback_data(data) + assert not dummy_framer.callback_data(data[0:split_len]) + dummy_framer.callback_request_response.assert_not_called() + used_len = dummy_framer.callback_data(data) else: last = len(data) for i in range(0, last -1): - assert not frame.callback_data(data[0:i+1]) - frame.callback_request_response.assert_not_called() - used_len = frame.callback_data(data) + assert not dummy_framer.callback_data(data[0:i+1]) + dummy_framer.callback_request_response.assert_not_called() + used_len = dummy_framer.callback_data(data) assert used_len == len(data) - frame.callback_request_response.assert_called_with(expected, dev_id, tid) + dummy_framer.callback_request_response.assert_called_with(expected, dev_id, tid) @pytest.mark.parametrize( - ("frame", "data", "exp_len"), + ("entry", "data", "exp"), [ - (FramerAscii, b':0003007C00017F\r\n', 17), # bad crc - # (MessageAscii, b'abc:0003007C00027F\r\n', 3), # garble in front - # (MessageAscii, b':0003007C00017F\r\nabc', 17), # bad crc, garble after - # (MessageAscii, b':0003007C00017F\r\n:0003', 17), # part second message - (FramerRTU, b'\x00\x83\x02\x91\x31', 0), # bad crc - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble in front - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble after - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # part second message + (FramerType.ASCII, b':0003007C00017F\r\n', [ # bad crc + (17, b''), + ]), + (FramerType.ASCII, b':0003007C00027F\r\n:0003007C00027F\r\n', [ # double good crc + (17, b'\x03\x00\x7c\x00\x02'), + (17, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\n:0003007C00027F\r\n', [ # bad crc + good CRC + (34, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b'abc:0003007C00027F\r\n', [ # garble in front + (20, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\nabc', [ # bad crc, garble after + (17, b''), + ]), + (FramerType.ASCII, b':0003007C00017F\r\nabcdefghijkl', [ # bad crc, garble after + (29, b''), + ]), + (FramerType.ASCII, b':0003007C00027F\r\nabc', [ # good crc, garble after + (17, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\n:0003', [ # bad crc, part second framer + (17, b''), + ]), + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', [ # double good crc + (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\xf0\x91\x31', [ # dummy char in stream, bad crc + # (5, b''), + #]), + # (FramerType.RTU, b'\x00\x83\x02\x91\x21\x00\x83\x02\x91\x31', [ # bad crc + good CRC + # (10, b'\x83\x02'), + #]), + #(FramerType.RTU, b'\x00\x83\x02\xf0\x91\x31\x00\x83\x02\x91\x31', [ # dummy char in stream, bad crc + good CRC + # (11, b''), + #]), + + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # garble in front + # (FramerType.ASCII, b'abc:0003007C00027F\r\n', [ # garble in front + # (20, b'\x03\x00\x7c\x00\x02'), + # ]), + + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # garble after + # (FramerType.ASCII, b':0003007C00017F\r\nabc', [ # bad crc, garble after + # (17, b''), + # ]), + # (FramerType.ASCII, b':0003007C00017F\r\nabcdefghijkl', [ # bad crc, garble after + # (29, b''), + # ]), + # (FramerType.ASCII, b':0003007C00027F\r\nabc', [ # good crc, garble after + # (17, b'\x03\x00\x7c\x00\x02'), + # ]), + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # part second framer + # (FramerType.ASCII, b':0003007C00017F\r\n:0003', [ # bad crc, part second framer + # (17, b''), + # ]), ] ) - async def test_decode_bad_crc(self, frame, data, exp_len): + async def test_decode_complicated(self, dummy_framer, data, exp): """Test encode method.""" - if frame == FramerRTU: - pytest.skip("Waiting for implementation.") - frame_obj = frame() - used_len, _, _, data = frame_obj.decode(data) - assert used_len == exp_len - assert not data + for ent in exp: + used_len, _, _, res_data = dummy_framer.handle.decode(data) + assert used_len == ent[0] + assert res_data == ent[1] diff --git a/test/framers/test_rtu.py b/test/framers/test_rtu.py index dd30f82182..13bdd8770c 100644 --- a/test/framers/test_rtu.py +++ b/test/framers/test_rtu.py @@ -13,6 +13,29 @@ def prepare_frame(): """Return message object.""" return FramerRTU() + @pytest.mark.skip() + @pytest.mark.parametrize( + ("packet", "used_len", "res_id", "res"), + [ + (b':010100010001FC\r\n', 17, 1, b'\x01\x00\x01\x00\x01'), + (b':00010001000AF4\r\n', 17, 0, b'\x01\x00\x01\x00\x0a'), + (b':01010001000AF3\r\n', 17, 1, b'\x01\x00\x01\x00\x0a'), + (b':61620001000A32\r\n', 17, 97, b'\x62\x00\x01\x00\x0a'), + (b':01270001000ACD\r\n', 17, 1, b'\x27\x00\x01\x00\x0a'), + (b':010100', 0, 0, b''), # short frame + (b':00010001000AF4', 0, 0, b''), + (b'abc:00010001000AF4', 3, 0, b''), # garble before frame + (b'abc00010001000AF4', 17, 0, b''), # only garble + (b':01010001000A00\r\n', 17, 0, b''), + ], + ) + def test_decode(self, frame, packet, used_len, res_id, res): + """Test decode.""" + res_len, tid, dev_id, data = frame.decode(packet) + assert res_len == used_len + assert data == res + assert tid == res_id + assert dev_id == res_id @pytest.mark.parametrize( ("data", "dev_id", "res_msg"),