Ejemplo n.º 1
0
    def is_request(req):
        """Return True iff the given item is potentially a read/write request target:
        
            <address>		-- "Tag|@<Class>/<Instance>/<Attribute>"
            ( <address>, <CIP-type> [, <units> ] )
            ( <address>, "CIP-type-name" [, <units> ] )
            ( <address>, (<CIP-type>, <CIP-type>, ...) [, <units> ])

        No validation of the provided <units> is done; it is passed thru unchanged.
        """
        log.detail("Validating request: %r", req)
        if isinstance(req, cpppo.type_str_base):
            return True
        if cpppo.is_listlike(req) and 2 <= len(req) <= 3:
            try:
                add, typ, _ = req
            except ValueError:
                add, typ = req
            if isinstance(add, cpppo.type_str_base):
                if isinstance(typ, (cpppo.type_str_base, type)):
                    return True
                if cpppo.is_listlike(typ):
                    if all(
                            isinstance(t, (cpppo.type_str_base, type))
                            for t in typ):
                        return True
        return False
Ejemplo n.º 2
0
        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 cpppo.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 cpppo.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)
Ejemplo n.º 3
0
        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 ( types if cpppo.is_listlike( types ) else [ types ] ):
                d		= None 		# No data-path, if user-supplied type
                if isinstance( t, int ):
                    # a CIP type number, eg 0x00ca == 202 ==> 'REAL'.  Look for CIP parsers w/ a
                    # known tag_type and get the CIP type name string.
                    for t_str,(t_prs,_) in self.CIP_TYPES.items():
                        if getattr( t_prs, 'tag_type', None ) == t:
                            t	= t_str
                            break
                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
Ejemplo n.º 4
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 cpppo.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 cpppo.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 (types if cpppo.is_listlike(types) else [types]):
                d = None  # No data-path, if user-supplied type
                if isinstance(t, int):
                    # a CIP type number, eg 0x00ca == 202 ==> 'REAL'.  Look for CIP parsers w/ a
                    # known tag_type and get the CIP type name string.
                    for t_str, (t_prs, _) in self.CIP_TYPES.items():
                        if getattr(t_prs, 'tag_type', None) == t:
                            t = t_str
                            break
                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 %r connection: %s", self.gateway,
                 "locked" if self.gateway.frame.lock.locked() else "available")
        blocked = cpppo.timer()
        with self.gateway as connection:  # waits 'til any Thread's txn. completes
            polling = cpppo.timer()
            try:
                log.info(
                    "Operating gateway %r connection, after blocking %7.3fs",
                    self.gateway, polling - blocked)
                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 %s", i, idx, dsc, sts
                        or "OK", val,
                        repr(rpy) if log.isEnabledFor(logging.INFO) else '')
                    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
                        # [Fragmented] (always a single CIP type parser).
                        typ_num = rpy.get('read_tag.type') or rpy.get(
                            'read_frag.type')
                        if typ_num:
                            try:
                                (typ_prs, _), = types_decode(typ_num)
                                if typ_prs:
                                    typ = typ_prs
                            except Exception as exc:
                                log.info(
                                    "Couldn't convert CIP type {typ_num}: {exc}"
                                    .format(typ_num=typ_num, exc=exc))
                        # 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 = cpppo.is_listlike(typ)
                    typ_dat = list(types_decode(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))
            finally:
                log.info(
                    "Releasing gateway %r connection, after polling  %7.3fs",
                    self.gateway,
                    cpppo.timer() - polling)