Jump to content

Moving from Action Engines to Classes


Recommended Posts

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.

Link to comment
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.

 

You don't need to duplicate the code. Just pass in the address to talk to Keithley A and Keithley B or, if it is already a "process", make it a Clone VI and launch an instance with the appropriate address..

 

If you switch to Classes, the methods will still be all very similar, only you will be forced to make each one atomic. Classes are very similar to polymorphic VIs in how they are constructed so if you have a VI with a couple of operations in a case statement (Set VISA/Get VISA, say) then you will have to make each one a separate VI, not only for that Class, but any overrides too. This is why classes bloat code exponentially and there has to be very good reasons (IMO) to consider them in the first place let alone if you already have a very adequate working system..

  • Like 1
Link to comment

Just want to make sure I have this clear in my head: You have two main components in your system: Measurements and Devices. A measurement will use a device to get some data and process it, right? The rest of my post is assuming this is correct.

The most important (imo) thing OO helps you enforce is encapsulation. This means you should be thinking about the interface between the two classes (measurements and devices) not exactly how it works. What does the measurement need from a device? For this example, we'll say it needs to get data from it. Ok, that means it needs a "Read Data" vi. How does it get this data? The measurement doesn't know (or care), it just knows that when it calls this method, it gets data. Perfect.

There will be a top level VI that will configure all of the devices. This will do things like setting serial ports, initializing, etc. Stuff that needs to be set or run once. After it's done all this configuration, it will pass the device objects (by value) to all of the different parts of the code (meaning to measurements).

Measurements aren't allowed to do things like change the serial port or close the port or whatever because there are no public methods to do this. Again, we use the access scope to limit what other pieces of code are allowed to do to the object.

How the Device class will work. The device class will have a DVR in it to the data I needs to share between all measurements. When you initialize the device, you create this DVR. This DVR will never be exposed by a public method. So maybe all the "Read Data" method we defined above does is copy the data from the DVR to the measurement. Sounds OK. We'll assume somewhere else in the code is periodically calling something like "Read from Device" which is populating the DVR. This means N different measurements can all be reading the data from the same device at almost the same time. There will still be some waiting going on while the DVR is getting accessed by each "Read Data", but since only the Device has access to the DVR, we can ensure that we only do things that are quick (your Action Engine does the same thing). This means that the responsibility for ensuring that there are no deadlocks now falls on the Designer of the Device class (you), not the users of the Device class (the students). This is why encapsulation is so good. If people are also writing code for devices, I'd also give them overiddable methods (using the protected scope) for the actions that they need to implement. Again, I'd keep them from ever seeing the DVR.

So now if you have two identical devices, you just instantiate and initialize two of the same classes. Give them different serial ports (or whatever data is different between the two) and then pass these device classes around. You can branch the device class wire all you want, you're still going to operate on the same data.

2 followup notes:

1. You say you're looking for an OO design pattern that works for your application. I think you're thinking of design pattern like Project Templates. Design patterns are small building blocks that you assemble as you need for your application. For example, I'd probably use the Template design pattern when implementing the "Read From Hardware" vi. Read From Hardware would be static, it would have an overridable method named "Read From Hardware core" that would output data, and I'd write that data to the DVR. Now when you implement a device, you have a small number of methods to override but you'd still get a lot of functionality. This also helps me make sure I don't give the DVR to someone who may mess it up. Maybe I'd also use the Factor pattern when building the devices in the application. I'd use the ideas from the design patterns to build up together for my specific application.

2. By ref (either with GOOP or DVRs) and Action Engines both can cause dead locks. If you want to eliminate deadlocks you have to either use global variables (which now introduces race conditions) or properly design your system. Action engines make it fairly easy to properly design it to avoid deadlocks. DVRs take a bit more thinking but get you a lot more benefit.

Check out Elijah Kerry's Plugin Hardware Abstraction Layer at the NI community. The documentation is very good at explaining what he is doing. There are a few other HAL examples within that group that are a little simpler and don't use the actor framework that are also worth looking at. Best of luck!
I stay away from linking this to people, especially those new to the OO scene. It's pretty complicated, and accomplishes a very specific task. It's like teaching a new C programmer how to write a web server when they really need to learn hello world.
You don't need to duplicate the code. Just pass in the address to talk to Keithley A and Keithley B or, if it is already a "process", make it a Clone VI and launch an instance with the appropriate address..

