Skip to content

Commit 2844dc6

Browse files
committed
Upgrade pyserial and pymodbus
1 parent 938cf8c commit 2844dc6

File tree

12 files changed

+387
-592
lines changed

12 files changed

+387
-592
lines changed

GNUmakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ VENV = $(VENV_DIR)/$(VENV_NAME)
6969
VENV_OPTS =
7070

7171
# To see all pytest output, uncomment --capture=no
72-
PYTESTOPTS=-vv --capture=no --log-cli-level=WARNING # INFO # 25 == NORMAL 23 == DETAIL
72+
PYTESTOPTS=-vv --capture=no --log-cli-level=INFO # 25 == NORMAL 23 == DETAIL
7373

7474
PY_TEST=TZ=$(TZ) $(PY) -m pytest $(PYTESTOPTS)
7575
PY2TEST=TZ=$(TZ) $(PY2) -m pytest $(PYTESTOPTS)

bin/modbus_sim.py

Lines changed: 87 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
7575
'''
7676
import argparse
77+
import asyncio
7778
import json
7879
import logging
7980
import os
@@ -82,15 +83,17 @@
8283
import time
8384
import traceback
8485

86+
from contextlib import suppress
87+
8588
#---------------------------------------------------------------------------#
8689
# import the various server implementations
8790
#---------------------------------------------------------------------------#
8891
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
89-
from pymodbus.constants import Defaults
90-
from pymodbus.register_read_message import ReadRegistersResponseBase
91-
from pymodbus.register_write_message import WriteSingleRegisterResponse, WriteMultipleRegistersResponse
92-
from pymodbus.transaction import ModbusSocketFramer
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
9395
from pymodbus.exceptions import NotImplementedException
96+
from pymodbus.datastore.store import ModbusSparseDataBlock
9497

9598
if __name__ == "__main__" and __package__ is None:
9699
# Ensure that importing works (whether cpppo installed or not) with:
@@ -107,7 +110,8 @@
107110
import cpppo
108111

109112
from cpppo.remote.pymodbus_fixes import (
110-
modbus_sparse_data_block, modbus_server_rtu, modbus_rtu_framer_collecting, modbus_server_tcp )
113+
modbus_sparse_data_block, modbus_server_rtu_printing, modbus_server_tcp_printing, Defaults
114+
)
111115

112116

113117
#---------------------------------------------------------------------------#
@@ -291,10 +295,10 @@ def register_context( registers, slaves=None ):
291295
hrd = definitions.get( 'hr' )
292296
ird = definitions.get( 'ir' )
293297
store = ModbusSlaveContext(
294-
di = modbus_sparse_data_block( did ) if did else None,
295-
co = modbus_sparse_data_block( cod ) if cod else None,
296-
hr = modbus_sparse_data_block( hrd ) if hrd else None,
297-
ir = modbus_sparse_data_block( ird ) if ird else None )
298+
di = ModbusSparseDataBlock( did ) if did else None,
299+
co = ModbusSparseDataBlock( cod ) if cod else None,
300+
hr = ModbusSparseDataBlock( hrd ) if hrd else None,
301+
ir = ModbusSparseDataBlock( ird ) if ird else None )
298302

299303
# If slaves is None, then just pass the store with single=True; it will be
300304
# used for every slave. Otherwise, map all the specified slave IDs to the
@@ -313,7 +317,7 @@ def register_context( registers, slaves=None ):
313317

314318

315319
# Global 'context'; The caller of 'main' may want a separate Thread to be able
316-
# to access/modify the data store...
320+
# to access/modify the data store of their single Start...ServerLogging instance
317321
context = None
318322

319323
#---------------------------------------------------------------------------#
@@ -322,49 +326,53 @@ def register_context( registers, slaves=None ):
322326
# - passes any remaining keywords to underlying Modbus...Server (eg.
323327
# serial port parameters)
324328
#---------------------------------------------------------------------------#
325-
def StartTcpServerLogging( registers, identity=None, framer=ModbusSocketFramer, address=None,
326-
slaves=None, **kwds ):
327-
''' A factory to start and run a Modbus/TCP server
329+
class StartAsyncServer( object ):
330+
'''An asyncio program to start and run an eg. Modbus/TCP server. Runs
331+
forever, or until its instance .stop is invoked, which attempts a graceful
332+
shutdown of the asynio event loop. Exactly one may be run in a Thread.
328333
329334
:param registers: The register ranges (and optional values) to serve
330335
:param identity: An optional identify structure
331336
:param address: An optional (interface, port) to bind to.
332337
:param slaves: An optional single (or list of) Slave IDs to serve
338+
333339
'''
334-
global context
335-
context = register_context( registers, slaves=slaves )
336-
server = modbus_server_tcp( context, framer, identity, address,
337-
**kwds )
338-
# Print the address successfully bound; this is useful, if attempts are made
339-
# to bind over a range of ports. If the port is dynamic, we must use the
340-
# socket.getsockname() result , which is available on .server_address.
341-
print( "Success; Started Modbus/TCP Simulator; PID = %d; address = %s:%s" % (
342-
os.getpid(), server.server_address[0], server.server_address[1] ))
343-
sys.stdout.flush()
344-
server.serve_forever()
345-
346-
347-
def StartRtuServerLogging( registers, identity=None, framer=modbus_rtu_framer_collecting,
348-
address=None, slaves=None, **kwds ):
349-
'''A factory to start and run a Modbus/RTU server
340+
def __init__( self, *args, registers=None, slaves=None, **kwds ):
341+
global context
342+
self.context = context = register_context( registers, slaves=slaves )
343+
asyncio.run( self.server_async( *args, **kwds ))
344+
345+
346+
class StartTcpServerLogging( StartAsyncServer ):
347+
348+
async def server_async( self, identity=None, framer=FramerType.SOCKET, address=None, **kwds ):
349+
logging.info( "Starting Modbus TCP/IP Server on {address}".format( address=address ))
350+
server = modbus_server_tcp_printing(
351+
address = address,
352+
context = self.context,
353+
framer = framer,
354+
identity = identity,
355+
**kwds
356+
)
350357

351-
:param registers: The register ranges (and optional values) to serve
352-
:param identity: An optional identify structure
353-
:param address: An optional serial port device to bind to (passes 'address' as 'port').
354-
:param slaves: An optional single (or list of) Slave IDs to serve
358+
with suppress(asyncio.exceptions.CancelledError):
359+
await server.serve_forever()
355360

356361

357-
'''
358-
global context
359-
context = register_context( registers, slaves=slaves )
360-
server = modbus_server_rtu( context, framer, identity, port=address,
361-
**kwds )
362+
class StartRtuServerLogging( StartAsyncServer ):
363+
364+
async def server_async( self, identity=None, framer=FramerType.RTU, address=None, **kwds ):
365+
logging.info( "Starting Modbus Serial Server on {address}".format( address=address ))
366+
server = modbus_server_rtu_printing(
367+
port = address,
368+
context = self.context,
369+
framer = framer,
370+
identity = identity,
371+
**kwds
372+
)
362373

