diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index aa5499cdd..f18ec3350 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -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. @@ -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, @@ -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, @@ -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, @@ -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, diff --git a/pymodbus/transport/serialtransport.py b/pymodbus/transport/serialtransport.py index 11759c2ac..c102014bf 100644 --- a/pymodbus/transport/serialtransport.py +++ b/pymodbus/transport/serialtransport.py @@ -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: @@ -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 @@ -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() @@ -176,6 +182,7 @@ async def create_serial_connection( bytesize, parity, stopbits, - timeout) + timeout, + rs485_settings) loop.call_soon(transport.setup) return transport, protocol diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index 36750a65a..d2922b622 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -58,6 +58,12 @@ from functools import partial from typing import Any + +try: + from serial.rs485 import RS485Settings +except ImportError: + RS485Settings = None + from pymodbus.logging import Log from pymodbus.transport.serialtransport import create_serial_connection @@ -98,6 +104,9 @@ class CommParams: parity: str = '' stopbits: int = -1 + # RS485 + rs485_settings: RS485Settings | None = None + @classmethod def generate_ssl( cls, @@ -204,6 +213,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: diff --git a/test/transport/test_serial.py b/test/transport/test_serial.py index acf73c9cd..dd55c3597 100644 --- a/test/transport/test_serial.py +++ b/test/transport/test_serial.py @@ -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), @@ -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) @@ -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): @@ -124,7 +124,7 @@ 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() @@ -132,7 +132,7 @@ async def test_poll_task(self): @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 @@ -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() @@ -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") @@ -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() @@ -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 @@ -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() @@ -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)