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:
