I've recently built a medium-sized application that controls a bunch of devices in a university laboratory. From the start, I've been focussed on writing a modular, extensible program, as people tend to add or remove equipment and write new measurement routines accordingly.
In my architecture, each device is represented by an Action Engine (i.e. Keithley Controller.vi). The UI is comprised of subpanels that contain a simple UI for a single device (i.e. Keithley Panel.vi). Due to the nature of AE's, any vi can send commands to any device; data changes are communicated to the panel through User Events.
This all works really well. Creating new measurements is extremely simple, which is vital as most of my colleagues and students are LabVIEW novices. Adding a device is relatively simple too, as Action Engines are easily understood.
And now, the downsides. Mainly, there is a lot of code duplication in this architecture. Many AE's look very similar, with actions like Set VISA Address popping up in every single device. Moreover, imagine having two identical Keithleys (a very real prospect)... one would have to duplicate each vi to create Keithley A and Keithley B, which seems silly indeed. Also, I don't particularly like the extensive use of Variants to limit the number of inputs on the AE.
All this tells me it's time for LVOOP, but so far I haven't found a satisfactory design pattern for my use case. The problem is that classes in LabVIEW are by-data instead of by-reference. Somehow, I need to get class instances of my devices across to different parts of my application (including measurement plug-ins), without having them operate on different copies of the instance. Solutions to this problem seem to come in two flavours: communicate to each instance via a command queue (Actor Framework) or operate on references (DVR, Single Element Queue). I find both approaches unsatisfactory: the former buries the elegance of a class (properties and methods) under a pile of asynchronous mumbo-jumbo I don't need, while the latter can introduce deadlocks and forces my users (students and co-workers who will create measurement routines) to place all calls inside de-referencing blocks (be it an In-Place Element structure or a Checkin-Checkout procedure). Finally, I should mention that I'm aware of the existence of GOOP, which seems to enable by-reference OOP in LabVIEW, but I would like to stick to native solutions if at all possible.
Is there a way of harnessing the power of OOP (inheritance, instancing) in LabVIEW, without moving to asynchronous designs or potential deadlocks? Where do I go from here?
PS: my apologies if this turned into something of a rant, but shifting to the OOP paradigm in LabVIEW seems overly complicated compared to other languages.
I have some projects on the horizon that have to be more reliable than anything I have done in the past. And this new requirement has me thinking about how I deal with errors in LabVIEW more and more. Simultaneously I have been looking into the Actor Framework. The sample project in LabVIEW 2012 for the Evaporative Cooler does a great job of showing how many of the pieces of a full application fit together. However, as I have studied the example, I have noticed that in many places error handling has simply been ignored.
An example of this is in the water level control... specifically, I take issue with the word "guarantee". Documentation states: "Water Level is a concrete implementation of a Level Controller. We will use it to guarantee that the water in our evaporative cooler’s reservoir never falls below a minimum safe level."
What if the building loses water pressure? Or the piping fails? Or the valve doesn't actually respond? Or any of a million scenarios that result in the program being unable to really guarantee anything.
I'd like to open a discussion of how the Evaporative Cooler example could improve on its error handling so that it was robust enough to safely deal with all the "what ifs" of the world. This question is covering everything from "what if writing to the value property of a UI indicator fails?" to "what if the building water supply fails and the water level doesn't respond when I command the valve?"
And those seem like two great places to start! In the context of the Evaporative Cooler example project, I found three VIs whose error handling I would like to discuss:
1. Timed Loop Actor.lvlib:Timed Loop Actor.lvclass:Read Controller Name.vi
2. Live User Controller.lvclass:Update Current Temperature.vi
3. Level Controller.lvlib:Level Controller.lvclass:Update Core.vi
In the case of #1, the VI does not have error in/out terminals, but some of the VI's it calls does. This is a situation I have encountered before: how do you add error handling to a very low level VI that does not (and for the sake of discussion cannot) have error terminals?
In the case of #2, the VI writes to a property node and ignores the error terminals. In a big application there are soooo many property nodes! What are the implications of ignoring the errors? When is it safe to ignore them? This is probably very common, and has some overlap with #1... In general, in LabVIEW, when can you ignore error terminals?
#3 is the logic VI that determines whether to open or close a valve based on water level. But it doesn't keep track of time and whether things are "working". And certainly if you put a human operator in place of the VI, the human would after some time decide that opening the valve hadn't helped the water level. Can the VI mimic this?
For all of this error handling business, I think I am really asking four questions:
A) What should the software do on its own to respond to the error?
B) What should the software tell the operator (who might have never heard of LabVIEW or college) about the error?
C) What should the software tell the operator's supervisor about the error?
D) What should the software tell the software developer about the error?
It is pretty easy to write an application when everything works the way it should! But when things might go haywire, how do you write application then?