venv added, updated
This commit is contained in:
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
303
myenv/lib/python3.12/site-packages/pymodbus/client/base.py
Normal file
303
myenv/lib/python3.12/site-packages/pymodbus/client/base.py
Normal 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}"
|
||||
)
|
||||
503
myenv/lib/python3.12/site-packages/pymodbus/client/mixin.py
Normal file
503
myenv/lib/python3.12/site-packages/pymodbus/client/mixin.py
Normal 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
|
||||
@@ -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}"
|
||||
)
|
||||
308
myenv/lib/python3.12/site-packages/pymodbus/client/serial.py
Normal file
308
myenv/lib/python3.12/site-packages/pymodbus/client/serial.py
Normal 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}>"
|
||||
)
|
||||
297
myenv/lib/python3.12/site-packages/pymodbus/client/tcp.py
Normal file
297
myenv/lib/python3.12/site-packages/pymodbus/client/tcp.py
Normal 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}>"
|
||||
)
|
||||
232
myenv/lib/python3.12/site-packages/pymodbus/client/tls.py
Normal file
232
myenv/lib/python3.12/site-packages/pymodbus/client/tls.py
Normal 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}>"
|
||||
)
|
||||
225
myenv/lib/python3.12/site-packages/pymodbus/client/udp.py
Normal file
225
myenv/lib/python3.12/site-packages/pymodbus/client/udp.py
Normal 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}>"
|
||||
)
|
||||
Reference in New Issue
Block a user