File: client.rst

package info (click to toggle)
pymodbus 3.8.6-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,720 kB
  • sloc: python: 14,867; makefile: 27; sh: 17
file content (334 lines) | stat: -rw-r--r-- 10,985 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
Client
======
Pymodbus offers both a :mod:`synchronous client` and a :mod:`asynchronous client`.
Both clients offer simple calls for each type of request, as well as a unified response, removing
a lot of the complexities in the modbus protocol.

In addition to the "pure" client, pymodbus offers a set of utilities converting to/from registers to/from "normal" python values.

The client is NOT thread safe, meaning the application must ensure that calls are serialized.
This is only a problem for synchronous applications that use multiple threads or for asynchronous applications
that use multiple :mod:`asyncio.create_task`.

It is allowed to have multiple client objects that e.g. each communicate with a TCP based device.


Client performance
------------------
There are currently a big performance gap between the 2 clients
(try it on your computer :github:`examples/client_performance.py`).
This is due to a rather old implementation of the synchronous client, we are currently working to update the client code.
Our aim is to achieve a similar data rate with both clients and at least double the data rate while keeping the stability.
Table below is a test with 1000 calls each reading 10 registers.

.. list-table::
   :header-rows: 1

   * - **client**
     - **asynchronous**
     - **synchronous**
   * - total time
     - 0,33 sec
     - 114,10 sec
   * - ms/call
     - 0,33 ms
     - 114,10 ms
   * - ms/register
     - 0,03 ms
     - 11,41 ms
   * - calls/sec
     - 3.030
     - 8
   * - registers/sec
     - 30.300
     - 87


Client protocols/framers
------------------------
Pymodbus offers clients with transport different protocols and different framers

.. list-table::
   :header-rows: 1

   * - **protocol**
     - ASCII
     - RTU
     - RTU_OVER_TCP
     - SOCKET
     - TLS
   * - SERIAL (RS-485)
     - Yes
     - Yes
     - No
     - No
     - No
   * - TCP
     - Yes
     - No
     - Yes
     - Yes
     - No
   * - TLS
     - No
     - No
     - No
     - No
     - Yes
   * - UDP
     - Yes
     - No
     - Yes
     - Yes
     - No


Serial (RS-485)
^^^^^^^^^^^^^^^
Pymodbus do not connect to the device (server) but connects to a comm port or usb port on the local computer.

RS-485 is a half duplex protocol, meaning the servers do nothing until the client sends a request then the server
being addressed responds. The client controls the traffic and as a consequence one RS-485 line can only have 1 client
but upto 254 servers (physical devices).

RS-485 is a simple 2 wire cabling with a pullup resistor. It is important to note that many USB converters do not have a
builtin resistor, this must be added manually. When experiencing many faulty packets and retries this is often the problem.


TCP
^^^
Pymodbus connects directly to the device using a standard socket and have a one-to-one connection with the device.
In case of multiple TCP devices the application must instantiate multiple client objects one for each connection.

.. tip:: a TCP device often represent multiple physical devices (e.g Ethernet-RS485 converter), each of these devices
    can be addressed normally


TLS
^^^
A variant of **TCP** that uses encryption and certificates. **TLS** is mostly used when the devices are connected to the internet.


UDP
^^^
A broadcast variant of **TCP**. **UDP** allows addressing of many devices with a single request, however there are no control
that a device have received the packet.


Client usage
------------
Using pymodbus client to set/get information from a device (server)
is done in a few simple steps.

Synchronous example
^^^^^^^^^^^^^^^^^^^

::

    from pymodbus.client import ModbusTcpClient

    client = ModbusTcpClient('MyDevice.lan')   # Create client object
    client.connect()                           # connect to device
    client.write_coil(1, True, slave=1)        # set information in device
    result = client.read_coils(2, 3, slave=1)  # get information from device
    print(result.bits[0])                      # use information
    client.close()                             # Disconnect device

The line :mod:`client.connect()` connects to the device (or comm port). If this cannot connect successfully within
the timeout it throws an exception. After this initial connection, further
calls to the same client (here, :mod:`client.write_coil(...)` and
:mod:`client.read_coils(...)` ) will check whether the client is still
connected, and automatically reconnect if not.

Asynchronous example
^^^^^^^^^^^^^^^^^^^^

::

    from pymodbus.client import AsyncModbusTcpClient

    client = AsyncModbusTcpClient('MyDevice.lan')    # Create client object
    await client.connect()                           # connect to device, reconnect automatically
    await client.write_coil(1, True, slave=1)        # set information in device
    result = await client.read_coils(2, 3, slave=1)  # get information from device
    print(result.bits[0])                            # use information
    client.close()                                   # Disconnect device

The line :mod:`client = AsyncModbusTcpClient('MyDevice.lan')` only creates the object; it does not activate
anything.

The line :mod:`await client.connect()` connects to the device (or comm port), if this cannot connect successfully within
the timeout it throws an exception. If connected successfully reconnecting later is handled automatically

The line :mod:`await client.write_coil(1, True, slave=1)` is an example of a write request, set address 1 to True on device 1 (slave).

