-
Notifications
You must be signed in to change notification settings - Fork 993
Update use pyserial RS485 #2205
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
Changes from all commits
33fac59
d85f1e9
7010a32
2e3e3b4
36a11fb
eb7bfa0
1c38542
bc598a0
0893839
577848a
f7381e3
0dbf835
a90488c
61007d4
1b6cd21
0ee5b75
69622df
13868fa
feb4da1
7209eb0
e6eec1a
f2a8215
11d7899
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
|
||
try: | ||
import serial | ||
from serial.rs485 import RS485Settings | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is already imported in the line above, do not import twice. |
||
|
||
PYSERIAL_MISSING = False | ||
except ImportError: | ||
|
@@ -37,6 +38,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. | ||
|
||
Common optional parameters: | ||
|
||
|
@@ -73,6 +75,7 @@ def __init__( | |
bytesize: int = 8, | ||
parity: str = "N", | ||
stopbits: int = 1, | ||
rs485_settings: RS485Settings | None = None, | ||
**kwargs: Any, | ||
) -> None: | ||
"""Initialize Asyncio Modbus Serial Client.""" | ||
|
@@ -90,6 +93,7 @@ def __init__( | |
bytesize=bytesize, | ||
parity=parity, | ||
stopbits=stopbits, | ||
rs485_settings=rs485_settings, | ||
**kwargs, | ||
) | ||
|
||
|
@@ -155,6 +159,7 @@ def __init__( | |
bytesize: int = 8, | ||
parity: str = "N", | ||
stopbits: int = 1, | ||
rs485_settings: RS485Settings | None = None, | ||
strict: bool = True, | ||
**kwargs: Any, | ||
) -> None: | ||
|
@@ -167,6 +172,7 @@ def __init__( | |
bytesize=bytesize, | ||
parity=parity, | ||
stopbits=stopbits, | ||
rs485_settings=rs485_settings, | ||
**kwargs, | ||
) | ||
self.socket: serial.Serial | None = None | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,25 +8,36 @@ | |
|
||
with contextlib.suppress(ImportError): | ||
import serial | ||
import serial.rs485 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is already imported in the line above. |
||
|
||
|
||
class SerialTransport(asyncio.Transport): | ||
"""An asyncio serial transport.""" | ||
|
||
force_poll: bool = os.name == "nt" | ||
|
||
def __init__(self, loop, protocol, *args, **kwargs) -> None: | ||
def __init__(self, loop, protocol, rs485_settings, *args, **kwargs) -> None: | ||
"""Initialize.""" | ||
super().__init__() | ||
self.async_loop = loop | ||
self.intern_protocol: asyncio.BaseProtocol = protocol | ||
self.sync_serial = serial.serial_for_url(*args, **kwargs) | ||
self.sync_serial = self._serial_for_args(rs485_settings, *args, **kwargs) | ||
self.intern_write_buffer: list[bytes] = [] | ||
self.poll_task: asyncio.Task | None = None | ||
self._poll_wait_time = 0.0005 | ||
self.sync_serial.timeout = 0 | ||
self.sync_serial.write_timeout = 0 | ||
|
||
def _serial_for_args(self, rs485_settings, *args, **kwargs) -> serial.Serial: | ||
sync_serial: serial.Serial | ||
if rs485_settings is not None: | ||
samskiter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sync_serial = serial.rs485.RS485(*args, **kwargs) | ||
sync_serial.rs485_mode = rs485_settings | ||
else: | ||
sync_serial = serial.serial_for_url(*args, **kwargs) | ||
|
||
return sync_serial | ||
|
||
def setup(self) -> None: | ||
"""Prepare to read/write.""" | ||
if self.force_poll: | ||
|
@@ -154,10 +165,10 @@ async def polling_task(self): | |
self.intern_read_ready() | ||
|
||
async def create_serial_connection( | ||
loop, protocol_factory, *args, **kwargs | ||
loop, protocol_factory, rs485_settings, *args, **kwargs | ||
) -> tuple[asyncio.Transport, asyncio.BaseProtocol]: | ||
"""Create a connection to a new serial port instance.""" | ||
protocol = protocol_factory() | ||
transport = SerialTransport(loop, protocol, *args, **kwargs) | ||
transport = SerialTransport(loop, protocol, rs485_settings, *args, **kwargs) | ||
loop.call_soon(transport.setup) | ||
return transport, protocol |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,6 +58,8 @@ | |
from functools import partial | ||
from typing import Any | ||
|
||
from serial.rs485 import RS485Settings | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if serial is not installed ?? Please import as serial is imported in other places. |
||
|
||
from pymodbus.logging import Log | ||
from pymodbus.transport.serialtransport import create_serial_connection | ||
|
||
|
@@ -98,6 +100,9 @@ class CommParams: | |
parity: str = '' | ||
stopbits: int = -1 | ||
|
||
# RS485 | ||
rs485_settings: RS485Settings | None = None | ||
samskiter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@classmethod | ||
def generate_ssl( | ||
cls, | ||
|
@@ -147,7 +152,6 @@ def __init__( | |
self.comm_params = params.copy() | ||
self.is_server = is_server | ||
self.is_closing = False | ||
|
||
self.transport: asyncio.BaseTransport = None # type: ignore[assignment] | ||
self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() | ||
self.recv_buffer: bytes = b"" | ||
|
@@ -188,14 +192,16 @@ def __init__( | |
self.comm_params.comm_type = CommType.TCP | ||
parts = host.split(":") | ||
host, port = parts[1][2:], int(parts[2]) | ||
self.init_setup_connect_listen(host, port) | ||
|
||
def init_setup_connect_listen(self, host: str, port: int) -> None: | ||
self.init_setup_connect_listen(host, port, self.comm_params.rs485_settings) | ||
|
||
def init_setup_connect_listen(self, host: str, port: int, rs485_settings: RS485Settings | None) -> None: | ||
"""Handle connect/listen handler.""" | ||
if self.comm_params.comm_type == CommType.SERIAL: | ||
self.call_create = partial(create_serial_connection, | ||
self.loop, | ||
self.handle_new_connection, | ||
rs485_settings, | ||
host, | ||
baudrate=self.comm_params.baudrate, | ||
bytesize=self.comm_params.bytesize, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
|
||
import pytest | ||
import serial | ||
from serial.rs485 import RS485Settings | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already imported in the line above, please do not import twice. |
||
|
||
from pymodbus.transport.serialtransport import ( | ||
SerialTransport, | ||
|
@@ -17,22 +18,25 @@ | |
@mock.patch( | ||
"pymodbus.transport.serialtransport.serial.serial_for_url", mock.MagicMock() | ||
) | ||
@mock.patch( | ||
"pymodbus.transport.serialtransport.serial.rs485.RS485", mock.MagicMock() | ||
) | ||
class TestTransportSerial: | ||
"""Test transport serial module.""" | ||
|
||
async def test_init(self): | ||
"""Test null modem init.""" | ||
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") | ||
SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
|
||
async def test_loop(self): | ||
"""Test asyncio abstract methods.""" | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
methods = [ | ||
partial(comm.get_protocol), | ||
partial(comm.set_protocol, None), | ||
|
@@ -51,14 +55,14 @@ 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") | ||
comm = SerialTransport(mock.MagicMock(), mock.Mock(), None, "dummy") | ||
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) | ||
comm.sync_serial.async_loop.add_writer = mock.MagicMock() | ||
comm.sync_serial.async_loop.add_reader = mock.MagicMock() | ||
comm.sync_serial.async_loop.remove_writer = mock.MagicMock() | ||
comm.sync_serial.async_loop.remove_reader = mock.MagicMock() | ||
comm.async_loop.add_writer = mock.MagicMock() | ||
samskiter marked this conversation as resolved.
Show resolved
Hide resolved
samskiter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
comm.async_loop.add_reader = mock.MagicMock() | ||
comm.async_loop.remove_writer = mock.MagicMock() | ||
comm.async_loop.remove_reader = mock.MagicMock() | ||
comm.sync_serial.in_waiting = False | ||
|
||
methods = [ | ||
|
@@ -73,17 +77,30 @@ async def test_external_methods(self, inx): | |
async def test_create_serial(self): | ||
"""Test external methods.""" | ||
transport, protocol = await create_serial_connection( | ||
asyncio.get_running_loop(), mock.Mock, url="dummy" | ||
asyncio.get_running_loop(), mock.Mock, None, url="dummy" | ||
) | ||
assert transport | ||
assert protocol | ||
transport.close() | ||
|
||
@pytest.mark.skipif(os.name == "nt", reason="Windows not supported") | ||
async def test_create_rs485_serial(self): | ||
"""Test external methods.""" | ||
settings = RS485Settings(rts_level_for_rx=True, rts_level_for_tx=True, delay_before_rx=0.4) | ||
transport, protocol = await create_serial_connection( | ||
asyncio.get_running_loop(), mock.Mock, settings, url="dummy" | ||
) | ||
assert transport | ||
assert protocol | ||
assert transport.sync_serial.rs485_mode | ||
assert transport.sync_serial.rs485_mode == settings | ||
transport.close() | ||
|
||
async def test_force_poll(self): | ||
"""Test external methods.""" | ||
SerialTransport.force_poll = True | ||
transport, protocol = await create_serial_connection( | ||
asyncio.get_running_loop(), mock.Mock, url="dummy" | ||
asyncio.get_running_loop(), mock.Mock, None, url="dummy" | ||
) | ||
await asyncio.sleep(0) | ||
assert transport | ||
|
@@ -96,7 +113,7 @@ async def test_write_force_poll(self): | |
"""Test write with poll.""" | ||
SerialTransport.force_poll = True | ||
transport, protocol = await create_serial_connection( | ||
asyncio.get_running_loop(), mock.Mock, url="dummy" | ||
asyncio.get_running_loop(), mock.Mock, None, url="dummy" | ||
samskiter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
await asyncio.sleep(0) | ||
transport.write(b"abcd") | ||
|
@@ -106,14 +123,14 @@ async def test_write_force_poll(self): | |
|
||
async def test_close(self): | ||
"""Test close.""" | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
comm.sync_serial = mock.MagicMock() | ||
comm.sync_serial.read.side_effect = asyncio.CancelledError("test") | ||
with contextlib.suppress(asyncio.CancelledError): | ||
|
@@ -122,15 +139,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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
comm.sync_serial = mock.MagicMock() | ||
comm.sync_serial = mock.MagicMock() | ||
comm.sync_serial.write.return_value = 4 | ||
|
@@ -142,7 +159,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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
comm.sync_serial = mock.MagicMock() | ||
comm.sync_serial.write.side_effect = BlockingIOError("test") | ||
comm.intern_write_ready() | ||
|
@@ -152,7 +169,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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
comm.sync_serial = mock.MagicMock() | ||
comm.sync_serial.write.return_value = 4 | ||
comm.intern_write_buffer.append(b"abcd") | ||
|
@@ -161,7 +178,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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
comm.sync_serial = mock.MagicMock() | ||
comm.sync_serial.write.return_value = 3 | ||
comm.async_loop.add_writer = mock.Mock() | ||
|
@@ -171,7 +188,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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
comm.poll_task = True | ||
comm.sync_serial = mock.MagicMock() | ||
comm.sync_serial.write.return_value = 3 | ||
|
@@ -181,7 +198,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") | ||
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), None, "dummy") | ||
comm.sync_serial = mock.MagicMock() | ||
comm.intern_protocol = mock.Mock() | ||
comm.sync_serial.read = mock.Mock() | ||
|
Uh oh!
There was an error while loading. Please reload this page.