Пример #1
0
def test_limit():
    # Force a limit on input symbols.  If we only accept only even b's, we'll
    # fail if we force a stoppage at a+b*9
    source			= cpppo.peekable( str( 'a'+'b'*100 ))
    data			= cpppo.dotdict()
    try:
        with cpppo.regex( initial=str( 'a(bb)*' ), context='even_b', limit=10 ) as machine:
            for i,(m,s) in enumerate( machine.run( source=source, data=data )):
                log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                          i, s, source.sent, source.peek(), data )
    except cpppo.NonTerminal:
        assert i == 10
        assert source.sent == 10
    else:
        assert False, "Should have failed with a cpppo.NonTerminal exception"


    # But odd b's OK
    for limit in [
            10, 
            '..somewhere.ten',
            lambda **kwds: 10, 
            lambda path=None, data=None, **kwds: data[path+'..somewhere.ten'] ]:
        source			= cpppo.peekable( str( 'a'+'b'*100 ))
        data			= cpppo.dotdict()
        data['somewhere.ten']	= 10
        with cpppo.regex( initial=str( 'ab(bb)*' ), context='odd_b', limit=limit ) as machine:
            for i,(m,s) in enumerate( machine.run( source=source, data=data )):
                log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                          i, s, source.sent, source.peek(), data )
            assert i == 10
            assert source.sent == 10
            assert ( data.odd_b.input.tostring()
                     if sys.version_info[0] < 3
                     else data.odd_b.input.tounicode() ) == str( 'a'+'b'*9 )
Пример #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
Пример #3
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
Пример #4
0
def test_readme():
    """The basic examples in the README"""

    # Basic DFA that accepts ab+
    E				= cpppo.state( "E" )
    A				= cpppo.state_input( "A" )
    B				= cpppo.state_input( "B", terminal=True )
    E['a']			= A
    A['b']			= B
    B['b']			= B

    data			= cpppo.dotdict()
    source			= cpppo.peekable( str( 'abbbb,ab' ))
    with cpppo.dfa( initial=E ) as abplus:
        for i,(m,s) in enumerate( abplus.run( source=source, path="ab+", data=data )):
            log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                      i, s, source.sent, source.peek(), data )
        assert i == 5
    assert source.peek() == str(',')
    
    # Composite state machine accepting ab+, ignoring ,[ ]* separators
    CSV				= cpppo.dfa( "CSV", initial=E, terminal=True )
    SEP				= cpppo.state_drop( "SEP" )

    CSV[',']			= SEP
    SEP[' ']			= SEP
    SEP[None]			= CSV

    source			= cpppo.peekable( str( 'abbbb, ab' ))
    with cpppo.dfa( initial=CSV ) as r2:
        for i,(m,s) in enumerate( r2.run( source=source, path="readme_CSV", data=data )):
            log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                      i, s, source.sent, source.peek(), data )
        assert i == 14
    assert source.peek() is None
Пример #5
0
def test_iterators():
    i				= cpppo.chaining()
    j				= cpppo.chainable( i )
    k				= cpppo.chainable( 'abc' )
    assert i is j
    try:
        next( i )
        assert False, "stream with no iterable should raise StopIteration"
    except StopIteration:
        pass
    assert k is not j
    assert isinstance( k, cpppo.chaining )

    assert cpppo.peekable( i ) is i
    p				= cpppo.peekable( '123' )
    assert cpppo.peekable( p ) is p
    assert cpppo.chainable( p ) is not p
    assert list( p ) == ['1', '2', '3']
    assert p.sent == 3
    p.push('x')
    assert p.sent == 2
    assert list( p ) == ['x']
    assert list( p ) == []


    i.chain('abc')
    i.chain('')
    i.chain( '123' )
    assert list( i ) == ['a','b','c','1','2','3']
    assert i.sent == 6

    i.chain( 'y' )
    i.push( 'x' )
    assert list( i ) == ['x','y']

    i.chain( None )
    try:
        next( i )
        assert False, "Expected TypeError to be raised"
    except TypeError:
        pass
    except Exception as e:
        assert False, "Expected TypeError, not %r" % ( e )


    r				= cpppo.rememberable( '123' )
    assert next( r ) == '1'
    assert r.memory == [ '1' ]
    try:
        r.push( 'x' )
        assert False, "Should have rejected push of inconsistent symbol"
    except AssertionError:
        pass
    assert r.sent == 1
    r.push( '1' )
    assert r.sent == 0
    assert r.memory == []
    assert list( r ) == r.memory == [ '1', '2','3' ]
Пример #6
0
def test_iterators():
    i = cpppo.chaining()
    j = cpppo.chainable(i)
    k = cpppo.chainable("abc")
    assert i is j
    try:
        next(i)
        assert False, "stream with no iterable should raise StopIteration"
    except StopIteration:
        pass
    assert k is not j
    assert isinstance(k, cpppo.chaining)

    assert cpppo.peekable(i) is i
    p = cpppo.peekable("123")
    assert cpppo.peekable(p) is p
    assert cpppo.chainable(p) is not p
    assert list(p) == ["1", "2", "3"]
    assert p.sent == 3
    p.push("x")
    assert p.sent == 2
    assert list(p) == ["x"]
    assert list(p) == []

    i.chain("abc")
    i.chain("")
    i.chain("123")
    assert list(i) == ["a", "b", "c", "1", "2", "3"]
    assert i.sent == 6

    i.chain("y")
    i.push("x")
    assert list(i) == ["x", "y"]

    i.chain(None)
    try:
        next(i)
        assert False, "Expected TypeError to be raised"
    except TypeError:
        pass
    except Exception as e:
        assert False, "Expected TypeError, not %r" % (e)

    r = cpppo.rememberable("123")
    assert next(r) == "1"
    assert r.memory == ["1"]
    try:
        r.push("x")
        assert False, "Should have rejected push of inconsistent symbol"
    except AssertionError:
        pass
    assert r.sent == 1
    r.push("1")
    assert r.sent == 0
    assert r.memory == []
    assert list(r) == r.memory == ["1", "2", "3"]
