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:
- Send CAL command
- Wait (about 60 sec) until END reply
- Chan N = 1
- Send request for Chan N’s ZERO result
- Wait on Chan N’s ZERO result
- Send request for Chan N’s SPAN result
- Wait on Chan N’s SPAN result
- N = N + 1. If N <= #Chans, go to 4
- 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:
- It knows its own state, one of a finite number of predefined states.
- It can determine whether the conditions for entering a different state have been met, and if so, switch to that state.
- 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?
- 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.
- 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.
- 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).