Exemple #1
0
def logix_test_once( obj, req ):
    req_source			= cpppo.peekable( req )
    req_data 			= cpppo.dotdict()
    with obj.parser as machine:
        for m,s in machine.run( source=req_source, data=req_data ):
            pass
    if log.isEnabledFor( logging.NORMAL ):
        log.normal( "Logix Request parsed: %s", enip.enip_format( req_data ))
    
    # If we ask a Logix Object to process the request, it should respond.
    processed			= obj.request( req_data )
    if log.isEnabledFor( logging.NORMAL ):
        log.normal( "Logix Request processed: %s", enip.enip_format( req_data ))

    # And, the same object should be able to parse the request's generated reply
    rpy_source			= cpppo.peekable( bytes( req_data.input ))
    rpy_data			= cpppo.dotdict()
    with obj.parser as machine:
        for i,(m,s) in enumerate( machine.run( source=rpy_source, data=rpy_data )):
            if log.isEnabledFor( logging.INFO ):
                log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                          i, s, rpy_source.sent, rpy_source.peek(), rpy_data )

    if log.isEnabledFor( logging.NORMAL ):
        log.normal( "Logix Reply   processed: %s", enip.enip_format( rpy_data ))

    return processed,req_data,rpy_data
Exemple #2
0
def logix_test_once(obj, req):
    req_source = cpppo.peekable(req)
    req_data = cpppo.dotdict()
    with obj.parser as machine:
        for m, s in machine.run(source=req_source, data=req_data):
            pass
    if log.isEnabledFor(logging.NORMAL):
        log.normal("Logix Request parsed: %s", enip.enip_format(req_data))

    # If we ask a Logix Object to process the request, it should respond.
    processed = obj.request(req_data)
    if log.isEnabledFor(logging.NORMAL):
        log.normal("Logix Request processed: %s", enip.enip_format(req_data))

    # And, the same object should be able to parse the request's generated reply
    rpy_source = cpppo.peekable(bytes(req_data.input))
    rpy_data = cpppo.dotdict()
    with obj.parser as machine:
        for i, (m,
                s) in enumerate(machine.run(source=rpy_source, data=rpy_data)):
            if log.isEnabledFor(logging.INFO):
                log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                         m.name_centered(), i, s, rpy_source.sent,
                         rpy_source.peek(), rpy_data)

    if log.isEnabledFor(logging.NORMAL):
        log.normal("Logix Reply   processed: %s", enip.enip_format(rpy_data))

    return processed, req_data, rpy_data
Exemple #3
0
    def test_once():
        source = cpppo.peekable(req_1)
        data = cpppo.dotdict()
        with Obj.parser as machine:
            for m, w in machine.run(source=source, data=data):
                pass
        log.normal("Logix Request parsed: %s", enip.enip_format(data))

        # If we ask a Logix Object to process the request, it should respond.
        processed = Obj.request(data)
        log.normal("Logix Request processed: %s", enip.enip_format(data))
        return processed, data
Exemple #4
0
 def test_once():
     source			= cpppo.peekable( req_1 )
     data 			= cpppo.dotdict()
     with Obj.parser as machine:
         for m,w in machine.run( source=source, data=data ):
             pass
     log.normal( "Logix Request parsed: %s", enip.enip_format( data ))
     
     # If we ask a Logix Object to process the request, it should respond.
     processed		= Obj.request( data )
     log.normal( "Logix Request processed: %s", enip.enip_format( data ))
     return processed, data
def search_lan(address, broadcast=True, timeout=1.0):
    """Discover the number of CIP chassis and backplane devices in the address/network."""
    chassis, devices = 0, 0
    for target in list_identity(address=address,
                                broadcast=broadcast,
                                timeout=timeout):
        identity_object = target.enip.CIP.list_identity.CPF.item[
            0].identity_object
        chassis += 1
        print(enip.enip_format(identity_object))
        print()
        print("%-32s @ %s:%s" %
              (identity_object.product_name, identity_object.sin_addr,
               identity_object.sin_port))
        for route_path, module in scan_backplane(
                address=(identity_object.sin_addr, identity_object.sin_port)):
            print("  %24s: %s" % (route_path, module))
            if route_path:
                print("  Slot %3s: %-32s (Ser. #%s)" %
                      (route_path[0]['link'], str(module[6]), module[5]))
            else:
                # No route_path; Must be a simple non-routing device (eg. MicroLogix)
                print("          : %-32s (Ser. #%s)" %
                      (str(module[6]), module[5]))

            devices += 1
    return chassis, devices
Exemple #6
0
def test_hart_pass_thru_simulated(simulated_hart_gateway):
    """Simulated HART I/O card; always returns Pass-thru Init handle 99 (won't work on a real device)"""
    command, address = simulated_hart_gateway
    #address			= ('127.0.0.1',44818) # If you run: python3 ./hart_test.py

    try:
        assert address, "Unable to detect HART EtherNet/IP CIP Gateway IP address"
        hio = client.connector(host=address[0], port=address[1])

        operations = [
            {
                "method": "service_code",
                "code": HART.PT_INI_REQ,
                "data": [1, 0],  # HART: Read primary variable
                "data_size":
                4 + 2,  # Known response size: command,status,<payload>
                "path": '@0x%X/8' %
                (HART.class_id),  # Instance 1-8 ==> HART Channel 0-7
            },
            {
                "method": "service_code",
                "code": HART.PT_QRY_REQ,
                "data": [99],  # HART: Pass-thru Query handle
                "data_size": 4 +
                5,  # Known response size: 5 (units + 4-byte real in network order)
                "path": '@0x%X/8' %
                (HART.class_id),  # Instance 1-8 ==> HART Channel 0-7
            },
        ]

        # Now, use the underlying client.connector to issue a HART "Read Dynamic Variable" Service Code
        cmdbuf = ''
        with hio:
            results = []
            failures = 0
            for idx, dsc, req, rpy, sts, val in hio.pipeline(
                    operations=client.parse_operations(operations),
                    **hart_kwds):
                log.normal("Client %s: %s --> %r: %s", hio, dsc, val,
                           enip.enip_format(rpy))
                if not val:
                    log.warning(
                        "Client %s harvested %d/%d results; failed request: %s",
                        hio, len(results), len(operations), rpy)
                    failures += 1
                results.append((dsc, val, rpy))
                #cmdbuf		= command_logging( command, cmdbuf )

            # assert failures == 0 # statuses represent HART I/O status, not CIP response status
            assert results[0][-1].init.status in (
                32, 33, 35)  # 32 busy, 33 initiated, 35 device offline
            assert results[1][-1].query.status in (
                0, 34, 35)  # 0 success, 34 running, 35 dead

    except Exception as exc:
        log.warning("Test terminated with exception: %s", exc)
        raise
Exemple #7
0
    def unconnected_send(self, request, route_path=None, send_path=None, timeout=None):
        if route_path is None:
            # Default to the CPU in chassis (link 0), port 1
            route_path = [{"link": 0, "port": 1}]
        if send_path is None:
            # Default to the Connection Manager
            send_path = [{"class": 6}, {"instance": 1}]
        assert isinstance(request, dict)

        data = cpppo.dotdict()
        data.enip = {}
        data.enip.session_handle = self.session
        data.enip.options = 0
        data.enip.status = 0
        data.enip.sender_context = {}
        data.enip.sender_context.input = bytearray([0x00] * 8)
        data.enip.CIP = {}
        data.enip.CIP.send_data = {}

        sd = data.enip.CIP.send_data
        sd.interface = 0
        sd.timeout = 0
        sd.CPF = {}
        sd.CPF.item = [cpppo.dotdict(), cpppo.dotdict()]
        sd.CPF.item[0].type_id = 0
        sd.CPF.item[1].type_id = 178
        sd.CPF.item[1].unconnected_send = {}

        us = sd.CPF.item[1].unconnected_send
        us.service = 82
        us.status = 0
        us.priority = 5
        us.timeout_ticks = 157
        us.path = {"segment": [cpppo.dotdict(d) for d in send_path]}
        us.route_path = {"segment": [cpppo.dotdict(d) for d in route_path]}

        us.request = request

        log.detail("Client Unconnected Send: %s", enip.enip_format(data))

        us.request.input = bytearray(logix.Logix.produce(us.request))
        sd.input = bytearray(enip.CPF.produce(sd.CPF))
        data.enip.input = bytearray(enip.CIP.produce(data.enip))
        data.input = bytearray(enip.enip_encode(data.enip))

        self.send(data.input, timeout=timeout)
        return data
