11"""Modbus RTU frame implementation."""
22from __future__ import annotations
33
4- import struct
4+ from collections import namedtuple
55
6- from pymodbus .exceptions import ModbusIOException
7- from pymodbus .factory import ClientDecoder
86from pymodbus .framer .base import FramerBase
97from pymodbus .logging import Log
108
@@ -17,26 +15,53 @@ class FramerRTU(FramerBase):
1715
1816 * Note: due to the USB converter and the OS drivers, timing cannot be quaranteed
1917 neither when receiving nor when sending.
18+
19+ Decoding is a complicated process because the RTU frame does not have a fixed prefix
20+ only suffix, therefore it is necessary to decode the content (PDU) to get length etc.
21+
22+ There are some protocol restrictions that help with the detection.
23+
24+ For client:
25+ - a request causes 1 response !
26+ - Multiple requests are NOT allowed (master-slave protocol)
27+ - the server will not retransmit responses
28+ this means decoding is always exactly 1 frame (response)
29+
30+ For server (Single device)
31+ - only 1 request allowed (master-slave) protocol
32+ - the client (master) may retransmit but in larger time intervals
33+ this means decoding is always exactly 1 frame (request)
34+
35+ For server (Multidrop line --> devices in parallel)
36+ - only 1 request allowed (master-slave) protocol
37+ - other devices will send responses
38+ - the client (master) may retransmit but in larger time intervals
39+ this means decoding is always exactly 1 frame request, however some requests
40+ will be for unknown slaves, which must be ignored together with the
41+ response from the unknown slave.
42+
43+ Recovery from bad cabling and unstable USB etc is important,
44+ the following scenarios is possible:
45+ - garble data before frame
46+ - garble data in frame
47+ - garble data after frame
48+ - data in frame garbled (wrong CRC)
49+ decoding assumes the frame is sound, and if not enters a hunting mode.
50+
51+ The 3.5 byte transmission time at the slowest speed 1.200Bps is 31ms.
52+ Device drivers will typically flush buffer after 10ms of silence.
53+ If no data is received for 50ms the transmission / frame can be considered
54+ complete.
2055 """
2156
2257 MIN_SIZE = 5
2358
59+ FC_LEN = namedtuple ("FC_LEN" , "req_len req_bytepos resp_len resp_bytepos" )
60+
2461 def __init__ (self ) -> None :
2562 """Initialize a ADU instance."""
2663 super ().__init__ ()
27- self .broadcast : bool = False
28- self .dev_ids : list [int ]
29- self .fc_calc : dict [int , int ]
30-
31- def set_dev_ids (self , dev_ids : list [int ]):
32- """Set/update allowed device ids."""
33- if 0 in dev_ids :
34- self .broadcast = True
35- self .dev_ids = dev_ids
36-
37- def set_fc_calc (self , fc : int , msg_size : int , count_pos : int ):
38- """Set/Update function code information."""
39- self .fc_calc [fc ] = msg_size if not count_pos else - count_pos
64+ self .fc_len : dict [int , FramerRTU .FC_LEN ] = {}
4065
4166
4267 @classmethod
@@ -58,120 +83,39 @@ def generate_crc16_table(cls) -> list[int]:
5883 return result
5984 crc16_table : list [int ] = [0 ]
6085
61- def _legacy_decode (self , callback , slave ): # noqa: C901
62- """Process new packet pattern."""
63-
64- def is_frame_ready (self ):
65- """Check if we should continue decode logic."""
66- size = self ._header .get ("len" , 0 )
67- if not size and len (self ._buffer ) > self ._hsize :
68- try :
69- self ._header ["uid" ] = int (self ._buffer [0 ])
70- self ._header ["tid" ] = int (self ._buffer [0 ])
71- func_code = int (self ._buffer [1 ])
72- pdu_class = self .decoder .lookupPduClass (func_code )
73- size = pdu_class .calculateRtuFrameSize (self ._buffer )
74- self ._header ["len" ] = size
75-
76- if len (self ._buffer ) < size :
77- raise IndexError
78- self ._header ["crc" ] = self ._buffer [size - 2 : size ]
79- except IndexError :
80- return False
81- return len (self ._buffer ) >= size if size > 0 else False
82-
83- def get_frame_start (self , slaves , broadcast , skip_cur_frame ):
84- """Scan buffer for a relevant frame start."""
85- start = 1 if skip_cur_frame else 0
86- if (buf_len := len (self ._buffer )) < 4 :
87- return False
88- for i in range (start , buf_len - 3 ): # <slave id><function code><crc 2 bytes>
89- if not broadcast and self ._buffer [i ] not in slaves :
90- continue
91- if (
92- self ._buffer [i + 1 ] not in self .function_codes
93- and (self ._buffer [i + 1 ] - 0x80 ) not in self .function_codes
94- ):
95- continue
96- if i :
97- self ._buffer = self ._buffer [i :] # remove preceding trash.
98- return True
99- if buf_len > 3 :
100- self ._buffer = self ._buffer [- 3 :]
101- return False
102-
103- def check_frame (self ):
104- """Check if the next frame is available."""
105- try :
106- self ._header ["uid" ] = int (self ._buffer [0 ])
107- self ._header ["tid" ] = int (self ._buffer [0 ])
108- func_code = int (self ._buffer [1 ])
109- pdu_class = self .decoder .lookupPduClass (func_code )
110- size = pdu_class .calculateRtuFrameSize (self ._buffer )
111- self ._header ["len" ] = size
112-
113- if len (self ._buffer ) < size :
114- raise IndexError
115- self ._header ["crc" ] = self ._buffer [size - 2 : size ]
116- frame_size = self ._header ["len" ]
117- data = self ._buffer [: frame_size - 2 ]
118- crc = self ._header ["crc" ]
119- crc_val = (int (crc [0 ]) << 8 ) + int (crc [1 ])
120- return FramerRTU .check_CRC (data , crc_val )
121- except (IndexError , KeyError , struct .error ):
122- return False
123-
124- self ._buffer = b'' # pylint: disable=attribute-defined-outside-init
125- broadcast = not slave [0 ]
126- skip_cur_frame = False
127- while get_frame_start (self , slave , broadcast , skip_cur_frame ):
128- self ._header : dict = {"uid" : 0x00 , "len" : 0 , "crc" : b"\x00 \x00 " } # pylint: disable=attribute-defined-outside-init
129- if not is_frame_ready (self ):
130- Log .debug ("Frame - not ready" )
131- break
132- if not check_frame (self ):
133- Log .debug ("Frame check failed, ignoring!!" )
134- # x = self._buffer
135- # self.resetFrame()
136- # self._buffer = x
137- skip_cur_frame = True
138- continue
139- start = 0x01 # self._hsize
140- end = self ._header ["len" ] - 2
141- buffer = self ._buffer [start :end ]
142- if end > 0 :
143- Log .debug ("Getting Frame - {}" , buffer , ":hex" )
144- data = buffer
145- else :
146- data = b""
147- if (result := ClientDecoder ().decode (data )) is None :
148- raise ModbusIOException ("Unable to decode request" )
149- result .slave_id = self ._header ["uid" ]
150- result .transaction_id = self ._header ["tid" ]
151- self ._buffer = self ._buffer [self ._header ["len" ] :] # pylint: disable=attribute-defined-outside-init
152- Log .debug ("Frame advanced, resetting header!!" )
153- callback (result ) # defer or push to a thread?
154-
155-
156- def hunt_frame_start (self , skip_cur_frame : bool , data : bytes ) -> int :
157- """Scan buffer for a relevant frame start."""
158- buf_len = len (data )
159- for i in range (1 if skip_cur_frame else 0 , buf_len - self .MIN_SIZE ):
160- if not (self .broadcast or data [i ] in self .dev_ids ):
161- continue
162- if (_fc := data [i + 1 ]) not in self .fc_calc :
163- continue
164- return i
165- return - i
86+
87+ def setup_fc_len (self , _fc : int ,
88+ _req_len : int , _req_byte_pos : int ,
89+ _resp_len : int , _resp_byte_pos : int
90+ ):
91+ """Define request/response lengths pr function code."""
92+ return
16693
16794 def decode (self , data : bytes ) -> tuple [int , int , int , bytes ]:
16895 """Decode ADU."""
169- if len (data ) < self .MIN_SIZE :
96+ if (buf_len := len (data )) < self .MIN_SIZE :
97+ Log .debug ("Short frame: {} wait for more data" , data , ":hex" )
17098 return 0 , 0 , 0 , b''
17199
172- while (i := self .hunt_frame_start (False , data )) > 0 :
173- pass
174- return - i , 0 , 0 , b''
100+ i = - 1
101+ try :
102+ while True :
103+ i += 1
104+ if i > buf_len - self .MIN_SIZE + 1 :
105+ break
106+ dev_id = int (data [i ])
107+ fc_len = 5
108+ msg_len = fc_len - 2 if fc_len > 0 else int (data [i - fc_len ])- fc_len + 1
109+ if msg_len + i + 2 > buf_len :
110+ break
111+ crc_val = (int (data [i + msg_len ]) << 8 ) + int (data [i + msg_len + 1 ])
112+ if not self .check_CRC (data [i :i + msg_len ], crc_val ):
113+ Log .debug ("Skipping frame CRC with len {} at index {}!" , msg_len , i )
114+ raise KeyError
115+ return i + msg_len + 2 , dev_id , dev_id , data [i + 1 :i + msg_len ]
116+ except KeyError :
117+ i = buf_len
118+ return i , 0 , 0 , b''
175119
176120
177121 def encode (self , pdu : bytes , device_id : int , _tid : int ) -> bytes :
0 commit comments