Skip to content

Commit 8f620ca

Browse files
committed
Merge branch 'feature-ops-parser'; Version 4.4.2
2 parents 4ebbe95 + a405d90 commit 8f620ca

File tree

6 files changed

+63
-7
lines changed

6 files changed

+63
-7
lines changed

README.org

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,58 @@
14391439
sys.exit( 1 )
14401440
#+END_EXAMPLE
14411441

1442+
**** Forcing .read/write to use CIP Get/Set Attribute Single
1443+
1444+
If the target device understands only basic CIP I/O requests, or you wish to perform special
1445+
processing on the stream of operations generated from the supplied tags, you can specify the
1446+
=proxy= with a specific =operations_parser=. The default is to use =client.parse_operations=
1447+
if data types are provided, and =get_attribute.attribute_operations= otherwise.
1448+
1449+
If we know we want to generate EtherNet/IP CIP Get/Set Attribute requests but we wish to pass
1450+
specific data types (eg. =INT=), create the =proxy_simple= device w/ an appropriate
1451+
=operations_parser= parameter:
1452+
1453+
#+BEGIN_EXAMPLE python
1454+
#
1455+
# Basic CIP I/O Test
1456+
#
1457+
# Target Simulator:
1458+
# python3 -m cpppo.server.enip -S -vv SCADA@0x4/0x96/3=INT[18]
1459+
#
1460+
import cpppo
1461+
from cpppo.server.enip.get_attribute import (
1462+
attribute_operations, proxy_simple as device )
1463+
from cpppo.server.enip import client
1464+
1465+
import logging
1466+
cpppo.log_cfg['level'] = logging.DEBUG
1467+
logging.basicConfig( **cpppo.log_cfg )
1468+
1469+
hostname = 'localhost'
1470+
1471+
# Our target CIP Attribute contains a 36 bytes == 18 x INT value
1472+
attribute = '@0x4/0x96/3'
1473+
values = "512,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0"
1474+
operations = [attribute + ' = (INT)' + values]
1475+
print( "Raw operations: %r" % operations )
1476+
1477+
operations_parser = attribute_operations
1478+
operations_out = list( operations_parser( operations ))
1479+
1480+
assert operations_out == [{
1481+
'method': 'set_attribute_single',
1482+
'path': [{'class': 4}, {'instance': 150}, {'attribute': 3}],
1483+
'tag_type': 195,
1484+
'data': [512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
1485+
'elements': 18
1486+
}]
1487+
1488+
# Force basic CIP Get/Set Attribute I/O operations
1489+
via = device( hostname, operations_parser=operations_parser )
1490+
val, = via.write( operations )
1491+
1492+
#+END_EXAMPLE
1493+
14421494
**** Using the =proxy= Context Manager API
14431495

14441496
There is a simple mechanism provided to ensure that all of the above

README.pdf

-34.3 KB
Binary file not shown.

server/enip/device.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ def parse_path( path, elm=None ):
325325
def parse_path_elements( path, elm=None, cnt=None ):
326326
"""Returns (<path>,<element>,<count>). If an element is specified (eg. Tag[#]), then it will be
327327
added to the path (or replace any existing element segment at the end of the path) and returned,
328-
otherwise None will be returned. If a count is specified (eg. Tag[#-#] or ...*#), then it will be
329-
returned; otherwise a None will be returned.
328+
otherwise None will be returned. If a count is specified (eg. Tag[#-#] or ...*#), then it will
329+
be returned; otherwise a None will be returned.
330330
331331
Any "."-separated EPATH component (except the last) including an element index must specify
332332
exactly None/one element, eg: "Tag.SubTag[5].AnotherTag[3-4]".

server/enip/get_attribute.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def attribute_operations( paths, int_type=None, **kwds ):
107107
op['method'] = 'set_attribute_single' if 'data' in op else 'get_attribute_single'
108108
else:
109109
raise AssertionError( "Path invalid for Attribute services: %r", op['path'] )
110+
log.detail( "CIP Operation: %s", parser.enip_format( op ))
110111
yield op
111112

112113

@@ -245,7 +246,8 @@ def parameter_substitution( self, iterable, parameters=None, pass_thru=None ):
245246
def __init__( self, host, port=44818, timeout=None, depth=None, multiple=None,
246247
gateway_class=None, route_path=None, send_path=None,
247248
priority_time_tick=None, timeout_ticks=None,
248-
identity_default=None, dialect=None, **gateway_kwds ):
249+
identity_default=None, dialect=None, operations_parser=None,
250+
**gateway_kwds ):
249251
"""Capture the desired I/O parameters for the target CIP Device.
250252
251253
By default, the CIP Device will be identified using a List Identity request each time a CIP
@@ -273,6 +275,7 @@ def __init__( self, host, port=44818, timeout=None, depth=None, multiple=None,
273275
self.identity_default = identity_default
274276
self.identity = identity_default
275277
self.dialect = dialect
278+
self.operations_parser = operations_parser
276279

277280
def __str__( self ):
278281
return "%s at %s" % ( self.identity.product_name if self.identity else None, self.gateway )
@@ -545,7 +548,8 @@ def opp__att_typ_uni( i ):
545548
# No conversion of data type if None; use a Read Tag [Fragmented]; works only
546549
# for [S]STRING/SINT/INT/DINT/REAL/BOOL. Otherwise, conversion of data type
547550
# desired; get raw data using Get Attribute Single.
548-
parser = client.parse_operations if typ is None else attribute_operations
551+
parser = self.operations_parser or ( client.parse_operations if typ is None
552+
else attribute_operations )
549553
opp, = parser( ( att, ), route_path=device.parse_route_path( self.route_path ),
550554
send_path=self.send_path, priority_time_tick=self.priority_time_tick,
551555
timeout_ticks=self.timeout_ticks )

server/enip/parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,7 @@ class EPATH( dfa ):
863863
'element': 0x28,
864864
'port': 0x00,
865865
}
866+
866867
def __init__( self, name=None, **kwds ):
867868
name = name or kwds.setdefault( 'context', self.__class__.__name__ )
868869

@@ -1147,8 +1148,7 @@ def produce( cls, data ):
11471148
assert False, "Invalid value for numeric EPATH segment %r == %d: %r" % (
11481149
segnam, segval, data )
11491150
break
1150-
if not found:
1151-
assert False, "Invalid EPATH segment %r found in %r" % ( segnam, data )
1151+
assert found, "Invalid EPATH segment %r found in %r" % ( segnam, data )
11521152
assert len( result ) % 2 == 0, \
11531153
"Failed to retain even EPATH word length after %r in %r" % ( segnam, data )
11541154

version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version_info__ = ( 4, 4, 1 )
1+
__version_info__ = ( 4, 4, 2 )
22
__version__ = '.'.join( map( str, __version_info__ ))

0 commit comments

Comments
 (0)