This commit is contained in:
2025-01-03 15:06:21 +01:00
parent 68a7c9cb0f
commit d91714829d
3441 changed files with 615211 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
"""Client"""
__all__ = [
"AsyncModbusSerialClient",
"AsyncModbusTcpClient",
"AsyncModbusTlsClient",
"AsyncModbusUdpClient",
"ModbusBaseClient",
"ModbusSerialClient",
"ModbusTcpClient",
"ModbusTlsClient",
"ModbusUdpClient",
]
from pymodbus.client.base import ModbusBaseClient
from pymodbus.client.serial import AsyncModbusSerialClient, ModbusSerialClient
from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
from pymodbus.client.tls import AsyncModbusTlsClient, ModbusTlsClient
from pymodbus.client.udp import AsyncModbusUdpClient, ModbusUdpClient

View File

@@ -0,0 +1,324 @@
"""Base for all clients."""
from __future__ import annotations
import asyncio
import socket
from dataclasses import dataclass
from typing import Any, Callable
from pymodbus.client.mixin import ModbusClientMixin
from pymodbus.exceptions import ConnectionException, ModbusIOException
from pymodbus.factory import ClientDecoder
from pymodbus.framer import ModbusFramer
from pymodbus.logging import Log
from pymodbus.pdu import ModbusRequest, ModbusResponse
from pymodbus.transaction import DictTransactionManager
from pymodbus.transport import CommParams, ModbusProtocol
from pymodbus.utilities import ModbusTransactionState
class ModbusBaseClient(ModbusClientMixin, ModbusProtocol):
"""**ModbusBaseClient**
**Parameters common to all clients**:
:param framer: (optional) Modbus Framer class.
:param timeout: (optional) Timeout for a request, in seconds.
:param retries: (optional) Max number of retries per request.
:param retry_on_empty: (optional) Retry on empty response.
:param close_comm_on_error: (optional) Close connection on error.
:param strict: (optional) Strict timing, 1.5 character between requests.
:param broadcast_enable: (optional) True to treat id 0 as broadcast address.
:param reconnect_delay: (optional) Minimum delay in milliseconds before reconnecting.
:param reconnect_delay_max: (optional) Maximum delay in milliseconds before reconnecting.
:param on_reconnect_callback: (optional) Function that will be called just before a reconnection attempt.
:param no_resend_on_retry: (optional) Do not resend request when retrying due to missing response.
:param kwargs: (optional) Experimental parameters.
.. tip::
Common parameters and all external methods for all clients are documented here,
and not repeated with each client.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
:mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
**Application methods, common to all clients**:
"""
@dataclass
class _params:
"""Parameter class."""
retries: int = None
retry_on_empty: bool = None
close_comm_on_error: bool = None
strict: bool = None
broadcast_enable: bool = None
reconnect_delay: int = None
source_address: tuple[str, int] = None
server_hostname: str = None
def __init__( # pylint: disable=too-many-arguments
self,
framer: type[ModbusFramer] = None,
timeout: float = 3,
retries: int = 3,
retry_on_empty: bool = False,
close_comm_on_error: bool = False,
strict: bool = True,
broadcast_enable: bool = False,
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
on_reconnect_callback: Callable[[], None] | None = None,
no_resend_on_retry: bool = False,
**kwargs: Any,
) -> None:
"""Initialize a client instance."""
ModbusClientMixin.__init__(self)
self.use_sync = kwargs.get("use_sync", False)
setup_params = CommParams(
comm_type=kwargs.get("CommType"),
comm_name="comm",
source_address=kwargs.get("source_address", ("0.0.0.0", 0)),
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
host=kwargs.get("host", None),
port=kwargs.get("port", 0),
sslctx=kwargs.get("sslctx", None),
baudrate=kwargs.get("baudrate", None),
bytesize=kwargs.get("bytesize", None),
parity=kwargs.get("parity", None),
stopbits=kwargs.get("stopbits", None),
handle_local_echo=kwargs.get("handle_local_echo", False),
)
if not self.use_sync:
ModbusProtocol.__init__(
self,
setup_params,
False,
)
else:
self.comm_params = setup_params
self.params = self._params()
self.params.retries = int(retries)
self.params.retry_on_empty = bool(retry_on_empty)
self.params.close_comm_on_error = bool(close_comm_on_error)
self.params.strict = bool(strict)
self.params.broadcast_enable = bool(broadcast_enable)
self.on_reconnect_callback = on_reconnect_callback
self.retry_on_empty: int = 0
self.no_resend_on_retry = no_resend_on_retry
self.slaves: list[int] = []
# Common variables.
self.framer = framer(ClientDecoder(), self)
self.transaction = DictTransactionManager(
self, retries=retries, retry_on_empty=retry_on_empty, **kwargs
)
self.reconnect_delay_current = self.params.reconnect_delay
self.use_udp = False
self.state = ModbusTransactionState.IDLE
self.last_frame_end: float = 0
self.silent_interval: float = 0
# ----------------------------------------------------------------------- #
# Client external interface
# ----------------------------------------------------------------------- #
@property
def connected(self):
"""Connect internal."""
return True
def register(self, custom_response_class: ModbusResponse) -> None:
"""Register a custom response class with the decoder (call **sync**).
:param custom_response_class: (optional) Modbus response class.
:raises MessageRegisterException: Check exception text.
Use register() to add non-standard responses (like e.g. a login prompt) and
have them interpreted automatically.
"""
self.framer.decoder.register(custom_response_class)
def close(self, reconnect=False) -> None:
"""Close connection."""
if reconnect:
self.connection_lost(asyncio.TimeoutError("Server not responding"))
else:
self.transport_close()
def idle_time(self) -> float:
"""Time before initiating next transaction (call **sync**).
Applications can call message functions without checking idle_time(),
this is done automatically.
"""
if self.last_frame_end is None or self.silent_interval is None:
return 0
return self.last_frame_end + self.silent_interval
def execute(self, request: ModbusRequest = None) -> ModbusResponse:
"""Execute request and get response (call **sync/async**).
:param request: The request to process
:returns: The result of the request execution
:raises ConnectionException: Check exception text.
"""
if self.use_sync:
if not self.connect():
raise ConnectionException(f"Failed to connect[{self!s}]")
return self.transaction.execute(request)
if not self.transport:
raise ConnectionException(f"Not connected[{self!s}]")
return self.async_execute(request)
# ----------------------------------------------------------------------- #
# Merged client methods
# ----------------------------------------------------------------------- #
async def async_execute(self, request=None):
"""Execute requests asynchronously."""
request.transaction_id = self.transaction.getNextTID()
packet = self.framer.buildPacket(request)
count = 0
while count <= self.params.retries:
if not count or not self.no_resend_on_retry:
self.transport_send(packet)
if self.params.broadcast_enable and not request.slave_id:
resp = b"Broadcast write sent - no response expected"
break
try:
req = self._build_response(request.transaction_id)
resp = await asyncio.wait_for(
req, timeout=self.comm_params.timeout_connect
)
break
except asyncio.exceptions.TimeoutError:
count += 1
if count > self.params.retries:
self.close(reconnect=True)
raise ModbusIOException(
f"ERROR: No response received after {self.params.retries} retries"
)
return resp
def callback_data(self, data: bytes, addr: tuple = None) -> int:
"""Handle received data
returns number of bytes consumed
"""
self.framer.processIncomingPacket(data, self._handle_response, slave=0)
return len(data)
def callback_disconnected(self, _reason: Exception) -> None:
"""Handle lost connection"""
for tid in list(self.transaction):
self.raise_future(
self.transaction.getTransaction(tid),
ConnectionException("Connection lost during request"),
)
async def connect(self):
"""Connect to the modbus remote host."""
def raise_future(self, my_future, exc):
"""Set exception of a future if not done."""
if not my_future.done():
my_future.set_exception(exc)
def _handle_response(self, reply, **_kwargs):
"""Handle the processed response and link to correct deferred."""
if reply is not None:
tid = reply.transaction_id
if handler := self.transaction.getTransaction(tid):
if not handler.done():
handler.set_result(reply)
else:
Log.debug("Unrequested message: {}", reply, ":str")
def _build_response(self, tid):
"""Return a deferred response for the current request."""
my_future = asyncio.Future()
if not self.transport:
self.raise_future(my_future, ConnectionException("Client is not connected"))
else:
self.transaction.addTransaction(my_future, tid)
return my_future
# ----------------------------------------------------------------------- #
# Internal methods
# ----------------------------------------------------------------------- #
def send(self, request):
"""Send request.
:meta private:
"""
if self.state != ModbusTransactionState.RETRYING:
Log.debug('New Transaction state "SENDING"')
self.state = ModbusTransactionState.SENDING
return request
def recv(self, size):
"""Receive data.
:meta private:
"""
return size
@classmethod
def _get_address_family(cls, address):
"""Get the correct address family."""
try:
_ = socket.inet_pton(socket.AF_INET6, address)
except OSError: # not a valid ipv6 address
return socket.AF_INET
return socket.AF_INET6
# ----------------------------------------------------------------------- #
# The magic methods
# ----------------------------------------------------------------------- #
def __enter__(self):
"""Implement the client with enter block.
:returns: The current instance of the client
:raises ConnectionException:
"""
if not self.connect():
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
return self
async def __aenter__(self):
"""Implement the client with enter block.
:returns: The current instance of the client
:raises ConnectionException:
"""
if not await self.connect():
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
return self
def __exit__(self, klass, value, traceback):
"""Implement the client with exit block."""
self.close()
async def __aexit__(self, klass, value, traceback):
"""Implement the client with exit block."""
self.close()
def __str__(self):
"""Build a string representation of the connection.
:returns: The string representation
"""
return (
f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}"
)

