Skip to content

Commit 258e4f7

Browse files
committed
ASCII.
1 parent 48d90c1 commit 258e4f7

File tree

12 files changed

+211
-96
lines changed

12 files changed

+211
-96
lines changed

pymodbus/framer/ascii_framer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pymodbus.exceptions import ModbusIOException
77
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
88
from pymodbus.logging import Log
9-
from pymodbus.utilities import checkLRC, computeLRC
9+
from pymodbus.message.ascii import MessageAscii
1010

1111

1212
ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
@@ -73,7 +73,7 @@ def checkFrame(self):
7373
self._header["uid"] = int(self._buffer[1:3], 16)
7474
self._header["lrc"] = int(self._buffer[end - 2 : end], 16)
7575
data = a2b_hex(self._buffer[start + 1 : end - 2])
76-
return checkLRC(data, self._header["lrc"])
76+
return MessageAscii.check_LRC(data, self._header["lrc"])
7777
return False
7878

7979
def advanceFrame(self):
@@ -141,7 +141,7 @@ def buildPacket(self, message):
141141
buffer = struct.pack(
142142
ASCII_FRAME_HEADER, message.slave_id, message.function_code
143143
)
144-
checksum = computeLRC(encoded + buffer)
144+
checksum = MessageAscii.compute_LRC(buffer + encoded)
145145

146146
packet = bytearray()
147147
packet.extend(self._start)

pymodbus/message/ascii.py

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,73 @@
66
"""
77
from __future__ import annotations
88

9+
from binascii import a2b_hex, b2a_hex
10+
11+
from pymodbus.logging import Log
912
from pymodbus.message.base import MessageBase
1013

1114

1215
class MessageAscii(MessageBase):
13-
"""Modbus Socket frame type.
16+
r"""Modbus ASCII Frame Controller.
17+
18+
[ Start ][Address ][ Function ][ Data ][ LRC ][ End ]
19+
1c 2c 2c Nc 1c 2c
1420
15-
[ MBAP Header ] [ Function Code] [ Data ]
16-
[ tid ][ pid ][ length ][ uid ]
17-
2b 2b 2b 1b 1b Nb
21+
* data can be 0 - 2x252 chars
22+
* end is "\\r\\n" (Carriage return line feed), however the line feed
23+
character can be changed via a special command
24+
* start is ":"
1825
19-
* length = uid + function code + data
26+
This framer is used for serial transmission. Unlike the RTU protocol,
27+
the data in this framer is transferred in plain text ascii.
2028
"""
2129

22-
def reset(self) -> None:
23-
"""Clear internal handling."""
30+
START = b':'
31+
END = b'\r\n'
32+
2433

25-
def decode(self, _data: bytes) -> tuple[int, int, int, bytes]:
34+
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
2635
"""Decode message."""
27-
return 0, 0, 0, b''
36+
if (used_len := len(data)) < 10:
37+
Log.debug("Short frame: {} wait for more data", data, ":hex")
38+
return 0, 0, 0, self.EMPTY
39+
if data[0:1] != self.START:
40+
if (start := data.find(self.START)) != -1:
41+
used_len = start
42+
Log.debug("Garble data before frame: {}, skip until start of frame", data, ":hex")
43+
return used_len, 0, 0, self.EMPTY
44+
if (used_len := data.find(self.END)) == -1:
45+
Log.debug("Incomplete frame: {} wait for more data", data, ":hex")
46+
return 0, 0, 0, self.EMPTY
2847

29-
def encode(self, _data: bytes, _device_id: int, _tid: int) -> bytes:
48+
dev_id = int(data[1:3], 16)
49+
lrc = int(data[used_len - 2: used_len], 16)
50+
msg = a2b_hex(data[1 : used_len - 2])
51+
if not self.check_LRC(msg, lrc):
52+
Log.debug("LRC wrong in frame: {} skipping", data, ":hex")
53+
return used_len+2, 0, 0, self.EMPTY
54+
return used_len+2, 0, dev_id, msg[1:]
55+
56+
def encode(self, data: bytes, device_id: int, _tid: int) -> bytes:
3057
"""Decode message."""
31-
return b''
58+
dev_id = device_id.to_bytes(1,'big')
59+
checksum = self.compute_LRC(dev_id + data)
60+
packet = bytearray()
61+
packet.extend(self.START)
62+
packet.extend(f"{device_id:02x}".encode())
63+
packet.extend(b2a_hex(data))
64+
packet.extend(f"{checksum:02x}".encode())
65+
packet.extend(self.END)
66+
return bytes(packet).upper()
67+
68+
@classmethod
69+
def compute_LRC(cls, data: bytes) -> int:
70+
"""Use to compute the longitudinal redundancy check against a string."""
71+
lrc = sum(int(a) for a in data) & 0xFF
72+
lrc = (lrc ^ 0xFF) + 1
73+
return lrc & 0xFF
74+
75+
@classmethod
76+
def check_LRC(cls, data: bytes, check: int) -> bool:
77+
"""Check if the passed in data matches the LRC."""
78+
return cls.compute_LRC(data) == check

