An Improved Analog Clock

Sometimes all that digital stuff is just too bland.

A bug undocumented feature of the original analog clock was that the markers on the scale were at intervals of 1.25 seconds, a consequence of LabVIEW preferring to use 4 intervals per major tick, when we silly humans use 5.  As a result, it looked a bit odd.  As I said then, if we’re going for an analog, then let’s go for the analog.

Attempts to coerce LabVIEW into making the scale the way we wanted were not successful; it has a long habit of thinking that four is a nice number and five is just an odd number, so I could not make it work.

So how do we improve it?  Simply disregard the built-in scale and substitute a picture.  After all, we all know where the numbers are on a clock face, if we’ve lined up the 0 and the 12 on our LabVIEW scales at the top of the circle, then all else has to fall into place.

Clock2Pic

Click here to download the example LLB file, in LV 8.0 format. It has the same math VI as before (read the previous post for details), just the clock face is different.

Enjoy.

Operations en Masse

The things that I used to do…

En masse is a French term meaning “as a whole” or “all together”; treating a group of something as a single unit.   LabVIEW has the ability to treat arrays this way, which can greatly reduce your workload. If you come to LabVIEW from a text-based language, it’s easy to miss the capabilities that are right at your fingertips.

For example, if you need to scale a series of readings into percent of the total (a procedure called normalizing),  then you tend to think:

I need to find the total:

  • I need to start with a zero sum     sum = 0.0;
  • I need to loop over every element  for (int i = 0; i < nElements; i++)
  • I need to add this element to the sum   sum += array[i]

Now I need to divide each entry by the total, to get the fraction of the total:

  • for (int i = 0; i < nElements; i++)
  • array[i] /= sum;

Now I need to multiply by 100 to get percentages:

  • for (int i = 0; i < nElements; i++)
  • array[i] *= 100.0;

That’s all well and good, and you could translate that literally into LabVIEW and it will get you the answer you want to see.  But that’s not the LabVIEW way of thinking.

What newcomers often fail to realize is that most primitive numeric functions (the ones with yellowish icons) will accept an array of numbers directly. This goes for basic arithmetic (add,subtract, multiply, divide), comparisons (greater than, less than, MAX/MIN), and many other operations.  It will happily multiply an array of numbers by a single scaler number, to produce an array of numbers.

This has great power to reduce the work that you do as the programmer. Consider the literal translation of the above code:

EnMasse-11

If that’s as good as it gets then why should I go with LabVIEW?

Well, it does get better.  There is a function in the numeric palette called ADD ARRAY ELEMENTS.  If we replace the entire first loop with this function, then we get to this:

EnMasse-21

Now for the en masse parts: You can replace the entire second loop with a single operation as well:

EnMasse-31

Any guesses what we can do with the third loop?    Yes, that’s right:

EnMasse-41

Now you have SO much more room to add comments about what you’re doing!

Now THIS is what makes you more productive in LabVIEW than in C; your chances for error are far less when you let the en masse operators handle the details, and you don’t even have to think about the details.

But be aware of what’s going on, however; there is no magic here.  Under the hood there is still a loop somewhere.  It’s now hidden somewhat; it’s not as obvious, but the work is still being done.  Don’t let the simplicity obscure the real processing that’s going on.

Here is an example of the normalizing function in use, from the real LabVIEW example examples\general\graphs\charts.llb\Draw Stacked Graph.vi (in LabVIEW 8.6. anyway).

EnMasse-5

This amounts to the same as our last part above.  In the example, the array given contains five elements and this is executed only once, so efficiency is not a concern.

But consider if the array was 10,000 elements. Don’t forget that the first operation is doing 10,000 divide operations, and the second is doing 10,000 multiplications.  Can you improve things?

Well, certainly! What you have to realize is that, by the associative property of numbers, (X / sum) * 100 is equal to (100 / sum) * X.  You also have to realize that 100 / sum, in this context, is a constant, and therefore needs to be calculated only once.  In effect, you are dividing by sum and multiplying by 100, but you are doing it 10,000 times!

With any luck at all, you get the same answer every time, so you only need to do it once:

EnMasse-6

THIS is why we use LabVIEW!

NOTE:  En masse is my term for this feature, it is not an official LabVIEW term.

Delays, delays, delays

Can’t you signals just work together?

Usually, in a data acquisition program,  all the signals you measure are “live”, meaning they represent the current conditions at the time they are sampled. However, in some cases you might have some signals which are not live, but delayed. For example, suppose you’re measuring engine operation, and you have gas analyzers sampling the exhaust airstream. These analyzers conduct the gas from the measurement point in the airflow system around the engine to the analyzer mechanism itself. This gas flows through the sampling tubing at a specific rate, and therefore arrives at the sample point at a specific time AFTER it left the main airstream. These delays might be multiple seconds in duration.  There could be other reasons for this delay, such as an echo-measuring device, or the mechanical response time of some piece of hardware.

For analysis purposes, you want to look at the engine holistically and see causes and effects. If the speed changed at a particular point in time, you want to see the CO concentration change as a consequence. But unless you do something to compensate, the change in gas concentration will lag far behind the change in speed that caused it in graphs and tables, making it difficult to judge consequences.

To arrange the signals back into time-synchronous alignment, the solution is to think of ALL signals has having TWO delay lines: one physical, and one logical.  In our example case, the physical part is the gas piping conducting the gas. The logical part is completely within the  DAS software. If EVERY channel has a physical delay time (even if it’s zero), and if EVERY channel also has a logical delay time (even if it’s zero), and if EVERY channel has the sum of the physical delay time and the logical delay time equal to a constant, then the signals coming out of the logical delay lines will be time-aligned.

Example

For example, consider three channels: Channel A records data “live”, i.e. no delay. Channel B has a delay of 3 seconds. Channel C has a delay of 5 seconds.

We want to make the entire chain (physical + logical) the same length for all channels, so we pick the longest delay (5 sec) and make that our total delay time. For channel A, which has no physical delay, we have to have a logical delay of 5 sec. For channel B, which has a physical delay of 3 sec, we add a logical delay of 2 sec for a total of 5. For channel C, which has a physical delay of 5 seconds, we have a logical delay of zero. 5+0 = 3+2 = 0+5, so every channel has the same delay, when we count both physical and logical.

The logical delay is implemented in queues with every channel having its own queue. Every sample received goes into a queue belonging to that channel; and we pull out one sample from the end of the queue to be recorded. Since one goes in and one comes out, the length of the queue never changes. The initial length of the queue corresponds to the delay time we need for a particular channel. If a queue is empty (of zero length), then the sample we put in and the sample we take out are the same sample. If the queue is of length 10, then the sample we get out was taken 10 sample times ago.

At configuration time, the queues are set up, and populated with zero values to set their length according to how much delay is required. After that, the sample process means putting a sample into a queue and taking one out for use.  Every channel operates the same when actually sampling, it’s only the configuration where they differ.

Note that if you are recording a TIMESTAMP value, then the TIMESTAMP signal requires its own queue as well, so that it comes out in alignment with the signals. Of course this means that the data does not become usable until all the zeroes have been flushed out of the queues. That will happen when the longest delay time has expired. We can judge this by putting a RECORD signal into its own queue. We don’t actually record data coming out of the queues until the RECORD signal coming out is true.

You can keep the data flowing into (and out of) these queues all the time.  When you want to start recording, set the RECORD flag TRUE. When you want to stop recording, set the RECORD flag FALSE.  Pay attention to the value out of the RECORD flag’s queue, and act appropriately. (Don’t act on the RECORD flag itself, act on the delayed RECORD flag).

This also means that when the test is over, there is still data remaining in the queues. We have to keep recording until the valuable data has worked it’s way through the queues, meaning 5 seconds after the last sample (in the example case).

About Type Definitions

The types, they are a-changin’

LabVIEW beginners often either don’t know about type definitions, or don’t appreciate their value. This article will attempt to explain their use and how they can save you boatloads of time and effort.

Suppose you have a cluster of items that’s very handy to your project. For the sake of discussion, we’ll call it a channel, and assume this is a data acquisition project. A channel might have these items:

  • Channel Name
  • Active switch
  • Scale factor
  • Units

So, suppose you have an array of these to match the channels on your MIO DAQ board.

You might develop a series of VIs that deal with channels:

  • You need an editor, so the user can rename them, select which ones to use, and scale them;
  • You need a VI that takes an array of channels, and configures the hardware;
  • You need to print the setup, so a VI takes an array of channels and formats a page for printing;
  • You need to export the data to a spreadsheet, so another VI takes the array of channels and produces a spreadsheet.

and so on. You’ve defined the cluster and all these subVIs and it all works.

Now, though, you realize that you need to add another item to the channel, namely OFFSET. You have a new 4-20mA transducer which produces a 1-5V signal when you use the right resistor. So, instead of the single SCALE FACTOR (which assumes 0.0EU at 0.0 Volts), you implement the standard linear equation Y = mX + b, where X is the measured voltage, m is the SCALE FACTOR (slope), b is the OFFSET, and Y is the resulting Engineering Units (EU). How do you go about it?

The brute force way to go about it is to open up every instance of the cluster you used, and add an OFFSET term to the cluster. OUCH! In the example, you have 4-8 instances; that’s painful enough, but imagine if you had 50!

A slightly (slightly) more civilized way is to add the OFFSET term to one cluster somewhere, then copy the cluster, and paste it over all the old instances.

One of the good things about LabVIEW is that you can follow all the broken RUN arrows and figure out the places you missed. But is this really what computers are for? After all, LabVIEW knows all these things are broken, it knows WHY they are broken, but it’s YOU that has to chase them all down. So, do this a few dozen times and you might start wishing for a better way.

A Better Way

Enter the TYPEDEF, short for Type Definition. A Typedef is a “master” control. To use it, you ask for a new custom control (File | New | Custom Control). Here’s where you define the cluster you want for a channel. The first time you would use the four items mentioned initially. You then set the control menu to STRICT TYPEDEF, and SAVE the file with a name like CHANNEL.ctl. Now, every time you want to use it, you use the SELECT A CONTROL option on the panel palette and choose that file. What you get is a cluster that looks just like the one you made originally. Place one on your VI for exporting, another on the VI for printing, etc., etc. You can copy and paste them just like anything else.

So far, there’s nothing different. Where the value come in is when you need to change it. You may notice that you can’t change the cluster on your export VI. Drag something on top of it, and the something just sits there, on top. It doesn’t get put “into” the cluster. If yuo really do want to change it, you open the CTL file. You can do this thru the FILE menu, or notice that every instance has an OPEN TYPE DEF entry in the pop-up menu now.

If you open the CTL file, you can add the OFFSET term there, and SAVE it. When you do, EVERY INSTANCE of that control/indicator updates to follow! Open any of your VIs that used it, and it will have the OFFSET term in it! You’ve just changed 4-8 (or 40-80) controls in one whack. This is convenient for this example, but on a larger program, it is absolutely essential.

Another use for them is in ENUMs. Suppose you have an ENUM for units, with the options “PSI, kPA, mmHG, InH2O”. You can create that, and copy and paste it wherever you need it, but if you have to add something to the list, you’re in the same boat as the cluster, above. The answer is the same, make it a TYPEDEF, and use the TYPEDEF wherever you need to. Then when it’s changing time, all the instances update at once.

An Analog Clock (first version)

Sometimes all that digital stuff is just too bland.

If you want to display a time-of-day clock in LabVIEW, it takes three seconds to plop down a TIMESTAMP indicator, and 10 more seconds to enter ADVANCED EDITING mode, and skip the month, day and year, and format it the way you want. Piece of cake.

And it looks just exactly like every other digital clock out there: six digits, two colons, everybody knows what it means. And that’s fine for a lot of purposes.

But sometimes you like to decorate your work a little bit. If you’ve got space on a front panel, and a few cycles of execution time in your run loop, then consider an analog clock.

clock

The visual part of this is pretty simple: a custom control with a cluster of three gauges, all with their scales stretched to a full 360° and both ends of the scale at the top:

  • A gauge indicator in the back: single dark red needle, and the scale set to 0..12.
  • A gauge indicator with an INVISIBLE scale of 0..60, and a bright red needle for minutes.
  • A gauge indicator with an INVISIBLE scale of 0..60, and a gray needle for seconds.

The three indicators are sized identically and lined up identically, with transparent backgrounds, so the needles show through.

An analog is an analogy

You might think it’s straightforward to use this thing – just pass a cluster of hours, minutes, and seconds to the indicator, and you’re done.

You’d be wrong.

Consider what would happen: at 9:22:10, as shown above, the hour hand would be EXACTLY on the 9, the minute hand EXACTLY on the 22. Not exactly what you would expect from an analog clock. It’s even worse 35 minutes later: the minute hand would be on the 57, but the hour hand is still EXACTLY on the 9. It looks for all the world like it’s three minutes before nine, but it’s actually 9:57. If you’re going for the analog, then you’ve got to do better than that.

The gauges accept floating point numbers, so we’re in luck. If you think of how an analog clock works, it’s not the same as a digital clock (and you can quote me on that!). The second hand may move smoothly around the dial, or it may snap from one second to the next. We choose the snap idea, as it’s easier. But the minute hand definitely does NOT snap – it moves smoothly.

So we have to incorporate the position of the SECOND hand into the position of the MINUTE hand. Namely, we add SECONDS / 60 to the MINUTES value given, so that 6:30 will be halfway between 6 and 7, and 6:57 will be just before 7. Similarly, we have to incorporate the position of the MINUTE hand into the position of the HOURS hand so that 6:30:00 puts the hour hand at 6.5, halfway between 6 and 7.

Download

Click to download the example (about 12kB ZIP), containing the typedef indicator you can put on your panel, and a VI to do the above math for it. You can display the CURRENT time by leaving the TIME input unwired, or display a specific time by supplying a timestamp.

Enjoy.

State of the Machine