View File

@@ -0,0 +1,582 @@
"""Modbus Client Common."""
import struct
from enum import Enum
from typing import Any, List, Tuple, Union
import pymodbus.bit_read_message as pdu_bit_read
import pymodbus.bit_write_message as pdu_bit_write
import pymodbus.diag_message as pdu_diag
import pymodbus.file_message as pdu_file_msg
import pymodbus.mei_message as pdu_mei
import pymodbus.other_message as pdu_other_msg
import pymodbus.register_read_message as pdu_reg_read
import pymodbus.register_write_message as pdu_req_write
from pymodbus.constants import INTERNAL_ERROR
from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ModbusRequest, ModbusResponse
class ModbusClientMixin: # pylint: disable=too-many-public-methods
"""**ModbusClientMixin**.
This is an interface class to facilitate the sending requests/receiving responses like read_coils.
execute() allows to make a call with non-standard or user defined function codes (remember to add a PDU
in the transport class to interpret the request/response).
Simple modbus message call::
response = client.read_coils(1, 10)
# or
response = await client.read_coils(1, 10)
Advanced modbus message call::
request = ReadCoilsRequest(1,10)
response = client.execute(request)
# or
request = ReadCoilsRequest(1,10)
response = await client.execute(request)
.. tip::
All methods can be used directly (synchronous) or
with await <method> (asynchronous) depending on the client used.
"""
def __init__(self):
"""Initialize."""
def execute(self, request: ModbusRequest) -> ModbusResponse:
"""Execute request (code ???).
:param request: Request to send
:raises ModbusException:
Call with custom function codes.
.. tip::
Response is not interpreted.
"""
raise ModbusException(INTERNAL_ERROR)
def read_coils(
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Read coils (code 0x01).
:param address: Start address to read from
:param count: (optional) Number of coils to read
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_bit_read.ReadCoilsRequest(address, count, slave, **kwargs)
)
def read_discrete_inputs(
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Read discrete inputs (code 0x02).
:param address: Start address to read from
:param count: (optional) Number of coils to read
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave, **kwargs)
)
def read_holding_registers(
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Read holding registers (code 0x03).
:param address: Start address to read from
:param count: (optional) Number of coils to read
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave, **kwargs)
)
def read_input_registers(
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Read input registers (code 0x04).
:param address: Start address to read from
:param count: (optional) Number of coils to read
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_reg_read.ReadInputRegistersRequest(address, count, slave, **kwargs)
)
def write_coil(
self, address: int, value: bool, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Write single coil (code 0x05).
:param address: Address to write to
:param value: Boolean to write
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_bit_write.WriteSingleCoilRequest(address, value, slave, **kwargs)
)
def write_register(
self, address: int, value: int, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Write register (code 0x06).
:param address: Address to write to
:param value: Value to write
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_req_write.WriteSingleRegisterRequest(address, value, slave, **kwargs)
)
def read_exception_status(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
"""Read Exception Status (code 0x07).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave, **kwargs))
def diag_query_data(
self, msg: bytearray, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose query data (code 0x08 sub 0x00).
:param msg: Message to be returned
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave, **kwargs))
def diag_restart_communication(
self, toggle: bool, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose restart communication (code 0x08 sub 0x01).
:param toggle: True if toggled.
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave, **kwargs)
)
def diag_read_diagnostic_register(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read diagnostic register (code 0x08 sub 0x02).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave, **kwargs)
)
def diag_change_ascii_input_delimeter(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose change ASCII input delimiter (code 0x08 sub 0x03).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave, **kwargs)
)
def diag_force_listen_only(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
"""Diagnose force listen only (code 0x08 sub 0x04).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave, **kwargs))
def diag_clear_counters(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
"""Diagnose clear counters (code 0x08 sub 0x0A).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_diag.ClearCountersRequest(slave=slave, **kwargs))
def diag_read_bus_message_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read bus message count (code 0x08 sub 0x0B).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnBusMessageCountRequest(slave=slave, **kwargs)
)
def diag_read_bus_comm_error_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave, **kwargs)
)
def diag_read_bus_exception_error_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave, **kwargs)
)
def diag_read_slave_message_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read Slave Message Count (code 0x08 sub 0x0E).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnSlaveMessageCountRequest(slave=slave, **kwargs)
)
def diag_read_slave_no_response_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read Slave No Response Count (code 0x08 sub 0x0F).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave, **kwargs)
)
def diag_read_slave_nak_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read Slave NAK Count (code 0x08 sub 0x10).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave, **kwargs))
def diag_read_slave_busy_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read Slave Busy Count (code 0x08 sub 0x11).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave, **kwargs))
def diag_read_bus_char_overrun_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave, **kwargs)
)
def diag_read_iop_overrun_count(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose read Iop overrun count (code 0x08 sub 0x13).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnIopOverrunCountRequest(slave=slave, **kwargs)
)
def diag_clear_overrun_counter(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave, **kwargs))
def diag_getclear_modbus_response(
self, slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Diagnose Get/Clear modbus plus (code 0x08 sub 0x15).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave, **kwargs))
def diag_get_comm_event_counter(self, **kwargs: Any) -> ModbusResponse:
"""Diagnose get event counter (code 0x0B).
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_other_msg.GetCommEventCounterRequest(**kwargs))
def diag_get_comm_event_log(self, **kwargs: Any) -> ModbusResponse:
"""Diagnose get event counter (code 0x0C).
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_other_msg.GetCommEventLogRequest(**kwargs))
def write_coils(
self,
address: int,
values: Union[List[bool], bool],
slave: int = 0,
**kwargs: Any,
) -> ModbusResponse:
"""Write coils (code 0x0F).
:param address: Start address to write to
:param values: List of booleans to write, or a single boolean to write
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave, **kwargs)
)
def write_registers(
self, address: int, values: Union[List[int], int], slave: int = 0, **kwargs: Any
) -> ModbusResponse:
"""Write registers (code 0x10).
:param address: Start address to write to
:param values: List of values to write, or a single value to write
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_req_write.WriteMultipleRegistersRequest(
address, values, slave, **kwargs
)
)
def report_slave_id(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
"""Report slave ID (code 0x11).
:param slave: (optional) Modbus slave ID
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave, **kwargs))
def read_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusResponse:
"""Read file record (code 0x14).
:param records: List of (Reference type, File number, Record Number, Record Length)
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_file_msg.ReadFileRecordRequest(records, **kwargs))
def write_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusResponse:
"""Write file record (code 0x15).
:param records: List of (Reference type, File number, Record Number, Record Length)
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(pdu_file_msg.WriteFileRecordRequest(records, **kwargs))
def mask_write_register(
self,
address: int = 0x0000,
and_mask: int = 0xFFFF,
or_mask: int = 0x0000,
**kwargs: Any,
) -> ModbusResponse:
"""Mask write register (code 0x16).
:param address: The mask pointer address (0x0000 to 0xffff)
:param and_mask: The and bitmask to apply to the register address
:param or_mask: The or bitmask to apply to the register address
:param kwargs: (optional) Experimental parameters.
:raises ModbusException:
"""
return self.execute(
pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, **kwargs)
)
def readwrite_registers(
self,
read_address: int = 0,
read_count: int = 0,
write_address: int = 0,
values: Union[List[int], int] = 0,
slave: int = 0,
**kwargs,
) -> ModbusResponse:
"""Read/Write registers (code 0x17).
:param read_address: The address to start reading from
:param read_count: The number of registers to read from address
:param write_address: The address to start writing to
:param values: List of values to write, or a single value to write
:param slave: (optional) Modbus slave ID
:param kwargs:
:raises ModbusException:
"""
return self.execute(
pdu_reg_read.ReadWriteMultipleRegistersRequest(
read_address=read_address,
read_count=read_count,
write_address=write_address,
write_registers=values,
slave=slave,
**kwargs,
)
)
def read_fifo_queue(self, address: int = 0x0000, **kwargs: Any) -> ModbusResponse:
"""Read FIFO queue (code 0x18).
:param address: The address to start reading from
:param kwargs:
:raises ModbusException:
"""
return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, **kwargs))
# code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED
def read_device_information(
self, read_code: int = None, object_id: int = 0x00, **kwargs: Any
) -> ModbusResponse:
"""Read FIFO queue (code 0x2B sub 0x0E).
:param read_code: The device information read code
:param object_id: The object to read from
:param kwargs:
:raises ModbusException:
"""
return self.execute(
pdu_mei.ReadDeviceInformationRequest(read_code, object_id, **kwargs)
)
# ------------------
# Converter methods
# ------------------
class DATATYPE(Enum):
"""Datatype enum (name and number of bytes), used for convert_* calls."""
INT16 = ("h", 1)
UINT16 = ("H", 1)
INT32 = ("i", 2)
UINT32 = ("I", 2)
INT64 = ("q", 4)
UINT64 = ("Q", 4)
FLOAT32 = ("f", 2)
FLOAT64 = ("d", 4)
STRING = ("s", 0)
@classmethod
def convert_from_registers(
cls, registers: List[int], data_type: DATATYPE
) -> Union[int, float, str]:
"""Convert registers to int/float/str.
:param registers: list of registers received from e.g. read_holding_registers()
:param data_type: data type to convert to
:returns: int, float or str depending on "data_type"
:raises ModbusException: when size of registers is not 1, 2 or 4
"""
byte_list = bytearray()
for x in registers:
byte_list.extend(int.to_bytes(x, 2, "big"))
if data_type == cls.DATATYPE.STRING:
if byte_list[-1:] == b"\00":
byte_list = byte_list[:-1]
return byte_list.decode("utf-8")
if len(registers) != data_type.value[1]:
raise ModbusException(
f"Illegal size ({len(registers)}) of register array, cannot convert!"
)
return struct.unpack(f">{data_type.value[0]}", byte_list)[0]
@classmethod
def convert_to_registers(
cls, value: Union[int, float, str], data_type: DATATYPE
) -> List[int]:
"""Convert int/float/str to registers (16/32/64 bit).
:param value: value to be converted
:param data_type: data type to be encoded as registers
:returns: List of registers, can be used directly in e.g. write_registers()
:raises TypeError: when there is a mismatch between data_type and value
"""
if data_type == cls.DATATYPE.STRING:
if not isinstance(value, str):
raise TypeError(f"Value should be string but is {type(value)}.")
byte_list = value.encode()
if len(byte_list) % 2:
byte_list += b"\x00"
else:
byte_list = struct.pack(f">{data_type.value[0]}", value)
regs = [
int.from_bytes(byte_list[x : x + 2], "big")
for x in range(0, len(byte_list), 2)
]
return regs

