Initial commit for tac2100_compteur_mbus2mqtt
This commit is contained in:
+19
@@ -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
|
||||
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
+324
@@ -0,0 +1,324 @@
|
||||
"""Base for all clients."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
|
||||
from pymodbus.client.mixin import ModbusClientMixin
|
||||
from pymodbus.exceptions import ConnectionException, ModbusIOException
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
from pymodbus.transaction import DictTransactionManager
|
||||
from pymodbus.transport import CommParams, ModbusProtocol
|
||||
from pymodbus.utilities import ModbusTransactionState
|
||||
|
||||
|
||||
class ModbusBaseClient(ModbusClientMixin, ModbusProtocol):
|
||||
"""**ModbusBaseClient**
|
||||
|
||||
**Parameters common to all clients**:
|
||||
|
||||
:param framer: (optional) Modbus Framer class.
|
||||
:param timeout: (optional) Timeout for a request, in seconds.
|
||||
:param retries: (optional) Max number of retries per request.
|
||||
:param retry_on_empty: (optional) Retry on empty response.
|
||||
:param close_comm_on_error: (optional) Close connection on error.
|
||||
:param strict: (optional) Strict timing, 1.5 character between requests.
|
||||
:param broadcast_enable: (optional) True to treat id 0 as broadcast address.
|
||||
:param reconnect_delay: (optional) Minimum delay in milliseconds before reconnecting.
|
||||
:param reconnect_delay_max: (optional) Maximum delay in milliseconds before reconnecting.
|
||||
:param on_reconnect_callback: (optional) Function that will be called just before a reconnection attempt.
|
||||
:param no_resend_on_retry: (optional) Do not resend request when retrying due to missing response.
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
|
||||
.. tip::
|
||||
Common parameters and all external methods for all clients are documented here,
|
||||
and not repeated with each client.
|
||||
|
||||
.. tip::
|
||||
**reconnect_delay** doubles automatically with each unsuccessful connect, from
|
||||
**reconnect_delay** to **reconnect_delay_max**.
|
||||
Set `reconnect_delay=0` to avoid automatic reconnection.
|
||||
|
||||
:mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
|
||||
|
||||
**Application methods, common to all clients**:
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
class _params:
|
||||
"""Parameter class."""
|
||||
|
||||
retries: int = None
|
||||
retry_on_empty: bool = None
|
||||
close_comm_on_error: bool = None
|
||||
strict: bool = None
|
||||
broadcast_enable: bool = None
|
||||
reconnect_delay: int = None
|
||||
|
||||
source_address: tuple[str, int] = None
|
||||
|
||||
server_hostname: str = None
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
framer: type[ModbusFramer] = None,
|
||||
timeout: float = 3,
|
||||
retries: int = 3,
|
||||
retry_on_empty: bool = False,
|
||||
close_comm_on_error: bool = False,
|
||||
strict: bool = True,
|
||||
broadcast_enable: bool = False,
|
||||
reconnect_delay: float = 0.1,
|
||||
reconnect_delay_max: float = 300,
|
||||
on_reconnect_callback: Callable[[], None] | None = None,
|
||||
no_resend_on_retry: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a client instance."""
|
||||
ModbusClientMixin.__init__(self)
|
||||
self.use_sync = kwargs.get("use_sync", False)
|
||||
setup_params = CommParams(
|
||||
comm_type=kwargs.get("CommType"),
|
||||
comm_name="comm",
|
||||
source_address=kwargs.get("source_address", ("0.0.0.0", 0)),
|
||||
reconnect_delay=reconnect_delay,
|
||||
reconnect_delay_max=reconnect_delay_max,
|
||||
timeout_connect=timeout,
|
||||
host=kwargs.get("host", None),
|
||||
port=kwargs.get("port", 0),
|
||||
sslctx=kwargs.get("sslctx", None),
|
||||
baudrate=kwargs.get("baudrate", None),
|
||||
bytesize=kwargs.get("bytesize", None),
|
||||
parity=kwargs.get("parity", None),
|
||||
stopbits=kwargs.get("stopbits", None),
|
||||
handle_local_echo=kwargs.get("handle_local_echo", False),
|
||||
)
|
||||
if not self.use_sync:
|
||||
ModbusProtocol.__init__(
|
||||
self,
|
||||
setup_params,
|
||||
False,
|
||||
)
|
||||
else:
|
||||
self.comm_params = setup_params
|
||||
self.params = self._params()
|
||||
self.params.retries = int(retries)
|
||||
self.params.retry_on_empty = bool(retry_on_empty)
|
||||
self.params.close_comm_on_error = bool(close_comm_on_error)
|
||||
self.params.strict = bool(strict)
|
||||
self.params.broadcast_enable = bool(broadcast_enable)
|
||||
self.on_reconnect_callback = on_reconnect_callback
|
||||
self.retry_on_empty: int = 0
|
||||
self.no_resend_on_retry = no_resend_on_retry
|
||||
self.slaves: list[int] = []
|
||||
|
||||
# Common variables.
|
||||
self.framer = framer(ClientDecoder(), self)
|
||||
self.transaction = DictTransactionManager(
|
||||
self, retries=retries, retry_on_empty=retry_on_empty, **kwargs
|
||||
)
|
||||
self.reconnect_delay_current = self.params.reconnect_delay
|
||||
self.use_udp = False
|
||||
self.state = ModbusTransactionState.IDLE
|
||||
self.last_frame_end: float = 0
|
||||
self.silent_interval: float = 0
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Client external interface
|
||||
# ----------------------------------------------------------------------- #
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return True
|
||||
|
||||
def register(self, custom_response_class: ModbusResponse) -> None:
|
||||
"""Register a custom response class with the decoder (call **sync**).
|
||||
|
||||
:param custom_response_class: (optional) Modbus response class.
|
||||
:raises MessageRegisterException: Check exception text.
|
||||
|
||||
Use register() to add non-standard responses (like e.g. a login prompt) and
|
||||
have them interpreted automatically.
|
||||
"""
|
||||
self.framer.decoder.register(custom_response_class)
|
||||
|
||||
def close(self, reconnect=False) -> None:
|
||||
"""Close connection."""
|
||||
if reconnect:
|
||||
self.connection_lost(asyncio.TimeoutError("Server not responding"))
|
||||
else:
|
||||
self.transport_close()
|
||||
|
||||
def idle_time(self) -> float:
|
||||
"""Time before initiating next transaction (call **sync**).
|
||||
|
||||
Applications can call message functions without checking idle_time(),
|
||||
this is done automatically.
|
||||
"""
|
||||
if self.last_frame_end is None or self.silent_interval is None:
|
||||
return 0
|
||||
return self.last_frame_end + self.silent_interval
|
||||
|
||||
def execute(self, request: ModbusRequest = None) -> ModbusResponse:
|
||||
"""Execute request and get response (call **sync/async**).
|
||||
|
||||
:param request: The request to process
|
||||
:returns: The result of the request execution
|
||||
:raises ConnectionException: Check exception text.
|
||||
"""
|
||||
if self.use_sync:
|
||||
if not self.connect():
|
||||
raise ConnectionException(f"Failed to connect[{self!s}]")
|
||||
return self.transaction.execute(request)
|
||||
if not self.transport:
|
||||
raise ConnectionException(f"Not connected[{self!s}]")
|
||||
return self.async_execute(request)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Merged client methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
async def async_execute(self, request=None):
|
||||
"""Execute requests asynchronously."""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
|
||||
count = 0
|
||||
while count <= self.params.retries:
|
||||
if not count or not self.no_resend_on_retry:
|
||||
self.transport_send(packet)
|
||||
if self.params.broadcast_enable and not request.slave_id:
|
||||
resp = b"Broadcast write sent - no response expected"
|
||||
break
|
||||
try:
|
||||
req = self._build_response(request.transaction_id)
|
||||
resp = await asyncio.wait_for(
|
||||
req, timeout=self.comm_params.timeout_connect
|
||||
)
|
||||
break
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
count += 1
|
||||
if count > self.params.retries:
|
||||
self.close(reconnect=True)
|
||||
raise ModbusIOException(
|
||||
f"ERROR: No response received after {self.params.retries} retries"
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
def callback_data(self, data: bytes, addr: tuple = None) -> int:
|
||||
"""Handle received data
|
||||
|
||||
returns number of bytes consumed
|
||||
"""
|
||||
self.framer.processIncomingPacket(data, self._handle_response, slave=0)
|
||||
return len(data)
|
||||
|
||||
def callback_disconnected(self, _reason: Exception) -> None:
|
||||
"""Handle lost connection"""
|
||||
for tid in list(self.transaction):
|
||||
self.raise_future(
|
||||
self.transaction.getTransaction(tid),
|
||||
ConnectionException("Connection lost during request"),
|
||||
)
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to the modbus remote host."""
|
||||
|
||||
def raise_future(self, my_future, exc):
|
||||
"""Set exception of a future if not done."""
|
||||
if not my_future.done():
|
||||
my_future.set_exception(exc)
|
||||
|
||||
def _handle_response(self, reply, **_kwargs):
|
||||
"""Handle the processed response and link to correct deferred."""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
if handler := self.transaction.getTransaction(tid):
|
||||
if not handler.done():
|
||||
handler.set_result(reply)
|
||||
else:
|
||||
Log.debug("Unrequested message: {}", reply, ":str")
|
||||
|
||||
def _build_response(self, tid):
|
||||
"""Return a deferred response for the current request."""
|
||||
my_future = asyncio.Future()
|
||||
if not self.transport:
|
||||
self.raise_future(my_future, ConnectionException("Client is not connected"))
|
||||
else:
|
||||
self.transaction.addTransaction(my_future, tid)
|
||||
return my_future
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Internal methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def send(self, request):
|
||||
"""Send request.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
if self.state != ModbusTransactionState.RETRYING:
|
||||
Log.debug('New Transaction state "SENDING"')
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
return request
|
||||
|
||||
def recv(self, size):
|
||||
"""Receive data.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return size
|
||||
|
||||
@classmethod
|
||||
def _get_address_family(cls, address):
|
||||
"""Get the correct address family."""
|
||||
try:
|
||||
_ = socket.inet_pton(socket.AF_INET6, address)
|
||||
except OSError: # not a valid ipv6 address
|
||||
return socket.AF_INET
|
||||
return socket.AF_INET6
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# The magic methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def __enter__(self):
|
||||
"""Implement the client with enter block.
|
||||
|
||||
:returns: The current instance of the client
|
||||
:raises ConnectionException:
|
||||
"""
|
||||
|
||||
if not self.connect():
|
||||
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Implement the client with enter block.
|
||||
|
||||
:returns: The current instance of the client
|
||||
:raises ConnectionException:
|
||||
"""
|
||||
if not await self.connect():
|
||||
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
|
||||
return self
|
||||
|
||||
def __exit__(self, klass, value, traceback):
|
||||
"""Implement the client with exit block."""
|
||||
self.close()
|
||||
|
||||
async def __aexit__(self, klass, value, traceback):
|
||||
"""Implement the client with exit block."""
|
||||
self.close()
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection.
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return (
|
||||
f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}"
|
||||
)
|
||||
+582
@@ -0,0 +1,582 @@
|
||||
"""Modbus Client Common."""
|
||||
import struct
|
||||
from enum import Enum
|
||||
from typing import Any, List, Tuple, Union
|
||||
|
||||
import pymodbus.bit_read_message as pdu_bit_read
|
||||
import pymodbus.bit_write_message as pdu_bit_write
|
||||
import pymodbus.diag_message as pdu_diag
|
||||
import pymodbus.file_message as pdu_file_msg
|
||||
import pymodbus.mei_message as pdu_mei
|
||||
import pymodbus.other_message as pdu_other_msg
|
||||
import pymodbus.register_read_message as pdu_reg_read
|
||||
import pymodbus.register_write_message as pdu_req_write
|
||||
from pymodbus.constants import INTERNAL_ERROR
|
||||
from pymodbus.exceptions import ModbusException
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
|
||||
|
||||
class ModbusClientMixin: # pylint: disable=too-many-public-methods
|
||||
"""**ModbusClientMixin**.
|
||||
|
||||
This is an interface class to facilitate the sending requests/receiving responses like read_coils.
|
||||
execute() allows to make a call with non-standard or user defined function codes (remember to add a PDU
|
||||
in the transport class to interpret the request/response).
|
||||
|
||||
Simple modbus message call::
|
||||
|
||||
response = client.read_coils(1, 10)
|
||||
# or
|
||||
response = await client.read_coils(1, 10)
|
||||
|
||||
Advanced modbus message call::
|
||||
|
||||
request = ReadCoilsRequest(1,10)
|
||||
response = client.execute(request)
|
||||
# or
|
||||
request = ReadCoilsRequest(1,10)
|
||||
response = await client.execute(request)
|
||||
|
||||
.. tip::
|
||||
All methods can be used directly (synchronous) or
|
||||
with await <method> (asynchronous) depending on the client used.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
|
||||
def execute(self, request: ModbusRequest) -> ModbusResponse:
|
||||
"""Execute request (code ???).
|
||||
|
||||
:param request: Request to send
|
||||
:raises ModbusException:
|
||||
|
||||
Call with custom function codes.
|
||||
|
||||
.. tip::
|
||||
Response is not interpreted.
|
||||
"""
|
||||
raise ModbusException(INTERNAL_ERROR)
|
||||
|
||||
def read_coils(
|
||||
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read coils (code 0x01).
|
||||
|
||||
:param address: Start address to read from
|
||||
:param count: (optional) Number of coils to read
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_bit_read.ReadCoilsRequest(address, count, slave, **kwargs)
|
||||
)
|
||||
|
||||
def read_discrete_inputs(
|
||||
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read discrete inputs (code 0x02).
|
||||
|
||||
:param address: Start address to read from
|
||||
:param count: (optional) Number of coils to read
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave, **kwargs)
|
||||
)
|
||||
|
||||
def read_holding_registers(
|
||||
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read holding registers (code 0x03).
|
||||
|
||||
:param address: Start address to read from
|
||||
:param count: (optional) Number of coils to read
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave, **kwargs)
|
||||
)
|
||||
|
||||
def read_input_registers(
|
||||
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read input registers (code 0x04).
|
||||
|
||||
:param address: Start address to read from
|
||||
:param count: (optional) Number of coils to read
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_reg_read.ReadInputRegistersRequest(address, count, slave, **kwargs)
|
||||
)
|
||||
|
||||
def write_coil(
|
||||
self, address: int, value: bool, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Write single coil (code 0x05).
|
||||
|
||||
:param address: Address to write to
|
||||
:param value: Boolean to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_bit_write.WriteSingleCoilRequest(address, value, slave, **kwargs)
|
||||
)
|
||||
|
||||
def write_register(
|
||||
self, address: int, value: int, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Write register (code 0x06).
|
||||
|
||||
:param address: Address to write to
|
||||
:param value: Value to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_req_write.WriteSingleRegisterRequest(address, value, slave, **kwargs)
|
||||
)
|
||||
|
||||
def read_exception_status(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
|
||||
"""Read Exception Status (code 0x07).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave, **kwargs))
|
||||
|
||||
def diag_query_data(
|
||||
self, msg: bytearray, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose query data (code 0x08 sub 0x00).
|
||||
|
||||
:param msg: Message to be returned
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave, **kwargs))
|
||||
|
||||
def diag_restart_communication(
|
||||
self, toggle: bool, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose restart communication (code 0x08 sub 0x01).
|
||||
|
||||
:param toggle: True if toggled.
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_diagnostic_register(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read diagnostic register (code 0x08 sub 0x02).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_change_ascii_input_delimeter(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose change ASCII input delimiter (code 0x08 sub 0x03).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_force_listen_only(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
|
||||
"""Diagnose force listen only (code 0x08 sub 0x04).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_clear_counters(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
|
||||
"""Diagnose clear counters (code 0x08 sub 0x0A).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ClearCountersRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_read_bus_message_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read bus message count (code 0x08 sub 0x0B).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnBusMessageCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_bus_comm_error_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_bus_exception_error_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_slave_message_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Slave Message Count (code 0x08 sub 0x0E).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnSlaveMessageCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_slave_no_response_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Slave No Response Count (code 0x08 sub 0x0F).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_slave_nak_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Slave NAK Count (code 0x08 sub 0x10).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_read_slave_busy_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Slave Busy Count (code 0x08 sub 0x11).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_read_bus_char_overrun_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_iop_overrun_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Iop overrun count (code 0x08 sub 0x13).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnIopOverrunCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_clear_overrun_counter(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_getclear_modbus_response(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose Get/Clear modbus plus (code 0x08 sub 0x15).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_get_comm_event_counter(self, **kwargs: Any) -> ModbusResponse:
|
||||
"""Diagnose get event counter (code 0x0B).
|
||||
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_other_msg.GetCommEventCounterRequest(**kwargs))
|
||||
|
||||
def diag_get_comm_event_log(self, **kwargs: Any) -> ModbusResponse:
|
||||
"""Diagnose get event counter (code 0x0C).
|
||||
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_other_msg.GetCommEventLogRequest(**kwargs))
|
||||
|
||||
def write_coils(
|
||||
self,
|
||||
address: int,
|
||||
values: Union[List[bool], bool],
|
||||
slave: int = 0,
|
||||
**kwargs: Any,
|
||||
) -> ModbusResponse:
|
||||
"""Write coils (code 0x0F).
|
||||
|
||||
:param address: Start address to write to
|
||||
:param values: List of booleans to write, or a single boolean to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave, **kwargs)
|
||||
)
|
||||
|
||||
def write_registers(
|
||||
self, address: int, values: Union[List[int], int], slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Write registers (code 0x10).
|
||||
|
||||
:param address: Start address to write to
|
||||
:param values: List of values to write, or a single value to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_req_write.WriteMultipleRegistersRequest(
|
||||
address, values, slave, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def report_slave_id(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
|
||||
"""Report slave ID (code 0x11).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave, **kwargs))
|
||||
|
||||
def read_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusResponse:
|
||||
"""Read file record (code 0x14).
|
||||
|
||||
:param records: List of (Reference type, File number, Record Number, Record Length)
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_file_msg.ReadFileRecordRequest(records, **kwargs))
|
||||
|
||||
def write_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusResponse:
|
||||
"""Write file record (code 0x15).
|
||||
|
||||
:param records: List of (Reference type, File number, Record Number, Record Length)
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_file_msg.WriteFileRecordRequest(records, **kwargs))
|
||||
|
||||
def mask_write_register(
|
||||
self,
|
||||
address: int = 0x0000,
|
||||
and_mask: int = 0xFFFF,
|
||||
or_mask: int = 0x0000,
|
||||
**kwargs: Any,
|
||||
) -> ModbusResponse:
|
||||
"""Mask write register (code 0x16).
|
||||
|
||||
:param address: The mask pointer address (0x0000 to 0xffff)
|
||||
:param and_mask: The and bitmask to apply to the register address
|
||||
:param or_mask: The or bitmask to apply to the register address
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, **kwargs)
|
||||
)
|
||||
|
||||
def readwrite_registers(
|
||||
self,
|
||||
read_address: int = 0,
|
||||
read_count: int = 0,
|
||||
write_address: int = 0,
|
||||
values: Union[List[int], int] = 0,
|
||||
slave: int = 0,
|
||||
**kwargs,
|
||||
) -> ModbusResponse:
|
||||
"""Read/Write registers (code 0x17).
|
||||
|
||||
:param read_address: The address to start reading from
|
||||
:param read_count: The number of registers to read from address
|
||||
:param write_address: The address to start writing to
|
||||
:param values: List of values to write, or a single value to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs:
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_reg_read.ReadWriteMultipleRegistersRequest(
|
||||
read_address=read_address,
|
||||
read_count=read_count,
|
||||
write_address=write_address,
|
||||
write_registers=values,
|
||||
slave=slave,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
def read_fifo_queue(self, address: int = 0x0000, **kwargs: Any) -> ModbusResponse:
|
||||
"""Read FIFO queue (code 0x18).
|
||||
|
||||
:param address: The address to start reading from
|
||||
:param kwargs:
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, **kwargs))
|
||||
|
||||
# code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED
|
||||
|
||||
def read_device_information(
|
||||
self, read_code: int = None, object_id: int = 0x00, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read FIFO queue (code 0x2B sub 0x0E).
|
||||
|
||||
:param read_code: The device information read code
|
||||
:param object_id: The object to read from
|
||||
:param kwargs:
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_mei.ReadDeviceInformationRequest(read_code, object_id, **kwargs)
|
||||
)
|
||||
|
||||
# ------------------
|
||||
# Converter methods
|
||||
# ------------------
|
||||
|
||||
class DATATYPE(Enum):
|
||||
"""Datatype enum (name and number of bytes), used for convert_* calls."""
|
||||
|
||||
INT16 = ("h", 1)
|
||||
UINT16 = ("H", 1)
|
||||
INT32 = ("i", 2)
|
||||
UINT32 = ("I", 2)
|
||||
INT64 = ("q", 4)
|
||||
UINT64 = ("Q", 4)
|
||||
FLOAT32 = ("f", 2)
|
||||
FLOAT64 = ("d", 4)
|
||||
STRING = ("s", 0)
|
||||
|
||||
@classmethod
|
||||
def convert_from_registers(
|
||||
cls, registers: List[int], data_type: DATATYPE
|
||||
) -> Union[int, float, str]:
|
||||
"""Convert registers to int/float/str.
|
||||
|
||||
:param registers: list of registers received from e.g. read_holding_registers()
|
||||
:param data_type: data type to convert to
|
||||
:returns: int, float or str depending on "data_type"
|
||||
:raises ModbusException: when size of registers is not 1, 2 or 4
|
||||
"""
|
||||
byte_list = bytearray()
|
||||
for x in registers:
|
||||
byte_list.extend(int.to_bytes(x, 2, "big"))
|
||||
if data_type == cls.DATATYPE.STRING:
|
||||
if byte_list[-1:] == b"\00":
|
||||
byte_list = byte_list[:-1]
|
||||
return byte_list.decode("utf-8")
|
||||
if len(registers) != data_type.value[1]:
|
||||
raise ModbusException(
|
||||
f"Illegal size ({len(registers)}) of register array, cannot convert!"
|
||||
)
|
||||
return struct.unpack(f">{data_type.value[0]}", byte_list)[0]
|
||||
|
||||
@classmethod
|
||||
def convert_to_registers(
|
||||
cls, value: Union[int, float, str], data_type: DATATYPE
|
||||
) -> List[int]:
|
||||
"""Convert int/float/str to registers (16/32/64 bit).
|
||||
|
||||
:param value: value to be converted
|
||||
:param data_type: data type to be encoded as registers
|
||||
:returns: List of registers, can be used directly in e.g. write_registers()
|
||||
:raises TypeError: when there is a mismatch between data_type and value
|
||||
"""
|
||||
if data_type == cls.DATATYPE.STRING:
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"Value should be string but is {type(value)}.")
|
||||
byte_list = value.encode()
|
||||
if len(byte_list) % 2:
|
||||
byte_list += b"\x00"
|
||||
else:
|
||||
byte_list = struct.pack(f">{data_type.value[0]}", value)
|
||||
regs = [
|
||||
int.from_bytes(byte_list[x : x + 2], "big")
|
||||
for x in range(0, len(byte_list), 2)
|
||||
]
|
||||
return regs
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
"""Modbus client async serial communication."""
|
||||
import asyncio
|
||||
import time
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from typing import Any, Type
|
||||
|
||||
from pymodbus.client.base import ModbusBaseClient
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.framer.rtu_framer import ModbusRtuFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.transport import CommType
|
||||
from pymodbus.utilities import ModbusTransactionState
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import serial
|
||||
|
||||
|
||||
class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol):
|
||||
"""**AsyncModbusSerialClient**.
|
||||
|
||||
:param port: Serial port used for communication.
|
||||
:param framer: (optional) Framer class.
|
||||
:param baudrate: (optional) Bits per second.
|
||||
:param bytesize: (optional) Number of bits per byte 7-8.
|
||||
:param parity: (optional) 'E'ven, 'O'dd or 'N'one
|
||||
:param stopbits: (optional) Number of stop bits 0-2¡.
|
||||
:param handle_local_echo: (optional) Discard local echo from dongle.
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
The serial communication is RS-485 based, and usually used with a usb RS485 dongle.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import AsyncModbusSerialClient
|
||||
|
||||
async def run():
|
||||
client = AsyncModbusSerialClient("dev/serial0")
|
||||
|
||||
await client.connect()
|
||||
...
|
||||
client.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: str,
|
||||
framer: Type[ModbusFramer] = ModbusRtuFramer,
|
||||
baudrate: int = 19200,
|
||||
bytesize: int = 8,
|
||||
parity: str = "N",
|
||||
stopbits: int = 1,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Asyncio Modbus Serial Client."""
|
||||
asyncio.Protocol.__init__(self)
|
||||
ModbusBaseClient.__init__(
|
||||
self,
|
||||
framer=framer,
|
||||
CommType=CommType.SERIAL,
|
||||
host=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=bytesize,
|
||||
parity=parity,
|
||||
stopbits=stopbits,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.is_active()
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Connect Async client."""
|
||||
self.reset_delay()
|
||||
Log.debug("Connecting to {}.", self.comm_params.host)
|
||||
return await self.transport_connect()
|
||||
|
||||
|
||||
class ModbusSerialClient(ModbusBaseClient):
|
||||
"""**ModbusSerialClient**.
|
||||
|
||||
:param port: Serial port used for communication.
|
||||
:param framer: (optional) Framer class.
|
||||
:param baudrate: (optional) Bits per second.
|
||||
:param bytesize: (optional) Number of bits per byte 7-8.
|
||||
:param parity: (optional) 'E'ven, 'O'dd or 'N'one
|
||||
:param stopbits: (optional) Number of stop bits 0-2¡.
|
||||
:param handle_local_echo: (optional) Discard local echo from dongle.
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
The serial communication is RS-485 based, and usually used with a usb RS485 dongle.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import ModbusSerialClient
|
||||
|
||||
def run():
|
||||
client = ModbusSerialClient("dev/serial0")
|
||||
|
||||
client.connect()
|
||||
...
|
||||
client.close()
|
||||
|
||||
|
||||
Remark: There are no automatic reconnect as with AsyncModbusSerialClient
|
||||
"""
|
||||
|
||||
state = ModbusTransactionState.IDLE
|
||||
inter_char_timeout: float = 0
|
||||
silent_interval: float = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: str,
|
||||
framer: Type[ModbusFramer] = ModbusRtuFramer,
|
||||
baudrate: int = 19200,
|
||||
bytesize: int = 8,
|
||||
parity: str = "N",
|
||||
stopbits: int = 1,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Modbus Serial Client."""
|
||||
self.transport = None
|
||||
kwargs["use_sync"] = True
|
||||
ModbusBaseClient.__init__(
|
||||
self,
|
||||
framer=framer,
|
||||
CommType=CommType.SERIAL,
|
||||
host=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=bytesize,
|
||||
parity=parity,
|
||||
stopbits=stopbits,
|
||||
**kwargs,
|
||||
)
|
||||
self.socket = None
|
||||
|
||||
self.last_frame_end = None
|
||||
|
||||
self._t0 = float(1 + 8 + 2) / self.comm_params.baudrate
|
||||
|
||||
"""
|
||||
The minimum delay is 0.01s and the maximum can be set to 0.05s.
|
||||
Setting too large a setting affects efficiency.
|
||||
"""
|
||||
self._recv_interval = (
|
||||
(round((100 * self._t0), 2) + 0.01)
|
||||
if (round((100 * self._t0), 2) + 0.01) < 0.05
|
||||
else 0.05
|
||||
)
|
||||
|
||||
if self.comm_params.baudrate > 19200:
|
||||
self.silent_interval = 1.75 / 1000 # ms
|
||||
else:
|
||||
self.inter_char_timeout = 1.5 * self._t0
|
||||
self.silent_interval = 3.5 * self._t0
|
||||
self.silent_interval = round(self.silent_interval, 6)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.connect()
|
||||
|
||||
def connect(self): # pylint: disable=invalid-overridden-method
|
||||
"""Connect to the modbus serial server."""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
self.socket = serial.serial_for_url(
|
||||
self.comm_params.host,
|
||||
timeout=self.comm_params.timeout_connect,
|
||||
bytesize=self.comm_params.bytesize,
|
||||
stopbits=self.comm_params.stopbits,
|
||||
baudrate=self.comm_params.baudrate,
|
||||
parity=self.comm_params.parity,
|
||||
)
|
||||
if isinstance(self.framer, ModbusRtuFramer):
|
||||
if self.params.strict:
|
||||
self.socket.interCharTimeout = self.inter_char_timeout
|
||||
self.last_frame_end = None
|
||||
except serial.SerialException as msg:
|
||||
Log.error("{}", msg)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self): # pylint: disable=arguments-differ
|
||||
"""Close the underlying socket connection."""
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _in_waiting(self):
|
||||
"""Return _in_waiting."""
|
||||
in_waiting = "in_waiting" if hasattr(self.socket, "in_waiting") else "inWaiting"
|
||||
|
||||
if in_waiting == "in_waiting":
|
||||
waitingbytes = getattr(self.socket, in_waiting)
|
||||
else:
|
||||
waitingbytes = getattr(self.socket, in_waiting)()
|
||||
return waitingbytes
|
||||
|
||||
def send(self, request):
|
||||
"""Send data on the underlying socket.
|
||||
|
||||
If receive buffer still holds some data then flush it.
|
||||
|
||||
Sleep if last send finished less than 3.5 character times ago.
|
||||
"""
|
||||
super().send(request)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
if request:
|
||||
try:
|
||||
if waitingbytes := self._in_waiting():
|
||||
result = self.socket.read(waitingbytes)
|
||||
if self.state == ModbusTransactionState.RETRYING:
|
||||
Log.debug(
|
||||
"Sending available data in recv buffer {}", result, ":hex"
|
||||
)
|
||||
return result
|
||||
Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
|
||||
except NotImplementedError:
|
||||
pass
|
||||
if self.state != ModbusTransactionState.SENDING:
|
||||
Log.debug('New Transaction state "SENDING"')
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
size = self.socket.write(request)
|
||||
return size
|
||||
return 0
|
||||
|
||||
def _wait_for_data(self):
|
||||
"""Wait for data."""
|
||||
size = 0
|
||||
more_data = False
|
||||
if (
|
||||
self.comm_params.timeout_connect is not None
|
||||
and self.comm_params.timeout_connect
|
||||
):
|
||||
condition = partial(
|
||||
lambda start, timeout: (time.time() - start) <= timeout,
|
||||
timeout=self.comm_params.timeout_connect,
|
||||
)
|
||||
else:
|
||||
condition = partial(lambda dummy1, dummy2: True, dummy2=None)
|
||||
start = time.time()
|
||||
while condition(start):
|
||||
available = self._in_waiting()
|
||||
if (more_data and not available) or (more_data and available == size):
|
||||
break
|
||||
if available and available != size:
|
||||
more_data = True
|
||||
size = available
|
||||
time.sleep(self._recv_interval)
|
||||
return size
|
||||
|
||||
def recv(self, size):
|
||||
"""Read data from the underlying descriptor."""
|
||||
super().recv(size)
|
||||
if not self.socket:
|
||||
raise ConnectionException(
|
||||
self.__str__() # pylint: disable=unnecessary-dunder-call
|
||||
)
|
||||
if size is None:
|
||||
size = self._wait_for_data()
|
||||
if size > self._in_waiting():
|
||||
self._wait_for_data()
|
||||
result = self.socket.read(size)
|
||||
return result
|
||||
|
||||
def is_socket_open(self):
|
||||
"""Check if socket is open."""
|
||||
if self.socket:
|
||||
if hasattr(self.socket, "is_open"):
|
||||
return self.socket.is_open
|
||||
return self.socket.isOpen()
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection."""
|
||||
return f"ModbusSerialClient({self.framer} baud[{self.comm_params.baudrate}])"
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
|
||||
f"framer={self.framer}, timeout={self.comm_params.timeout_connect}>"
|
||||
)
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
"""Modbus client async TCP communication."""
|
||||
import asyncio
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
from typing import Any, Tuple, Type
|
||||
|
||||
from pymodbus.client.base import ModbusBaseClient
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.transport import CommType
|
||||
from pymodbus.utilities import ModbusTransactionState
|
||||
|
||||
|
||||
class AsyncModbusTcpClient(ModbusBaseClient, asyncio.Protocol):
|
||||
"""**AsyncModbusTcpClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication
|
||||
:param framer: (optional) Framer class
|
||||
:param source_address: (optional) source address of client
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import AsyncModbusTcpClient
|
||||
|
||||
async def run():
|
||||
client = AsyncModbusTcpClient("localhost")
|
||||
|
||||
await client.connect()
|
||||
...
|
||||
client.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
framer: Type[ModbusFramer] = ModbusSocketFramer,
|
||||
source_address: Tuple[str, int] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Asyncio Modbus TCP Client."""
|
||||
asyncio.Protocol.__init__(self)
|
||||
if "CommType" not in kwargs:
|
||||
kwargs["CommType"] = CommType.TCP
|
||||
if source_address:
|
||||
kwargs["source_address"] = source_address
|
||||
ModbusBaseClient.__init__(
|
||||
self,
|
||||
framer=framer,
|
||||
host=host,
|
||||
port=port,
|
||||
**kwargs,
|
||||
)
|
||||
self.params.source_address = source_address
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Initiate connection to start client."""
|
||||
self.reset_delay()
|
||||
Log.debug(
|
||||
"Connecting to {}:{}.",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
)
|
||||
return await self.transport_connect()
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return true if connected."""
|
||||
return self.is_active()
|
||||
|
||||
|
||||
class ModbusTcpClient(ModbusBaseClient):
|
||||
"""**ModbusTcpClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication
|
||||
:param framer: (optional) Framer class
|
||||
:param source_address: (optional) source address of client
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
|
||||
async def run():
|
||||
client = ModbusTcpClient("localhost")
|
||||
|
||||
client.connect()
|
||||
...
|
||||
client.close()
|
||||
|
||||
Remark: There are no automatic reconnect as with AsyncModbusTcpClient
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
framer: Type[ModbusFramer] = ModbusSocketFramer,
|
||||
source_address: Tuple[str, int] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Modbus TCP Client."""
|
||||
if "CommType" not in kwargs:
|
||||
kwargs["CommType"] = CommType.TCP
|
||||
kwargs["use_sync"] = True
|
||||
self.transport = None
|
||||
super().__init__(framer=framer, host=host, port=port, **kwargs)
|
||||
self.params.source_address = source_address
|
||||
self.socket = None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.socket is not None
|
||||
|
||||
def connect(self): # pylint: disable=invalid-overridden-method
|
||||
"""Connect to the modbus tcp server."""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
self.socket = socket.create_connection(
|
||||
(self.comm_params.host, self.comm_params.port),
|
||||
timeout=self.comm_params.timeout_connect,
|
||||
source_address=self.params.source_address,
|
||||
)
|
||||
Log.debug(
|
||||
"Connection to Modbus server established. Socket {}",
|
||||
self.socket.getsockname(),
|
||||
)
|
||||
except OSError as msg:
|
||||
Log.error(
|
||||
"Connection to ({}, {}) failed: {}",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
msg,
|
||||
)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self): # pylint: disable=arguments-differ
|
||||
"""Close the underlying socket connection."""
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _check_read_buffer(self):
|
||||
"""Check read buffer."""
|
||||
time_ = time.time()
|
||||
end = time_ + self.comm_params.timeout_connect
|
||||
data = None
|
||||
ready = select.select([self.socket], [], [], end - time_)
|
||||
if ready[0]:
|
||||
data = self.socket.recv(1024)
|
||||
return data
|
||||
|
||||
def send(self, request):
|
||||
"""Send data on the underlying socket."""
|
||||
super().send(request)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
if self.state == ModbusTransactionState.RETRYING:
|
||||
if data := self._check_read_buffer():
|
||||
return data
|
||||
|
||||
if request:
|
||||
return self.socket.send(request)
|
||||
return 0
|
||||
|
||||
def recv(self, size):
|
||||
"""Read data from the underlying descriptor."""
|
||||
super().recv(size)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
|
||||
# socket.recv(size) waits until it gets some data from the host but
|
||||
# not necessarily the entire response that can be fragmented in
|
||||
# many packets.
|
||||
# To avoid split responses to be recognized as invalid
|
||||
# messages and to be discarded, loops socket.recv until full data
|
||||
# is received or timeout is expired.
|
||||
# If timeout expires returns the read data, also if its length is
|
||||
# less than the expected size.
|
||||
self.socket.setblocking(0)
|
||||
|
||||
timeout = self.comm_params.timeout_connect
|
||||
|
||||
# If size isn't specified read up to 4096 bytes at a time.
|
||||
if size is None:
|
||||
recv_size = 4096
|
||||
else:
|
||||
recv_size = size
|
||||
|
||||
data = []
|
||||
data_length = 0
|
||||
time_ = time.time()
|
||||
end = time_ + timeout
|
||||
while recv_size > 0:
|
||||
try:
|
||||
ready = select.select([self.socket], [], [], end - time_)
|
||||
except ValueError:
|
||||
return self._handle_abrupt_socket_close(size, data, time.time() - time_)
|
||||
if ready[0]:
|
||||
if (recv_data := self.socket.recv(recv_size)) == b"":
|
||||
return self._handle_abrupt_socket_close(
|
||||
size, data, time.time() - time_
|
||||
)
|
||||
data.append(recv_data)
|
||||
data_length += len(recv_data)
|
||||
time_ = time.time()
|
||||
|
||||
# If size isn't specified continue to read until timeout expires.
|
||||
if size:
|
||||
recv_size = size - data_length
|
||||
|
||||
# Timeout is reduced also if some data has been received in order
|
||||
# to avoid infinite loops when there isn't an expected response
|
||||
# size and the slave sends noisy data continuously.
|
||||
if time_ > end:
|
||||
break
|
||||
|
||||
return b"".join(data)
|
||||
|
||||
def _handle_abrupt_socket_close(self, size, data, duration):
|
||||
"""Handle unexpected socket close by remote end.
|
||||
|
||||
Intended to be invoked after determining that the remote end
|
||||
has unexpectedly closed the connection, to clean up and handle
|
||||
the situation appropriately.
|
||||
|
||||
:param size: The number of bytes that was attempted to read
|
||||
:param data: The actual data returned
|
||||
:param duration: Duration from the read was first attempted
|
||||
until it was determined that the remote closed the
|
||||
socket
|
||||
:return: The more than zero bytes read from the remote end
|
||||
:raises ConnectionException: If the remote end didn't send any
|
||||
data at all before closing the connection.
|
||||
"""
|
||||
self.close()
|
||||
size_txt = size if size else "unbounded read"
|
||||
readsize = f"read of {size_txt} bytes"
|
||||
msg = (
|
||||
f"{self}: Connection unexpectedly closed "
|
||||
f"{duration} seconds into {readsize}"
|
||||
)
|
||||
if data:
|
||||
result = b"".join(data)
|
||||
Log.warning(" after returning {} bytes: {} ", len(result), result)
|
||||
return result
|
||||
msg += " without response from slave before it closed connection"
|
||||
raise ConnectionException(msg)
|
||||
|
||||
def is_socket_open(self):
|
||||
"""Check if socket is open."""
|
||||
return self.socket is not None
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection.
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return f"ModbusTcpClient({self.comm_params.host}:{self.comm_params.port})"
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
|
||||
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, timeout={self.comm_params.timeout_connect}>"
|
||||
)
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
"""Modbus client async TLS communication."""
|
||||
import socket
|
||||
import ssl
|
||||
from typing import Any, Type
|
||||
|
||||
from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.framer.tls_framer import ModbusTlsFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.transport import CommParams, CommType
|
||||
|
||||
|
||||
class AsyncModbusTlsClient(AsyncModbusTcpClient):
|
||||
"""**AsyncModbusTlsClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication
|
||||
:param framer: (optional) Framer class
|
||||
:param source_address: (optional) Source address of client
|
||||
:param sslctx: (optional) SSLContext to use for TLS
|
||||
:param certfile: (optional) Cert file path for TLS server request
|
||||
:param keyfile: (optional) Key file path for TLS server request
|
||||
:param password: (optional) Password for for decrypting private key file
|
||||
:param server_hostname: (optional) Bind certificate to host
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
..tip::
|
||||
See ModbusBaseClient for common parameters.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import AsyncModbusTlsClient
|
||||
|
||||
async def run():
|
||||
client = AsyncModbusTlsClient("localhost")
|
||||
|
||||
await client.connect()
|
||||
...
|
||||
client.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 802,
|
||||
framer: Type[ModbusFramer] = ModbusTlsFramer,
|
||||
sslctx: ssl.SSLContext = None,
|
||||
certfile: str = None,
|
||||
keyfile: str = None,
|
||||
password: str = None,
|
||||
server_hostname: str = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Asyncio Modbus TLS Client."""
|
||||
AsyncModbusTcpClient.__init__(
|
||||
self,
|
||||
host,
|
||||
port=port,
|
||||
framer=framer,
|
||||
CommType=CommType.TLS,
|
||||
sslctx=CommParams.generate_ssl(
|
||||
False, certfile, keyfile, password, sslctx=sslctx
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
self.params.server_hostname = server_hostname
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Initiate connection to start client."""
|
||||
self.reset_delay()
|
||||
Log.debug(
|
||||
"Connecting to {}:{}.",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
)
|
||||
return await self.transport_connect()
|
||||
|
||||
|
||||
class ModbusTlsClient(ModbusTcpClient):
|
||||
"""**ModbusTlsClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication
|
||||
:param framer: (optional) Framer class
|
||||
:param source_address: (optional) Source address of client
|
||||
:param sslctx: (optional) SSLContext to use for TLS
|
||||
:param certfile: (optional) Cert file path for TLS server request
|
||||
:param keyfile: (optional) Key file path for TLS server request
|
||||
:param password: (optional) Password for decrypting private key file
|
||||
:param server_hostname: (optional) Bind certificate to host
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
..tip::
|
||||
See ModbusBaseClient for common parameters.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import ModbusTlsClient
|
||||
|
||||
async def run():
|
||||
client = ModbusTlsClient("localhost")
|
||||
|
||||
client.connect()
|
||||
...
|
||||
client.close()
|
||||
|
||||
|
||||
Remark: There are no automatic reconnect as with AsyncModbusTlsClient
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 802,
|
||||
framer: Type[ModbusFramer] = ModbusTlsFramer,
|
||||
sslctx: ssl.SSLContext = None,
|
||||
certfile: str = None,
|
||||
keyfile: str = None,
|
||||
password: str = None,
|
||||
server_hostname: str = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Modbus TLS Client."""
|
||||
self.transport = None
|
||||
super().__init__(
|
||||
host, CommType=CommType.TLS, port=port, framer=framer, **kwargs
|
||||
)
|
||||
self.sslctx = CommParams.generate_ssl(
|
||||
False, certfile, keyfile, password, sslctx=sslctx
|
||||
)
|
||||
self.params.server_hostname = server_hostname
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.transport is not None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the modbus tls server."""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
if self.params.source_address:
|
||||
sock.bind(self.params.source_address)
|
||||
self.socket = self.sslctx.wrap_socket(
|
||||
sock, server_side=False, server_hostname=self.comm_params.host
|
||||
)
|
||||
self.socket.settimeout(self.comm_params.timeout_connect)
|
||||
self.socket.connect((self.comm_params.host, self.comm_params.port))
|
||||
except OSError as msg:
|
||||
Log.error(
|
||||
"Connection to ({}, {}) failed: {}",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
msg,
|
||||
)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection."""
|
||||
return f"ModbusTlsClient({self.comm_params.host}:{self.comm_params.port})"
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
|
||||
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, sslctx={self.sslctx}, "
|
||||
f"timeout={self.comm_params.timeout_connect}>"
|
||||
)
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
"""Modbus client async UDP communication."""
|
||||
import asyncio
|
||||
import socket
|
||||
from typing import Any, Tuple, Type
|
||||
|
||||
from pymodbus.client.base import ModbusBaseClient
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.transport import CommType
|
||||
|
||||
|
||||
DGRAM_TYPE = socket.SOCK_DGRAM
|
||||
|
||||
|
||||
class AsyncModbusUdpClient(
|
||||
ModbusBaseClient, asyncio.Protocol, asyncio.DatagramProtocol
|
||||
):
|
||||
"""**AsyncModbusUdpClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication.
|
||||
:param framer: (optional) Framer class.
|
||||
:param source_address: (optional) source address of client,
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
..tip::
|
||||
See ModbusBaseClient for common parameters.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import AsyncModbusUdpClient
|
||||
|
||||
async def run():
|
||||
client = AsyncModbusUdpClient("localhost")
|
||||
|
||||
await client.connect()
|
||||
...
|
||||
client.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
framer: Type[ModbusFramer] = ModbusSocketFramer,
|
||||
source_address: Tuple[str, int] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Asyncio Modbus UDP Client."""
|
||||
asyncio.DatagramProtocol.__init__(self)
|
||||
asyncio.Protocol.__init__(self)
|
||||
ModbusBaseClient.__init__(
|
||||
self, framer=framer, CommType=CommType.UDP, host=host, port=port, **kwargs
|
||||
)
|
||||
self.params.source_address = source_address
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return true if connected."""
|
||||
return self.is_active()
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Start reconnecting asynchronous udp client.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self.reset_delay()
|
||||
Log.debug(
|
||||
"Connecting to {}:{}.",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
)
|
||||
return await self.transport_connect()
|
||||
|
||||
|
||||
class ModbusUdpClient(ModbusBaseClient):
|
||||
"""**ModbusUdpClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication.
|
||||
:param framer: (optional) Framer class.
|
||||
:param source_address: (optional) source address of client,
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
..tip::
|
||||
See ModbusBaseClient for common parameters.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import ModbusUdpClient
|
||||
|
||||
async def run():
|
||||
client = ModbusUdpClient("localhost")
|
||||
|
||||
client.connect()
|
||||
...
|
||||
client.close()
|
||||
|
||||
Remark: There are no automatic reconnect as with AsyncModbusUdpClient
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
framer: Type[ModbusFramer] = ModbusSocketFramer,
|
||||
source_address: Tuple[str, int] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Modbus UDP Client."""
|
||||
kwargs["use_sync"] = True
|
||||
self.transport = None
|
||||
super().__init__(
|
||||
framer=framer, port=port, host=host, CommType=CommType.UDP, **kwargs
|
||||
)
|
||||
self.params.source_address = source_address
|
||||
|
||||
self.socket = None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.socket is not None
|
||||
|
||||
def connect(self): # pylint: disable=invalid-overridden-method
|
||||
"""Connect to the modbus tcp server.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
family = ModbusUdpClient._get_address_family(self.comm_params.host)
|
||||
self.socket = socket.socket(family, socket.SOCK_DGRAM)
|
||||
self.socket.settimeout(self.comm_params.timeout_connect)
|
||||
except OSError as exc:
|
||||
Log.error("Unable to create udp socket {}", exc)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self): # pylint: disable=arguments-differ
|
||||
"""Close the underlying socket connection.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self.socket = None
|
||||
|
||||
def send(self, request):
|
||||
"""Send data on the underlying socket.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
super().send(request)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
if request:
|
||||
return self.socket.sendto(
|
||||
request, (self.comm_params.host, self.comm_params.port)
|
||||
)
|
||||
return 0
|
||||
|
||||
def recv(self, size):
|
||||
"""Read data from the underlying descriptor.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
super().recv(size)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
return self.socket.recvfrom(size)[0]
|
||||
|
||||
def is_socket_open(self):
|
||||
"""Check if socket is open.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection."""
|
||||
return f"ModbusUdpClient({self.comm_params.host}:{self.comm_params.port})"
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
|
||||
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, timeout={self.comm_params.timeout_connect}>"
|
||||
)
|
||||
Reference in New Issue
Block a user