Пример #7
0
def test_decide():
    """Allow state transition decisions based on collected context other than just
    the next source symbol.

    """
    e = cpppo.state("enter")
    e["a"] = a = cpppo.state_input("a", context="a")
    a[" "] = s1 = cpppo.state_drop("s1")
    s1[" "] = s1
    s1[None] = i1 = cpppo.integer("i1", context="i1")

    i1[" "] = s2 = cpppo.state_drop("s2")
    s2[" "] = s2
    s2[None] = i2 = cpppo.integer("i2", context="i2")
    less = cpppo.state("less", terminal=True)
    greater = cpppo.state("greater", terminal=True)
    equal = cpppo.state("equal", terminal=True)
    i2[None] = cpppo.decide("isless", less, predicate=lambda machine, source, path, data: data.i1 < data.i2)
    i2[None] = cpppo.decide("isgreater", greater, predicate=lambda machine, source, path, data: data.i1 > data.i2)
    i2[None] = equal

    source = cpppo.peekable(str("a 1 2"))
    data = cpppo.dotdict()
    with cpppo.dfa("comparo", initial=e) as comparo:
        for i, (m, s) in enumerate(comparo.run(source=source, data=data)):
            log.info(
                "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                m.name_centered(),
                i,
                s,
                source.sent,
                source.peek(),
                data,
            )
        assert i == 11
        assert s is less

    source = cpppo.peekable(str("a 33 33"))
    data = cpppo.dotdict()
    with cpppo.dfa("comparo", initial=e) as comparo:
        for i, (m, s) in enumerate(comparo.run(source=source, data=data)):
            log.info(
                "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                m.name_centered(),
                i,
                s,
                source.sent,
                source.peek(),
                data,
            )
        assert i == 13
        assert s is equal
Пример #8
0
def test_tnet_string():
    testvec			= [
        "The π character is called pi",
    ]

    successes			= 0
    for t in testvec:
      with tnet.tnet_machine() as tnsmach:
        path			= "test_tnet"
        tns			= tnetstrings.dump( t )

        data			= cpppo.dotdict()
        source			= cpppo.peekable( tns )

        for mch, sta in tnsmach.run( source=source, data=data, path=path ):
            log.info( "%s byte %5d: data: %r",
                      misc.centeraxis( mch, 25, clip=True ), source.sent, data )
            log.info("Parsing tnetstring:\n%s\n%s (byte %d)", repr(bytes(tns)),
                     '-' * (len(repr(bytes(tns[:source.sent])))-1) + '^', source.sent )
        if sta is None or not sta.terminal:
            # Ended in a non-terminal state
            log.info( "%s byte %5d: failure: data: %r; Not terminal; unrecognized", 
                      misc.centeraxis( tnsmach, 25, clip=True ), source.sent, data )
        else:
            # Ended in a terminal state.
            if source.peek() is None:
                log.info( "%s byte %5d: success: data: %r", 
                          misc.centeraxis( tnsmach, 25, clip=True ), source.sent, data )
                successes      += 1
            else:
                log.info( "%s byte %5d: failure: data: %r; Terminal, but TNET string wasn't consumed",
                          misc.centeraxis( tnsmach, 25, clip=True ), source.sent, data )

    assert successes == len( testvec )
Пример #9
0
def main():
    """The basic examples in the README"""

    # Basic DFA that accepts ab+
    E			= cpppo.state( 'E' )
    A			= cpppo.state_input( 'A' )
    B			= cpppo.state_input( 'B', terminal=True )
    E['a']		= A
    A['b']		= B
    B['b']		= B

    BASIC		= cpppo.dfa( 'ab+', initial=E, context='basic' )

    # Composite state machine accepting ab+, ignoring ,[ ]* separators
    ABP			= cpppo.dfa( 'ab+', initial=E, terminal=True )
    SEP			= cpppo.state_drop( 'SEP' )
    ABP[',']		= SEP
    SEP[' ']		= SEP
    SEP[None]		= ABP

    CSV			= cpppo.dfa( 'CSV', initial=ABP, context='csv' )

    # A regular expression; he default dfa name is the regular expression itself.
    REGEX		= cpppo.regex( initial='(ab+)((,[ ]*)(ab+))*', context='regex' )

    data		= cpppo.dotdict()
    for machine in [ BASIC, CSV, REGEX ]:
        path		= machine.context() + '.input' # default for state_input data
        source		= cpppo.peekable( str( 'abbbb, ab' ))
        with machine:
            for i,(m,s) in enumerate( machine.run( source=source, data=data )):
                print( "%s #%3d; next byte %3d: %-10.10r: %r" % (
                       m.name_centered(), i, source.sent, source.peek(), data.get(path) ))
        print( "Accepted: %r; remaining: %r\n" % ( data.get(path), ''.join( source )))
    print( "Final: %r" % ( data ))
Пример #10
0
def test_tnet_string():
    testvec			= [
        "The π character is called pi",
    ]

    successes			= 0
    for t in testvec:
      with tnet.tnet_machine() as tnsmach:
        path			= "test_tnet"
        tns			= tnetstrings.dump( t )

        data			= cpppo.dotdict()
        source			= cpppo.peekable( tns )

        for mch, sta in tnsmach.run( source=source, data=data, path=path ):
            log.info( "%s byte %5d: data: %r",
                      misc.centeraxis( mch, 25, clip=True ), source.sent, data )
            log.info("Parsing tnetstring:\n%s\n%s (byte %d)", repr(bytes(tns)),
                     '-' * (len(repr(bytes(tns[:source.sent])))-1) + '^', source.sent )
        if sta is None or not sta.terminal:
            # Ended in a non-terminal state
            log.info( "%s byte %5d: failure: data: %r; Not terminal; unrecognized", 
                      misc.centeraxis( tnsmach, 25, clip=True ), source.sent, data )
        else:
            # Ended in a terminal state.
            if source.peek() is None:
                log.info( "%s byte %5d: success: data: %r", 
                          misc.centeraxis( tnsmach, 25, clip=True ), source.sent, data )
                successes      += 1
            else:
                log.info( "%s byte %5d: failure: data: %r; Terminal, but TNET string wasn't consumed",
                          misc.centeraxis( tnsmach, 25, clip=True ), source.sent, data )

    assert successes == len( testvec )