pymodbus/message/base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
class MessageBase:
1313
"""Intern base."""
1414

15+
EMPTY = b''
16+
1517
def __init__(
1618
self,
1719
device_ids: list[int] | None,
@@ -24,9 +26,9 @@ def __init__(
2426
self.device_ids = device_ids
2527
self.is_server = is_server
2628

27-
@abstractmethod
28-
def reset(self) -> None:
29-
"""Clear internal handling."""
29+
def validate_device_id(self, dev_id: int) -> bool:
30+
"""Check if device id is expected."""
31+
return not (self.device_ids and dev_id not in self.device_ids)
3032

3133
@abstractmethod
3234
def decode(self, _data: bytes) -> tuple[int, int, int, bytes]:

pymodbus/message/message.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,17 @@ def __init__(self,
7676

7777
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
7878
"""Handle received data."""
79-
used_len, tid, device_id, data = self.msg_handle.decode(data)
80-
if data:
81-
self.callback_request_response(data, device_id, tid)
82-
return used_len
79+
tot_len = len(data)
80+
start = 0
81+
while True:
82+
used_len, tid, device_id, msg = self.msg_handle.decode(data[start:])
83+
if msg:
84+
self.callback_request_response(msg, device_id, tid)
85+
if not used_len:
86+
return start
87+
start += used_len
88+
if start == tot_len:
89+
return tot_len
8390

8491
# --------------------- #
8592
# callbacks and helpers #
@@ -98,7 +105,3 @@ def build_send(self, data: bytes, device_id: int, tid: int, addr: tuple | None =
98105
"""
99106
send_data = self.msg_handle.encode(data, device_id, tid)
100107
self.send(send_data, addr)
101-
102-
def reset(self) -> None:
103-
"""Reset handling."""
104-
self.msg_handle.reset()

pymodbus/message/raw.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
"""ModbusMessage layer."""
22
from __future__ import annotations
33

4+
from pymodbus.logging import Log
45
from pymodbus.message.base import MessageBase
56

67

78
class MessageRaw(MessageBase):
8-
"""Raw header.
9+
r"""Modbus RAW Frame Controller.
910
10-
HEADER:
11-
byte[0] = device_id
12-
byte[1] = transaction_id
13-
byte[2..] = request/response
11+
[ Device id ][Transaction id ][ Data ]
12+
1c 2c Nc
1413
15-
This is mainly for test purposes.
16-
"""
14+
* data can be 1 - X chars
1715
18-
def reset(self) -> None:
19-
"""Clear internal handling."""
16+
This framer is used for non modbus communication and testing purposes.
17+
"""
2018

2119
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
2220
"""Decode message."""
2321
if len(data) < 3:
24-
return 0, 0, 0, b''
25-
return len(data), int(data[0]), int(data[1]), data[2:]
22+
Log.debug("Short frame: {} wait for more data", data, ":hex")
23+
return 0, 0, 0, self.EMPTY
24+
dev_id = int(data[0])
25+
tid = int(data[1])
26+
if not self.validate_device_id(dev_id):
27+
Log.debug("Device id: {} in frame {} unknown, skipping.", dev_id, data, ":hex")
28+
29+
return len(data), dev_id, tid, data[2:]
2630

2731
def encode(self, data: bytes, device_id: int, tid: int) -> bytes:
2832
"""Decode message."""

pymodbus/message/rtu.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ class MessageRTU(MessageBase):
4040
neither when receiving nor when sending.
4141
"""
4242

43-
def reset(self) -> None:
44-
"""Clear internal handling."""
45-
4643
def decode(self, _data: bytes) -> tuple[int, int, int, bytes]:
4744
"""Decode message."""
4845
return 0, 0, 0, b''

pymodbus/message/socket.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ class MessageSocket(MessageBase):
1919
* length = uid + function code + data
2020
"""
2121

22-
def reset(self) -> None:
23-
"""Clear internal handling."""
24-
2522
def decode(self, _data: bytes) -> tuple[int, int, int, bytes]:
2623
"""Decode message."""
2724
return 0, 0, 0, b''

pymodbus/message/tls.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ class MessageTLS(MessageBase):
1616
1b Nb
1717
"""
1818

19-
def reset(self) -> None:
20-
"""Clear internal handling."""
21-
2219
def decode(self, _data: bytes) -> tuple[int, int, int, bytes]:
2320
"""Decode message."""
2421
return 0, 0, 0, b''

pymodbus/utilities.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
"default",
1313
"computeCRC",
1414
"checkCRC",
15-
"computeLRC",
16-
"checkLRC",
1715
"rtuFrameSize",
1816
]
1917

@@ -205,32 +203,6 @@ def checkCRC(data, check): # pylint: disable=invalid-name
205203
return computeCRC(data) == check
206204

207205

208-
def computeLRC(data): # pylint: disable=invalid-name
209-
"""Use to compute the longitudinal redundancy check against a string.
210-
211-
This is only used on the serial ASCII
212-
modbus protocol. A full description of this implementation
213-
can be found in appendix B of the serial line modbus description.
214-
215-
:param data: The data to apply a lrc to
216-
:returns: The calculated LRC
217-
218-
"""
219-
lrc = sum(int(a) for a in data) & 0xFF
220-
lrc = (lrc ^ 0xFF) + 1
221-
return lrc & 0xFF
222-
223-
224-
def checkLRC(data, check): # pylint: disable=invalid-name
225-
"""Check if the passed in data matches the LRC.
226-
227-
:param data: The data to calculate
228-
:param check: The LRC to validate
229-
:returns: True if matched, False otherwise
230-
"""
231-
return computeLRC(data) == check
232-
233-
234206
def rtuFrameSize(data, byte_count_pos): # pylint: disable=invalid-name
235207
"""Calculate the size of the frame based on the byte count.
236208

test/message/test_ascii.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Test transport."""
2+
import struct
3+
4+
import pytest
5+
6+
from pymodbus.message.ascii import MessageAscii
7+
8+
9+
class TestMessageAscii:
10+
"""Test message module."""
11+
12+
@staticmethod
13+
@pytest.fixture(name="frame")
14+
def prepare_frame():
15+
"""Return message object."""
16+
return MessageAscii([1], False)
17+
18+
19+
def test_check_LRC(self):
20+
"""Test check_LRC."""
21+
data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
22+
assert MessageAscii.check_LRC(data, 0x1C)
23+
24+
def test_check_noLRC(self):
25+
"""Test check_LRC."""
26+
data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
27+
assert not MessageAscii.check_LRC(data, 0x0C)
28+
29+
def test_compute_LRC(self):
30+
"""Test compute_LRC."""
31+
data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
32+
assert MessageAscii.compute_LRC(data) == 0x1c
33+
34+
def test_roundtrip_LRC(self):
35+
"""Test combined compute/check LRC."""
36+
data = struct.pack(">HHHH", 0x1234, 0x2345, 0x3456, 0x4567)
37+
assert MessageAscii.compute_LRC(data) == 0x1c
38+
assert MessageAscii.check_LRC(data, 0x1C)
39+
40+
@pytest.mark.parametrize(
41+
("packet", "used_len", "res_id", "res"),
42+
[
43+
(b':010100010001FC\r\n', 17, 1, b'\x01\x00\x01\x00\x01'),
44+
(b':00010001000AF4\r\n', 17, 0, b'\x01\x00\x01\x00\x0a'),
45+
(b':01010001000AF3\r\n', 17, 1, b'\x01\x00\x01\x00\x0a'),
46+
(b':61620001000A32\r\n', 17, 97, b'\x62\x00\x01\x00\x0a'),
47+
(b':01270001000ACD\r\n', 17, 1, b'\x27\x00\x01\x00\x0a'),
48+
(b':010100', 0, 0, b''), # short frame
49+
(b':00010001000AF4', 0, 0, b''),
50+
(b'abc:00010001000AF4', 3, 0, b''), # garble before frame
51+
(b'abc00010001000AF4', 17, 0, b''), # only garble
52+
(b':01010001000A00\r\n', 17, 0, b''),
53+
],
54+
)
55+
def test_decode(self, frame, packet, used_len, res_id, res):
56+
"""Test decode."""
57+
res_len, tid, dev_id, data = frame.decode(packet)
58+
assert res_len == used_len
59+
assert data == res
60+
assert not tid
61+
assert dev_id == res_id
62+
63+
@pytest.mark.parametrize(
64+
("data", "dev_id", "res_msg"),
65+
[
66+
(b'\x01\x05\x04\x00\x17', 1, b':010105040017DE\r\n'),
67+
(b'\x03\x07\x06\x00\x73', 2, b':0203070600737B\r\n'),
68+
(b'\x08\x00\x01', 3, b':03080001F4\r\n'),
69+
],
70+
)
71+
def test_encode(self, frame, data, dev_id, res_msg):
72+
"""Test encode."""
73+
msg = frame.encode(data, dev_id, 0)
74+
assert res_msg == msg
75+
assert dev_id == int(msg[1:3], 16)
76+
77+
@pytest.mark.parametrize(
78+
("data", "dev_id", "res_msg"),
79+
[
80+
(b'\x01\x05\x04\x00\x17', 1, b':010105040017DF\r\n'),
81+
(b'\x03\x07\x06\x00\x73', 2, b':0203070600737D\r\n'),
82+
(b'\x08\x00\x01', 3, b':03080001F7\r\n'),
83+
],
84+
)
85+
def test_roundtrip(self, frame, data, dev_id, res_msg):
86+
"""Test encode."""
87+
msg = frame.encode(data, dev_id, 0)
88+
res_len, _, res_id, res_data = frame.decode(msg)
89+
assert data == res_data
90+
assert dev_id == res_id
91+
assert res_len == len(res_msg)

0 commit comments

Comments
 (0)