Skip to content
Open
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
2 changes: 2 additions & 0 deletions API_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ API changes 3.12.0
------------------
- when using no_response_expected=, the call returns None
- remove idle_time() from sync client since it is void
- ModbusSerialServer new parameter "allow_multiple_devices"
which gives limited multipoint support with baudrate < 19200 and a good RS485 line.

API changes 3.11.0
------------------
Expand Down
2 changes: 1 addition & 1 deletion doc/source/server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ communication in 2 versions:
synchronous servers are just an interface layer allowing synchronous
applications to use the server as if it was synchronous.

*Warning* The current framer implementation does not support running the server on a shared rs485 line (multipoint).
*Warning* The current framer implementation offer only limited support for running the server on a shared rs485 line (multipoint).

.. automodule:: pymodbus.server
:members:
Expand Down
13 changes: 13 additions & 0 deletions pymodbus/framer/rtu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

from ..logging import Log
from ..pdu import DecodePDU
from .base import FramerBase


Expand Down Expand Up @@ -80,6 +81,7 @@ class FramerRTU(FramerBase):

MIN_SIZE = 4 # <device id><function code><crc 2 bytes>


@classmethod
def generate_crc16_table(cls) -> list[int]:
"""Generate a crc16 lookup table.
Expand All @@ -99,6 +101,17 @@ def generate_crc16_table(cls) -> list[int]:
return result
crc16_table: list[int] = [0]

def __init__(
self,
decoder: DecodePDU,
bz_bps: tuple[int, int, int] | None = None
) -> None:
"""Initialize a RTU ADU (framer) instance."""
super().__init__(decoder)
self.inter_msg_time = 0.0
if bz_bps:
# time to transmit 1.5Char + 10% with stop bits etc.
self.inter_msg_time = 1.5 * 1.1 * (float(1 + bz_bps[0] + bz_bps[1]) / bz_bps[2])

def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
"""Decode ADU."""
Expand Down
9 changes: 7 additions & 2 deletions pymodbus/server/requesthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ..constants import ExcCodes
from ..exceptions import ModbusIOException, NoSuchIdException
from ..framer import FramerRTU
from ..logging import Log
from ..pdu import ExceptionResponse
from ..transaction import TransactionManager
Expand All @@ -28,11 +29,15 @@ def __init__(self, owner, trace_packet, trace_pdu, trace_connect):
handle_local_echo=owner.comm_params.handle_local_echo,
)
self.server = owner
self.framer = self.server.framer(self.server.decoder)
self.running = False
if owner.comm_params.allow_multiple_devices:
t0 = (owner.comm_params.bytesize, owner.comm_params.stopbits, owner.comm_params.baudrate)
framer = FramerRTU(owner.decoder, bz_bps=t0)
else:
framer = owner.framer(owner.decoder)
super().__init__(
params,
self.framer,
framer,
0,
True,
trace_packet,
Expand Down
14 changes: 12 additions & 2 deletions pymodbus/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,16 @@ def __init__(
:param trace_pdu: Called with PDU received/to be sent
:param trace_connect: Called when connected/disconnected
:param custom_pdu: list of ModbusPDU custom classes
:param allow_multiple_devices: True if the rs485 have multiple devices connected.
**Remark** only works with baudrates < 19.200 and with an error free RS485.
"""
allow_multiple_devices = kwargs.get("allow_multiple_devices", False)
baudrate = kwargs.get("baudrate", 19200) >= 19200
if allow_multiple_devices:
if baudrate >= 19200:
raise TypeError("allow_multiple_devices only allowed with baudrate < 19200")
if framer != FramerType.RTU:
raise TypeError("allow_multiple_devices only allowed with FramerType.RTU")
params = CommParams(
comm_type=CommType.SERIAL,
comm_name="server_listener",
Expand All @@ -260,9 +269,10 @@ def __init__(
source_address=(kwargs.get("port", 0), 0),
bytesize=kwargs.get("bytesize", 8),
parity=kwargs.get("parity", "N"),
baudrate=kwargs.get("baudrate", 19200),
baudrate=baudrate,
stopbits=kwargs.get("stopbits", 1),
handle_local_echo=kwargs.get("handle_local_echo", False)
handle_local_echo=kwargs.get("handle_local_echo", False),
allow_multiple_devices=allow_multiple_devices
)
super().__init__(
params,
Expand Down
3 changes: 2 additions & 1 deletion pymodbus/transport/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ class CommParams:
host: str = "localhost" # On some machines this will now be ::1
port: int = 0
source_address: tuple[str, int] | None = None
handle_local_echo: bool = False

# tls
sslctx: ssl.SSLContext | None = None
Expand All @@ -98,6 +97,8 @@ class CommParams:
bytesize: int = -1
parity: str = ''
stopbits: int = -1
handle_local_echo: bool = False
allow_multiple_devices: bool = False # Only for server!

@classmethod
def generate_ssl(
Expand Down
10 changes: 7 additions & 3 deletions test/client/test_client_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def test_basic_syn_udp_client(self):
# receive/send
client = ModbusUdpClient("127.0.0.1")
client.socket = mockSocket()
assert not client.send(None)
assert not client.send(b'')
assert client.send(b"\x50") == 1
assert client.recv(1) == b"\x50"

Expand All @@ -44,7 +44,7 @@ def test_basic_syn_udp_client(self):
client.close()

# already closed socket
client.socket = False
client.socket = None
client.close()

assert str(client) == "ModbusUdpClient 127.0.0.1:502"
Expand Down Expand Up @@ -281,6 +281,10 @@ def test_sync_serial_client_instantiation(self):
ModbusSerialClient("/dev/null", framer=FramerType.RTU).framer,
FramerRTU,
)
assert isinstance(
ModbusSerialClient("/dev/null", baudrate=38400, framer=FramerType.RTU).framer,
FramerRTU,
)

@mock.patch("serial.Serial")
def test_basic_sync_serial_client(self, mock_serial):
Expand All @@ -293,7 +297,7 @@ def test_basic_sync_serial_client(self, mock_serial):
client = ModbusSerialClient("/dev/null")
client.socket = mock_serial
client.state = 0
assert not client.send(None)
assert not client.send(b'')
client.state = 0
assert client.send(b"\x00") == 1
assert client.recv(1) == b"\x00"
Expand Down
3 changes: 3 additions & 0 deletions test/framer/test_framer.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ async def test_handleFrame2(self):
assert used_len == len(msg)
assert pdu

async def test_bz_bps(self):
"""Test bz_bps=."""
assert FramerRTU(DecodePDU(True), bz_bps = (8, 2, 9600))

class TestFramerType:
"""Test classes."""
Expand Down
5 changes: 3 additions & 2 deletions test/server/test_requesthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ModbusServerContext,
)
from pymodbus.exceptions import ModbusIOException, NoSuchIdException
from pymodbus.framer import FramerType
from pymodbus.pdu import ExceptionResponse
from pymodbus.server import ModbusBaseServer
from pymodbus.transport import CommParams, CommType
Expand All @@ -33,13 +34,13 @@ async def requesthandler(self):
reconnect_delay=0.0,
reconnect_delay_max=0.0,
timeout_connect=0.0,
source_address=(0, 0),
source_address=("0", 0),
),
ModbusServerContext(devices=store, single=True),
False,
False,
None,
"socket",
FramerType.SOCKET,
None,
None,
None,
Expand Down