Skip to content

Commit 9f8d5f6

Browse files
committed
Progess toward pymodbus 3.7 support
1 parent 2844dc6 commit 9f8d5f6

File tree

10 files changed

+782
-824
lines changed

10 files changed

+782
-824
lines changed

README.org

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2296,14 +2296,6 @@ val, = via.write( operations )
22962296
input buffer which (in concert with our proper serial read
22972297
=modbus_rtu_read=) allows us to implement correct Modbus/RTU framing.
22982298

2299-
**** =modbus_sparse_data_block=
2300-
2301-
The provided =ModbusSparseDataBlock= incorrectly deduces the base address,
2302-
and is wildly inefficient for large data blocks. We correctly deduce the
2303-
base register address. The provided =.validate= method is O(N+V) for data
2304-
blocks of size N when validating V registers; we provide an O(V)
2305-
implementation.
2306-
23072299
* Deterministic Finite Automata
23082300

23092301
A cpppo.dfa will consume symbols from its source iterable, and yield
@@ -2327,6 +2319,9 @@ val, = via.write( operations )
23272319
+-------+
23282320
#+END_SRC
23292321

2322+
#+RESULTS:
2323+
[[file:abplus.png]]
2324+
23302325
This machine is easily created like this:
23312326

23322327
#+LATEX: {\scriptsize
@@ -2364,6 +2359,9 @@ val, = via.write( operations )
23642359
+----------------------------------------+
23652360
#+END_SRC
23662361

2362+
#+RESULTS:
2363+
[[file:abplus_csv.png]]
2364+
23672365
This is implemented:
23682366

23692367
#+LATEX: {\scriptsize
@@ -2407,6 +2405,9 @@ val, = via.write( operations )
24072405
None None None None
24082406
#+END_SRC
24092407

2408+
#+RESULTS:
2409+
[[file:abplus_regex.png]]
2410+
24102411
The =True= transition out of each state ensures that the =cpppo.state=
24112412
machine will yield a None (non-transition) when encountering an invalid
24122413
symbol in the language described by the regular expression grammar. Only if

README.pdf

13.3 KB
Binary file not shown.

README.txt

Lines changed: 735 additions & 744 deletions
Large diffs are not rendered by default.

bin/modbus_sim.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@
8989
# import the various server implementations
9090
#---------------------------------------------------------------------------#
9191
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
92-
from pymodbus.pdu.register_read_message import ReadRegistersResponseBase
93-
from pymodbus.pdu.register_write_message import WriteSingleRegisterResponse, WriteMultipleRegistersResponse
94-
from pymodbus.framer import FramerType, FRAMER_NAME_TO_CLASS
92+
from pymodbus.pdu.register_message import ReadHoldingRegistersResponse, WriteSingleRegisterResponse, WriteMultipleRegistersResponse
93+
from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType
9594
from pymodbus.exceptions import NotImplementedException
9695
from pymodbus.datastore.store import ModbusSparseDataBlock
9796

@@ -109,9 +108,7 @@
109108
sys.path.insert( 0, os.path.dirname( os.path.dirname( os.path.dirname( os.path.abspath( __file__ )))))
110109
import cpppo
111110

112-
from cpppo.remote.pymodbus_fixes import (
113-
modbus_sparse_data_block, modbus_server_rtu_printing, modbus_server_tcp_printing, Defaults
114-
)
111+
from cpppo.remote.pymodbus_fixes import modbus_server_rtu_printing, modbus_server_tcp_printing, Defaults
115112

116113

117114
#---------------------------------------------------------------------------#
@@ -190,7 +187,7 @@ def register_decode( txt, default=None ):
190187

191188
def register_definitions( registers, default=None ):
192189
"""Parse the register ranges, as: registers[, registers ...], and produce a keywords dictionary
193-
suitable for construction of modbus_sparse_data_block instances for a ModbusSlaveContext, for
190+
suitable for construction of ModbusSparseDataBlock instances for a ModbusSlaveContext, for
194191
'hr' (Holding Registers), 'co' (Coils), etc.:
195192
196193
40001=999
@@ -459,7 +456,8 @@ def main( argv=None ):
459456
starter_kwds = {}
460457
try:
461458
# See if it's an <interface>[:<port>]. If it has '/' in it, assume its a device
462-
assert '/' not in address
459+
assert '/' not in args.address and not os.path.exists( args.address ), \
460+
"appears to be a file: {address}".format( address=address )
463461
starter = StartTcpServerLogging
464462
framer = FramerType.SOCKET
465463
address = cpppo.parse_ip_port( args.address, default=(None,Defaults.Port) )
@@ -494,7 +492,6 @@ def main( argv=None ):
494492
assert args.range == 1, \
495493
"A range of serial ports is unsupported"
496494

497-
logging.info( "Modbus Framer: {framer}".format( framer=framer ))
498495
framer = FRAMER_NAME_TO_CLASS[framer]
499496

500497
#---------------------------------------------------------------------------#
@@ -536,7 +533,7 @@ def buildFrame(self, message):
536533
delay = self.delay
537534
if isinstance( delay, (list,tuple) ):
538535
delay = random.uniform( *delay )
539-
time.sleep( delay )
536+
time.sleep( delay ) # blocks the entire asyncio program! No other I/O occurs.
540537

541538
return packet
542539

@@ -577,7 +574,7 @@ def buildFrame(self, message):
577574
packet = super( EvilFramerCorruptResponse, self ).buildFrame( message )
578575
message.transaction_id ^= 0xFFFF
579576
elif self.what == "registers":
580-
if isinstance( message, ReadRegistersResponseBase ):
577+
if isinstance( message, ReadHoldingRegistersResponse ):
581578
# These have '.registers' attribute, which is a list.
582579
# Add/remove some
583580
saveregs = message.registers

modbus_test.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from .tools.waits import waitfor
3333

3434
RTU_WAIT = 2.0 # How long to wait for the simulator
35-
RTU_LATENCY = 0.05 # poll for command-line I/O response
35+
RTU_LATENCY = 0.05 # poll for command-line I/O response
3636

3737
class nonblocking_command( object ):
3838
"""Set up a non-blocking command producing output. Read the output using:
@@ -196,7 +196,7 @@ def start_modbus_simulator( *options ):
196196
here.
197197
198198
"""
199-
return start_simulator(
199+
return start_simulator(
200200
os.path.join( os.path.dirname( os.path.abspath( __file__ )), 'bin', 'modbus_sim.py' ),
201201
*options
202202
)
@@ -213,12 +213,12 @@ def run_plc_modbus_polls( plc ):
213213
wfkw = dict( timeout=timeout, intervals=intervals )
214214

215215
plc.poll( 40001, rate=rate )
216-
216+
217217
success,elapsed = waitfor( lambda: plc.read( 40001 ) is not None, "40001 polled", **wfkw )
218218
assert success
219219
assert elapsed < 1.0
220220
assert plc.read( 40001 ) == 0
221-
221+
222222
assert plc.read( 1 ) == None
223223
assert plc.read( 40002 ) == None
224224
success,elapsed = waitfor( lambda: plc.read( 40002 ) is not None, "40002 polled", **wfkw )
@@ -234,11 +234,11 @@ def run_plc_modbus_polls( plc ):
234234
# number of distinct poll ranges will increase, and then decrease as we in-fill and the
235235
# inter-register range drops below the merge reach 10, allowing the polling to merge ranges.
236236
# Thus, keep track of the number of registers added, and allow
237-
#
238-
# avg.
237+
#
238+
# avg.
239239
# poll
240240
# time
241-
#
241+
#
242242
# |
243243
# |
244244
# 4s| ..
@@ -252,7 +252,7 @@ def run_plc_modbus_polls( plc ):
252252
# need to more than double the Nyquist-rate timeout
253253
wfkw['timeout'] *= 2.5
254254
wfkw['intervals'] *= 2.5
255-
255+
256256
regs = {}
257257
extent = 100 # how many each of coil/holding registers
258258
total = extent*2 # total registers in play

remote/plc_modbus.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@
4141

4242

4343
from pymodbus.exceptions import ModbusException, ParameterException
44-
from pymodbus.pdu.bit_read_message import ReadDiscreteInputsRequest, ReadCoilsRequest
45-
from pymodbus.pdu.bit_write_message import WriteSingleCoilRequest, WriteMultipleCoilsRequest
46-
from pymodbus.pdu.register_read_message import ReadHoldingRegistersRequest, ReadInputRegistersRequest
47-
from pymodbus.pdu.register_write_message import WriteSingleRegisterRequest, WriteMultipleRegistersRequest
44+
from pymodbus.pdu.bit_message import ReadDiscreteInputsRequest, ReadCoilsRequest, WriteSingleCoilRequest, WriteMultipleCoilsRequest
45+
from pymodbus.pdu.register_message import ReadHoldingRegistersRequest, ReadInputRegistersRequest, WriteSingleRegisterRequest, WriteMultipleRegistersRequest
4846
from pymodbus.pdu import ExceptionResponse, ModbusPDU as ModbusResponse
4947

5048
log = logging.getLogger( __package__ )

remote/pymodbus_fixes.py

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,12 @@ def _eintr_retry(func, *args):
7070

7171
from pymodbus import __version__ as pymodbus_version
7272
from pymodbus.server import ModbusTcpServer, ModbusSerialServer
73-
#from pymodbus.transaction import ModbusSocketFramer, ModbusRtuFramer
74-
7573
from pymodbus.client import ModbusTcpClient, ModbusSerialClient
76-
#from pymodbus.factory import ClientDecoder
7774
from pymodbus.exceptions import ConnectionException
7875
from pymodbus.pdu import ExceptionResponse
79-
#from pymodbus.utilities import checkCRC
8076
from pymodbus.datastore.store import ModbusSparseDataBlock
8177

78+
8279
# Historically part of pymodbus to contain global defaults; now hosted here
8380
@dataclass
8481
class Defaults:
@@ -87,30 +84,6 @@ class Defaults:
8784
Timeout = 1.0
8885

8986

90-
# Correct an invalid default; ensure our *ModbusDataStore always correctly bases
91-
# requests from 0 (the human-readable addresses, eg. 1, 10001, 40001) have been
92-
# parsed and converted to zero-based addresses by the client, before the request
93-
# is sent). We will always convert standard Modbus typed addresses (eg. 40001
94-
# for Holding Registers) from 1-base to 0-base before making the appropriate
95-
# pymodbus register I/O requests. Pymodbus shouldn't be doing this for us.
96-
97-
#Defaults.ZeroMode = True
98-
99-
class modbus_sparse_data_block( ModbusSparseDataBlock ):
100-
"""Implement a ModbusSparseDataBlock that isn't spectacularly inefficient, and also correctly
101-
deduces the lowest address.
102-
103-
"""
104-
def __init__( self, values ):
105-
super( modbus_sparse_data_block, self ).__init__( values )
106-
self.address = min( self.values )
107-
108-
# def validate( self, address, count=1 ):
109-
# logging.debug( "checking %5d-%5d", address, address + count - 1 )
110-
# if count == 0: return False
111-
# return all( r in self.values for r in range( address, address + count ))
112-
113-
11487
class modbus_communication_monitor( object ):
11588
"""Outfit a pymodbus asyncio ModbusXxxServer to report communication_failed when a connect or
11689
listen is attempted, but does not succeed. This is necessary in order to abort a ...Server if a
@@ -152,7 +125,7 @@ class modbus_server_tcp( modbus_communication_monitor, ModbusTcpServer ):
152125
"""An asyncio.BaseProtocol based Modbus TCP server. """
153126
pass
154127

155-
128+
156129
class modbus_server_tcp_printing( modbus_server_tcp ):
157130

158131
def callback_communication( self, established ):

remote_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
from .remote.plc_modbus import poller_modbus, merge, shatter
4949
from .remote.pymodbus_fixes import modbus_client_tcp, modbus_server_tcp, Defaults
5050
has_pymodbus = True
51-
except ImportError:
51+
except ImportError as exc:
5252
logging.warning( "Failed to import pymodbus module; skipping Modbus/TCP related tests; run 'pip install pymodbus': {exc}".format( exc=exc ))
5353

5454

requirements-modbus.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
minimalmodbus >=2.1,
2-
pymodbus >=3.7, <4.0
1+
minimalmodbus >=2.1, <3
2+
pymodbus >=3.7, <4

serial_test.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
import pytest
3636

37-
#
37+
#
3838
# We require *explicit* access to the /dev/ttys[012] serial ports to perform this test -- we must
3939
# not assume that can safely use the serial port(s), or that they are configured for us to run our
4040
# tests! Therefore, we will run these tests ONLY if "serial" tests are explicitly called for
@@ -43,12 +43,12 @@
4343
# They must be configured as RS485, in the following multi-drop pattern:
4444
#
4545
# - master - - slaves -----------------
46-
# ttyS0(COM1) --> ttys1(COM2) --> ttys2(COM3)
47-
#
46+
# ttyS0(COM1) --> ttyS1(COM2) --> ttyS2(COM3)
47+
#
4848
# The Modbus Master will be on ttyS0, and two Modbus slave-ids (unit numbers) will be simulated on
4949
# each of ttyS1 and ttyS2. Since they each must ignore requests to slave-ids they do not simulate,
5050
# pymodbus >= 1.3.0 is required.
51-
#
51+
#
5252
PORT_MASTER = "ttyS0"
5353
PORT_SLAVES = {
5454
"ttyS1": [1,3],
@@ -68,7 +68,7 @@
6868
has_pyserial = True
6969
except ImportError:
7070
logging.warning( "Failed to import pyserial module; skipping Modbus/RTU related tests; run 'pip install pyserial'" )
71-
71+
7272
has_minimalmodbus = False
7373
try:
7474
# Configure minimalmodbus to use the specified port serial framing
@@ -135,7 +135,6 @@ def test_pymodbus_rs485_sync():
135135
# handle_local_echo=False,
136136
)
137137

138-
# Start the server on
139138
import asyncio
140139
from contextlib import suppress
141140

@@ -193,14 +192,14 @@ async def server_start( port, unit ):
193192
**serial_args,
194193
)
195194
client.connect()
196-
197-
rr1 = client.read_coils( 1, 1, slave=1 )
198-
rr2 = client.read_coils( 2, 1, slave=2 )
195+
196+
rr1 = client.read_coils( 1, count=1, slave=1 )
197+
rr2 = client.read_coils( 2, count=1, slave=2 )
199198
assert (( not rr2.isError() and rr2.bits[0] == True ) or
200-
( not rr1.isError() and rr.bits[0] == False ))
201-
rr3 = client.read_coils( 2, 1, slave=3 )
199+
( not rr1.isError() and rr1.bits[0] == False ))
200+
rr3 = client.read_coils( 2, count=1, slave=3 )
202201
assert rr3.isError()
203-
202+
204203
client.close()
205204
del client
206205

@@ -217,7 +216,7 @@ def reader():
217216
with client:
218217
unit = 1+a%len(SERVER_ttyS)
219218
expect = not bool( a%2 )
220-
rr = client.read_coils( 1, a, slave=unit )
219+
rr = client.read_coils( a, count=1, slave=unit )
221220
if rr.isError() or rr.bits[0] != expect:
222221
logging.warning( "Expected unit {unit} coil {a} == {expect}, got {val}".format(
223222
unit=unit, a=a, expect=expect, val=( rr if rr.isError() else rr.bits[0] )))
@@ -238,7 +237,6 @@ def reader():
238237
asyncio.run_coroutine_threadsafe( s.shutdown(), l )
239238
for u in servers:
240239
servers[u].join()
241-
242240

243241

244242
RTU_TIMEOUT = 0.1 # latency while simulated slave awaits next incoming byte
@@ -275,7 +273,7 @@ def simulated_modbus_rtu( tty ):
275273

276274
@pytest.fixture( scope="module" )
277275
def simulated_modbus_rtu_ttyS1( request ):
278-
command,address = simulated_modbus_rtu( "ttyS1" )
276+
command,address = simulated_modbus_rtu( "ttyS1" )
279277
request.addfinalizer( command.kill )
280278
return command,address
281279

@@ -342,7 +340,7 @@ def test_rs485_poll( simulated_modbus_rtu_ttyS1 ):
342340
assert success
343341
assert elapsed < 1.0
344342
assert plc.read( 40001 ) == 0
345-
343+
346344
assert plc.read( 1 ) == None
347345
assert plc.read( 40002 ) == None
348346
success,elapsed = waitfor( lambda: plc.read( 40002 ) is not None, "40002 polled", **wfkw )
@@ -358,7 +356,7 @@ def test_rs485_poll( simulated_modbus_rtu_ttyS1 ):
358356
success,elapsed = waitfor( lambda: plc.read( 40001 ) == 99, "40001 polled", **wfkw )
359357
assert success
360358
assert elapsed < 1.0
361-
359+
362360
# See if we converge on our target poll time
363361
count = plc.counter
364362
while plc.counter < count + 20:

0 commit comments

Comments
 (0)