Exemple #8
0
def main(argv=None):
    """Read the specified tag(s).  Pass the desired argv (excluding the program
    name in sys.arg[0]; typically pass argv=None, which is equivalent to
    argv=sys.argv[1:], the default for argparse.  Requires at least one tag to
    be defined.

    """
    ap = argparse.ArgumentParser(description="An EtherNet/IP Client",
                                 epilog="")

    ap.add_argument('-v',
                    '--verbose',
                    default=0,
                    action="count",
                    help="Display logging information.")
    ap.add_argument(
        '-a',
        '--address',
        default=("%s:%d" % enip.address),
        help="EtherNet/IP interface[:port] to connect to (default: %s:%d)" %
        (enip.address[0], enip.address[1]))
    ap.add_argument('-l', '--log', help="Log file, if desired")
    ap.add_argument('-t',
                    '--timeout',
                    default=5.0,
                    help="EtherNet/IP timeout (default: 5s)")
    ap.add_argument('-r',
                    '--repeat',
                    default=1,
                    help="Repeat EtherNet/IP request (default: 1)")
    ap.add_argument('tags',
                    nargs="+",
                    help="Any tags to read/write, eg: SCADA[1]")

    args = ap.parse_args(argv)

    addr = args.address.split(':')
    assert 1 <= len(
        addr
    ) <= 2, "Invalid --address [<interface>]:[<port>}: %s" % args.address
    addr = (str(addr[0]) if addr[0] else enip.address[0],
            int(addr[1]) if len(addr) > 1 and addr[1] else enip.address[1])

    # Set up logging level (-v...) and --log <file>
    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)

    timeout = float(args.timeout)
    repeat = int(args.repeat)

    begun = misc.timer()
    cli = client(host=addr[0], port=addr[1])
    assert cli.writable(timeout=timeout)
    elapsed = misc.timer() - begun
    log.normal("Client Connected in  %7.3f/%7.3fs" % (elapsed, timeout))

    # Register, and harvest EtherNet/IP Session Handle
    begun = misc.timer()
    request = cli.register(timeout=timeout)
    elapsed = misc.timer() - begun
    log.normal("Client Register Sent %7.3f/%7.3fs: %s" %
               (elapsed, timeout, enip.enip_format(request)))
    data = None  # In case nothing is returned by cli iterable
    for data in cli:
        elapsed = misc.timer() - begun
        log.detail("Client Register Resp %7.3f/%7.3fs: %s" %
                   (elapsed, timeout, enip.enip_format(data)))
        if data is None:
            if elapsed <= timeout:
                cli.readable(timeout=timeout - elapsed)
                continue
        break
    elapsed = misc.timer() - begun
    log.normal("Client Register Rcvd %7.3f/%7.3fs: %s" %
               (elapsed, timeout, enip.enip_format(data)))
    assert data is not None, "Failed to receive any response"
    assert 'enip.status' in data, "Failed to receive EtherNet/IP response"
    assert data.enip.status == 0, "EtherNet/IP response indicates failure: %s" % data.enip.status
    assert 'enip.CIP.register' in data, "Failed to receive Register response"

    cli.session = data.enip.session_handle

    # Parse each EtherNet/IP Tag Read or Write; only write operations will have 'data'
    #     TAG[0] 		read 1 value index 0 (default)
    #     TAG[1-5]		read 5 values from indices 1 to 5
    #     TAG[4-7]=1,2,3,4	write 4 values from indices 4 to 7

    operations = []
    for tag in args.tags:
        # Compute tag, elm, end and cnt (default elm is 0, cnt is 1)
        val = ''
        if '=' in tag:
            tag, val = tag.split('=', 1)
        if '[' in tag:
            tag, elm = tag.split('[', 1)
            elm, _ = elm.split(']')
            end = elm
            if '-' in elm:
                elm, end = elm.split('-')
            elm, end = int(elm), int(end)
        else:
            elm, end = 0, 0
        cnt = end + 1 - elm
        opr = {
            'path': [{
                'symbolic': tag
            }, {
                'element': elm
            }],
            'elements': cnt,
        }
        if val:
            if '.' in val:
                opr['tag_type'] = enip.REAL.tag_type
                cast = lambda x: float(x)
            else:
                opr['tag_type'] = enip.INT.tag_type
                cast = lambda x: int(x)
            # Allow an optional (TYPE)value,value,...
            if ')' in val:

                def int_validate(x, lo, hi):
                    res = int(x)
                    assert lo <= res <= hi, "Invalid %d; not in range (%d,%d)" % (
                        res, lo, hi)
                    return res

                typ, val = val.split(')')
                _, typ = typ.split('(')
                opr['tag_type'], cast = {
                    'REAL': (enip.REAL.tag_type, lambda x: float(x)),
                    'DINT': (enip.DINT.tag_type,
                             lambda x: int_validate(x, -2**31, 2**31 - 1)),
                    'INT': (enip.INT.tag_type,
                            lambda x: int_validate(x, -2**15, 2**15 - 1)),
                    'SINT': (enip.SINT.tag_type,
                             lambda x: int_validate(x, -2**7, 2**7 - 1)),
                }[typ.upper()]
            opr['data'] = list(map(cast, val.split(',')))

            assert len( opr['data'] ) == cnt, \
                "Number of data values (%d) doesn't match element count (%d): %s=%s" % (
                    len( opr['data'] ), cnt, tag, val )
        operations.append(opr)

    # Perform all specified tag operations, the specified number of repeat times.  Doesn't handle
    # fragmented reads yet.  If any operation fails, return a non-zero exit status.
    status = 0
    start = misc.timer()
    for i in range(repeat):
        for op in operations:  # {'path': [...], 'elements': #}
            begun = misc.timer()
            if 'data' in op:
                descr = "Write Frag"
                request = cli.write(offset=0, timeout=timeout, **op)
            else:
                descr = "Read  Frag"
                request = cli.read(offset=0, timeout=timeout, **op)
            elapsed = misc.timer() - begun
            log.normal("Client %s Sent %7.3f/%7.3fs: %s" %
                       (descr, elapsed, timeout, enip.enip_format(request)))
            response = None
            for response in cli:
                elapsed = misc.timer() - begun
                log.normal(
                    "Client %s Resp %7.3f/%7.3fs: %s" %
                    (descr, elapsed, timeout, enip.enip_format(response)))
                if response is None:
                    if elapsed <= timeout:
                        cli.readable(timeout=timeout - elapsed)
                        continue
                break
            elapsed = misc.timer() - begun
            log.normal("Client %s Rcvd %7.3f/%7.3fs: %s" %
                       (descr, elapsed, timeout, enip.enip_format(response)))
            tag = op['path'][0]['symbolic']
            elm = op['path'][1]['element']
            cnt = op['elements']
            val = []  # data values read/written
            res = None  # result of request
            act = "??"  # denotation of request action

            try:
                # The response should contain either an status code (possibly with an extended
                # status), or the read_frag request's data.  Remember; a successful response may
                # carry read_frag.data, but report a status == 6 indicating that more data remains
                # to return via a subsequent fragmented read request.
                request = response.enip.CIP.send_data.CPF.item[
                    1].unconnected_send.request
                if 'read_frag' in request:
                    act = "=="
                    val = request.read_frag.data
                elif 'write_frag' in request:
                    act = "<="
                    val = op['data']
                if not request.status:
                    res = "OK"
                else:
                    res = "Status %d %s" % (request.status,
                                            repr(request.status_ext.data)
                                            if 'status_ext' in request and
                                            request.status_ext.size else "")
                if request.status:
                    if not status:
                        status = request.status
                    log.warning("Client %s returned non-zero status: %s",
                                descr, res)

            except AttributeError as exc:
                status = 1
                res = "Client %s Response missing data: %s" % (descr, exc)
            except Exception as exc:
                status = 1
                res = "Client %s Exception: %s" % exc

            log.warning("%10s[%5d-%-5d] %s %r: %r" %
                        (tag, elm, elm + cnt - 1, act, val, res))

    duration = misc.timer() - start
    log.warning("Client ReadFrg. Average %7.3f TPS (%7.3fs ea)." %
                (repeat / duration, duration / repeat))
    return status
Exemple #9
0
def test_logix_multiple():
    """Test the Multiple Request Service.  Ensure multiple requests can be successfully handled, and
    invalid tags are correctly rejected.

    The Logix is a Message_Router instance, and is expected to be at Class 2, Instance 1.  Eject any
    non-Logix Message_Router that presently exist.

    """
    Obj				= enip.device.lookup( enip.device.Message_Router.class_id, instance_id=1 )
    if not isinstance( Obj, logix.Logix ):
        if Obj is not None:
            del enip.device.directory['2']['1']
        Obj			= logix.Logix( instance_id=1 )

    # Create some Attributes to test, but mask the big ones from Get Attributes All.
    size			= 1000
    Obj_a1 = Obj.attribute['1']	= enip.device.Attribute( 'parts',       enip.parser.DINT, default=[n for n in range( size )],
                                                         mask=enip.device.Attribute.MASK_GA_ALL )
    Obj_a2 = Obj.attribute['2']	= enip.device.Attribute( 'ControlWord', enip.parser.DINT, default=[0,0])
    Obj_a3 = Obj.attribute['3']	= enip.device.Attribute( 'SCADA_40001', enip.parser.INT,  default=[n for n in range( size )],
                                                         mask=enip.device.Attribute.MASK_GA_ALL )
    Obj_a4 = Obj.attribute['4']	= enip.device.Attribute( 'number',      enip.parser.REAL, default=0.0)

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['parts']	= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':1 }
    enip.device.symbol['ControlWord'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':2 }
    enip.device.symbol['SCADA_40001'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':3 }
    enip.device.symbol['number'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':4 }


    assert len( Obj_a1 ) == size
    assert len( Obj_a3 ) == size
    assert len( Obj_a4 ) == 1
    Obj_a1[0]			= 42
    Obj_a2[0]			= 476
    Obj_a4[0]			= 1.0
    # Ensure that the basic CIP Object requests work on a derived Class.
    for description,original,produced,parsed,result,response in GA_tests:
        request			= cpppo.dotdict( original )

        log.warning( "%s; request: %s", description, enip.enip_format( request ))
        encoded			= Obj.produce( request )
        assert encoded == produced, "%s: Didn't produce correct encoded request: %r != %r" % (
            description, encoded, produced )

        # Now, use the Message_Router's parser to decode the encoded bytes
        source			= cpppo.rememberable( encoded )
        decoded			= cpppo.dotdict()
        with Obj.parser as machine:
            for m,s in enumerate( machine.run( source=source, data=decoded )):
                pass
        for k,v in cpppo.dotdict( parsed ).items():
            assert decoded[k] == v, "%s: Didn't parse expected value: %s != %r in %s" % (
                description, k, v, enip.enip_format( decoded ))

        # Process the request into a reply, and ensure we get the expected result (some Attributes
        # are filtered from Get Attributes All; only a 2-element DINT array and a single REAL should
        # be produced)
        Obj.request( request )
        logging.warning("%s: reply:   %s", description, enip.enip_format( request ))
        for k,v in cpppo.dotdict( result ).items():
            assert k in request and request[k] == v, \
                "%s: Didn't result in expected response: %s != %r; got %r" % (
                    description, k, v, request[k] if k in request else "(not found)" )

        # Finally, produce the encoded response
        encoded			= Obj.produce( request )
        assert encoded == response, "%s: Didn't produce correct encoded response: %r != %r" % (
            description, encoded, response )


    # Test that we correctly compute beg,end,endactual for various Read Tag Fragmented scenarios,
    # with 2-byte and 4-byte types.  For the purposes of this test, we only look at path...elements.
    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.read_frag		= {}
    data.read_frag.elements	= 1000
    data.read_frag.offset	= 0
    
    # Reply maximum size limited
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 0 and end == 125 and endactual == 1000 # DINT == 4 bytes
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 250 and endactual == 1000 # INT == 2 bytes

    data.read_frag.offset	= 125*4 # OK, second request; begin after byte offset of first
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 125 and end == 250 and endactual == 1000 # DINT == 4 bytes

    # Request elements limited; 0 offset
    data.read_frag.elements	= 30
    data.read_frag.offset	= 0
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 30 and endactual == 30 # INT == 2 bytes

    # Request elements limited; +'ve offset
    data.read_frag.elements	= 70
    data.read_frag.offset	= 80
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 40 and end == 70 and endactual == 70 # INT == 2 bytes

    # Request limited by size of data provided (Write Tag [Fragmented])
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.write_frag		= {}
    data.write_frag.data	= [0] * 100 # 100 elements provided in this request
    data.write_frag.elements	= 200       # Total request is to write 200 elements
    data.write_frag.offset	= 16        # request starts 16 bytes in (8 INTs)
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8 and end == 108 and endactual == 200 # INT == 2 bytes

    # ... same, but lets say request started somewhere in the middle of the array
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 222 },
                                                       ]] }
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8+222 and end == 108+222 and endactual == 200+222 # INT == 2 bytes

    # Ensure correct computation of (beg,end] that are byte-offset and data/size limited
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [] }

    data.write_frag		= {}
    data.write_frag.data	= [3,4,5,6]
    data.write_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 3 and end == 7 and endactual == 1000 # INT == 2 bytes

    # Trigger the error cases only accessible via write

    # Too many elements provided for attribute capacity
    data.write_frag.offset	= ( 1000 - 3 ) * 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'write_frag' )
        assert False, "Should have raised Exception due to capacity"
    except Exception as exc:
        assert "capacity exceeded" in str( exc )

    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [] }

    data.read_frag		= {}
    data.read_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 3 and end == 253 and endactual == 1000 # INT == 2 bytes

    # And we should be able to read with an offset right up to the last element
    data.read_frag.offset	= 1998
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 999 and end == 1000 and endactual == 1000 # INT == 2 bytes


    # Trigger all the remaining error cases

    # Unknown service
    data.service		= Obj.RD_FRG_REQ
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to service"
    except Exception as exc:
        assert "unknown service" in str( exc )

    # Offset indivisible by element size
    data.service		= Obj.RD_FRG_RPY
    data.read_frag.offset	= 7
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to odd byte offset"
    except Exception as exc:
        assert "element boundary" in str( exc )

    # Initial element outside bounds
    data.read_frag.offset	= 2000
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to initial element"
    except Exception as exc:
        assert "initial element invalid" in str( exc )

    # Ending element outside bounds
    data.read_frag.offset	= 0
    data.read_frag.elements	= 1001
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element"
    except Exception as exc:
        assert "ending element invalid" in str( exc )

    # Beginning element after ending (should be no way to trigger).  This request doesn't specify an
    # element in the path, hence defaults to element 0, and asks for a number of elements == 2.
    # Thus, there is no 6-byte offset possible (a 2-byte offset is, though).
    data.read_frag.offset	= 6
    data.read_frag.elements	= 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element order"
    except Exception as exc:
        assert "ending element before beginning" in str( exc )
    data.read_frag.offset	= 2
    data.read_frag.elements	= 2
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 1 and end == 2 and endactual == 2 # INT == 2 bytes


    # Test an example valid multiple request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request	= [
        cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict() ] 
    req				= data.multiple.request

    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'parts'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'ControlWord'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1

    req[2].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[2].read_tag		= {}
    req[2].read_tag.elements	= 1

    req[3].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[3].write_tag		= {}
    req[3].write_tag.elements	= 1
    req[3].write_tag.type	= 0x00ca
    req[3].write_tag.data	= [1.25]

    req[4].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[4].read_tag		= {}
    req[4].read_tag.elements	= 1


    request			= Obj.produce( data )

    req_1			= bytes(bytearray([
        0x0A,
        0x02,
        0x20, 0x02, 0x24, 0x01,
        
        0x05, 0x00,
        
        0x0c, 0x00,
        0x18, 0x00,
        0x2a, 0x00,
        0x36, 0x00,
        0x48, 0x00,
        
        0x4C,
        0x04, 0x91, 0x05, 0x70, 0x61,
        0x72, 0x74, 0x73, 0x00,
        0x01, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0B, 0x43, 0x6F,
        0x6E, 0x74, 0x72, 0x6F, 0x6C,
        0x57, 0x6F, 0x72, 0x64, 0x00,
        0x01, 0x00,

        b'L'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0x01, 0x00,

        b'M'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0xca, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa0, 0x3f,

        b'L'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0x01, 0x00,
    ]))
                       
    assert request == req_1, \
        "Unexpected result from Multiple Request Service; got: \n%r\nvs.\n%r " % ( request, req_1 )

    # Now, use the Message_Router's parser
    source			= cpppo.rememberable( request )
    data			= cpppo.dotdict()
    with Obj.parser as machine:
        for i,(m,s) in enumerate( machine.run( source=source, data=data )):
            pass
    log.normal( "Multiple Request: %s", enip.enip_format( data ))
    assert 'multiple' in data, \
        "No parsed multiple found in data: %s" % enip.enip_format( data )
    assert data.service == enip.device.Message_Router.MULTIPLE_REQ, \
        "Expected a Multiple Request Service request: %s" % enip.enip_format( data )
    assert data.multiple.number == 5, \
        "Expected 5 requests in request.multiple: %s" % enip.enip_format( data )

    # And ensure if we re-encode the parsed result, we get the original encoded request back
    assert Obj.produce( data ) == req_1

    # Process the request into a reply.
    Obj.request( data )
    log.normal( "Multiple Response: %s", enip.enip_format( data ))
    assert data.service == enip.device.Message_Router.MULTIPLE_RPY, \
        "Expected a Multiple Request Service reply: %s" % enip.enip_format( data )

    rpy_1			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x05, 0x00,

        0x0c, 0x00,
        0x16, 0x00,
        0x20, 0x00,
        0x2a, 0x00,
        0x2e, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0x2A, 0x00, 0x00, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0xDC, 0x01, 0x00, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xCA, 0x00, 0x00, 0x00, 0x80, 0x3F,

        0xcd, 0x00, 0x00, 0x00,

        0xcc, 0x00, 0x00, 0x00,
        0xca, 0x00, 0x00, 0x00, 0xa0, 0x3f,
    ])

    assert data.input == rpy_1, \
        "Unexpected reply from Multiple Request Service request; got: \n%r\nvs.\n%r " % ( data.input, rpy_1 )

    # Now lets try some valid and invalid requests
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_good			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x01, 0x00,
        
        0x04, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_good, \
        "Unexpected result from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( request, req_good )

    Obj.request( data )
    rpy_good			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x01, 0x00,

        0x04, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,
    ])

    assert data.input == rpy_good, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( data.input, rpy_good )

    # Add an invalid request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict(), cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40002'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_bad			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x02, 0x00,
        
        0x06, 0x00,
        0x18, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,

        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('2'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_bad, \
        "Unexpected result from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( request, req_bad )

    Obj.request( data )
    rpy_bad			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x02, 0x00,

        0x06, 0x00,
        0x0e, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,

        0xCC, 0x00, 0x05, 0x01, # Status code 0x05 (invalid path)
        0x00, 0x00,
    ])
    assert data.input == rpy_bad, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( data.input, rpy_bad )
Exemple #10
0
def test_hart_simple(simulated_hart_gateway):
    # No Multiple Service Packet supported by HART I/O Card simulator
    command, address = simulated_hart_gateway
    #address			= ("127.0.0.1", 44818)
    #address			= ("100.100.102.10", 44818)
    route_path = None
    route_path = [{'link': 2, 'port': 1}]
    try:
        assert address, "Unable to detect HART EtherNet/IP CIP Gateway IP address"
        hio = client.connector(host=address[0], port=address[1])
        # Establish an Implicit EtherNet/IP CIP connection using Forward Open
        #hio			= client.implicit( host=address[0], port=address[1], connection_path=None )
        PV = 1.23
        operations = [
            {
                "method": "service_code",
                "code": HART.RD_VAR_REQ,
                "data": [],  # No payload
                "data_size":
                4 + 36,  # Known response size: command,status,<payload>
                "path": '@0x%X/8' %
                (HART.class_id),  # Instance 1-8 ==> HART Channel 0-7
            },
            'HART_7_Data.PV = (REAL)0',  # would fail 'til first HART Read Dynamic Variable is done
            {
                "method": "service_code",
                "code": HART.RD_VAR_REQ,
                "data": [],  # No payload
                "data_size":
                4 + 36,  # Known response size: command,status,<payload>
                "path": '@0x%X/8' %
                (HART.class_id),  # Instance 1-8 ==> HART Channel 0-7
            },
            'HART_7_Data.PV = (REAL)%s' % PV,
            {
                "method": "service_code",
                "code": HART.RD_VAR_REQ,
                "data": [],  # No payload
                "data_size":
                4 + 36,  # Known response size: command,status,<payload>
                "path": '@0x%X/8' %
                (HART.class_id),  # Instance 1-8 ==> HART Channel 0-7
            },
        ]

        # Now, use the underlying client.connector to issue a HART "Read Dynamic Variable" Service Code
        simout = ''
        with hio:
            results = []
            failures = 0
            for idx, dsc, req, rpy, sts, val in hio.pipeline(
                    operations=client.parse_operations(operations,
                                                       route_path=route_path),
                    **hart_kwds):
                log.normal("Client %s: %s --> %r: %s", hio, dsc, val,
                           enip.enip_format(rpy))
                if not val:
                    log.warning(
                        "Client %s harvested %d/%d results; failed request: %s",
                        hio, len(results), len(operations), rpy)
                    failures += 1
                results.append((dsc, val, rpy))

            rpylast = results[-1][-1]
            assert failures in (0, 1)
            assert near(rpylast.read_var.PV, PV)

    except Exception as exc:
        log.warning("Test terminated with exception: %s", exc)
        raise
Exemple #11
0
def test_logix_multiple():
    """Test the Multiple Request Service.  Ensure multiple requests can be successfully handled, and
    invalid tags are correctly rejected.

    The Logix is a Message_Router instance, and is expected to be at Class 2, Instance 1.  Eject any
    non-Logix Message_Router that presently exist.

    """
    enip.lookup_reset()  # Flush out any existing CIP Objects for a fresh start
    Obj = logix.Logix(instance_id=1)

    # Create some Attributes to test, but mask the big ones from Get Attributes All.
    size = 1000
    Obj_a1 = Obj.attribute['1'] = enip.device.Attribute(
        'parts',
        enip.parser.DINT,
        default=[n for n in range(size)],
        mask=enip.device.Attribute.MASK_GA_ALL)
    Obj_a2 = Obj.attribute['2'] = enip.device.Attribute('ControlWord',
                                                        enip.parser.DINT,
                                                        default=[0, 0])
    Obj_a3 = Obj.attribute['3'] = enip.device.Attribute(
        'SCADA_40001',
        enip.parser.INT,
        default=[n for n in range(size)],
        mask=enip.device.Attribute.MASK_GA_ALL)
    Obj_a4 = Obj.attribute['4'] = enip.device.Attribute('number',
                                                        enip.parser.REAL,
                                                        default=0.0)

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['parts'] = {
        'class': Obj.class_id,
        'instance': Obj.instance_id,
        'attribute': 1
    }
    enip.device.symbol['ControlWord'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':2 }
    enip.device.symbol['SCADA_40001'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':3 }
    enip.device.symbol['number'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':4 }

    assert len(Obj_a1) == size
    assert len(Obj_a3) == size
    assert len(Obj_a4) == 1
    Obj_a1[0] = 42
    Obj_a2[0] = 476
    Obj_a4[0] = 1.0
    # Ensure that the basic CIP Object requests work on a derived Class.
    for description, original, produced, parsed, result, response in GA_tests:
        request = cpppo.dotdict(original)

        log.warning("%s; request: %s", description, enip.enip_format(request))
        encoded = Obj.produce(request)
        assert encoded == produced, "%s: Didn't produce correct encoded request: %r != %r" % (
            description, encoded, produced)

        # Now, use the Message_Router's parser to decode the encoded bytes
        source = cpppo.rememberable(encoded)
        decoded = cpppo.dotdict()
        with Obj.parser as machine:
            for m, s in enumerate(machine.run(source=source, data=decoded)):
                pass
        for k, v in cpppo.dotdict(parsed).items():
            assert decoded[
                k] == v, "%s: Didn't parse expected value: %s != %r in %s" % (
                    description, k, v, enip.enip_format(decoded))

        # Process the request into a reply, and ensure we get the expected result (some Attributes
        # are filtered from Get Attributes All; only a 2-element DINT array and a single REAL should
        # be produced)
        Obj.request(request)
        logging.warning("%s: reply:   %s", description,
                        enip.enip_format(request))
        for k, v in cpppo.dotdict(result).items():
            assert k in request and request[k] == v, \
                "%s: Didn't result in expected response: %s != %r; got %r" % (
                    description, k, v, request[k] if k in request else "(not found)" )

        # Finally, produce the encoded response
        encoded = Obj.produce(request)
        assert encoded == response, "%s: Didn't produce correct encoded response: %r != %r" % (
            description, encoded, response)

    # Test that we correctly compute beg,end,endactual for various Read Tag Fragmented scenarios,
    # with 2-byte and 4-byte types.  For the purposes of this test, we only look at path...elements.
    data = cpppo.dotdict()
    data.service = Obj.RD_FRG_RPY
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 0
            },
        ]]
    }
    data.read_frag = {}
    data.read_frag.elements = 1000
    data.read_frag.offset = 0

    # Reply maximum size limited
    beg, end, endactual = Obj.reply_elements(Obj_a1, data, 'read_frag')
    assert beg == 0 and end == 125 and endactual == 1000  # DINT == 4 bytes
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 0 and end == 250 and endactual == 1000  # INT == 2 bytes

    data.read_frag.offset = 125 * 4  # OK, second request; begin after byte offset of first
    beg, end, endactual = Obj.reply_elements(Obj_a1, data, 'read_frag')
    assert beg == 125 and end == 250 and endactual == 1000  # DINT == 4 bytes

    # Request elements limited; 0 offset
    data.read_frag.elements = 30
    data.read_frag.offset = 0
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 0 and end == 30 and endactual == 30  # INT == 2 bytes

    # Request elements limited; +'ve offset
    data.read_frag.elements = 70
    data.read_frag.offset = 80
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 40 and end == 70 and endactual == 70  # INT == 2 bytes

    # Request limited by size of data provided (Write Tag [Fragmented])
    data = cpppo.dotdict()
    data.service = Obj.WR_FRG_RPY
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 0
            },
        ]]
    }
    data.write_frag = {}
    data.write_frag.data = [0] * 100  # 100 elements provided in this request
    data.write_frag.elements = 200  # Total request is to write 200 elements
    data.write_frag.offset = 16  # request starts 16 bytes in (8 INTs)
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 8 and end == 108 and endactual == 200  # INT == 2 bytes

    # ... same, but lets say request started somewhere in the middle of the array
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 222
            },
        ]]
    }
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 8 + 222 and end == 108 + 222 and endactual == 200 + 222  # INT == 2 bytes

    # Ensure correct computation of (beg,end] that are byte-offset and data/size limited
    data = cpppo.dotdict()
    data.service = Obj.WR_FRG_RPY
    data.path = {'segment': []}

    data.write_frag = {}
    data.write_frag.data = [3, 4, 5, 6]
    data.write_frag.offset = 6
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 3 and end == 7 and endactual == 1000  # INT == 2 bytes

    # Trigger the error cases only accessible via write

    # Too many elements provided for attribute capacity
    data.write_frag.offset = (1000 - 3) * 2
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
        assert False, "Should have raised Exception due to capacity"
    except Exception as exc:
        assert "capacity exceeded" in str(exc)

    data = cpppo.dotdict()
    data.service = Obj.RD_FRG_RPY
    data.path = {'segment': []}

    data.read_frag = {}
    data.read_frag.offset = 6
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 3 and end == 253 and endactual == 1000  # INT == 2 bytes

    # And we should be able to read with an offset right up to the last element
    data.read_frag.offset = 1998
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 999 and end == 1000 and endactual == 1000  # INT == 2 bytes

    # Trigger all the remaining error cases

    # Unknown service
    data.service = Obj.RD_FRG_REQ
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to service"
    except Exception as exc:
        assert "unknown service" in str(exc)

    # Offset indivisible by element size
    data.service = Obj.RD_FRG_RPY
    data.read_frag.offset = 7
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to odd byte offset"
    except Exception as exc:
        assert "element boundary" in str(exc)

    # Initial element outside bounds
    data.read_frag.offset = 2000
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to initial element"
    except Exception as exc:
        assert "initial element invalid" in str(exc)

    # Ending element outside bounds
    data.read_frag.offset = 0
    data.read_frag.elements = 1001
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to ending element"
    except Exception as exc:
        assert "ending element invalid" in str(exc)

    # Beginning element after ending (should be no way to trigger).  This request doesn't specify an
    # element in the path, hence defaults to element 0, and asks for a number of elements == 2.
    # Thus, there is no 6-byte offset possible (a 2-byte offset is, though).
    data.read_frag.offset = 6
    data.read_frag.elements = 2
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to ending element order"
    except Exception as exc:
        assert "ending element before beginning" in str(exc)
    data.read_frag.offset = 2
    data.read_frag.elements = 2
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 1 and end == 2 and endactual == 2  # INT == 2 bytes

    # Test an example valid multiple request
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = [
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict()
    ]
    req = data.multiple.request

    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'parts'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1

    req[1].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'ControlWord'
        }]]
    }
    req[1].read_tag = {}
    req[1].read_tag.elements = 1

    req[2].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[2].read_tag = {}
    req[2].read_tag.elements = 1

    req[3].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[3].write_tag = {}
    req[3].write_tag.elements = 1
    req[3].write_tag.type = 0x00ca
    req[3].write_tag.data = [1.25]

    req[4].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[4].read_tag = {}
    req[4].read_tag.elements = 1

    request = Obj.produce(data)

    req_1 = bytes(
        bytearray([
            0x0A,
            0x02,
            0x20,
            0x02,
            0x24,
            0x01,
            0x05,
            0x00,
            0x0c,
            0x00,
            0x18,
            0x00,
            0x2a,
            0x00,
            0x36,
            0x00,
            0x48,
            0x00,
            0x4C,
            0x04,
            0x91,
            0x05,
            0x70,
            0x61,
            0x72,
            0x74,
            0x73,
            0x00,
            0x01,
            0x00,
            0x4C,
            0x07,
            0x91,
            0x0B,
            0x43,
            0x6F,
            0x6E,
            0x74,
            0x72,
            0x6F,
            0x6C,
            0x57,
            0x6F,
            0x72,
            0x64,
            0x00,
            0x01,
            0x00,
            b'L'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0x01,
            0x00,
            b'M'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0xca,
            0x00,
            0x01,
            0x00,
            0x00,
            0x00,
            0xa0,
            0x3f,
            b'L'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0x01,
            0x00,
        ]))

    assert request == req_1, \
        "Unexpected result from Multiple Request Service; got: \n%r\nvs.\n%r " % ( request, req_1 )

    # Now, use the Message_Router's parser
    source = cpppo.rememberable(request)
    data = cpppo.dotdict()
    with Obj.parser as machine:
        for i, (m, s) in enumerate(machine.run(source=source, data=data)):
            pass
    log.normal("Multiple Request: %s", enip.enip_format(data))
    assert 'multiple' in data, \
        "No parsed multiple found in data: %s" % enip.enip_format( data )
    assert data.service == enip.device.Message_Router.MULTIPLE_REQ, \
        "Expected a Multiple Request Service request: %s" % enip.enip_format( data )
    assert data.multiple.number == 5, \
        "Expected 5 requests in request.multiple: %s" % enip.enip_format( data )

    # And ensure if we re-encode the parsed result, we get the original encoded request back
    assert Obj.produce(data) == req_1

    # Process the request into a reply.
    Obj.request(data)
    log.normal("Multiple Response: %s", enip.enip_format(data))
    assert data.service == enip.device.Message_Router.MULTIPLE_RPY, \
        "Expected a Multiple Request Service reply: %s" % enip.enip_format( data )

    rpy_1 = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x05,
        0x00,
        0x0c,
        0x00,
        0x16,
        0x00,
        0x20,
        0x00,
        0x2a,
        0x00,
        0x2e,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC4,
        0x00,
        0x2A,
        0x00,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC4,
        0x00,
        0xDC,
        0x01,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xCA,
        0x00,
        0x00,
        0x00,
        0x80,
        0x3F,
        0xcd,
        0x00,
        0x00,
        0x00,
        0xcc,
        0x00,
        0x00,
        0x00,
        0xca,
        0x00,
        0x00,
        0x00,
        0xa0,
        0x3f,
    ])

    assert data.input == rpy_1, \
        "Unexpected reply from Multiple Request Service request; got: \n%r\nvs.\n%r " % ( data.input, rpy_1 )

    # Now lets try some valid and invalid requests
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = req = [cpppo.dotdict()]
    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40001'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1
    data.multiple.number = len(data.multiple.request)

    request = Obj.produce(data)

    req_good = bytearray([
        0x0A,
        0x02,
        0x20,
        0x02,
        ord('$'),
        0x01,
        0x01,
        0x00,
        0x04,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('1'),
        0x00,
        0x01,
        0x00,
    ])
    assert request == req_good, \
        "Unexpected result from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( request, req_good )

    Obj.request(data)
    rpy_good = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x01,
        0x00,
        0x04,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC3,
        0x00,
        0x00,
        0x00,
    ])

    assert data.input == rpy_good, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( data.input, rpy_good )

    # Add an invalid request
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = req = [cpppo.dotdict(), cpppo.dotdict()]
    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40001'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1
    req[1].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40002'
        }]]
    }
    req[1].read_tag = {}
    req[1].read_tag.elements = 1
    data.multiple.number = len(data.multiple.request)

    request = Obj.produce(data)

    req_bad = bytearray([
        0x0A,
        0x02,
        0x20,
        0x02,
        ord('$'),
        0x01,
        0x02,
        0x00,
        0x06,
        0x00,
        0x18,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('1'),
        0x00,
        0x01,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('2'),
        0x00,
        0x01,
        0x00,
    ])
    assert request == req_bad, \
        "Unexpected result from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( request, req_bad )

    Obj.request(data)
    rpy_bad = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x02,
        0x00,
        0x06,
        0x00,
        0x0e,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC3,
        0x00,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x05,
        0x01,  # Status code 0x05 (invalid path)
        0x00,
        0x00,
    ])
    assert data.input == rpy_bad, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( data.input, rpy_bad )
Exemple #12
0
"""python -m cpppo.server.enip.list_identity_simple <hostname>

Returns any List Identity responses from the given hostname or IP address (default:
255.255.255.255), received before timeout (default: 1.0 second) expires.

"""

from __future__ import print_function

import sys

from cpppo.server import enip
from cpppo.server.enip import client

timeout			= 1.0
host			= sys.argv[1] if sys.argv[1:] else '255.255.255.255'
with client.client( host=host, udp=True, broadcast=True ) as conn:
    conn.list_identity( timeout=timeout )
    while True:
        response,elapsed= client.await( conn, timeout=timeout )
        if response:
            print( enip.enip_format( response ))
        else:
            break # No response (None) w'in timeout or EOF ({})
Exemple #13
0
def test_logix_multiple():
    """Test the Multiple Request Service.  Ensure multiple requests can be successfully handled, and
    invalid tags are correctly rejected.

    """
    size			= 1000
    Obj				= logix.Logix()
    Obj_a1 = Obj.attribute['1']	= enip.device.Attribute( 'parts',       enip.parser.DINT, default=[n for n in range( size )])
    Obj_a2 = Obj.attribute['2']	= enip.device.Attribute( 'ControlWord', enip.parser.DINT, default=[n for n in range( size )])
    Obj_a3 = Obj.attribute['3']	= enip.device.Attribute( 'SCADA_40001', enip.parser.INT,  default=[n for n in range( size )])

    assert len( Obj_a1 ) == size
    assert len( Obj_a2 ) == size
    Obj_a1[0]			= 42
    Obj_a2[0]			= 476

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['parts']	= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':1 }
    enip.device.symbol['ControlWord'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':2 }
    enip.device.symbol['SCADA_40001'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':3 }

    # Test that we correctly compute beg,end,endactual for various Read Tag Fragmented scenarios,
    # with 2-byte and 4-byte types.  For the purposes of this test, we only look at path...elements.
    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.read_frag		= {}
    data.read_frag.elements	= 1000
    data.read_frag.offset	= 0
    
    # Reply maximum size limited
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 0 and end == 125 and endactual == 1000 # DINT == 4 bytes
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 250 and endactual == 1000 # INT == 2 bytes

    data.read_frag.offset	= 125*4 # OK, second request; begin after byte offset of first
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 125 and end == 250 and endactual == 1000 # DINT == 4 bytes

    # Request elements limited; 0 offset
    data.read_frag.elements	= 30
    data.read_frag.offset	= 0
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 30 and endactual == 30 # INT == 2 bytes

    # Request elements limited; +'ve offset
    data.read_frag.elements	= 70
    data.read_frag.offset	= 80
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 40 and end == 70 and endactual == 70 # INT == 2 bytes

    # Request limited by size of data provided (Write Tag [Fragmented])
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.write_frag		= {}
    data.write_frag.data	= [0] * 100 # 100 elements provided in this request
    data.write_frag.elements	= 200       # Total request is to write 200 elements
    data.write_frag.offset	= 16        # request starts 16 bytes in (8 INTs)
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8 and end == 108 and endactual == 200 # INT == 2 bytes

    # ... same, but lets say request started somewhere in the middle of the array
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 222 },
                                                       ]] }
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8+222 and end == 108+222 and endactual == 200+222 # INT == 2 bytes

    # Ensure correct computation of (beg,end] that are byte-offset and data/size limited
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [] }

    data.write_frag		= {}
    data.write_frag.data	= [3,4,5,6]
    data.write_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 3 and end == 7 and endactual == 1000 # INT == 2 bytes

    # Trigger the error cases only accessible via write

    # Too many elements provided for attribute capacity
    data.write_frag.offset	= ( 1000 - 3 ) * 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'write_frag' )
        assert False, "Should have raised Exception due to capacity"
    except Exception as exc:
        assert "capacity exceeded" in str( exc )

    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [] }

    data.read_frag		= {}
    data.read_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 3 and end == 253 and endactual == 1000 # INT == 2 bytes

    # And we should be able to read with an offset right up to the last element
    data.read_frag.offset	= 1998
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 999 and end == 1000 and endactual == 1000 # INT == 2 bytes


    # Trigger all the remaining error cases

    # Unknown service
    data.service		= Obj.RD_FRG_REQ
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to service"
    except Exception as exc:
        assert "unknown service" in str( exc )

    # Offset indivisible by element size
    data.service		= Obj.RD_FRG_RPY
    data.read_frag.offset	= 7
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to odd byte offset"
    except Exception as exc:
        assert "element boundary" in str( exc )

    # Initial element outside bounds
    data.read_frag.offset	= 2000
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to initial element"
    except Exception as exc:
        assert "initial element invalid" in str( exc )

    # Ending element outside bounds
    data.read_frag.offset	= 0
    data.read_frag.elements	= 1001
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element"
    except Exception as exc:
        assert "ending element invalid" in str( exc )

    # Beginning element after ending (should be no way to trigger).  This request doesn't specify an
    # element in the path, hence defaults to element 0, and asks for a number of elements == 2.
    # Thus, there is no 6-byte offset possible (a 2-byte offset is, though).
    data.read_frag.offset	= 6
    data.read_frag.elements	= 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element order"
    except Exception as exc:
        assert "ending element before beginning" in str( exc )
    data.read_frag.offset	= 2
    data.read_frag.elements	= 2
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 1 and end == 2 and endactual == 2 # INT == 2 bytes


    # Test an example valid multiple request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request	= [ cpppo.dotdict(), cpppo.dotdict() ]
    req				= data.multiple.request

    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'parts'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'ControlWord'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1

    request			= Obj.produce( data )

    req_1			= bytes(bytearray([
        0x0A,
        0x02,
        0x20, 0x02, 0x24, 0x01,
        
        0x02, 0x00,
        
        0x06, 0x00,
        0x12, 0x00,
        
        0x4C,
        0x04, 0x91, 0x05, 0x70, 0x61,
        0x72, 0x74, 0x73, 0x00,
        0x01, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0B, 0x43, 0x6F,
        0x6E, 0x74, 0x72, 0x6F, 0x6C,
        0x57, 0x6F, 0x72, 0x64, 0x00,
        0x01, 0x00,
    ]))
                       
    assert request == req_1, \
        "Unexpected result from Multiple Request Service; got: \n%r\nvs.\n%r " % ( request, req_1 )

    # Now, use the Message_Router's parser
    source			= cpppo.rememberable( request )
    data			= cpppo.dotdict()
    with Obj.parser as machine:
        for i,(m,s) in enumerate( machine.run( source=source, data=data )):
            pass
    log.normal( "Multiple Request: %s", enip.enip_format( data ))
    assert 'multiple' in data, \
        "No parsed multiple found in data: %s" % enip.enip_format( data )
    assert data.service == device.Message_Router.MULTIPLE_REQ, \
        "Expected a Multiple Request Service request: %s" % enip.enip_format( data )
    assert data.multiple.number == 2, \
        "Expected 2 requests in request.multiple: %s" % enip.enip_format( data )

    # And ensure if we re-encode the parsed result, we get the original encoded request back
    assert Obj.produce( data ) == req_1

    # Process the request into a reply.
    Obj.request( data )
    log.normal( "Multiple Response: %s", enip.enip_format( data ))
    assert data.service == device.Message_Router.MULTIPLE_RPY, \
        "Expected a Multiple Request Service reply: %s" % enip.enip_format( data )

    rpy_1			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x02, 0x00,

        0x06, 0x00,
        0x10, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0x2A, 0x00, 0x00, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0xDC, 0x01, 0x00, 0x00,
    ])

    assert data.input == rpy_1, \
        "Unexpected reply from Multiple Request Service request; got: \n%r\nvs.\n%r " % ( data.input, rpy_1 )

    # Now lets try some valid and invalid requests
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_good			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x01, 0x00,
        
        0x04, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_good, \
        "Unexpected result from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( request, req_good )

    Obj.request( data )
    rpy_good			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x01, 0x00,

        0x04, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,
    ])

    assert data.input == rpy_good, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( data.input, rpy_good )

    # Add an invalid request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict(), cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40002'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_bad			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x02, 0x00,
        
        0x06, 0x00,
        0x18, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,

        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('2'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_bad, \
        "Unexpected result from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( request, req_bad )

    Obj.request( data )
    rpy_bad			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x02, 0x00,

        0x06, 0x00,
        0x0e, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,

        0xCC, 0x00, 0x05, 0x01, # Status code 0x05 (invalid path)
        0x00, 0x00,
    ])
    assert data.input == rpy_bad, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( data.input, rpy_bad )
