Skip to content

Commit a38e0d2

Browse files
committed
Merge branch 'dev'
2 parents efb02eb + aa8c479 commit a38e0d2

21 files changed

+122
-167
lines changed

AUTHORS.rst

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Thanks to
1616
- Alex Ruddick
1717
- Alexander Lanin
1818
- Alexandre CUER
19+
- alexis-care
1920
- Alois Hockenschlohe
2021
- Andy Walker
2122
- Arjan
@@ -30,6 +31,7 @@ Thanks to
3031
- Chris Hung
3132
- Christian Krause
3233
- Christian Pfisterer
34+
- daanwtb
3335
- Daniel Rauber
3436
- dhoomakethu
3537
- doelki
@@ -74,6 +76,7 @@ Thanks to
7476
- Pavel Kostromitinov
7577
- peufeu2
7678
- Philip Couling
79+
- Philip Jones
7780
- Qi Li
7881
- Sebastian Machuca
7982
- Sefa Keleş

CHANGELOG.rst

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ helps make pymodbus a better product.
77

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

10+
Version 3.8.1
11+
-------------
12+
* Convert endianness (#2506)
13+
* Fix sync serial client, loop. (#2510)
14+
* Correct future. (#2507)
15+
* Correct #2501 (#2504)
16+
* Raise exception on no response in async client. (#2502)
17+
* re-instatiate Future on reconnect (#2501)
18+
* Remove all trailing zeroes during string decoding (#2493)
19+
* Fix too many sync client log messages. (#2491)
20+
1021
Version 3.8.0
1122
-------------
1223
* slave_id -> dev_id (internally). (#2486)

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Upgrade examples:
2626
- 3.6.1 -> 3.7.0: Smaller changes to the pymodbus calls might be needed
2727
- 2.5.4 -> 3.0.0: Major changes in the application might be needed
2828

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

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

doc/source/_static/examples.tgz

-594 KB
Binary file not shown.

doc/source/_static/examples.zip

218 Bytes
Binary file not shown.

doc/source/roadmap.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ It is the community that decides how pymodbus evolves NOT the maintainers !
1515

1616
The following bullet points are what the maintainers focus on:
1717

18-
- 3.8.1, bug fix release, with:
18+
- 3.8.2, bug fix release, with:
1919
- Currently not planned
2020
- 3.9.0, with:
21+
- All of branch wait_next_api
2122
- ModbusControlBlock pr slave
2223
- New custom PDU (function codes)
2324
- Remove remote_datastore

examples/client_async_calls.py

+8
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ async def async_handle_holding_registers(client):
146146
assert not rr.isError() # test that call was OK
147147
assert rr.registers == [10]
148148

149+
value_int32 = 13211
150+
registers = client.convert_to_registers(value_int32, client.DATATYPE.INT32)
151+
await client.write_registers(1, registers, slave=SLAVE)
152+
rr = await client.read_holding_registers(1, count=len(registers), slave=SLAVE)
153+
assert not rr.isError() # test that call was OK
154+
value = client.convert_from_registers(rr.registers, client.DATATYPE.INT32)
155+
assert value_int32 == value
156+
149157
_logger.info("### write read holding registers")
150158
arguments = {
151159
"read_address": 1,

examples/client_calls.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,13 @@ def handle_holding_registers(client):
135135
assert not rr.isError() # test that call was OK
136136
assert rr.registers[0] == 10
137137

138-
client.write_registers(1, [10] * 8, slave=SLAVE)
139-
rr = client.read_holding_registers(1, count=8, slave=SLAVE)
138+
value_int32 = 13211
139+
registers = client.convert_to_registers(value_int32, client.DATATYPE.INT32)
140+
client.write_registers(1, registers, slave=SLAVE)
141+
rr = client.read_holding_registers(1, count=len(registers), slave=SLAVE)
140142
assert not rr.isError() # test that call was OK
141-
assert rr.registers == [10] * 8
143+
value = client.convert_from_registers(rr.registers, client.DATATYPE.INT32)
144+
assert value_int32 == value
142145

143146
_logger.info("### write read holding registers")
144147
arguments = {

examples/client_custom_msg.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from pymodbus import FramerType
1717
from pymodbus.client import AsyncModbusTcpClient as ModbusClient
18+
from pymodbus.exceptions import ModbusIOException
1819
from pymodbus.pdu import ExceptionResponse, ModbusPDU
1920
from pymodbus.pdu.bit_message import ReadCoilsRequest
2021

@@ -128,8 +129,12 @@ async def main(host="localhost", port=5020):
128129
client.register(CustomModbusPDU)
129130
slave=1
130131
request1 = CustomRequest(32, slave=slave)
131-
result = await client.execute(False, request1)
132-
print(result)
132+
try:
133+
result = await client.execute(False, request1)
134+
except ModbusIOException:
135+
print("Server do not support CustomRequest.")
136+
else:
137+
print(result)
133138

134139
# inherited request
135140
request2 = Read16CoilsRequest(32, slave)

pymodbus/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818
from pymodbus.pdu import ExceptionResponse
1919

2020

21-
__version__ = "3.8.0"
21+
__version__ = "3.8.1"
2222
__version_full__ = f"[pymodbus, version {__version__}]"

pymodbus/client/base.py

-13
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from pymodbus.pdu import DecodePDU, ModbusPDU
1212
from pymodbus.transaction import TransactionManager
1313
from pymodbus.transport import CommParams
14-
from pymodbus.utilities import ModbusTransactionState
1514

1615

1716
class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusPDU]]):
@@ -44,7 +43,6 @@ def __init__(
4443
trace_pdu,
4544
trace_connect,
4645
)
47-
self.state = ModbusTransactionState.IDLE
4846

4947
@property
5048
def connected(self) -> bool:
@@ -146,10 +144,8 @@ def __init__(
146144
)
147145
self.reconnect_delay_current = self.comm_params.reconnect_delay or 0
148146
self.use_udp = False
149-
self.state = ModbusTransactionState.IDLE
150147
self.last_frame_end: float | None = 0
151148
self.silent_interval: float = 0
152-
self.transport = None
153149

154150
# ----------------------------------------------------------------------- #
155151
# Client external interface
@@ -192,15 +188,6 @@ def execute(self, no_response_expected: bool, request: ModbusPDU) -> ModbusPDU:
192188
# ----------------------------------------------------------------------- #
193189
# Internal methods
194190
# ----------------------------------------------------------------------- #
195-
def _start_send(self):
196-
"""Send request.
197-
198-
:meta private:
199-
"""
200-
if self.state != ModbusTransactionState.RETRYING:
201-
Log.debug('New Transaction state "SENDING"')
202-
self.state = ModbusTransactionState.SENDING
203-
204191
@abstractmethod
205192
def send(self, request: bytes, addr: tuple | None = None) -> int:
206193
"""Send request.

pymodbus/client/mixin.py

+45-23
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import struct
55
from abc import abstractmethod
66
from enum import Enum
7-
from typing import Generic, TypeVar
7+
from typing import Generic, Literal, TypeVar, cast
88

99
import pymodbus.pdu.bit_message as pdu_bit
1010
import pymodbus.pdu.diag_message as pdu_diag
@@ -691,61 +691,83 @@ class DATATYPE(Enum):
691691
FLOAT32 = ("f", 2)
692692
FLOAT64 = ("d", 4)
693693
STRING = ("s", 0)
694-
BITS = "bits"
694+
BITS = ("bits", 0)
695695

696696
@classmethod
697697
def convert_from_registers(
698-
cls, registers: list[int], data_type: DATATYPE
699-
) -> int | float | str | list[bool]:
698+
cls, registers: list[int], data_type: DATATYPE, word_order: Literal["big", "little"] = "big"
699+
) -> int | float | str | list[bool] | list[int] | list[float]:
700700
"""Convert registers to int/float/str.
701701
702702
:param registers: list of registers received from e.g. read_holding_registers()
703703
:param data_type: data type to convert to
704-
:returns: int, float, str or list[bool] depending on "data_type"
705-
:raises ModbusException: when size of registers is not 1, 2 or 4
706-
"""
707-
byte_list = bytearray()
708-
for x in registers:
709-
byte_list.extend(int.to_bytes(x, 2, "big"))
710-
if data_type == cls.DATATYPE.STRING:
711-
if byte_list[-1:] == b"\00":
712-
byte_list = byte_list[:-1]
713-
return byte_list.decode("utf-8")
714-
if data_type == cls.DATATYPE.BITS:
704+
:param word_order: "big"/"little" order of words/registers
705+
:returns: scalar or array of "data_type"
706+
:raises ModbusException: when size of registers is not a multiple of data_type
707+
"""
708+
if not (data_len := data_type.value[1]):
709+
byte_list = bytearray()
710+
if word_order == "little":
711+
registers.reverse()
712+
for x in registers:
713+
byte_list.extend(int.to_bytes(x, 2, "big"))
714+
if data_type == cls.DATATYPE.STRING:
715+
trailing_nulls_begin = len(byte_list)
716+
while trailing_nulls_begin > 0 and not byte_list[trailing_nulls_begin - 1]:
717+
trailing_nulls_begin -= 1
718+
byte_list = byte_list[:trailing_nulls_begin]
719+
return byte_list.decode("utf-8")
715720
return unpack_bitstring(byte_list)
716-
if len(registers) != data_type.value[1]:
721+
if (reg_len := len(registers)) % data_len:
717722
raise ModbusException(
718-
f"Illegal size ({len(registers)}) of register array, cannot convert!"
723+
f"Registers illegal size ({len(registers)}) expected multiple of {data_len}!"
719724
)
720-
return struct.unpack(f">{data_type.value[0]}", byte_list)[0]
725+
726+
result = []
727+
for i in range(0, reg_len, data_len):
728+
regs = registers[i:i+data_len]
729+
if word_order == "little":
730+
regs.reverse()
731+
byte_list = bytearray()
732+
for x in regs:
733+
byte_list.extend(int.to_bytes(x, 2, "big"))
734+
result.append(struct.unpack(f">{data_type.value[0]}", byte_list)[0])
735+
return result if len(result) != 1 else result[0]
721736

722737
@classmethod
723738
def convert_to_registers(
724-
cls, value: int | float | str | list[bool], data_type: DATATYPE
739+
cls, value: int | float | str | list[bool] | list[int] | list[float] , data_type: DATATYPE, word_order: Literal["big", "little"] = "big"
725740
) -> list[int]:
726741
"""Convert int/float/str to registers (16/32/64 bit).
727742
728743
:param value: value to be converted
729-
:param data_type: data type to be encoded as registers
744+
:param data_type: data type to convert from
745+
:param word_order: "big"/"little" order of words/registers
730746
:returns: List of registers, can be used directly in e.g. write_registers()
731747
:raises TypeError: when there is a mismatch between data_type and value
732748
"""
733749
if data_type == cls.DATATYPE.BITS:
734750
if not isinstance(value, list):
735-
raise TypeError(f"Value should be string but is {type(value)}.")
751+
raise TypeError(f"Value should be list of bool but is {type(value)}.")
736752
if (missing := len(value) % 16):
737753
value = value + [False] * (16 - missing)
738-
byte_list = pack_bitstring(value)
754+
byte_list = pack_bitstring(cast(list[bool], value))
739755
elif data_type == cls.DATATYPE.STRING:
740756
if not isinstance(value, str):
741757
raise TypeError(f"Value should be string but is {type(value)}.")
742758
byte_list = value.encode()
743759
if len(byte_list) % 2:
744760
byte_list += b"\x00"
745761
else:
746-
byte_list = struct.pack(f">{data_type.value[0]}", value)
762+
if not isinstance(value, list):
763+
value = cast(list[int], [value])
764+
byte_list = bytearray()
765+
for v in value:
766+
byte_list.extend(struct.pack(f">{data_type.value[0]}", v))
747767
regs = [
748768
int.from_bytes(byte_list[x : x + 2], "big")
749769
for x in range(0, len(byte_list), 2)
750770
]
771+
if word_order == "little":
772+
regs.reverse()
751773
return regs

pymodbus/client/serial.py

+5-59
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from pymodbus.logging import Log
1414
from pymodbus.pdu import ModbusPDU
1515
from pymodbus.transport import CommParams, CommType
16-
from pymodbus.utilities import ModbusTransactionState
1716

1817

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

161-
state = ModbusTransactionState.IDLE
162-
inter_byte_timeout: float = 0
163-
silent_interval: float = 0
164-
165160
def __init__( # pylint: disable=too-many-arguments
166161
self,
167162
port: str,
@@ -219,6 +214,8 @@ def __init__( # pylint: disable=too-many-arguments
219214
# Set a minimum of 1ms for high baudrates
220215
self._recv_interval = max(self._recv_interval, 0.001)
221216

217+
self.inter_byte_timeout: float = 0
218+
self.silent_interval: float = 0
222219
if baudrate > 19200:
223220
self.silent_interval = 1.75 / 1000 # ms
224221
else:
@@ -264,14 +261,9 @@ def _in_waiting(self):
264261
"""Return waiting bytes."""
265262
return getattr(self.socket, "in_waiting") if hasattr(self.socket, "in_waiting") else getattr(self.socket, "inWaiting")()
266263

267-
def _send(self, request: bytes) -> int: # pragma: no cover
268-
"""Send data on the underlying socket.
269-
270-
If receive buffer still holds some data then flush it.
271-
272-
Sleep if last send finished less than 3.5 character times ago.
273-
"""
274-
super()._start_send()
264+
def send(self, request: bytes, addr: tuple | None = None) -> int:
265+
"""Send data on the underlying socket."""
266+
_ = addr
275267
if not self.socket:
276268
raise ConnectionException(str(self))
277269
if request:
@@ -283,52 +275,6 @@ def _send(self, request: bytes) -> int: # pragma: no cover
283275
return size
284276
return 0
285277

286-
def send(self, request: bytes, addr: tuple | None = None) -> int: # pragma: no cover
287-
"""Send data on the underlying socket."""
288-
_ = addr
289-
start = time.time()
290-
if hasattr(self,"ctx"):
291-
timeout = start + self.ctx.comm_params.timeout_connect
292-
else:
293-
timeout = start + self.comm_params.timeout_connect
294-
while self.state != ModbusTransactionState.IDLE:
295-
if self.state == ModbusTransactionState.TRANSACTION_COMPLETE:
296-
timestamp = round(time.time(), 6)
297-
Log.debug(
298-
"Changing state to IDLE - Last Frame End - {} Current Time stamp - {}",
299-
self.last_frame_end,
300-
timestamp,
301-
)
302-
if self.last_frame_end:
303-
idle_time = self.idle_time()
304-
if round(timestamp - idle_time, 6) <= self.silent_interval:
305-
Log.debug(
306-
"Waiting for 3.5 char before next send - {} ms",
307-
self.silent_interval * 1000,
308-
)
309-
time.sleep(self.silent_interval)
310-
else:
311-
# Recovering from last error ??
312-
time.sleep(self.silent_interval)
313-
self.state = ModbusTransactionState.IDLE
314-
elif self.state == ModbusTransactionState.RETRYING:
315-
# Simple lets settle down!!!
316-
# To check for higher baudrates
317-
time.sleep(self.comm_params.timeout_connect)
318-
break
319-
elif time.time() > timeout:
320-
Log.debug(
321-
"Spent more time than the read time out, "
322-
"resetting the transaction to IDLE"
323-
)
324-
self.state = ModbusTransactionState.IDLE
325-
else:
326-
Log.debug("Sleeping")
327-
time.sleep(self.silent_interval)
328-
size = self._send(request)
329-
self.last_frame_end = round(time.time(), 6)
330-
return size
331-
332278
def _wait_for_data(self) -> int:
333279
"""Wait for data."""
334280
size = 0

0 commit comments

Comments
 (0)