@@ -32,8 +32,285 @@ class ModbusBaseClient(ModbusClientMixin, ModbusProtocol):
3232 :param close_comm_on_error: Close connection on error.
3333 :param strict: Strict timing, 1.5 character between requests.
3434 :param broadcast_enable: True to treat id 0 as broadcast address.
35- :param reconnect_delay: Minimum delay in milliseconds before reconnecting.
36- :param reconnect_delay_max: Maximum delay in milliseconds before reconnecting.
35+ :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
36+ :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
37+ :param on_reconnect_callback: Function that will be called just before a reconnection attempt.
38+ :param no_resend_on_retry: Do not resend request when retrying due to missing response.
39+ :param kwargs: Experimental parameters.
40+
41+ .. tip::
42+ **reconnect_delay** doubles automatically with each unsuccessful connect, from
43+ **reconnect_delay** to **reconnect_delay_max**.
44+ Set `reconnect_delay=0` to avoid automatic reconnection.
45+
46+ :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
47+
48+ **Application methods, common to all clients**:
49+ """
50+
51+ def __init__ ( # pylint: disable=too-many-arguments
52+ self ,
53+ framer : Framer ,
54+ timeout : float = 3 ,
55+ retries : int = 3 ,
56+ retry_on_empty : bool = False ,
57+ close_comm_on_error : bool = False ,
58+ strict : bool = True ,
59+ broadcast_enable : bool = False ,
60+ reconnect_delay : float = 0.1 ,
61+ reconnect_delay_max : float = 300 ,
62+ on_reconnect_callback : Callable [[], None ] | None = None ,
63+ no_resend_on_retry : bool = False ,
64+ ** kwargs : Any ,
65+ ) -> None :
66+ """Initialize a client instance."""
67+ ModbusClientMixin .__init__ (self )
68+ ModbusProtocol .__init__ (
69+ self ,
70+ CommParams (
71+ comm_type = kwargs .get ("CommType" ),
72+ comm_name = "comm" ,
73+ source_address = kwargs .get ("source_address" , ("0.0.0.0" , 0 )),
74+ reconnect_delay = reconnect_delay ,
75+ reconnect_delay_max = reconnect_delay_max ,
76+ timeout_connect = timeout ,
77+ host = kwargs .get ("host" , None ),
78+ port = kwargs .get ("port" , 0 ),
79+ sslctx = kwargs .get ("sslctx" , None ),
80+ baudrate = kwargs .get ("baudrate" , None ),
81+ bytesize = kwargs .get ("bytesize" , None ),
82+ parity = kwargs .get ("parity" , None ),
83+ stopbits = kwargs .get ("stopbits" , None ),
84+ handle_local_echo = kwargs .get ("handle_local_echo" , False ),
85+ ),
86+ False ,
87+ )
88+ self .on_reconnect_callback = on_reconnect_callback
89+ self .retry_on_empty : int = 0
90+ self .no_resend_on_retry = no_resend_on_retry
91+ self .slaves : list [int ] = []
92+ self .retries : int = retries
93+ self .broadcast_enable = broadcast_enable
94+
95+ # Common variables.
96+ self .framer = FRAMER_NAME_TO_CLASS .get (
97+ framer , cast (Type [ModbusFramer ], framer )
98+ )(ClientDecoder (), self )
99+ self .transaction = DictTransactionManager (
100+ self , retries = retries , retry_on_empty = retry_on_empty , ** kwargs
101+ )
102+ self .use_udp = False
103+ self .state = ModbusTransactionState .IDLE
104+ self .last_frame_end : float | None = 0
105+ self .silent_interval : float = 0
106+
107+ # ----------------------------------------------------------------------- #
108+ # Client external interface
109+ # ----------------------------------------------------------------------- #
110+ @property
111+ def connected (self ) -> bool :
112+ """Return state of connection."""
113+ return self .is_active ()
114+
115+ def register (self , custom_response_class : ModbusResponse ) -> None :
116+ """Register a custom response class with the decoder (call **sync**).
117+
118+ :param custom_response_class: (optional) Modbus response class.
119+ :raises MessageRegisterException: Check exception text.
120+
121+ Use register() to add non-standard responses (like e.g. a login prompt) and
122+ have them interpreted automatically.
123+ """
124+ self .framer .decoder .register (custom_response_class )
125+
126+ def close (self , reconnect : bool = False ) -> None :
127+ """Close connection."""
128+ if reconnect :
129+ self .connection_lost (asyncio .TimeoutError ("Server not responding" ))
130+ else :
131+ self .transport_close ()
132+
133+ def idle_time (self ) -> float :
134+ """Time before initiating next transaction (call **sync**).
135+
136+ Applications can call message functions without checking idle_time(),
137+ this is done automatically.
138+ """
139+ if self .last_frame_end is None or self .silent_interval is None :
140+ return 0
141+ return self .last_frame_end + self .silent_interval
142+
143+ def execute (self , request : ModbusRequest | None = None ) -> ModbusResponse :
144+ """Execute request and get response (call **sync/async**).
145+
146+ :param request: The request to process
147+ :returns: The result of the request execution
148+ :raises ConnectionException: Check exception text.
149+ """
150+ if not self .transport :
151+ raise ConnectionException (f"Not connected[{ self !s} ]" )
152+ return self .async_execute (request )
153+
154+ # ----------------------------------------------------------------------- #
155+ # Merged client methods
156+ # ----------------------------------------------------------------------- #
157+ async def async_execute (self , request = None ):
158+ """Execute requests asynchronously."""
159+ request .transaction_id = self .transaction .getNextTID ()
160+ packet = self .framer .buildPacket (request )
161+
162+ count = 0
163+ while count <= self .retries :
164+ if not count or not self .no_resend_on_retry :
165+ self .transport_send (packet )
166+ if self .broadcast_enable and not request .slave_id :
167+ resp = b"Broadcast write sent - no response expected"
168+ break
169+ try :
170+ req = self ._build_response (request .transaction_id )
171+ resp = await asyncio .wait_for (
172+ req , timeout = self .comm_params .timeout_connect
173+ )
174+ break
175+ except asyncio .exceptions .TimeoutError :
176+ count += 1
177+ if count > self .retries :
178+ self .close (reconnect = True )
179+ raise ModbusIOException (
180+ f"ERROR: No response received after { self .retries } retries"
181+ )
182+
183+ return resp
184+
185+ def callback_data (self , data : bytes , addr : tuple | None = None ) -> int :
186+ """Handle received data.
187+
188+ returns number of bytes consumed
189+ """
190+ self .framer .processIncomingPacket (data , self ._handle_response , slave = 0 )
191+ return len (data )
192+
193+ def callback_disconnected (self , _reason : Exception | None ) -> None :
194+ """Handle lost connection."""
195+ for tid in list (self .transaction ):
196+ self .raise_future (
197+ self .transaction .getTransaction (tid ),
198+ ConnectionException ("Connection lost during request" ),
199+ )
200+
201+ async def connect (self ):
202+ """Connect to the modbus remote host."""
203+
204+ def raise_future (self , my_future , exc ):
205+ """Set exception of a future if not done."""
206+ if not my_future .done ():
207+ my_future .set_exception (exc )
208+
209+ def _handle_response (self , reply , ** _kwargs ):
210+ """Handle the processed response and link to correct deferred."""
211+ if reply is not None :
212+ tid = reply .transaction_id
213+ if handler := self .transaction .getTransaction (tid ):
214+ if not handler .done ():
215+ handler .set_result (reply )
216+ else :
217+ Log .debug ("Unrequested message: {}" , reply , ":str" )
218+
219+ def _build_response (self , tid ):
220+ """Return a deferred response for the current request."""
221+ my_future = asyncio .Future ()
222+ if not self .transport :
223+ self .raise_future (my_future , ConnectionException ("Client is not connected" ))
224+ else :
225+ self .transaction .addTransaction (my_future , tid )
226+ return my_future
227+
228+ # ----------------------------------------------------------------------- #
229+ # Internal methods
230+ # ----------------------------------------------------------------------- #
231+ def send (self , request ):
232+ """Send request.
233+
234+ :meta private:
235+ """
236+ if self .state != ModbusTransactionState .RETRYING :
237+ Log .debug ('New Transaction state "SENDING"' )
238+ self .state = ModbusTransactionState .SENDING
239+ return request
240+
241+ def recv (self , size ):
242+ """Receive data.
243+
244+ :meta private:
245+ """
246+ return size
247+
248+ @classmethod
249+ def _get_address_family (cls , address ):
250+ """Get the correct address family."""
251+ try :
252+ _ = socket .inet_pton (socket .AF_INET6 , address )
253+ except OSError : # not a valid ipv6 address
254+ return socket .AF_INET
255+ return socket .AF_INET6
256+
257+ # ----------------------------------------------------------------------- #
258+ # The magic methods
259+ # ----------------------------------------------------------------------- #
260+ def __enter__ (self ):
261+ """Implement the client with enter block.
262+
263+ :returns: The current instance of the client
264+ :raises ConnectionException:
265+ """
266+ if not self .connect ():
267+ raise ConnectionException (f"Failed to connect[{ self .__str__ ()} ]" )
268+ return self
269+
270+ async def __aenter__ (self ):
271+ """Implement the client with enter block.
272+
273+ :returns: The current instance of the client
274+ :raises ConnectionException:
275+ """
276+ if not await self .connect ():
277+ raise ConnectionException (f"Failed to connect[{ self .__str__ ()} ]" )
278+ return self
279+
280+ def __exit__ (self , klass , value , traceback ):
281+ """Implement the client with exit block."""
282+ self .close ()
283+
284+ async def __aexit__ (self , klass , value , traceback ):
285+ """Implement the client with exit block."""
286+ self .close ()
287+
288+ def __str__ (self ):
289+ """Build a string representation of the connection.
290+
291+ :returns: The string representation
292+ """
293+ return (
294+ f"{ self .__class__ .__name__ } { self .comm_params .host } :{ self .comm_params .port } "
295+ )
296+
297+ class ModbusBaseSyncClient (ModbusClientMixin , ModbusProtocol ):
298+ """**ModbusBaseClient**.
299+
300+ Fixed parameters:
301+
302+ :param framer: Framer enum name
303+
304+ Optional parameters:
305+
306+ :param timeout: Timeout for a request, in seconds.
307+ :param retries: Max number of retries per request.
308+ :param retry_on_empty: Retry on empty response.
309+ :param close_comm_on_error: Close connection on error.
310+ :param strict: Strict timing, 1.5 character between requests.
311+ :param broadcast_enable: True to treat id 0 as broadcast address.
312+ :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
313+ :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
37314 :param on_reconnect_callback: Function that will be called just before a reconnection attempt.
38315 :param no_resend_on_retry: Do not resend request when retrying due to missing response.
39316 :param kwargs: Experimental parameters.
@@ -73,15 +350,14 @@ def __init__( # pylint: disable=too-many-arguments
73350 strict : bool = True ,
74351 broadcast_enable : bool = False ,
75352 reconnect_delay : float = 0.1 ,
76- reconnect_delay_max : float = 300 ,
353+ reconnect_delay_max : float = 300.0 ,
77354 on_reconnect_callback : Callable [[], None ] | None = None ,
78355 no_resend_on_retry : bool = False ,
79356 ** kwargs : Any ,
80357 ) -> None :
81358 """Initialize a client instance."""
82359 ModbusClientMixin .__init__ (self )
83- self .use_sync = kwargs .get ("use_sync" , False )
84- setup_params = CommParams (
360+ self .comm_params = CommParams (
85361 comm_type = kwargs .get ("CommType" ),
86362 comm_name = "comm" ,
87363 source_address = kwargs .get ("source_address" , ("0.0.0.0" , 0 )),
@@ -97,14 +373,6 @@ def __init__( # pylint: disable=too-many-arguments
97373 stopbits = kwargs .get ("stopbits" , None ),
98374 handle_local_echo = kwargs .get ("handle_local_echo" , False ),
99375 )
100- if not self .use_sync :
101- ModbusProtocol .__init__ (
102- self ,
103- setup_params ,
104- False ,
105- )
106- else :
107- self .comm_params = setup_params
108376 self .params = self ._params ()
109377 self .params .retries = int (retries )
110378 self .params .retry_on_empty = bool (retry_on_empty )
@@ -172,13 +440,9 @@ def execute(self, request: ModbusRequest | None = None) -> ModbusResponse:
172440 :returns: The result of the request execution
173441 :raises ConnectionException: Check exception text.
174442 """
175- if self .use_sync :
176- if not self .connect ():
177- raise ConnectionException (f"Failed to connect[{ self !s} ]" )
178- return self .transaction .execute (request )
179- if not self .transport :
180- raise ConnectionException (f"Not connected[{ self !s} ]" )
181- return self .async_execute (request )
443+ if not self .connect ():
444+ raise ConnectionException (f"Failed to connect[{ self !s} ]" )
445+ return self .transaction .execute (request )
182446
183447 # ----------------------------------------------------------------------- #
184448 # Merged client methods
0 commit comments