Friday, December 26, 2008

LabVIEW API Design: A Quick Case Study

Happy holidays, everybody. It's Friday, December 26, and I'm one of a handful of NI Austin employees not taking a vacation today. I got a really good parking spot. :)

A couple of recent blog comments have asked for more thoughts on LabVIEW API design. As it happens, I've been working with a device lately with a LabVIEW instrument driver that I'm not entirely happy with. So I will use this driver as an example to highlight a few points on API design.

To be fair, this driver is actually pretty good. It's a driver for a Hokuyo laser rangefinder, written by one of our interns. I think this is the first driver that we've written for a laser rangefinder, also called a LIDAR. The first driver for a particular type of instrument is the hard. (That's why our instrument driver development tools encourage you to start with a driver for a similar instrument if you can.)

I'm revisiting this driver for a couple of reasons... 1) I am trying to extend the driver to support a newer, faster model in the same LIDAR family, and 2) I'm trying to get data from my current LIDAR faster.

If the first driver of a particular type is hard, the second is harder. Once I have two similar, but different, instruments, I have to resolve the differences in a way that makes sense for both devices.

Okay, let's dive into the API...

Not Enough Choices

When Initialize is called, instrument drivers generally query the instrument they are talking to, in order to ensure that it's really the right kind of instrument. Most Initialize functions let you turn this check off, either for performance reasons, or to let you try to use this driver for a different instrument with a compatible set of commands.

Here's the help window for the Hokuyo URG-04LX instrument driver Initialize VI...

There's something missing that just about every instrument driver Initalize function includes--the "ID Query" Boolean input. ("ID" as in "Identification".)

This latter example is what I was trying to do. I had a new model of Hokuyo URG LIDAR that I wanted to talk to. As expected, the instrument driver failed on the Initialize, since the identification string returned from the LIDAR didn't match an expected value.

Why wasn't the ID Query Boolean included on the Initialize VI? I don't know. I can imagine that the author didn't feel confident that the code worked with anything more than the specific hardware he or she tested. But I still would have included the Boolean, to make it easier for end users to try anyway.

Too Many Choices

There's another input on Initialize labelled "Protocol"...

This LIDAR has two slightly different ASCII command sets that it can support--version 1.1 and version 2.0.

How does a user know which to choose? Let's see what the help says for this input...
Hmm. Sounds like 2.0 is better. Why not default to that? Why give the user a choice at all?

I can think of two reasons. First and most importantly, early units of this model of LIDAR had firmware that don't support SCIP 2.0. So, if we assume that SCIP 2.0 is available, this driver wouldn't work with those units. Second, the SCIP 2.0 protocol uses a few more bytes per command than the SCIP 1.1 protocol, and thus might be slightly slower in certain uses.

But I think we should not expose this protocol choice to users. Here's my reasoning... 1) The only users of this LIDAR that we know are using it with LabVIEW have the latest firmware, thus they wouldn't be affected by depending on 2.0. 2) The firmware is field-upgradable, so the user could update the LIDAR to be compatible with the driver. 3) The newer model of LIDAR I'm using only supports the 2.0 protocol, so it's confusing to have the option for 1.1 when it won't work on some devices. 4) The extra bytes for the commands don't seem to have a measurable impact; the measurement time of the instrument appears to be the gating factor on performance. 5) Dropping support for the old command set lets us remove almost half the code in the driver. Most VIs have a case structure for the 1.1 and 2.0 cases. We can now remove those case structures and just leave the 2.0 code inline.

Another couple of minor nit-picks as long as I'm showing you this front panel for Initialize... I would have hidden the digital displays for baud rate and protocol. The menu ring's label says 19200, so I don't need to see that the data value is also 19200. And the protocol data value of "1" is used only internally and doesn't need to be exposed to the user.

Giving Users Choices

I seem to have come up with two contradictory examples about giving users choices. For ID Query, I'm complaining that the API designer didn't give me the choice to turn it off. For the protocol, I'm complaining that the API designer gave me a choice I didn't want to make.

