tmodbus 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tmodbus/__init__.py ADDED
@@ -0,0 +1,137 @@
1
+ """tModbus library."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import TYPE_CHECKING, Any, Unpack
5
+
6
+ from .client.async_client import AsyncModbusClient
7
+ from .transport import AsyncRtuTransport, AsyncSmartTransport, AsyncTcpTransport
8
+ from .transport.async_rtu import PySerialOptions
9
+
10
+ if TYPE_CHECKING:
11
+ from tenacity import AsyncRetrying
12
+
13
+ try:
14
+ from ._version import __version__
15
+ except ImportError: # pragma: no cover
16
+ __version__ = "0.0.0"
17
+
18
+
19
+ def create_async_tcp_client( # noqa: PLR0913
20
+ host: str,
21
+ port: int = 502,
22
+ *,
23
+ unit_id: int,
24
+ timeout: float = 10.0,
25
+ connect_timeout: float = 10.0,
26
+ wait_between_requests: float = 0.0,
27
+ wait_after_connect: float = 0.0,
28
+ auto_reconnect: "bool | AsyncRetrying" = True,
29
+ on_reconnected: Callable[[], Awaitable[None] | None] | None = None,
30
+ response_retry_strategy: "AsyncRetrying | None" = None,
31
+ retry_on_device_busy: bool = True,
32
+ retry_on_device_failure: bool = False,
33
+ **connection_kwargs: Any,
34
+ ) -> AsyncModbusClient:
35
+ """Create an asynchronous TCP Modbus client with automatic reconnect and request retry functionality.
36
+
37
+ Args:
38
+ host: The IP address or hostname of the Modbus server.
39
+ port: The port number of the Modbus server (default is 502).
40
+ unit_id: The unit ID to use for requests.
41
+ timeout: Timeout in seconds, default 10.0s
42
+ connect_timeout: Timeout for establishing connection, default 10.0s
43
+ wait_between_requests: Wait time between requests in seconds (default: 0.0s)
44
+ wait_after_connect: Wait time after connection establishment in seconds (default: 0.0s)
45
+ auto_reconnect: Whether to automatically reconnect on connection loss (default: True).
46
+ Can be a custom AsyncRetrying instance when more control is needed.
47
+ on_reconnected: Callback to be called after a successful reconnection.
48
+ response_retry_strategy: Retry strategy for handling failed requests (default: None).
49
+ retry_on_device_busy: Whether to retry on device busy errors (default: True).
50
+ Can be a custom AsyncRetrying instance when more control is needed.
51
+ retry_on_device_failure: Whether to retry on device failure errors (default: False).
52
+ Can be a custom AsyncRetrying instance when more control is needed.
53
+ connection_kwargs: Additional connection parameters passed to `asyncio.open_connection` (e.g., SSL context)
54
+
55
+ Returns:
56
+ An instance of AsyncModbusClient configured for TCP transport.
57
+
58
+ """
59
+ smart_transport = AsyncSmartTransport(
60
+ AsyncTcpTransport(
61
+ host,
62
+ port,
63
+ timeout=timeout,
64
+ connect_timeout=connect_timeout,
65
+ **connection_kwargs,
66
+ ),
67
+ wait_between_requests=wait_between_requests,
68
+ wait_after_connect=wait_after_connect,
69
+ auto_reconnect=auto_reconnect,
70
+ on_reconnected=on_reconnected,
71
+ response_retry_strategy=response_retry_strategy,
72
+ retry_on_device_busy=retry_on_device_busy,
73
+ retry_on_device_failure=retry_on_device_failure,
74
+ )
75
+ return AsyncModbusClient(smart_transport, unit_id=unit_id)
76
+
77
+
78
+ def create_async_rtu_client( # noqa: PLR0913
79
+ port: str,
80
+ *,
81
+ unit_id: int,
82
+ wait_between_requests: float = 0.0,
83
+ wait_after_connect: float = 0.0,
84
+ auto_reconnect: "bool | AsyncRetrying" = True,
85
+ on_reconnected: Callable[[], Awaitable[None] | None] | None = None,
86
+ response_retry_strategy: "AsyncRetrying | None" = None,
87
+ retry_on_device_busy: bool = True,
88
+ retry_on_device_failure: bool = False,
89
+ **pyserial_options: Unpack[PySerialOptions],
90
+ ) -> AsyncModbusClient:
91
+ """Create an asynchronous RTU Modbus client with automatic reconnect and request retry functionality.
92
+
93
+ Args:
94
+ port: The port number of the Modbus server (default is 502).
95
+ unit_id: The unit ID to use for requests.
96
+ timeout: Timeout in seconds, default 10.0s
97
+ connect_timeout: Timeout for establishing connection, default 10.0s
98
+ wait_between_requests: Wait time between requests in seconds (default: 0.0s)
99
+ wait_after_connect: Wait time after connection establishment in seconds (default: 0.0s)
100
+ auto_reconnect: Whether to automatically reconnect on connection loss (default: True).
101
+ Can be a custom AsyncRetrying instance when more control is needed.
102
+ on_reconnected: Callback to be called after a successful reconnection.
103
+ response_retry_strategy: Retry strategy for handling failed requests (default: None).
104
+ retry_on_device_busy: Whether to retry on device busy errors (default: True).
105
+ Can be a custom AsyncRetrying instance when more control is needed.
106
+ retry_on_device_failure: Whether to retry on device failure errors (default: False).
107
+ Can be a custom AsyncRetrying instance when more control is needed.
108
+ pyserial_options: Additional connection parameters passed to `pyserial` (e.g., SSL context)
109
+
110
+ Returns:
111
+ An instance of AsyncModbusClient configured for TCP transport.
112
+
113
+ """
114
+ smart_transport = AsyncSmartTransport(
115
+ AsyncRtuTransport(
116
+ port,
117
+ **pyserial_options,
118
+ ),
119
+ wait_between_requests=wait_between_requests,
120
+ wait_after_connect=wait_after_connect,
121
+ auto_reconnect=auto_reconnect,
122
+ on_reconnected=on_reconnected,
123
+ response_retry_strategy=response_retry_strategy,
124
+ retry_on_device_busy=retry_on_device_busy,
125
+ retry_on_device_failure=retry_on_device_failure,
126
+ )
127
+ return AsyncModbusClient(smart_transport, unit_id=unit_id)
128
+
129
+
130
+ __all__ = [
131
+ "AsyncModbusClient",
132
+ "AsyncRtuTransport",
133
+ "AsyncSmartTransport",
134
+ "AsyncTcpTransport",
135
+ "create_async_rtu_client",
136
+ "create_async_tcp_client",
137
+ ]
tmodbus/_version.py ADDED
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,5 @@
1
+ """tModbus Clients."""
2
+
3
+ from .async_client import AsyncModbusClient
4
+
5
+ __all__ = ["AsyncModbusClient"]
@@ -0,0 +1,341 @@
1
+ """ModbusLink Asynchronous Client Implementation.
2
+
3
+ Provides user-friendly asynchronous Modbus client API.
4
+ """
5
+
6
+ import logging
7
+ from types import TracebackType
8
+ from typing import Literal, Self, TypeVar
9
+
10
+ from tmodbus.pdu import (
11
+ BaseClientPDU,
12
+ ReadCoilsPDU,
13
+ ReadDeviceIdentificationPDU,
14
+ ReadDiscreteInputsPDU,
15
+ ReadHoldingRegistersPDU,
16
+ ReadInputRegistersPDU,
17
+ WriteMultipleCoilsPDU,
18
+ WriteMultipleRegistersPDU,
19
+ WriteSingleCoilPDU,
20
+ WriteSingleRegisterPDU,
21
+ )
22
+ from tmodbus.pdu.holding_registers_struct import HoldingRegisterReadMixin, HoldingRegisterWriteMixin
23
+ from tmodbus.transport.async_base import AsyncBaseTransport
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ RT = TypeVar("RT")
28
+
29
+
30
+ class AsyncModbusClient(HoldingRegisterReadMixin, HoldingRegisterWriteMixin):
31
+ """Asynchronous Modbus Client.
32
+
33
+ Provides an user-friendly asynchronous Modbus interface to a single Modbus device.
34
+ All methods use Python native data types (int, float, str, list, etc.),
35
+
36
+ If you want to query another device on the same connection, use the `for_unit_id` method.
37
+
38
+ This class is agnostic to the transport layer: just pass the desired transport instance.
39
+
40
+ Example:
41
+ >>> import asyncio
42
+ >>> from tmodbus import AsyncModbusClient, AsyncTcpTransport
43
+ >>> async def main():
44
+ ... transport = AsyncTcpTransport('localhost', 502)
45
+ ... client = AsyncModbusClient(transport, unit_id=1)
46
+ ... async with client:
47
+ ... print("Contents of register 0:", await client.read_holding_registers(0, 1))
48
+ ...
49
+ >>> asyncio.run(main())
50
+
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ transport: AsyncBaseTransport,
56
+ *,
57
+ unit_id: int,
58
+ word_order: Literal["big", "little"] = "big",
59
+ ) -> None:
60
+ """Initialize Async Modbus Client.
61
+
62
+ Args:
63
+ transport: Async transport layer instance (AsyncTcpTransport, etc.)
64
+ unit_id: Unit ID of the Modbus device
65
+ word_order: Word order for multi-register values ('big' or 'little').
66
+
67
+ """
68
+ HoldingRegisterReadMixin.__init__(self, word_order=word_order)
69
+ HoldingRegisterWriteMixin.__init__(self, word_order=word_order)
70
+ self.transport = transport
71
+
72
+ if not (0 <= unit_id <= 255):
73
+ msg = "Unit ID must be in range 0-255"
74
+ raise ValueError(msg)
75
+
76
+ self.unit_id = unit_id
77
+
78
+ async def connect(self) -> None:
79
+ """Connect to the server."""
80
+ await self.transport.open()
81
+
82
+ @property
83
+ def connected(self) -> bool:
84
+ """Report if the client is connected to the server."""
85
+ return self.transport.is_open()
86
+
87
+ async def disconnect(self) -> None:
88
+ """Close the server connection."""
89
+ await self.transport.close()
90
+
91
+ async def execute(self, pdu: BaseClientPDU[RT]) -> RT:
92
+ """Execute PDU Request.
93
+
94
+ Args:
95
+ pdu: Modbus PDU instance
96
+ Returns:
97
+ Response PDU bytes
98
+
99
+ Raises:
100
+ InvalidResponseError: If response is invalid or does not match request
101
+
102
+ """
103
+ return await self.transport.send_and_receive(self.unit_id, pdu)
104
+
105
+ async def read_coils(
106
+ self,
107
+ start_address: int,
108
+ quantity: int,
109
+ ) -> list[bool]:
110
+ """Read Coil Status (Function Code 0x01).
111
+
112
+ Args:
113
+ start_address: Starting address
114
+ quantity: Quantity to read (1-2000)
115
+
116
+ Returns:
117
+ List of coil status, True for ON, False for OFF
118
+
119
+ Raises:
120
+ InvalidResponseError: If response is invalid or does not match request
121
+
122
+ Example:
123
+ >>> coils = await client.read_coils(1, 0, 8)
124
+ [True, False, True, False, False, False, True, False]
125
+
126
+ """
127
+ return await self.execute(ReadCoilsPDU(start_address, quantity))
128
+
129
+ async def read_discrete_inputs(
130
+ self,
131
+ start_address: int,
132
+ quantity: int,
133
+ ) -> list[bool]:
134
+ """Read Discrete Inputs (Function Code 0x02).
135
+
136
+ Args:
137
+ start_address: Starting address
138
+ quantity: Quantity to read (1-2000)
139
+
140
+ Returns:
141
+ List of coil status, True for ON, False for OFF
142
+
143
+ Example:
144
+ >>> coils = await client.read_coils(1, 0, 8)
145
+ [True, False, True, False, False, False, True, False]
146
+
147
+ """
148
+ return await self.execute(ReadDiscreteInputsPDU(start_address, quantity))
149
+
150
+ async def read_holding_registers(
151
+ self,
152
+ start_address: int,
153
+ quantity: int,
154
+ ) -> list[int]:
155
+ """Read Holding Registers (Function Code 0x03).
156
+
157
+ Args:
158
+ start_address: Starting address
159
+ quantity: Quantity to read (1-125)
160
+
161
+
162
+ Returns:
163
+ List of register values, each value is a 16-bit unsigned integer (0-65535)
164
+
165
+ Example:
166
+ >>> registers = await client.read_holding_registers(0, 4) # Read holding registers 0, 1, 2, 3
167
+ [1234, 5678, 9012, 3456]
168
+
169
+ """
170
+ return await self.execute(ReadHoldingRegistersPDU(start_address, quantity))
171
+
172
+ async def read_input_registers(
173
+ self,
174
+ start_address: int,
175
+ quantity: int,
176
+ ) -> list[int]:
177
+ """Read Input Registers (Function Code 0x04).
178
+
179
+ Args:
180
+ start_address: Starting address
181
+ quantity: Quantity to read (1-125)
182
+
183
+
184
+ Returns:
185
+ List of register values, each value is a 16-bit unsigned integer (0-65535)
186
+
187
+ Example:
188
+ >>> registers = await client.read_input_registers(0, 4) # Read input registers 0, 1, 2, 3
189
+ [1234, 5678, 9012, 3456]
190
+
191
+ """
192
+ return await self.execute(ReadInputRegistersPDU(start_address, quantity))
193
+
194
+ async def write_single_coil(
195
+ self,
196
+ address: int,
197
+ value: bool, # noqa: FBT001
198
+ ) -> int:
199
+ """Write Single Coil (Function Code 0x05).
200
+
201
+ Args:
202
+ address: Coil address
203
+ value: Coil value (True for ON, False for OFF)
204
+
205
+
206
+ Returns:
207
+ The value that was written
208
+
209
+ Example:
210
+ >>> await client.write_single_coil(0, True) # Write ON to coil 0
211
+
212
+ """
213
+ return await self.execute(WriteSingleCoilPDU(address, value))
214
+
215
+ async def write_single_register(
216
+ self,
217
+ address: int,
218
+ value: int,
219
+ ) -> int:
220
+ """Write Single Register (Function Code 0x06).
221
+
222
+ Args:
223
+ address: Register address
224
+ value: Register value (0-65535)
225
+
226
+ Returns:
227
+ the value that was written
228
+
229
+ Example:
230
+ >>> await client.write_single_register(0, 1234) # Write 1234 to register 0
231
+
232
+ """
233
+ return await self.execute(WriteSingleRegisterPDU(address, value))
234
+
235
+ async def write_multiple_coils(
236
+ self,
237
+ start_address: int,
238
+ values: list[bool],
239
+ ) -> int:
240
+ """Write Multiple Coils (Function Code 0x0F).
241
+
242
+ Args:
243
+ start_address: Starting address
244
+ values: List of coil values, True for ON, False for OFF
245
+
246
+
247
+ Returns:
248
+ The number of coils that have been written to.
249
+
250
+ Example:
251
+ >>> await client.write_multiple_coils(0, [True, False, True, False])
252
+
253
+ """
254
+ return await self.execute(WriteMultipleCoilsPDU(start_address, values))
255
+
256
+ async def write_multiple_registers(
257
+ self,
258
+ start_address: int,
259
+ values: list[int],
260
+ ) -> int:
261
+ """Write Multiple Registers (Function Code 0x10).
262
+
263
+ Args:
264
+ start_address: Starting address
265
+ values: List of register values, each value 0-65535
266
+
267
+
268
+ Returns:
269
+ The number of registers that have been written to.
270
+
271
+ Example:
272
+ >>> await client.write_multiple_registers(0, [1234, 5678, 9012])
273
+
274
+ """
275
+ return await self.execute(WriteMultipleRegistersPDU(start_address, values))
276
+
277
+ async def read_device_identification(
278
+ self,
279
+ device_code: Literal[0x01, 0x02, 0x03, 0x04],
280
+ object_id: int,
281
+ ) -> dict[int, bytes]:
282
+ """Read Device Identification (Function Code 0x2B/0x0E).
283
+
284
+ Args:
285
+ device_code: Device code (0x01 for Basic, 0x02 for Regular, 0x03 for Extended, 0x04 for Specific)
286
+ object_id: Object ID to start reading from (0x00 to 0xFF)
287
+
288
+
289
+ Returns:
290
+ A dictionary mapping object IDs to their corresponding string values.
291
+
292
+ Example:
293
+ >>> device_info = await client.read_device_identification(1, 0)
294
+ {0: 'VendorName', 1: 'ProductCode', ...}
295
+
296
+ """
297
+ result: dict[int, bytes] = {}
298
+ more = True
299
+ number_of_objects: int | None = None
300
+ while more:
301
+ response = await self.execute(ReadDeviceIdentificationPDU(device_code, object_id))
302
+ result.update(response.objects)
303
+ more = response.more
304
+ object_id = response.next_object_id
305
+
306
+ if number_of_objects is None:
307
+ number_of_objects = response.number_of_objects
308
+ elif number_of_objects != response.number_of_objects:
309
+ logger.warning(
310
+ "Number of objects changed between requests: was %d, now %d",
311
+ number_of_objects,
312
+ response.number_of_objects,
313
+ )
314
+
315
+ return result
316
+
317
+ async def __aenter__(self) -> Self:
318
+ """Async context manager entry."""
319
+ await self.transport.open()
320
+ return self
321
+
322
+ async def __aexit__(
323
+ self,
324
+ exc_type: type[BaseException] | None,
325
+ exc_val: BaseException | None,
326
+ exc_tb: TracebackType | None,
327
+ ) -> None:
328
+ """Async context manager exit."""
329
+ await self.transport.close()
330
+
331
+ def for_unit_id(self, unit_id: int) -> "AsyncModbusClient":
332
+ """Create a new client instance for a different unit ID, but using the same connection.
333
+
334
+ Args:
335
+ unit_id: The unit ID for the new client instance.
336
+
337
+ Returns:
338
+ A new instance of AsyncModbusClient configured for the specified unit ID.
339
+
340
+ """
341
+ return AsyncModbusClient(self.transport, unit_id=unit_id, word_order=self.word_order)
tmodbus/const.py ADDED
@@ -0,0 +1,42 @@
1
+ """Constants."""
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class FunctionCode(IntEnum):
7
+ """Modbus Function Codes."""
8
+
9
+ READ_COILS = 0x01
10
+ READ_DISCRETE_INPUTS = 0x02
11
+ READ_HOLDING_REGISTERS = 0x03
12
+ READ_INPUT_REGISTERS = 0x04
13
+ WRITE_SINGLE_COIL = 0x05
14
+ WRITE_SINGLE_REGISTER = 0x06
15
+ READ_EXCEPTION_STATUS = 0x07 # (serial line only)
16
+ DIAGNOSTICS = 0x08 # (serial line only)
17
+ GET_COM_EVENT_COUNTER = 0x0B # (serial line only)
18
+ GET_COM_EVENT_LOG = 0x0C # (serial line only)
19
+ WRITE_MULTIPLE_COILS = 0x0F
20
+ WRITE_MULTIPLE_REGISTERS = 0x10
21
+ REPORT_SERVER_ID = 0x11 # (serial line only)
22
+ READ_FILE_RECORD = 0x14
23
+ WRITE_FILE_RECORD = 0x15
24
+ MASK_WRITE_REGISTER = 0x16
25
+ READ_WRITE_MULTIPLE_REGISTERS = 0x17
26
+ READ_FIFO_QUEUE = 0x18
27
+ ENCAPSULATED_INTERFACE_TRANSPORT = 0x2B
28
+
29
+
30
+ class ExceptionCode(IntEnum):
31
+ """Modbus Exception Codes."""
32
+
33
+ ILLEGAL_FUNCTION = 0x01
34
+ ILLEGAL_DATA_ADDRESS = 0x02
35
+ ILLEGAL_DATA_VALUE = 0x03
36
+ SERVER_DEVICE_FAILURE = 0x04
37
+ ACKNOWLEDGE = 0x05
38
+ SERVER_DEVICE_BUSY = 0x06
39
+ MEMORY_PARITY_ERROR = 0x08
40
+ GATEWAY_PATH_UNAVAILABLE = 0x0A
41
+ GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0B
42
+ ABNORNMAL_DEVICE_DESCRIPTION = 0xAB