View File

@@ -0,0 +1,291 @@
"""Modbus client async serial communication."""
import asyncio
import time
from contextlib import suppress
from functools import partial
from typing import Any, Type
from pymodbus.client.base import ModbusBaseClient
from pymodbus.exceptions import ConnectionException
from pymodbus.framer import ModbusFramer
from pymodbus.framer.rtu_framer import ModbusRtuFramer
from pymodbus.logging import Log
from pymodbus.transport import CommType
from pymodbus.utilities import ModbusTransactionState
with suppress(ImportError):
import serial
class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol):
"""**AsyncModbusSerialClient**.
:param port: Serial port used for communication.
:param framer: (optional) Framer class.
:param baudrate: (optional) Bits per second.
:param bytesize: (optional) Number of bits per byte 7-8.
:param parity: (optional) 'E'ven, 'O'dd or 'N'one
:param stopbits: (optional) Number of stop bits 0-2¡.
:param handle_local_echo: (optional) Discard local echo from dongle.
:param kwargs: (optional) Experimental parameters
The serial communication is RS-485 based, and usually used with a usb RS485 dongle.
Example::
from pymodbus.client import AsyncModbusSerialClient
async def run():
client = AsyncModbusSerialClient("dev/serial0")
await client.connect()
...
client.close()
"""
def __init__(
self,
port: str,
framer: Type[ModbusFramer] = ModbusRtuFramer,
baudrate: int = 19200,
bytesize: int = 8,
parity: str = "N",
stopbits: int = 1,
**kwargs: Any,
) -> None:
"""Initialize Asyncio Modbus Serial Client."""
asyncio.Protocol.__init__(self)
ModbusBaseClient.__init__(
self,
framer=framer,
CommType=CommType.SERIAL,
host=port,
baudrate=baudrate,
bytesize=bytesize,
parity=parity,
stopbits=stopbits,
**kwargs,
)
@property
def connected(self):
"""Connect internal."""
return self.is_active()
async def connect(self) -> bool:
"""Connect Async client."""
self.reset_delay()
Log.debug("Connecting to {}.", self.comm_params.host)
return await self.transport_connect()
class ModbusSerialClient(ModbusBaseClient):
"""**ModbusSerialClient**.
:param port: Serial port used for communication.
:param framer: (optional) Framer class.
:param baudrate: (optional) Bits per second.
:param bytesize: (optional) Number of bits per byte 7-8.
:param parity: (optional) 'E'ven, 'O'dd or 'N'one
:param stopbits: (optional) Number of stop bits 0-2¡.
:param handle_local_echo: (optional) Discard local echo from dongle.
:param kwargs: (optional) Experimental parameters
The serial communication is RS-485 based, and usually used with a usb RS485 dongle.
Example::
from pymodbus.client import ModbusSerialClient
def run():
client = ModbusSerialClient("dev/serial0")
client.connect()
...
client.close()
Remark: There are no automatic reconnect as with AsyncModbusSerialClient
"""
state = ModbusTransactionState.IDLE
inter_char_timeout: float = 0
silent_interval: float = 0
def __init__(
self,
port: str,
framer: Type[ModbusFramer] = ModbusRtuFramer,
baudrate: int = 19200,
bytesize: int = 8,
parity: str = "N",
stopbits: int = 1,
**kwargs: Any,
) -> None:
"""Initialize Modbus Serial Client."""
self.transport = None
kwargs["use_sync"] = True
ModbusBaseClient.__init__(
self,
framer=framer,
CommType=CommType.SERIAL,
host=port,
baudrate=baudrate,
bytesize=bytesize,
parity=parity,
stopbits=stopbits,
**kwargs,
)
self.socket = None
self.last_frame_end = None
self._t0 = float(1 + 8 + 2) / self.comm_params.baudrate
"""
The minimum delay is 0.01s and the maximum can be set to 0.05s.
Setting too large a setting affects efficiency.
"""
self._recv_interval = (
(round((100 * self._t0), 2) + 0.01)
if (round((100 * self._t0), 2) + 0.01) < 0.05
else 0.05
)
if self.comm_params.baudrate > 19200:
self.silent_interval = 1.75 / 1000 # ms
else:
self.inter_char_timeout = 1.5 * self._t0
self.silent_interval = 3.5 * self._t0
self.silent_interval = round(self.silent_interval, 6)
@property
def connected(self):
"""Connect internal."""
return self.connect()
def connect(self): # pylint: disable=invalid-overridden-method
"""Connect to the modbus serial server."""
if self.socket:
return True
try:
self.socket = serial.serial_for_url(
self.comm_params.host,
timeout=self.comm_params.timeout_connect,
bytesize=self.comm_params.bytesize,
stopbits=self.comm_params.stopbits,
baudrate=self.comm_params.baudrate,
parity=self.comm_params.parity,
)
if isinstance(self.framer, ModbusRtuFramer):
if self.params.strict:
self.socket.interCharTimeout = self.inter_char_timeout
self.last_frame_end = None
except serial.SerialException as msg:
Log.error("{}", msg)
self.close()
return self.socket is not None
def close(self): # pylint: disable=arguments-differ
"""Close the underlying socket connection."""
if self.socket:
self.socket.close()
self.socket = None
def _in_waiting(self):
"""Return _in_waiting."""
in_waiting = "in_waiting" if hasattr(self.socket, "in_waiting") else "inWaiting"
if in_waiting == "in_waiting":
waitingbytes = getattr(self.socket, in_waiting)
else:
waitingbytes = getattr(self.socket, in_waiting)()
return waitingbytes
def send(self, request):
"""Send data on the underlying socket.
If receive buffer still holds some data then flush it.
Sleep if last send finished less than 3.5 character times ago.
"""
super().send(request)
if not self.socket:
raise ConnectionException(str(self))
if request:
try:
if waitingbytes := self._in_waiting():
result = self.socket.read(waitingbytes)
if self.state == ModbusTransactionState.RETRYING:
Log.debug(
"Sending available data in recv buffer {}", result, ":hex"
)
return result
Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
except NotImplementedError:
pass
if self.state != ModbusTransactionState.SENDING:
Log.debug('New Transaction state "SENDING"')
self.state = ModbusTransactionState.SENDING
size = self.socket.write(request)
return size
return 0
def _wait_for_data(self):
"""Wait for data."""
size = 0
more_data = False
if (
self.comm_params.timeout_connect is not None
and self.comm_params.timeout_connect
):
condition = partial(
lambda start, timeout: (time.time() - start) <= timeout,
timeout=self.comm_params.timeout_connect,
)
else:
condition = partial(lambda dummy1, dummy2: True, dummy2=None)
start = time.time()
while condition(start):
available = self._in_waiting()
if (more_data and not available) or (more_data and available == size):
break
if available and available != size:
more_data = True
size = available
time.sleep(self._recv_interval)
return size
def recv(self, size):
"""Read data from the underlying descriptor."""
super().recv(size)
if not self.socket:
raise ConnectionException(
self.__str__() # pylint: disable=unnecessary-dunder-call
)
if size is None:
size = self._wait_for_data()
if size > self._in_waiting():
self._wait_for_data()
result = self.socket.read(size)
return result
def is_socket_open(self):
"""Check if socket is open."""
if self.socket:
if hasattr(self.socket, "is_open"):
return self.socket.is_open
return self.socket.isOpen()
return False
def __str__(self):
"""Build a string representation of the connection."""
return f"ModbusSerialClient({self.framer} baud[{self.comm_params.baudrate}])"
def __repr__(self):
"""Return string representation."""
return (
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
f"framer={self.framer}, timeout={self.comm_params.timeout_connect}>"
)