Пример #11
0
def test_decide():
    """Allow state transition decisions based on collected context other than just
    the next source symbol.

    """
    e = cpppo.state("enter")
    e['a'] = a = cpppo.state_input("a", context='a')
    a[' '] = s1 = cpppo.state_drop("s1")
    s1[' '] = s1
    s1[None] = i1 = cpppo.integer("i1", context='i1')

    i1[' '] = s2 = cpppo.state_drop("s2")
    s2[' '] = s2
    s2[None] = i2 = cpppo.integer("i2", context='i2')
    less = cpppo.state("less", terminal=True)
    greater = cpppo.state("greater", terminal=True)
    equal = cpppo.state("equal", terminal=True)
    i2[None] = cpppo.decide(
        "isless",
        less,
        predicate=lambda machine, source, path, data: data.i1 < data.i2)
    i2[None] = cpppo.decide(
        "isgreater",
        greater,
        predicate=lambda machine, source, path, data: data.i1 > data.i2)
    i2[None] = equal

    source = cpppo.peekable(str('a 1 2'))
    data = cpppo.dotdict()
    with cpppo.dfa("comparo", initial=e) as comparo:
        for i, (m, s) in enumerate(comparo.run(source=source, data=data)):
            log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                     m.name_centered(), i, s, source.sent, source.peek(), data)
        assert i == 12
        assert s is less

    source = cpppo.peekable(str('a 33 33'))
    data = cpppo.dotdict()
    with cpppo.dfa("comparo", initial=e) as comparo:
        for i, (m, s) in enumerate(comparo.run(source=source, data=data)):
            log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                     m.name_centered(), i, s, source.sent, source.peek(), data)
        assert i == 14
        assert s is equal
Пример #12
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
Пример #13
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
Пример #14
0
def main():
    """The basic examples in the README"""

    # Basic DFA that accepts ab+
    E = cpppo.state('E')
    A = cpppo.state_input('A')
    B = cpppo.state_input('B', terminal=True)
    E['a'] = A
    A['b'] = B
    B['b'] = B

    BASIC = cpppo.dfa('ab+', initial=E, context='basic')

    # Composite state machine accepting ab+, ignoring ,[ ]* separators
    ABP = cpppo.dfa('ab+', initial=E, terminal=True)
    SEP = cpppo.state_drop('SEP')
    ABP[','] = SEP
    SEP[' '] = SEP
    SEP[None] = ABP

    CSV = cpppo.dfa('CSV', initial=ABP, context='csv')

    # A regular expression; he default dfa name is the regular expression itself.
    REGEX = cpppo.regex(initial='(ab+)((,[ ]*)(ab+))*', context='regex')

    data = cpppo.dotdict()
    for machine in [BASIC, CSV, REGEX]:
        path = machine.context() + '.input'  # default for state_input data
        source = cpppo.peekable(str('abbbb, ab'))
        with machine:
            for i, (m, s) in enumerate(machine.run(source=source, data=data)):
                print("%s #%3d; next byte %3d: %-10.10r: %r" %
                      (m.name_centered(), i, source.sent, source.peek(),
                       data.get(path)))
        print("Accepted: %r; remaining: %r\n" %
              (data.get(path), ''.join(source)))
    print("Final: %r" % (data))
Пример #15
0
    def __next__(self):
        """Return the next available response, or None if no complete response is available.  Raises
        StopIteration (cease iterating) on EOF.  Any other Exception indicates a client failure,
        and should result in the client instance being discarded.
        
        If no input is presently available, harvest any input immediately available; terminate on EOF.

        """
        if self.source.peek() is None:
            rcvd = network.recv(self.conn, timeout=0)
            log.detail("EtherNet/IP-->%16s:%-5d rcvd %5d: %s", self.addr[0],
                       self.addr[1],
                       len(rcvd) if rcvd is not None else 0, repr(rcvd))
            if rcvd is not None:
                # Some input (or EOF); source is empty; if no input available, terminate
                if not len(rcvd):
                    raise StopIteration
                self.source.chain(rcvd)
            else:
                # Don't create parsing engine 'til we have some I/O to process.  This avoids the
                # degenerate situation where empty I/O (EOF) always matches the empty command (used
                # to indicate the end of an EtherNet/IP session).
                if self.engine is None:
                    return None

        # Initiate or continue parsing input using the machine's engine; discard the engine at
        # termination or on error (Exception).  Any exception (including cpppo.NonTerminal) will be
        # propagated.
        result = None
        with self.frame as machine:
            try:
                if self.engine is None:
                    self.data = cpppo.dotdict()
                    self.engine = machine.run(source=self.source,
                                              data=self.data)
                    log.detail(
                        "EtherNet/IP   %16s:%-5d run.: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0], self.addr[1], machine.name_centered(),
                        machine.current, self.source.sent, self.source.peek(),
                        self.data)

                for m, s in self.engine:
                    log.detail(
                        "EtherNet/IP<--%16s:%-5d rpy.: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0], self.addr[1], machine.name_centered(), s,
                        self.source.sent, self.source.peek(), self.data)
            except Exception as exc:
                log.warning("EtherNet/IP<x>%16s:%-5d err.: %s", self.addr[0],
                            self.addr[1], str(exc))
                self.engine = None
                raise
            if machine.terminal:
                log.detail(
                    "EtherNet/IP   %16s:%-5d done: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                    self.addr[0], self.addr[1],
                    machine.name_centered(), machine.current, self.source.sent,
                    self.source.peek(), self.data)
                # Got an EtherNet/IP frame.  Return it (after parsing its payload.)
                self.engine = None
                result = self.data

        # Parse the EtherNet/IP encapsulated CIP frame
        if result is not None:
            with self.cip as machine:
                for m, s in machine.run(path='enip',
                                        source=cpppo.peekable(
                                            result.enip.input),
                                        data=result):
                    log.detail(
                        "EtherNet/IP<--%16s:%-5d CIP : %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0], self.addr[1], machine.name_centered(), s,
                        self.source.sent, self.source.peek(), self.data)
                    pass
                assert machine.terminal, "No CIP payload in the EtherNet/IP frame: %r" % (
                    result)

        # Parse the Logix request responses in the EtherNet/IP CIP payload's CPF items
        if result is not None and 'enip.CIP.send_data' in result:
            for item in result.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 self.lgx as machine:
                        for m, s in machine.run(
                                source=cpppo.peekable(
                                    item.unconnected_send.request.input),
                                data=item.unconnected_send.request):
                            pass
                        assert machine.terminal, "No Logix request in the EtherNet/IP CIP CPF frame: %r" % (
                            result)

        return result
