Introduction
In this post, I want to document the design of the Hardware Abstraction Framework and end up with a set of classes with defined relationships that form the basis for actual development in LabVIEW. As I won't be undertaking any round-trip engineering, the design posted here won't be covering every method, attribute, supporting class etc that might ultimately be developed. The process is likely to be iterative, even though I've already had a go at this with LabVIEW NXG as the products are not 1:1 compatible - in fact, LabVIEW has many more features and I'm hoping that I can take advantage of those. Time will tell.
Table of Contents
Requirements
Thinking about interfacing LabVIEW and test instruments, what am I trying to achieve? Fundamentally, when I automate a test I will create a command in a syntax the instrument understands, send it and, if appropriate, read a response and process it. Typically, I would likely be sending multiple commands and if I'm going to the trouble of automating this, then it's just as likely that multiple instruments are involved and sequencing and coordination between them is required. These tests could be one-offs, repetitive, or perhaps over a long duration (soak testing); perhaps a combination of all three. Instruments these days seem to provide a SCPI interface but generally only a subset of available commands that are relevant; they may have variations in the parameters they accept on common commands. There are also instruments that may not offer a SCPI interface but perhaps a home-grown syntax, e.g. an application running on an Arduino.
I've come up with the following high-level requirements (about the only level of detail I'll go into as I'll work out the detail as I go):
- Run tests against one or more instruments connected to the PC that are discoverable by LabVIEW.
- Encapsulate the device-specific communication protocol within the framework.
- Work with commands in a form that is human identifiable rather than programmatic and that is technology/syntax independent. E.g. work with a command "Identity" rather than "*IDN" or "Ident". Leave it to the framework to handle any translations from "English" to "technical" (perhaps a future iteration could localise this.)
- Be able to create tests that are instrument independent, e.g. be able to swap one manufacturers DMM for another's without having to rewrite tests.
- Be able to write tests that can be run once; for a specified period of time; repeatable one or more times; or run until user intervention is made. I'll call these run controllers for the sake of reference.
- I want commands to be grouped into these run controllers and be able to use multiple run controllers in a test. Commands in a run controller are handled sequentially and a run controller is considered "processed" when all the contained commands are run.
- Provide an application that can be reused or modified as a starting point for future test applications - fits in well with using LabVIEW Project Templates.
- Provide callout mechanisms at appropriate points of the framework to allow the execution cycle to be modified for specific scenarios.
It doesn't seem like many but there's plenty of devil in the detail here.
Domain Terms
It would be useful to identify the domain terms within these requirements so they can be used in the design and source code. I can identify: Framework, Instrument, Protocol, Command, SCPI command (as a specific type of Command), Run Controller, Test.
LabVIEW considers instruments in terms of "Instrument Drivers" so I can add Driver to these terms; in fact within LabVIEW the driver encapsulates the functionality for communicating with instruments thus it also covers the term Protocol which can be removed; this means an Instrument, then, is an actual physical device. Further, I don't like the term "Run Controller" so I'm going to rename that as Command Director; also, because an actual test will consist of processing one or more Command Directors, I'm going to use Test Processor instead of just Test:
- Framework: the functionality that actually executes the tests, encapsulating the complexity, sequencing and result management. This is extendable by test applications to add real commands and real instruments to be used.
- Instrument: as LabVIEW uses the term "Instrument Driver" to cover the concept of actually communicating with connected devices, an Instrument is a physical device connected to the computer that LabVIEW can interact with.
- Driver: the means by which LabVIEW actually interacts with an Instrument, responsible for managing the underlying communication protocol.
- Protocol: See Driver - the term Protocol won't be used further
- Command: a generically named, technology independent command that can be sent to an Instrument (after suitable translation to a command the Instrument understands.) E.g. the Command "Identity" is the generic command; "*IDN" is the translated specific command to be sent to an Instrument that understands SCPI. By generalising commands this way, they ought to be reusable across different syntaxes/technologies.
- SCPI Command: a specific type of Command used by Instruments that understand them.
- Command Director: A container used to hold one or more commands, each to be run sequentially, and in a specific way. A Command Director can process its collection of commands once, for a period of time (i.e. repeated until a period of time has elapsed), repeated a number of times, or forever until user intervention.
- Test Processor: the complete set of Command Directors that must be processed until they finish and which then constitutes the notion of an end-to-end test.
Design Model - Iteration 1
I will create the design model in Visual Paradigm using a small number of UML model elements and diagrams. This isn't a tutorial on Visual Paradigm or UML and I would expect any reader to Google terms not understood; having said that, it ought to be relatively easy to follow along. The design will evolve iteratively as I create the framework because whilst I have a good idea how it will work, I need to understand more about LabVIEW and how it functions and the features it offers. This iteration is my first stab at structure and classes involved.
Package Diagram
Organising model elements within Packages gives two advantages:
- Identifies related model elements so is a way of achieving the right level of coherence between elements and which can map into namespaces in the source code, providing a direct link between design and "code".
- Mapping dependencies between Packages ensures that there are no cyclic dependencies between or relationships that map in the wrong direction. If Package B is dependent upon Package A then, irrespective of any intermediary packages, a class in Package A cannot hold a reference or dependency to a class in Package B. There's nothing actually stopping someone actually building such a relationship in the source but that is clearly against the design.
I usually find that there are helper classes that are independent of the main application but provide support for it, and classes that are purely reusable bits of utility code. These are best managed through their own packages rather than within the main application package(s.)
The Hardware Abstraction Framework consists of three main elements:
- Framework: the core framework that provides the necessary abstraction.
- Implementation: classes that provide an implementation layer of (a) instruments, specifically the instruments I own; and (b) commands that are unique to an instrument (perhaps more than one) and/or which extend a base SCPI variant. This package structure allows me to reuse my implementation code across future test applications (but, say, may not be relevant to someone else who uses the framework.)
- Helper: classes that provide useful functionality across the whole framework and also, potentially, to external implementations.
The dependencies shown in the diagram give my initial thoughts of how I see this working: Running a test will actually run the configured commands which use the Drivers to interact with instruments.
Class Diagram
I've identified a first cut of classes based on the Domain Language and initial dependencies from how I see it working:
There are no attributes or methods defined as yet (on the diagram - dependencies between classes translate into attributes in the class implementation.) This is also the first iteration so this could obviously change as the framework is developed. The following describes the classes in more detail, based on how I saw this working in the first iteration of the design when I wrote this post. Packages not shown above currently have no classes defined for them.
Driver and Instrument
Driver is responsible for interfacing with actual test instruments providing the necessary mappings between a generic test language and instrument specific language. By doing this a user of the framework can standardise on names for things like commands, parameter names, setting names and so on, and leave the Driver to translate on an instrument-by-instrument basis. Driver represents the LabVIEW NI-VISA (Virtual Instrument Software Architecture) protocol for interacting with instruments and is actually an abstract class: concrete classes are the instruments themselves which are specialisations providing any specific functionality or attributes required for that instrument. There is also a specialisation, SerialDriver, which is used by instruments that connect through a COM port rather than, say, a USB port and which require a serial protocol to interface.
Driver will execute an instruction for the owning Command when directed by the framework, obtaining a response as necessary.
Setting is meant to represent a specific configuration setting that can be sent to the instrument by the Driver. There are two types of setting:
- DriverSetting: this represents a setting known to the instrument.
- CommandSetting: this represents a generic non-instrument specific setting, i.e. a setting that can be used across all instruments. The Driver maps this CommandSetting to a DriverSetting which then removes any concern of naming differences. E.g. one instrument might have a setting called "Settle Time" and one might have "Settling Time"; the CommandSetting might then be "Time to Settle" and can be translated to the specific name when applied.
These classes, then, give me a way to encapsulate how to interact with an instrument by wrapping the necessary LabVIEW processes with a simple interface.
Command
A Command represents an instruction that can be sent to a Driver for running on an Instrument. I can see these evolving over time into other execution constructs such as optional pathways (if...then...else...) or directives. There are two types of Command:
- WriteCommand: represents a command that can be sent to an instrument but which needs no response.
- ReadCommand: represents a command that can be sent to an instrument and for which a response or result will be returned. It inherits some of its functionality from WriteCommand so I don't need to run a WriteCommand and a RunCommand as the latter incorporates the functionality.
A Command is associated with a Driver whose responsibility will be to undertake any specific translation of a command. The framework will arrange for the Driver to issue the command at the appropriate moment. I can see two ways the Driver will issue a command to the instrument:
- Immediately: the Driver sends it straightaway
- When able: the Driver will wait for the instrument to finish processing any existing instruction before sending this one.
Some commands can take parameters to issue with the base command. These will be represented by the Parameter class. The framework will automatically add any requested parameters to the command before it is sent to the instrument. Parameters induce a result from the instrument as well as the result for the command.
TestProcessor
Running a test and processing commands will be accomplished through the TestProcessor and one or more CommandDirector instances.
- TestProcessor: this is the master controller for a test and will process the CommandDirectors that it knows about. The TestProcessor runs in a loop until all CommandDirectors report that they have finished.
- CommandDirector: the Director controls the processing of one or more Commands that it knows about, in sequence. Each Command will be run once, but may be run multiple times, depending upon the CommandDirector actually used:
- CommandDirector: runs each Command once, then reports that it has finished.
- RepeatingDirector: runs each Command a specific number of times, specified when the RepeatingDirector instance is created, and then reports it has finished.
- ContinuousDirector: runs each Command continuously until ordered to stop - I see this being a raised event - at which point it finishes running any remaining commands and then reports itself finished.
- TimedDirector: runs each Command for a minimum period of time. When that time has elapsed, any remaining Commands will be run and then it will report itself finished.
A CommandDirector can hold multiple Commands and each will be run sequentially in the order added. The TestProcessor can hold multiple CommandDirectors and these will be run sequentially in the order added until they report finished. Note that a CommandDirector instance may finish whilst others will still run - the TestProcessor will just skip these during the processing loop.
Other Thoughts
At this stage I envisage a need:
- to handle returned values and cast them into LabVIEW types
- for events to interject into the processing loop and ContinuousDirectors
- to report results as they arrive rather than collect, collate and report at the end
- to script tests and create CommandDirectors and Commands from a parsed text file
- to export test results into a format compatible with external tools, e.g. in a CSV format.
These I shall keep in mind as the framework is developed.
Design Model - Iteration 2
I've added this section after developing the framework for some time and provides an update on where it's got to.
Package Diagram
There's been some restructuring of Packages as it became clearer what should be in the Framework itself and what should reside outside it, in Implementation; it's the latter package that's likely to change the most in future through adding new Instruments and Commands.
Class Diagram
There's been some change here, mostly as part of the restructuring of Packages. The main change though has been in how Driver and Write Command interact. It became obvious that Driver would need to make callbacks on the Command in order to, for example, get the actual command string to send to an instrument. This created a circular dependency which I'm really not happy about (ever!) and the obvious fix is to use an Interface: the WriteCommand then implements (inherits in LabVIEW terms) that interface and can provide the necessary behaviour without tying Driver directly to an implementation. The dependencies remain one-way.
State Machine
During development it became clear that what is being developed is a State Machine:
This shows the states that a test goes through whilst it is running. The Process State actually processes each Command Director and the Continue State determines whether the test has finished or not (as notified by the Command Directors.) A test transitions between these two states until the test is done. Pause and Resume are states that are initiated from a User Application.
Messages and Events
Communicating within the Framework and between the user test application and the framework is accomplished through message queues and events. The Framework has its own message queue which is used to signal state transitions. The user application has a notification message queue which the framework uses to publish results whilst running. Actually there are two means of handling results: via a Results Notifier on the Notification Queue which allows for asynchronous processing, useful for long-running tests; or via a Results message on the Notification Queue which will build up and can be processed "en-masse" once the test has finished, useful for simpler short-running tests.
Whilst the test is running, Events are used from the user application to the Test Processor to signal directives - e.g. Pause, Resume, Stop and so on. The Test Processor acts on these at the end of a Processing Loop (see image above). The Test Processor can also send a Stop event to the user application when the test has finished: this allows the user application to take any appropriate action at the end of the test.
There's one more Event: Inject Command. Whilst a test is running it may be desirable to add additional Commands (Command Directors) to the test, e.g. with updated UI values perhaps, and this is achieved with this event. The new Command Directors are added to the end of the current set of Command Directors for processing.
Summary
The design evolved over time, resulting in a State Machine being developed to represent the lifecycle of a Test. Inter-Framework and Framework to user application communication is handled with simple messages, notifications and events. These can control state transitions and present test results during the test run.
Next: Project Organisation.