Skip to content

Commit

Permalink
integrate message.encode() into framer.buildPacket.
Browse files Browse the repository at this point in the history
  • Loading branch information
janiversen committed Mar 5, 2024
1 parent 16b63b1 commit 29d3558
Show file tree
Hide file tree
Showing 11 changed files with 1,062 additions and 821 deletions.
56 changes: 54 additions & 2 deletions pymodbus/message/rtu.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,62 @@ class MessageRTU(MessageBase):
neither when receiving nor when sending.
"""

@classmethod
def generate_crc16_table(cls) -> list[int]:
"""Generate a crc16 lookup table.
.. note:: This will only be generated once
"""
result = []
for byte in range(256):
crc = 0x0000
for _ in range(8):
if (byte ^ crc) & 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
byte >>= 1
result.append(crc)
return result
crc16_table: list[int] = [0]

def decode(self, _data: bytes) -> tuple[int, int, int, bytes]:
"""Decode message."""
return 0, 0, 0, b''

def encode(self, _data: bytes, _device_id: int, _tid: int) -> bytes:
def encode(self, data: bytes, device_id: int, _tid: int) -> bytes:
"""Decode message."""
return b''
packet = device_id.to_bytes(1,'big') + data
return packet + MessageRTU.compute_CRC(packet).to_bytes(2,'big')

@classmethod
def check_CRC(cls, data: bytes, check: int) -> bool:
"""Check if the data matches the passed in CRC.
:param data: The data to create a crc16 of
:param check: The CRC to validate
:returns: True if matched, False otherwise
"""
return cls.compute_CRC(data) == check

@classmethod
def compute_CRC(cls, data: bytes) -> int:
"""Compute a crc16 on the passed in bytes.
For modbus, this is only used on the binary serial protocols (in this
case RTU).
The difference between modbus's crc16 and a normal crc16
is that modbus starts the crc value out at 0xffff.
:param data: The data to create a crc16 of
:returns: The calculated CRC
"""
crc = 0xFFFF
for data_byte in data:
idx = cls.crc16_table[(crc ^ int(data_byte)) & 0xFF]
crc = ((crc >> 8) & 0xFF) ^ idx
swapped = ((crc << 8) & 0xFF00) | ((crc >> 8) & 0x00FF)
return swapped

MessageRTU.crc16_table = MessageRTU.generate_crc16_table()
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,11 @@ source = [
"pymodbus/",
"test/",
]
omit = ["examples/contrib/"]
omit = [
"examples/contrib/",
"test/message/to_do*",
"test/message/generator.py",
]
branch = true

[tool.coverage.report]
Expand Down
46 changes: 46 additions & 0 deletions test/message/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Build framer encode responses."""

from pymodbus.factory import ClientDecoder, ServerDecoder
from pymodbus.framer import (
ModbusAsciiFramer,
ModbusRtuFramer,
ModbusSocketFramer,
ModbusTlsFramer,
)
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.register_read_message import (
ReadHoldingRegistersRequest,
ReadHoldingRegistersResponse,
)


def set_calls():
"""Define calls."""
for framer in (ModbusAsciiFramer, ModbusRtuFramer, ModbusSocketFramer, ModbusTlsFramer):
print(f"framer --> {framer}")
for dev_id in (0, 17, 255):
print(f" dev_id --> {dev_id}")
for tid in (0, 3077):
print(f" tid --> {tid}")
client = framer(ClientDecoder())
request = ReadHoldingRegistersRequest(124, 2, dev_id)
request.transaction_id = tid
result = client.buildPacket(request)
print(f" request --> {result}")
print(f" request --> {result.hex()}")
server = framer(ServerDecoder())
response = ReadHoldingRegistersResponse([141,142])
response.slave_id = dev_id
response.transaction_id = tid
result = server.buildPacket(response)
print(f" response --> {result}")
print(f" response --> {result.hex()}")
exception = request.doException(merror.IllegalAddress)
exception.transaction_id = tid
exception.slave_id = dev_id
result = server.buildPacket(exception)
print(f" exception --> {result}")
print(f" exception --> {result.hex()}")