Пример #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
Пример #17
0
    def read_details( self, attributes ):
        """Assumes that self.gateway has been established; does not close_gateway on Exception.  If you
        use this interface, ensure that you maintain the gateway (eg. ):

            via = proxy( 'hostname' )
            with via:
                for val,(sts,(att,typ,uni) in via.read_details( [...] ):

        Read the specified CIP Tags/Attributes in the string or iterable 'attributes', using Read
        Tag [Fragmented] (returning the native type), or Get Attribute Single/All (converting it to
        the specified EtherNet/IP CIP type(s)).

        The reason iterables are used and a generator returned, is to allow the underlying
        cpppo.server.enip.client connector to aggregate multiple CIP operations using Multiple
        Service Packet requests and/or "pipeline" multiple requests in-flight, while receiving the
        results of earlier requests.

        The 'attributes' must be either a simple string Tag name (no Type, implying the use of
        *Logix Read Tag [Fragmented] service), eg:

            "Tag"

        or an iterable containing 2 or 3 values; a Tag/address, a type/types (may be None, to force
        Tag I/O), and an optional description (eg. Units)

            [
                "Tag",
                ( "Tag", None, "kWh" ),
                ( "@1/1/1", "INT" )
                ( "@1/1/1", "INT", "Hz" )
                ( "@1/1", ( "INT", "INT", "INT", "INT", "INT", "DINT", "SSTRING", "USINT" ))
                ( "@1/1", ( "INT", "INT", "INT", "INT", "INT", "DINT", "SSTRING", "USINT" ), "Identity" )
            ]

        Produces a generator yielding the corresponding sequence of results and details for the
        supplied 'attributes' iterable.  Each individual request may succeed or fail with a non-zero
        status code (remember: status code 0x06 indicates successful return of a partial result).

        Upon successful I/O, a tuple containing the result value and details about the result (a
        status, and the attribute's details (address, type, and units)) corresponding to each of the
        supplied 'attributes' elements is yielded as a sequence.  Each result value is always a list
        of values, or None if the request failed:

            (
                ([0],(0, ("Tag", enip.INT, None))),
                ([1.23],(0, "Tag", enip.REAL, "kWh"))),
                ([1], (0, ("@1/1/1", enip.INT, None))),
                ([1], (0, ("@1/1/1", enip.INT, "Hz"))),
                ([1, 2, 3, 4, 5 6, "Something", 255],
                    (0, ("@1/1", [
                        enip.INT, enip.INT, enip.INT,  enip.INT,
                        enip.INT, enip.DINT, enip.STRING, enip.USINT ], None ))),
                ([1, 2, 3, 4, 5 6, "Something", 255],
                    (0, ("@1/1", [
                        enip.INT, enip.INT, enip.INT,  enip.INT,
                        enip.INT, enip.DINT, enip.STRING, enip.USINT ], "Identity" ))),
            )

        The read_details API raises exception on failure to parse request, or result data type
        conversion problem.  The simple 'read' API also raises an Exception on attribute access
        error, the return of failure status code.  Not all of these strictly necessitate a closure
        of the EtherNet/IP CIP connection, but should be sufficiently rare (once configured) that
        they all must signal closure of the connection gateway (which is re-established on the next
        call for I/O).

        EXAMPLES

            proxy		= enip_proxy( '10.0.1.2' )
            try:
                with contextlib.closing( proxy.read( [ ("@1/1/7", "SSTRING") ] )) as reader: # CIP Device Name
                    value	= next( reader )
            except Exception as exc:
                proxy.close_gateway( exc )

            # If CPython (w/ reference counting) is your only target, you can use the simpler:
            proxy		= enip_proxy( '10.0.1.2' )
            try:
                value,		= proxy.read( [ ("@1/1/7", "SSTRING") ] ) # CIP Device Name
            except Exception as exc:
                proxy.close_gateway( exc )

        """
        if isinstance( attributes, cpppo.type_str_base ):
            attributes		= [ attributes ]

        def opp__att_typ_uni( i ):
            """Generate sequence containing the enip.client operation, and the original attribute
            specified, its type(s) (if any), and any description.  Augment produced operation with
            data type (if known), to allow estimation of reply sizes (and hence, Multiple Service
            Packet use); requires cpppo>=3.8.1.

            Yields: (opp,(att,typ,dsc))

            """
            for a in i:
                assert self.is_request( a ), \
                    "Not a valid read/write target: %r" % ( a, )
                try:
                    # The attribute description is either a plain Tag, an (address, type), or an
                    # (address, type, description)
                    if is_listlike( a ):
                        att,typ,uni = a if len( a ) == 3 else a+(None,)
                    else:
                        att,typ,uni = a,None,None
                    # No conversion of data type if None; use a Read Tag [Fragmented]; works only for
                    # SINT/INT/DINT/REAL/BOOL.  Otherwise, conversion of data type desired; get raw
                    # data using Get Attribute Single.
                    parser	= client.parse_operations if typ is None else attribute_operations
                    opp,	= parser( ( att, ), route_path=self.route_path, send_path=self.send_path )
                except Exception as exc:
                    log.warning( "Failed to parse attribute %r; %s", att, exc )
                    raise
                if typ is not None and not is_listlike( typ ):
                    t		= typ
                    if isinstance( typ, cpppo.type_str_base ):
                        td	= self.CIP_TYPES.get( t.strip().lower() )
                        if td is not None:
                            t,d	= td
                    if hasattr( t, 'tag_type' ):
                        opp['tag_type'] = t.tag_type

                log.detail( "Parsed attribute %r (type %r) into operation: %r", att, typ, opp )
                yield opp,(att,typ,uni)

        def types_decode( types ):
            """Produce a sequence of type class,data-path, eg. (enip.REAL,"SSTRING.string").  If a
            user-supplied type (or None) is provided, data-path is None, and the type is passed.

            """
            for t in typ if is_listlike( typ ) else [ typ ]:
                d		= None 		# No data-path, if user-supplied type
                if isinstance( t, cpppo.type_str_base ):
                    td		= self.CIP_TYPES.get( t.strip().lower() )
                    assert td, "Invalid EtherNet/IP CIP type name %r specified" % ( t, )
                    t,d		= td
                assert type( t ) in (type,type(None)), \
                    "Expected None or CIP type class, not %r" % ( t, )
                yield t,d

        # Get duplicate streams; one to feed the the enip.client's connector.operate, and one for
        # post-processing based on the declared type(s).
        operations,attrtypes	= itertools.tee( opp__att_typ_uni( attributes ))

        # Process all requests w/ the specified pipeline depth, Multiple Service Packet
        # configuration.  The 'idx' is the EtherNet/IP CIP request packet index; 'i' is the
        # individual I/O request index (for indexing att/typ/operations).
        # 
        # This Thread may block here attempting to gain exclusive access to the cpppo.dfa used
        # by the cpppo.server.enip.client connector.  This uses a threading.Lock, which will raise
        # an exception on recursive use, but block appropriately on multi-Thread contention.
        # 
        # assert not self.gateway.frame.lock.locked(), \
        #     "Attempting recursive read on %r" % ( self.gateway.frame, )
        log.info( "Acquiring gateway connection: %s",
                      "locked" if self.gateway.frame.lock.locked() else "available" )
        with self.gateway as connection:
            for i,(idx,dsc,req,rpy,sts,val) in enumerate( connection.operate(
                    ( opr for opr,_ in operations ),
                    depth=self.depth, multiple=self.multiple, timeout=self.timeout )):
                log.detail( "%3d (pkt %3d) %16s %-12s: %r ", 
                                i, idx, dsc, sts or "OK", val )
                opr,(att,typ,uni) = next( attrtypes )
                if typ is None or sts not in (0,6):
                    # No type conversion; just return whatever type produced by Read Tag.  Also, if
                    # failure status (OK if no error, or if just not all data could be returned), we
                    # can't do any more with this value...
                    yield val,(sts,(att,typ,uni))
                    continue

                # Parse the raw data using the type (or list of types) desired.  If one type, then
                # all data will be parsed using it.  If a list, then the data will be sequentially
                # parsed using each type.  Finally, the target data will be extracted from each
                # parsed item, and added to the result.  For example, for the parsed SSTRING
                # 
                #     data = { "SSTRING": {"length": 3, "string": "abc"}}
                # 
                # we just want to return data['SSTRING.string'] == "abc"; each recognized CIP type
                # has a data path which we'll use to extract just the result data.  If a
                # user-defined type is supplied, of course we'll just return the full result.
                source		= cpppo.peekable( bytes( bytearray( val ))) # Python2/3 compat.
                res		= []
                typ_is_list	= is_listlike( typ )
                typ_dat		= list( types_decode( typ if typ_is_list else [typ] ))
                for t,d in typ_dat:
                    with t() as machine:
                        while source.peek() is not None: # More data available; keep parsing.
                            data= cpppo.dotdict()
                            for m,s in machine.run( source=source, data=data ):
                                assert not ( s is None and source.peek() is None ), \
                                    "Data exhausted before completing parsing a %s" % ( t.__name__, )
                            res.append( data[d] if d else data )
                            # If t is the only type, keep processing it 'til out of data...
                            if len( typ_dat ) == 1:
                                continue
                            break
                typ_types	= [t for t,_ in typ_dat] if typ_is_list else typ_dat[0][0]
                yield res,(sts,(att,typ_types,uni))
