venv added, updated

This commit is contained in:
Norbert
2024-09-13 09:46:28 +02:00
parent 577596d9f3
commit 82af8c809a
4812 changed files with 640223 additions and 2 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,303 @@
"""Base for all clients."""
from __future__ import annotations
import asyncio
import socket
from abc import abstractmethod
from collections.abc import Awaitable, Callable
from typing import cast
from pymodbus.client.mixin import ModbusClientMixin
from pymodbus.client.modbusclientprotocol import ModbusClientProtocol
from pymodbus.exceptions import ConnectionException, ModbusIOException
from pymodbus.factory import ClientDecoder
from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer
from pymodbus.logging import Log
from pymodbus.pdu import ModbusRequest, ModbusResponse
from pymodbus.transaction import SyncModbusTransactionManager
from pymodbus.transport import CommParams
from pymodbus.utilities import ModbusTransactionState
class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]]):
"""**ModbusBaseClient**.
:mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
"""
def __init__(
self,
framer: FramerType,
retries: int,
on_connect_callback: Callable[[bool], None] | None,
comm_params: CommParams | None = None,
) -> None:
"""Initialize a client instance."""
ModbusClientMixin.__init__(self) # type: ignore[arg-type]
if comm_params:
self.comm_params = comm_params
self.retries = retries
self.ctx = ModbusClientProtocol(
framer,
self.comm_params,
on_connect_callback,
)
# Common variables.
self.use_udp = False
self.state = ModbusTransactionState.IDLE
self.last_frame_end: float | None = 0
self.silent_interval: float = 0
self._lock = asyncio.Lock()
@property
def connected(self) -> bool:
"""Return state of connection."""
return self.ctx.is_active()
async def connect(self) -> bool:
"""Call transport connect."""
self.ctx.reset_delay()
Log.debug(
"Connecting to {}:{}.",
self.ctx.comm_params.host,
self.ctx.comm_params.port,
)
return await self.ctx.connect()
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.ctx.framer.decoder.register(custom_response_class)
def close(self, reconnect: bool = False) -> None:
"""Close connection."""
if reconnect:
self.ctx.connection_lost(asyncio.TimeoutError("Server not responding"))
else:
self.ctx.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):
"""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 not self.ctx.transport:
raise ConnectionException(f"Not connected[{self!s}]")
return self.async_execute(request)
async def async_execute(self, request) -> ModbusResponse:
"""Execute requests asynchronously."""
request.transaction_id = self.ctx.transaction.getNextTID()
packet = self.ctx.framer.buildPacket(request)
count = 0
while count <= self.retries:
async with self._lock:
req = self.build_response(request)
self.ctx.framer.resetFrame()
self.ctx.send(packet)
if not request.slave_id:
resp = None
break
try:
resp = await asyncio.wait_for(
req, timeout=self.ctx.comm_params.timeout_connect
)
break
except asyncio.exceptions.TimeoutError:
count += 1
if count > self.retries:
self.close(reconnect=True)
raise ModbusIOException(
f"ERROR: No response received after {self.retries} retries"
)
return resp # type: ignore[return-value]
def build_response(self, request: ModbusRequest):
"""Return a deferred response for the current request."""
my_future: asyncio.Future = asyncio.Future()
request.fut = my_future
if not self.ctx.transport:
if not my_future.done():
my_future.set_exception(ConnectionException("Client is not connected"))
else:
self.ctx.transaction.addTransaction(request)
return my_future
async def __aenter__(self):
"""Implement the client with enter block.
:returns: The current instance of the client
:raises ConnectionException:
"""
await self.connect()
return self
async def __aexit__(self, klass, value, traceback):
"""Implement the client with aexit block."""
self.close()
def __str__(self):
"""Build a string representation of the connection.
:returns: The string representation
"""
return (
f"{self.__class__.__name__} {self.ctx.comm_params.host}:{self.ctx.comm_params.port}"
)
class ModbusBaseSyncClient(ModbusClientMixin[ModbusResponse]):
"""**ModbusBaseClient**.
:mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
"""
def __init__(
self,
framer: FramerType,
retries: int,
comm_params: CommParams | None = None,
) -> None:
"""Initialize a client instance."""
ModbusClientMixin.__init__(self) # type: ignore[arg-type]
if comm_params:
self.comm_params = comm_params
self.retries = retries
self.slaves: list[int] = []
# Common variables.
self.framer: ModbusFramer = FRAMER_NAME_TO_CLASS.get(
framer, cast(type[ModbusFramer], framer)
)(ClientDecoder(), self)
self.transaction = SyncModbusTransactionManager(
self,
self.retries,
)
self.reconnect_delay_current = self.comm_params.reconnect_delay or 0
self.use_udp = False
self.state = ModbusTransactionState.IDLE
self.last_frame_end: float | None = 0
self.silent_interval: float = 0
self.transport = None
# ----------------------------------------------------------------------- #
# Client external interface
# ----------------------------------------------------------------------- #
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 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) -> 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 not self.connect():
raise ConnectionException(f"Failed to connect[{self!s}]")
return self.transaction.execute(request)
# ----------------------------------------------------------------------- #
# Internal methods
# ----------------------------------------------------------------------- #
def _start_send(self):
"""Send request.
:meta private:
"""
if self.state != ModbusTransactionState.RETRYING:
Log.debug('New Transaction state "SENDING"')
self.state = ModbusTransactionState.SENDING
@abstractmethod
def send(self, request: bytes) -> int:
"""Send request.
:meta private:
"""
@abstractmethod
def recv(self, size: int | None) -> bytes:
"""Receive data.
:meta private:
"""
@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
def connect(self) -> bool: # type: ignore[empty-body]
"""Connect to other end, overwritten."""
def close(self):
"""Close connection, overwritten."""
# ----------------------------------------------------------------------- #
# The magic methods
# ----------------------------------------------------------------------- #
def __enter__(self):
"""Implement the client with enter block.
:returns: The current instance of the client
:raises ConnectionException:
"""
self.connect()
return self
def __exit__(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,503 @@
"""Modbus Client Common."""
from __future__ import annotations
import struct
from enum import Enum
from typing import Generic, TypeVar
import pymodbus.pdu.bit_read_message as pdu_bit_read
import pymodbus.pdu.bit_write_message as pdu_bit_write
import pymodbus.pdu.diag_message as pdu_diag
import pymodbus.pdu.file_message as pdu_file_msg
import pymodbus.pdu.mei_message as pdu_mei
import pymodbus.pdu.other_message as pdu_other_msg
import pymodbus.pdu.register_read_message as pdu_reg_read
import pymodbus.pdu.register_write_message as pdu_req_write
from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ModbusRequest
T = TypeVar("T", covariant=False)
class ModbusClientMixin(Generic[T]): # 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) -> T:
"""Execute request (code ???).
:raises ModbusException:
Call with custom function codes.
.. tip::
Response is not interpreted.
"""
raise NotImplementedError("execute of ModbusClientMixin needs to be overridden")
def read_coils(self, address: int, count: int = 1, slave: int = 1) -> T:
"""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
:raises ModbusException:
"""
return self.execute(pdu_bit_read.ReadCoilsRequest(address, count, slave=slave))
def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 1) -> T:
"""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
:raises ModbusException:
"""
return self.execute(pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave=slave))
def read_holding_registers(self, address: int, count: int = 1, slave: int = 1) -> T:
"""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
:raises ModbusException:
"""
return self.execute(pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave=slave))
def read_input_registers(self, address: int, count: int = 1, slave: int = 1) -> T:
"""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
:raises ModbusException:
"""
return self.execute(pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave))
def write_coil(self, address: int, value: bool, slave: int = 1) -> T:
"""Write single coil (code 0x05).
:param address: Address to write to
:param value: Boolean to write
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave))
def write_register(self, address: int, value: int, slave: int = 1) -> T:
"""Write register (code 0x06).
:param address: Address to write to
:param value: Value to write
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave))
def read_exception_status(self, slave: int = 1) -> T:
"""Read Exception Status (code 0x07).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave=slave))
def diag_query_data(
self, msg: bytes, slave: int = 1) -> T:
"""Diagnose query data (code 0x08 sub 0x00).
:param msg: Message to be returned
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave))
def diag_restart_communication(
self, toggle: bool, slave: int = 1) -> T:
"""Diagnose restart communication (code 0x08 sub 0x01).
:param toggle: True if toggled.
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave)
)
def diag_read_diagnostic_register(self, slave: int = 1) -> T:
"""Diagnose read diagnostic register (code 0x08 sub 0x02).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave)
)
def diag_change_ascii_input_delimeter(self, slave: int = 1) -> T:
"""Diagnose change ASCII input delimiter (code 0x08 sub 0x03).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave)
)
def diag_force_listen_only(self, slave: int = 1) -> T:
"""Diagnose force listen only (code 0x08 sub 0x04).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave))
def diag_clear_counters(self, slave: int = 1) -> T:
"""Diagnose clear counters (code 0x08 sub 0x0A).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_diag.ClearCountersRequest(slave=slave))
def diag_read_bus_message_count(self, slave: int = 1) -> T:
"""Diagnose read bus message count (code 0x08 sub 0x0B).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnBusMessageCountRequest(slave=slave)
)
def diag_read_bus_comm_error_count(self, slave: int = 1) -> T:
"""Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave)
)
def diag_read_bus_exception_error_count(self, slave: int = 1) -> T:
"""Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave)
)
def diag_read_slave_message_count(self, slave: int = 1) -> T:
"""Diagnose read Slave Message Count (code 0x08 sub 0x0E).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnSlaveMessageCountRequest(slave=slave)
)
def diag_read_slave_no_response_count(self, slave: int = 1) -> T:
"""Diagnose read Slave No Response Count (code 0x08 sub 0x0F).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave)
)
def diag_read_slave_nak_count(self, slave: int = 1) -> T:
"""Diagnose read Slave NAK Count (code 0x08 sub 0x10).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave))
def diag_read_slave_busy_count(self, slave: int = 1) -> T:
"""Diagnose read Slave Busy Count (code 0x08 sub 0x11).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave))
def diag_read_bus_char_overrun_count(self, slave: int = 1) -> T:
"""Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave)
)
def diag_read_iop_overrun_count(self, slave: int = 1) -> T:
"""Diagnose read Iop overrun count (code 0x08 sub 0x13).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(
pdu_diag.ReturnIopOverrunCountRequest(slave=slave)
)
def diag_clear_overrun_counter(self, slave: int = 1) -> T:
"""Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave))
def diag_getclear_modbus_response(self, slave: int = 1) -> T:
"""Diagnose Get/Clear modbus plus (code 0x08 sub 0x15).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave))
def diag_get_comm_event_counter(self, slave: int = 1) -> T:
"""Diagnose get event counter (code 0x0B).
:raises ModbusException:
"""
return self.execute(pdu_other_msg.GetCommEventCounterRequest(slave=slave))
def diag_get_comm_event_log(self, slave: int = 1) -> T:
"""Diagnose get event counter (code 0x0C).
:raises ModbusException:
"""
return self.execute(pdu_other_msg.GetCommEventLogRequest(slave=slave))
def write_coils(
self,
address: int,
values: list[bool] | bool,
slave: int = 1,
) -> T:
"""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
:raises ModbusException:
"""
return self.execute(
pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave)
)
def write_registers(
self, address: int, values: list[int], slave: int = 1, skip_encode: bool = False) -> T:
"""Write registers (code 0x10).
:param address: Start address to write to
:param values: List of values to write
:param slave: (optional) Modbus slave ID
:param skip_encode: (optional) do not encode values
:raises ModbusException:
"""
return self.execute(
pdu_req_write.WriteMultipleRegistersRequest(address, values, slave=slave, skip_encode=skip_encode)
)
def report_slave_id(self, slave: int = 1) -> T:
"""Report slave ID (code 0x11).
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave=slave))
def read_file_record(self, records: list[tuple], slave: int = 1) -> T:
"""Read file record (code 0x14).
:param records: List of (Reference type, File number, Record Number, Record Length)
:param slave: device id
:raises ModbusException:
"""
return self.execute(pdu_file_msg.ReadFileRecordRequest(records, slave=slave))
def write_file_record(self, records: list[tuple], slave: int = 1) -> T:
"""Write file record (code 0x15).
:param records: List of (Reference type, File number, Record Number, Record Length)
:param slave: (optional) Device id
:raises ModbusException:
"""
return self.execute(pdu_file_msg.WriteFileRecordRequest(records, slave=slave))
def mask_write_register(
self,
address: int = 0x0000,
and_mask: int = 0xFFFF,
or_mask: int = 0x0000,
slave: int = 1,
) -> T:
"""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 slave: (optional) device id
:raises ModbusException:
"""
return self.execute(
pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave)
)
def readwrite_registers(
self,
read_address: int = 0,
read_count: int = 0,
write_address: int = 0,
address: int | None = None,
values: list[int] | int = 0,
slave: int = 1,
) -> T:
"""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 address: (optional) use as read/write address
:param values: List of values to write, or a single value to write
:param slave: (optional) Modbus slave ID
:raises ModbusException:
"""
if address:
read_address = address
write_address = address
return self.execute(
pdu_reg_read.ReadWriteMultipleRegistersRequest(
read_address=read_address,
read_count=read_count,
write_address=write_address,
write_registers=values,
slave=slave,
)
)
def read_fifo_queue(self, address: int = 0x0000, slave: int = 1) -> T:
"""Read FIFO queue (code 0x18).
:param address: The address to start reading from
:param slave: (optional) device id
:raises ModbusException:
"""
return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, slave=slave))
# code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED
def read_device_information(
self, read_code: int | None = None, object_id: int = 0x00, slave: int = 1) -> T:
"""Read FIFO queue (code 0x2B sub 0x0E).
:param read_code: The device information read code
:param object_id: The object to read from
:param slave: (optional) Device id
:raises ModbusException:
"""
return self.execute(
pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave)
)
# ------------------
# 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
) -> 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: 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,81 @@
"""ModbusProtocol implementation for all clients."""
from __future__ import annotations
from collections.abc import Callable
from typing import cast
from pymodbus.factory import ClientDecoder
from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer
from pymodbus.logging import Log
from pymodbus.transaction import ModbusTransactionManager
from pymodbus.transport import CommParams, ModbusProtocol
class ModbusClientProtocol(ModbusProtocol):
"""**ModbusClientProtocol**.
:mod:`ModbusClientProtocol` is normally not referenced outside :mod:`pymodbus`.
"""
def __init__(
self,
framer: FramerType,
params: CommParams,
on_connect_callback: Callable[[bool], None] | None = None,
) -> None:
"""Initialize a client instance."""
ModbusProtocol.__init__(
self,
params,
False,
)
self.on_connect_callback = on_connect_callback
# Common variables.
self.framer = FRAMER_NAME_TO_CLASS.get(
framer, cast(type[ModbusFramer], framer)
)(ClientDecoder(), self)
self.transaction = ModbusTransactionManager()
def _handle_response(self, reply):
"""Handle the processed response and link to correct deferred."""
if reply is not None:
tid = reply.transaction_id
if handler := self.transaction.getTransaction(tid):
reply.request = handler
if not handler.fut.done():
handler.fut.set_result(reply)
else:
Log.debug("Unrequested message: {}", reply, ":str")
def callback_new_connection(self):
"""Call when listener receive new connection request."""
def callback_connected(self) -> None:
"""Call when connection is succcesfull."""
if self.on_connect_callback:
self.loop.call_soon(self.on_connect_callback, True)
self.framer.resetFrame()
def callback_disconnected(self, exc: Exception | None) -> None:
"""Call when connection is lost."""
Log.debug("callback_disconnected called: {}", exc)
if self.on_connect_callback:
self.loop.call_soon(self.on_connect_callback, False)
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
"""Handle received data.
returns number of bytes consumed
"""
self.framer.processIncomingPacket(data, self._handle_response, 0)
return len(data)
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,308 @@
"""Modbus client async serial communication."""
from __future__ import annotations
import contextlib
import sys
import time
from collections.abc import Callable
from functools import partial
from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient
from pymodbus.exceptions import ConnectionException
from pymodbus.framer import FramerType
from pymodbus.logging import Log
from pymodbus.transport import CommParams, CommType
from pymodbus.utilities import ModbusTransactionState
with contextlib.suppress(ImportError):
import serial
class AsyncModbusSerialClient(ModbusBaseClient):
"""**AsyncModbusSerialClient**.
Fixed parameters:
:param port: Serial port used for communication.
Optional parameters:
:param framer: Framer name, default FramerType.RTU
:param baudrate: Bits per second.
:param bytesize: Number of bits per byte 7-8.
:param parity: 'E'ven, 'O'dd or 'N'one
:param stopbits: Number of stop bits 1, 1.5, 2.
:param handle_local_echo: Discard local echo from dongle.
:param name: Set communication name, used in logging
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
:param timeout: Timeout for a connection request, in seconds.
:param retries: Max number of retries per request.
:param on_reconnect_callback: Function that will be called just before a reconnection attempt.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
Example::
from pymodbus.client import AsyncModbusSerialClient
async def run():
client = AsyncModbusSerialClient("dev/serial0")
await client.connect()
...
client.close()
Please refer to :ref:`Pymodbus internals` for advanced usage.
"""
def __init__( # pylint: disable=too-many-arguments
self,
port: str,
framer: FramerType = FramerType.RTU,
baudrate: int = 19200,
bytesize: int = 8,
parity: str = "N",
stopbits: int = 1,
handle_local_echo: bool = False,
name: str = "comm",
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
timeout: float = 3,
retries: int = 3,
on_connect_callback: Callable[[bool], None] | None = None,
) -> None:
"""Initialize Asyncio Modbus Serial Client."""
if "serial" not in sys.modules:
raise RuntimeError(
"Serial client requires pyserial "
'Please install with "pip install pyserial" and try again.'
)
self.comm_params = CommParams(
comm_type=CommType.SERIAL,
host=port,
baudrate=baudrate,
bytesize=bytesize,
parity=parity,
stopbits=stopbits,
handle_local_echo=handle_local_echo,
comm_name=name,
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
)
ModbusBaseClient.__init__(
self,
framer,
retries,
on_connect_callback,
)
def close(self, reconnect: bool = False) -> None:
"""Close connection."""
super().close(reconnect=reconnect)
class ModbusSerialClient(ModbusBaseSyncClient):
"""**ModbusSerialClient**.
Fixed parameters:
:param port: Serial port used for communication.
Optional parameters:
:param framer: Framer name, default FramerType.RTU
:param baudrate: Bits per second.
:param bytesize: Number of bits per byte 7-8.
:param parity: 'E'ven, 'O'dd or 'N'one
:param stopbits: Number of stop bits 0-2.
:param handle_local_echo: Discard local echo from dongle.
:param name: Set communication name, used in logging
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
:param timeout: Timeout for a connection request, in seconds.
:param retries: Max number of retries per request.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
Example::
from pymodbus.client import ModbusSerialClient
def run():
client = ModbusSerialClient("dev/serial0")
client.connect()
...
client.close()
Please refer to :ref:`Pymodbus internals` for advanced usage.
Remark: There are no automatic reconnect as with AsyncModbusSerialClient
"""
state = ModbusTransactionState.IDLE
inter_byte_timeout: float = 0
silent_interval: float = 0
def __init__( # pylint: disable=too-many-arguments
self,
port: str,
framer: FramerType = FramerType.RTU,
baudrate: int = 19200,
bytesize: int = 8,
parity: str = "N",
stopbits: int = 1,
handle_local_echo: bool = False,
name: str = "comm",
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
timeout: float = 3,
retries: int = 3,
) -> None:
"""Initialize Modbus Serial Client."""
self.comm_params = CommParams(
comm_type=CommType.SERIAL,
host=port,
baudrate=baudrate,
bytesize=bytesize,
parity=parity,
stopbits=stopbits,
handle_local_echo=handle_local_echo,
comm_name=name,
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
)
super().__init__(
framer,
retries,
)
if "serial" not in sys.modules:
raise RuntimeError(
"Serial client requires pyserial "
'Please install with "pip install pyserial" and try again.'
)
self.socket: serial.Serial | None = None
self.last_frame_end = None
self._t0 = float(1 + bytesize + stopbits) / baudrate
# Check every 4 bytes / 2 registers if the reading is ready
self._recv_interval = self._t0 * 4
# Set a minimum of 1ms for high baudrates
self._recv_interval = max(self._recv_interval, 0.001)
if baudrate > 19200:
self.silent_interval = 1.75 / 1000 # ms
else:
self.inter_byte_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) -> bool:
"""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,
exclusive=True,
)
self.socket.inter_byte_timeout = self.inter_byte_timeout
self.last_frame_end = None
# except serial.SerialException as msg:
# pyserial raises undocumented exceptions like termios
except Exception as msg: # pylint: disable=broad-exception-caught
Log.error("{}", msg)
self.close()
return self.socket is not None
def close(self):
"""Close the underlying socket connection."""
if self.socket:
self.socket.close()
self.socket = None
def _in_waiting(self):
"""Return waiting bytes."""
return getattr(self.socket, "in_waiting") if hasattr(self.socket, "in_waiting") else getattr(self.socket, "inWaiting")()
def send(self, request: bytes) -> int:
"""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()._start_send()
if not self.socket:
raise ConnectionException(str(self))
if request:
if waitingbytes := self._in_waiting():
result = self.socket.read(waitingbytes)
Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
if (size := self.socket.write(request)) is None:
size = 0
return size
return 0
def _wait_for_data(self) -> int:
"""Wait for data."""
size = 0
more_data = False
condition = partial(
lambda start, timeout: (time.time() - start) <= timeout,
timeout=self.comm_params.timeout_connect,
)
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: int | None) -> bytes:
"""Read data from the underlying descriptor."""
if not self.socket:
raise ConnectionException(str(self))
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) -> bool:
"""Check if socket is open."""
if self.socket:
return self.socket.is_open
return False
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,297 @@
"""Modbus client async TCP communication."""
from __future__ import annotations
import select
import socket
import time
from collections.abc import Callable
from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient
from pymodbus.exceptions import ConnectionException
from pymodbus.framer import FramerType
from pymodbus.logging import Log
from pymodbus.transport import CommParams, CommType
class AsyncModbusTcpClient(ModbusBaseClient):
"""**AsyncModbusTcpClient**.
Fixed parameters:
:param host: Host IP address or host name
Optional parameters:
:param framer: Framer name, default FramerType.SOCKET
:param port: Port used for communication
:param name: Set communication name, used in logging
:param source_address: source address of client
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
:param timeout: Timeout for a connection request, in seconds.
:param retries: Max number of retries per request.
:param on_reconnect_callback: Function that will be called just before a reconnection attempt.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
Example::
from pymodbus.client import AsyncModbusTcpClient
async def run():
client = AsyncModbusTcpClient("localhost")
await client.connect()
...
client.close()
Please refer to :ref:`Pymodbus internals` for advanced usage.
"""
socket: socket.socket | None
def __init__( # pylint: disable=too-many-arguments
self,
host: str,
framer: FramerType = FramerType.SOCKET,
port: int = 502,
name: str = "comm",
source_address: tuple[str, int] | None = None,
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
timeout: float = 3,
retries: int = 3,
on_connect_callback: Callable[[bool], None] | None = None,
) -> None:
"""Initialize Asyncio Modbus TCP Client."""
if not hasattr(self,"comm_params"):
self.comm_params = CommParams(
comm_type=CommType.TCP,
host=host,
port=port,
comm_name=name,
source_address=source_address,
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
)
ModbusBaseClient.__init__(
self,
framer,
retries,
on_connect_callback,
)
def close(self, reconnect: bool = False) -> None:
"""Close connection."""
super().close(reconnect=reconnect)
class ModbusTcpClient(ModbusBaseSyncClient):
"""**ModbusTcpClient**.
Fixed parameters:
:param host: Host IP address or host name
Optional parameters:
:param framer: Framer name, default FramerType.SOCKET
:param port: Port used for communication
:param name: Set communication name, used in logging
:param source_address: source address of client
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
:param timeout: Timeout for a connection request, in seconds.
:param retries: Max number of retries per request.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
Example::
from pymodbus.client import ModbusTcpClient
async def run():
client = ModbusTcpClient("localhost")
client.connect()
...
client.close()
Please refer to :ref:`Pymodbus internals` for advanced usage.
Remark: There are no automatic reconnect as with AsyncModbusTcpClient
"""
socket: socket.socket | None
def __init__(
self,
host: str,
framer: FramerType = FramerType.SOCKET,
port: int = 502,
name: str = "comm",
source_address: tuple[str, int] | None = None,
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
timeout: float = 3,
retries: int = 3,
) -> None:
"""Initialize Modbus TCP Client."""
if not hasattr(self,"comm_params"):
self.comm_params = CommParams(
comm_type=CommType.TCP,
host=host,
port=port,
comm_name=name,
source_address=source_address,
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
)
super().__init__(framer, retries)
self.socket = None
@property
def connected(self) -> bool:
"""Connect internal."""
return self.socket is not None
def connect(self):
"""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.comm_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):
"""Close the underlying socket connection."""
if self.socket:
self.socket.close()
self.socket = None
def send(self, request):
"""Send data on the underlying socket."""
super()._start_send()
if not self.socket:
raise ConnectionException(str(self))
if request:
return self.socket.send(request)
return 0
def recv(self, size: int | None) -> bytes:
"""Read data from the underlying descriptor."""
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(False)
timeout = self.comm_params.timeout_connect or 0
# 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: list[bytes] = []
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: int | None, data: list[bytes], duration: float) -> bytes:
"""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:.3f} 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) -> bool:
"""Check if socket is open."""
return self.socket is not None
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,232 @@
"""Modbus client async TLS communication."""
from __future__ import annotations
import socket
import ssl
from collections.abc import Callable
from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
from pymodbus.framer import FramerType
from pymodbus.logging import Log
from pymodbus.transport import CommParams, CommType
class AsyncModbusTlsClient(AsyncModbusTcpClient):
"""**AsyncModbusTlsClient**.
Fixed parameters:
:param host: Host IP address or host name
Optional parameters:
:param sslctx: SSLContext to use for TLS
:param framer: Framer name, default FramerType.TLS
:param port: Port used for communication
:param name: Set communication name, used in logging
:param source_address: Source address of client
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
:param timeout: Timeout for a connection request, in seconds.
:param retries: Max number of retries per request.
:param on_reconnect_callback: Function that will be called just before a reconnection attempt.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
Example::
from pymodbus.client import AsyncModbusTlsClient
async def run():
client = AsyncModbusTlsClient("localhost")
await client.connect()
...
client.close()
Please refer to :ref:`Pymodbus internals` for advanced usage.
"""
def __init__( # pylint: disable=too-many-arguments
self,
host: str,
sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT),
framer: FramerType = FramerType.TLS,
port: int = 802,
name: str = "comm",
source_address: tuple[str, int] | None = None,
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
timeout: float = 3,
retries: int = 3,
on_connect_callback: Callable[[bool], None] | None = None,
):
"""Initialize Asyncio Modbus TLS Client."""
self.comm_params = CommParams(
comm_type=CommType.TLS,
host=host,
sslctx=sslctx,
port=port,
comm_name=name,
source_address=source_address,
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
)
AsyncModbusTcpClient.__init__(
self,
"",
framer=framer,
retries=retries,
on_connect_callback=on_connect_callback,
)
@classmethod
def generate_ssl(
cls,
certfile: str | None = None,
keyfile: str | None = None,
password: str | None = None,
) -> ssl.SSLContext:
"""Generate sslctx from cert/key/password.
:param certfile: Cert file path for TLS server request
:param keyfile: Key file path for TLS server request
:param password: Password for for decrypting private key file
Remark:
- MODBUS/TCP Security Protocol Specification demands TLSv2 at least
- verify_mode is set to ssl.NONE
"""
return CommParams.generate_ssl(
False, certfile=certfile, keyfile=keyfile, password=password
)
class ModbusTlsClient(ModbusTcpClient):
"""**ModbusTlsClient**.
Fixed parameters:
:param host: Host IP address or host name
Optional parameters:
:param sslctx: SSLContext to use for TLS
:param framer: Framer name, default FramerType.TLS
:param port: Port used for communication
:param name: Set communication name, used in logging
:param source_address: Source address of client
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
:param timeout: Timeout for a connection request, in seconds.
:param retries: Max number of retries per request.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
Example::
from pymodbus.client import ModbusTlsClient
async def run():
client = ModbusTlsClient("localhost")
client.connect()
...
client.close()
Please refer to :ref:`Pymodbus internals` for advanced usage.
Remark: There are no automatic reconnect as with AsyncModbusTlsClient
"""
def __init__( # pylint: disable=too-many-arguments
self,
host: str,
sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT),
framer: FramerType = FramerType.TLS,
port: int = 802,
name: str = "comm",
source_address: tuple[str, int] | None = None,
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
timeout: float = 3,
retries: int = 3,
):
"""Initialize Modbus TLS Client."""
self.comm_params = CommParams(
comm_type=CommType.TLS,
host=host,
sslctx=sslctx,
port=port,
comm_name=name,
source_address=source_address,
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
)
super().__init__(
"",
framer=framer,
retries=retries,
)
@classmethod
def generate_ssl(
cls,
certfile: str | None = None,
keyfile: str | None = None,
password: str | None = None,
) -> ssl.SSLContext:
"""Generate sslctx from cert/key/password.
:param certfile: Cert file path for TLS server request
:param keyfile: Key file path for TLS server request
:param password: Password for for decrypting private key file
Remark:
- MODBUS/TCP Security Protocol Specification demands TLSv2 at least
- verify_mode is set to ssl.NONE
"""
return CommParams.generate_ssl(
False, certfile=certfile, keyfile=keyfile, password=password,
)
@property
def connected(self) -> bool:
"""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.comm_params.source_address:
sock.bind(self.comm_params.source_address)
self.socket = self.comm_params.sslctx.wrap_socket(sock, server_side=False) # type: ignore[union-attr]
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 __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.comm_params.sslctx}, "
f"timeout={self.comm_params.timeout_connect}>"
)

View File

@@ -0,0 +1,225 @@
"""Modbus client async UDP communication."""
from __future__ import annotations
import socket
from collections.abc import Callable
from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient
from pymodbus.exceptions import ConnectionException
from pymodbus.framer import FramerType
from pymodbus.logging import Log
from pymodbus.transport import CommParams, CommType
DGRAM_TYPE = socket.SOCK_DGRAM
class AsyncModbusUdpClient(ModbusBaseClient):
"""**AsyncModbusUdpClient**.
Fixed parameters:
:param host: Host IP address or host name
Optional parameters:
:param framer: Framer name, default FramerType.SOCKET
:param port: Port used for communication.
:param name: Set communication name, used in logging
:param source_address: source address of client,
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
:param timeout: Timeout for a connection request, in seconds.
:param retries: Max number of retries per request.
:param on_reconnect_callback: Function that will be called just before a reconnection attempt.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
Example::
from pymodbus.client import AsyncModbusUdpClient
async def run():
client = AsyncModbusUdpClient("localhost")
await client.connect()
...
client.close()
Please refer to :ref:`Pymodbus internals` for advanced usage.
"""
def __init__( # pylint: disable=too-many-arguments
self,
host: str,
framer: FramerType = FramerType.SOCKET,
port: int = 502,
name: str = "comm",
source_address: tuple[str, int] | None = None,
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
timeout: float = 3,
retries: int = 3,
on_connect_callback: Callable[[bool], None] | None = None,
) -> None:
"""Initialize Asyncio Modbus UDP Client."""
self.comm_params = CommParams(
comm_type=CommType.UDP,
host=host,
port=port,
comm_name=name,
source_address=source_address,
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
)
ModbusBaseClient.__init__(
self,
framer,
retries,
on_connect_callback,
)
self.source_address = source_address
@property
def connected(self):
"""Return true if connected."""
return self.ctx.is_active()
class ModbusUdpClient(ModbusBaseSyncClient):
"""**ModbusUdpClient**.
Fixed parameters:
:param host: Host IP address or host name
Optional parameters:
:param framer: Framer name, default FramerType.SOCKET
:param port: Port used for communication.
:param name: Set communication name, used in logging
:param source_address: source address of client,
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
:param timeout: Timeout for a connection request, in seconds.
:param retries: Max number of retries per request.
.. tip::
**reconnect_delay** doubles automatically with each unsuccessful connect, from
**reconnect_delay** to **reconnect_delay_max**.
Set `reconnect_delay=0` to avoid automatic reconnection.
Example::
from pymodbus.client import ModbusUdpClient
async def run():
client = ModbusUdpClient("localhost")
client.connect()
...
client.close()
Please refer to :ref:`Pymodbus internals` for advanced usage.
Remark: There are no automatic reconnect as with AsyncModbusUdpClient
"""
socket: socket.socket | None
def __init__(
self,
host: str,
framer: FramerType = FramerType.SOCKET,
port: int = 502,
name: str = "comm",
source_address: tuple[str, int] | None = None,
reconnect_delay: float = 0.1,
reconnect_delay_max: float = 300,
timeout: float = 3,
retries: int = 3,
) -> None:
"""Initialize Modbus UDP Client."""
self.comm_params = CommParams(
comm_type=CommType.UDP,
host=host,
port=port,
comm_name=name,
source_address=source_address,
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
)
super().__init__(framer, retries)
self.socket = None
@property
def connected(self) -> bool:
"""Connect internal."""
return self.socket is not None
def connect(self):
"""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):
"""Close the underlying socket connection.
:meta private:
"""
self.socket = None
def send(self, request: bytes) -> int:
"""Send data on the underlying socket.
:meta private:
"""
super()._start_send()
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: int | None) -> bytes:
"""Read data from the underlying descriptor.
:meta private:
"""
if not self.socket:
raise ConnectionException(str(self))
if size is None:
size = 0
return self.socket.recvfrom(size)[0]
def is_socket_open(self):
"""Check if socket is open.
:meta private:
"""
return True
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}>"
)