11"""Modbus Master plugin connection management utilities."""
22
33import time
4- from typing import Optional
4+ from typing import Literal , Optional , Union
55
6- from pymodbus .client import ModbusTcpClient
6+ from pymodbus .client import ModbusTcpClient , ModbusSerialClient
7+
8+ TransportType = Literal ["tcp" , "rtu" ]
9+ ParityType = Literal ["N" , "E" , "O" ]
710
811
912class ModbusConnectionManager : # pylint: disable=too-many-instance-attributes
10- """Manages Modbus TCP connections with retry logic."""
13+ """Manages Modbus TCP and RTU connections with retry logic."""
14+
15+ def __init__ (
16+ self ,
17+ transport : TransportType = "tcp" ,
18+ # TCP parameters
19+ host : str = "127.0.0.1" ,
20+ port : int = 502 ,
21+ # RTU parameters
22+ serial_port : str = "" ,
23+ baud_rate : int = 9600 ,
24+ parity : ParityType = "N" ,
25+ stop_bits : int = 1 ,
26+ data_bits : int = 8 ,
27+ # Common parameters
28+ timeout_ms : int = 1000 ,
29+ slave_id : int = 1 ,
30+ ):
31+ self .transport = transport
32+ self .timeout = timeout_ms / 1000.0 # Convert to seconds
33+ self .slave_id = slave_id # Unit/Slave ID for Modbus operations
1134
12- def __init__ ( self , host : str , port : int , timeout_ms : int ):
35+ # TCP configuration
1336 self .host = host
1437 self .port = port
15- self .timeout = timeout_ms / 1000.0 # Convert to seconds
38+
39+ # RTU configuration
40+ self .serial_port = serial_port
41+ self .baud_rate = baud_rate
42+ self .parity = parity
43+ self .stop_bits = stop_bits
44+ self .data_bits = data_bits
1645
1746 # Retry configuration
1847 self .retry_delay_base = 2.0 # initial delay between attempts (seconds)
@@ -21,9 +50,34 @@ def __init__(self, host: str, port: int, timeout_ms: int):
2150
2251 # Connection state - is_connected is the authoritative flag for connection health
2352 # It is set to False when any error occurs, forcing reconnection on next cycle
24- self .client : Optional [ModbusTcpClient ] = None
53+ self .client : Optional [Union [ ModbusTcpClient , ModbusSerialClient ] ] = None
2554 self .is_connected = False
2655
56+ def _create_client (self ) -> Union [ModbusTcpClient , ModbusSerialClient ]:
57+ """Create the appropriate Modbus client based on transport type."""
58+ if self .transport == "tcp" :
59+ return ModbusTcpClient (
60+ host = self .host ,
61+ port = self .port ,
62+ timeout = self .timeout
63+ )
64+ else : # RTU
65+ return ModbusSerialClient (
66+ port = self .serial_port ,
67+ baudrate = self .baud_rate ,
68+ parity = self .parity ,
69+ stopbits = self .stop_bits ,
70+ bytesize = self .data_bits ,
71+ timeout = self .timeout
72+ )
73+
74+ def get_connection_info (self ) -> str :
75+ """Return human-readable connection information."""
76+ if self .transport == "tcp" :
77+ return f"TCP { self .host } :{ self .port } "
78+ else :
79+ return f"RTU { self .serial_port } @{ self .baud_rate } "
80+
2781 def connect_with_retry (self , stop_event = None ) -> bool :
2882 """
2983 Attempts to connect to Modbus device with infinite retry.
@@ -35,6 +89,7 @@ def connect_with_retry(self, stop_event=None) -> bool:
3589 True if connected successfully, False if interrupted
3690 """
3791 retry_count = 0
92+ conn_info = self .get_connection_info ()
3893
3994 while stop_event is None or not stop_event .is_set ():
4095 try :
@@ -45,30 +100,28 @@ def connect_with_retry(self, stop_event=None) -> bool:
45100 self .client .close ()
46101 except Exception :
47102 pass
48- self .client = ModbusTcpClient (
49- host = self .host , port = self .port , timeout = self .timeout
50- )
103+ self .client = self ._create_client ()
51104
52105 # Attempt to connect
53106 if self .client .connect ():
54107 print (
55- f"(PASS) Connected to { self . host } : { self . port } (attempt { retry_count + 1 } )"
108+ f"(PASS) Connected to { conn_info } (attempt { retry_count + 1 } )"
56109 )
57110 self .is_connected = True
58111 self .retry_delay_current = self .retry_delay_base # Reset delay
59112 return True
60113
61114 except Exception as e :
62- print (f"(FAIL) Connection attempt { retry_count + 1 } failed: { e } " )
115+ print (f"(FAIL) Connection attempt { retry_count + 1 } to { conn_info } failed: { e } " )
63116
64117 # Increment counter and calculate delay
65118 retry_count += 1
66119
67120 # Attempt logging
68121 if retry_count == 1 :
69- print (f"Failed to connect to { self . host } : { self . port } , starting retry attempts..." )
122+ print (f"Failed to connect to { conn_info } , starting retry attempts..." )
70123 elif retry_count % 10 == 0 : # Log every 10 attempts
71- print (f"Connection attempt { retry_count } failed, continuing retries..." )
124+ print (f"Connection attempt { retry_count } to { conn_info } failed, continuing retries..." )
72125
73126 # Wait with increasing delay (limited exponential backoff)
74127 delay = min (self .retry_delay_current , self .retry_delay_max )
@@ -127,21 +180,23 @@ def mark_disconnected(self):
127180 ModbusIOException, etc.) to ensure the connection is properly re-established.
128181 """
129182 self .is_connected = False
183+ conn_info = self .get_connection_info ()
130184 print (
131- f"Connection to { self . host } : { self . port } marked as disconnected, "
185+ f"Connection to { conn_info } marked as disconnected, "
132186 "will reconnect on next cycle"
133187 )
134188
135189 def disconnect (self ):
136190 """Close the connection and clean up resources."""
191+ conn_info = self .get_connection_info ()
137192 try :
138193 if self .client :
139194 self .client .close ()
140195 self .client = None
141196 self .is_connected = False
142- print (f"Disconnected from { self . host } : { self . port } " )
197+ print (f"Disconnected from { conn_info } " )
143198 except Exception as e :
144- print (f"(FAIL) Error disconnecting from { self . host } : { self . port } : { e } " )
199+ print (f"(FAIL) Error disconnecting from { conn_info } : { e } " )
145200
146201 def is_healthy (self ) -> bool :
147202 """
0 commit comments