set_calls()
19 changes: 1 addition & 18 deletions test/message/test_ascii.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""Test transport."""
import struct

import pytest

from pymodbus.message.ascii import MessageAscii
Expand All @@ -16,24 +14,9 @@ def prepare_frame():
return MessageAscii([1], False)


def test_check_LRC(self):
"""Test check_LRC."""
data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
assert MessageAscii.check_LRC(data, 0x1C)

def test_check_noLRC(self):
"""Test check_LRC."""
data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
assert not MessageAscii.check_LRC(data, 0x0C)

def test_compute_LRC(self):
"""Test compute_LRC."""
data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
assert MessageAscii.compute_LRC(data) == 0x1c

def test_roundtrip_LRC(self):
"""Test combined compute/check LRC."""
data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
data = b'\x12\x34\x23\x45\x34\x56\x45\x67'
assert MessageAscii.compute_LRC(data) == 0x1c
assert MessageAscii.check_LRC(data, 0x1C)

Expand Down
109 changes: 109 additions & 0 deletions test/message/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import pytest

from pymodbus.message import MessageType
from pymodbus.message.ascii import MessageAscii
from pymodbus.message.rtu import MessageRTU
from pymodbus.message.socket import MessageSocket
from pymodbus.message.tls import MessageTLS
from pymodbus.transport import CommParams


Expand Down Expand Up @@ -96,3 +100,108 @@ async def test_encode(self, msg, data, dev_id, tid, res_data):
"""Test decode method in all types."""
t_data = msg.msg_handle.encode(data, dev_id, tid)
assert res_data == t_data

@pytest.mark.parametrize(
("func", "lrc", "expect"),
[(MessageAscii.check_LRC, 0x1c, True),
(MessageAscii.check_LRC, 0x0c, False),
(MessageAscii.compute_LRC, None, 0x1c),
(MessageRTU.check_CRC, 0xE2DB, True),
(MessageRTU.check_CRC, 0xDBE2, False),
(MessageRTU.compute_CRC, None, 0xE2DB),
]
)
def test_LRC_CRC(self, func, lrc, expect):
"""Test check_LRC."""
data = b'\x12\x34\x23\x45\x34\x56\x45\x67'
assert expect == func(data, lrc) if lrc else func(data)


class TestMessages: # pylint: disable=too-few-public-methods
"""Test message classes."""

@pytest.mark.parametrize(
("frame", "frame_expected"),
[
(MessageAscii, [
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',
]),
(MessageRTU, [
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|\x00\x02\x10\x0d',
b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3',
b'\xff\x83\x02\xa1\x01',
]),
(MessageSocket, [
b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02',
b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e',
b'\x00\x00\x00\x00\x00\x03\x00\x83\x02',
b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02',
b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e',
b'\x00\x00\x00\x00\x00\x03\x11\x83\x02',
b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02',
b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e',
b'\x00\x00\x00\x00\x00\x03\xff\x83\x02',
b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02',
b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e',
b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02',
b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02',
b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e',
b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02',
b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02',
b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e',
b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02',
]),
(MessageTLS, [
b'\x03\x00\x7c\x00\x02',
b'\x03\x04\x00\x8d\x00\x8e',
b'\x83\x02',
]),
]
)
@pytest.mark.parametrize(
("inx1", "data"),
[
(0, b"\x03\x00\x7c\x00\x02",), # Request
(1, b"\x03\x04\x00\x8d\x00\x8e",), # Response
(2, b'\x83\x02',), # Exception
]
)
@pytest.mark.parametrize(
("inx2", "dev_id"),
[
(0, 0),
(3, 17),
(6, 255),
]
)
@pytest.mark.parametrize(
("inx3", "tid"),
[
(0, 0),
(9, 3077),
]
)
def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3):
"""Test encode method."""
if frame != MessageSocket and tid:
return
if frame == MessageTLS and (tid or dev_id):
return
frame_obj = frame(None, True)
expected = frame_expected[inx1 + inx2 + inx3]
encoded_data = frame_obj.encode(data, dev_id, tid)
assert encoded_data == expected
101 changes: 101 additions & 0 deletions test/message/test_rtu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Test transport."""
import pytest

from pymodbus.message.rtu import MessageRTU


class TestMessageRTU:
"""Test message module."""

@staticmethod
@pytest.fixture(name="frame")
def prepare_frame():
"""Return message object."""
return MessageRTU([1], False)

def test_crc16_table(self):
"""Test the crc16 table is prefilled."""
assert len(MessageRTU.crc16_table) == 256
assert isinstance(MessageRTU.crc16_table[0], int)
assert isinstance(MessageRTU.crc16_table[255], int)

def test_roundtrip_CRC(self):
"""Test combined compute/check CRC."""
data = b'\x12\x34\x23\x45\x34\x56\x45\x67'
assert MessageRTU.compute_CRC(data) == 0xE2DB
assert MessageRTU.check_CRC(data, 0xE2DB)

# b"\x02\x01\x01\x00Q\xcc"
# b"\x01\x01\x03\x01\x00\n\xed\x89"
# b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x43"
# b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD"

# b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03" # good frame + part of next frame

# b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAC" # invalid frame CRC
# b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAC" # bad crc
# b"\x61\x62\x00\x01\x00\n\xec\x1c" # bad function code
# b"\x01\x03\x03\x01\x00\n\x94\x49" # Not ok

# test frame ready
# (b"", False),
# (b"\x11", False),
# (b"\x11\x03", False),
# (b"\x11\x03\x06", False),
# (b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49", False),
# (b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD", True),
# (b"\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\xAB\xCD", True),


@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 xtest_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 not tid
assert dev_id == res_id

@pytest.mark.parametrize(
("data", "dev_id", "res_msg"),
[
(b'\x01\x01\x00', 2, b'\x02\x01\x01\x00\x51\xcc'),
(b'\x03\x06\xAE\x41\x56\x52\x43\x40', 17, b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD'),
(b'\x01\x03\x01\x00\x0a', 1, b'\x01\x01\x03\x01\x00\x0a\xed\x89'),
],
)
def test_encode(self, frame, data, dev_id, res_msg):
"""Test encode."""
msg = frame.encode(data, dev_id, 0)
assert res_msg == msg
assert dev_id == int(msg[0])

@pytest.mark.parametrize(
("data", "dev_id", "res_msg"),
[
(b'\x01\x01\x00', 2, b'\x02\x01\x01\x00\x51\xcc'),
(b'\x03\x06\xAE\x41\x56\x52\x43\x40', 17, b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD'),
(b'\x01\x03\x01\x00\x0a', 1, b'\x01\x01\x03\x01\x00\x0a\xed\x89'),
],
)
def xtest_roundtrip(self, frame, data, dev_id, res_msg):
"""Test encode."""
msg = frame.encode(data, dev_id, 0)
res_len, _, res_id, res_data = frame.decode(msg)
assert data == res_data
assert dev_id == res_id
assert res_len == len(res_msg)
Loading

0 comments on commit 29d3558

Please sign in to comment.