Skip to content

Commit bda50b7

Browse files
committed
Allow configuring pyserial hardware RS485 settings
1 parent 430529e commit bda50b7

File tree

4 files changed

+40
-18
lines changed

4 files changed

+40
-18
lines changed

pymodbus/client/serial.py

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class AsyncModbusSerialClient(ModbusBaseClient):
3434
:param parity: 'E'ven, 'O'dd or 'N'one
3535
:param stopbits: Number of stop bits 1, 1.5, 2.
3636
:param handle_local_echo: Discard local echo from dongle.
37+
:param rs485_settings: Allow configuring the underlying serial port for RS485 mode.
3738
:param name: Set communication name, used in logging
3839
:param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting.
3940
:param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting.
@@ -69,6 +70,7 @@ def __init__( # pylint: disable=too-many-arguments
6970
bytesize: int = 8,
7071
parity: str = "N",
7172
stopbits: int = 1,
73+
rs485_settings: serial.rs485.RS485Settings | None = None,
7274
handle_local_echo: bool = False,
7375
name: str = "comm",
7476
reconnect_delay: float = 0.1,
@@ -92,6 +94,7 @@ def __init__( # pylint: disable=too-many-arguments
9294
bytesize=bytesize,
9395
parity=parity,
9496
stopbits=stopbits,
97+
rs485_settings=rs485_settings,
9598
handle_local_echo=handle_local_echo,
9699
comm_name=name,
97100
reconnect_delay=reconnect_delay,
@@ -160,6 +163,7 @@ def __init__( # pylint: disable=too-many-arguments
160163
bytesize: int = 8,
161164
parity: str = "N",
162165
stopbits: int = 1,
166+
rs485_settings: serial.rs485.RS485Settings | None = None,
163167
handle_local_echo: bool = False,
164168
name: str = "comm",
165169
reconnect_delay: float = 0.1,
@@ -182,6 +186,7 @@ def __init__( # pylint: disable=too-many-arguments
182186
bytesize=bytesize,
183187
parity=parity,
184188
stopbits=stopbits,
189+
rs485_settings=rs485_settings,
185190
handle_local_echo=handle_local_echo,
186191
comm_name=name,
187192
reconnect_delay=reconnect_delay,

pymodbus/transport/serialtransport.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ class SerialTransport(asyncio.Transport):
1616

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

19-
def __init__(self, loop, protocol, url, baudrate, bytesize, parity, stopbits, timeout) -> None:
19+
def __init__(
20+
self, loop, protocol, url, baudrate, bytesize, parity, stopbits, timeout, rs485_settings
21+
) -> None:
2022
"""Initialize."""
2123
super().__init__()
2224
if "serial" not in sys.modules:
@@ -26,9 +28,12 @@ def __init__(self, loop, protocol, url, baudrate, bytesize, parity, stopbits, ti
2628
)
2729
self.async_loop = loop
2830
self.intern_protocol: asyncio.BaseProtocol = protocol
29-
self.sync_serial = serial.serial_for_url(url, exclusive=True,
31+
self.sync_serial = serial.serial_for_url(url, do_not_open=True, exclusive=True,
3032
baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=timeout
31-
)
33+
)
34+
if rs485_settings is not None:
35+
self.sync_serial.rs485_mode = rs485_settings
36+
self.sync_serial.open()
3237
self.intern_write_buffer: list[bytes] = []
3338
self.poll_task: asyncio.Task | None = None
3439
self._poll_wait_time = 0.0005
@@ -168,6 +173,7 @@ async def create_serial_connection(
168173
parity=None,
169174
stopbits=None,
170175
timeout=None,
176+
rs485_settings=None
171177
) -> tuple[asyncio.Transport, asyncio.BaseProtocol]:
172178
"""Create a connection to a new serial port instance."""
173179
protocol = protocol_factory()
@@ -176,6 +182,7 @@ async def create_serial_connection(
176182
bytesize,
177183
parity,
178184
stopbits,
179-
timeout)
185+
timeout,
186+
rs485_settings)
180187
loop.call_soon(transport.setup)
181188
return transport, protocol

pymodbus/transport/transport.py

+10
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@
5858
from functools import partial
5959
from typing import Any
6060

61+
62+
try:
63+
from serial.rs485 import RS485Settings
64+
except ImportError:
65+
RS485Settings = None
66+
6167
from pymodbus.logging import Log
6268
from pymodbus.transport.serialtransport import create_serial_connection
6369

@@ -98,6 +104,9 @@ class CommParams:
98104
parity: str = ''
99105
stopbits: int = -1
100106

107+
# RS485
108+
rs485_settings: RS485Settings | None = None
109+
101110
@classmethod
102111
def generate_ssl(
103112
cls,
@@ -204,6 +213,7 @@ def init_setup_connect_listen(self, host: str, port: int) -> None:
204213
parity=self.comm_params.parity,
205214
stopbits=self.comm_params.stopbits,
206215
timeout=self.comm_params.timeout_connect,
216+
rs485_settings=self.comm_params.rs485_settings,
207217
)
208218
return
209219
if self.comm_params.comm_type == CommType.UDP:

test/transport/test_serial.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@ class TestTransportSerial:
2323

2424
async def test_init(self):
2525
"""Test null modem init."""
26-
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
26+
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
2727

2828
async def test_loop(self):
2929
"""Test asyncio abstract methods."""
30-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
30+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
3131
assert comm.loop
3232

3333
@pytest.mark.parametrize("inx", range(0, 11))
3434
async def test_abstract_methods(self, inx):
3535
"""Test asyncio abstract methods."""
36-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
36+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
3737
methods = [
3838
partial(comm.get_protocol),
3939
partial(comm.set_protocol, None),
@@ -52,7 +52,7 @@ async def test_abstract_methods(self, inx):
5252
@pytest.mark.parametrize("inx", range(0, 4))
5353
async def test_external_methods(self, inx):
5454
"""Test external methods."""
55-
comm = SerialTransport(mock.MagicMock(), mock.Mock(), "dummy", None, None, None, None, None)
55+
comm = SerialTransport(mock.MagicMock(), mock.Mock(), "dummy", None, None, None, None, None, None)
5656
comm.sync_serial.read = mock.MagicMock(return_value="abcd")
5757
comm.sync_serial.write = mock.MagicMock(return_value=4)
5858
comm.sync_serial.fileno = mock.MagicMock(return_value=2)
@@ -108,14 +108,14 @@ async def test_write_force_poll(self):
108108

109109
async def test_close(self):
110110
"""Test close."""
111-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
111+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
112112
comm.sync_serial = None
113113
comm.close()
114114

115115
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
116116
async def test_polling(self):
117117
"""Test polling."""
118-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
118+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
119119
comm.sync_serial = mock.MagicMock()
120120
comm.sync_serial.read.side_effect = asyncio.CancelledError("test")
121121
with contextlib.suppress(asyncio.CancelledError):
@@ -124,15 +124,15 @@ async def test_polling(self):
124124
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
125125
async def test_poll_task(self):
126126
"""Test polling."""
127-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
127+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
128128
comm.sync_serial = mock.MagicMock()
129129
comm.sync_serial.read.side_effect = serial.SerialException("test")
130130
await comm.polling_task()
131131

132132
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
133133
async def test_poll_task2(self):
134134
"""Test polling."""
135-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
135+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
136136
comm.sync_serial = mock.MagicMock()
137137
comm.sync_serial = mock.MagicMock()
138138
comm.sync_serial.write.return_value = 4
@@ -144,7 +144,7 @@ async def test_poll_task2(self):
144144
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
145145
async def test_write_exception(self):
146146
"""Test write exception."""
147-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
147+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
148148
comm.sync_serial = mock.MagicMock()
149149
comm.sync_serial.write.side_effect = BlockingIOError("test")
150150
comm.intern_write_ready()
@@ -154,7 +154,7 @@ async def test_write_exception(self):
154154
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
155155
async def test_write_ok(self):
156156
"""Test write exception."""
157-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
157+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
158158
comm.sync_serial = mock.MagicMock()
159159
comm.sync_serial.write.return_value = 4
160160
comm.intern_write_buffer.append(b"abcd")
@@ -163,7 +163,7 @@ async def test_write_ok(self):
163163
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
164164
async def test_write_len(self):
165165
"""Test write exception."""
166-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
166+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
167167
comm.sync_serial = mock.MagicMock()
168168
comm.sync_serial.write.return_value = 3
169169
comm.async_loop.add_writer = mock.Mock()
@@ -173,7 +173,7 @@ async def test_write_len(self):
173173
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
174174
async def test_write_force(self):
175175
"""Test write exception."""
176-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
176+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
177177
comm.poll_task = True
178178
comm.sync_serial = mock.MagicMock()
179179
comm.sync_serial.write.return_value = 3
@@ -183,7 +183,7 @@ async def test_write_force(self):
183183
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported")
184184
async def test_read_ready(self):
185185
"""Test polling."""
186-
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
186+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)
187187
comm.sync_serial = mock.MagicMock()
188188
comm.intern_protocol = mock.Mock()
189189
comm.sync_serial.read = mock.Mock()
@@ -199,4 +199,4 @@ async def test_import_pyserial(self):
199199
with mock.patch.dict(sys.modules, {'no_modules': None}) as mock_modules:
200200
del mock_modules['serial']
201201
with pytest.raises(RuntimeError):
202-
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None)
202+
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None, None)

0 commit comments

Comments
 (0)