363-
# Print the address successfully bound; a serial device in this case.
364-
print( "Success; Started Modbus/RTU Simulator; PID = %d; address = %s" % (
365-
os.getpid(), address ))
366-
sys.stdout.flush()
367-
server.serve_forever()
374+
with suppress(asyncio.exceptions.CancelledError):
375+
await server.serve_forever()
368376

369377

370378
def main( argv=None ):
@@ -441,16 +449,31 @@ def main( argv=None ):
441449
# Deduce interface:port to bind, and correct types. Interface defaults to
442450
# '' (INADDR_ANY) if only :port is supplied. Port defaults to 502 if only
443451
# interface is supplied. After this block, 'address' is always a tuple like
444-
# ("interface",502). If '/', then start a Modbus/RTU serial server,
445-
# otherwise a Modbus/TCP network server. Create an address_sequence
446-
# yielding all the relevant target addresses we might need to try.
452+
# ("interface",502). If the device address is a file, then start a
453+
# Modbus/RTU serial server, otherwise a Modbus/TCP network server. Create
454+
# an address_sequence yielding all the relevant target addresses we might
455+
# need to try.
447456

448457
# We must initialize 'framer' here (even if its the same as the 'starter'
449458
# default), because we may make an Evil...() derived class below...
450459
starter_kwds = {}
451-
if args.address.startswith( '/' ):
460+
try:
461+
# See if it's an <interface>[:<port>]. If it has '/' in it, assume its a device
462+
assert '/' not in address
463+
starter = StartTcpServerLogging
464+
framer = FramerType.SOCKET
465+
address = cpppo.parse_ip_port( args.address, default=(None,Defaults.Port) )
466+
address = str(address[0]),int(address[1])
467+
log.info( "--server '%s' produces address=%r", args.address, address )
468+
address_sequence = (
469+
(address[0],port)
470+
for port in range( address[1], address[1] + int( args.range ))
471+
)
472+
except Exception as exc:
473+
# Not an address[:port]; assume it's a serial port
474+
logging.debug( "Not an interface address: {exc}".format( exc=exc ))
452475
starter = StartRtuServerLogging
453-
framer = modbus_rtu_framer_collecting
476+
framer = FramerType.RTU
454477
try:
455478
import serial
456479
except ImportError:
@@ -470,33 +493,23 @@ def main( argv=None ):
470493
address_sequence = [ args.address ]
471494
assert args.range == 1, \
472495
"A range of serial ports is unsupported"
473-
else:
474-
starter = StartTcpServerLogging
475-
framer = ModbusSocketFramer
476-
address = args.address.split(':')
477-
assert 1 <= len( address ) <= 2
478-
address = (
479-
str( address[0] ),
480-
int( address[1] ) if len( address ) > 1 else Defaults.Port )
481-
log.info( "--server '%s' produces address=%r", args.address, address )
482-
address_sequence = (
483-
(address[0],port)
484-
for port in range( address[1], address[1] + int( args.range ))
485-
)
496+
497+
logging.info( "Modbus Framer: {framer}".format( framer=framer ))
498+
framer = FRAMER_NAME_TO_CLASS[framer]
486499

