PAC Control 201: Subroutines! (Roll your own command)


#1

Subroutines are fun and helpful because they let you build your own PAC Control command, which you can then use over and over. Before you use subroutines, be sure to read the fabulous manual ([B][U]form 1700, The PAC Control Users Guide, Chapter 12: Using Subroutines[/U][/B]).

In this post I’ll show you an example, give some of the pros/cons to using a subroutine vs. not, and cover some of the more subtle points related to passing parameters. This is NOT a step-by-step post, so be sure to read the “Using Subroutines” chapter for that. You can download and open the subroutine example so you can see what’s in it.

EXAMPLE - Take an average

Let’s suppose I wanted a command to calculate the average of some values in a Numeric Table. I check the built-in commands, I even check the OptoForums (lots of good samples and donated code there!), but alas, I find no such existing built-in command or subroutine.

I could write some code in my chart, but what if I wanted to use it again somewhere else, or in another strategy? I’d have to copy/paste, which is time-consuming and could be dangerous, as we talk about in [B][U]this video[/U][/B].

But if I put my logic in a subroutine, I can add it to a chart like a regular command. It’s shown in the list of instructions:

I’m even prompted for the parameters I’ve specified myself:

This even works for our handy “Include parameter helps” option for OptoScript users, which inserts comments to indicate the parameters required for a command:

Gives you:


Neat-o.

SUBROUTINE PROS VS. CONS

So a subroutine lets you make your own command and use it in multiple charts and strategies. As mentioned above and in the [B][U]Maintainable Code video[/U][/B], putting logic in a subroutine makes it easy to re-use that logic later.

It also decreases your odds of clobbering variables you use in one chart while running another chart–since all strategy variables, except those in a subroutine, are global and can be changed by multiple charts running at the same time.

You can even call a subroutine from more than one chart at the same time, because each chart has its own section of memory where it runs a unique copy of the subroutine, without sharing any of its local variables with another chart.

Also, by putting logic (and its corresponding “local” variables) in a subroutine, you’ll have fewer variables cluttering up your main strategy tree: safer AND tidier!

But there are a few downsides to using subroutines.

[B]Relative Paths[/B]

For versions 9.3 and earlier, subroutines had absolute paths with no option of relative paths. What does this mean? If you archive your strategy, send it to someone else, and/or open the archive on a different PC with a different file structure, you’ll have to tell your strategy where those subs are located. (Usually they’re all in the same place, though, so you only have to do this once.)
[B]Note: [/B]Versions 9.4 and newer are better/smarter about paths for subroutines. When you open a strategy, PAC Control is more likely to automatically find the corresponding subs for you.

Speaking of paths and file locations–use caution when the subroutine file is located on a shared or networked drive (where the connection could be lost or files get locked up while “sync’d”). I’d recommend keeping all your strategy files on your local hard drive while you’re working with them. When you’re finished, make sure you back up the archive to a safe place, including on the SNAP PAC controller itself!

[B]Only One Deep - until 9.5[/B]

If you’re using PAC Control software and PAC firmware pre-9.4, you’ll have this “only one deep” limitation: unlike a chart, you can’t have a subroutine call another subroutine in PAC Control. That means if you already have some code you’d like to make into a subroutine, but that code calls a subroutine, you’ll need to copy all the logic into one big subroutine. For example, if I wanted to use this [B][U]64-bit encoding[/U][/B] sub in another sub that needs to do this (perhaps to control an IP camera that expects an encrypted username/password), I just have to copy the relevant blocks into my new subroutine. Again, not a big deal, just a bit inconvenient. But if you have 9.5 firmware/PAC Control, you CAN have subroutines call subroutines, so perhaps that’s a good reason to upgrade!

[B]Type Limitations[/B]

You’ll notice when you try to copy some logic from a chart to a subroutine that not all types can be pasted directly into a subroutine. Also, some types cannot be passed into the subroutine.

If you’ve copied some blocks from a chart and try to paste them into a subroutine, and they include one of these types, you’ll get this friendly reminder from PAC Control:

Note you CAN pass a pointer table to a subroutine, which can be especially handy if you have a large number of values to pass in (more than the max of 12 parameters).

Let’s have a look at what you CAN (and should) do with passed-in parameters.

PASSING PARAMETERS

If you copy logic from a chart into a new subroutine you created by selecting Subroutine > New, you’ll get a Paste Report which is very similar to what you’ll see when importing a chart into a strategy.

This is a good time to decide which of those variables should be parameters that are passed into the subroutine by the chart that will call it, and which should be variables that are used only locally within the subroutine and don’t correspond to anything outside the sub.

For example, to calculate an average of some elements in a table, I have a running total called [B]fRunningTotal [/B]where I’ll add up all the values I want to average, before I divide it by the total number of values, to get the [B]fAverageValueToReturn[/B].

That [B]fRunningTotal [/B]variable is just for temporary use in the calculation, so it should be strictly local. On the other hand, the [B]fAverageValueToReturn [/B]value is what we need to return, so it MUST be a passed parameter (and can’t be a literal; more on that later).

PAC Control has this handy feature where, if you name your new passed-in parameter with the same name as one of those existing variables, it will ask you if you want to convert it!

Neat-o!

While we’re on parameters, let me highlight an important point mentioned in the manual:

Although most variables passed in and out of a subroutine must be of a specific type, literals that are passed into subroutines can take several types. Using a string literal, you can pass in either a string literal or a string variable. Using a numeric literal, you can pass in an analog point, a digital point, an integer variable, a float variable, or a timer variable…This flexibility in using literals makes it easier to use a subroutine in multiple strategies. For example, a literal passed into a subroutine from two strategies might be a float value in one strategy and an analog point in the other.
Mary’s rule of thumb: always make a passed parameter a “literal” if you can.

Let’s look more closely at this [B]GetTableAverage [/B]example.


