LabVIEW

cancel
Showing results for 
Search instead for 
Did you mean: 

Hardware Abstraction Layer using Object Oriented Programming: Adapting to different interfaces

Solved!
Go to solution

Hello everyone,

 

Working in a laboratory, we have various equipment of different brands/models that sometimes serve ( more or less) the same purpose. In order to reduce development time, I would like to create a Hardware Abstraction Layer that allows to work with all instruments of the same kind, with a high-level driver (abstraction). I think the best "architecture" to use is OOP,  but I face an issue.

 

As an example, let's take a parent class (abstraction layer) PowerSupply.lvclass. Children will be PS_ModelA.lvclass, PS_ModelB.lvclass and so on.

The parent class defines methods Initialize.vi, SetVoltage.vi and Close.vi

Now, the issue: If PS_ModelA uses USB interface (VISA/COM port) while PS_ModelB uses GPIB/IVI, the control to select the port to communicate with the instrument is not of the same type.

Because of this, I cannot use a "port" control (VISA or IVI) as an input of the parent method "Initialize", as there will be a type mismatch for one or the other child.

 

My current "hack" is to define a polymorphic VI Initialize, which calls static VIs "Initialize PS_A" or "Initialize PS_B". However, I lose the dynamic part of OOP and I can already see scalability issues coming with this method, so it just does not feel right.

 

What's a better solution? Thanks!

0 Kudos
Message 1 of 7
(277 Views)
Solution
Accepted by topic author seb_bd

So, the simple solution here is to have the Initialize be dynamic dispatch (you probably already got this far) and then use private data in the child class.

 

Provide a Write accessor for the necessary details (VISA resource for A, GPIB for B, etc) in each class and call them on a class constant before the start of your program. You can, if you like, create subVIs (either as class members, or outside of the child class, but perhaps within a library) that accept these inputs and produce a class object. (This is essentially similar to what you describe with the Initialize PS_A and Initialize PS_B VIs).

 

An alternative solution, if you will have lots of similar cases, is to define classes which carry out only communication, for example "VISA Communicator", "GPIB Communicator" and have them inherit from a "General Communicator" class with "Read", "Write", "Init" and so on. Then, you give a "Communicator" as an input to Initialize (for the PSs) and can retain some sort of generality there. However, you'd still need to set up the Communicator child, which will require the same type of code as you currently are concerned about regarding the PS_A, PS_B initialization.

 

I do this for some of my code, and it has an additional benefit - you can far more easily test your power supply code by giving it a "Fake Communicator", which stores for example an array of strings that are passed to "Write", or allows you to set a fixed string to provide when "Read" is called, to check the behaviour of your Power Supply in response to various conditions. Useful if you want to do some Unit Testing.

 

Remaining options include using either a Variant or a String (which in this case can I believe carry both necessary names to initialize your power supplies) but this loses you some type-safety, and prevents e.g. clicking a drop-down list control to check for available VISA ports. The variant could allow use of a VISA Resource Name control, then Variant to Data in the child class to a VISA resource, but it's a fairly murky solution that requires you know the data type required by a child, and you don't get that information from the connector pane in that case. Probably best to avoid this option, but you can consider it...


GCentral
Message 2 of 7
(245 Views)

Thanks cbutcher for your suggestions. I guess the solution with "Communicator" class is the best, as it brings additional benefits (fake communicator for unit testing, which is also something I'd like to add!).

 

I'm not a fan of the Variant/String solution, unless the interface information (COM port, GPIB address) would be really fixed in time and could be stored in some kind of INI file. Unfortunately, as things move around, this may not be always the case, so I will avoid this solution.

 

Thanks a lot!

0 Kudos
Message 3 of 7
(234 Views)

I know that you already marked this solved, but I thought I'd just post an image showing some section of power supply code I have (ignore the project name - it's in a library alongside some Arduino code that previously controlled the power supply before we bought the Serial module for the PS...)

psExample.png