If you switch to Classes, the methods will still be all very similar, only you will be forced to make each one atomic. Classes are very similar to polymorphic VIs in how they are constructed so if you have a VI with a couple of operations in a case statement (Set VISA/Get VISA, say) then you will have to make each one a separate VI, not only for that Class, but any overrides too. This is why classes bloat code exponentially and there has to be very good reasons (IMO) to consider them in the first place let alone if you already have a very adequate working system..

I'll argue that you increase the number of VIs in exchange for code readability. I'd much rather see a well defined interface to a class being used versus an enum going into a case structure with a ton of cases. Add in a variant that needs casting, or a cluster-saurus where only some of the fields are valid based on the enum and classes are way easier to read. Easier to read = Easier to debug = easier to maintain. Number of VIs in the project isn't really a concern for me, and I don't see why it should be. Edited by QueueYueue
Link to comment
I'll argue that you increase the number of VIs in exchange for code readability. I'd much rather see a well defined interface to a class being used versus an enum going into a case structure with a ton of cases. Add in a variant that needs casting, or a cluster-saurus where only some of the fields are valid based on the enum and classes are way easier to read. Easier to read = Easier to debug = easier to maintain. Number of VIs in the project isn't really a concern for me, and I don't see why it should be.

Sure. Readability is good, IF there is no penalty. Unfortunately, this is the reason why LVOOP projects take hours to compile.

But it doesn't necessarily improve readability. Most of the time it's just boiler-plate code with a trivial difference multiplied by the number of children. It is the equivalent in other languages of having a file for every function and you'd be shot if you did that.

  • Like 1
Link to comment
Sure. Readability is good, IF there is no penalty. Unfortunately, this is the reason why LVOOP projects take hours to compile.

But it doesn't necessarily improve readability. Most of the time it's just boiler-plate code with a trivial difference multiplied by the number of children. It is the equivalent in other languages of having a file for every function and you'd be shot if you did that.

Compile time: Yeah, sometimes. Hours is being a bit dramatic. I'm pretty sure this is a symptom of LVLibs, not just classes. If you watch your relationships right, you wont see this.

 

Boiler plate etc: Sounds like you're doing it wrong. AF Do msgs have a lot of boiler plate, most OO code does not. If you find yourself with a lot of boiler plate then you need to rethink your design.

 

Other languages file for every function: Non-OO Labview does this too. And again, Why should I care if there's a lot of files on disk? Things are organized in the project appropriately, so I really don't care how many files are in my project.

 

Thread's drifting so hard.

Link to comment

On the Plugin HAL solution:

I had looked at the Plugin HAL extensively before opening this topic. If I remember correctly, it uses SEQs to pass around the class instances. I wouldn't mind building a simpler application using the same idea, but the Plugin HAL itself seems daunting and bulky.

 

On including a device identifier in each call to an AE:

I had considered this as well. It would be the quickest fix, but feels hacky and doesn't provide OOP candy like inheritance. Two very similar devices, for example, would still require code duplication.

 

On using DVRs only in private methods:

This seems like a good idea. It would restrict the more tricky bits of code to the Device level, so that writing new Measurements remains simple. Also, the code blocks inside the DVR In-Place Element Structures would be short and simple, minimizing the chance of deadlocks.

 

I'll write some mock-up code using the latter solution, and see how it works out. Thank you all!

Link to comment

For my projects, a big plus of the action engine design is data handling and speed of execution/performance.  In many applications, it makes sense to have the action engine acquire and buffer continuous, large data sets and keep the data inside the action engine in shift registers rather than passing it out.  Then you call the action engine to do operations on that data within itself such as averaging, decimation, peak detection, statistical analysis, graphing, saving to file.

 

As you said, a downside to the action engine is when you have a lot of different actions with each one requiring a different set of inputs and outputs.  Then you have to write in the VI description which inputs apply to which actions, and that's a little messy.  LVOOP methods are neater because there is a different subVI for each action showing only the inputs and outputs that apply to that action.

Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.