Dane Sabo 3299181c70 Auto sync: 2025-09-02 22:47:53 (10335 files changed)
M  lazy-lock.json

M  lua/custom/configs/lspconfig.lua

M  lua/custom/init.lua

A  lua/custom/journal.lua

A  nvim_venv/bin/Activate.ps1

A  nvim_venv/bin/activate

A  nvim_venv/bin/activate.csh

A  nvim_venv/bin/activate.fish
2025-09-02 22:47:53 -04:00

321 lines
11 KiB
Python

"""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.pdu import ModbusPDU
from pymodbus.transport import CommParams, CommType
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 connecting and receiving data, in seconds.
:param retries: Max number of retries per request.
:param trace_packet: Called with bytestream received/to be sent
:param trace_pdu: Called with PDU received/to be sent
:param trace_connect: Called when connected/disconnected
.. tip::
The trace methods allow to modify the datastream/pdu !
.. 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,
trace_packet: Callable[[bool, bytes], bytes] | None = None,
trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None,
trace_connect: Callable[[bool], None] | None = None,
) -> None:
"""Initialize Asyncio Modbus Serial Client."""
if "serial" not in sys.modules: # pragma: no cover
raise RuntimeError(
"Serial client requires pyserial "
'Please install with "pip install pyserial" and try again.'
)
if framer not in [FramerType.ASCII, FramerType.RTU]:
raise TypeError("Only FramerType RTU/ASCII allowed.")
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,
self.comm_params,
trace_packet,
trace_pdu,
trace_connect,
)
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: Not used in the sync client
:param reconnect_delay_max: Not used in the sync client
:param timeout: Timeout for connecting and receiving data, in seconds.
:param retries: Max number of retries per request.
:param trace_packet: Called with bytestream received/to be sent
:param trace_pdu: Called with PDU received/to be sent
:param trace_connect: Called when connected/disconnected
.. tip::
The trace methods allow to modify the datastream/pdu !
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.
"""
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,
trace_packet: Callable[[bool, bytes], bytes] | None = None,
trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None,
trace_connect: Callable[[bool], None] | None = None,
) -> None:
"""Initialize Modbus Serial Client."""
if "serial" not in sys.modules: # pragma: no cover
raise RuntimeError(
"Serial client requires pyserial "
'Please install with "pip install pyserial" and try again.'
)
if framer not in [FramerType.ASCII, FramerType.RTU]:
raise TypeError("Only RTU/ASCII allowed.")
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,
self.comm_params,
trace_packet,
trace_pdu,
trace_connect,
)
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)
self.inter_byte_timeout: float = 0
self.silent_interval: float = 0
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) -> bool:
"""Check if socket exists."""
return self.socket is not None
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, addr: tuple | None = None) -> int:
"""Send data on the underlying socket."""
_ = addr
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)
self.last_frame_end = round(time.time(), 6)
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}>"
)