I think it all comes down to a judgement call. What are the chances that someone is going to want to use one of Hokuyo's other models of LIDARs? High, I think. What are the chances that somebody really must use the 1.1 communications protocol. Low, I think.

If the API designer really wanted to support both communications protocols, I would insist that he or she document how to choose between them. Which leads to a related issue in the driver...

Documenting Expected Usage

One of these LIDARs can use serial to communicate. (Both can use USB.) With serial, one of the parameters you have to configure is the baud rate--the communication speed of the device.

Most serial devices use a baud rate configured through either DIP switches, or through a front panel menu that stores the rate in non-volatile storage.

The Hokuyo URG-04LX uses a command sent through the serial port to configure the baud rate for the serial port. This is a classic "chicken and egg" problem. I have to use the serial port to configure the serial port.

When this device powers on, it defaults to 19,200 baud. If I want to transfer data at 115,200 baud, I have to connect at 19,200, then send a command to tell the LIDAR to communicate at 115,200 baud, then change my own connection to 115,200 before continuing the conversation.

Now suppose I finish my program and want to run it again. What should the initial baud rate be? In this case, I have to know that when I reconnect to the device, that it is already configured to 115,200 baud. (Or I have to reset or power-cycle the LIDAR in between programs, to revert to 19,200 baud.)

This can get pretty confusing, which is why my most devices configure their baud rates through physical switches.

Back to the API, there are two places where baud rate can be configured. First is on Initialize, as shown above. As I learned by studying the block diagram of Initialize, this baud rate is used for the initial communications to the instrument.

The second place you configure baud rate is with a configuration VI...

As I learned by studying the block diagram of this VI, it sends a command to the LIDAR to configure its serial baud rate. It does not change the baud rate of the host side.

So I think the expected usage is that you would call Initialize to establish the initial connection, then Configure Serial Baud Rate to increase the LIDAR link speed, and then use a VISA property node to change the host link speed. And I think you have to put in a short delay after that to get things to settle down after the baud rate change on the LIDAR.

Suggestion #1: Document this! Don't make every user have to figure this out on their own. If nothing else, make an example. It may not be a common use case, but if it's non-intuitive, an example or a little bit of documentation can be very helpful.

Suggestion #2: If two steps should always happen together, combine them. I modified the Configure Serial Baud Rate VI to also change the local baud rate and add the delay. That way, the end user doesn't have to remember to do this. I also thought about moving all of this into Initialize (which would take two baud rate inputs--initial, and desired). I voted against this, since there might be a use case where you just want to change the baud rate after you've already initialized.

Making Changes

So what's going to happen to this instrument driver? For myself, I've hacked together an instrument driver that does what I want for my two LIDARs. Now, I'll be asking someone to go back in and make the edits the "right way", now that we know more about what the "right way' is. We'll then feed this update back onto the Instrument Driver Network for others to download and use.


Yair said...

This is indeed an important topic. Here are some issues that come to mind:

1. Which operations do you include in the API? How much power do you give the user and what kind of errors do you return if the user made an invalid choice?

2. What happens when you have too many choices? I hate VIs with more than the 4-2-2-4 pattern, but sometimes you just need a lot of inputs? What would you do? Use a larger con pane? Use another VI for setting the config? Use a cluster, which can become cumbersome in the diagram? Unfortunately, I can't say I have a very good solution for this.

3. Often, figuring out the common parts isn't impossibly hard, but figuring out how to incorporate the parts which are sort of similar but not exactly can be the difficult part.

4. Inheritance would be great. Is NI planning on using LVOOP for drivers anytime soon? It's been out for more than two years and I see that more and more parts of LabVIEW are written with it, so I assume drivers are forthcoming, but I'm wondering about a timeline.

Brian Powell said...

Thanks, Yair. Those are all excellent topics, and I'll create a full post to discuss them.

As I think you've figured out, there aren't always clean, simple, black and white answers to these questions. So sometimes, it's just a gut feel based on our experience.