The line :mod:`result = await client.read_coils(2, 3, slave=1)` is an example of a read request, get the value of address 2, 3 and 4 (count = 3) from device 1 (slave).

The last line :mod:`client.close()` closes the connection and render the object inactive.

Retry logic for async clients
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If no response is received to a request (call), it is retried (parameter retries) times, if not successful
an exception response is returned, BUT the connection is not touched.

If 3 consequitve requests (calls) do not receive a response, the connection is terminated.

Development notes
^^^^^^^^^^^^^^^^^

Large parts of the implementation are shared between the different classes,
to ensure high stability and efficient maintenance.

The synchronous clients are not thread safe nor is a single client intended
to be used from multiple threads. Due to the nature of the modbus protocol,
it makes little sense to have client calls split over different threads,
however the application can do it with proper locking implemented.

The asynchronous client only runs in the thread where the asyncio loop is created,
it does not provide mechanisms to prevent (semi)parallel calls,
that must be prevented at application level.


Client device addressing
------------------------

With **TCP**, **TLS** and **UDP**, the tcp/ip address of the physical device is defined when creating the object.
Logical devices represented by the device is addressed with the :mod:`slave=` parameter.

With **Serial**, the comm port is defined when creating the object.
The physical devices are addressed with the :mod:`slave=` parameter.

:mod:`slave=0` is defined as broadcast in the modbus standard, but pymodbus treats it as a normal device.
please note :mod:`slave=0` can only be used to address devices that truly have id=0 ! Using :mod:`slave=0` to
address a single device with id not 0 is against the protocol.

If an application is expecting multiple responses to a broadcast request, it must call :mod:`client.execute` and deal with the responses.

If no response is expected to a request, the :mod:`no_response_expected=True` argument can be used
in the normal API calls, this will cause the call to return immediately with :mod:`ExceptionResponse(0xff)`.


Client response handling
------------------------

All simple request calls (mixin) return a unified result independent whether it´s a read, write or diagnostic call.

The application should evaluate the result generically::

    try:
        rr = await client.read_coils(1, 1, slave=1)
    except ModbusException as exc:
        _logger.error(f"ERROR: exception in pymodbus {exc}")
        raise exc
    if rr.isError():
        _logger.error("ERROR: pymodbus returned an error!")
        raise ModbusException(txt)

:mod:`except ModbusException as exc:` happens generally when pymodbus experiences an internal error.
There are a few situation where an unexpected response from a device can cause an exception.

:mod:`rr.isError()` is set whenever the device reports a problem.

And in case of read retrieve the data depending on type of request

- :mod:`rr.bits` is set for coils / input_register requests
- :mod:`rr.registers` is set for other requests

Remark if using :mod:`no_response_expected=True` rr will always be None.

Client interface classes
------------------------

There are a client class for each type of communication and for asynchronous/synchronous

.. list-table::

   * - **Serial**
     - :mod:`AsyncModbusSerialClient`
     - :mod:`ModbusSerialClient`
   * - **TCP**
     - :mod:`AsyncModbusTcpClient`
     - :mod:`ModbusTcpClient`
   * - **TLS**
     - :mod:`AsyncModbusTlsClient`
     - :mod:`ModbusTlsClient`
   * - **UDP**
     - :mod:`AsyncModbusUdpClient`
     - :mod:`ModbusUdpClient`

Client common
^^^^^^^^^^^^^
Some methods are common to all clients:

.. autoclass:: pymodbus.client.base.ModbusBaseClient
    :members:
    :member-order: bysource
    :show-inheritance:

.. autoclass:: pymodbus.client.base.ModbusBaseSyncClient
    :members:
    :member-order: bysource
    :show-inheritance:

Client serial
^^^^^^^^^^^^^
.. autoclass:: pymodbus.client.AsyncModbusSerialClient
    :members:
    :member-order: bysource
    :show-inheritance:

.. autoclass:: pymodbus.client.ModbusSerialClient
    :members:
    :member-order: bysource
    :show-inheritance:

Client TCP
^^^^^^^^^^
.. autoclass:: pymodbus.client.AsyncModbusTcpClient
    :members:
    :member-order: bysource
    :show-inheritance:

.. autoclass:: pymodbus.client.ModbusTcpClient
    :members:
    :member-order: bysource
    :show-inheritance:

Client TLS
^^^^^^^^^^
.. autoclass:: pymodbus.client.AsyncModbusTlsClient
    :members:
    :member-order: bysource
    :show-inheritance:

.. autoclass:: pymodbus.client.ModbusTlsClient
    :members:
    :member-order: bysource
    :show-inheritance:

Client UDP
^^^^^^^^^^
.. autoclass:: pymodbus.client.AsyncModbusUdpClient
    :members:
    :member-order: bysource
    :show-inheritance:

.. autoclass:: pymodbus.client.ModbusUdpClient
    :members:
    :member-order: bysource
    :show-inheritance:


Modbus calls
------------

Pymodbus makes all standard modbus requests/responses available as simple calls.

Using Modbus<transport>Client.register() custom messages can be added to pymodbus,
and handled automatically.

.. autoclass:: pymodbus.client.mixin.ModbusClientMixin
    :members:
    :member-order: bysource
    :show-inheritance: