Jump to content

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.

Share this post


Link to post
Share on other sites

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!

Share this post


Link to post
Share on other sites
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

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites
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

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites

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!

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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.


  • Similar Content

    • By Ryan Vallieu
      I have seemingly found an issue with the shipping example code for Nested Malleable VIs.  Another user has verified that he saw the same behavior in 2019.
       
      I am working through the examples and the presentation from NIWeek 2019.  In running the Lesson 2b code (C:\Program Files (x86)\National Instruments\LabVIEW 2019\examples\Malleable VIs\Nested Malleable VIs) I found the Equals.vi in the class was not being leveraged and the search failed.  When I went to my LabVIEW 2018 machine and ran the Lesson 2b.vi the code worked to find the element by correctly leveraging the in-class Equals.vi.
      One difference I see is that in the 2018 example the Equal.vi is in the example folder with the code, and in 2019 the Equal.vi has been moved to VI.lib - otherwise the code looks to be the same.  The Equals.vi code looks identical, and the calling VIM look identical.  I posted on the LabVIEW NI.com forum here: 
      https://forums.ni.com/t5/LabVIEW/LabVIEW-2019-Malleable-VIs-Shipping-Examples-Lesson-2b-Nested/m-p/3966044/highlight/false#M1129678
       
      I am trying to determine what may have broken or changed between the implementation in 2018 and 2019, visually the code looks the same.
    • By Voklaif
      Hello all,
      I am programming with LabVIEW for around 2 years and was recently stumbled upon LVOOP.
      I am required to write a communication protocol to work with a micro-controller, which later will be also used for ATP and debug purposes.
      I want to build the program "correctly" from the beginning so it will be maintainable and flexible to additions and changes.
      My natural way of building a program would have been a queued state machine, with several loops, each loop is in charge of a different module (one for GUI obviously), but as I stated in the beginning, I want to use LVOOP.
      Does anyone have a LVOOP project I can use as reference? I've searched online and found some nice examples, but they are small and teach you the basic stuff.
      For me it's important to see the how to use the project tree wisely, where to place the classes, see the managing loop and to learn as much as possible before I create one of my own.
      Thanks in advance,
      Voklaif
    • By GregFreeman
      I have an array of classes, let's call the object TestPass, of size 1 (but it is an array because it can scale out to multiple test passes). In this class, there is one other nested class which is not too complex, then various numeric and string fields to hold some private data. There is also an array of clusters. In this cluster there is a string, two XY pair clusters, and an integer. Not very confusing.
      This array of clusters gets fairly large, however, upwards of 80-100k elements. What I am finding is when I index the array of pass classes it is crazy slow. On the order of 30 ms. Doesn't seem like much, but we are indexing the array in our method to "Get Current Pass" which is used in various places throughout our code. This is adding potentially hours to our test time over the 80k devices we are testing. 
      So, I started digging. When I flatten the class to a string and get the length, it's 3 mb. But, when I run the function with the profiler is is allocating close to 20 mb of memory!
      My gut feel was that the string is causing the issues. So I removed the string from the cluster and the index time went to 0 ms. 
      Luckily we can normalize a bit and pull the strings out of the cluster since a lot of them are duplicates. But it makes our data model a bit uglier. 
      Has anyone seen these kind of performance issues before? I saw them in 2013 and 2017.
    • By ted Francis
      I am new to LVOOP and have jsut started writing my first LVOOP program which I have attached.
      I would appreciate greatly help with the question I have
      Thank you in advance 
      Ted
      This vi will perform two tasks 
      1.Generating Report data sheet for metrology 
      2. updating the scales in a MAX .nce file
      1. Metrology will input calibration information into the tables on the tabs
      Metrology will then click "Update Tables" then "Create Report ( create report section of code is not yet written
      Update Tables will write all information entered in the tabs to class varaibles and will also delete current Max informatiomn
      2. Metrology will click "Load NCE Scale"
      vi will prompt for nce file to load and then once file is selected, display existing scales for two channels (Current Motor 1 and 
      Current Motor 2)
      Metrology will then click "Update Scales"  the program will replace the existing scales with those entered in Step 5.14 and 5.15
      from the tables on the tab
      Question 1.  Steps 5.14 and 5.15 are needed by both classes ( Table Variable and MAX) - what is the best way to share this information
       
      CAT0000032 Class Version.zip
    • By shoneill
      I was browing through the actor framework discussions on the NI site yesterday and I came across a statement by AQ.
      Never inherit a concrete class from another concrete class
      I had to think about that for a second.  The more I think about it, the more I realise that all of the LVOOP software I have been writing more or less adheres to this idea.  But I had never seen it stated so succinctly before.  Now that may just be down to me being a bit slow and all, but in the muddy and murky world of "correct" in OOP-land, this seems to be a pretty good rult to hold on to.
      Are there others which can help wannabe plebs like me grasp the correct notions a bit better?  How about only ever calling concrete methods from within the owning class, never from without?  I'm learning for a long time now, but somehow, my expectations of LVOOP and the reality always seem a little disconnected.  AQs statement above helped crystallise out some things which, up to that point, had been a bit nebulous in my mind.  Well, I say I'm learning..... I'm certainly using my brain to investigate the subject, whether or not I'm actually LEARNING is a matter for discussion... The older I get, the less sure I am that I've actually properly grasped something.  The old grey cells just seem to get more sceptical with time.  Maybe that in itself is learning...
×
×
  • Create New...

Important Information

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