Exemple #14
0
def main(argv=None):
    """Read the specified tag(s).  Pass the desired argv (excluding the program
    name in sys.arg[0]; typically pass argv=None, which is equivalent to
    argv=sys.argv[1:], the default for argparse.  Requires at least one tag to
    be defined.

    """
    ap = argparse.ArgumentParser(
        description="An EtherNet/IP Client",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""\
One or more EtherNet/IP CIP Tags may be read or written.  The full format for
specifying a tag and an operation is:

    Tag[<first>-<last>]+<offset>=(SINT|INT|DINT|REAL)<value>,<value>

All components except Tag are optional.  Specifying a +<offset> (in bytes)
forces the use of the Fragmented command, regardless of whether --[no-]fragment
was specified.  If an element range [<first>] or [<first>-<last>] was specified
and --no-fragment selected, then the exact correct number of elements must be
provided.""",
    )

    ap.add_argument("-v", "--verbose", default=0, action="count", help="Display logging information.")
    ap.add_argument(
        "-a",
        "--address",
        default=("%s:%d" % enip.address),
        help="EtherNet/IP interface[:port] to connect to (default: %s:%d)" % (enip.address[0], enip.address[1]),
    )
    ap.add_argument("-p", "--print", default=False, action="store_true", help="Print a summary of operations to stdout")
    ap.add_argument("-l", "--log", help="Log file, if desired")
    ap.add_argument("-t", "--timeout", default=5.0, help="EtherNet/IP timeout (default: 5s)")
    ap.add_argument("-r", "--repeat", default=1, help="Repeat EtherNet/IP request (default: 1)")
    ap.add_argument(
        "-m", "--multiple", action="store_true", help="Use Multiple Service Packet request (default: False)"
    )
    ap.add_argument(
        "-f",
        "--fragment",
        dest="fragment",
        action="store_true",
        help="Use Read/Write Tag Fragmented requests (default: True)",
    )
    ap.add_argument(
        "-n",
        "--no-fragment",
        dest="fragment",
        action="store_false",
        help="Use Read/Write Tag requests (default: False)",
    )
    ap.set_defaults(fragment=False)
    ap.add_argument("tags", nargs="+", help="Tags to read/write, eg: SCADA[1], SCADA[2-10]+4=(DINT)3,4,5")

    args = ap.parse_args(argv)

    addr = args.address.split(":")
    assert 1 <= len(addr) <= 2, "Invalid --address [<interface>]:[<port>}: %s" % args.address
    addr = (
        str(addr[0]) if addr[0] else enip.address[0],
        int(addr[1]) if len(addr) > 1 and addr[1] else enip.address[1],
    )

    # Set up logging level (-v...) and --log <file>
    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)

    timeout = float(args.timeout)
    repeat = int(args.repeat)

    begun = misc.timer()
    cli = client(host=addr[0], port=addr[1])
    assert cli.writable(timeout=timeout)
    elapsed = misc.timer() - begun
    log.normal("Client Connected in  %7.3f/%7.3fs" % (elapsed, timeout))

    # Register, and harvest EtherNet/IP Session Handle
    begun = misc.timer()
    request = cli.register(timeout=timeout)
    elapsed = misc.timer() - begun
    log.detail("Client Register Sent %7.3f/%7.3fs: %s" % (elapsed, timeout, enip.enip_format(request)))
    data = None  # In case nothing is returned by cli iterable
    for data in cli:
        elapsed = misc.timer() - begun
        log.info("Client Register Resp %7.3f/%7.3fs: %s" % (elapsed, timeout, enip.enip_format(data)))
        if data is None:
            if elapsed <= timeout:
                cli.readable(timeout=timeout - elapsed)
                continue
        break
    elapsed = misc.timer() - begun
    log.detail("Client Register Rcvd %7.3f/%7.3fs: %s" % (elapsed, timeout, enip.enip_format(data)))
    assert data is not None, "Failed to receive any response"
    assert "enip.status" in data, "Failed to receive EtherNet/IP response"
    assert data.enip.status == 0, "EtherNet/IP response indicates failure: %s" % data.enip.status
    assert "enip.CIP.register" in data, "Failed to receive Register response"

    cli.session = data.enip.session_handle

    # Parse each EtherNet/IP Tag Read or Write; only write operations will have 'data'
    #     TAG	 		read 1 value (no element index)
    #     TAG[0] 		read 1 value from element index 0
    #     TAG[1-5]		read 5 values from element indices 1 to 5
    #     TAG[1-5]+4		read 5 values from element indices 1 to 5, beginning at byte offset 4
    #     TAG[4-7]=1,2,3,4	write 4 values from indices 4 to 7
    #
    # To support access to scalar attributes (no element index allowed in path), we cannot default
    # to supply an element index of 0; default is no element in path, and a data value count of 1.
    # If a byte offset is specified, the request is forced to use Read/Write Tag Fragmented
    # (regardless of whether --[no-]fragment was specified)

    operations = []
    for tag in args.tags:
        # Compute tag, elm, end and cnt (default elm is None (no element index), cnt is 1)
        val = ""
        off = None
        elm, lst = None, None
        cnt = 1
        if "=" in tag:
            # A write; strip off the values into 'val'
            tag, val = tag.split("=", 1)
        if "+" in tag:
            # A byte offset (valid for Fragmented)
            tag, off = tag.split("+", 1)
        if "[" in tag:
            tag, elm = tag.split("[", 1)
            elm, _ = elm.split("]")
            lst = elm
            if "-" in elm:
                elm, lst = elm.split("-")
            elm, lst = int(elm), int(lst)
            cnt = lst + 1 - elm

        opr = {}
        opr["path"] = [{"symbolic": tag}]
        if elm is not None:
            opr["path"] += [{"element": elm}]
        opr["elements"] = cnt
        if off:
            opr["offset"] = int(off)

        if val:
            if "." in val:
                opr["tag_type"] = enip.REAL.tag_type
                size = enip.REAL().calcsize
                cast = lambda x: float(x)
            else:
                opr["tag_type"] = enip.INT.tag_type
                size = enip.INT().calcsize
                cast = lambda x: int(x)
            # Allow an optional (TYPE)value,value,...
            if ")" in val:

                def int_validate(x, lo, hi):
                    res = int(x)
                    assert lo <= res <= hi, "Invalid %d; not in range (%d,%d)" % (res, lo, hi)
                    return res

                typ, val = val.split(")")
                _, typ = typ.split("(")
                opr["tag_type"], size, cast = {
                    "REAL": (enip.REAL.tag_type, enip.REAL().calcsize, lambda x: float(x)),
                    "DINT": (
                        enip.DINT.tag_type,
                        enip.DINT().calcsize,
                        lambda x: int_validate(x, -2 ** 31, 2 ** 31 - 1),
                    ),
                    "INT": (enip.INT.tag_type, enip.INT().calcsize, lambda x: int_validate(x, -2 ** 15, 2 ** 15 - 1)),
                    "SINT": (enip.SINT.tag_type, enip.SINT().calcsize, lambda x: int_validate(x, -2 ** 7, 2 ** 7 - 1)),
                }[typ.upper()]
            opr["data"] = list(map(cast, val.split(",")))

            if "offset" not in opr and not args.fragment:
                # Non-fragment write.  The exact correct number of data elements must be provided
                assert len(opr["data"]) == cnt, "Number of data values (%d) doesn't match element count (%d): %s=%s" % (
                    len(opr["data"]),
                    cnt,
                    tag,
                    val,
                )
            elif elm != lst:
                # Fragmented write, to an identified range of indices, hence we can check length.
                # If the byte offset + data provided doesn't match the number of elements, then a
                # subsequent Write Tag Fragmented command will be required to write the balance.
                byte = opr.get("offset") or 0
                assert byte % size == 0, "Invalid byte offset %d for elements of size %d bytes" % (byte, size)
                beg = byte // size
                end = beg + len(opr["data"])
                assert end <= cnt, (
                    "Number of elements (%d) provided and byte offset %d / %d-byte elements exceeds element count %d: "
                    % (len(opr["data"]), byte, size, cnt)
                )
                if beg != 0 or end != cnt:
                    log.normal("Partial Write Tag Fragmented; elements %d-%d of %d", beg, end - 1, cnt)
        operations.append(opr)

    def output(out):
        log.normal(out)
        if args.print:
            print(out)

    # Perform all specified tag operations, the specified number of repeat times.  Doesn't handle
    # fragmented reads yet.  If any operation fails, return a non-zero exit status.  If --multiple
    # specified, perform all operations in a single Multiple Service Packet request.

    status = 0
    start = misc.timer()
    for i in range(repeat):
        requests = []  # If --multiple, collects all requests, else one at at time
        for o in range(len(operations)):
            op = operations[o]  # {'path': [...], 'elements': #}
            begun = misc.timer()
            if "offset" not in op:
                op["offset"] = 0 if args.fragment else None
            if "data" in op:
                descr = "Write "
                req = cli.write(timeout=timeout, send=not args.multiple, **op)
            else:
                descr = "Read  "
                req = cli.read(timeout=timeout, send=not args.multiple, **op)
            descr += "Frag" if op["offset"] is not None else "Tag "
            if args.multiple:
                # Multiple requests; each request is returned simply, not in an Unconnected Send
                requests.append(req)
                if o != len(operations) - 1:
                    continue
                # No more operations!  Issue the Multiple Service Packet containing all operations
                descr = "Multiple  "
                cli.multiple(request=requests, timeout=timeout)
            else:
                # Single request issued
                requests = [req]

            # Issue the request(s), and get the response
            elapsed = misc.timer() - begun
            log.detail("Client %s Sent %7.3f/%7.3fs: %s" % (descr, elapsed, timeout, enip.enip_format(request)))
            response = None
            for response in cli:
                elapsed = misc.timer() - begun
                log.debug("Client %s Resp %7.3f/%7.3fs: %s" % (descr, elapsed, timeout, enip.enip_format(response)))
                if response is None:
                    if elapsed <= timeout:
                        cli.readable(timeout=timeout - elapsed)
                        continue
                break
            elapsed = misc.timer() - begun
            log.detail("Client %s Rcvd %7.3f/%7.3fs: %s" % (descr, elapsed, timeout, enip.enip_format(response)))

            # Find the replies in the response; could be single or multiple; should match requests!
            replies = []
            if response.enip.status != 0:
                status = 1
                output("Client %s Response EtherNet/IP status: %d" % (descr, response.enip.status))
            elif (
                args.multiple and "enip.CIP.send_data.CPF.item[1].unconnected_send.request.multiple.request" in response
            ):
                # Multiple Service Packet; request.multiple.request is an array of read/write_tag/frag
                replies = response.enip.CIP.send_data.CPF.item[1].unconnected_send.request.multiple.request
            elif "enip.CIP.send_data.CPF.item[1].unconnected_send.request" in response:
                # Single request; request is a read/write_tag/frag
                replies = [response.enip.CIP.send_data.CPF.item[1].unconnected_send.request]
            else:
                status = 1
                output("Client %s Response Unrecognized: " % (descr, enip.enip_format(response)))

            for request, reply in zip(requests, replies):
                log.detail("Client %s Request: %s", descr, enip.enip_format(request))
                log.detail("  Yields Reply: %s", enip.enip_format(reply))
                val = []  # data values read/written
                res = None  # result of request
                act = "??"  # denotation of request action
                try:
                    tag = request.path.segment[0].symbolic
                    try:
                        elm = request.path.segment[1].element  # array access
                    except IndexError:
                        elm = None  # scalar access

                    # The response should contain either an status code (possibly with an extended
                    # status), or the read_frag request's data.  Remember; a successful response may
                    # carry read_frag.data, but report a status == 6 indicating that more data remains
                    # to return via a subsequent fragmented read request.
                    if "read_frag" in reply:
                        act = "=="
                        val = reply.read_frag.data
                        cnt = request.read_frag.elements
                    elif "read_tag" in reply:
                        act = "=="
                        val = reply.read_tag.data
                        cnt = request.read_tag.elements
                    elif "write_frag" in reply:
                        act = "<="
                        val = request.write_frag.data
                        cnt = request.write_frag.elements
                    elif "write_tag" in reply:
                        act = "<="
                        val = request.write_tag.data
                        cnt = request.write_tag.elements
                    if not reply.status:
                        res = "OK"
                    else:
                        res = "Status %d %s" % (
                            reply.status,
                            repr(reply.status_ext.data) if "status_ext" in reply and reply.status_ext.size else "",
                        )
                    if reply.status:
                        if not status:
                            status = reply.status
                        log.warning("Client %s returned non-zero status: %s", descr, res)

                except AttributeError as exc:
                    status = 1
                    res = "Client %s Response missing data: %s" % (descr, exc)
                    log.detail("%s: %s", res, "".join(traceback.format_exception(*sys.exc_info())))
                except Exception as exc:
                    status = 1
                    res = "Client %s Exception: %s" % (descr, exc)
                    log.detail("%s: %s", res, "".join(traceback.format_exception(*sys.exc_info())))

                if elm is None:
                    output("%20s              %s %r: %r" % (tag, act, val, res))  # scalar access
                else:
                    output("%20s[%5d-%-5d] %s %r: %r" % (tag, elm, elm + cnt - 1, act, val, res))

    duration = misc.timer() - start
    log.normal(
        "Client Tag I/O  Average %7.3f TPS (%7.3fs ea)."
        % (repeat * len(operations) / duration, duration / repeat / len(operations))
    )
    return status
Exemple #15
0
def main( argv=None ):
    """Read the specified tag(s).  Pass the desired argv (excluding the program
    name in sys.arg[0]; typically pass argv=None, which is equivalent to
    argv=sys.argv[1:], the default for argparse.  Requires at least one tag to
    be defined.

    """
    ap				= argparse.ArgumentParser(
        description = "An EtherNet/IP Client",
        epilog = "" )

    ap.add_argument( '-v', '--verbose',
                     default=0, action="count",
                     help="Display logging information." )
    ap.add_argument( '-a', '--address',
                     default=( "%s:%d" % enip.address ),
                     help="EtherNet/IP interface[:port] to connect to (default: %s:%d)" % (
                         enip.address[0], enip.address[1] ))
    ap.add_argument( '-l', '--log',
                     help="Log file, if desired" )
    ap.add_argument( '-t', '--timeout',
                     default=5.0,
                     help="EtherNet/IP timeout (default: 5s)" )
    ap.add_argument( '-r', '--repeat',
                     default=1,
                     help="Repeat EtherNet/IP request (default: 1)" )
    ap.add_argument( 'tags', nargs="+",
                     help="Any tags to read/write, eg: SCADA[1]")

    args			= ap.parse_args( argv )

    addr			= args.address.split(':')
    assert 1 <= len( addr ) <= 2, "Invalid --address [<interface>]:[<port>}: %s" % args.address
    addr			= ( str( addr[0] ) if addr[0] else enip.address[0],
                                    int( addr[1] ) if len( addr ) > 1 and addr[1] else enip.address[1] )
    
    # Set up logging level (-v...) and --log <file>
    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 )

    timeout			= float( args.timeout )
    repeat			= int( args.repeat )

    begun			= misc.timer()
    cli				= client( host=addr[0], port=addr[1] )
    assert cli.writable( timeout=timeout )
    elapsed			= misc.timer() - begun
    log.normal( "Client Connected in  %7.3f/%7.3fs" % ( elapsed, timeout ))

    # Register, and harvest EtherNet/IP Session Handle
    begun			= misc.timer()
    request			= cli.register( timeout=timeout )
    elapsed			= misc.timer() - begun
    log.normal( "Client Register Sent %7.3f/%7.3fs: %s" % ( elapsed, timeout, enip.enip_format( request )))
    data			= None # In case nothing is returned by cli iterable
    for data in cli:
        elapsed			= misc.timer() - begun
        log.detail( "Client Register Resp %7.3f/%7.3fs: %s" % ( elapsed, timeout, enip.enip_format( data )))
        if data is None:
            if elapsed <= timeout:
                cli.readable( timeout=timeout - elapsed )
                continue
        break
    elapsed			= misc.timer() - begun
    log.normal( "Client Register Rcvd %7.3f/%7.3fs: %s" % ( elapsed, timeout, enip.enip_format( data )))
    assert data is not None, "Failed to receive any response"
    assert 'enip.status' in data, "Failed to receive EtherNet/IP response"
    assert data.enip.status == 0, "EtherNet/IP response indicates failure: %s" % data.enip.status
    assert 'enip.CIP.register' in data, "Failed to receive Register response"

    cli.session			= data.enip.session_handle
    
    # Parse each EtherNet/IP Tag Read or Write; only write operations will have 'data'
    #     TAG[0] 		read 1 value index 0 (default)
    #     TAG[1-5]		read 5 values from indices 1 to 5
    #     TAG[4-7]=1,2,3,4	write 4 values from indices 4 to 7

    operations			= []
    for tag in args.tags:
        # Compute tag, elm, end and cnt (default elm is 0, cnt is 1)
        val			= ''
        if '=' in tag:
            tag,val		= tag.split( '=', 1 )
        if '[' in tag:
            tag,elm		= tag.split( '[', 1 )
            elm,_		= elm.split( ']' )
            end			= elm
            if '-' in elm:
                elm,end		= elm.split( '-' )
            elm,end		= int(elm), int(end)
        else:
            elm,end		= 0,0
        cnt			= end + 1 - elm
        opr			= {
            'path':	[{'symbolic': tag}, {'element': elm}],
            'elements': cnt,
        }
        if val:
            if '.' in val:
                opr['tag_type']	= enip.REAL.tag_type
                cast		= lambda x: float( x )
            else:
                opr['tag_type']	= enip.INT.tag_type
                cast		= lambda x: int( x )
            # Allow an optional (TYPE)value,value,...
            if ')' in val:
                def int_validate( x, lo, hi ):
                    res		= int( x )
                    assert lo <= res <= hi, "Invalid %d; not in range (%d,%d)" % ( res, lo, hi)
                    return res
                typ,val		= val.split( ')' )
                _,typ		= typ.split( '(' )
                opr['tag_type'],cast = {
                    'REAL': 	(enip.REAL.tag_type,	lambda x: float( x )),
                    'DINT':	(enip.DINT.tag_type,	lambda x: int_validate( x, -2**31, 2**31-1 )),
                    'INT':	(enip.INT.tag_type,	lambda x: int_validate( x, -2**15, 2**15-1 )),
                    'SINT':	(enip.SINT.tag_type,	lambda x: int_validate( x, -2**7,  2**7-1 )),
                }[typ.upper()]
            opr['data']		= list( map( cast, val.split( ',' )))

            assert len( opr['data'] ) == cnt, \
                "Number of data values (%d) doesn't match element count (%d): %s=%s" % (
                    len( opr['data'] ), cnt, tag, val )
        operations.append( opr )

            
    # Perform all specified tag operations, the specified number of repeat times.  Doesn't handle
    # fragmented reads yet.  If any operation fails, return a non-zero exit status.
    status			= 0
    start			= misc.timer()
    for i in range( repeat ):
        for op in operations: # {'path': [...], 'elements': #}
            begun		= misc.timer()
            if 'data' in op:
                descr		= "Write Frag"
                request		= cli.write( offset=0, timeout=timeout, **op )
            else:
                descr		= "Read  Frag"
                request		= cli.read( offset=0, timeout=timeout, **op )
            elapsed		= misc.timer() - begun
            log.normal( "Client %s Sent %7.3f/%7.3fs: %s" % ( descr, elapsed, timeout, enip.enip_format( request )))
            response			= None
            for response in cli:
                elapsed		= misc.timer() - begun
                log.normal( "Client %s Resp %7.3f/%7.3fs: %s" % ( descr, elapsed, timeout, enip.enip_format( response )))
                if response is None:
                    if elapsed <= timeout:
                        cli.readable( timeout=timeout - elapsed )
                        continue
                break
            elapsed		= misc.timer() - begun
            log.normal( "Client %s Rcvd %7.3f/%7.3fs: %s" % ( descr, elapsed, timeout, enip.enip_format( response )))
            tag			= op['path'][0]['symbolic']
            elm			= op['path'][1]['element']
            cnt			= op['elements']
            val			= []   # data values read/written
            res			= None # result of request
            act			= "??" # denotation of request action

            try:
                # The response should contain either an status code (possibly with an extended
                # status), or the read_frag request's data.  Remember; a successful response may
                # carry read_frag.data, but report a status == 6 indicating that more data remains
                # to return via a subsequent fragmented read request.
                request		= response.enip.CIP.send_data.CPF.item[1].unconnected_send.request
                if 'read_frag' in request:
                    act		= "=="
                    val		= request.read_frag.data
                elif 'write_frag' in request:
                    act		= "<="
                    val		= op['data']
                if not request.status:
                    res		= "OK"
                else:
                    res		= "Status %d %s" % ( request.status,
                        repr( request.status_ext.data ) if 'status_ext' in request and request.status_ext.size else "" )
                if request.status:
                    if not status:
                        status	= request.status
                    log.warning( "Client %s returned non-zero status: %s", descr, res )

            except AttributeError as exc:
                status		= 1
                res		= "Client %s Response missing data: %s" % ( descr, exc )
            except Exception as exc:
                status		= 1
                res		= "Client %s Exception: %s" % exc

            log.warning( "%10s[%5d-%-5d] %s %r: %r" % ( tag, elm, elm + cnt - 1, act, val, res ))

    duration			= misc.timer() - start
    log.warning( "Client ReadFrg. Average %7.3f TPS (%7.3fs ea)." % ( repeat / duration, duration / repeat ))
    return status
Exemple #16
0
def test_CIP_HART(repeat=1):
    """HART protocol enip CIP messages
    """
    enip.lookup_reset()  # Flush out any existing CIP Objects for a fresh start

    ENIP = enip.enip_machine(context='enip')
    CIP = enip.CIP()
    # We'll use a HART Message Router, to handle its expanded porfolio of commands
    MR = HART(instance_id=1)

    for pkt, tst in client.recycle(CIP_HART_tests, times=repeat):
        # Parse just the CIP portion following the EtherNet/IP encapsulation header
        data = cpppo.dotdict()
        source = cpppo.chainable(pkt)
        with ENIP as machine:
            for i, (m, s) in enumerate(machine.run(source=source, data=data)):
                log.detail("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                           machine.name_centered(), i, s, source.sent,
                           source.peek(), data)
        # In a real protocol implementation, an empty header (EOF with no input at all) is
        # acceptable; it indicates a session closed by the client.
        if not data:
            log.normal("EtherNet/IP Request: Empty (session terminated): %s",
                       enip.enip_format(data))
            continue

        if log.isEnabledFor(logging.NORMAL):
            log.normal("EtherNet/IP Request: %s", enip.enip_format(data))

        # Parse the encapsulated .input
        with CIP as machine:
            for i, (m, s) in enumerate(
                    machine.run(path='enip',
                                source=cpppo.peekable(
                                    data.enip.get('input', b'')),
                                data=data)):
                log.detail("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                           machine.name_centered(), i, s, source.sent,
                           source.peek(), data)

        if log.isEnabledFor(logging.NORMAL):
            log.normal("EtherNet/IP CIP Request: %s", enip.enip_format(data))

        # Assume the request in the CIP's CPF items are HART requests.
        # Now, parse the encapsulated message(s).  We'll assume it is destined for a HART Object.
        if 'enip.CIP.send_data' in data:
            for item in data.enip.CIP.send_data.CPF.item:
                if 'unconnected_send.request' in item:
                    # An Unconnected Send that contained an encapsulated request (ie. not just a Get
                    # Attribute All)
                    with MR.parser as machine:
                        if log.isEnabledFor(logging.NORMAL):
                            log.normal(
                                "Parsing %3d bytes using %s.parser, from %s",
                                len(item.unconnected_send.request.input), MR,
                                enip.enip_format(item))
                        # Parse the unconnected_send.request.input octets, putting parsed items into the
                        # same request context
                        for i, (m, s) in enumerate(
                                machine.run(
                                    source=cpppo.peekable(
                                        item.unconnected_send.request.input),
                                    data=item.unconnected_send.request)):
                            log.detail(
                                "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                                machine.name_centered(), i, s, source.sent,
                                source.peek(), data)
                    # Post-processing of some parsed items is only performed after lock released!
                    if log.isEnabledFor(logging.NORMAL):
                        log.normal(
                            "Parsed  %3d bytes using %s.parser, into %s",
                            len(item.unconnected_send.request.input), MR,
                            enip.enip_format(data))
        try:
            for k, v in tst.items():
                assert data[k] == v, ("data[%r] == %r\n"
                                      "expected:   %r" % (k, data[k], v))
        except:
            log.warning("%r not in data, or != %r: %s", k, v,
                        enip.enip_format(data))
            raise

        # Ensure that we can get the original EtherNet/IP CIP back
        for k in list(data.keys()):
            if k.endswith('input') and 'sender_context' not in k:
                log.detail("del data[%r]", k)
                del data[k]
        try:
            # First reconstruct any SendRRData CPF items, containing encapsulated requests/responses
            if 'enip.CIP.send_data' in data:
                cpf = data.enip.CIP.send_data
                for item in cpf.CPF.item:
                    if 'unconnected_send' in item:
                        item.unconnected_send.request.input = bytearray(
                            MR.produce(item.unconnected_send.request))
                        log.normal("Produce HART message from: %r",
                                   item.unconnected_send.request)

            # Next, reconstruct the CIP Register, ListIdentity, ListServices, or SendRRData.  The CIP.produce must
            # be provided the EtherNet/IP header, because it contains data (such as .command)
            # relevant to interpreting the .CIP... contents.
            data.enip.input = bytearray(enip.CIP.produce(data.enip))
            # And finally the EtherNet/IP encapsulation itself
            data.input = bytearray(enip.enip_encode(data.enip))
            log.detail("EtherNet/IP CIP Request produced payload: %r",
                       bytes(data.input))
            assert data.input == pkt, "original:\n" + hexdump(
                pkt) + "\nproduced:\n" + hexdump(data.input)
        except Exception as exc:
            log.warning(
                "Exception %s; Invalid packet produced from EtherNet/IP CIP data: %s",
                exc, enip.enip_format(data))
            raise
Exemple #17
0
def test_hart_pass_thru_poll(simulated_hart_gateway):
    r"""To test a remote C*Logix w/ a HART card, set up a remote port forward from another host in the
    same LAN.  Here's a windows example, using putty.  This windows machine (at 100.100.102.1)
    forwards a port 44818 on fat2.kundert.ca, to the PLC at 100.100.102.10:44818:

        C:\Users\Engineer\Desktop\putty.exe -R 44818:100.100.102.10:44818 [email protected]


    Now, from another host that can see fat2.kundert.ca:

        $ python -m cpppo.server.enip.list_services --list-identity -a fat2.kundert.ca:44818
        {
            "peer": [
                "fat2.kundert.ca",
                44818
            ],
            ...
            "enip.status": 0,
            "enip.CIP.list_services.CPF.count": 1,
            "enip.CIP.list_services.CPF.item[0].communications_service.capability": 288,
            "enip.CIP.list_services.CPF.item[0].communications_service.service_name": "Communications",
        }
        {
            ...
            "enip.status": 0,
            "enip.CIP.list_identity.CPF.item[0].identity_object.sin_addr": "100.100.102.10",
            "enip.CIP.list_identity.CPF.item[0].identity_object.status_word": 96,
            "enip.CIP.list_identity.CPF.item[0].identity_object.vendor_id": 1,
            "enip.CIP.list_identity.CPF.item[0].identity_object.product_name": "1756-EN2T/D",
            "enip.CIP.list_identity.CPF.item[0].identity_object.sin_port": 44818,
            "enip.CIP.list_identity.CPF.item[0].identity_object.state": 3,
            "enip.CIP.list_identity.CPF.item[0].identity_object.version": 1,
            "enip.CIP.list_identity.CPF.item[0].identity_object.device_type": 12,
            "enip.CIP.list_identity.CPF.item[0].identity_object.sin_family": 2,
            "enip.CIP.list_identity.CPF.item[0].identity_object.serial_number": 11866067,
            "enip.CIP.list_identity.CPF.item[0].identity_object.product_code": 166,
            "enip.CIP.list_identity.CPF.item[0].identity_object.product_revision": 1802,
        }

    """
    command, address = simulated_hart_gateway

    # For testing, we'll hit a specific device
    #address			= ("fat2.kundert.ca", 44818)
    #address			= ("100.100.102.10", 44818)
    #address			= ("localhost", 44818)
    route_path = None
    route_path = [{'link': 2, 'port': 1}]
    try:
        assert address, "Unable to detect HART EtherNet/IP CIP Gateway IP address"
        #hio				= client.implicit( host=address[0], port=address[1] )
        hio = client.connector(host=address[0], port=address[1])

        # Just get the primary variable, to see if the HART device is there.
        operations = [
            {
                "method": "service_code",
                "code": HART.RD_VAR_REQ,
                "data": [],  # No payload
                "data_size":
                4 + 36,  # Known response size: command,status,<payload>
                "path": '@0x%X/8' %
                (HART.class_id),  # Instance 1-8 ==> HART Channel 0-7
                "route_path": route_path,
            },
        ]

        with hio:
            for idx, dsc, req, rpy, sts, val in hio.pipeline(
                    operations=client.parse_operations(operations),
                    **hart_kwds):
                log.normal("Client %s: %s --> %r: %s", hio, dsc, val,
                           enip.enip_format(rpy))

        path = '@0x%X/8' % (HART.class_id)
        data = hart_pass_thru(hio,
                              path=path,
                              hart_data=[1, 0],
                              route_path=route_path,
                              data_size=4)  # with no size

        # The small response carries the 4-byte value, the long CIP MSG response additionally
        # carries the data type We receive the long response.
        value = None
        if data and len(data) >= 4:
            units = data[0] if len(data) > 4 else None
            packer = struct.Struct(enip.REAL_network.struct_format)
            value, = packer.unpack_from(buffer=bytearray(data[-4:]))
        log.normal("Read primary variable Value: %s (units: %s), from: %r",
                   value, units, data)

        # HART Command 3 gets all 4 variables
        data = hart_pass_thru(hio,
                              path=path,
                              hart_data=[3, 0],
                              route_path=route_path,
                              data_size=4 * 4)  # with no size

        # small response carries PV, SV, TV, FV values, no data types
        value = []
        if data and len(data) == 4 * 4:
            # Short
            packer = struct.Struct(enip.REAL_network.struct_format)
            for i in range(0, len(data), 4):
                value += packer.unpack_from(buffer=bytearray(data[i:i + 4]))
        elif data and len(data) >= 24:
            # Long
            packer = struct.Struct(enip.REAL_network.struct_format)
            value = cpppo.dotdict()
            value.current, = packer.unpack_from(buffer=bytearray(data[0:]))
            value.PV_units = data[4]
            value.PV, = packer.unpack_from(buffer=bytearray(data[5:]))
            value.SV_units = data[10]
            value.SV, = packer.unpack_from(buffer=bytearray(data[11:]))
            value.TV_units = data[14]
            value.TV, = packer.unpack_from(buffer=bytearray(data[15:]))
            value.FV_units = data[19]
            value.FV, = packer.unpack_from(buffer=bytearray(data[20:]))
        log.normal("Read all variables Values: %s, from: %r",
                   enip.enip_format(value), data)

        # HART Command 12 gets the 24-character Message
        data = hart_pass_thru(hio,
                              path=path,
                              hart_data=[12, 0],
                              route_path=route_path,
                              data_size=4 * 4)  # with no size
        value = None
        if data and len(data):
            try:
                value = bytes(data).decode('ascii')
            except:
                value = hexdump(data)
            log.normal("Read Message: \n%s\nfrom: %r", value, data)

        # HART Command 0 gets the identity
        data = hart_pass_thru(hio,
                              path=path,
                              hart_data=[0, 0],
                              route_path=route_path,
                              data_size=4 * 4)  # with no size
        value = None
        if data and len(data):
            value = hexdump(data)
            log.normal("Read Identity: \n%s\nfrom: %r", value, data)

        # HART Command 13 gets the Tag
        data = hart_pass_thru(hio,
                              path=path,
                              hart_data=[13, 0],
                              route_path=route_path,
                              data_size=4 * 4)  # with no size
        value = None
        if data and len(data):
            value = hexdump(data)
            log.normal("Read Tag: \n%s\nfrom: %r", value, data)

    except Exception as exc:
        log.warning("Test terminated with exception: %s", exc)
        raise
Exemple #18
0
def hart_pass_thru(io, path, hart_data, data_size, route_path=None):
    """For eg. hart_data=[1, 0], data_size=4 for HART command 1.  Returns None on failure, or the HART
    command response data payload.
    
    Harvests a Pass-thru Init handle, and issues Query on it 'til successs.

    """
    # Try to start the Pass-thru command, with the Pass-thru Init, and get handle
    operations = [
        {
            "method": "service_code",
            "code": HART.PT_INI_REQ,
            "data": hart_data,
            "data_size":
            4 + 2,  # Known response size: command,status,<payload>
            "path": path,  # Instance 1-8 ==> HART Channel 0-7
            "route_path": route_path,
        },
    ]

    # Look for a reply init.status of 33 initiated. Actually, it appears that status 0 indicates success.
    handle = None
    while handle is None:
        time.sleep(.1)
        with io:
            for idx, dsc, req, rpy, sts, val in io.pipeline(
                    operations=client.parse_operations(operations),
                    **hart_kwds):
                log.detail("Client %s: %s --> %r: request: %s\nreply:%s", io,
                           dsc, val, enip.enip_format(req),
                           enip.enip_format(rpy))
                if rpy.status == 0 and rpy.init.status in (
                        33, ):  # 32 busy, 33 initiated, 35 device offline
                    handle = rpy.init.handle
    log.normal("HART Pass-thru command Handle: %s", handle)

    # Query for success/failure (loop on running)
    operations = [
        {
            "method": "service_code",
            "code": HART.PT_QRY_REQ,
            "data": [handle],  # HART: Pass-thru Query handle
            "data_size": 4 +
            data_size,  # Known response size: 5 (units + 4-byte real in network order)
            "path": path,  # Instance 1-8 ==> HART Channel 0-7
            "route_path": route_path,
        },
    ]

    reply = {}
    while not reply or (reply.status == 0 and reply.query.status
                        == 34):  # 0 success, 34 running, 35 dead
        time.sleep(.1)
        with io:
            for idx, dsc, req, rpy, sts, val in io.pipeline(
                    operations=client.parse_operations(operations),
                    **hart_kwds):
                log.detail("Client %s: %s --> %r: %s", io, dsc, val,
                           enip.enip_format(rpy))
                reply = rpy
        log.normal("HART pass-thru command Status: %s",
                   reply.get('query.status'))

    return reply.get('query.reply_data.data', None)