Skip to content

Commit

Permalink
Rought implementation of Modbus/TCP polling, and version 1.9.5
Browse files Browse the repository at this point in the history
  • Loading branch information
pjkundert committed Feb 13, 2014
1 parent 469dcf9 commit a041402
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 11 deletions.
134 changes: 134 additions & 0 deletions bin/modbus_poll.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env python
'''
modbus_poll.py -- A Modbus PLC Poller
Polls the specified registers from the target PLC, logging data changes.
OPTIONS
--address <addr>[:port] Address to bind to (default all, port 502)
--reach N Combine ranges of registers up N registers distant from each-other
<begin>-<end> Ranges of registers to serve, and their initial value(s)
EXAMPLE
modbus_poll.py --address localhost:7502 --reach 10 40001-40100 40120-40150
Starts a simulated PLC serving Holding registers 40001-40100 == 0, on port 7502
on interface 'localhost', which delays all responses for 2.5 seconds.
'''
import os
import sys
import traceback
import random
import time
import socket
import struct
import logging
import argparse

import cpppo
from cpppo.remote.plc_modbus import (poller_modbus, Defaults)

#---------------------------------------------------------------------------#
# configure the service logging
#---------------------------------------------------------------------------#

log = logging.getLogger( 'modbus_poll' )


def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog = """\
Register range(s) and value(s) must be supplied:
<begin>[-<end>]
EXAMPLE
modbus_poll --address localhost:7502 40001-40100
""" )
parser.add_argument( '-v', '--verbose',
default=0, action="count", help="Display logging information." )
parser.add_argument('-l', '--log',
type=str, default=None, help="Direct log output to the specified file" )
parser.add_argument( '-a', '--address', default="0.0.0.0:502",
help="Default [interface][:port] to bind to (default: any, port 502)" )
parser.add_argument( '-r', '--reach', default=1,
help="Merge polls within <reach> registers of each-other" )
parser.add_argument( '-R', '--rate', default=1.0,
help="Target poll rate" )
parser.add_argument( 'registers', nargs="+" )
args = parser.parse_args()

# Deduce logging level and target file (if any)
levelmap = {
0: logging.WARNING,
1: logging.NORMAL,
2: logging.DETAIL,
3: logging.INFO,
4: logging.DEBUG,
}
cpppo.log_cfg['level'] = ( levelmap[args.verbose]
if args.verbose in levelmap
else logging.DEBUG )
if args.log:
cpppo.log_cfg['filename'] = args.log
logging.basicConfig( **cpppo.log_cfg )

# (INADDR_ANY) if only :port is supplied. Port defaults to 502 if only
# interface is supplied. After this block, 'address' is always a tuple
# like ("interface",502)
address = None
if args.address:
address = args.address.split(':')
assert 1 <= len( address ) <= 2
address = (
str( address[0] ),
int( address[1] ) if len( address ) > 1 else Defaults.Port )
log.info( "--address '%s' produces address=%r" % ( args.address, address ))

# Start the PLC poller

poller = poller_modbus(
"Modbus/TCP", host=address[0], port=address[1], reach=int( args.reach ), rate=float( args.rate ))


for r in args.registers:
rng = r.split('-')
beg,cnt = int(rng[0]), int(rng[1])-int(rng[0])+1 if len(rng) else 1
for reg in range( beg, beg+cnt ):
poller.poll( reg )

load = ''
fail = ''
poll = ''
regs = {}
while True:
loadcur = "%.2f" % ( poller.load[0] if poller.load[0] else 0 )
if loadcur != load:
load = loadcur
logging.detail( "load: %s", loadcur )
failcur = ", ".join( [ ("%d-%d" % (b,b+c-1)) for b,c in poller.failing ] )
pollcur = ", ".join( [ ("%d-%d" % (b,b+c-1)) for b,c in poller.polling ] )
if ( failcur != fail or pollcur != poll ):
fail, poll = failcur, pollcur
logging.normal( "failing: %s, polling: %s", fail, poll )
# log data changes
for beg,cnt in poller.polling:
for reg in range( beg, beg+cnt ):
val = poller.read( reg )
old = regs.get( reg ) # may be None
if val != old:
logging.warning( "%5d == %5d (was: %s)" %( reg, val, old ))
regs[reg] = val

time.sleep( 1 )

if __name__ == "__main__":
sys.exit( main() )
15 changes: 7 additions & 8 deletions remote/plc_modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,16 @@ def service_actions( self ):
pass

class ModbusTcpClientTimeout( ModbusTcpClient ):
"""
Enforces a strict timeout on a complete transaction, including connection and I/O. The
beginning of a transaction is indicated by assigning a timeout to the transaction property (if
None, uses Defaults.Timeout). At any point, the remaining time available is computed by
accessing the transaction property.
"""Enforces a strict timeout on a complete transaction, including connection and I/O. The
beginning of a transaction is indicated by assigning a timeout to the transaction property. At
any point, the remaining time available is computed by accessing the transaction property.
If transaction is never set or set to None, Defaults.Timeout is always applied to every I/O
If .timeout is set to True/0, uses Defaults.Timeout around the entire transaction. If
transaction is never set or set to None, Defaults.Timeout is always applied to every I/O
operation, independently (the original behaviour).
Otherwise, the specified non-zero timeout is applied to the transaction; if set to 0 or simply
True, Defaults.Timeout is used.
Otherwise, the specified non-zero timeout is applied to the entire transaction.
"""
def __init__( self, *args, **kwargs):
super( ModbusTcpClientTimeout, self ).__init__( *args, **kwargs )
Expand Down
11 changes: 9 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
"cpppo/bin": "./bin",
},
entry_points = {
'console_scripts': ['modbus_sim = cpppo.bin.modbus_sim:main'],
'console_scripts': [
'modbus_sim = cpppo.bin.modbus_sim:main',
'modbus_poll = cpppo.bin.modbus_poll:main',
],
},
include_package_data = True,
author = "Perry Kundert",
Expand All @@ -44,7 +47,11 @@
create Python programs which can parse requests in this protocol (eg. as a
server, to implement something like a simulated Controller) or originate
requests in this protocol (eg. as a client, sending commands to a
Controller).""",
Controller).
In addition, the ability to read, write and poll remote PLCs of
various types including Modbus/TCP is provided.
""",
license = "Dual License; GPLv3 and Proprietary",
keywords = "cpppo protocol parser DFA",
url = "https://github.com/pjkundert/cpppo",
Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.9.4"
__version__ = "1.9.5"

0 comments on commit a041402

Please sign in to comment.