Ok so that’s a really dramatic title for what we think is a pretty cool technique we’ve been working on for the last few months.
A little back-story if you will. We’re working on an application for a customer that requires us to download recipe parameters to a PLC. No big deal, right? Just use the FMO from the EOM. First problem was that due to some licensing changes, using the FMO wasn’t an option. Ok, well I guess we can just write our own FMO, that can’t be too hard. We started down this path but quickly realized that having to create multiple UDA’s for every attribute was a maintenance nightmare and not something we wanted our customers to deal with. By the way, to get a sense of scale for what this customer needed, we have a machine where we are downloading over 2000 recipe values to a single machine. Yes, that 2 THOUSAND. The machine isn’t that big but it does have a massive array of implements that all have about 16 set points so you have a matrix of about 16 * 100 for one section of the machine.. that’s how you get into thousands of formula parameters.
After batting around a number of ideas we came across what ended up being the ultimate solution, and that’s what we’re going to share with you. We’re not going to do a massive code dump and give away the keys to the kingdom (there’s lots of hours in this method) but we will show you the main idea so you can possibly implement your own solution.
It all starts with Indirects. If you’ve never used Indirects you really should learn about them. Basically an Indirect is a variable type you can declare in a script. Once you declare an indirect you call the “BindTo” function on that variable. Once you have called the BindTo you can now read and write to that indirect and the values will through to the variable you have bound to. This method can solve a ton of issues especially if you have an unknown quantity in an array or want to write a generic object that can work in a bunch of different settings. Below is a really really simple example.
Dim X as Indirect;
Dim Timer as Integer;
‘ Wait till the quality becomes good before attempting to read or write
Timer = Timer + 1;
‘ Read from X
LogMessage(“Value of X is “ + X);
’ Write to X
X = 12.3;
There is some basic reading in the help files that give you a little more information. Here’s where the magic starts. Way back in the 2.1 days there used to be a note… at least I think there was… that said you couldn’t bind across engines. I always wondered why that was? Turns out through some other investigations that hooking up to I/O that isn’t on the same engine takes at least one scan. Why does that matter? Well, if in a script you declare the Indirect at the top of the script that reference is destroyed at the end of the scan cycle. If its’ destroyed at the end of the scan cycle then how can it persist across scans to survive to hook up to another engine?
So how do you get around this limitation? Simple, move your declaration up into the rarely used declarations section of your script.
If you didn’t know, variables declared in this section are persisted across scans. Now we can declare an indirect in this section, do some binding in the script itself, and use the results of that binding in a subsequent scan. Very cool! I’ve been doing IAS for a long time and I was frankly amazed that I never put this little trick together (thanks David!). Actually after using this for this technique I’ve started using these guys a lot more in place of placeholder UDA’s for basic things like flags and multi-scan counters. Just remember, these are not checkpointed so anything that needs to persist across a failover should be a UDA, not one of these variables.
So now that we’ve got the dynamic part, what’s this hyper-scale thing?
So back to the formulas. Since the FMO was out we basically wrote our own Formula database in SQL Server. Not terribly difficult to do a basic one. We ended up blinging this one out with lots of really cool features; that part we’re not going to share. At the end of the day we make a stored procedure call to the database and it not only retrieves the basic parts of the recipe (names, values, units, limits, etc.) but it also downloads I/O references. Using these I/O references we iterate over some arrays and dynamically bind our Indirects to the appropriate I/O source. Did you notice our arrays of Indirects in our declarations? Pretty sweet huh?
So what does all this mean? This means that you can have an arbitrary set of parameters that you need to read and write to and as long as you can get a list of I/O addresses you can scale to your heart’s content.
One question you might ask is how do you see the values if they aren’t UDA’s? We solve that issue by copying out the contents to an array of strings that is dynamically scaled based on the number of I/O we’re dealing with. Super simple example below.
Dim Count as Integer;
Dim Index as Integer;
Count = Me.ValsArray.Dimension1;
For Index = 1 to Count
Me.ValsArray[Index] = IndirectValsArray[Index];
Knowing that you can access the size of an array at runtime via the Dimension1 parameter makes things a lot more flexible. Also, did you know that you can set the Dimension1 at runtime to dynamically change the size of the array? Even cooler is that if you set Dimension1 to 0 then immediately to some positive value you just cleared out the array. Is this more efficient than setting the size then iterating and clearing… don’t know, never tested that.
Back to the problem at hand.
At the end of the day what matters just as much as flexibility is how well it actually scales. We’ve tested our solution at 10,000 I/O to a Control Logix L62 PLC (yes, it’s a big array so lots of efficiency there) and we run at about 250 ms to get through the entire array reading every attribute’s value and quality and copying them into our UDA arrays so we can view them with object viewer. If you’re doing something with 10,000 I/O you might just have to accept a 250ms cycle time for something this far off the reserve. With something this big I’m thinking you might be on your own engine to avoid async issues with other objects and scripts. Something we have noticed in our testing from 1 element through 10,000 elements is that the scaling is pretty linear. That’s good to know if we needed to keep going. To be fair things may fall apart at 11,000 elements but it looks pretty good up to that 10,000.
If you were really astute, and you’ve worked with Indirects before, one question you might ask is how in the world you design a script that waits for all of the qualities to go good before reading and writing data. That’s going to be the subject of our next post, designing a system that allows for multi-scan execution of a script without using Asynchronous scripts. I know you’ll be waiting with baited breath for that one