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 4, 2024
1 parent 65a6b9b commit 640d75a
Show file tree
Hide file tree
Showing 12 changed files with 672 additions and 413 deletions.
6 changes: 3 additions & 3 deletions pymodbus/framer/binary_framer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pymodbus.exceptions import ModbusIOException
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
from pymodbus.logging import Log
from pymodbus.utilities import checkCRC, computeCRC
from pymodbus.message.rtu import MessageRTU


BINARY_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
Expand Down Expand Up @@ -76,7 +76,7 @@ def check_frame(self) -> bool:
self._header["uid"] = struct.unpack(">B", self._buffer[1:2])[0]
self._header["crc"] = struct.unpack(">H", self._buffer[end - 2 : end])[0]
data = self._buffer[1 : end - 2]
return checkCRC(data, self._header["crc"])
return MessageRTU.check_CRC(data, self._header["crc"])
return False

while len(self._buffer) > 1:
Expand Down Expand Up @@ -113,7 +113,7 @@ def buildPacket(self, message):
struct.pack(BINARY_FRAME_HEADER, message.slave_id, message.function_code)
+ data
)
packet += struct.pack(">H", computeCRC(packet))
packet += struct.pack(">H", MessageRTU.compute_CRC(packet))
packet = self._start + packet + self._end
return packet

Expand Down
7 changes: 4 additions & 3 deletions pymodbus/framer/rtu_framer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
)
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
from pymodbus.logging import Log
from pymodbus.utilities import ModbusTransactionState, checkCRC, computeCRC
from pymodbus.message.rtu import MessageRTU
from pymodbus.utilities import ModbusTransactionState


RTU_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
Expand Down Expand Up @@ -131,7 +132,7 @@ def check_frame(self):
data = self._buffer[: frame_size - 2]
crc = self._header["crc"]
crc_val = (int(crc[0]) << 8) + int(crc[1])
return checkCRC(data, crc_val)
return MessageRTU.check_CRC(data, crc_val)
except (IndexError, KeyError, struct.error):
return False

Expand Down Expand Up @@ -175,7 +176,7 @@ def buildPacket(self, message):
struct.pack(RTU_FRAME_HEADER, message.slave_id, message.function_code)
+ data
)
packet += struct.pack(">H", computeCRC(packet))
packet += struct.pack(">H", MessageRTU.compute_CRC(packet))
# Ensure that transaction is actually the slave id for serial comms
if message.slave_id:
message.transaction_id = message.slave_id
Expand Down
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()
53 changes: 0 additions & 53 deletions pymodbus/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
"pack_bitstring",
"unpack_bitstring",
"default",
"computeCRC",
"checkCRC",
"rtuFrameSize",
]

Expand Down Expand Up @@ -152,57 +150,6 @@ def unpack_bitstring(data: bytes) -> list[bool]:
# --------------------------------------------------------------------------- #


def __generate_crc16_table():
"""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 = __generate_crc16_table()


def computeCRC(data): # pylint: disable=invalid-name
"""Compute a crc16 on the passed in string.
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 = __crc16_table[(crc ^ int(data_byte)) & 0xFF]
crc = ((crc >> 8) & 0xFF) ^ idx
swapped = ((crc << 8) & 0xFF00) | ((crc >> 8) & 0x00FF)
return swapped


def checkCRC(data, check): # pylint: disable=invalid-name
"""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 computeCRC(data) == check


def rtuFrameSize(data, byte_count_pos): # pylint: disable=invalid-name
"""Calculate the size of the frame based on the byte count.
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
112 changes: 112 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,111 @@ 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'
if lrc:
assert expect == func(data, lrc)
else:
assert expect == func(data)


class TestMessages:
"""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
Loading

0 comments on commit 640d75a

Please sign in to comment.