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 +137 -0
- tmodbus/_version.py +34 -0
- tmodbus/client/__init__.py +5 -0
- tmodbus/client/async_client.py +341 -0
- tmodbus/const.py +42 -0
- tmodbus/exceptions.py +195 -0
- tmodbus/pdu/__init__.py +127 -0
- tmodbus/pdu/base.py +206 -0
- tmodbus/pdu/coils.py +362 -0
- tmodbus/pdu/device.py +135 -0
- tmodbus/pdu/discrete_inputs.py +11 -0
- tmodbus/pdu/holding_registers.py +539 -0
- tmodbus/pdu/holding_registers_struct.py +619 -0
- tmodbus/transport/__init__.py +13 -0
- tmodbus/transport/async_base.py +88 -0
- tmodbus/transport/async_rtu.py +346 -0
- tmodbus/transport/async_smart.py +245 -0
- tmodbus/transport/async_tcp.py +266 -0
- tmodbus/utils/__init__.py +1 -0
- tmodbus/utils/crc.py +63 -0
- tmodbus/utils/raw_traffic_logger.py +29 -0
- tmodbus/utils/word_aware_struct.py +135 -0
- tmodbus-0.1.0.dist-info/METADATA +152 -0
- tmodbus-0.1.0.dist-info/RECORD +25 -0
- tmodbus-0.1.0.dist-info/WHEEL +4 -0
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,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
|