Skip to content

Commit 74782b2

Browse files
authored
Patch 1 (#292)
* Fix #289 and other misc enhancements * Replace nosetest with pytest * Update Changelog * serial sync client wait till timeout/some data is available in read buffer + update changelog * serial sync client read updates when timeout is None and Zero * fix sync client unit test and example
1 parent c49038a commit 74782b2

19 files changed

+227
-110
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ Version 1.5.0
1010
* Move framers from transaction.py to respective modules
1111
* Fix modbus payload builder and decoder
1212
* Async servers can now have an option to defer `reactor.run()` when using `Start<Tcp/Serial/Udo>Server(...,defer_reactor_run=True)`
13-
* Fix UDP client issue while handling MEI messages (ReadDeviceInformationRequest)
13+
* Fix UDP client issue while handling MEI messages (ReadDeviceInformationRequest)
14+
* Add expected response lengths for WriteMultipleCoilRequest and WriteMultipleRegisterRequest
15+
* Fix struct errors while decoding stray response
16+
* Modbus read retries works only when empty/no message is received
17+
* Change test runner from nosetest to pytest
1418
* Fix Misc examples
1519

1620
Version 1.4.0

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ check: install
3939

4040
test: install
4141
@pip install --quiet --requirement=requirements-tests.txt
42-
@nosetests --with-coverage --cover-html
42+
@py.test
4343
@coverage report --fail-under=90
4444

4545
tox: install

examples/common/synchronous_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# import the various server implementations
1818
# --------------------------------------------------------------------------- #
1919
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
20-
#from pymodbus.client.sync import ModbusUdpClient as ModbusClient
20+
# from pymodbus.client.sync import ModbusUdpClient as ModbusClient
2121
# from pymodbus.client.sync import ModbusSerialClient as ModbusClient
2222

2323
# --------------------------------------------------------------------------- #
@@ -62,6 +62,7 @@ def run_sync_client():
6262
# client = ModbusClient('localhost', retries=3, retry_on_empty=True)
6363
# ------------------------------------------------------------------------#
6464
client = ModbusClient('localhost', port=5020)
65+
# from pymodbus.transaction import ModbusRtuFramer
6566
# client = ModbusClient('localhost', port=5020, framer=ModbusRtuFramer)
6667
# client = ModbusClient(method='binary', port='/dev/ptyp0', timeout=1)
6768
# client = ModbusClient(method='ascii', port='/dev/ptyp0', timeout=1)

examples/common/synchronous_client_ext.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# from pymodbus.client.sync import ModbusTcpClient as ModbusClient
1414
# from pymodbus.client.sync import ModbusUdpClient as ModbusClient
1515
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
16-
from pymodbus.transaction import ModbusRtuFramer
16+
1717

1818
# --------------------------------------------------------------------------- #
1919
# import the extended messages to perform
@@ -51,6 +51,8 @@ def execute_extended_requests():
5151
# client = ModbusClient(method='ascii', port="/dev/ptyp0")
5252
# client = ModbusClient(method='binary', port="/dev/ptyp0")
5353
# client = ModbusClient('127.0.0.1', port=5020)
54+
# from pymodbus.transaction import ModbusRtuFramer
55+
# client = ModbusClient('127.0.0.1', port=5020, framer=ModbusRtuFramer)
5456
client.connect()
5557

5658
# ----------------------------------------------------------------------- #

pymodbus/bit_write_message.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,13 @@ def __str__(self):
214214
params = (self.address, len(self.values))
215215
return "WriteNCoilRequest (%d) => %d " % params
216216

217+
def get_response_pdu_size(self):
218+
"""
219+
Func_code (1 byte) + Output Address (2 byte) + Quantity of Outputs (2 Bytes)
220+
:return:
221+
"""
222+
return 1 + 2 + 2
223+
217224

218225
class WriteMultipleCoilsResponse(ModbusResponse):
219226
'''

pymodbus/client/sync.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import serial
33
import time
44
import sys
5-
5+
from functools import partial
66
from pymodbus.constants import Defaults
77
from pymodbus.utilities import hexlify_packets, ModbusTransactionState
88
from pymodbus.factory import ClientDecoder
@@ -230,7 +230,35 @@ def _recv(self, size):
230230
"""
231231
if not self.socket:
232232
raise ConnectionException(self.__str__())
233-
return self.socket.recv(size)
233+
# socket.recv(size) waits until it gets some data from the host but
234+
# not necessarily the entire response that can be fragmented in
235+
# many packets.
236+
# To avoid the splitted responses to be recognized as invalid
237+
# messages and to be discarded, loops socket.recv until full data
238+
# is received or timeout is expired.
239+
# If timeout expires returns the read data, also if its length is
240+
# less than the expected size.
241+
self.socket.setblocking(0)
242+
begin = time.time()
243+
244+
data = b''
245+
if size is not None:
246+
while len(data) < size:
247+
try:
248+
data += self.socket.recv(size - len(data))
249+
except socket.error:
250+
pass
251+
if not self.timeout or (time.time() - begin > self.timeout):
252+
break
253+
else:
254+
while True:
255+
try:
256+
data += self.socket.recv(1)
257+
except socket.error:
258+
pass
259+
if not self.timeout or (time.time() - begin > self.timeout):
260+
break
261+
return data
234262

235263
def is_socket_open(self):
236264
return True if self.socket is not None else False
@@ -423,6 +451,16 @@ def close(self):
423451
self.socket.close()
424452
self.socket = None
425453

454+
def _in_waiting(self):
455+
in_waiting = ("in_waiting" if hasattr(
456+
self.socket, "in_waiting") else "inWaiting")
457+
458+
if in_waiting == "in_waiting":
459+
waitingbytes = getattr(self.socket, in_waiting)
460+
else:
461+
waitingbytes = getattr(self.socket, in_waiting)()
462+
return waitingbytes
463+
426464
def _send(self, request):
427465
""" Sends data on the underlying socket
428466
@@ -438,13 +476,7 @@ def _send(self, request):
438476
raise ConnectionException(self.__str__())
439477
if request:
440478
try:
441-
in_waiting = ("in_waiting" if hasattr(
442-
self.socket, "in_waiting") else "inWaiting")
443-
444-
if in_waiting == "in_waiting":
445-
waitingbytes = getattr(self.socket, in_waiting)
446-
else:
447-
waitingbytes = getattr(self.socket, in_waiting)()
479+
waitingbytes = self._in_waiting()
448480
if waitingbytes:
449481
result = self.socket.read(waitingbytes)
450482
if _logger.isEnabledFor(logging.WARNING):
@@ -457,6 +489,19 @@ def _send(self, request):
457489
return size
458490
return 0
459491

492+
def _wait_for_data(self):
493+
if self.timeout is not None and self.timeout != 0:
494+
condition = partial(lambda start, timeout: (time.time() - start) <= timeout, timeout=self.timeout)
495+
else:
496+
condition = partial(lambda dummy1, dummy2: True, dummy2=None)
497+
start = time.time()
498+
while condition(start):
499+
size = self._in_waiting()
500+
if size:
501+
break
502+
time.sleep(0.01)
503+
return size
504+
460505
def _recv(self, size):
461506
""" Reads data from the underlying descriptor
462507
@@ -465,6 +510,8 @@ def _recv(self, size):
465510
"""
466511
if not self.socket:
467512
raise ConnectionException(self.__str__())
513+
if size is None:
514+
size = self._wait_for_data()
468515
result = self.socket.read(size)
469516
return result
470517

pymodbus/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class Defaults(Singleton):
103103
Stopbits = 1
104104
ZeroMode = False
105105
IgnoreMissingSlaves = False
106-
106+
ReadSize = 1024
107107

108108
class ModbusStatus(Singleton):
109109
'''

pymodbus/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def __init__(self, string=""):
7878
ModbusException.__init__(self, message)
7979

8080

81-
class InvalidMessageRecievedException(ModbusException):
81+
class InvalidMessageReceivedException(ModbusException):
8282
"""
8383
Error resulting from invalid response received or decoded
8484
"""

pymodbus/factory.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ def decode(self, message):
223223
return self._helper(message)
224224
except ModbusException as er:
225225
_logger.error("Unable to decode response %s" % er)
226+
227+
except Exception as ex:
228+
_logger.error(ex)
226229
return None
227230

228231
def _helper(self, data):
@@ -234,8 +237,13 @@ def _helper(self, data):
234237
:param data: The response packet to decode
235238
:returns: The decoded request or an exception response object
236239
'''
237-
function_code = byte2int(data[0])
238-
_logger.debug("Factory Response[%d]" % function_code)
240+
fc_string = function_code = byte2int(data[0])
241+
if function_code in self.__lookup:
242+
fc_string = "%s: %s" % (
243+
str(self.__lookup[function_code]).split('.')[-1].rstrip("'>"),
244+
function_code
245+
)
246+
_logger.debug("Factory Response[%s]" % fc_string)
239247
response = self.__lookup.get(function_code, lambda: None)()
240248
if function_code > 0x80:
241249
code = function_code & 0x7f # strip error portion

pymodbus/framer/rtu_framer.py

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import time
33

44
from pymodbus.exceptions import ModbusIOException
5-
from pymodbus.exceptions import InvalidMessageRecievedException
5+
from pymodbus.exceptions import InvalidMessageReceivedException
66
from pymodbus.utilities import checkCRC, computeCRC
77
from pymodbus.utilities import hexlify_packets, ModbusTransactionState
88
from pymodbus.compat import byte2int
@@ -213,27 +213,16 @@ def processIncomingPacket(self, data, callback, unit, **kwargs):
213213
unit = [unit]
214214
self.addToFrame(data)
215215
single = kwargs.get("single", False)
216-
while True:
217-
if self.isFrameReady():
218-
if self.checkFrame():
219-
if self._validate_unit_id(unit, single):
220-
self._process(callback)
221-
else:
222-
_logger.debug("Not a valid unit id - {}, "
223-
"ignoring!!".format(self._header['uid']))
224-
self.resetFrame()
225-
216+
if self.isFrameReady():
217+
if self.checkFrame():
218+
if self._validate_unit_id(unit, single):
219+
self._process(callback)
226220
else:
227-
# Could be an error response
228-
if len(self._buffer):
229-
# Possible error ???
230-
self._process(callback, error=True)
231-
else:
232-
if len(self._buffer):
233-
# Possible error ???
234-
if self._header.get('len', 0) < 2:
235-
self._process(callback, error=True)
236-
break
221+
_logger.debug("Not a valid unit id - {}, "
222+
"ignoring!!".format(self._header['uid']))
223+
self.resetFrame()
224+
else:
225+
_logger.debug("Frame - [{}] not ready".format(data))
237226

238227
def buildPacket(self, message):
239228
"""
@@ -258,7 +247,7 @@ def sendPacket(self, message):
258247
# ModbusTransactionState.to_string(self.client.state))
259248
# )
260249
while self.client.state != ModbusTransactionState.IDLE:
261-
if self.client.state == ModbusTransactionState.TRANSCATION_COMPLETE:
250+
if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE:
262251
ts = round(time.time(), 6)
263252
_logger.debug("Changing state to IDLE - Last Frame End - {}, "
264253
"Current Time stamp - {}".format(
@@ -296,11 +285,6 @@ def recvPacket(self, size):
296285
:return:
297286
"""
298287
result = self.client.recv(size)
299-
# if self.client.state != ModbusTransactionState.PROCESSING_REPLY:
300-
# _logger.debug("Changing transaction state from "
301-
# "'WAITING FOR REPLY' to 'PROCESSING REPLY'")
302-
# self.client.state = ModbusTransactionState.PROCESSING_REPLY
303-
304288
self.client.last_frame_end = round(time.time(), 6)
305289
return result
306290

@@ -313,7 +297,7 @@ def _process(self, callback, error=False):
313297
if result is None:
314298
raise ModbusIOException("Unable to decode request")
315299
elif error and result.function_code < 0x80:
316-
raise InvalidMessageRecievedException(result)
300+
raise InvalidMessageReceivedException(result)
317301
else:
318302
self.populateResult(result)
319303
self.advanceFrame()

0 commit comments

Comments
 (0)