487500
#---------------------------------------------------------------------------#
488501
# Evil Framers, manipulate packets resulting from underlying Framers
489502
#---------------------------------------------------------------------------#
490503
if args.evil == "truncate":
491504

492505
class EvilFramerTruncateResponse( framer ):
493-
def buildPacket(self, message):
506+
def buildFrame(self, message):
494507
''' Creates a *truncated* ready to send modbus packet. Truncates from 1
495508
to all of the bytes, before returning response.
496509
497510
:param message: The populated request/response to send
498511
'''
499-
packet = super( EvilFramerTruncateResponse, self ).buildPacket( message )
512+
packet = super( EvilFramerTruncateResponse, self ).buildFrame( message )
500513
datalen = len( packet )
501514
corrlen = datalen - random.randint( 1, datalen )
502515

@@ -512,12 +525,12 @@ def buildPacket(self, message):
512525
class EvilFramerDelayResponse( framer ):
513526
delay = 5
514527

515-
def buildPacket(self, message):
528+
def buildFrame(self, message):
516529
''' Creates a ready to send modbus packet but delays the return.
517530
518531
:param message: The populated request/response to send
519532
'''
520-
packet = super( EvilFramerDelayResponse, self ).buildPacket( message )
533+
packet = super( EvilFramerDelayResponse, self ).buildFrame( message )
521534

522535
log.info( "Delaying response for %s seconds", self.delay )
523536
delay = self.delay
@@ -546,7 +559,7 @@ def buildPacket(self, message):
546559
class EvilFramerCorruptResponse( framer ):
547560
what = "transaction"
548561

