I developed the Measurement Framework over the last year in order to experiment with and explore some of the latest architectures, technologies and design approaches that are becoming increasingly important for large LabVIEW systems. I’ve used sections of this application to illustrate technical topics in previous posts, but I wanted to use today’s entry to explore the overall design of this system and several of lessons I learned. My hope is to provide insights for any of you that are attempting to address similar requirements. I also hope to lay the groundwork to some deeper-dives into some of these topics in future entries.
The primary goal of this software when I started was:
- Define an architecture that allowed other developers to add functionality (ie: ‘plugins’) without modifying the calling framework
- Determine the best way to give people writing plugins starting points that included the necessary APIs, parent classes, and stub code in order to minimize the work required
- Develop a strategy to deploy the framework as an executable and manage common dependencies of those plugins appropriately
- Explore the implementation of a hardware abstraction layer, or HAL, which quickly also led to the creation of a measurement abstraction layer, or MAL
Secondary goals were to:
- Understand how to compartmentalize code for the sake of enabling large teams of developers
- Explore the mechanisms for managing communication amongst multiple dynamically-spawned, free-running processes
- Package and distribute source code for the sake of reuse
- Enable the plugins to be installed (or even uninstalled) separately from the deployed copy of the framework
These goals led to the formulation of the following high-level requirements:
- This system shall be able to run multiple measurements at the same time
- It shall be possible to load additional measurements at run-time without modifying the calling code
- Measurements shall return results in a format that is specific to the type of information requested, and such that it can be customized or changed for the purpose of future measurements
- Measurements shall call the appropriate type of hardware class, but not rely upon hard-coded calls to a specific driver or device
- It shall be possible to load additional hardware at run-time without modifying the calling code
- A user interface will allow users to run a loaded measurement using specific hardware devices and configure measurement settings
- Results shall be logged and available for later viewing via the user interface
- The user interface shall be completely decoupled from measurements, hardware and results
- It shall be possible to load a different user interface
Use of Object-Orientation and the Actor Framework
As should be apparent from the requirements, extensibility of this application is extremely important – measurements, hardware, results, and even the user interface should be possible to change, extend or even replace. As a result, the selection of an object-oriented architecture was the natural choice given that OO simplifies the task of reusing and adding to pre-existing functionality.
The architecture of the latest version of this system was built against the version of the Actor Framework that was shipped in LabVIEW 2012. For those of you unfamiliar with the Actor Framework, actors are defined by classes that effectively represent queued message handlers. As a class, an actor defines the capabilities of a queued message handler, thereby allowing users to dynamically spawn multiple instances of an actor and even override and extend an actor’s capabilities through child actors. The components of this system that serve as free-running processes were all defined as children of the actor class. The hardware class hierarchy wraps driver calls; consequently, they are not defined as actors.
The actors that were defined for this system were:
Controller: this actor is responsible for the business logic of this system. It is aware of the available measurements, available hardware, stores results and is the only actor that communicates with the user interface.
Measurement: this is a generic implementation of a measurement, which includes methods like configure, acquire, measure and close. Controller is the only other actor that can send messages to this actor.
User Interface: this actor is seen by the user when the system starts and sends commands directly to the controller, who then relays the command to the appropriate destination. The user interface may request information from the controller in order to display it to the user, but no information is stored in the user interface
Results: this actor is created by a measurement and passed to the controller, who then stores the information. The results actor is only ever run if requested by the user interface, which could then present a custom display for visualizing and even analyzing acquired data
Error Handler: though not fully implemented yet, this was intended to be a separately running entity that received error messages from the rest of the system and had access via the controller to the queues necessary to shut the system down if necessary.
As mentioned, an additional class hierarchy was defined for hardware, though these are not actors. A class named ‘Generic Hardware’ is used by the controller to define any hardware that might ever be used. Four children of ‘Generic Hardware’ are included for the sake of defining common methods certain classes of devices would typically need – these four interfaces are: DMM, Scope, Function Generator and Power Supply. The children of these interfaces define the concrete implementation of a device, or the simulation of a device:
I relied exclusively on the messaging architecture that is built-in to the Actor Framework in order to construct and send messages between actors.
The following diagram illustrates the various methods (in white boxes) belonging to actors (in blue boxes) and the messages that are passed by actors (arrows either to themselves or another actor). Note that the same message class always invokes the same method of the same actor, though it can be sent by any actor who has the appropriate queue reference.
Conceptually, it helps to think of the straight blue line as representing ‘Actor core.vi,’ as this is the VI responsible for invoking a method upon receiving a message. The orange circles represent events that are fired as a result of a user interaction with the UI.
Defining Abstraction Layers
A growing number of customers seem interested in how best to develop a hardware abstraction layer in LabVIEW. I do not believe that their is a single one-size-fits-all answer to this question. The answer to this question should be driven by the specific requirements of an application and the reasons for wanting to add a layer of abstraction.
In this case, I abstracted both the measurements and the hardware. The generic measurement class defines capabilities I anticipate all measurements could potentially want to implement, along with functionality that would be reused by children. It is assumed that a measurement not only acquires data from hardware, but includes the code necessary for analysis in order to return meaningful information.
Conceptually, a measurement may actually need to use multiple hardware devices to accomplish a task. Measurements rely upon being passed the appropriate classes of hardware, but they do not require knowing the concrete device that they will be interfacing with. By developing measurements without knowing the specific device they will be using, this makes it possible to add new or different devices over time.
This implementation is just one proposal of how to create layers of abstraction.
Dynamic Loading of Measurements and Hardware
Loading children that implement specific functionality at run-time is a fairly simple task from a programming perspective; however, I realized I had to make two very important decisions:
- Who defines where these items are loaded from?
- Where would dependencies of these dynamically loaded items be found at run-time?
I determined that the location of these files should be stored within a configuration file that is parsed at launch of the application, but one of my goals was to enable users to modify this configuration file in order to add or change the items that were to be loaded. In order to account for a number of ways in which a user would potentially define these locations, I decided that the loaded mechanism would be able to handle paths that were either absolute or relative to the top-level VI, or perhaps even a folder location from which to find these items. The code responsible for parsing the configuration file also checked a boolean flag to see if the public application data directories should be searched – this became especially useful for deploying the application and enabling users to create separate installers for adding new measurements to a deployed system, as they only needed to be placed in the public data directory.
Simply referencing the child class to be loaded worked as expected in the development environment, but failed the first time I tried it in the run-time engine. What I failed to consider is the difference in the search-paths LabVIEW uses between these two different contexts. Within the development environment, LabVIEW looks in vi.lib for any VIs that are not found in memory when called. However, the run-time engine assumes that the development environment is not installed and therefore looks in the neighboring folders of your application and specifically the directory it is loaded from.
I plan to explore different strategies through which this problem can be addressed in a later entry dedicated to the deployment of this system.
The Use of Lookup Tables
Intra-process communication presents many challenges – this scenario required 1:N communication between a single controller and multiple asynchronous processes, which happen to be actors in this system. The controller is the first ‘top-level’ actor in this system, who then spawns all of the other actors; consequently, all actors can send a message to the controller, but the controller needed a mechanism by which it could quickly find a queue and send a message to an arbitrary entity, such as a running measurement.
The original framework was built using arrays of queue references for all of the actors the controller would ever need to communicate with, but this is not necessarily the most scalable nor elegant implementation when dealing with an arbitrary number of entities while also addressing the need to randomly retrieve an arbitrary queue reference and properties associated with these entities. To better address this scenario, I chose instead to use key-value pairs that were stored in a variant attribute lookup table.
I also quickly realized that I would need multiple tables. In all, the Measurement Framework uses a total of seven lookup tables to store various pieces of information, all of which are retrieved using a unique identifier string and stored in the private data of the controller. The data-type of the value returned is predefined and indicated in the parenthesis:
- Running Measurements Lookup Table. Given the name of a measurement, return the queue to send messages to this task (Queue Reference)
- Available Measurements Object Lookup Table. Given the name of a measurement, return the Object representing this measurement (Class)
- Detected Hardware Objects Lookup Table. Given the name of a piece of hardware, return the Object representing this device (Class)
- Matching Hardware Lookup Table. Given the name of a piece of measurement, return an array of device types (ie: DMM, FGEN) it needs (Array of Strings)
- Compatible Hardware Type Lookup Table. Given the type of hardware needed, return an array of device IDs (ie: PXIe-4110) that match the type (Array of Strings)
- Hardware Availability Lookup Table. Given the device ID of a piece of hardware, return whether or not it is currently in use (Boolean)
- Results Lookup Table. Given the name and time of a previously run measurement, return the Object representing the results of this measurement (Class)
As an example, the following code populates tables 2 and 4 with the appropriate information upon detecting a new measurement: the object itself, and the array of compatible hardware types. While the compatible hardware could be retrieved from the object itself, storing this in a separate table simplifies retrieving this information later on – this architecture is predicated on the design decision that this particular property of a measurement will not be modified once it has been loaded.
For more information on the use of variant attribute lookup tables, see: Variant-Attribute Lookup Tables.
The controller’s private data also includes an array of waiting measurements, paths to locations where hardware and measurements should be loaded from disk, and a single queue reference for communication with the UI.
Enabling Plugin Development
Admittedly, my solution to this problem is perhaps one of the aspects of this proejct that I’m most pleased with. Simplifying the work required to write a new plugin was a fundamental goal of this project – I’ve saved this explanation for last in order to provide a general overview of the system and the architecture first.
My solution to this problem took full advantage of a new feature in LabVIEW 2012; specifically the new ‘Create Project’ dialog, which features built-in templates and sample projects. One important characteristic of this dialog is that it creates a new copy of a project that a user selects and applies a new name and even file prefixes to this project – it effectively branches a template, unlike the older .vit files. As a result, users can safely create as many instances of a template as they want – plus, these templates can include multiple files and even pre-defined built specifications.
A VI Package file is used to distribute the template, and it ensures that the necessary parent classes are installed on the system. The parent classes, along with other common components are packaged in a ‘Measurement Framework Common Components’ package file that places them into user.lib – in the future, I plan to replace these with packed versions of the current libraries.
Consequently, a new project containing only the leaf-level measurement class can be made into a template. Because it’s distributed using a VI Package File, I can also boil-up the APIs (as shown here) I expect a developer to use for development of a new measurement to a designated palette – for the sake of this example I expose the APIs necessary to interface with the generic hardware classes.
I took it one step further and used scripting to customize the template based on some basic configuration that the user is prompted with from the ‘Create Project’ dialog. As you can see below, the user is presented with options that actually invoke scripting methods under the hood to customize the code accordingly.
Try it For Yourself
The Measurement Framework is a ready-to-run example that you can download and use with LabVIEW 2012 or later. Specifically, you can:
- Download and install the VI Package file containing all the source code, common dependencies and templates
- Create a new instance of the top-level framework
- Build the framework into an executable by running any of the pre-defined build specifications
- Install this executable onto a clean machine
- Create a new measurement from a template
- Run the pre-defined build specifications to create and install the new measurement on the same machine where the framework is installed
- Launch the executable and see that it calls and runs the measurement
… all without modifying a single line of code
I still have additional ideas I want to explore using this project, including:
- the use of the measurement class to launch continuously running measurements
- packaging of common components as PPLs (packed-project libraries)
- distribution of plugins as PPLs
- The use of wirebird labs ‘Deploy’ to distribute the framework
Look for more on these and other topics in the future.
Download the Measurement Framework Example:
It’s available for you to explore and use from this link: Measurement Framework
As always, let me know if you have any questions comments or feedback.