Пример #18
0
    def read_details( self, attributes ):
        """Assumes that self.gateway has been established; does not close_gateway on Exception.  If you
        use this interface, ensure that you maintain the gateway (eg. ):

            via = proxy( 'hostname' )
            with via:
                for val,(sts,(att,typ,uni) in via.read_details( [...] ):

        Read the specified CIP Tags/Attributes in the string or iterable 'attributes', using Read
        Tag [Fragmented] (returning the native type), or Get Attribute Single/All (converting it to
        the specified EtherNet/IP CIP type(s)).

        The reason iterables are used and a generator returned, is to allow the underlying
        cpppo.server.enip.client connector to aggregate multiple CIP operations using Multiple
        Service Packet requests and/or "pipeline" multiple requests in-flight, while receiving the
        results of earlier requests.

        The 'attributes' must be either a simple string Tag name (no Type, implying the use of
        *Logix Read Tag [Fragmented] service), eg:

            "Tag"

        or an iterable containing 2 or 3 values; a Tag/address, a type/types (may be None, to force
        Tag I/O), and an optional description (eg. Units)

            [
                "Tag",
                ( "Tag", None, "kWh" ),
                ( "@1/1/1", "INT" )
                ( "@1/1/1", "INT", "Hz" )
                ( "@1/1", ( "INT", "INT", "INT", "INT", "INT", "DINT", "SSTRING", "USINT" ))
                ( "@1/1", ( "INT", "INT", "INT", "INT", "INT", "DINT", "SSTRING", "USINT" ), "Identity" )
            ]

        Produces a generator yielding the corresponding sequence of results and details for the
        supplied 'attributes' iterable.  Each individual request may succeed or fail with a non-zero
        status code (remember: status code 0x06 indicates successful return of a partial result).

        Upon successful I/O, a tuple containing the result value and details about the result (a
        status, and the attribute's details (address, type, and units)) corresponding to each of the
        supplied 'attributes' elements is yielded as a sequence.  Each result value is always a list
        of values, or None if the request failed:

            (
                ([0],(0, ("Tag", parser.INT, None))),
                ([1.23],(0, "Tag", parser.REAL, "kWh"))),
                ([1], (0, ("@1/1/1", parser.INT, None))),
                ([1], (0, ("@1/1/1", parser.INT, "Hz"))),
                ([1, 2, 3, 4, 5 6, "Something", 255],
                    (0, ("@1/1", [
                        parser.INT, parser.INT, parser.INT,  parser.INT,
                        parser.INT, parser.DINT, parser.STRING, parser.USINT ], None ))),
                ([1, 2, 3, 4, 5 6, "Something", 255],
                    (0, ("@1/1", [
                        parser.INT, parser.INT, parser.INT,  parser.INT,
                        parser.INT, parser.DINT, parser.STRING, parser.USINT ], "Identity" ))),
            )

        The read_details API raises exception on failure to parse request, or result data type
        conversion problem.  The simple 'read' API also raises an Exception on attribute access
        error, the return of failure status code.  Not all of these strictly necessitate a closure
        of the EtherNet/IP CIP connection, but should be sufficiently rare (once configured) that
        they all must signal closure of the connection gateway (which is re-established on the next
        call for I/O).

        EXAMPLES

            proxy		= enip_proxy( '10.0.1.2' )
            try:
                with contextlib.closing( proxy.read( [ ("@1/1/7", "SSTRING") ] )) as reader: # CIP Device Name
                    value	= next( reader )
            except Exception as exc:
                proxy.close_gateway( exc )

            # If CPython (w/ reference counting) is your only target, you can use the simpler:
            proxy		= enip_proxy( '10.0.1.2' )
            try:
                value,		= proxy.read( [ ("@1/1/7", "SSTRING") ] ) # CIP Device Name
            except Exception as exc:
                proxy.close_gateway( exc )

        """
        if isinstance( attributes, cpppo.type_str_base ):
            attributes		= [ attributes ]

        def opp__att_typ_uni( i ):
            """Generate sequence containing the enip.client operation, and the original attribute
            specified, its type(s) (if any), and any description.  Augment produced operation with
            data type (if known), to allow estimation of reply sizes (and hence, Multiple Service
            Packet use); requires cpppo>=3.8.1.

            Yields: (opp,(att,typ,dsc))

            """
            for a in i:
                assert self.is_request( a ), \
                    "Not a valid read/write target: %r" % ( a, )
                try:
                    # The attribute description is either a plain Tag, an (address, type), or an
                    # (address, type, description)
                    if is_listlike( a ):
                        att,typ,uni = a if len( a ) == 3 else a+(None,)
                    else:
                        att,typ,uni = a,None,None
                    # No conversion of data type if None; use a Read Tag [Fragmented]; works only
                    # for [S]STRING/SINT/INT/DINT/REAL/BOOL.  Otherwise, conversion of data type
                    # desired; get raw data using Get Attribute Single.
                    parser	= client.parse_operations if typ is None else attribute_operations
                    opp,	= parser( ( att, ), route_path=device.parse_route_path( self.route_path ),
                                          send_path=self.send_path, priority_time_tick=self.priority_time_tick,
                                          timeout_ticks=self.timeout_ticks )
                except Exception as exc:
                    log.warning( "Failed to parse attribute %r; %s", a, exc )
                    raise
                # For read_tag.../get_attribute..., tag_type is never required; but, it is used (if
                # provided) to estimate data sizes for Multiple Service Packets.  For
                # write_tag.../set_attribute..., the data has specified its data type, if not the
                # default (INT for write_tag, SINT for set_attribute).
                if typ is not None and not is_listlike( typ ) and 'tag_type' not in opp:
                    t		= typ
                    if isinstance( typ, cpppo.type_str_base ):
                        td	= self.CIP_TYPES.get( t.strip().lower() )
                        if td is not None:
                            t,d	= td
                    if hasattr( t, 'tag_type' ):
                        opp['tag_type'] = t.tag_type

                log.detail( "Parsed attribute %r (type %r) into operation: %r", att, typ, opp )
                yield opp,(att,typ,uni)

        def types_decode( types ):
            """Produce a sequence of type class,data-path, eg. (parser.REAL,"SSTRING.string").  If a
            user-supplied type (or None) is provided, data-path is None, and the type is passed.

            """
            for t in typ if is_listlike( typ ) else [ typ ]:
                d		= None 		# No data-path, if user-supplied type
                if isinstance( t, cpppo.type_str_base ):
                    td		= self.CIP_TYPES.get( t.strip().lower() )
                    assert td, "Invalid EtherNet/IP CIP type name %r specified" % ( t, )
                    t,d		= td
                assert type( t ) in (type,type(None)), \
                    "Expected None or CIP type class, not %r" % ( t, )
                yield t,d

        # Get duplicate streams; one to feed the the enip.client's connector.operate, and one for
        # post-processing based on the declared type(s).
        operations,attrtypes	= itertools.tee( opp__att_typ_uni( attributes ))

        # Process all requests w/ the specified pipeline depth, Multiple Service Packet
        # configuration.  The 'idx' is the EtherNet/IP CIP request packet index; 'i' is the
        # individual I/O request index (for indexing att/typ/operations).
        # 
        # This Thread may block here attempting to gain exclusive access to the cpppo.dfa used
        # by the cpppo.server.enip.client connector.  This uses a threading.Lock, which will raise
        # an exception on recursive use, but block appropriately on multi-Thread contention.
        # 
        # assert not self.gateway.frame.lock.locked(), \
        #     "Attempting recursive read on %r" % ( self.gateway.frame, )
        log.info( "Acquiring gateway connection: %s",
                      "locked" if self.gateway.frame.lock.locked() else "available" )
        with self.gateway as connection: # waits 'til any Thread's txn. completes
            for i,(idx,dsc,req,rpy,sts,val) in enumerate( connection.operate(
                    ( opr for opr,_ in operations ),
                    depth=self.depth, multiple=self.multiple, timeout=self.timeout )):
                log.detail( "%3d (pkt %3d) %16s %-12s: %r ", 
                                i, idx, dsc, sts or "OK", val )
                opr,(att,typ,uni) = next( attrtypes )
                if typ is None or sts not in (0,6) or val in (True,None):
                    # No type conversion; just return whatever type produced by Read Tag.  Also, if
                    # failure status (OK if no error, or if just not all data could be returned), we
                    # can't do any more with this value...  Also, if actually a Write Tag or Set
                    # Attribute ..., then val True/None indicates success/failure (no data returned).
                    yield val,(sts,(att,typ,uni))
                    continue

                # Parse the raw data using the type (or list of types) desired.  If one type, then
                # all data will be parsed using it.  If a list, then the data will be sequentially
                # parsed using each type.  Finally, the target data will be extracted from each
                # parsed item, and added to the result.  For example, for the parsed SSTRING
                # 
                #     data = { "SSTRING": {"length": 3, "string": "abc"}}
                # 
                # we just want to return data['SSTRING.string'] == "abc"; each recognized CIP type
                # has a data path which we'll use to extract just the result data.  If a
                # user-defined type is supplied, of course we'll just return the full result.
                source		= cpppo.peekable( bytes( bytearray( val ))) # Python2/3 compat.
                res		= []
                typ_is_list	= is_listlike( typ )
                typ_dat		= list( types_decode( typ if typ_is_list else [typ] ))
                for t,d in typ_dat:
                    with t() as machine:
                        while source.peek() is not None: # More data available; keep parsing.
                            data= cpppo.dotdict()
                            for m,s in machine.run( source=source, data=data ):
                                assert not ( s is None and source.peek() is None ), \
                                    "Data exhausted before completing parsing a %s" % ( t.__name__, )
                            res.append( data[d] if d else data )
                            # If t is the only type, keep processing it 'til out of data...
                            if len( typ_dat ) == 1:
                                continue
                            break
                typ_types	= [t for t,_ in typ_dat] if typ_is_list else typ_dat[0][0]
                yield res,(sts,(att,typ_types,uni))
