Efficiency and Pylogix

 

I receive quite a few questions about how to speed pylogix up.  It is important to understand how pylogix reads/writes tags, once we have a good understanding, there are a few different strategies, depending on your usage.  I’ll cover a few use cases.

 

How Pylogix Reads/Writes Tags

When you create an instance of pylogix, there is a dictionary KnownTags that stores each unique tag name read and its data type. The data type is necessary for a write because the PLC needs to know how many bytes you are sending.  These data types are stored in the dictionary CIPTypes.  I’m only going to cover the basic types, not STRUCTS (UDT’s, CONTROL, TIMER, COUNTER, etc.).  Dealing with STRUCTS is a different subject.

 

Since the PLC needs to know the type when writing a value to a tag, the first time a user reads or writes, I first make a read to get the type, that is saved in KnownTags dictionary.  The next time you interact with the tag, I used the saved type.  Of course, this happens within the same instance of PLC, if you create a new instance, the memory of known tags will be lost.  This process isn’t the most efficient, I chose user simplicity over pure performance. 

 

With a simple script to read a single tag, if you were to watch the process in Wireshark, you would see the following:

  •          Register Session
  •          Forward Open (specifies the connection parameters)
  •          Symbolic Read (to get data type)
  •          Symbolic Read (to get value)
  •          Forward Close
  •          Unregister Session

 

When discussing efficiency using pylogix, it is that Symbolic Read to get the data type that we are talking about.  Register/Unregister/FowardOpen/ForwardClose will happen once per new connection and the Symbolic Read must happen each time you request the value with Read (or write a value with Write).

 

Being More Efficient

There are a few things as a user you can do to be more efficient if performance is that important to you. When reading many unique tags, providing the data type up front will save the discovery packet.  An example:

ret = comm.Read("ProductPointer", datatype=0xc4) # 0xc4 = DINT

 

Personally, I almost never use this, that is mainly because for my use cases, the extra time typing in the data type takes longer than letting pylogix get it for me.  If you are going to connect and read 500 unique tags then get out, it might make sense to give the data type up front, this would save 500 packets.

 

Probably the most effective way to be efficient is to read/write using lists.  Providing the Read method a list of tags allows pylogix to pack multiple requests into a single packet.  These days, pylogix will pack the data-type discovery and value read/write in to packets.  The number that fits in a request depends on the data type and connection size.  Modern controllers use a packet size of 4002 bytes. Older controllers only support 508-byte packets.

 

Of course, you can utilize both lists and provide the data type to gain more efficiency.  When utilizing lists, you must also provide the length of the read, even if it is just 1.

 

For ultimate performance, arrange your data in the PLC in arrays.  Reading/Writing arrays is by far the most efficient way to transfer data.  This will often mean more work in the PLC to organize the data, but if maximum performance is required, there is no faster way.

 

Use Case Examples

We’ll go over a few use cases and show the performance of each.  We’re going to read 10 tags, showing the various ways it can be done.  Gaining efficiency with each example.  I’ll be using a 5069 CompactLogix controller, which is very fast, these tests would be slower with the older series, or a ControlLogix with EN2T.  The first example, I’d consider wrong, but I see it happen a lot, so we’ll demonstrate it.  We’re going to run each test 20 times and take the average time.  Here are the tags:

BaseDINT, BaseSTRING, BaseINT, BaseREAL, BaseSINT, BaseLINT, BaseTIMER.ACC, BaseBOOL, BaseCOUNTER.PRE, BaseBITS.0

 

 

First Test (Wrong)

    tags = ["BaseDINT", "BaseSTRING", "BaseINT", "BaseREAL", "BaseSINT", "BaseLINT",             "BaseTIMER.ACC""BaseBOOL", "BaseCOUNTER.PRE", "BaseBITS.0"]
 
    for tag in tags:
        with pylogix.PLC("192.168.1.10") as comm:
            comm.Read(tag)

 

In this test, we read the tags one at a time, what we did incorrectly is create an instance of PLC with each read.  This will open/close a connection with each read.  Not good.  This test took an average of 152.255ms and took 222 packets. 

The packet count was high because each read included register/unregister session and forward open/forward close.  I show this example because I see users make this mistake.  It’s easy to do an not realize the impact.

 

Second Test (Corrected)

    tags = ["BaseDINT", "BaseSTRING", "BaseINT", "BaseREAL", "BaseSINT",                        "BaseLINT", "BaseTIMER.ACC","BaseBOOL", "BaseCOUNTER.PRE", "BaseBITS.0"]
 
    with pylogix.PLC("192.168.1.10") as comm:
        for tag in tags:
            comm.Read(tag)

 

In this test, we reversed the order of the with/for.  Create an instance of PLC, then iterate the tag list.  Doing this will keep the connection open, using one connection, which eliminates all the extra sessions.  This test took an average of 30.639 and 69 packets.  A lot better, in fact, the biggest gain we’ll get.  We can improve a little more though.

 

Third Test (Read using Lists)

    tags = ["BaseDINT", "BaseSTRING", "BaseINT", "BaseREAL", "BaseSINT", "BaseLINT",             "BaseTIMER.ACC""BaseBOOL", "BaseCOUNTER.PRE", "BaseBITS.0"]
 
    with pylogix.PLC("192.168.1.10") as comm:
        comm.Read(tags) 

The tags are already in list format, so this time we’ll use it.  Not only will it be faster, but the code is simplified.  This test took an average of 25.834ms and took 20 packets.  The reason for the reduction in packets is because multiple tag reads are packed into a packet.  In this case, one packet to get all 10 data types and one packet to get the value.

 

Fourth Test (Include Data Type)

    tags = [("BaseDINT", 1, 0xc4), ("BaseSTRING", 1, 0xa0), ("BaseINT", 1, 0xc3),                ("BaseREAL", 1, 0xca), ("BaseSINT", 1, 0xc2), ("BaseLINT", 1, 0xc5),                ("BaseTIMER.ACC", 1, 0xc4), ("BaseBOOL", 1, 0x0c1),
            ("BaseCOUNTER.PRE", 1, 0xc4), ("BaseBITS.0", 1, 0xc1)]
 
    with pylogix.PLC("192.168.1.10") as comm:
        comm.Read(tags)

 

This test includes the data type with the read, which eliminates the packet to get the type.  The number of packets was reduced to 12 and took an average of 17.256ms.  I’m personally not a fan of this much optimization, the extra time it took to type out the list in that format wasn’t worth the 8ms saved, but your application may benefit from it.

Comments

Popular Posts