LabVIEW offers a rich environment for instrumentation programming and has many features for analysis and display. By closely integrating the data logger and Lua with LabVIEW, it was ensured that the full power of the LabVIEW development environment remains available: the limitations inherent in combining loosely coupled systems are avoided. This is of particular value when new instrumentation must be integrated.
Instead of calling an instrument driver directly, it is often useful to wrap it in some code so as to abstract or simplify access, or encapsulate instrument management. Abstraction is possible by exposing the physical values or parameters sensed or controlled by the instrument in a manner that hides instrument-specific details. This makes the code that uses the wrapper more readable and less dependent on the particulars of the instrument. By exposing a simplified view of the instrument, a wrapper can hide the full complexity of the instrumentation driver and provide an interface tailored to the subset of the instrument's functionality that is actually being used. By encapsulating the configuration, initialisation, state tracking, and so on, a wrapper can hide instrument management operations from the rest of the system.
As is clear from the above, it is impossible to provide a one-size-fits-all wrapper solution: the best choice depends not just on the details of the instrument, but also on the needs of the application using the instrument. Still, there are some common issues that arise when trying to talk to instrumentation. This appendix aims to bring those issues into focus and assist with programming wrappers. Some general advice is given in the first two sections:
The later sections detail how to make use of the partial solutions provided by Lua for LabVIEW and the Lua for LabVIEW data logger to construct wrappers:
When the instrument is complicated, it is necessary to become familiar with its capabilities and quirks before commencing with writing a wrapper. This typically involves writing little LabVIEW test programs that use the instrument's driver to explore the various required operations. This section lists some random tips and warnings to aid with this task:
To encapsulate as many instrumentation-specific details as possible, wrappers for complex instruments need to manage and keep track of the state. Where possible, the wrapper should hide the details of this management from its interface to the wider system. Management and tracking can include:
Note that there is not necessarily a one-on-one relation between wrappers and instruments. Sometimes, the only way to effectively hide instrumentation details is to jointly manage the state of multiple instruments. Consider for example a digital multimeter fronted by a separate multiplexer. A simple interface can only be presented by having a single wrapper encapsulate the drivers of both these co-operating instruments.
The important physical parameters and signals controlled or acquired by an instrument often need to be exposed to allow things like monitoring, test scripting, signal analysis, and so on. Instead of inventing an ad-hoc interface for each wrapper, it is preferable to have wrappers of various instruments present a more general and uniform interface. Tags allow just that. By writing acquired physical values to tags, or reading parameter settings from tags, an instrument wrapper can provide an abstract interface to the rest of the system that is made up of a set of tags. This interface is accessible from both Lua and LabVIEW. Because writing and reading are decoupled, tags provide an asynchronous interface. The writer is free to proceed and let the read process the data at its own pace. Note that tags can be used as variables (by reading only the current value) or as queues (by exhaustively reading the written values).
If possible, try to avoid exposing instrument-specific details through tags. For example, writing calibrated values instead of raw sensor readings hides the instrumentation-specific scaling. Ideally, only state that generalises, such as physical values, is included in the tag interface. This makes it easier to replace or simulate instruments because a sufficiently abstract tag interface can remain unchanged when doing so.
For certain tasks, an asynchronous interface is inconvenient or insufficient. For example, when performing a step in a test sequence, the corresponding instrumentation operation has to be known to have completed before the next step can commence. Asynchronous interfaces can be extended to support synchronous operation, for example by having a message and reply queue and having the sender of the message wait for a reply before commencing. This is unnecessarily complicated. It is much simpler to use a callable interface such that the operation is complete when the call returns without an error. For example, a function that can be called from Lua, or a subVI that can be called from LabVIEW. Since a wrapper is likely to be used by multiple test scripts or control loops, investing some effort to provide a simple synchronous interface tends to pay for itself.
When providing a callable interface, the first question to answer is what language it is to be called from. When calling from LabVIEW, having the wrapper expose instrument operations through a set of subVIs is a simple solution. It is however incompatible with dynamically loading the instrument wrapper. Also, when multiple instruments of the same type are in use, it is desirable to avoid code duplication by instantiating the same wrapper multiple times. This makes the use of a direct subVI interface impossible as well. For objected-oriented languages, this would be a trivial problem since these include class loaders and can instantiate objects that expose methods. To get around this LabVIEW limitation, several frameworks have been developed that leverage VI-server and call-by-reference functionality to provide a limited object-oriented-programming (OOP) capability. However OOP is fundamentally different from data-flow programming. Mixing the two leads to ugly code that does not look like what it does, thus defeating the whole point of graphical programming. Instead of trying to make a dog walk on its hind legs, consider calling wrappers from Lua when dynamic loading and instantiation are prerequisites.
When calling from Lua, expose the synchronous instrument operations by binding LabVIEW driver code as Lua for LabVIEW functions. All types of Lua for LabVIEW function are compatible with dynamic loading. Module functions and methods attached to Lua for LabVIEW objects are compatible with instantiation. Since Lua is suited for OOP, the calling code will look pretty.
Sometimes it is useful to log information on a synchronous operation. If so, the wrapper should write to a corresponding tag when performing that operation. To provide the most choice, make the tag name and the enabling of tag writing configurable. Though the data logger also allows disk logging to be disabled per tag, disabling the in-memory writes to a tag reduces overhead even further and makes that the tag need not exist until writing is enabled.
Wrappers can be implemented in pure LabVIEW or as some mix of Lua and LabVIEW. A combination of a Lua for LabVIEW task and a Lua for LabVIEW module is likely to be a particularly efficient choice provided that:
When these conditions are met, create a customised module (for an example, see "<LabVIEW>/examples/Lua for LabVIEW/VI templates/Module Template.vit") and write a task script that opens or instantiates the module while passing the required configuration parameters as a table. Typical examples of such parameters are instrument IDs, scaling factors, channel mappings, and so on. When required, define module export functions to expose a synchronous Lua-callable interface. The remainder of the task script can be used to perform periodic or event-driven actions. Private module functions can be defined to support these actions.
For example, the task script can do one or some combination of the following:
This mix is efficient for several reasons:
The resulting wrapper can be loaded by starting its task script, reconfigured by editing its task script, reloaded with a new configuration at run time by swapping its task, and integrated into a server by making the task script part of a runlevel.