Пример #19
0
def test_decode():
    # Test decode of regexes over bytes data.  Operates in raw bytes symbols., works in Python 2/3.
    source = cpppo.peekable('π'.encode('utf-8'))
    data = cpppo.dotdict()
    with cpppo.string_bytes('pi',
                            initial='.*',
                            greedy=True,
                            context='pi',
                            decode='utf-8') as machine:
        for i, (m, s) in enumerate(machine.run(source=source, data=data)):
            log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                     m.name_centered(), i, s, source.sent, source.peek(), data)
        assert i == 3
        assert source.sent == 2
        assert data.pi == 'π'

    if sys.version_info[0] < 3:
        # Test regexes over plain string data (no decode required).  Force non-unicode (counteracts
        # import unicode_literals above).  We can't use greenery.lego regexes on unicode data in
        # Python 2...
        source = cpppo.peekable(str('pi'))
        data = cpppo.dotdict()
        with cpppo.string('pi', initial='.*', greedy=True,
                          context='pi') as machine:
            for i, (m, s) in enumerate(machine.run(source=source, data=data)):
                log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                         m.name_centered(), i, s, source.sent, source.peek(),
                         data)
            assert i == 3
            assert source.sent == 2
            assert data.pi == 'pi'

    else:
        # Test regexes over Python 3 unicode string data (no decode required).  Operates in native
        # unicode symbols.
        source = cpppo.peekable('π')
        data = cpppo.dotdict()
        with cpppo.string('pi', initial='.*', greedy=True,
                          context='pi') as machine:
            for i, (m, s) in enumerate(machine.run(source=source, data=data)):
                log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                         m.name_centered(), i, s, source.sent, source.peek(),
                         data)
            assert i == 2
            assert source.sent == 1
            assert data.pi == 'π'

    source = cpppo.peekable(str('123'))
    data = cpppo.dotdict()
    with cpppo.integer('value') as machine:
        for i, (m, s) in enumerate(machine.run(source=source, data=data)):
            log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                     m.name_centered(), i, s, source.sent, source.peek(), data)
        assert i == 4
        assert source.sent == 3
        assert data.integer == 123

    source = cpppo.peekable('123'.encode('ascii'))
    data = cpppo.dotdict()
    with cpppo.integer_bytes('value') as machine:
        for i, (m, s) in enumerate(machine.run(source=source, data=data)):
            log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                     m.name_centered(), i, s, source.sent, source.peek(), data)
        assert i == 4
        assert source.sent == 3
        assert data.integer == 123

    # Try using a integer (str) parser over bytes data.  Works in Python 2, not so much in Python 3
    try:
        source = cpppo.peekable('123'.encode('ascii'))
        data = cpppo.dotdict()
        with cpppo.integer('value') as machine:
            for i, (m, s) in enumerate(machine.run(source=source, data=data)):
                log.info("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                         m.name_centered(), i, s, source.sent, source.peek(),
                         data)
            assert i == 4
            assert source.sent == 3
            assert data.integer == 123
        assert sys.version_info[0] < 3, \
            "Should have failed in Python3; str/bytes iterator both produce str/int"
    except AssertionError:
        assert not sys.version_info[0] < 3, \
            "Shouldn't have failed in Python2; str/bytes iterator both produce str"
