Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
janiversen committed Dec 18, 2024
2 parents efb02eb + aa8c479 commit a38e0d2
Show file tree
Hide file tree
Showing 21 changed files with 122 additions and 167 deletions.
3 changes: 3 additions & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Thanks to
- Alex Ruddick
- Alexander Lanin
- Alexandre CUER
- alexis-care
- Alois Hockenschlohe
- Andy Walker
- Arjan
Expand All @@ -30,6 +31,7 @@ Thanks to
- Chris Hung
- Christian Krause
- Christian Pfisterer
- daanwtb
- Daniel Rauber
- dhoomakethu
- doelki
Expand Down Expand Up @@ -74,6 +76,7 @@ Thanks to
- Pavel Kostromitinov
- peufeu2
- Philip Couling
- Philip Jones
- Qi Li
- Sebastian Machuca
- Sefa Keleş
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ helps make pymodbus a better product.

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

Version 3.8.1
-------------
* Convert endianness (#2506)
* Fix sync serial client, loop. (#2510)
* Correct future. (#2507)
* Correct #2501 (#2504)
* Raise exception on no response in async client. (#2502)
* re-instatiate Future on reconnect (#2501)
* Remove all trailing zeroes during string decoding (#2493)
* Fix too many sync client log messages. (#2491)

Version 3.8.0
-------------
* slave_id -> dev_id (internally). (#2486)
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Upgrade examples:
- 3.6.1 -> 3.7.0: Smaller changes to the pymodbus calls might be needed
- 2.5.4 -> 3.0.0: Major changes in the application might be needed

Current release is `3.8.0 <https://github.com/pymodbus-dev/pymodbus/releases/tag/v3.8.0>`_.
Current release is `3.8.1 <https://github.com/pymodbus-dev/pymodbus/releases/tag/v3.8.1>`_.

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

Expand Down
Binary file modified doc/source/_static/examples.tgz
Binary file not shown.
Binary file modified doc/source/_static/examples.zip
Binary file not shown.
3 changes: 2 additions & 1 deletion doc/source/roadmap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ It is the community that decides how pymodbus evolves NOT the maintainers !

The following bullet points are what the maintainers focus on:

- 3.8.1, bug fix release, with:
- 3.8.2, bug fix release, with:
- Currently not planned
- 3.9.0, with:
- All of branch wait_next_api
- ModbusControlBlock pr slave
- New custom PDU (function codes)
- Remove remote_datastore
Expand Down
8 changes: 8 additions & 0 deletions examples/client_async_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ async def async_handle_holding_registers(client):
assert not rr.isError() # test that call was OK
assert rr.registers == [10]

value_int32 = 13211
registers = client.convert_to_registers(value_int32, client.DATATYPE.INT32)
await client.write_registers(1, registers, slave=SLAVE)
rr = await client.read_holding_registers(1, count=len(registers), slave=SLAVE)
assert not rr.isError() # test that call was OK
value = client.convert_from_registers(rr.registers, client.DATATYPE.INT32)
assert value_int32 == value

_logger.info("### write read holding registers")
arguments = {
"read_address": 1,
Expand Down
9 changes: 6 additions & 3 deletions examples/client_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,13 @@ def handle_holding_registers(client):
assert not rr.isError() # test that call was OK
assert rr.registers[0] == 10

client.write_registers(1, [10] * 8, slave=SLAVE)
rr = client.read_holding_registers(1, count=8, slave=SLAVE)
value_int32 = 13211
registers = client.convert_to_registers(value_int32, client.DATATYPE.INT32)
client.write_registers(1, registers, slave=SLAVE)
rr = client.read_holding_registers(1, count=len(registers), slave=SLAVE)
assert not rr.isError() # test that call was OK
assert rr.registers == [10] * 8
value = client.convert_from_registers(rr.registers, client.DATATYPE.INT32)
assert value_int32 == value

_logger.info("### write read holding registers")
arguments = {
Expand Down
9 changes: 7 additions & 2 deletions examples/client_custom_msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from pymodbus import FramerType
from pymodbus.client import AsyncModbusTcpClient as ModbusClient
from pymodbus.exceptions import ModbusIOException
from pymodbus.pdu import ExceptionResponse, ModbusPDU
from pymodbus.pdu.bit_message import ReadCoilsRequest

Expand Down Expand Up @@ -128,8 +129,12 @@ async def main(host="localhost", port=5020):
client.register(CustomModbusPDU)
slave=1
request1 = CustomRequest(32, slave=slave)
result = await client.execute(False, request1)
print(result)
try:
result = await client.execute(False, request1)
except ModbusIOException:
print("Server do not support CustomRequest.")
else:
print(result)

# inherited request
request2 = Read16CoilsRequest(32, slave)
Expand Down
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.8.0"
__version__ = "3.8.1"
__version_full__ = f"[pymodbus, version {__version__}]"
13 changes: 0 additions & 13 deletions pymodbus/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from pymodbus.pdu import DecodePDU, ModbusPDU
from pymodbus.transaction import TransactionManager
from pymodbus.transport import CommParams
from pymodbus.utilities import ModbusTransactionState


class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusPDU]]):
Expand Down Expand Up @@ -44,7 +43,6 @@ def __init__(
trace_pdu,
trace_connect,
)
self.state = ModbusTransactionState.IDLE

@property
def connected(self) -> bool:
Expand Down Expand Up @@ -146,10 +144,8 @@ def __init__(
)
self.reconnect_delay_current = self.comm_params.reconnect_delay or 0
self.use_udp = False
self.state = ModbusTransactionState.IDLE
self.last_frame_end: float | None = 0
self.silent_interval: float = 0
self.transport = None

# ----------------------------------------------------------------------- #
# Client external interface
Expand Down Expand Up @@ -192,15 +188,6 @@ def execute(self, no_response_expected: bool, request: ModbusPDU) -> ModbusPDU:
# ----------------------------------------------------------------------- #
# Internal methods
# ----------------------------------------------------------------------- #
def _start_send(self):
"""Send request.
:meta private:
"""
if self.state != ModbusTransactionState.RETRYING:
Log.debug('New Transaction state "SENDING"')
self.state = ModbusTransactionState.SENDING

@abstractmethod
def send(self, request: bytes, addr: tuple | None = None) -> int:
"""Send request.
Expand Down
68 changes: 45 additions & 23 deletions pymodbus/client/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import struct
from abc import abstractmethod
from enum import Enum
from typing import Generic, TypeVar
from typing import Generic, Literal, TypeVar, cast

import pymodbus.pdu.bit_message as pdu_bit
import pymodbus.pdu.diag_message as pdu_diag
Expand Down Expand Up @@ -691,61 +691,83 @@ class DATATYPE(Enum):
FLOAT32 = ("f", 2)
FLOAT64 = ("d", 4)
STRING = ("s", 0)
BITS = "bits"
BITS = ("bits", 0)

@classmethod
def convert_from_registers(
cls, registers: list[int], data_type: DATATYPE
) -> int | float | str | list[bool]:
cls, registers: list[int], data_type: DATATYPE, word_order: Literal["big", "little"] = "big"
) -> int | float | str | list[bool] | list[int] | list[float]:
"""Convert registers to int/float/str.
:param registers: list of registers received from e.g. read_holding_registers()
:param data_type: data type to convert to
:returns: int, float, str or list[bool] depending on "data_type"
:raises ModbusException: when size of registers is not 1, 2 or 4
"""
byte_list = bytearray()
for x in registers:
byte_list.extend(int.to_bytes(x, 2, "big"))
if data_type == cls.DATATYPE.STRING:
if byte_list[-1:] == b"\00":
byte_list = byte_list[:-1]
return byte_list.decode("utf-8")
if data_type == cls.DATATYPE.BITS:
:param word_order: "big"/"little" order of words/registers
:returns: scalar or array of "data_type"
:raises ModbusException: when size of registers is not a multiple of data_type
"""
if not (data_len := data_type.value[1]):
byte_list = bytearray()
if word_order == "little":
registers.reverse()
for x in registers:
byte_list.extend(int.to_bytes(x, 2, "big"))
if data_type == cls.DATATYPE.STRING:
trailing_nulls_begin = len(byte_list)
while trailing_nulls_begin > 0 and not byte_list[trailing_nulls_begin - 1]:
trailing_nulls_begin -= 1
byte_list = byte_list[:trailing_nulls_begin]
return byte_list.decode("utf-8")
return unpack_bitstring(byte_list)
if len(registers) != data_type.value[1]:
if (reg_len := len(registers)) % data_len:
raise ModbusException(
f"Illegal size ({len(registers)}) of register array, cannot convert!"
f"Registers illegal size ({len(registers)}) expected multiple of {data_len}!"
)
return struct.unpack(f">{data_type.value[0]}", byte_list)[0]

result = []
for i in range(0, reg_len, data_len):
regs = registers[i:i+data_len]
if word_order == "little":
regs.reverse()
byte_list = bytearray()
for x in regs:
byte_list.extend(int.to_bytes(x, 2, "big"))
result.append(struct.unpack(f">{data_type.value[0]}", byte_list)[0])
return result if len(result) != 1 else result[0]

@classmethod
def convert_to_registers(
cls, value: int | float | str | list[bool], data_type: DATATYPE
cls, value: int | float | str | list[bool] | list[int] | list[float] , data_type: DATATYPE, word_order: Literal["big", "little"] = "big"
) -> list[int]:
"""Convert int/float/str to registers (16/32/64 bit).
:param value: value to be converted
:param data_type: data type to be encoded as registers
:param data_type: data type to convert from
:param word_order: "big"/"little" order of words/registers
:returns: List of registers, can be used directly in e.g. write_registers()
:raises TypeError: when there is a mismatch between data_type and value
"""
if data_type == cls.DATATYPE.BITS:
if not isinstance(value, list):
raise TypeError(f"Value should be string but is {type(value)}.")
raise TypeError(f"Value should be list of bool but is {type(value)}.")
if (missing := len(value) % 16):
value = value + [False] * (16 - missing)
byte_list = pack_bitstring(value)
byte_list = pack_bitstring(cast(list[bool], value))
elif data_type == cls.DATATYPE.STRING:
if not isinstance(value, str):
raise TypeError(f"Value should be string but is {type(value)}.")
byte_list = value.encode()
if len(byte_list) % 2:
byte_list += b"\x00"
else:
byte_list = struct.pack(f">{data_type.value[0]}", value)
if not isinstance(value, list):
value = cast(list[int], [value])
byte_list = bytearray()
for v in value:
byte_list.extend(struct.pack(f">{data_type.value[0]}", v))
regs = [
int.from_bytes(byte_list[x : x + 2], "big")
for x in range(0, len(byte_list), 2)
]
if word_order == "little":
regs.reverse()
return regs
64 changes: 5 additions & 59 deletions pymodbus/client/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from pymodbus.logging import Log
from pymodbus.pdu import ModbusPDU
from pymodbus.transport import CommParams, CommType
from pymodbus.utilities import ModbusTransactionState


with contextlib.suppress(ImportError):
Expand Down Expand Up @@ -158,10 +157,6 @@ def run():
Please refer to :ref:`Pymodbus internals` for advanced usage.
"""

state = ModbusTransactionState.IDLE
inter_byte_timeout: float = 0
silent_interval: float = 0

def __init__( # pylint: disable=too-many-arguments
self,
port: str,
Expand Down Expand Up @@ -219,6 +214,8 @@ def __init__( # pylint: disable=too-many-arguments
# Set a minimum of 1ms for high baudrates
self._recv_interval = max(self._recv_interval, 0.001)

self.inter_byte_timeout: float = 0
self.silent_interval: float = 0
if baudrate > 19200:
self.silent_interval = 1.75 / 1000 # ms
else:
Expand Down Expand Up @@ -264,14 +261,9 @@ def _in_waiting(self):
"""Return waiting bytes."""
return getattr(self.socket, "in_waiting") if hasattr(self.socket, "in_waiting") else getattr(self.socket, "inWaiting")()

def _send(self, request: bytes) -> int: # pragma: no cover
"""Send data on the underlying socket.
If receive buffer still holds some data then flush it.
Sleep if last send finished less than 3.5 character times ago.
"""
super()._start_send()
def send(self, request: bytes, addr: tuple | None = None) -> int:
"""Send data on the underlying socket."""
_ = addr
if not self.socket:
raise ConnectionException(str(self))
if request:
Expand All @@ -283,52 +275,6 @@ def _send(self, request: bytes) -> int: # pragma: no cover
return size
return 0

def send(self, request: bytes, addr: tuple | None = None) -> int: # pragma: no cover
"""Send data on the underlying socket."""
_ = addr
start = time.time()
if hasattr(self,"ctx"):
timeout = start + self.ctx.comm_params.timeout_connect
else:
timeout = start + self.comm_params.timeout_connect
while self.state != ModbusTransactionState.IDLE:
if self.state == ModbusTransactionState.TRANSACTION_COMPLETE:
timestamp = round(time.time(), 6)
Log.debug(
"Changing state to IDLE - Last Frame End - {} Current Time stamp - {}",
self.last_frame_end,
timestamp,
)
if self.last_frame_end:
idle_time = self.idle_time()
if round(timestamp - idle_time, 6) <= self.silent_interval:
Log.debug(
"Waiting for 3.5 char before next send - {} ms",
self.silent_interval * 1000,
)
time.sleep(self.silent_interval)
else:
# Recovering from last error ??
time.sleep(self.silent_interval)
self.state = ModbusTransactionState.IDLE
elif self.state == ModbusTransactionState.RETRYING:
# Simple lets settle down!!!
# To check for higher baudrates
time.sleep(self.comm_params.timeout_connect)
break
elif time.time() > timeout:
Log.debug(
"Spent more time than the read time out, "
"resetting the transaction to IDLE"
)
self.state = ModbusTransactionState.IDLE
else:
Log.debug("Sleeping")
time.sleep(self.silent_interval)
size = self._send(request)
self.last_frame_end = round(time.time(), 6)
return size

def _wait_for_data(self) -> int:
"""Wait for data."""
size = 0
Expand Down
Loading

0 comments on commit a38e0d2

Please sign in to comment.