View File

@@ -0,0 +1,275 @@
"""Modbus client async TCP communication."""
import asyncio
import select
import socket
import time
from typing import Any, Tuple, Type
from pymodbus.client.base import ModbusBaseClient
from pymodbus.exceptions import ConnectionException
from pymodbus.framer import ModbusFramer
from pymodbus.framer.socket_framer import ModbusSocketFramer
from pymodbus.logging import Log
from pymodbus.transport import CommType
from pymodbus.utilities import ModbusTransactionState
class AsyncModbusTcpClient(ModbusBaseClient, asyncio.Protocol):
"""**AsyncModbusTcpClient**.
:param host: Host IP address or host name
:param port: (optional) Port used for communication
:param framer: (optional) Framer class
:param source_address: (optional) source address of client
:param kwargs: (optional) Experimental parameters
Example::
from pymodbus.client import AsyncModbusTcpClient
async def run():
client = AsyncModbusTcpClient("localhost")
await client.connect()
...
client.close()
"""
def __init__(
self,
host: str,
port: int = 502,
framer: Type[ModbusFramer] = ModbusSocketFramer,
source_address: Tuple[str, int] = None,
**kwargs: Any,
) -> None:
"""Initialize Asyncio Modbus TCP Client."""
asyncio.Protocol.__init__(self)
if "CommType" not in kwargs:
kwargs["CommType"] = CommType.TCP
if source_address:
kwargs["source_address"] = source_address
ModbusBaseClient.__init__(
self,
framer=framer,
host=host,
port=port,
**kwargs,
)
self.params.source_address = source_address
async def connect(self) -> bool:
"""Initiate connection to start client."""
self.reset_delay()
Log.debug(
"Connecting to {}:{}.",
self.comm_params.host,
self.comm_params.port,
)
return await self.transport_connect()
@property
def connected(self):
"""Return true if connected."""
return self.is_active()
class ModbusTcpClient(ModbusBaseClient):
"""**ModbusTcpClient**.
:param host: Host IP address or host name
:param port: (optional) Port used for communication
:param framer: (optional) Framer class
:param source_address: (optional) source address of client
:param kwargs: (optional) Experimental parameters
Example::
from pymodbus.client import ModbusTcpClient
async def run():
client = ModbusTcpClient("localhost")
client.connect()
...
client.close()
Remark: There are no automatic reconnect as with AsyncModbusTcpClient
"""
def __init__(
self,
host: str,
port: int = 502,
framer: Type[ModbusFramer] = ModbusSocketFramer,
source_address: Tuple[str, int] = None,
**kwargs: Any,
) -> None:
"""Initialize Modbus TCP Client."""
if "CommType" not in kwargs:
kwargs["CommType"] = CommType.TCP
kwargs["use_sync"] = True
self.transport = None
super().__init__(framer=framer, host=host, port=port, **kwargs)
self.params.source_address = source_address
self.socket = None
@property
def connected(self):
"""Connect internal."""
return self.socket is not None
def connect(self): # pylint: disable=invalid-overridden-method
"""Connect to the modbus tcp server."""
if self.socket:
return True
try:
self.socket = socket.create_connection(
(self.comm_params.host, self.comm_params.port),
timeout=self.comm_params.timeout_connect,
source_address=self.params.source_address,
)
Log.debug(
"Connection to Modbus server established. Socket {}",
self.socket.getsockname(),
)
except OSError as msg:
Log.error(
"Connection to ({}, {}) failed: {}",
self.comm_params.host,
self.comm_params.port,
msg,
)
self.close()
return self.socket is not None
def close(self): # pylint: disable=arguments-differ
"""Close the underlying socket connection."""
if self.socket:
self.socket.close()
self.socket = None
def _check_read_buffer(self):
"""Check read buffer."""
time_ = time.time()
end = time_ + self.comm_params.timeout_connect
data = None
ready = select.select([self.socket], [], [], end - time_)
if ready[0]:
data = self.socket.recv(1024)
return data
def send(self, request):
"""Send data on the underlying socket."""
super().send(request)
if not self.socket:
raise ConnectionException(str(self))
if self.state == ModbusTransactionState.RETRYING:
if data := self._check_read_buffer():
return data
if request:
return self.socket.send(request)
return 0
def recv(self, size):
"""Read data from the underlying descriptor."""
super().recv(size)
if not self.socket:
raise ConnectionException(str(self))
# socket.recv(size) waits until it gets some data from the host but
# not necessarily the entire response that can be fragmented in
# many packets.
# To avoid split responses to be recognized as invalid
# messages and to be discarded, loops socket.recv until full data
# is received or timeout is expired.
# If timeout expires returns the read data, also if its length is
# less than the expected size.
self.socket.setblocking(0)
timeout = self.comm_params.timeout_connect
# If size isn't specified read up to 4096 bytes at a time.
if size is None:
recv_size = 4096
else:
recv_size = size
data = []
data_length = 0
time_ = time.time()
end = time_ + timeout
while recv_size > 0:
try:
ready = select.select([self.socket], [], [], end - time_)
except ValueError:
return self._handle_abrupt_socket_close(size, data, time.time() - time_)
if ready[0]:
if (recv_data := self.socket.recv(recv_size)) == b"":
return self._handle_abrupt_socket_close(
size, data, time.time() - time_
)
data.append(recv_data)
data_length += len(recv_data)
time_ = time.time()
# If size isn't specified continue to read until timeout expires.
if size:
recv_size = size - data_length
# Timeout is reduced also if some data has been received in order
# to avoid infinite loops when there isn't an expected response
# size and the slave sends noisy data continuously.
if time_ > end:
break
return b"".join(data)
def _handle_abrupt_socket_close(self, size, data, duration):
"""Handle unexpected socket close by remote end.
Intended to be invoked after determining that the remote end
has unexpectedly closed the connection, to clean up and handle
the situation appropriately.
:param size: The number of bytes that was attempted to read
:param data: The actual data returned
:param duration: Duration from the read was first attempted
until it was determined that the remote closed the
socket
:return: The more than zero bytes read from the remote end
:raises ConnectionException: If the remote end didn't send any
data at all before closing the connection.
"""
self.close()
size_txt = size if size else "unbounded read"
readsize = f"read of {size_txt} bytes"
msg = (
f"{self}: Connection unexpectedly closed "
f"{duration} seconds into {readsize}"
)
if data:
result = b"".join(data)
Log.warning(" after returning {} bytes: {} ", len(result), result)
return result
msg += " without response from slave before it closed connection"
raise ConnectionException(msg)
def is_socket_open(self):
"""Check if socket is open."""
return self.socket is not None
def __str__(self):
"""Build a string representation of the connection.
:returns: The string representation
"""
return f"ModbusTcpClient({self.comm_params.host}:{self.comm_params.port})"
def __repr__(self):
"""Return string representation."""
return (
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, timeout={self.comm_params.timeout_connect}>"
)