Starting from the top left and moving anticlockwise, I have

  • Initialize the child Serial class (VISA Serial). Not exactly the same as what you're wanting to do, but pretty similar.
  • Set Main Power.vi (one of several commands I wanted to implement for my Power Supply). This knows about the command for this particular PS, but doesn't handle the specific message formatting
  • Send Message and Get Acknowledgement.vi: Formats the message (all commands call this subVI) and then tries to send the message and receive a response. Still a part of the PS class, but this could be modified if you had many power supplies to be more general (or dynamic dispatch, etc)
  • Send Message to Serial.vi: Member of the PS class, calls the nested object "Serial" with Write.vi then Read.vi. Has no knowledge at this level of the port etc (those were set in the Init VI).
  • Write.vi (in a separate library, via PPL): actually carries out the VISA Write. In the background you see the class hierarchy for this class - it has a sibling which contains an internal array of strings for reading and writing fixed responses. Uses its own private data to open the serial port in the initialize VI.

Perhaps this system is already obvious from my first reply, but since we're graphical programmers, a picture can sometimes be helpful too!


GCentral
Message 4 of 7
(216 Views)

Thanks for the additional details, it really helps to get a better understanding of the structure.

One more question: I see in the hierarchy that you have a class Serial, with two children Serial Visa and Serial Sim (The icon is really small, but I think that is Sim). Is this the "simulator/communicator" class you mentioned in the previous reply? 

If yes, then, how do you implement the "Fake communicator" for each instrument? Are they children of the Serial Sim class? I'd be interested to see that hierarchy as well.

Thanks!

0 Kudos
Message 5 of 7
(147 Views)

@seb_bd wrote:

One more question: I see in the hierarchy that you have a class Serial, with two children Serial Visa and Serial Sim (The icon is really small, but I think that is Sim). Is this the "simulator/communicator" class you mentioned in the previous reply? 


Yep - that's right. Sorry for the small icon!

 

Opening another project that uses this PPL (with some additional images from the Sim Serial class thrown in for an example default implementation), I have:

Serial Communication.lvlib_Simulated Serial.lvclass_Write_BD.pngSim-Write (of course, called Write.vi)

Serial Communication.lvlib_Simulated Serial.lvclass_Read_BD.pngSim-Read

These are the defaults - writing and reading from a string array. If you don't manually (i.e. in a test case) write the buffer, then you'll read back whatever you wrote.

The code in the Read will re-prepend the message if you read too few bytes. Termination characters aren't really handled - it's just assumed that the message you write always ends appropriately and ends up in the buffer. YMMV.

Simulated Cooler Serial.lvclass_Write_BD.pngSim-Cooler Write.vi

A more detailed Write.vi. This one checks a global variable (...) to determine if it should parse the message or not. It still calls the parent method to store in the buffer, but the parsing can be written in the test object (Sim Cooler) if you want. You can make this as detailed as you like, but of course, if your code here doesn't match the real hardware, you'll be testing against an invalid target...

Simulated Cooler Serial.lvclass_Parse Message_BD.pngSim's Parse Message

The Parse Message VI is responsible for the backend of the testing object. Here I've pasted sections of the device manual to help me check values, and used a global variable to hold the state of the device between Reads and Writes.

The VI also allows for checking the LRC (checksum) and if it fails, ignoring the message. It has a global variable that allows simulation of a failed checksum (e.g. due to noise, perhaps) so that the real Cooler code can see what it would do in that case.

 

Below is the setUp.vi (using JKI's VI Tester) for the test case:

SimCoolerTestCase.lvclass_setUp_BD.pngsetUp.vi

Here the Connect function for the Cooler requires a Serial Communicator device. A Simulated version is passed (this is the child of the SimSerial class) allowing manipulation of responses and checking of messages.

All of these are snippets so you should be able to open them in LabVIEW (one option appears to be left clicking the image, then choosing to download (it's one of the buttons on the bar at the bottom of the image, if the buttons are still missing. I think from left to right the buttons are "Kudos", "Report" and "Download"), then dragging from Explorer). Dragging directly from the browser used to work, but I think it hasn't for quite a while now after the forum was "upgraded".

Without the full project and the PPLs, you'll probably have a bunch of broken stuff, but you could browse through different cases etc.


GCentral
Message 6 of 7
(136 Views)

Thanks for all these details! This should really help me get started.

0 Kudos
Message 7 of 7
(127 Views)