Пример #20
0
    def __next__(self):
        """Return the next available response, or None if no complete response is available.  Raises
        StopIteration (cease iterating) on EOF.  Any other Exception indicates a client failure,
        and should result in the client instance being discarded.
        
        If no input is presently available, harvest any input immediately available; terminate on EOF.

        The response may not actually contain a payload, eg. if the EtherNet/IP header contains a
        non-zero status.
        """
        if self.source.peek() is None:
            rcvd = network.recv(self.conn, timeout=0)
            log.info(
                "EtherNet/IP-->%16s:%-5d rcvd %5d: %s",
                self.addr[0],
                self.addr[1],
                len(rcvd) if rcvd is not None else 0,
                repr(rcvd),
            )
            if rcvd is not None:
                # Some input (or EOF); source is empty; if no input available, terminate
                if not len(rcvd):
                    raise StopIteration
                self.source.chain(rcvd)
            else:
                # Don't create parsing engine 'til we have some I/O to process.  This avoids the
                # degenerate situation where empty I/O (EOF) always matches the empty command (used
                # to indicate the end of an EtherNet/IP session).
                if self.engine is None:
                    return None

        # Initiate or continue parsing input using the machine's engine; discard the engine at
        # termination or on error (Exception).  Any exception (including cpppo.NonTerminal) will be
        # propagated.
        result = None
        with self.frame as machine:
            try:
                if self.engine is None:
                    self.data = cpppo.dotdict()
                    self.engine = machine.run(source=self.source, data=self.data)
                    log.debug(
                        "EtherNet/IP   %16s:%-5d run.: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0],
                        self.addr[1],
                        machine.name_centered(),
                        machine.current,
                        self.source.sent,
                        self.source.peek(),
                        self.data,
                    )

                for m, s in self.engine:
                    log.debug(
                        "EtherNet/IP<--%16s:%-5d rpy.: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0],
                        self.addr[1],
                        machine.name_centered(),
                        s,
                        self.source.sent,
                        self.source.peek(),
                        self.data,
                    )
            except Exception as exc:
                log.warning("EtherNet/IP<x>%16s:%-5d err.: %s", self.addr[0], self.addr[1], str(exc))
                self.engine = None
                raise
            if machine.terminal:
                log.info(
                    "EtherNet/IP   %16s:%-5d done: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                    self.addr[0],
                    self.addr[1],
                    machine.name_centered(),
                    machine.current,
                    self.source.sent,
                    self.source.peek(),
                    self.data,
                )
                # Got an EtherNet/IP frame.  Return it (after parsing its payload.)
                self.engine = None
                result = self.data

        # Parse the EtherNet/IP encapsulated CIP frame, if any.  If the EtherNet/IP header .size was
        # zero, it's status probably indicates why.
        if result is not None and "enip.input" in result:
            with self.cip as machine:
                for m, s in machine.run(path="enip", source=cpppo.peekable(result.enip.input), data=result):
                    log.debug(
                        "EtherNet/IP<--%16s:%-5d CIP : %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0],
                        self.addr[1],
                        machine.name_centered(),
                        s,
                        self.source.sent,
                        self.source.peek(),
                        self.data,
                    )
                    pass
                assert machine.terminal, "No CIP payload in the EtherNet/IP frame: %r" % (result)

        # Parse the Logix request responses in the EtherNet/IP CIP payload's CPF items
        if result is not None and "enip.CIP.send_data" in result:
            for item in result.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 self.lgx as machine:
                        for m, s in machine.run(
                            source=cpppo.peekable(item.unconnected_send.request.input),
                            data=item.unconnected_send.request,
                        ):
                            pass
                        assert machine.terminal, "No Logix request in the EtherNet/IP CIP CPF frame: %r" % (result)

        return result