Let’s consider a couple of the passed parameters circled above. The [B]Average Value[/B] (#4) is the point of this whole function–what the subroutine calculates–so I can’t pick literal there. I have to pass in a variable that will get filled in by the subroutine.

The [B]Number of Values[/B] to average, however, is not something that this subroutine will change. In fact, I might want to just pass, say, a literal 10 to the subroutine vs. a variable with the value of 10. If I choose a non-literal type for that parameter, I’ll HAVE to create a variable of that type in any chart that calls this subroutine. But if the parameter is a literal, I can pass in more than one type, so one chart can pass in an analog input and another a float or integer.

If I’m using OptoScript, I could even pass the return value of the call to another command, as shown here:


Okay, now that we’ve passed some values into the subroutine, let’s see how the sub is actually put together. Here I’m doing a simple average, which can be done in just a few lines of code (as I do in OptoScript in the guts of this GetTableAverage subroutine):


But in the subroutine I add lots of comments and more OptoScript before I do the actual calculation, so I’ll have more control over things that could possibly go wrong. Error handling is a whole topic in itself, but you can see some of what I’ve done in the example. For one thing, I’ll return a string which tells what went wrong.

Because this subroutine with its literal parameters can accept all kinds of different inputs and could be called from all kinds of charts or strategies, I’ll take special care to anticipate potential problems that would mess up an average. For example, if we average 0 elements we could get a divide-by-0 error. Or if we request a range that goes off the end of the table passed, we’d get another error. In both of these cases, you’d end up with errors in the message queue which could mess up any message-queue handling logic you may already have in place.

I’d also like to make my life easier by adding the -1 option for the “Number of Parameters” value, like we have in many of the built-in table commands, to say: “give me the whole thing.”

That’s all handled here in the first block of my subroutine.


One final side note: I like to make the backgrounds of my subroutines yellow to distinguish them from charts. And because we love to have a yellow subroutine! Har!

Oh, and did we mention [B][U]a certain green submarine[/U][/B] uses subroutines too?

Double neat-o.

-Mary

Example strategy: (requires R9.3 PAC Control Basic or better) TableTricks.Archive.D04292014.T162343.zip (8.19 KB)


Opto22 to External Serial Devices
Modbus - getting multiple reads when I only want one
Greatest Hits - and mini-lessons
#2

To go with this set of “Table Tricks,” here’s another subroutine that does a few more statistical calculations on a table, including: mean, median, variance and standard deviation.

Just include this subroutine in your 9.3 or newer PAC Control strategy and you’ll have a new “GetTableStats” commands show up in your list of command options.

Happy Calculating!

-OptoMary


#3

Hi Mary,
I have a quick question. When I start a subroutine in an action block or a script block, do I have to wait for it to be finished or can I set it up so that my chart keeps running. I need to open valves with a subroutine, but each valve opening takes 20sec and I don’t want to freeze my strategy.
Thanks for your help!

-Clement


#4

Hello crowhurst/Clement,

Excellent question!

Short answer: you wait for it to finish, just like when you do a “Call Chart” (vs. a “Start Chart”) command.

However, although the chart that calls the subroutine is “suspended” – all your other charts are still running. (So you wouldn’t “freeze” the [I]whole [/I]strategy, just whichever chart called the subroutine.)

How many charts do you currently have running at once? What is it in the sub that takes the 20 seconds? Could you possibly split out the sub into more than one? Can you tell us a little more about your overall process? I’m betting there’s several ways to approach this!

-OptoMary


#5

Hi Mary,
Well I have 4 valves who require a somewhat complicate chain of event to open or close, they all are identical. So I wrote a subroutine to handle the open and close sequence, then I wrote a chart who monitors if the user is asking a valve to open up. so it contains 4 condition blocks in series (change valve 1 status? if false go to next). When the user requests that a valve opens, he has to wait for this valve to be opened before he can open another one, kind of annoying.
Ideally, I’d like to have an interrupt. I’ve programmed micro-controllers before, where you could run a chunk of code when a logic pin gets switched. Is there such thing with Opto?
Currently in the code, the 20 secs delay is caused by the valve itself, so I cannot speed things up.

Thanks for your help
Clement


#6

Hi Clement,

There are a few ways you might handle this. Perhaps the simplest, conceptually, and given what you’ve already written, might be to add 1 chart for each valve. Let’s say they’re called: Valve1Control, Valve2Control, Valve3Control, Valve4Control. Each of these just calls your subroutine for that particular valve.

Then, in your main chart you can have “change value 2 to open?” if so, you’d START the appropriate Valve1Control, etc. chart. Then the main chart continues on after calling “Start Chart” (vs. “Call Chart” which would wait), [I]while[/I] that Valve2Control chart does its thing.

Make sense?

I can think of a few other options/variations, too, depending on your speed requirements (how long between when you detect the need to open and when you need to start that subroutine)? Can it be a few seconds? Dozen milliseconds? Milliseconds?

-OptoMary


#7

Hi Mary,Yep, it makes sense. I will start charts every time, it shouldn’t be too bad given there’s only 4 of them. To answer your question, I don’t need something super snappy, a couple seconds of delay would be acceptable.Thanks again for your help!-Clement


#8

So, internal variables are reset/can be overwritten by the next go-through? Can I run the same subroutine concurrently on multiple charts? How do I end a subroutine?


#9

Yes, the [I]local[/I] Variables in a sub all go away when the subroutine is done running. (Vs. the Passed Parameters – specifically the variables rather than literals – can be altered by the subroutine. That’s how you’d pass something back like an error code or a value the subroutine calculates, for example.)

Yes, you can run the same subroutine on multiple charts (each chart has it’s own copy).

Subroutine ends the same way a chart ends, when it’s done doing all it’s block. (Usually, you wouldn’t have it looping – the logic should flow through so you return to the place in your chart where you originally called it.)

I hope that makes sense!


#10

Thank you for the GetTableStats sub OptoMary! Saved me some time : )

One caveat I found when calculating median with this sub is the data must be sorted prior to the call otherwise you just get the middle table values rather than the middle data values.

Also, I had to modify the following line in block 7 to allow median calculations on data that did not start at index 0
from:

nMiddleIndex = Truncate(nValuesToAverage / 2);

to:

nMiddleIndex = Truncate(nValuesToAverage / 2) + nStartIndex;