Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow configuring pyserial hardware RS485 settings #2460

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pymodbus/client/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class AsyncModbusSerialClient(ModbusBaseClient):
: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 rs485_settings: Allow configuring the underlying serial port for RS485 mode.
: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.
Expand Down Expand Up @@ -69,6 +70,7 @@ def __init__( # pylint: disable=too-many-arguments
bytesize: int = 8,
parity: str = "N",
stopbits: int = 1,
rs485_settings: serial.rs485.RS485Settings | None = None,
handle_local_echo: bool = False,
name: str = "comm",
reconnect_delay: float = 0.1,
Expand All @@ -92,6 +94,7 @@ def __init__( # pylint: disable=too-many-arguments
bytesize=bytesize,
parity=parity,
stopbits=stopbits,
rs485_settings=rs485_settings,
handle_local_echo=handle_local_echo,
comm_name=name,
reconnect_delay=reconnect_delay,
Expand Down Expand Up @@ -160,6 +163,7 @@ def __init__( # pylint: disable=too-many-arguments
bytesize: int = 8,
parity: str = "N",
stopbits: int = 1,
rs485_settings: serial.rs485.RS485Settings | None = None,
handle_local_echo: bool = False,
name: str = "comm",
reconnect_delay: float = 0.1,
Expand All @@ -182,6 +186,7 @@ def __init__( # pylint: disable=too-many-arguments
bytesize=bytesize,
parity=parity,
stopbits=stopbits,
rs485_settings=rs485_settings,
handle_local_echo=handle_local_echo,
comm_name=name,
reconnect_delay=reconnect_delay,
Expand Down
15 changes: 11 additions & 4 deletions pymodbus/transport/serialtransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class SerialTransport(asyncio.Transport):

force_poll: bool = os.name == "nt"

def __init__(self, loop, protocol, url, baudrate, bytesize, parity, stopbits, timeout) -> None:
def __init__(
self, loop, protocol, url, baudrate, bytesize, parity, stopbits, timeout, rs485_settings
) -> None:
"""Initialize."""
super().__init__()
if "serial" not in sys.modules:
Expand All @@ -26,9 +28,12 @@ def __init__(self, loop, protocol, url, baudrate, bytesize, parity, stopbits, ti
)
self.async_loop = loop
self.intern_protocol: asyncio.BaseProtocol = protocol
self.sync_serial = serial.serial_for_url(url, exclusive=True,
self.sync_serial = serial.serial_for_url(url, do_not_open=True, exclusive=True,
baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=timeout
)
)
if rs485_settings is not None:
self.sync_serial.rs485_mode = rs485_settings
self.sync_serial.open()
self.intern_write_buffer: list[bytes] = []
self.poll_task: asyncio.Task | None = None
self._poll_wait_time = 0.0005
Expand Down Expand Up @@ -168,6 +173,7 @@ async def create_serial_connection(
parity=None,
stopbits=None,
timeout=None,
rs485_settings=None
) -> tuple[asyncio.Transport, asyncio.BaseProtocol]:
"""Create a connection to a new serial port instance."""
protocol = protocol_factory()
Expand All @@ -176,6 +182,7 @@ async def create_serial_connection(
bytesize,
parity,
stopbits,
timeout)
timeout,
rs485_settings)
loop.call_soon(transport.setup)
return transport, protocol
10 changes: 9 additions & 1 deletion pymodbus/transport/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,16 @@
from contextlib import suppress
from enum import Enum
from functools import partial
from typing import Any
from typing import TYPE_CHECKING, Any

from pymodbus.logging import Log
from pymodbus.transport.serialtransport import create_serial_connection


if TYPE_CHECKING:
import serial


NULLMODEM_HOST = "__pymodbus_nullmodem"


Expand Down Expand Up @@ -98,6 +102,9 @@ class CommParams:
parity: str = ''
stopbits: int = -1

# RS485
rs485_settings: 'serial.rs485.RS485Settings' | None = None # noqa: UP037

@classmethod
def generate_ssl(
cls,
Expand Down Expand Up @@ -204,6 +211,7 @@ def init_setup_connect_listen(self, host: str, port: int) -> None:
parity=self.comm_params.parity,
stopbits=self.comm_params.stopbits,
timeout=self.comm_params.timeout_connect,
rs485_settings=self.comm_params.rs485_settings,
)
return
if self.comm_params.comm_type == CommType.UDP:
Expand Down
28 changes: 14 additions & 14 deletions test/transport/test_serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ class TestTransportSerial:

async def test_init(self):
"""Test null modem init."""
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)

async def test_loop(self):
"""Test asyncio abstract methods."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
assert comm.loop

@pytest.mark.parametrize("inx", range(0, 11))
async def test_abstract_methods(self, inx):
"""Test asyncio abstract methods."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
methods = [
partial(comm.get_protocol),
partial(comm.set_protocol, None),
Expand All @@ -52,7 +52,7 @@ async def test_abstract_methods(self, inx):
@pytest.mark.parametrize("inx", range(0, 4))
async def test_external_methods(self, inx):
"""Test external methods."""
comm = SerialTransport(mock.MagicMock(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(mock.MagicMock(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial.read = mock.MagicMock(return_value="abcd")
comm.sync_serial.write = mock.MagicMock(return_value=4)
comm.sync_serial.fileno = mock.MagicMock(return_value=2)
Expand Down Expand Up @@ -108,14 +108,14 @@ async def test_write_force_poll(self):

async def test_close(self):
"""Test close."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial = None
comm.close()

@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
async def test_polling(self):
"""Test polling."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial = mock.MagicMock()
comm.sync_serial.read.side_effect = asyncio.CancelledError("test")
with contextlib.suppress(asyncio.CancelledError):
Expand All @@ -124,15 +124,15 @@ async def test_polling(self):
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
async def test_poll_task(self):
"""Test polling."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial = mock.MagicMock()
comm.sync_serial.read.side_effect = serial.SerialException("test")
await comm.polling_task()

@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
async def test_poll_task2(self):
"""Test polling."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial = mock.MagicMock()
comm.sync_serial = mock.MagicMock()
comm.sync_serial.write.return_value = 4
Expand All @@ -144,7 +144,7 @@ async def test_poll_task2(self):
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
async def test_write_exception(self):
"""Test write exception."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial = mock.MagicMock()
comm.sync_serial.write.side_effect = BlockingIOError("test")
comm.intern_write_ready()
Expand All @@ -154,7 +154,7 @@ async def test_write_exception(self):
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
async def test_write_ok(self):
"""Test write exception."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial = mock.MagicMock()
comm.sync_serial.write.return_value = 4
comm.intern_write_buffer.append(b"abcd")
Expand All @@ -163,7 +163,7 @@ async def test_write_ok(self):
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
async def test_write_len(self):
"""Test write exception."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial = mock.MagicMock()
comm.sync_serial.write.return_value = 3
comm.async_loop.add_writer = mock.Mock()
Expand All @@ -173,7 +173,7 @@ async def test_write_len(self):
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
async def test_write_force(self):
"""Test write exception."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.poll_task = True
comm.sync_serial = mock.MagicMock()
comm.sync_serial.write.return_value = 3
Expand All @@ -183,7 +183,7 @@ async def test_write_force(self):
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
async def test_read_ready(self):
"""Test polling."""
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
comm.sync_serial = mock.MagicMock()
comm.intern_protocol = mock.Mock()
comm.sync_serial.read = mock.Mock()
Expand All @@ -199,4 +199,4 @@ async def test_import_pyserial(self):
with mock.patch.dict(sys.modules, {'no_modules': None}) as mock_modules:
del mock_modules['serial']
with pytest.raises(RuntimeError):
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)