View File

@@ -0,0 +1,171 @@
"""Modbus client async TLS communication."""
import socket
import ssl
from typing import Any, Type
from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
from pymodbus.framer import ModbusFramer
from pymodbus.framer.tls_framer import ModbusTlsFramer
from pymodbus.logging import Log
from pymodbus.transport import CommParams, CommType
class AsyncModbusTlsClient(AsyncModbusTcpClient):
"""**AsyncModbusTlsClient**.
:param host: Host IP address or host name
:param port: (optional) Port used for communication
:param framer: (optional) Framer class
:param source_address: (optional) Source address of client
:param sslctx: (optional) SSLContext to use for TLS
:param certfile: (optional) Cert file path for TLS server request
:param keyfile: (optional) Key file path for TLS server request
:param password: (optional) Password for for decrypting private key file
:param server_hostname: (optional) Bind certificate to host
:param kwargs: (optional) Experimental parameters
..tip::
See ModbusBaseClient for common parameters.
Example::
from pymodbus.client import AsyncModbusTlsClient
async def run():
client = AsyncModbusTlsClient("localhost")
await client.connect()
...
client.close()
"""
def __init__(
self,
host: str,
port: int = 802,
framer: Type[ModbusFramer] = ModbusTlsFramer,
sslctx: ssl.SSLContext = None,
certfile: str = None,
keyfile: str = None,
password: str = None,
server_hostname: str = None,
**kwargs: Any,
):
"""Initialize Asyncio Modbus TLS Client."""
AsyncModbusTcpClient.__init__(
self,
host,
port=port,
framer=framer,
CommType=CommType.TLS,
sslctx=CommParams.generate_ssl(
False, certfile, keyfile, password, sslctx=sslctx
),
**kwargs,
)
self.params.server_hostname = server_hostname
async def connect(self) -> bool:
"""Initiate connection to start client."""
self.reset_delay()
Log.debug(
"Connecting to {}:{}.",
self.comm_params.host,
self.comm_params.port,
)
return await self.transport_connect()
class ModbusTlsClient(ModbusTcpClient):
"""**ModbusTlsClient**.
:param host: Host IP address or host name
:param port: (optional) Port used for communication
:param framer: (optional) Framer class
:param source_address: (optional) Source address of client
:param sslctx: (optional) SSLContext to use for TLS
:param certfile: (optional) Cert file path for TLS server request
:param keyfile: (optional) Key file path for TLS server request
:param password: (optional) Password for decrypting private key file
:param server_hostname: (optional) Bind certificate to host
:param kwargs: (optional) Experimental parameters
..tip::
See ModbusBaseClient for common parameters.
Example::
from pymodbus.client import ModbusTlsClient
async def run():
client = ModbusTlsClient("localhost")
client.connect()
...
client.close()
Remark: There are no automatic reconnect as with AsyncModbusTlsClient
"""
def __init__(
self,
host: str,
port: int = 802,
framer: Type[ModbusFramer] = ModbusTlsFramer,
sslctx: ssl.SSLContext = None,
certfile: str = None,
keyfile: str = None,
password: str = None,
server_hostname: str = None,
**kwargs: Any,
):
"""Initialize Modbus TLS Client."""
self.transport = None
super().__init__(
host, CommType=CommType.TLS, port=port, framer=framer, **kwargs
)
self.sslctx = CommParams.generate_ssl(
False, certfile, keyfile, password, sslctx=sslctx
)
self.params.server_hostname = server_hostname
@property
def connected(self):
"""Connect internal."""
return self.transport is not None
def connect(self):
"""Connect to the modbus tls server."""
if self.socket:
return True
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.params.source_address:
sock.bind(self.params.source_address)
self.socket = self.sslctx.wrap_socket(
sock, server_side=False, server_hostname=self.comm_params.host
)
self.socket.settimeout(self.comm_params.timeout_connect)
self.socket.connect((self.comm_params.host, self.comm_params.port))
except OSError as msg:
Log.error(
"Connection to ({}, {}) failed: {}",
self.comm_params.host,
self.comm_params.port,
msg,
)
self.close()
return self.socket is not None
def __str__(self):
"""Build a string representation of the connection."""
return f"ModbusTlsClient({self.comm_params.host}:{self.comm_params.port})"
def __repr__(self):
"""Return string representation."""
return (
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, sslctx={self.sslctx}, "
f"timeout={self.comm_params.timeout_connect}>"
)