Handle command sequences for one or a thousand devices the same way.

In dealing with external devices, there are often command sequences that require coordination between the host computer and the device. For example, a recent project of mine involved a TCP connection to (gas concentration measurement) devices which require frequent calibration (using gases of known concentration). The sequence went like this:

  1. Send CAL command
  2. Wait (about 60 sec) until END reply
  3. Chan N = 1
  4. Send request for Chan N’s ZERO result
  5. Wait on Chan N’s ZERO result
  6. Send request for Chan N’s SPAN result
  7. Wait on Chan N’s SPAN result
  8. N = N + 1. If N <= #Chans, go to 4
  9. Reset Device to standby.

That is written in what I call linear style; it assumes you have nothing else to do in the meantime. If that’s the case, then it’s well enough to implement it in exactly the above way. But that’s never the case: you have user interface issues to attend to, if nothing else. So what’s the best way to handle it? A State Machine is a piece of code, with three distinguishing characteristics:

  1. It knows its own state, one of a finite number of predefined states.
  2. It can determine whether the conditions for entering a different state have been met, and if so, switch to that state.
  3. It is called often and performs the above steps quickly, and returns without waiting.

For our gas analyzer example, the states could be Sending CAL, Waiting for END, Requesting Zero, Waiting on Zero, Requesting Span, Waiting on Span, Next Channel, and Resetting Device. For most cases, an Idle state is also useful.

I often combine the state machine itself with other code that pertains to it. The “manager” has several functions, one of which is to service the state machine. Other manager functions might be to Initialize (connect), Shutdown (disconnect), and to initiate the start of a CAL sequence.

Suppose we’re in the Sending Cal state when we get a service call. We know the conditions for getting out of this state are having sent the command to CAL. So we send the command and switch states to Waiting for End. This service call lasted only long enough for use to generate the command string, and send it down the pipe, and we’re done. The CPU is free; the caller can do other things.

The next time we get a service call, we see that we’re in the Waiting for End state. The conditions for exiting that state are having received the END reply from the device. We check the TCP read to see if we have a reply but we do not wait for one. That would violate rule C. We either have a reply or we don’t. If we have the reply, we switch to the next state; if we don’t, we stay in the same state; but either way, we return quickly.

If we are in the Requesting Zero state, we send the command to ask for the zero value of a certain channel and enter the next state. The Waiting on Zero state checks for a reply, and so on and so on.

Benefits

So what is the point of all this? What benefits do we gain from this architecture?

  1. The CPU is not tied up handling the device. If you run the linear style, you’re stuck waiting for 60 seconds. You have to insert other code to handle user stuff like clicking buttons.
  2. The code is re-usable. Keep the state machine and its manager completely separate from the user interface stuff, and you’ll be able to use the same manager code the next time you deal with that device, even in a different project.
  3. You can add a second (third, fourth…) device to the mix, and use the same code! There’s a small change you have to make to go from one device to two. After that, going from two to three or three hundred is trivial!

Multiple Devices

To use the code to manage more than one device, you have to make it reentrant, using the VI PROPERTIES – EXECUTION page. Making it reentrant means two things:

  • The code can be interrupted, run from the beginning by a different caller, and then resumed by the first caller. We don’t care so much about that in our case.
  • Each caller has its own dataspace. In other words, if we’re called from one place in the main code, we have one set of local data (local variables, and uninitialized shift registers), and if we’re called from a second place, we work with a completely different set of data, This we care very much about, since it allows us to store our state, and anything else we want, in shift registers, but have them be different for each device.

The rules for reentrancy dictate that each time you place the VI on the diagram you create a new dataspace for it. In other words, if you called the INIT function from one place and the START CAL function from another, they are not dealing with the same data. That’s no help at all.

The solution is to have a “wrapper” VI, which has all the controls and indicators that your core VI (the reentrant one) does, plus another integer which determines which instance to call. In other words, if you want four devices, you have a wrapper VI with a case structure in it. The case structure has cases 0,1,2,3. Inside each case is a call to the reentrant state machine manager. All its inputs come from the wrapper’s inputs, all its outputs go to the wrapper’s outputs. An additional input selects which device to use. That way, the re-entrant VI is only on the diagram in four places. But you can call the wrapper from as many places as you need to.

An example will probably help clarify that. Click to download example (100 kB ZIP).

    Affiliations

    • TI Alliance

    Calendar

    July 2010
    M T W T F S S
    « Mar    
     1234
    567891011
    12131415161718
    19202122232425
    262728293031  

Testimonial  Contact Info

207-790-0949

1-877-676-8175 (toll-free)

Fax: 1-815-572-8269

General questions:

Sales@Culverson.com

Accounts/billing questions:

Accounts@Culverson.com


Logo