549-
def buildPacket(self, message):
562+
def buildFrame(self, message):
550563
''' Creates a *corrupted* ready to send modbus packet. Truncates from 1
551564
to all of the bytes, before returning response.
552565
@@ -561,7 +574,7 @@ def buildPacket(self, message):
561574

562575
if self.what == "transaction":
563576
message.transaction_id ^= 0xFFFF
564-
packet = super( EvilFramerCorruptResponse, self ).buildPacket( message )
577+
packet = super( EvilFramerCorruptResponse, self ).buildFrame( message )
565578
message.transaction_id ^= 0xFFFF
566579
elif self.what == "registers":
567580
if isinstance( message, ReadRegistersResponseBase ):
@@ -572,32 +585,32 @@ def buildPacket(self, message):
572585
message.registers += [999]
573586
else:
574587
message.registers = message.registers[:-1]
575-
packet = super( EvilFramerCorruptResponse, self ).buildPacket( message )
588+
packet = super( EvilFramerCorruptResponse, self ).buildFrame( message )
576589
message.registers = saveregs
577590
elif isinstance( message, WriteSingleRegisterResponse ):
578591
# Flip the responses address bits and then flip them back.
579592
message.address ^= 0xFFFF
580-
packet = super( EvilFramerCorruptResponse, self ).buildPacket( message )
593+
packet = super( EvilFramerCorruptResponse, self ).buildFrame( message )
581594
message.address ^= 0xFFFF
582595
elif isinstance( message, WriteMultipleRegistersResponse ):
583596
# Flip the responses address bits and then flip them back.
584597
message.address ^= 0xFFFF
585-
packet = super( EvilFramerCorruptResponse, self ).buildPacket( message )
598+
packet = super( EvilFramerCorruptResponse, self ).buildFrame( message )
586599
message.address ^= 0xFFFF
587600
else:
588601
raise NotImplementedException(
589602
"Unhandled class for register corruption; not implemented" )
590603
elif self.what == "protocol":
591604
message.protocol_id ^= 0xFFFF
592-
packet = super( EvilFramerCorruptResponse, self ).buildPacket( message )
605+
packet = super( EvilFramerCorruptResponse, self ).buildFrame( message )
593606
message.protocol_id ^= 0xFFFF
594607
elif self.what == "unit":
595608
message.unit_id ^= 0xFF
596-
packet = super( EvilFramerCorruptResponse, self ).buildPacket( message )
609+
packet = super( EvilFramerCorruptResponse, self ).buildFrame( message )
597610
message.unit_id ^= 0xFF
598611
elif self.what == "function":
599612
message.function_code ^= 0xFF
600-
packet = super( EvilFramerCorruptResponse, self ).buildPacket( message )
613+
packet = super( EvilFramerCorruptResponse, self ).buildFrame( message )
601614
message.function_code ^= 0xFF
602615
else:
603616
raise NotImplementedException(
@@ -635,6 +648,7 @@ def buildPacket(self, message):
635648
for k in sorted( starter_kwds.keys() ):
636649
log.info( "config: %24s: %s", k, starter_kwds[k] )
637650
starter( registers=args.registers, framer=framer, address=address, **starter_kwds )
651+
return 0
638652
except KeyboardInterrupt:
639653
return 1
640654
except Exception:

remote/plc_modbus.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@
3636
import traceback
3737

3838
from .. import misc
39-
from .pymodbus_fixes import modbus_client_timeout, modbus_client_tcp
39+
from .pymodbus_fixes import modbus_client_timeout, modbus_client_tcp, Defaults
4040
from .plc import poller, PlcOffline
4141

42-
from pymodbus.constants import Defaults
42+
4343
from pymodbus.exceptions import ModbusException, ParameterException
44-
from pymodbus.bit_read_message import ReadDiscreteInputsRequest, ReadCoilsRequest
45-
from pymodbus.bit_write_message import WriteSingleCoilRequest, WriteMultipleCoilsRequest
46-
from pymodbus.register_read_message import ReadHoldingRegistersRequest, ReadInputRegistersRequest
47-
from pymodbus.register_write_message import WriteSingleRegisterRequest, WriteMultipleRegistersRequest
48-
from pymodbus.pdu import ExceptionResponse, ModbusResponse
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
48+
from pymodbus.pdu import ExceptionResponse, ModbusPDU as ModbusResponse
4949

5050
log = logging.getLogger( __package__ )
5151

0 commit comments

Comments
 (0)