Пример #21
0
def test_decode():
    # Test decode of regexes over bytes data.  Operates in raw bytes symbols., works in Python 2/3.
    source			= cpppo.peekable( 'π'.encode( 'utf-8' ))
    data			= cpppo.dotdict()
    with cpppo.string_bytes( 'pi', initial='.*', greedy=True, context='pi', decode='utf-8' ) as machine:
        for i,(m,s) in enumerate( machine.run( source=source, data=data )):
            log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                      i, s, source.sent, source.peek(), data )
        assert i == 3
        assert source.sent == 2
        assert data.pi == 'π'
    
    if sys.version_info[0] < 3:
        # Test regexes over plain string data (no decode required).  Force non-unicode (counteracts
        # import unicode_literals above).  We can't use greenery.lego regexes on unicode data in
        # Python 2...
        source			= cpppo.peekable( str( 'pi' ))
        data			= cpppo.dotdict()
        with cpppo.string( 'pi', initial='.*', greedy=True, context='pi' ) as machine:
            for i,(m,s) in enumerate( machine.run( source=source, data=data )):
                log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                          i, s, source.sent, source.peek(), data )
            assert i == 3
            assert source.sent == 2
            assert data.pi == 'pi'

    else:
        # Test regexes over Python 3 unicode string data (no decode required).  Operates in native
        # unicode symbols.
        source			= cpppo.peekable( 'π' )
        data			= cpppo.dotdict()
        with cpppo.string( 'pi', initial='.*', greedy=True, context='pi' ) as machine:
            for i,(m,s) in enumerate( machine.run( source=source, data=data )):
                log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                          i, s, source.sent, source.peek(), data )
            assert i == 2
            assert source.sent == 1
            assert data.pi == 'π'

    source			= cpppo.peekable( str( '123' ))
    data			= cpppo.dotdict()
    with cpppo.integer( 'value' ) as machine:
        for i,(m,s) in enumerate( machine.run( source=source, data=data )):
            log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                      i, s, source.sent, source.peek(), data )
        assert i == 4
        assert source.sent == 3
        assert data.integer == 123


    source			= cpppo.peekable( '123'.encode( 'ascii' ))
    data			= cpppo.dotdict()
    with cpppo.integer_bytes( 'value' ) as machine:
        for i,(m,s) in enumerate( machine.run( source=source, data=data )):
            log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                      i, s, source.sent, source.peek(), data )
        assert i == 4
        assert source.sent == 3
        assert data.integer == 123

    # Try using a integer (str) parser over bytes data.  Works in Python 2, not so much in Python 3
    try:
        source			= cpppo.peekable( '123'.encode( 'ascii' ))
        data			= cpppo.dotdict()
        with cpppo.integer( 'value' ) as machine:
            for i,(m,s) in enumerate( machine.run( source=source, data=data )):
                log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
                          i, s, source.sent, source.peek(), data )
            assert i == 4
            assert source.sent == 3
            assert data.integer == 123
        assert sys.version_info[0] < 3, \
            "Should have failed in Python3; str/bytes iterator both produce str/int"
    except AssertionError:
        assert not sys.version_info[0] < 3, \
            "Shouldn't have failed in Python2; str/bytes iterator both produce str"