Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
janiversen committed Jan 9, 2024
2 parents 799e8df + df5d192 commit e3e8537
Show file tree
Hide file tree
Showing 18 changed files with 144 additions and 62 deletions.
4 changes: 2 additions & 2 deletions API_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ API changes
Versions (X.Y.Z) where Z > 0 e.g. 3.0.1 do NOT have API changes!


API changes 3.6.0 (future)
--------------------------
API changes 3.6.0
-----------------
- framer= is an enum: pymodbus.Framer, but still accept a framer class


Expand Down
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Thanks to
- Alexandre CUER
- Alois Hockenschlohe
- Arjan
- André Srinivasan
- banana-sun
- Blaise Thompson
- cgernert
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ helps make pymodbus a better product.

:ref:`Authors`: contains a complete list of volunteers have contributed to each major version.

Version 3.6.3
-------------
* solve Socket_framer problem with Exception response (#1925)
* Allow socket frames to be split in multiple packets (#1923)
* Reset frame for serial connections.
* Source address None not 0.0.0.0 for IPv6
* Missing Copyright in License file
* Correct wrong url to modbus protocol spec.
* Fix serial port in TestComm.

Version 3.6.2
-------------
* Set documentation to v3.6.2.
Expand Down
2 changes: 2 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Copyright 2008-2023 Pymodbus

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Expand Down
7 changes: 5 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PyModbus - A Python Modbus Stack

Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API a well as simulators.

Current release is `3.6.2 <https://github.com/pymodbus-dev/pymodbus/releases/tag/v3.6.2>`_.
Current release is `3.6.3 <https://github.com/pymodbus-dev/pymodbus/releases/tag/v3.6.3>`_.

Bleeding edge (not released) is `dev <https://github.com/pymodbus-dev/pymodbus/tree/dev>`_.

Expand All @@ -34,7 +34,7 @@ Pymodbus consist of 5 parts:

Common features
^^^^^^^^^^^^^^^
* Full `modbus standard protocol <_static/Modbus_Application_Protocol_V1_1b3.pdf>`_ implementation
* Full modbus standard protocol implementation
* Support for custom function codes
* support serial (rs-485), tcp, tls and udp communication
* support all standard frames: socket, rtu, rtu-over-tcp, tcp and ascii
Expand All @@ -45,6 +45,9 @@ Common features
* automatically tested on Windows, Linux and MacOS combined with python 3.8 - 3.12
* strongly typed API (py.typed present)

The modbus protocol specification: Modbus_Application_Protocol_V1_1b3.pdf can be found on
`modbus org <https://modbus.org>`_


Client Features
^^^^^^^^^^^^^^^
Expand Down
Binary file not shown.
Binary file modified doc/source/_static/examples.tgz
Binary file not shown.
Binary file modified doc/source/_static/examples.zip
Binary file not shown.
2 changes: 1 addition & 1 deletion pymodbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
from pymodbus.pdu import ExceptionResponse


__version__ = "3.6.2"
__version__ = "3.6.3"
__version_full__ = f"[pymodbus, version {__version__}]"
4 changes: 2 additions & 2 deletions pymodbus/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(
CommParams(
comm_type=kwargs.get("CommType"),
comm_name="comm",
source_address=kwargs.get("source_address", ("0.0.0.0", 0)),
source_address=kwargs.get("source_address", None),
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
Expand Down Expand Up @@ -358,7 +358,7 @@ def __init__( # pylint: disable=too-many-arguments
CommParams(
comm_type=kwargs.get("CommType"),
comm_name="comm",
source_address=kwargs.get("source_address", ("0.0.0.0", 0)),
source_address=kwargs.get("source_address", None),
reconnect_delay=reconnect_delay,
reconnect_delay_max=reconnect_delay_max,
timeout_connect=timeout,
Expand Down
1 change: 1 addition & 0 deletions pymodbus/framer/rtu_framer.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ def sendPacket(self, message):
:param message: Message to be sent over the bus
:return:
"""
super().resetFrame()
start = time.time()
timeout = start + self.client.comm_params.timeout_connect
while self.client.state != ModbusTransactionState.IDLE:
Expand Down
57 changes: 25 additions & 32 deletions pymodbus/framer/socket_framer.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,21 @@ def checkFrame(self):
Return true if we were successful.
"""
if self.isFrameReady():
(
self._header["tid"],
self._header["pid"],
self._header["len"],
self._header["uid"],
) = struct.unpack(">HHHB", self._buffer[0 : self._hsize])

# someone sent us an error? ignore it
if self._header["len"] < 2:
self.advanceFrame()
# we have at least a complete message, continue
elif len(self._buffer) - self._hsize + 1 >= self._header["len"]:
return True
if not self.isFrameReady():
return False
(
self._header["tid"],
self._header["pid"],
self._header["len"],
self._header["uid"],
) = struct.unpack(">HHHB", self._buffer[0 : self._hsize])

# someone sent us an error? ignore it
if self._header["len"] < 2:
self.advanceFrame()
# we have at least a complete message, continue
elif len(self._buffer) - self._hsize + 1 >= self._header["len"]:
return True
# we don't have enough of a message yet, wait
return False

Expand Down Expand Up @@ -95,7 +96,7 @@ def getFrame(self):
:returns: The next full frame buffer
"""
length = self._hsize + self._header["len"] - 1
length = self._hsize + self._header["len"]
return self._buffer[self._hsize : length]

# ----------------------------------------------------------------------- #
Expand Down Expand Up @@ -128,23 +129,15 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs
The processed and decoded messages are pushed to the callback
function to process and send.
"""
while True:
if not self.isFrameReady():
if len(self._buffer):
# Possible error ???
if self._header["len"] < 2:
self._process(callback, tid, error=True)
break
if not self.checkFrame():
Log.debug("Frame check failed, ignoring!!")
self.resetFrame()
continue
if not self._validate_slave_id(slave, single):
header_txt = self._header["uid"]
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
self.resetFrame()
continue
self._process(callback, tid)
if not self.checkFrame():
Log.debug("Frame check failed, ignoring!!")
return
if not self._validate_slave_id(slave, single):
header_txt = self._header["uid"]
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
self.resetFrame()
return
self._process(callback, tid)

def _process(self, callback, tid, error=False):
"""Process incoming packets irrespective error condition."""
Expand Down
14 changes: 10 additions & 4 deletions pymodbus/transport/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ class CommParams:
reconnect_delay: float | None = None
reconnect_delay_max: float = 0.0
timeout_connect: float | None = None
host: str = "127.0.0.1"
host: str = "localhost" # On some machines this will now be ::1
port: int = 0
source_address: tuple[str, int] = ("0.0.0.0", 0)
source_address: tuple[str, int] | None = None
handle_local_echo: bool = False

# tls
Expand Down Expand Up @@ -171,8 +171,14 @@ def __init__(

# ModbusProtocol specific setup
if self.is_server:
host = self.comm_params.source_address[0]
port = int(self.comm_params.source_address[1])
if self.comm_params.source_address is not None:
host = self.comm_params.source_address[0]
port = int(self.comm_params.source_address[1])
else:
# This behaviour isn't quite right.
# It listens on any IPv4 address rather than the more natural default of any address (v6 or v4).
host = "0.0.0.0" # Any IPv4 host
port = 0 # Server will select an ephemeral port for itself
else:
host = self.comm_params.host
port = int(self.comm_params.port)
Expand Down
3 changes: 2 additions & 1 deletion pymodbus/transport/transport_serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def __init__(self, loop, protocol, *args, **kwargs) -> None:
self._poll_wait_time = 0.0005
self.sync_serial.timeout = 0
self.sync_serial.write_timeout = 0
self.future: asyncio.Task | None = None

def setup(self):
"""Prepare to read/write."""
Expand All @@ -46,7 +47,7 @@ def close(self, exc: Exception | None = None) -> None:
self.flush()
if self.poll_task:
self.poll_task.cancel()
_ = asyncio.ensure_future(self.poll_task)
self.future = asyncio.ensure_future(self.poll_task)
self.poll_task = None
else:
self.async_loop.remove_reader(self.sync_serial.fileno())
Expand Down
2 changes: 1 addition & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def pytest_configure():
BASE_PORTS = {
"TestBasicModbusProtocol": 7100,
"TestBasicSerial": 7200,
"TestCommModbusProtocol": 7300,
"TestCommModbusProtocol": 7305,
"TestCommNullModem": 7400,
"TestExamples": 7500,
"TestAsyncExamples": 7600,
Expand Down
9 changes: 8 additions & 1 deletion test/sub_client/test_client_faulty_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from pymodbus.exceptions import ModbusIOException
from pymodbus.factory import ClientDecoder
from pymodbus.framer import ModbusSocketFramer
from pymodbus.framer import ModbusRtuFramer, ModbusSocketFramer


class TestFaultyResponses:
Expand All @@ -30,6 +30,13 @@ def test_ok_frame(self, framer, callback):
framer.processIncomingPacket(self.good_frame, callback, self.slaves)
callback.assert_called_once()

def test_1917_frame(self, callback):
"""Test invalid frame in issue 1917."""
recv = b"\x01\x86\x02\x00\x01"
framer = ModbusRtuFramer(ClientDecoder())
framer.processIncomingPacket(recv, callback, self.slaves)
callback.assert_not_called()

def test_faulty_frame1(self, framer, callback):
"""Test ok frame."""
faulty_frame = b"\x00\x04\x00\x00\x00\x05\x00\x03\x0a\x00\x04"
Expand Down
33 changes: 21 additions & 12 deletions test/sub_transport/test_comm.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Test transport."""
import asyncio
import sys
import time
from unittest import mock

Expand Down Expand Up @@ -34,8 +35,9 @@ def get_port_in_class(base_ports):
(CommType.SERIAL, "socket://localhost:5004"),
],
)
async def test_connect(self, client):
async def test_connect(self, client, use_port):
"""Test connect()."""
print(f"JAN test_connect --> {use_port}", file=sys.stderr)
start = time.time()
assert not await client.transport_connect()
delta = time.time() - start
Expand All @@ -51,8 +53,9 @@ async def test_connect(self, client):
(CommType.SERIAL, "/dev/tty007pymodbus_5008"),
],
)
async def test_connect_not_ok(self, client):
async def test_connect_not_ok(self, client, use_port):
"""Test connect()."""
print(f"JAN test_connect_not_ok --> {use_port}", file=sys.stderr)
start = time.time()
assert not await client.transport_connect()
delta = time.time() - start
Expand All @@ -68,8 +71,9 @@ async def test_connect_not_ok(self, client):
(CommType.SERIAL, "socket://localhost:5012"),
],
)
async def test_listen(self, server):
async def test_listen(self, server, use_port):
"""Test listen()."""
print(f"JAN test_listen --> {use_port}", file=sys.stderr)
assert await server.transport_listen()
assert server.transport
server.transport_close()
Expand All @@ -83,8 +87,9 @@ async def test_listen(self, server):
(CommType.SERIAL, "/dev/tty007pymodbus_5016"),
],
)
async def test_listen_not_ok(self, server):
async def test_listen_not_ok(self, server, use_port):
"""Test listen()."""
print(f"JAN test_listen_not_ok --> {use_port}", file=sys.stderr)
assert not await server.transport_listen()
assert not server.transport
server.transport_close()
Expand All @@ -95,11 +100,12 @@ async def test_listen_not_ok(self, server):
(CommType.TCP, "localhost"),
(CommType.TLS, "localhost"),
(CommType.UDP, "localhost"),
(CommType.SERIAL, "socket://localhost:5020"),
(CommType.SERIAL, "socket://localhost:7302"),
],
)
async def test_connected(self, client, server, use_comm_type):
async def test_connected(self, client, server, use_comm_type, use_port):
"""Test connection and data exchange."""
print(f"JAN test_connected --> {use_port}", file=sys.stderr)
assert await server.transport_listen()
assert await client.transport_connect()
await asyncio.sleep(0.5)
Expand Down Expand Up @@ -133,11 +139,12 @@ def wrapped_write(self, data):
@pytest.mark.parametrize(
("use_comm_type", "use_host"),
[
(CommType.SERIAL, "socket://localhost:5020"),
(CommType.SERIAL, "socket://localhost:7303"),
],
)
async def test_split_serial_packet(self, client, server):
async def test_split_serial_packet(self, client, server, use_port):
"""Test connection and data exchange."""
print(f"JAN test_split_serial_packet --> {use_port}", file=sys.stderr)
assert await server.transport_listen()
assert await client.transport_connect()
await asyncio.sleep(0.5)
Expand All @@ -161,11 +168,12 @@ async def test_split_serial_packet(self, client, server):
@pytest.mark.parametrize(
("use_comm_type", "use_host"),
[
(CommType.SERIAL, "socket://localhost:5020"),
(CommType.SERIAL, "socket://localhost:7300"),
],
)
async def test_serial_poll(self, client, server):
async def test_serial_poll(self, client, server, use_port):
"""Test connection and data exchange."""
print(f"JAN test_serial_poll --> {use_port}", file=sys.stderr)
assert await server.transport_listen()
SerialTransport.force_poll = True
assert await client.transport_connect()
Expand All @@ -187,11 +195,12 @@ async def test_serial_poll(self, client, server):
(CommType.TCP, "localhost"),
(CommType.TLS, "localhost"),
# (CommType.UDP, "localhost"), reuses same connection
# (CommType.SERIAL, "socket://localhost:5020"), no multipoint
# (CommType.SERIAL, "socket://localhost:7301"), no multipoint
],
)
async def test_connected_multiple(self, client, server):
async def test_connected_multiple(self, client, server, use_port):
"""Test connection and data exchange."""
print(f"JAN test_connected_multiple --> {use_port}", file=sys.stderr)
client.comm_params.reconnect_delay = 0.0
assert await server.transport_listen()
assert await client.transport_connect()
Expand Down
Loading

0 comments on commit e3e8537

Please sign in to comment.