View File

@@ -0,0 +1,190 @@
"""Modbus client async UDP communication."""
import asyncio
import socket
from typing import Any, Tuple, Type
from pymodbus.client.base import ModbusBaseClient
from pymodbus.exceptions import ConnectionException
from pymodbus.framer import ModbusFramer
from pymodbus.framer.socket_framer import ModbusSocketFramer
from pymodbus.logging import Log
from pymodbus.transport import CommType
DGRAM_TYPE = socket.SOCK_DGRAM
class AsyncModbusUdpClient(
ModbusBaseClient, asyncio.Protocol, asyncio.DatagramProtocol
):
"""**AsyncModbusUdpClient**.
:param host: Host IP address or host name
:param port: (optional) Port used for communication.
:param framer: (optional) Framer class.
:param source_address: (optional) source address of client,
:param kwargs: (optional) Experimental parameters
..tip::
See ModbusBaseClient for common parameters.
Example::
from pymodbus.client import AsyncModbusUdpClient
async def run():
client = AsyncModbusUdpClient("localhost")
await client.connect()
...
client.close()
"""
def __init__(
self,
host: str,
port: int = 502,
framer: Type[ModbusFramer] = ModbusSocketFramer,
source_address: Tuple[str, int] = None,
**kwargs: Any,
) -> None:
"""Initialize Asyncio Modbus UDP Client."""
asyncio.DatagramProtocol.__init__(self)
asyncio.Protocol.__init__(self)
ModbusBaseClient.__init__(
self, framer=framer, CommType=CommType.UDP, host=host, port=port, **kwargs
)
self.params.source_address = source_address
@property
def connected(self):
"""Return true if connected."""
return self.is_active()
async def connect(self) -> bool:
"""Start reconnecting asynchronous udp client.
:meta private:
"""
self.reset_delay()
Log.debug(
"Connecting to {}:{}.",
self.comm_params.host,
self.comm_params.port,
)
return await self.transport_connect()
class ModbusUdpClient(ModbusBaseClient):
"""**ModbusUdpClient**.
:param host: Host IP address or host name
:param port: (optional) Port used for communication.
:param framer: (optional) Framer class.
:param source_address: (optional) source address of client,
:param kwargs: (optional) Experimental parameters
..tip::
See ModbusBaseClient for common parameters.
Example::
from pymodbus.client import ModbusUdpClient
async def run():
client = ModbusUdpClient("localhost")
client.connect()
...
client.close()
Remark: There are no automatic reconnect as with AsyncModbusUdpClient
"""
def __init__(
self,
host: str,
port: int = 502,
framer: Type[ModbusFramer] = ModbusSocketFramer,
source_address: Tuple[str, int] = None,
**kwargs: Any,
) -> None:
"""Initialize Modbus UDP Client."""
kwargs["use_sync"] = True
self.transport = None
super().__init__(
framer=framer, port=port, host=host, CommType=CommType.UDP, **kwargs
)
self.params.source_address = source_address
self.socket = None
@property
def connected(self):
"""Connect internal."""
return self.socket is not None
def connect(self): # pylint: disable=invalid-overridden-method
"""Connect to the modbus tcp server.
:meta private:
"""
if self.socket:
return True
try:
family = ModbusUdpClient._get_address_family(self.comm_params.host)
self.socket = socket.socket(family, socket.SOCK_DGRAM)
self.socket.settimeout(self.comm_params.timeout_connect)
except OSError as exc:
Log.error("Unable to create udp socket {}", exc)
self.close()
return self.socket is not None
def close(self): # pylint: disable=arguments-differ
"""Close the underlying socket connection.
:meta private:
"""
self.socket = None
def send(self, request):
"""Send data on the underlying socket.
:meta private:
"""
super().send(request)
if not self.socket:
raise ConnectionException(str(self))
if request:
return self.socket.sendto(
request, (self.comm_params.host, self.comm_params.port)
)
return 0
def recv(self, size):
"""Read data from the underlying descriptor.
:meta private:
"""
super().recv(size)
if not self.socket:
raise ConnectionException(str(self))
return self.socket.recvfrom(size)[0]
def is_socket_open(self):
"""Check if socket is open.
:meta private:
"""
return True
def __str__(self):
"""Build a string representation of the connection."""
return f"ModbusUdpClient({self.comm_params.host}:{self.comm_params.port})"
def __repr__(self):
"""Return string representation."""
return (
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, timeout={self.comm_params.timeout_connect}>"
)