The gadget::DeviceInterface<T> Helper Class

The concept of device interfaces in VR Juggler applications is something that often causes confusion for new users. Two object-oriented design patterns are combined by gadget::DeviceInterface<T>: smart pointers and proxies. Within this section, we aim to explain Gadgeteer device interfaces clearly and simply. We begin with a high-level description and then move right into using the class.

High-Level Description

Physical devices are never accessed directly by VR Juggler applications. Instead, the applications are granted access to the device through a proxy. A proxy is nothing more than an intermediary who forwards information between two parties. In this case, the two parties are a VR Juggler application and an input device. The application makes requests on the input device through the proxy.

The (template) class gadget::DeviceInterface<T> is designed to be a wrapper class around the proxies. Applications could use the proxy classes directly, but gadget::DeviceInterface<T> and its type-specific instances (gadget::PositionInterface, gadget::DigitalInterface, etc.) simplify use of the proxy object they contain. Thus, typical VR Juggler application objects will have one or more device interface member variables.

Note

The class gadget::DeviceInterface<T> is a templated class based on the proxy type it wraps. Example instantiations of gadget::DeviceInterface<T> are gadget::DeviceInterface<gadget::PositionProxy> and gadget::DeviceInterface<gadget::KeyboardProxy>. Typedefs such as gadget::PositionInterface (another name for gadget::DeviceInterface<gadget::PositionProxy>) make the code more readable.

In the application object, a device interface member variable is used as a smart pointer to the proxy. In C++, a smart pointer is not usually an actual object pointer. Instead, the class acting as a smart pointer overloads the dereference operator -> so that a special action can be taken when the “pointer” is dereferenced. The dereference operator is just another operator like the addition and subtraction operators, and overloading the deference operator allows some “magic” to occur behind the scenes. On the surface, the code looks exactly the same as a normal pointer dereference, and in most cases, people reading and writing the code can think of the smart pointer as a standard pointer. It may also be convenient to think of a smart pointer as a handle.

With that background, we can move on to explain how gadget::DeviceInterface<T> uses these concepts. First, know that gadget::DeviceInterface<T> is a base class for all other device interface classes such as digital interfaces (wand buttons), position interfaces (wands, a tracked user's head), etc. In user code, there will be instances of objects such as gadget::DigitalInterface, gadget::PositionInterface, gadget::KeyboardInterface, and the like. Once they are properly initialized, device interface objects (whatever their types may be) will act as smart pointers to the actual Gadgeteer device proxy objects they wrap.

All the instances of gadget::DeviceInterface<T> encapsulate a pointer to a Gadgeteer device proxy object. (Remember that these proxy objects act as an intermediary between the application and an input device.) The subclasses also overload the dereference operator -> which allows them to act as smart pointers. The dereference operator on a device interface object gives access to the object's hidden proxy pointer. With that access, the methods of the encapsulated proxy object can be invoked, usually to read data. The end result is that user applications get access to the proxy objects they need but through a simpler interface than using the proxies directly.

At this point, it is perfectly reasonable to wonder why Gadgeteer uses a concept that requires all sorts of documentation and explanation. The extra effort is worth it because it allows Gadgeteer to hide the actual type of the device being used. There is no need to know that some specific VR system uses a wireless mouse connected to a PC reading bytes from a PS/2 port that represent button presses. All that matters is knowing which buttons are pressed at a given instant. The class gadget::DigialInterface gives exactly that information, and it quietly hides the messiness of dealing with that crazy mouse, its ugly driver, and its overly complex protocol.

Using gadget::DeviceInterface<T>

As noted above, VR Juggler applications do not usually use gadget::DeviceInterface<T> directly. Instead, the typedefs for specific instantiations of gadget::DeviceInterface<T> mentioned above will be used. Within this section, we will refer to instantiations of gadget::DeviceInterface<T> as “device interfaces.” The high-level description has already made use of this convention.

Before using a device interface, some objects must be declared. Programmers must choose the type that is appropriate for the type of devices relevant to a given application. All device interface objects must be initialized in the application object's init() method. Each device interface type has a method called init(). This method takes a single string argument naming the proxy to which the interface will connect. Example names are “VJHead”, “VJWand”, “VJButton0”, and “VJAccelerate. These are all symbolic names specified in VR Juggler configuration files. This makes them easier to remember, and it also contributes to hiding the details about the physical device. With this system, no one needs to care how transformation information from the user's head is generated. Gadgeteer cares, but there is no need for it to tell anyone else. All developers care about is the head transformation matrix. An example of initializing a gadget::PositionInterface that connects with the user head proxy is:

gadget::PositionInterface head;

head.init("VJHead");

Remember that this is to be done in an application object's init() method. The actual object used would be a member variable of the application class. Note that here, the normal syntax for calling the method of a C++ object is used rather than using the dereference operator. Until it is initialized, the device interface object cannot act as a smart pointer.

Once device interface objects are all initialized and ready to use, it is time to start using them as smart pointers. This is best part! VR Juggler and Gadgeteer are already working hard in the background to update device proxies, and the application is free to access them. (It is usually best to reference them in the preFrame() method, but this may not necessarily be true for all proxies.) Continuing with our example of a gadget::PositionInterface to the user head proxy, the following code shows how to read the transformation matrix for the user's head:

gmtl::Matrix44f head_mat;

head_mat = head->getData();

But wait, that was easy! Believe it or not, the code really is that simple. Simply use the overloaded dereference operator to get access to the position proxy object hidden in gadget::PositionInterface to read data from the proxy. Of course, we have not explained the getData() method at all yet. That comes from the position proxy class, and that is documented elsewhere.

The Gory Details

What is truly amazing about Gadgeteer device interfaces is, despite their seeming complexity, there is really nothing to them. Trying to trace through the source code is a little tricky, but conceptually, it is all about pointers. Keep in mind that all this documentation was written using nothing more than the Gadgeteer header files as a reference.

As mentioned, the class gadget::DeviceInterface<T> is a base class for all the specific types of device interfaces such as positional interfaces, digital interfaces, and analog interfaces. This class maintains the name of the proxy and the proxy index, it provides the all-important init() method, and it overloads the dereference operator. C++ templates then handle the different type instantiations for the different Gadgeteer proxy types.

Instantiations of gadget::DeviceInterface<T> are used to provide the wrapper to a specific type of proxy. They each contain a pointer to a proxy object of the same conceptual type (positional, digital, and so on). Regardless of the specific instantiations of gadget::DeviceInterface<T>, they all return a pointer to their contained proxy so that user code can get the current data from the proxy.

The beauty of it all is that the proxy object being pointed to by the device interface can be changed without affecting the execution of the user application. In other words, the proxies can be changed at run time to point to different physical devices. All the while, the user code is still using the smart pointer interface and getting data of some sort. This flexibility is one of the most important features of Gadgeteer, and it is important to understand.